feat: 精简供应商应付表分页并冻结前列

This commit is contained in:
苏木 2026-11-05 13:16:23 +08:00
parent 850db1a0b4
commit df90a5032b
3 changed files with 140 additions and 8 deletions

View File

@ -4100,8 +4100,12 @@ class Pages extends Controller
}));
}
// 货款/退货明细较宽,固定每页 15 行,减少纵向滚动并保持横向核对区稳定。
$perPage = in_array($dataset, ['payment-reconciliation', 'return-reconciliation'], true) ? 15 : 50;
// 宽表固定每页 15 行,减少纵向滚动并保持横向核对区稳定。
$perPage = in_array($dataset, [
'payment-reconciliation',
'return-reconciliation',
'supplier-payables-report',
], true) ? 15 : 50;
$currentPage = max((int) request('page', 1), 1);
return [

View File

@ -9,6 +9,8 @@
$finalKeyword = $payablesFinal['keyword'] ?? '';
$finalActions = $payablesFinal['actions'] ?? '';
$finalMaintenance = ['dataset' => $finalDataset, 'fields' => $finalFields];
$freezeThroughSupplier = $finalDataset === 'supplier-payables-report';
$freezeThroughSupplierFields = ['summary_month', 'checker', 'settlement_method', 'company', 'supplier'];
@endphp
<div class="payables-final-page">
@ -42,6 +44,15 @@
.payables-final-table .is-number { text-align: right; }
.payables-final-table .is-empty td { height: 8rem; color: var(--vip-content-muted, #595959); text-align: center; }
.payables-final-pagination { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.75rem 1rem; color: var(--vip-content-muted, #595959); font-size: 0.8125rem; }
.payables-final-table--freeze-through-supplier { border-collapse: separate; border-spacing: 0; }
.payables-final-table--freeze-through-supplier th,
.payables-final-table--freeze-through-supplier td { border-right: 1px solid var(--vip-border, #D8D2C7); }
.payables-final-table--freeze-through-supplier th:last-child,
.payables-final-table--freeze-through-supplier td:last-child { border-right: 0; }
.payables-final-table--freeze-through-supplier tbody tr:hover td { background: #FBF8F0; }
.payables-final-freeze-column { position: sticky; z-index: 1; background: var(--vip-surface, #FFFFFF); }
.payables-final-table thead .payables-final-freeze-column { z-index: 3; background: var(--vip-content-accent-soft, #F7F0DE); }
.payables-final-freeze-edge { box-shadow: inset -2px 0 0 var(--vip-content-accent, #B9954F); }
</style>
<form method="GET" class="payables-final-toolbar" aria-label="{{ $page['title'] ?? '' }}筛选">
@ -77,12 +88,26 @@
<div class="payables-final-card">
<div class="payables-final-scroll">
<table class="payables-final-table">
<table @class([
'payables-final-table',
'payables-final-table--freeze-through-supplier' => $freezeThroughSupplier,
])>
<thead>
<tr>
<th>序号</th>
<th
@if ($freezeThroughSupplier) data-final-column="__index" @endif
@class(['payables-final-freeze-column' => $freezeThroughSupplier])
>序号</th>
@foreach ($finalFields as $field)
<th @class(['is-number' => $field['type'] === 'number'])>{{ $field['label'] }}</th>
@php $freezeField = $freezeThroughSupplier && in_array($field['name'], $freezeThroughSupplierFields, true); @endphp
<th
@if ($freezeField) data-final-column="{{ $field['name'] }}" @endif
@class([
'is-number' => $field['type'] === 'number',
'payables-final-freeze-column' => $freezeField,
'payables-final-freeze-edge' => $freezeThroughSupplier && $field['name'] === 'supplier',
])
>{{ $field['label'] }}</th>
@endforeach
</tr>
</thead>
@ -90,10 +115,21 @@
@forelse ($finalRows ?? [] as $index => $row)
<tr data-supplier-maintenance-row data-key="{{ $row['_action_key'] ?? '' }}"
data-row="{!! e(json_encode($row, JSON_UNESCAPED_UNICODE)) !!}" tabindex="0" aria-selected="false">
<td>{{ ($finalRows->firstItem() ?? 1) + $index }}</td>
<td
@if ($freezeThroughSupplier) data-final-column="__index" @endif
@class(['payables-final-freeze-column' => $freezeThroughSupplier])
>{{ ($finalRows->firstItem() ?? 1) + $index }}</td>
@foreach ($finalFields as $field)
@php $value = $row[$field['name']] ?? null; @endphp
<td @class(['is-number' => $field['type'] === 'number'])>
@php $freezeField = $freezeThroughSupplier && in_array($field['name'], $freezeThroughSupplierFields, true); @endphp
<td
@if ($freezeField) data-final-column="{{ $field['name'] }}" @endif
@class([
'is-number' => $field['type'] === 'number',
'payables-final-freeze-column' => $freezeField,
'payables-final-freeze-edge' => $freezeThroughSupplier && $field['name'] === 'supplier',
])
>
@if ($value === null || $value === '')
-
@elseif ($field['type'] === 'number' && is_numeric($value))
@ -119,7 +155,7 @@
<a href="{{ $finalRows->previousPageUrl() }}">上一页</a>
@endif
第 {{ $finalRows?->currentPage() ?? 1 }} 页 / 共 {{ $finalRows?->lastPage() ?? 1 }} 页
· 每页 {{ $finalRows?->perPage() ?? (in_array($finalDataset, ['payment-reconciliation', 'return-reconciliation'], true) ? 15 : 50) }} 条
· 每页 {{ $finalRows?->perPage() ?? (in_array($finalDataset, ['payment-reconciliation', 'return-reconciliation', 'supplier-payables-report'], true) ? 15 : 50) }} 条
@if ($finalRows?->hasMorePages())
<a href="{{ $finalRows->nextPageUrl() }}">下一页</a>
@endif
@ -128,6 +164,51 @@
</div>
</div>
@if ($freezeThroughSupplier)
<script data-caiwu-replay>
(() => {
document.querySelectorAll('.payables-final-table--freeze-through-supplier').forEach((table) => {
if (table.dataset.freezeColumnsReady === 'true') {
return;
}
const headers = [...table.querySelectorAll('thead th[data-final-column]')];
const supplierIndex = headers.findIndex((header) => header.dataset.finalColumn === 'supplier');
if (supplierIndex < 0) {
return;
}
table.dataset.freezeColumnsReady = 'true';
const frozenHeaders = headers.slice(0, supplierIndex + 1);
const updateFreezeOffsets = () => {
let offset = 0;
frozenHeaders.forEach((header) => {
const column = header.dataset.finalColumn;
table.querySelectorAll(`[data-final-column="${column}"]`).forEach((cell) => {
cell.style.left = `${offset}px`;
});
offset += header.getBoundingClientRect().width;
});
};
updateFreezeOffsets();
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', updateFreezeOffsets, { passive: true });
return;
}
const resizeObserver = new ResizeObserver(updateFreezeOffsets);
resizeObserver.observe(table);
frozenHeaders.forEach((header) => resizeObserver.observe(header));
});
})();
</script>
@endif
@include('business.pages.supplier-payables.maintenance-blade-modal', [
'content' => ['supplierPayableMaintenance' => $finalMaintenance],
'supplierMaintenanceAction' => $finalActions,

View File

@ -295,6 +295,53 @@ class SupplierPayableFinalPagesTest extends FeatureTestCase
->assertDontSeeText('PM-01');
}
public function test_final_supplier_payables_report_paginates_and_freezes_through_supplier(): void
{
$records = [];
for ($index = 1; $index <= 20; $index++) {
$records[sprintf('supplier-payable:%02d', $index)] = [
'summary_month' => '2026-09',
'checker' => sprintf('对账人%02d', $index),
'settlement_method' => '月结',
'company' => sprintf('供应商主体%02d', $index),
'supplier' => sprintf('SUP-%02d', $index),
'express_freight' => '0',
];
}
app(SupplierPayableOverrideStore::class)->saveBatch(
'final:supplier:supplier-payables-report',
$records,
(int) $this->company->id,
(int) $this->user->id,
);
$baseUrl = route('business.pages.show', [
'company_id' => (int) $this->company->id,
'section' => 'supplier-payables-final',
'page' => 'supplier-payables-report',
]);
$url = fn (int $page): string => $baseUrl . '?page=' . $page;
$this->loginAs()->get($url(1))
->assertOk()
->assertSee('payables-final-table--freeze-through-supplier', false)
->assertSee('data-final-column="summary_month"', false)
->assertSee('data-final-column="supplier"', false)
->assertSee('payables-final-freeze-edge', false)
->assertSee('data-caiwu-replay', false)
->assertSeeText('每页 15 条')
->assertSeeText('第 1 页 / 共 2 页')
->assertSeeText('SUP-01')
->assertDontSeeText('SUP-16');
$this->loginAs()->get($url(2))
->assertOk()
->assertSeeText('第 2 页 / 共 2 页')
->assertSeeText('SUP-16')
->assertSeeText('SUP-20')
->assertDontSeeText('SUP-01');
}
public function test_final_import_reads_files_larger_than_one_chunk(): void
{
$lines = ['月份,入库单号,采购单号,日期,供应商,货号,商品编码,规格名称,数量,单价,金额,预估单价,预估金额,供应商商品编码,备注,仓库名称,快递名称,快递单号,未发出,对账人'];