From a7f3d5f3b5864f183f29b0367eeb904ee8ebc6f5 Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Wed, 19 Aug 2026 07:36:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=A9=E6=95=88=E5=B7=A5=E8=B5=84?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=89=8B=E5=8A=A8=E8=8E=B7=E5=8F=96+?= =?UTF-8?q?=E6=8C=89=E8=80=83=E6=A0=B8=E7=B3=BB=E6=95=B0=E7=9D=80=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - 建立批次时绩效工资填0,不自动带出 - 获取绩效工资接口返回 details(grade/coefficient/performanceSalary) - 无考核记录时系数为1.0(绩效工资=基数全额) 前端: - 存储获取绩效工资返回的 details 到 perfDetails state - 绩效工资列按系数着色:A级(1.2)绿色/C级(0.8)橙色/D级(0.6)红色 - B级(1.0)和无考核正常色,0值灰色 - hover显示考核等级和系数 --- backend/src/routes/payroll2.routes.ts | 10 +++-- frontend/src/pages/money/BatchTab.tsx | 59 ++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/payroll2.routes.ts b/backend/src/routes/payroll2.routes.ts index ac1408d..5d4c5f2 100644 --- a/backend/src/routes/payroll2.routes.ts +++ b/backend/src/routes/payroll2.routes.ts @@ -493,11 +493,11 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti } else if (prevPayslip) { // 非试用期:优先用上月工资条的基本工资(保持薪资连续性) baseSalary = prevPayslip.baseSalary - performanceSalary = prevPayslip.performanceSalary || 0 + // 绩效工资不自动带出,需手动点"获取绩效工资"按钮 } else if (empBaseSalary > 0 || empPerformanceSalary > 0) { // 新数据:有工资结构 baseSalary = empBaseSalary - performanceSalary = empPerformanceSalary + // 绩效工资不自动带出,需手动点"获取绩效工资"按钮 } else if (emp.monthlySalary) { // 旧数据兼容:回退到 monthlySalary try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 } @@ -515,7 +515,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti performanceSalary = 0 } else if (empBaseSalary > 0 || empPerformanceSalary > 0) { baseSalary = empBaseSalary - performanceSalary = empPerformanceSalary + // 绩效工资不自动带出,需手动点"获取绩效工资"按钮 } else if (emp.monthlySalary && mode === 'custom') { // custom 模式旧数据兼容 try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 } @@ -840,11 +840,13 @@ router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res: let filled = 0 let totalAmount = 0 + const details: { employeeId: string; grade: string | null; coefficient: number; performanceSalary: number }[] = [] for (const entry of entries) { const basePerfSalary = empPerfSalaryMap.get(entry.employeeId) || 0 if (basePerfSalary <= 0) continue // 绩效工资基数为0则跳过 const perfRecord = perfMap.get(entry.employeeId) + const grade = perfRecord?.grade || null const coefficient = perfRecord ? (gradeCoefficients[perfRecord.grade] || 1.0) : 1.0 const calculatedPerfSalary = Math.round(basePerfSalary * coefficient * 100) / 100 @@ -853,6 +855,7 @@ router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res: data: { performanceSalary: calculatedPerfSalary }, }) await recalcEntry(orgId, batchId, batch.month, batch.type, entry.id, entry.employeeId) + details.push({ employeeId: entry.employeeId, grade, coefficient, performanceSalary: calculatedPerfSalary }) filled++ totalAmount += calculatedPerfSalary } @@ -865,6 +868,7 @@ router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res: data: { filled, totalAmount: Math.round(totalAmount * 100) / 100, + details, message: filled > 0 ? `已填充 ${filled} 人绩效工资,合计 ¥${Math.round(totalAmount * 100) / 100}${performanceRecords.length > 0 ? `(${performanceRecords.length} 人有考核记录)` : '(无考核记录,按系数1.0计算)'}` : '无绩效工资数据(员工未设置绩效工资基数)', diff --git a/frontend/src/pages/money/BatchTab.tsx b/frontend/src/pages/money/BatchTab.tsx index 0d6a35d..d79010e 100644 --- a/frontend/src/pages/money/BatchTab.tsx +++ b/frontend/src/pages/money/BatchTab.tsx @@ -489,6 +489,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void const [taxDetailFor, setTaxDetailFor] = useState<{ employeeId: string; name: string } | null>(null) const [taxDetailData, setTaxDetailData] = useState(null) const [taxDetailLoading, setTaxDetailLoading] = useState(false) + const [perfDetails, setPerfDetails] = useState>(new Map()) const { data: batch, isLoading } = useQuery({ queryKey: ['batch-detail', batchId], @@ -599,6 +600,13 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void onSuccess: (res: any) => { queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) queryClient.invalidateQueries({ queryKey: ['batches'] }) + if (res.data?.details) { + const map = new Map() + for (const d of res.data.details) { + map.set(d.employeeId, { grade: d.grade, coefficient: d.coefficient }) + } + setPerfDetails(map) + } if (res.data?.filled > 0) { toast.success(res.data.message) } else { @@ -1229,7 +1237,56 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void })()} {renderCell(entry, 'baseSalary')} - {renderCell(entry, 'performanceSalary')} + {(() => { + const perfVal = entry.performanceSalary || 0 + const perfInfo = perfDetails.get(entry.employeeId) + const coef = perfInfo?.coefficient + const grade = perfInfo?.grade + // 颜色:A(1.2)=绿色, C(0.8)=橙色, D(0.6)=红色, B(1.0)/无=默认 + let colorClass = '' + let title = '' + if (coef != null && coef !== 1.0) { + if (coef > 1.0) { colorClass = 'text-green-600'; title = `考核等级 ${grade},系数 ${coef}` } + else if (coef >= 0.8) { colorClass = 'text-orange-600'; title = `考核等级 ${grade},系数 ${coef}` } + else { colorClass = 'text-red-600'; title = `考核等级 ${grade},系数 ${coef}` } + } else if (coef === 1.0 && grade) { + title = `考核等级 ${grade},系数 1.0` + } + const isEditing = editCell?.employeeId === entry.employeeId && editCell?.field === 'performanceSalary' + const canEdit = !isArchived && editableFields.includes('performanceSalary') + if (isEditing) { + return ( + + setEditValue(e.target.value)} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + /> + + ) + } + return ( + canEdit && startEdit(entry.employeeId, 'performanceSalary', perfVal)} + title={title} + > + {canEdit ? ( + + {fmt(perfVal)} + + ) : ( + {fmt(perfVal)} + )} + + ) + })()} {renderCell(entry, 'overtimePay')} {renderCell(entry, 'allowance')} {renderCell(entry, 'bonus')}