gaoluyang
5 天以前 7f6313626df7f49b0e45e1d9d9f197753152e29e
九鸿:薪酬核算表按客户工资表重排字段,员工档案补标准项

- 薪酬核算列表/预览按客户工资表列顺序展示(姓名、入职日期、岗位、标准出勤天数…扣款合计、税后工资)
- 预览弹窗可手工调整 浮动绩效、7 项加项、3 项扣款,实时重算 应付工资/加项合计/扣款合计/税后工资
- 员工档案新增 标准工资/岗位工资/加班工资标准/安全环保奖标准/绩效标准 表单与详情展示
- 接口类型补齐新字段

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改6个文件
535 ■■■■■ 文件已修改
src/api/hrm/employee/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/calculation/index.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/data.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/detail.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/data.ts 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/index.vue 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts
@@ -31,6 +31,16 @@
    sickLeaveBalance?: number;
    salaryStructureId?: number;
    baseSalary?: number;
    /** 标准工资 */
    standardSalary?: number;
    /** 岗位工资 */
    postSalary?: number;
    /** 加班工资标准(元/小时) */
    overtimeHourlyRate?: number;
    /** 安全环保奖标准 */
    safetyAwardStandard?: number;
    /** 绩效标准 */
    performanceStandard?: number;
    roleIds?: number[];
    roleNames?: string[];
    bankName?: string;
src/api/hrm/salary/calculation/index.ts
@@ -12,6 +12,62 @@
    userName?: string;
    deptId?: number;
    deptName?: string;
    /** 入职日期 */
    hireDate?: string;
    /** 岗位名称 */
    postName?: string;
    // ===== 九泓工资表:标准项(取自员工档案) =====
    /** 标准工资 */
    standardSalary?: number;
    /** 岗位工资 */
    postSalary?: number;
    /** 加班工资标准(元/小时) */
    overtimeHourlyRate?: number;
    /** 安全环保奖标准 */
    safetyAwardStandard?: number;
    /** 绩效标准 */
    performanceStandard?: number;
    // ===== 九泓工资表:出勤与计薪 =====
    /** 计薪出勤(天) */
    paidAttendanceDays?: number;
    /** 标准出勤时长(小时) */
    standardWorkHours?: number;
    /** 法定假日时长(小时) */
    legalHolidayHours?: number;
    /** 标准出勤工资 */
    standardAttendancePay?: number;
    /** 法定假工资 */
    legalHolidayPay?: number;
    /** 安全环保奖 */
    safetyAward?: number;
    /** 浮动绩效工资 */
    floatingPerformancePay?: number;
    /** 应付工资 */
    payableSalary?: number;
    // ===== 九泓工资表:加项 / 扣项 =====
    /** 工龄工资 */
    seniorityPay?: number;
    /** 夜班补助 */
    nightShiftAllowance?: number;
    /** 学历补贴 */
    educationAllowance?: number;
    /** 体检 */
    healthCheckAllowance?: number;
    /** 优秀员工奖 */
    excellentEmployeeAward?: number;
    /** 其他补助 */
    otherSubsidy?: number;
    /** 部门绩效 */
    deptPerformance?: number;
    /** 加项合计 */
    additionTotal?: number;
    /** 漏打卡 */
    missedPunchDeduction?: number;
    /** 迟到早退 */
    lateEarlyDeduction?: number;
    /** 扣款合计 */
    deductionTotal?: number;
    // ===== 兼容旧字段 =====
    baseSalary?: number;
    performanceSalary?: number;
    overtimePay?: number;
src/views/hrm/employee/data.ts
@@ -268,6 +268,66 @@
      },
    },
    {
      fieldName: 'postSalary',
      label: '岗位工资',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入岗位工资',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
      help: '与基本工资一起构成工资表中的标准工资',
    },
    {
      fieldName: 'standardSalary',
      label: '标准工资',
      component: 'InputNumber',
      componentProps: {
        placeholder: '不填则自动按「基本工资 + 岗位工资」计算',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
      help: '工资表中按计薪出勤折算的基数;留空时取基本工资 + 岗位工资',
    },
    {
      fieldName: 'overtimeHourlyRate',
      label: '加班工资标准',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入每小时加班单价(元/小时)',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
      help: '元/小时;留空时按「基本工资 / 21.75 / 8」折算',
    },
    {
      fieldName: 'safetyAwardStandard',
      label: '安全环保奖标准',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入安全环保奖标准',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
      help: '按计薪出勤比例折算后计入应付工资',
    },
    {
      fieldName: 'performanceStandard',
      label: '绩效标准',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入绩效标准',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
      help: '按计薪出勤比例折算后计入「浮动绩效工资」',
    },
    {
      fieldName: 'bankName',
      label: '开户银行',
      component: 'Input',
src/views/hrm/employee/modules/detail.vue
@@ -64,6 +64,13 @@
      <DescriptionsItem label="转正日期">{{ employee.regularDate || '-' }}</DescriptionsItem>
      <DescriptionsItem label="离职日期">{{ employee.leaveDate || '-' }}</DescriptionsItem>
      <DescriptionsItem label="基本工资">{{ employee.baseSalary ? `¥${employee.baseSalary.toFixed(2)}` : '-' }}</DescriptionsItem>
      <DescriptionsItem label="岗位工资">{{ employee.postSalary ? `¥${employee.postSalary.toFixed(2)}` : '-' }}</DescriptionsItem>
      <DescriptionsItem label="标准工资">
        {{ employee.standardSalary ? `¥${employee.standardSalary.toFixed(2)}` : (employee.baseSalary || employee.postSalary ? `¥${((employee.baseSalary || 0) + (employee.postSalary || 0)).toFixed(2)}(基本+岗位自动合成)` : '-') }}
      </DescriptionsItem>
      <DescriptionsItem label="加班工资标准">{{ employee.overtimeHourlyRate ? `¥${employee.overtimeHourlyRate.toFixed(2)}/小时` : '-' }}</DescriptionsItem>
      <DescriptionsItem label="安全环保奖标准">{{ employee.safetyAwardStandard ? `¥${employee.safetyAwardStandard.toFixed(2)}` : '-' }}</DescriptionsItem>
      <DescriptionsItem label="绩效标准">{{ employee.performanceStandard ? `¥${employee.performanceStandard.toFixed(2)}` : '-' }}</DescriptionsItem>
      <DescriptionsItem label="用户角色" :span="2">
        {{ Array.isArray(employee.roleNames) ? employee.roleNames.join('、') : employee.roleNames || '-' }}
      </DescriptionsItem>
src/views/hrm/salary/calculation/data.ts
@@ -1,3 +1,4 @@
import type { TableColumnsType } from 'ant-design-vue';
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryCalculationApi } from '#/api/hrm/salary/calculation';
@@ -67,29 +68,49 @@
  ];
}
/** 列表的字段 */
/** 列表的字段(按九泓工资表列顺序) */
export function useGridColumns(): VxeTableGridOptions<HrmSalaryCalculationApi.SalaryCalculation>['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { field: 'no', title: '核算单号', minWidth: 140 },
    { field: 'period', title: '核算周期', width: 100 },
    { field: 'userName', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { type: 'checkbox', width: 50, fixed: 'left' },
    { field: 'no', title: '核算单号', width: 150, fixed: 'left' },
    { field: 'period', title: '核算周期', width: 100, fixed: 'left' },
    { field: 'userName', title: '姓名', width: 90, fixed: 'left' },
    { field: 'hireDate', title: '入职日期', width: 110 },
    { field: 'postName', title: '岗位', width: 110 },
    { field: 'deptName', title: '部门', width: 120 },
    { field: 'workDays', title: '标准出勤天数', width: 110 },
    { field: 'paidAttendanceDays', title: '计薪出勤', width: 100, formatter: 'formatAmount2' },
    { field: 'standardSalary', title: '标准工资', width: 100, formatter: 'formatAmount2' },
    { field: 'baseSalary', title: '基本工资', width: 100, formatter: 'formatAmount2' },
    { field: 'performanceSalary', title: '绩效工资', width: 100, formatter: 'formatAmount2' },
    { field: 'overtimePay', title: '加班费', width: 100, formatter: 'formatAmount2' },
    { field: 'mealAllowance', title: '餐补', width: 80, formatter: 'formatAmount2' },
    { field: 'transportAllowance', title: '交通补贴', width: 80, formatter: 'formatAmount2' },
    { field: 'otherAllowance', title: '其他补贴', width: 80, formatter: 'formatAmount2' },
    { field: 'socialSecurityDeduction', title: '社保扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'housingFundDeduction', title: '公积金扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'taxDeduction', title: '个税扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'otherDeduction', title: '其他扣款', width: 80, formatter: 'formatAmount2' },
    { field: 'leaveDeduction', title: '请假扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'actualSalary', title: '实发工资', width: 100, formatter: 'formatAmount2' },
    { field: 'workDays', title: '出勤天数', width: 80 },
    { field: 'overtimeHours', title: '加班时长(h)', width: 100 },
    { field: 'totalWorkHours', title: '排班工时(h)', width: 100 },
    { field: 'postSalary', title: '岗位工资', width: 100, formatter: 'formatAmount2' },
    { field: 'overtimeHourlyRate', title: '加班工资(标准)', width: 120, formatter: 'formatAmount2' },
    { field: 'safetyAwardStandard', title: '安全环保奖(标准)', width: 130, formatter: 'formatAmount2' },
    { field: 'performanceStandard', title: '绩效(标准)', width: 110, formatter: 'formatAmount2' },
    { field: 'standardWorkHours', title: '标准出勤时长', width: 120, formatter: 'formatAmount2' },
    { field: 'overtimeHours', title: '加班时长', width: 100 },
    { field: 'legalHolidayHours', title: '法定假日时长', width: 120 },
    { field: 'standardAttendancePay', title: '标准出勤工资', width: 120, formatter: 'formatAmount2' },
    { field: 'overtimePay', title: '加班工资', width: 100, formatter: 'formatAmount2' },
    { field: 'legalHolidayPay', title: '法定假工资', width: 110, formatter: 'formatAmount2' },
    { field: 'safetyAward', title: '安全环保奖', width: 110, formatter: 'formatAmount2' },
    { field: 'floatingPerformancePay', title: '浮动绩效工资', width: 120, formatter: 'formatAmount2' },
    { field: 'payableSalary', title: '应付工资', width: 100, formatter: 'formatAmount2' },
    { field: 'seniorityPay', title: '工龄工资', width: 100, formatter: 'formatAmount2' },
    { field: 'nightShiftAllowance', title: '夜班补助', width: 100, formatter: 'formatAmount2' },
    { field: 'educationAllowance', title: '学历补贴', width: 100, formatter: 'formatAmount2' },
    { field: 'healthCheckAllowance', title: '体检', width: 90, formatter: 'formatAmount2' },
    { field: 'excellentEmployeeAward', title: '优秀员工奖', width: 110, formatter: 'formatAmount2' },
    { field: 'otherSubsidy', title: '其他补助', width: 100, formatter: 'formatAmount2' },
    { field: 'deptPerformance', title: '部门绩效', width: 100, formatter: 'formatAmount2' },
    { field: 'additionTotal', title: '加项合计', width: 100, formatter: 'formatAmount2' },
    { field: 'missedPunchDeduction', title: '漏打卡', width: 90, formatter: 'formatAmount2' },
    { field: 'lateEarlyDeduction', title: '迟到早退', width: 100, formatter: 'formatAmount2' },
    { field: 'otherDeduction', title: '其他扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'deductionTotal', title: '扣款合计', width: 100, formatter: 'formatAmount2' },
    { field: 'socialSecurityDeduction', title: '社保', width: 100, formatter: 'formatAmount2' },
    { field: 'housingFundDeduction', title: '公积金', width: 100, formatter: 'formatAmount2' },
    { field: 'taxDeduction', title: '个税', width: 100, formatter: 'formatAmount2' },
    { field: 'actualSalary', title: '税后工资', width: 110, formatter: 'formatAmount2', fixed: 'right' },
    {
      field: 'status',
      title: '状态',
@@ -110,25 +131,78 @@
  ];
}
/** 预览表格的列定义(用于Ant Design Table) */
export function usePreviewColumns() {
/** 预览表格里允许手工调整的字段(其余均为自动计算) */
export const PREVIEW_EDITABLE_FIELDS = [
  'floatingPerformancePay',
  'seniorityPay',
  'nightShiftAllowance',
  'educationAllowance',
  'healthCheckAllowance',
  'excellentEmployeeAward',
  'otherSubsidy',
  'deptPerformance',
  'missedPunchDeduction',
  'lateEarlyDeduction',
  'otherDeduction',
] as const;
/** 加项字段(汇总为「加项合计」) */
export const PREVIEW_ADDITION_FIELDS = [
  'seniorityPay',
  'nightShiftAllowance',
  'educationAllowance',
  'healthCheckAllowance',
  'excellentEmployeeAward',
  'otherSubsidy',
  'deptPerformance',
] as const;
/** 扣款字段(汇总为「扣款合计」) */
export const PREVIEW_DEDUCTION_FIELDS = [
  'missedPunchDeduction',
  'lateEarlyDeduction',
  'otherDeduction',
] as const;
/** 预览表格的列定义(用于Ant Design Table,按九泓工资表列顺序) */
export function usePreviewColumns(): TableColumnsType {
  return [
    { title: '员工姓名', dataIndex: 'userName', width: 100, fixed: 'left' },
    { title: '姓名', dataIndex: 'userName', width: 90, fixed: 'left' },
    { title: '入职日期', dataIndex: 'hireDate', width: 110 },
    { title: '岗位', dataIndex: 'postName', width: 110 },
    { title: '部门', dataIndex: 'deptName', width: 120 },
    { title: '基本工资', dataIndex: 'baseSalary', width: 110 },
    { title: '绩效工资', dataIndex: 'performanceSalary', width: 110 },
    { title: '加班费', dataIndex: 'overtimePay', width: 110 },
    { title: '餐补', dataIndex: 'mealAllowance', width: 100 },
    { title: '交通补贴', dataIndex: 'transportAllowance', width: 100 },
    { title: '其他补贴', dataIndex: 'otherAllowance', width: 100 },
    { title: '社保扣款', dataIndex: 'socialSecurityDeduction', width: 110 },
    { title: '公积金扣款', dataIndex: 'housingFundDeduction', width: 110 },
    { title: '个税扣款', dataIndex: 'taxDeduction', width: 110 },
    { title: '其他扣款', dataIndex: 'otherDeduction', width: 100 },
    { title: '请假扣款', dataIndex: 'leaveDeduction', width: 110 },
    { title: '出勤天数', dataIndex: 'workDays', width: 90 },
    { title: '标准出勤天数', dataIndex: 'workDays', width: 110 },
    { title: '计薪出勤', dataIndex: 'paidAttendanceDays', width: 100 },
    { title: '标准工资', dataIndex: 'standardSalary', width: 100 },
    { title: '基本工资', dataIndex: 'baseSalary', width: 100 },
    { title: '岗位工资', dataIndex: 'postSalary', width: 100 },
    { title: '加班工资(标准)', dataIndex: 'overtimeHourlyRate', width: 120 },
    { title: '安全环保奖(标准)', dataIndex: 'safetyAwardStandard', width: 130 },
    { title: '绩效(标准)', dataIndex: 'performanceStandard', width: 110 },
    { title: '标准出勤时长', dataIndex: 'standardWorkHours', width: 120 },
    { title: '加班时长', dataIndex: 'overtimeHours', width: 100 },
    { title: '排班工时(h)', dataIndex: 'totalWorkHours', width: 100 },
    { title: '实发工资', dataIndex: 'actualSalary', width: 100, fixed: 'right' },
    { title: '法定假日时长', dataIndex: 'legalHolidayHours', width: 120 },
    { title: '标准出勤工资', dataIndex: 'standardAttendancePay', width: 120 },
    { title: '加班工资', dataIndex: 'overtimePay', width: 100 },
    { title: '法定假工资', dataIndex: 'legalHolidayPay', width: 110 },
    { title: '安全环保奖', dataIndex: 'safetyAward', width: 110 },
    { title: '浮动绩效工资', dataIndex: 'floatingPerformancePay', width: 120 },
    { title: '应付工资', dataIndex: 'payableSalary', width: 100 },
    { title: '工龄工资', dataIndex: 'seniorityPay', width: 100 },
    { title: '夜班补助', dataIndex: 'nightShiftAllowance', width: 100 },
    { title: '学历补贴', dataIndex: 'educationAllowance', width: 100 },
    { title: '体检', dataIndex: 'healthCheckAllowance', width: 90 },
    { title: '优秀员工奖', dataIndex: 'excellentEmployeeAward', width: 110 },
    { title: '其他补助', dataIndex: 'otherSubsidy', width: 100 },
    { title: '部门绩效', dataIndex: 'deptPerformance', width: 100 },
    { title: '加项合计', dataIndex: 'additionTotal', width: 100 },
    { title: '漏打卡', dataIndex: 'missedPunchDeduction', width: 90 },
    { title: '迟到早退', dataIndex: 'lateEarlyDeduction', width: 100 },
    { title: '其他扣款', dataIndex: 'otherDeduction', width: 100 },
    { title: '扣款合计', dataIndex: 'deductionTotal', width: 100 },
    { title: '社保', dataIndex: 'socialSecurityDeduction', width: 100 },
    { title: '公积金', dataIndex: 'housingFundDeduction', width: 100 },
    { title: '个税', dataIndex: 'taxDeduction', width: 100 },
    { title: '税后工资', dataIndex: 'actualSalary', width: 110, fixed: 'right' },
  ];
}
src/views/hrm/salary/calculation/index.vue
@@ -26,7 +26,14 @@
import { handleTree } from '#/packages/utils/src';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema, usePreviewColumns } from './data';
import {
  PREVIEW_ADDITION_FIELDS,
  PREVIEW_DEDUCTION_FIELDS,
  PREVIEW_EDITABLE_FIELDS,
  useGridColumns,
  useGridFormSchema,
  usePreviewColumns,
} from './data';
// 核算弹窗相关
const calculateVisible = ref(false);
@@ -75,6 +82,8 @@
      period: calculatePeriod.value,
      deptId: calculateDeptId.value,
    });
    // 预览行先按当前口径重算合计,保证弹窗里显示的合计与保存结果一致
    previewData.value.forEach((record) => recalculateRecord(record));
    if (previewData.value.length === 0) {
      message.info('该条件下没有需要核算的员工');
    }
@@ -83,26 +92,89 @@
  }
}
/** 计算实发工资 */
function calculateActualSalary(record: HrmSalaryCalculationApi.SalaryCalculation) {
  const income = (record.baseSalary || 0) +
    (record.performanceSalary || 0) +
    (record.overtimePay || 0) +
    (record.mealAllowance || 0) +
    (record.transportAllowance || 0) +
    (record.otherAllowance || 0);
  const deduction = (record.socialSecurityDeduction || 0) +
    (record.housingFundDeduction || 0) +
    (record.taxDeduction || 0) +
    (record.otherDeduction || 0) +
    (record.leaveDeduction || 0);
  return income - deduction;
/** 预览表中按原样展示的字段(非金额) */
const PLAIN_FIELDS = [
  'userName',
  'hireDate',
  'postName',
  'deptName',
  'workDays',
  'paidAttendanceDays',
  'standardWorkHours',
  'overtimeHours',
  'legalHolidayHours',
];
/** 是否为可手工调整的列 */
function isEditableField(field: string) {
  return (PREVIEW_EDITABLE_FIELDS as readonly string[]).includes(field);
}
/** 更新行数据并计算实发工资 */
function updateFieldValue(record: HrmSalaryCalculationApi.SalaryCalculation, field: keyof HrmSalaryCalculationApi.SalaryCalculation, value: number) {
  (record as any)[field] = value;
  record.actualSalary = calculateActualSalary(record);
/** 读取行字段值 */
function fieldValue(
  record: HrmSalaryCalculationApi.SalaryCalculation,
  field: string,
): number | string | undefined {
  return (record as unknown as Record<string, number | string | undefined>)[
    field
  ];
}
/** 金额展示:保留两位小数 */
function formatAmount(value?: number | string) {
  if (value === null || value === undefined || value === '' || Number.isNaN(Number(value))) {
    return '-';
  }
  return Number(value).toFixed(2);
}
/** 求和(空值按 0 计) */
function sumFields(
  record: HrmSalaryCalculationApi.SalaryCalculation,
  fields: readonly string[],
) {
  return fields.reduce(
    (total, field) =>
      total + Number((record as Record<string, unknown>)[field] ?? 0),
    0,
  );
}
/**
 * 按九泓工资表口径重算:
 * 应付工资 = 标准出勤工资 + 加班工资 + 法定假工资 + 安全环保奖 + 浮动绩效工资
 * 加项合计 = 工龄工资 + 夜班补助 + 学历补贴 + 体检 + 优秀员工奖 + 其他补助 + 部门绩效
 * 扣款合计 = 漏打卡 + 迟到早退 + 其他扣款
 * 税后工资 = 应付工资 + 加项合计 - 扣款合计 - 社保 - 公积金 - 个税
 */
function recalculateRecord(
  record: HrmSalaryCalculationApi.SalaryCalculation,
) {
  record.payableSalary =
    Number(record.standardAttendancePay ?? 0) +
    Number(record.overtimePay ?? 0) +
    Number(record.legalHolidayPay ?? 0) +
    Number(record.safetyAward ?? 0) +
    Number(record.floatingPerformancePay ?? 0);
  record.additionTotal = sumFields(record, PREVIEW_ADDITION_FIELDS);
  record.deductionTotal = sumFields(record, PREVIEW_DEDUCTION_FIELDS);
  record.actualSalary =
    record.payableSalary +
    record.additionTotal -
    record.deductionTotal -
    Number(record.socialSecurityDeduction ?? 0) -
    Number(record.housingFundDeduction ?? 0) -
    Number(record.taxDeduction ?? 0);
}
/** 更新行数据并重算合计 */
function updateFieldValue(
  record: HrmSalaryCalculationApi.SalaryCalculation,
  field: string,
  value: number,
) {
  (record as Record<string, unknown>)[field] = value;
  recalculateRecord(record);
}
/** 提交薪酬核算(手动调整后保存) */
@@ -256,12 +328,12 @@
    <Modal
      v-model:open="calculateVisible"
      title="薪酬核算"
      width="1200px"
      width="95%"
      :footer="null"
      :maskClosable="false"
    >
      <Alert
        message="核算流程:选好核算周期与部门后点「预览数据」→ 核对/调整各项金额 → 点「保存核算」落库"
        message="核算流程:选好核算周期与部门后点「预览数据」→ 核对自动计算结果,可手工调整浮动绩效、加项与扣款 → 点「保存核算」落库。标准工资/岗位工资/加班工资(标准)/安全环保奖(标准)/绩效(标准) 在员工档案中维护。"
        type="info"
        show-icon
        style="margin-bottom: 16px"
@@ -301,144 +373,40 @@
        :columns="usePreviewColumns()"
        :data-source="previewData"
        :pagination="{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `共 ${t} 人` }"
        :scroll="{ x: 1800, y: 400 }"
        :scroll="{ x: 4000, y: 420 }"
        size="small"
        row-key="userId"
        bordered
        class="mb-4"
      >
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'baseSalary'">
          <template v-if="isEditableField(String(column.dataIndex))">
            <InputNumber
              v-model:value="record.baseSalary"
              :value="fieldValue(record, String(column.dataIndex)) as number"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'baseSalary', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'performanceSalary'">
            <InputNumber
              v-model:value="record.performanceSalary"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'performanceSalary', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'overtimePay'">
            <InputNumber
              v-model:value="record.overtimePay"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'overtimePay', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'mealAllowance'">
            <InputNumber
              v-model:value="record.mealAllowance"
              :min="0"
              :precision="2"
              size="small"
              style="width: 80px"
              @change="(val: number) => updateFieldValue(record, 'mealAllowance', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'transportAllowance'">
            <InputNumber
              v-model:value="record.transportAllowance"
              :min="0"
              :precision="2"
              size="small"
              style="width: 80px"
              @change="(val: number) => updateFieldValue(record, 'transportAllowance', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'otherAllowance'">
            <InputNumber
              v-model:value="record.otherAllowance"
              :min="0"
              :precision="2"
              size="small"
              style="width: 80px"
              @change="(val: number) => updateFieldValue(record, 'otherAllowance', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'socialSecurityDeduction'">
            <InputNumber
              v-model:value="record.socialSecurityDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'socialSecurityDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'housingFundDeduction'">
            <InputNumber
              v-model:value="record.housingFundDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'housingFundDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'taxDeduction'">
            <InputNumber
              v-model:value="record.taxDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'taxDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'otherDeduction'">
            <InputNumber
              v-model:value="record.otherDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 80px"
              @change="(val: number) => updateFieldValue(record, 'otherDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'leaveDeduction'">
            <InputNumber
              v-model:value="record.leaveDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'leaveDeduction', val)"
              @change="
                (val: number | string | null) =>
                  updateFieldValue(
                    record,
                    String(column.dataIndex),
                    Number(val ?? 0),
                  )
              "
            />
          </template>
          <template v-else-if="column.dataIndex === 'actualSalary'">
            <span class="font-bold text-green-600">{{ record.actualSalary?.toFixed(2) }}</span>
            <span class="font-bold text-green-600">{{
              formatAmount(record.actualSalary)
            }}</span>
          </template>
          <template v-else-if="column.dataIndex === 'workDays'">
            <InputNumber
              v-model:value="record.workDays"
              :min="0"
              :max="31"
              :precision="0"
              size="small"
              style="width: 70px"
            />
          <template v-else-if="PLAIN_FIELDS.includes(String(column.dataIndex))">
            {{ fieldValue(record, String(column.dataIndex)) ?? '-' }}
          </template>
          <template v-else-if="column.dataIndex === 'overtimeHours'">
            <InputNumber
              v-model:value="record.overtimeHours"
              :min="0"
              :precision="1"
              size="small"
              style="width: 80px"
            />
          <template v-else>
            {{ formatAmount(fieldValue(record, String(column.dataIndex))) }}
          </template>
        </template>
      </Table>