diff --git a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductTable.test.tsx b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductTable.test.tsx index 4d9ac36b..08e2af98 100644 --- a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductTable.test.tsx +++ b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductTable.test.tsx @@ -43,6 +43,12 @@ const rows: FinishedProductProgressSummaryRow[] = [{ isSummary: true, }] +const secondRow = { ...row, id: 'finished-2', contractDateNumber: '260902002', contractNumber: 'HT-002' } +const groupedRows: FinishedProductProgressSummaryRow[] = [ + rows[0], + { id: 'group-2', contractDateNumber: secondRow.contractDateNumber, details: [secondRow], detailCount: 1, isSummary: true }, +] + describe('FinishedProductTable selection', () => { const renderTable = (overrides: Partial> = {}) => { const props: ComponentProps = { @@ -77,4 +83,13 @@ describe('FinishedProductTable selection', () => { expect(screen.getByRole('checkbox', { name: '全选当前页' })).toBeDisabled() }) + + it('marks each contract group with a strong separator and alternating band', () => { + renderTable({ rows: groupedRows }) + + expect(screen.getByText('HT-001').closest('tr')).toHaveClass('finished-contract-start') + expect(screen.getByText('HT-001').closest('td')).toHaveClass('finished-contract-band-a') + expect(screen.getByText('HT-002').closest('tr')).toHaveClass('finished-contract-start') + expect(screen.getByText('HT-002').closest('td')).toHaveClass('finished-contract-band-b') + }) }) diff --git a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductTable.tsx b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductTable.tsx index c3c08307..77d492cf 100644 --- a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductTable.tsx +++ b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductTable.tsx @@ -8,29 +8,52 @@ import type { FinishedProductProgressColumn, FinishedProductProgressColumnKey } export function FinishedProductTable({ rows, columns, canEdit, selectedIds, allSelected, onToggleSelect, onToggleAll, onEdit, onView, onHistory, onPreview, onDelete }: { rows: FinishedProductProgressSummaryRow[]; columns: readonly FinishedProductProgressColumn[]; canEdit: boolean; selectedIds: ReadonlySet; allSelected: boolean; onToggleSelect: (id: string) => void; onToggleAll: () => void; onEdit: (row: FinishedProductProgressRow) => void; onView: (row: FinishedProductProgressRow) => void; onHistory: (row: FinishedProductProgressRow) => void; onPreview: (row: FinishedProductProgressRow) => void; onDelete: (row: FinishedProductProgressRow) => void }) { const minWidth = columns.reduce((total, column) => total + column.minWidth, 0) const detailCount = rows.reduce((total, group) => total + group.details.length, 0) + const tableRows = buildFinishedProductTableRows(rows) return
{canEdit && }{columns.map((column) => )} - {rows.flatMap((group) => group.details.map((row, index) => { - const contractKey = row.contractNumber.trim() || row.id - const isContractFirst = index === 0 || group.details[index - 1]?.contractNumber.trim() !== row.contractNumber.trim() - const contractRunLength = isContractFirst - ? group.details.slice(index).findIndex((candidate, offset) => offset > 0 && candidate.contractNumber.trim() !== row.contractNumber.trim()) - : 0 - const contractRowSpan = isContractFirst ? (contractRunLength === -1 ? group.details.length - index : contractRunLength) : 0 - return {canEdit && isContractFirst && }{columns.map((column) => column.key === 'contractDateNumber' + {tableRows.map(({ group, row, index, contractKey, isContractFirst, contractRowSpan, contractBandClass }) => { + return {canEdit && isContractFirst && }{columns.map((column) => column.key === 'contractDateNumber' ? index === 0 - ? + ? : null - : )} - }))} + : )} + })}
{column.label}
onToggleSelect(contractKey)} aria-label={`选择合同号 ${row.contractNumber || '-'}`} />
onToggleSelect(contractKey)} aria-label={`选择合同号 ${row.contractNumber || '-'}`} />{group.contractDateNumber || '-'}{group.contractDateNumber || '-'}
{detailCount === 0 &&
暂无匹配的成品进度记录
}
} -function FinishedProductCell({ column, row, canEdit, onEdit, onView, onHistory, onPreview, onDelete }: { column: FinishedProductProgressColumnKey; row: FinishedProductProgressDetailRow; canEdit: boolean; onEdit: (row: FinishedProductProgressRow) => void; onView: (row: FinishedProductProgressRow) => void; onHistory: (row: FinishedProductProgressRow) => void; onPreview: (row: FinishedProductProgressRow) => void; onDelete: (row: FinishedProductProgressRow) => void }) { - const base = 'whitespace-nowrap border-b border-zinc-100 bg-white px-3 py-3 text-zinc-700 group-hover:bg-zinc-50' +function buildFinishedProductTableRows(rows: FinishedProductProgressSummaryRow[]) { + let previousContractVisualKey: string | null = null + let contractBandIndex = -1 + return rows.flatMap((group) => group.details.map((row, index) => { + const contractNumber = row.contractNumber.trim() + const contractKey = contractNumber || row.id + // Summary groups are already separated by date number; keep their visual bands + // distinct even when a legacy import happens to reuse the same contract number. + const contractVisualKey = `${group.id}:${contractNumber || '__blank__'}` + const isContractFirst = contractVisualKey !== previousContractVisualKey + if (isContractFirst) contractBandIndex += 1 + previousContractVisualKey = contractVisualKey + const contractRunLength = isContractFirst + ? group.details.slice(index).findIndex((candidate, offset) => offset > 0 && candidate.contractNumber.trim() !== row.contractNumber.trim()) + : 0 + const contractRowSpan = isContractFirst ? (contractRunLength === -1 ? group.details.length - index : contractRunLength) : 0 + return { + group, + row, + index, + contractKey, + isContractFirst, + contractRowSpan, + contractBandClass: contractBandIndex % 2 === 0 ? 'finished-contract-band-a' : 'finished-contract-band-b', + } + })) +} + +function FinishedProductCell({ column, row, canEdit, bandClass, onEdit, onView, onHistory, onPreview, onDelete }: { column: FinishedProductProgressColumnKey; row: FinishedProductProgressDetailRow; canEdit: boolean; bandClass: string; onEdit: (row: FinishedProductProgressRow) => void; onView: (row: FinishedProductProgressRow) => void; onHistory: (row: FinishedProductProgressRow) => void; onPreview: (row: FinishedProductProgressRow) => void; onDelete: (row: FinishedProductProgressRow) => void }) { + const base = `whitespace-nowrap border-b border-zinc-100 px-3 py-3 text-zinc-700 group-hover:bg-zinc-50 ${bandClass}` if (column === 'contractNumber') return {row.contractNumber || '-'} if (column === 'followUpOwner') return {row.followUpOwner || '-'} if (column === 'imageUrl') return {row.imageUrl ? : -} diff --git a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/finishedProductProgressStyles.ts b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/finishedProductProgressStyles.ts index 6fdb1995..bc655883 100644 --- a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/finishedProductProgressStyles.ts +++ b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/finishedProductProgressStyles.ts @@ -14,7 +14,7 @@ export const finishedProductProgressStyles = String.raw` .finished-filter-multi{position:relative;min-width:0}.finished-filter-multi>button{display:flex;height:38px;width:100%;align-items:center;justify-content:space-between;gap:8px;border:1px solid #d4d4d8;border-radius:5px;background:#fff;padding:0 11px;color:#3f3f46;font-size:12px}.finished-filter-multi>button:hover,.finished-filter-multi>button[aria-expanded=true]{border-color:#71717a}.finished-filter-multi-panel{position:absolute;left:0;top:43px;z-index:55;width:max(100%,320px);max-width:calc(100vw - 28px);overflow:hidden;border:1px solid #d4d4d8;border-radius:6px;background:#fff;box-shadow:0 12px 28px rgba(24,24,27,.14)}.finished-filter-multi-header{display:flex;height:38px;align-items:center;justify-content:space-between;border-bottom:1px solid #e4e4e7;padding:0 10px;font-size:12px}.finished-filter-multi-header button{display:flex;height:25px;width:25px;align-items:center;justify-content:center;border:0;border-radius:4px;background:transparent;color:#71717a}.finished-filter-multi-header button:hover{background:#f4f4f5}.finished-filter-multi-search{display:flex;align-items:center;gap:7px;margin:7px 7px 0;height:34px;padding:0 9px;border:1px solid #d4d4d8;border-radius:4px;color:#71717a}.finished-filter-multi-search input{height:30px;min-width:0;flex:1;border:0!important;box-shadow:none!important;padding:0!important;outline:none}.finished-filter-multi-options{max-height:280px;overflow-y:auto;padding:6px}.finished-filter-multi-options label{display:grid;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:7px;min-height:34px;border-radius:4px;padding:4px 7px;color:#3f3f46;font-size:12px;cursor:pointer}.finished-filter-multi-options label:hover{background:#f4f4f5}.finished-filter-multi-options input{position:absolute;height:0;width:0;opacity:0}.finished-filter-multi-check{display:flex;height:16px;width:16px;align-items:center;justify-content:center;border:1px solid #d4d4d8;border-radius:3px}.finished-filter-multi-options input:checked+.finished-filter-multi-check{border-color:#27272a;background:#27272a;color:#fff}.finished-filter-multi-options label>span:last-child{min-width:0;overflow-wrap:anywhere;white-space:normal;line-height:1.45}.finished-filter-multi-empty{display:block;padding:18px;text-align:center;color:#a1a1aa;font-size:12px} .finished-product-table-shell{position:relative;display:flex;min-height:0;flex:1;flex-direction:column;margin:16px 20px 20px;overflow:hidden;border:1px solid #e4e4e7;border-radius:7px;background:#fff;box-shadow:0 8px 24px rgba(24,24,27,.05)} .finished-product-table-shell>div:first-child{padding-bottom:76px} - .finished-field{display:flex;flex-direction:column;gap:6px}.finished-field>span{font-size:12px;font-weight:650;color:#52525b}.finished-field em{margin-left:3px;color:#b91c1c;font-style:normal}.finished-field input,.finished-field textarea{width:100%;box-sizing:border-box;border:1px solid #d4d4d8;border-radius:5px;background:#fff;padding:9px 10px;color:#27272a;font-size:13px;outline:none}.finished-field input{height:40px}.finished-field textarea{resize:vertical;line-height:1.6}.finished-field input:focus,.finished-field textarea:focus{border-color:#71717a;box-shadow:0 0 0 2px rgba(82,82,91,.08)}.finished-product-select-cell{width:44px;min-width:44px}.finished-product-select-cell input{height:16px;width:16px;accent-color:#27272a;cursor:pointer} + .finished-field{display:flex;flex-direction:column;gap:6px}.finished-field>span{font-size:12px;font-weight:650;color:#52525b}.finished-field em{margin-left:3px;color:#b91c1c;font-style:normal}.finished-field input,.finished-field textarea{width:100%;box-sizing:border-box;border:1px solid #d4d4d8;border-radius:5px;background:#fff;padding:9px 10px;color:#27272a;font-size:13px;outline:none}.finished-field input{height:40px}.finished-field textarea{resize:vertical;line-height:1.6}.finished-field input:focus,.finished-field textarea:focus{border-color:#71717a;box-shadow:0 0 0 2px rgba(82,82,91,.08)}.finished-product-select-cell{width:44px;min-width:44px}.finished-product-select-cell input{height:16px;width:16px;accent-color:#27272a;cursor:pointer}.finished-contract-band-a{background:#fff}.finished-contract-band-b{background:#f5f7f7}.finished-contract-start>td{border-top:2px solid #52525b} @media(max-width:1280px){.finished-product-filters{grid-template-columns:repeat(4,minmax(140px,1fr))}.finished-product-filters>input:first-child{grid-column:span 2}} @media(max-width:760px){.finished-product-toolbar{align-items:flex-start;padding:14px}.finished-product-actions{justify-content:flex-start}.finished-product-filters{grid-template-columns:1fr 1fr;padding:10px 14px}.finished-product-filters>input:first-child{grid-column:1/-1}.finished-product-table-shell{margin:10px}.finished-product-table-shell>div:first-child{padding-bottom:72px}.finished-page-button{padding:0 7px}.finished-product-toolbar p{display:none}} `