fix: 批次表格锁定列偏移量修正 - table-layout:fixed 确保列宽精确
- 使用 table-layout: fixed 防止列宽被内容压缩 - 动态计算表格总宽: 固定列宽 + 动态列数×90 + 右侧锁定列总宽 - 右侧锁定列偏移量从常量计算,不再硬编码 - 操作列宽度调整为60px,确保移除按钮可见
This commit is contained in:
@@ -753,6 +753,19 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
// 预置字段集合(固定列存储)
|
// 预置字段集合(固定列存储)
|
||||||
const PRESET_FIELDS = new Set(['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'socialOrg', 'housingEmp', 'housingOrg', 'positionSalary', 'senioritySalary', 'transportAllowance', 'mealAllowance', 'housingAllowance', 'communicationAllowance', 'otherDeduction'])
|
const PRESET_FIELDS = new Set(['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'socialOrg', 'housingEmp', 'housingOrg', 'positionSalary', 'senioritySalary', 'transportAllowance', 'mealAllowance', 'housingAllowance', 'communicationAllowance', 'otherDeduction'])
|
||||||
|
|
||||||
|
// 表格列宽计算(table-layout: fixed 需要精确宽度)
|
||||||
|
const COL_W = { employee: 140, contract: 100, dynamic: 90, netPay: 110, deferred: 110, risk: 50, action: 60 }
|
||||||
|
const dynamicCols = (templateItems || []).filter((t: any) => (!isBonus || t.code === 'bonus') && t.code !== 'netPay')
|
||||||
|
const rightLockedWidth = COL_W.netPay + COL_W.deferred + COL_W.risk + (isArchived ? 0 : COL_W.action)
|
||||||
|
const tableMinWidth = COL_W.employee + COL_W.contract + dynamicCols.length * COL_W.dynamic + rightLockedWidth
|
||||||
|
// 右侧锁定列偏移量(从右到左累加)
|
||||||
|
const rightOffsets = {
|
||||||
|
action: 0,
|
||||||
|
risk: isArchived ? 0 : COL_W.action,
|
||||||
|
deferred: isArchived ? COL_W.risk : COL_W.action + COL_W.risk,
|
||||||
|
netPay: isArchived ? COL_W.risk + COL_W.deferred : COL_W.action + COL_W.risk + COL_W.deferred,
|
||||||
|
}
|
||||||
|
|
||||||
// 点击单元格进入编辑
|
// 点击单元格进入编辑
|
||||||
const startEdit = (employeeId: string, field: string, currentValue: number) => {
|
const startEdit = (employeeId: string, field: string, currentValue: number) => {
|
||||||
if (isArchived || !editableFields.includes(field)) return
|
if (isArchived || !editableFields.includes(field)) return
|
||||||
@@ -1235,27 +1248,27 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
{/* 人员表格 */}
|
{/* 人员表格 */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="overflow-x-auto" style={{ maxWidth: '100%' }}>
|
<div className="overflow-x-auto" style={{ maxWidth: '100%' }}>
|
||||||
<table className="text-sm" style={{ borderCollapse: 'separate', borderSpacing: 0, minWidth: 1200 }}>
|
<table className="text-sm" style={{ borderCollapse: 'separate', borderSpacing: 0, tableLayout: 'fixed', width: tableMinWidth }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs text-gray-500">
|
<tr className="text-left text-xs text-gray-500">
|
||||||
<th className="py-2 px-3 whitespace-nowrap sticky left-0 z-20 bg-white border-r border-gray-200" style={{ width: 140 }}>员工</th>
|
<th className="py-2 px-3 whitespace-nowrap sticky left-0 z-20 bg-white border-r border-gray-200" style={{ width: COL_W.employee }}>员工</th>
|
||||||
<th className="py-2 px-2 whitespace-nowrap sticky z-20 bg-white border-r border-gray-200" style={{ width: 100, left: 140 }}>合同类型</th>
|
<th className="py-2 px-2 whitespace-nowrap sticky z-20 bg-white border-r border-gray-200" style={{ width: COL_W.contract, left: COL_W.employee }}>合同类型</th>
|
||||||
{/* 动态生成模板项列(排除 netPay,由固定实发列显示) */}
|
{/* 动态生成模板项列(排除 netPay,由固定实发列显示) */}
|
||||||
{(templateItems || []).filter((t: any) => (!isBonus || t.code === 'bonus') && t.code !== 'netPay').map((t: any) => (
|
{dynamicCols.map((t: any) => (
|
||||||
<th key={t.code} className={`py-2 px-2 text-right whitespace-nowrap ${t.type === 'CALCULATED' ? 'text-gray-500' : ''}`} style={{ minWidth: 80 }}>
|
<th key={t.code} className={`py-2 px-2 text-right whitespace-nowrap ${t.type === 'CALCULATED' ? 'text-gray-500' : ''}`} style={{ width: COL_W.dynamic }}>
|
||||||
{t.name}
|
{t.name}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
<th className="py-2 px-2 text-right text-gray-500 whitespace-nowrap sticky z-20 bg-white border-l border-gray-200" style={{ width: 110, right: isArchived ? 160 : 210 }}>实发</th>
|
<th className="py-2 px-2 text-right text-gray-500 whitespace-nowrap sticky z-20 bg-white border-l border-gray-200" style={{ width: COL_W.netPay, right: rightOffsets.netPay }}>实发</th>
|
||||||
<th className="py-2 px-2 text-right text-gray-500 whitespace-nowrap sticky z-20 bg-white border-l border-gray-200" style={{ width: 110, right: isArchived ? 50 : 100 }}>递延扣款</th>
|
<th className="py-2 px-2 text-right text-gray-500 whitespace-nowrap sticky z-20 bg-white border-l border-gray-200" style={{ width: COL_W.deferred, right: rightOffsets.deferred }}>递延扣款</th>
|
||||||
<th className="py-2 px-2 text-center whitespace-nowrap sticky z-20 bg-white border-l border-gray-200" style={{ width: 50, right: isArchived ? 0 : 50 }}>风险</th>
|
<th className="py-2 px-2 text-center whitespace-nowrap sticky z-20 bg-white border-l border-gray-200" style={{ width: COL_W.risk, right: rightOffsets.risk }}>风险</th>
|
||||||
{!isArchived && <th className="py-2 px-2 text-center whitespace-nowrap sticky z-20 bg-white border-l border-gray-200" style={{ width: 50, right: 0 }}>操作</th>}
|
{!isArchived && <th className="py-2 px-2 text-center whitespace-nowrap sticky z-20 bg-white border-l border-gray-200" style={{ width: COL_W.action, right: rightOffsets.action }}>操作</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{batch.entries.slice((page - 1) * pageSize, page * pageSize).map((entry: any) => (
|
{batch.entries.slice((page - 1) * pageSize, page * pageSize).map((entry: any) => (
|
||||||
<tr key={entry.id} className="group border-b last:border-0">
|
<tr key={entry.id} className="group border-b last:border-0">
|
||||||
<td className="py-2 px-3 sticky left-0 z-10 bg-white group-hover:bg-gray-50 border-r border-gray-200" style={{ width: 140 }}>
|
<td className="py-2 px-3 sticky left-0 z-10 bg-white group-hover:bg-gray-50 border-r border-gray-200" style={{ width: COL_W.employee }}>
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<div className="text-xs font-medium">{entry.employee.name}</div>
|
<div className="text-xs font-medium">{entry.employee.name}</div>
|
||||||
{entry.employee.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{entry.employee.idCardNumber}</span>}
|
{entry.employee.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{entry.employee.idCardNumber}</span>}
|
||||||
@@ -1265,7 +1278,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-2 sticky z-10 bg-white group-hover:bg-gray-50 border-r border-gray-200" style={{ width: 100, left: 140 }}>
|
<td className="py-2 px-2 sticky z-10 bg-white group-hover:bg-gray-50 border-r border-gray-200" style={{ width: COL_W.contract, left: COL_W.employee }}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const ct = entry.employee.contracts?.[0]?.contractType
|
const ct = entry.employee.contracts?.[0]?.contractType
|
||||||
const cfg: Record<string, { label: string; style: string }> = {
|
const cfg: Record<string, { label: string; style: string }> = {
|
||||||
@@ -1289,7 +1302,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
})()}
|
})()}
|
||||||
</td>
|
</td>
|
||||||
{/* 动态生成模板项单元格(排除 netPay) */}
|
{/* 动态生成模板项单元格(排除 netPay) */}
|
||||||
{(templateItems || []).filter((t: any) => (!isBonus || t.code === 'bonus') && t.code !== 'netPay').map((t: any) => {
|
{dynamicCols.map((t: any) => {
|
||||||
// 绩效工资:保留特殊渲染(显示绩效等级)
|
// 绩效工资:保留特殊渲染(显示绩效等级)
|
||||||
if (t.code === 'performanceSalary') {
|
if (t.code === 'performanceSalary') {
|
||||||
const perfVal = entry.performanceSalary || 0
|
const perfVal = entry.performanceSalary || 0
|
||||||
@@ -1354,12 +1367,12 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
return renderCell(entry, t.code, t.type === 'CALCULATED' ? 'font-medium text-gray-600' : '')
|
return renderCell(entry, t.code, t.type === 'CALCULATED' ? 'font-medium text-gray-600' : '')
|
||||||
})}
|
})}
|
||||||
{/* 实发:最低工资保护触发时高亮显示 */}
|
{/* 实发:最低工资保护触发时高亮显示 */}
|
||||||
<td className={`py-2 px-2 text-right font-bold sticky z-10 bg-white group-hover:bg-gray-50 border-l border-gray-200 ${(entry.minWageApplied || 0) > 0 ? 'text-amber-600' : 'text-safe'}`} style={{ width: 110, right: isArchived ? 160 : 210 }} title={(entry.minWageApplied || 0) > 0 ? `最低工资保护已触发\n应发 ¥${fmt(entry.totalPay)} → 实发补齐到 ¥${fmt(entry.minWage)}\n免扣社保 ¥${fmt(entry.deferredSocialEmp || 0)} + 免扣公积金 ¥${fmt(entry.deferredHousingEmp || 0)} + 额外补齐 ¥${fmt(entry.deferredMinWage || 0)} = 递延 ¥${fmt((entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0))}(次月补扣)` : ''}>
|
<td className={`py-2 px-2 text-right font-bold sticky z-10 bg-white group-hover:bg-gray-50 border-l border-gray-200 ${(entry.minWageApplied || 0) > 0 ? 'text-amber-600' : 'text-safe'}`} style={{ width: COL_W.netPay, right: rightOffsets.netPay }} title={(entry.minWageApplied || 0) > 0 ? `最低工资保护已触发\n应发 ¥${fmt(entry.totalPay)} → 实发补齐到 ¥${fmt(entry.minWage)}\n免扣社保 ¥${fmt(entry.deferredSocialEmp || 0)} + 免扣公积金 ¥${fmt(entry.deferredHousingEmp || 0)} + 额外补齐 ¥${fmt(entry.deferredMinWage || 0)} = 递延 ¥${fmt((entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0))}(次月补扣)` : ''}>
|
||||||
{fmt(entry.netPay)}
|
{fmt(entry.netPay)}
|
||||||
{(entry.minWageApplied || 0) > 0 && <span className="text-xs text-amber-500 ml-1">★</span>}
|
{(entry.minWageApplied || 0) > 0 && <span className="text-xs text-amber-500 ml-1">★</span>}
|
||||||
</td>
|
</td>
|
||||||
{/* 递延扣款明细 */}
|
{/* 递延扣款明细 */}
|
||||||
<td className="py-2 px-2 text-right text-xs sticky z-10 bg-white group-hover:bg-gray-50 border-l border-gray-200" style={{ width: 110, right: isArchived ? 50 : 100 }}>
|
<td className="py-2 px-2 text-right text-xs sticky z-10 bg-white group-hover:bg-gray-50 border-l border-gray-200" style={{ width: COL_W.deferred, right: rightOffsets.deferred }}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const deferred = (entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0)
|
const deferred = (entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0)
|
||||||
const prevDeferred = (entry.prevDeferredSocialEmp || 0) + (entry.prevDeferredHousingEmp || 0) + (entry.prevDeferredMinWage || 0)
|
const prevDeferred = (entry.prevDeferredSocialEmp || 0) + (entry.prevDeferredHousingEmp || 0) + (entry.prevDeferredMinWage || 0)
|
||||||
@@ -1372,7 +1385,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
return <span className="text-gray-300">—</span>
|
return <span className="text-gray-300">—</span>
|
||||||
})()}
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-2 text-center sticky z-10 bg-white group-hover:bg-gray-50 border-l border-gray-200" style={{ width: 50, right: isArchived ? 0 : 50 }}>
|
<td className="py-2 px-2 text-center sticky z-10 bg-white group-hover:bg-gray-50 border-l border-gray-200" style={{ width: COL_W.risk, right: rightOffsets.risk }}>
|
||||||
{entry.riskWarnings && entry.riskWarnings.length > 0 ? (
|
{entry.riskWarnings && entry.riskWarnings.length > 0 ? (
|
||||||
<span className="text-danger cursor-help" title={entry.riskWarnings.join('\n')}>
|
<span className="text-danger cursor-help" title={entry.riskWarnings.join('\n')}>
|
||||||
<AlertTriangle className="w-4 h-4" />
|
<AlertTriangle className="w-4 h-4" />
|
||||||
@@ -1386,7 +1399,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
{!isArchived && (
|
{!isArchived && (
|
||||||
<td className="py-2 px-2 text-center sticky z-10 bg-white group-hover:bg-gray-50 border-l border-gray-200" style={{ width: 50, right: 0 }}>
|
<td className="py-2 px-2 text-center sticky z-10 bg-white group-hover:bg-gray-50 border-l border-gray-200" style={{ width: COL_W.action, right: rightOffsets.action }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeEmployeeMutation.mutate(entry.employeeId)}
|
onClick={() => removeEmployeeMutation.mutate(entry.employeeId)}
|
||||||
className="text-gray-500 hover:text-danger p-1"
|
className="text-gray-500 hover:text-danger p-1"
|
||||||
|
|||||||
Reference in New Issue
Block a user