财务台账:支持自定义账户总表列宽 #433

Merged
shanshi merged 2 commits from feature/shanshi-narrow-ledger-plan-keep-columns into main 2026-10-21 14:57:36 +08:00
2 changed files with 156 additions and 10 deletions

View File

@ -102,8 +102,19 @@
.ledger-accounts-table thead th,
.ledger-accounts-table thead th.num { text-align:center; }
.ledger-accounts-table { min-width:max-content; }
.ledger-accounts-table tbody td { overflow:hidden; text-overflow:ellipsis; }
.ledger-accounts-table .is-column-hidden { display:none !important; }
.ledger-keep-column { width:6.5rem; min-width:6.5rem; text-align:center; }
.ledger-column-resizer { position:absolute; top:0; right:0; z-index:4; width:.45rem; height:100%; cursor:col-resize; touch-action:none; }
.ledger-column-resizer::after { content:""; position:absolute; top:20%; bottom:20%; left:50%; width:1px; background:transparent; transition:background .15s ease; }
.ledger-column-resizer:hover::after,
.ledger-column-resizer.is-active::after { background:var(--vip-content-accent-dark, #856632); }
.ledger-column-resizer:focus { outline:none; }
.ledger-column-resizer:focus-visible::after { width:2px; background:var(--vip-content-accent-dark, #856632); }
body.is-resizing-ledger-column { cursor:col-resize; user-select:none; }
.ledger-table .ledger-plan-column,
.ledger-table .ledger-keep-column { padding-right:.2rem; padding-left:.2rem; text-align:center; }
.ledger-plan-column { width:5.6rem; min-width:5.6rem; }
.ledger-keep-column { width:5.4rem; min-width:5.4rem; }
.ledger-table td.wrap { white-space:normal; min-width:14rem; max-width:22rem; line-height:1.5; }
.ledger-table .muted { color:var(--vip-content-muted, #595959); }
.ledger-table .danger { color:var(--vip-danger, #B80000); }
@ -132,11 +143,11 @@
.ledger-map-input.is-saving { border-color:var(--vip-content-accent, #B9954F); background:#fffdf6; }
.ledger-map-input.is-error { border-color:var(--vip-danger, #B80000); background:#fff5f5; }
.ledger-table .ledger-status-column { width:4.6rem; min-width:4.6rem; padding-right:.2rem; padding-left:.2rem; text-align:center; }
.ledger-inline-select { display:block; height:1.75rem; margin:0 auto; padding:0 .1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-size:.68rem; font-weight:600; text-align:center; text-align-last:center; cursor:pointer; }
.ledger-status-select { width:4.15rem; min-width:4.15rem; }
.ledger-type-select { width:5.2rem; min-width:5.2rem; }
.ledger-plan-select { width:6.5rem; min-width:6.5rem; }
.ledger-keep-select { width:4.8rem; min-width:4.8rem; }
.ledger-inline-select { display:block; width:100%; max-width:100%; min-width:0; height:1.75rem; margin:0 auto; padding:0 .1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-size:.68rem; font-weight:600; text-align:center; text-align-last:center; cursor:pointer; }
.ledger-status-select { max-width:4.15rem; }
.ledger-type-select { max-width:5.2rem; }
.ledger-plan-select { max-width:5rem; }
.ledger-keep-select { max-width:3.9rem; }
.ledger-inline-select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
.ledger-inline-select[data-status="normal"] { border-color:rgba(83,121,62,.38); background:#f3f7ee; color:var(--vip-success, #53793E); }
.ledger-inline-select[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.34); background:#fff5f5; color:var(--vip-danger, #B80000); }
@ -203,7 +214,7 @@
<div class="ledger-column-picker" data-ledger-column-picker data-column-storage-key="caiwu.financial-ledger.accounts.preferences.v1.{{ auth()->id() }}.{{ company_id() }}">
<button class="ledger-btn" type="button" data-ledger-column-toggle aria-expanded="false"><span class="material-icons-outlined" aria-hidden="true">view_column</span>列设置</button>
<div class="ledger-column-panel" data-ledger-column-panel role="group" aria-label="选择账户总表展示列" hidden>
<p class="ledger-column-help">拖动字段可调整列顺序;按住 Alt 并使用 ↑ / ↓ 也可移动。</p>
<p class="ledger-column-help">拖动字段可调整列顺序;拖动表头右侧边界可调整列宽,双击边界恢复默认。</p>
<div class="ledger-column-options" data-ledger-column-options></div>
<div class="ledger-column-actions"><span data-ledger-column-count></span><div class="ledger-column-action-group"><button type="button" data-ledger-column-all>显示全部</button><button type="button" data-ledger-column-reset>恢复默认</button></div></div>
</div>
@ -219,7 +230,7 @@
</div>
<div class="ledger-table-wrap ledger-accounts-table-wrap">
<table class="ledger-table ledger-accounts-table">
<thead><tr><th>公司名称</th><th>账户简称</th><th>账户类型</th><th class="ledger-status-column">账户状态</th><th>地区</th><th>账号</th><th>开户行</th><th>开户城市/网点</th><th>公司规划</th><th class="ledger-keep-column">账户是否保留</th><th>后4位</th><th>单笔限额</th><th>日限额</th><th>月限额</th><th>年限额</th><th class="num">可用金额</th><th class="num">不可用金额</th><th class="num">每日可出</th><th class="num">预计出清天数</th><th>提额目标</th><th>提额状态</th><th>提额动作</th><th>已做动作</th><th>下一步</th><th>备注</th><th>责任人</th></tr></thead>
<thead><tr><th>公司名称</th><th>账户简称</th><th>账户类型</th><th class="ledger-status-column">账户状态</th><th>地区</th><th>账号</th><th>开户行</th><th>开户城市/网点</th><th class="ledger-plan-column">公司规划</th><th class="ledger-keep-column">账户是否保留</th><th>后4位</th><th>单笔限额</th><th>日限额</th><th>月限额</th><th>年限额</th><th class="num">可用金额</th><th class="num">不可用金额</th><th class="num">每日可出</th><th class="num">预计出清天数</th><th>提额目标</th><th>提额状态</th><th>提额动作</th><th>已做动作</th><th>下一步</th><th>备注</th><th>责任人</th></tr></thead>
<tbody>
@forelse ($ledgerAccounts as $row)
<tr>
@ -251,7 +262,7 @@
@endif
</td>
<td>{{ $row->region }}</td><td>{{ $row->account_no }}</td><td>{{ $row->bank_name }}</td><td>{{ $row->bank_city_branch }}</td>
<td>
<td class="ledger-plan-column">
<span class="ledger-account-readonly" data-account-edit-display data-account-plan-display>{{ $row->company_plan ?: '未填写' }}</span>
@if ($canEditAccountStatus)
<select class="ledger-inline-select ledger-plan-select ledger-account-editor" data-account-editor data-account-plan data-account-short="{{ $row->account_short }}" data-url="{{ route('business.financial-ledger.accounts.plan') }}" aria-label="账户 {{ $row->account_short }} 公司规划" disabled>
@ -490,6 +501,57 @@
});
let visibleAccountColumns = new Set(initialAccountColumnKeys);
let accountColumnOrder = [...initialAccountColumnKeys];
const initialAccountColumnWidths = new Map(
initialAccountColumnHeaders.map((header, index) => [
initialAccountColumnKeys[index],
Math.max(48, Math.round(header.getBoundingClientRect().width * 10) / 10),
]),
);
let accountColumnWidths = new Map(initialAccountColumnWidths);
const clampAccountColumnWidth = (value) => Math.min(720, Math.max(48, Math.round(Number(value) || 0)));
const accountColumnGroup = () => {
const table = accountPanel?.querySelector('.ledger-accounts-table');
if (!table) return null;
let group = table.querySelector('colgroup[data-ledger-column-group]');
if (!group) {
group = document.createElement('colgroup');
group.dataset.ledgerColumnGroup = 'true';
table.insertBefore(group, table.firstChild);
}
initialAccountColumnKeys.forEach((key) => {
if (group.querySelector('col[data-ledger-column-key="' + key + '"]')) return;
const column = document.createElement('col');
column.dataset.ledgerColumnKey = key;
group.appendChild(column);
});
return group;
};
const applyAccountColumnWidths = () => {
const table = accountPanel?.querySelector('.ledger-accounts-table');
const group = accountColumnGroup();
if (!table || !group) return;
const visible = new Set(visibleAccountColumns);
let totalWidth = 0;
accountColumnOrder.forEach((key) => {
const column = group.querySelector('col[data-ledger-column-key="' + key + '"]');
if (!column) return;
const width = visible.has(key)
? clampAccountColumnWidth(accountColumnWidths.get(key) ?? initialAccountColumnWidths.get(key))
: 0;
column.style.width = width + 'px';
totalWidth += width;
const handle = table.querySelector(
'thead th[data-ledger-column-key="' + key + '"] [data-ledger-column-resizer]',
);
handle?.setAttribute('aria-valuenow', String(width));
});
table.style.tableLayout = 'fixed';
table.style.width = Math.max(1, totalWidth) + 'px';
table.style.minWidth = Math.max(1, totalWidth) + 'px';
};
const normaliseColumnKeys = (values, fallback) => {
const valid = new Set(initialAccountColumnKeys);
@ -508,6 +570,16 @@
if (!table) return;
const headerRow = table.tHead?.rows?.[0];
if (!headerRow) return;
const group = accountColumnGroup();
if (group) {
const columnsByKey = new Map(
[...group.children].map((column) => [column.dataset.ledgerColumnKey, column]),
);
accountColumnOrder.forEach((key) => {
const column = columnsByKey.get(key);
if (column) group.appendChild(column);
});
}
const headersByKey = new Map(
[...headerRow.children].map((header) => [header.dataset.ledgerColumnKey, header]),
);
@ -540,6 +612,7 @@
if (accountColumnCount) {
accountColumnCount.textContent = '显示 ' + visible.size + ' / ' + initialAccountColumnKeys.length + ' 列';
}
applyAccountColumnWidths();
};
const saveAccountColumnPreferences = () => {
@ -547,6 +620,7 @@
localStorage.setItem(accountColumnKey, JSON.stringify({
visible: [...visibleAccountColumns],
order: accountColumnOrder,
widths: Object.fromEntries(accountColumnWidths),
}));
} catch (_) {}
};
@ -560,11 +634,81 @@
const savedVisible = Array.isArray(saved?.visible) ? saved.visible : initialAccountColumnKeys;
visibleAccountColumns = new Set(normaliseColumnKeys(savedVisible, initialAccountColumnKeys));
accountColumnOrder = normaliseColumnKeys(saved?.order, initialAccountColumnKeys);
accountColumnWidths = new Map(initialAccountColumnWidths);
initialAccountColumnKeys.forEach((key) => {
if (saved?.widths && saved.widths[key] !== undefined) {
accountColumnWidths.set(key, clampAccountColumnWidth(saved.widths[key]));
}
});
} catch (_) {
visibleAccountColumns = new Set(initialAccountColumnKeys);
accountColumnOrder = [...initialAccountColumnKeys];
accountColumnWidths = new Map(initialAccountColumnWidths);
}
initialAccountColumnHeaders.forEach((header, index) => {
const key = initialAccountColumnKeys[index];
if (header.querySelector('[data-ledger-column-resizer]')) return;
const label = header.textContent.trim() || '未命名字段';
const resizer = document.createElement('span');
resizer.className = 'ledger-column-resizer';
resizer.dataset.ledgerColumnResizer = 'true';
resizer.tabIndex = 0;
resizer.setAttribute('role', 'separator');
resizer.setAttribute('aria-orientation', 'vertical');
resizer.setAttribute('aria-label', '调整“' + label + '”列宽');
resizer.setAttribute('aria-valuemin', '48');
resizer.setAttribute('aria-valuemax', '720');
resizer.title = '拖动调整列宽,双击恢复默认';
const setWidth = (value, persist = false) => {
accountColumnWidths.set(key, clampAccountColumnWidth(value));
applyAccountColumnWidths();
if (persist) saveAccountColumnPreferences();
};
resizer.addEventListener('pointerdown', (event) => {
if (event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
const startX = event.clientX;
const startWidth = Number(accountColumnWidths.get(key))
|| header.getBoundingClientRect().width;
resizer.classList.add('is-active');
document.body.classList.add('is-resizing-ledger-column');
const move = (moveEvent) => {
setWidth(startWidth + moveEvent.clientX - startX);
};
const finish = () => {
document.removeEventListener('pointermove', move);
document.removeEventListener('pointerup', finish);
document.removeEventListener('pointercancel', finish);
resizer.classList.remove('is-active');
document.body.classList.remove('is-resizing-ledger-column');
saveAccountColumnPreferences();
};
document.addEventListener('pointermove', move);
document.addEventListener('pointerup', finish);
document.addEventListener('pointercancel', finish);
});
resizer.addEventListener('keydown', (event) => {
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
event.preventDefault();
const currentWidth = Number(accountColumnWidths.get(key))
|| header.getBoundingClientRect().width;
setWidth(currentWidth + (event.key === 'ArrowLeft' ? -1 : 1) * (event.shiftKey ? 24 : 8), true);
});
resizer.addEventListener('dblclick', (event) => {
event.preventDefault();
event.stopPropagation();
setWidth(initialAccountColumnWidths.get(key), true);
notify(label + '列宽已恢复默认');
});
resizer.addEventListener('click', (event) => event.stopPropagation());
header.appendChild(resizer);
});
accountColumnOrder.forEach((key) => {
const header = headersByKey.get(key);
if (!header) return;
@ -619,6 +763,7 @@
accountColumnPicker.querySelector('[data-ledger-column-reset]')?.addEventListener('click', () => {
visibleAccountColumns = new Set(initialAccountColumnKeys);
accountColumnOrder = [...initialAccountColumnKeys];
accountColumnWidths = new Map(initialAccountColumnWidths);
accountColumnOptions.querySelectorAll('[data-ledger-column-check]').forEach((checkbox) => {
checkbox.checked = true;
});

View File

@ -88,7 +88,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
->assertSee('data-account-template-download', false)
->assertSee('data-account-template-download-status', false)
->assertSee('<th>账户简称</th><th>账户类型</th><th class="ledger-status-column">账户状态</th>', false)
->assertSee('<th>公司规划</th><th class="ledger-keep-column">账户是否保留</th><th>后4位</th>', false)
->assertSee('<th class="ledger-plan-column">公司规划</th><th class="ledger-keep-column">账户是否保留</th><th>后4位</th>', false)
->assertSee('data-account-short="测试账户" data-account-should-keep="是"', false)
->assertSee('网商银行')
->assertSee('ledger-accounts-table-wrap', false)
@ -103,6 +103,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
->assertSee('data-account-edit-display', false)
->assertSee('列设置')
->assertSee('拖动字段可调整列顺序')
->assertSee('拖动表头右侧边界可调整列宽')
->assertSee('恢复默认')
->assertSee('可用金额合计')
->assertSee('不可用金额合计')