冻结待办类型列并固定横向滚动条 #577

Merged
yueyang merged 1 commits from feature/yueyang-systemization-todo-frozen-scroll into main 2026-10-26 10:40:31 +08:00
2 changed files with 69 additions and 6 deletions

View File

@ -36,16 +36,19 @@
.sys-todo-filter__multi-menu input[type="checkbox"] { flex:0 0 auto; width:1rem; height:1rem; padding:0; accent-color:var(--sys-gold); }
.sys-todo-filter__multi-empty { padding:.65rem; color:#777; font-size:.75rem; text-align:center; }
.sys-todo-filter__actions { display:flex; flex:0 0 auto; gap:.45rem; }
.sys-todo-table-wrap { overflow-x:auto; overflow-y:visible; border:1px solid var(--sys-border); border-radius:.65rem; background:#fff; scrollbar-gutter:stable; }
.sys-todo-table-wrap { overflow-x:auto; overflow-y:visible; border:1px solid var(--sys-border); border-radius:.65rem; background:#fff; scrollbar-width:none; }
.sys-todo-table-wrap::-webkit-scrollbar { height:0; }
.sys-todo-table { width:100%; min-width:74rem; border-collapse:separate; border-spacing:0; font-size:.75rem; }
.sys-todo-table th { position:sticky; top:0; z-index:4; padding:.55rem .48rem; border-bottom:1px solid var(--sys-border); background:#f3ece0; color:#3d3a35; text-align:left; white-space:nowrap; }
.sys-todo-table td { padding:.65rem .55rem; border-bottom:1px solid #eee7da; background:#fff; color:#2f2a23; line-height:1.55; vertical-align:top; }
.sys-todo-table th:first-child { left:0; z-index:8; width:5.4rem; min-width:5.4rem; text-align:center; box-shadow:1px 0 0 #e5ddcf; }
.sys-todo-table td { padding:.72rem .6rem; border-bottom:1px solid #eee7da; background:#fff; color:#2f2a23; line-height:1.6; vertical-align:top; }
.sys-todo-table tbody tr { cursor:pointer; }
.sys-todo-table tbody tr:hover td { background:#fdfbf6; }
.sys-todo-table tbody tr:focus-visible { outline:2px solid var(--sys-gold); outline-offset:-2px; }
.sys-todo-table__index-cell { width:3.6rem; min-width:3.6rem; text-align:center; }
.sys-todo-table__index { color:#6d5630; font-weight:600; font-variant-numeric:tabular-nums; white-space:nowrap; }
.sys-todo-table__group { padding-top:.8rem; padding-bottom:.8rem; background:#fcfaf5; font-weight:600; line-height:1.65; vertical-align:middle; }
.sys-todo-table__group { padding-top:.9rem; padding-bottom:.9rem; background:#fcfaf5; font-weight:600; line-height:1.7; vertical-align:middle; }
.sys-todo-table__type { position:sticky; left:0; z-index:3; width:5.4rem; min-width:5.4rem; text-align:center; white-space:nowrap; box-shadow:1px 0 0 #e5ddcf; }
.sys-todo-table__section { min-width:11rem; max-width:17rem; }
.sys-todo-table__title, .sys-todo-table__progress, .sys-todo-table__notes { line-height:1.45; white-space:pre-line; overflow-wrap:anywhere; }
.sys-todo-table__title { min-width:17rem; max-width:24rem; font-weight:600; }
@ -61,7 +64,13 @@
.sys-todo-shots { display:flex; flex-wrap:wrap; gap:.3rem; max-width:13rem; }
.sys-todo-shots button { display:block; width:4.25rem; height:3.2rem; padding:0; border:1px solid #ddd4c4; border-radius:.35rem; overflow:hidden; background:#faf8f3; cursor:zoom-in; }
.sys-todo-shots img { width:100%; height:100%; object-fit:cover; }
.sys-todo-table__add { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.65rem; margin-top:.6rem; padding:.55rem .7rem; border:1px dashed #d8cdb8; border-radius:.55rem; background:#fcfaf5; color:#666; font-size:.72rem; }
.sys-todo-table__add { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.65rem; margin-top:1rem; padding:.55rem .7rem; border:1px dashed #d8cdb8; border-radius:.55rem; background:#fcfaf5; color:#666; font-size:.72rem; }
.sys-todo-scrollrail { position:fixed; bottom:.85rem; z-index:35; height:1.15rem; padding:2px; border:1px solid #d8cdb8; border-radius:999px; background:rgba(243,236,224,.96); box-shadow:0 5px 18px rgba(23,23,23,.16); opacity:0; pointer-events:none; overflow-x:scroll; overflow-y:hidden; transition:opacity 150ms ease; }
.sys-todo-scrollrail.is-visible { opacity:1; pointer-events:auto; }
.sys-todo-scrollrail__spacer { height:1px; }
.sys-todo-scrollrail::-webkit-scrollbar { height:.7rem; }
.sys-todo-scrollrail::-webkit-scrollbar-track { border-radius:999px; background:#eee7d9; }
.sys-todo-scrollrail::-webkit-scrollbar-thumb { border-radius:999px; background:var(--sys-gold); }
.sys-todo-table__add button { display:inline-flex; align-items:center; gap:.25rem; min-height:2rem; padding:0 .7rem; border:1px solid var(--sys-border); border-radius:.4rem; background:#fff; color:#51483c; font:inherit; font-size:.76rem; cursor:pointer; }
.sys-todo-table__add button:hover { border-color:#c7b687; background:var(--sys-gold-soft); }
.sys-todo-table__add .material-icons-outlined { font-size:1rem; }
@ -302,7 +311,7 @@
$lastSectionLabel = $items->isNotEmpty() ? ($sectionLabels[$items->count() - 1] ?? '') : '';
@endphp
<div class="sys-todo-table-wrap" tabindex="0" aria-label="待办列表,可滚动查看全部内容">
<div class="sys-todo-table-wrap" tabindex="0" aria-label="待办列表,可横向滚动查看全部内容" data-sys-todo-table-wrap>
<table class="sys-todo-table" aria-label="财务系统化待办清单">
<thead>
<tr>
@ -350,7 +359,7 @@
@endphp
<tr tabindex="0" data-sys-todo-row-edit="{{ $todoPayloadEncoded }}" title="点击整行直接编辑">
@if ($categorySpan !== 0)
<td class="sys-todo-table__group" @if ($categorySpan > 1) rowspan="{{ $categorySpan }}" @endif>{{ $item->category ?: '—' }}</td>
<td class="sys-todo-table__group sys-todo-table__type" @if ($categorySpan > 1) rowspan="{{ $categorySpan }}" @endif>{{ $item->category ?: '—' }}</td>
@endif
@if ($sectionSpan !== 0)
<td class="sys-todo-table__group sys-todo-table__section" @if ($sectionSpan > 1) rowspan="{{ $sectionSpan }}" @endif>
@ -396,6 +405,9 @@
</tbody>
</table>
</div>
<div class="sys-todo-scrollrail" tabindex="0" aria-label="表格横向滚动条" data-sys-todo-scrollrail>
<div class="sys-todo-scrollrail__spacer" data-sys-todo-scrollrail-spacer></div>
</div>
<div class="sys-todo-table__add">
<span>共 {{ number_format($items->count()) }} 条,已全部显示。点击任意事项行可直接编辑。</span>
@ -536,6 +548,10 @@
var ownerSummary = ownerFilter.querySelector('[data-sys-todo-owner-summary]');
var assigneeFilter = document.querySelector('[data-sys-todo-assignee-filter]');
var assigneeSummary = assigneeFilter.querySelector('[data-sys-todo-assignee-summary]');
var tableWrap = document.querySelector('[data-sys-todo-table-wrap]');
var scrollRail = document.querySelector('[data-sys-todo-scrollrail]');
var scrollRailSpacer = document.querySelector('[data-sys-todo-scrollrail-spacer]');
var scrollRailFrame = null;
function decodePayload(value) {
var binary = atob(value);
@ -549,6 +565,33 @@
if (field) field.value = value || '';
}
function syncScrollRail() {
var rect = tableWrap.getBoundingClientRect();
var left = Math.max(8, rect.left);
var right = Math.min(window.innerWidth - 8, rect.right);
var hasOverflow = tableWrap.scrollWidth > tableWrap.clientWidth + 1;
var isVisible = hasOverflow
&& rect.top < window.innerHeight - 32
&& rect.bottom > 0;
scrollRail.style.left = left + 'px';
scrollRail.style.width = Math.max(0, right - left) + 'px';
scrollRailSpacer.style.width = tableWrap.scrollWidth + 'px';
scrollRail.classList.toggle('is-visible', isVisible);
if (Math.abs(scrollRail.scrollLeft - tableWrap.scrollLeft) > 1) {
scrollRail.scrollLeft = tableWrap.scrollLeft;
}
}
function scheduleScrollRailSync() {
if (scrollRailFrame !== null) return;
scrollRailFrame = window.requestAnimationFrame(function () {
scrollRailFrame = null;
syncScrollRail();
});
}
function selectedFilesLabel() {
return Array.prototype.slice.call(screenshotInput.files || [])
.map(function (file) { return file.name; })
@ -839,6 +882,23 @@
uploadField.classList.remove('is-dragging');
appendScreenshotFiles(event.dataTransfer?.files || []);
});
tableWrap.addEventListener('scroll', function () {
if (Math.abs(scrollRail.scrollLeft - tableWrap.scrollLeft) > 1) {
scrollRail.scrollLeft = tableWrap.scrollLeft;
}
scheduleScrollRailSync();
}, { passive: true });
scrollRail.addEventListener('scroll', function () {
if (Math.abs(tableWrap.scrollLeft - scrollRail.scrollLeft) > 1) {
tableWrap.scrollLeft = scrollRail.scrollLeft;
}
}, { passive: true });
window.addEventListener('scroll', scheduleScrollRailSync, { passive: true });
window.addEventListener('resize', scheduleScrollRailSync);
if (typeof ResizeObserver === 'function') {
new ResizeObserver(scheduleScrollRailSync).observe(tableWrap);
}
scheduleScrollRailSync();
screenshotInput.addEventListener('change', function () {
if (screenshotInput.files.length) {
uploadNote.textContent = '已选择 ' + screenshotInput.files.length + ' 张:' + selectedFilesLabel() + '。保存后上传。';

View File

@ -43,6 +43,8 @@ class SystemizationTodosTest extends FeatureTestCase
->assertSee('data-sys-todo-choose-files', false)
->assertSee('data-sys-todo-open-delete', false)
->assertSee('id="sysTodoDeleteConfirm"', false)
->assertSee('data-sys-todo-table-wrap', false)
->assertSee('data-sys-todo-scrollrail', false)
->assertDontSee('<th scope="col">操作</th>', false)
->assertDontSeeText('下一页')
->assertSee('id="sysTodoImageViewer"', false);
@ -186,6 +188,7 @@ class SystemizationTodosTest extends FeatureTestCase
->assertOk()
->assertSee('rowspan="2"', false)
->assertSee('<th scope="col">内容</th><th scope="col">序号</th><th scope="col">待办事项</th>', false)
->assertSee('sys-todo-table__type', false)
->assertSee('title="当前类型内序号">1</span>', false)
->assertSee('title="当前类型内序号">2</span>', false)
->assertDontSee('1、平台收入核对')