huminmin
2026-07-29 62294084c9c35d35594e9f2d6c41342ae84e9526
src/views/personnelManagement/monthlyStatistics/components/formDia.vue
@@ -48,6 +48,14 @@
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="工作日">
                <el-input v-model.number="monthWorkDays"
                          type="number"
                          placeholder="自动计算"
                          @input="handleMonthWorkDaysInput" />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="选择部门"
                            prop="deptIds">
                <el-select v-model="form.deptIds"
@@ -59,12 +67,14 @@
                  <el-option v-for="item in deptOptions"
                             :key="item.deptId"
                             :label="item.deptName"
                             :value="item.deptId"
                             :disabled="isDeptDisabled(item.deptId)" />
                            :value="item.deptId"
                            :disabled="isDeptDisabled(item.deptId)" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
          </el-row>
          <el-row :gutter="24">
            <el-col :span="12">
              <el-form-item label="备注"
                            prop="remark">
                <el-input v-model="form.remark"
@@ -72,9 +82,7 @@
                          clearable />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="24">
            <el-col :span="6">
            <el-col :span="4">
              <el-form-item label="支付银行"
                            prop="payBank">
                <el-select v-model="form.payBank"
@@ -89,7 +97,7 @@
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
            <el-col :span="8">
              <el-form-item label="审核人"
                            prop="auditUserId">
                <el-select v-model="form.auditUserId"
@@ -128,6 +136,9 @@
          <el-table-column label="员工姓名"
                           prop="staffName"
                           minWidth="100" />
          <el-table-column label="入司日期"
                           prop="entryDate"
                           minWidth="110" />
          <el-table-column label="部门"
                           prop="deptName"
                           minWidth="100" />
@@ -151,27 +162,28 @@
                        disabled />
            </template>
          </el-table-column>
          <el-table-column label="月时间合计"
          <el-table-column label="日计工资"
                           minWidth="110">
            <template #default="{ row }">
              <el-input v-model.number="row.monthTimeTotal"
              <el-input v-model.number="row.dailySalary"
                        type="number"
                        placeholder="0"
                        size="small"
                        disabled />
            </template>
          </el-table-column>
          <el-table-column label="月标准工时"
                           minWidth="110">
          <el-table-column label="出勤天数"
                           minWidth="130">
            <template #default="{ row }">
              <el-input v-model.number="row.standardHours"
              <el-input v-model.number="row.workDays"
                        type="number"
                        placeholder="0"
                        size="small"
                        disabled />
                        style="width: 100%"
                        @input="handleWorkDaysInput(row)" />
            </template>
          </el-table-column>
          <el-table-column label="折合倍数"
          <el-table-column label="效益"
                           minWidth="100">
            <template #default="{ row }">
              <el-input v-model.number="row.conversionRatio"
@@ -181,30 +193,70 @@
                        disabled />
            </template>
          </el-table-column>
          <el-table-column label="计件工资"
          <el-table-column label="月时间合计"
                           minWidth="110">
            <template #default="{ row }">
              <el-input v-model.number="row.pieceSalary"
              <el-input v-model.number="row.monthTimeTotal"
                        type="number"
                        placeholder="0"
                        size="small"
                        @input="row.pieceSalary = parseNum(row.pieceSalary)" />
            </template>
          </el-table-column>
          <el-table-column label="计时工资"
                           minWidth="110">
            <template #default="{ row }">
              <el-input v-model.number="row.hourlySalary"
                        type="number"
                        placeholder="0"
                        size="small"
                        @input="row.hourlySalary = parseNum(row.hourlySalary)" />
                        @input="handlePieceSalaryInput(row)" />
            </template>
          </el-table-column>
          <el-table-column label="其他收入"
                           minWidth="110">
            <template #default="{ row }">
              <el-input v-model.number="row.otherIncome"
                        type="number"
                        placeholder="0"
                        size="small"
                        @input="handleSalaryInput(row)" />
            </template>
          </el-table-column>
          <el-table-column label="事假"
                           minWidth="90">
            <template #default="{ row }">
              <el-input v-model.number="row.personalLeave"
                        type="number"
                        placeholder="0"
                        size="small"
                        @input="handleLeaveInput(row)" />
            </template>
          </el-table-column>
          <el-table-column label="病假"
                           minWidth="90">
            <template #default="{ row }">
              <el-input v-model.number="row.sickLeave"
                        type="number"
                        placeholder="0"
                        size="small"
                        @input="handleLeaveInput(row)" />
            </template>
          </el-table-column>
          <el-table-column label="丧假"
                           minWidth="90">
            <template #default="{ row }">
              <el-input v-model.number="row.bereavementLeave"
                        type="number"
                        placeholder="0"
                        size="small"
                        @input="handleLeaveInput(row)" />
            </template>
          </el-table-column>
          <el-table-column label="婚假"
                           minWidth="90">
            <template #default="{ row }">
              <el-input v-model.number="row.marriageLeave"
                        type="number"
                        placeholder="0"
                        size="small"
                        @input="handleLeaveInput(row)" />
            </template>
          </el-table-column>
          <el-table-column label="津贴"
                           minWidth="110">
            <template #default="{ row }">
              <el-input v-model.number="row.allowance"
                        type="number"
                        placeholder="0"
                        size="small"
@@ -456,16 +508,20 @@
    deptName: "",
    basicSalary: 0,
    attendanceBonus: 100,
    pieceSalary: 0,
    entryDate: "",
    workDays: 0,
    dailySalary: 0,
    hourlySalary: 0,
    actualOutput: 0,
    standardQuantity: 0,
    conversionFactor: 1,
    convertedWorkHours: 0,
    monthTimeTotal: 0,
    standardHours: 0,
    conversionRatio: 0,
    otherIncome: 0,
    personalLeave: 0,
    sickLeave: 0,
    bereavementLeave: 0,
    marriageLeave: 0,
    allowance: 0,
    socialPersonal: 0,
    fundPersonal: 0,
    otherDeduct: 0,
@@ -488,16 +544,20 @@
    deptName: source?.deptName ?? fallback.deptName ?? "",
    basicSalary: parseNum(source?.basicSalary ?? fallback.basicSalary),
    attendanceBonus: parseNum(source?.attendanceBonus ?? fallback.attendanceBonus ?? 100) || 100,
    pieceSalary: parseNum(source?.pieceSalary ?? fallback.pieceSalary),
    entryDate: source?.entryDate ?? source?.contractStartTime ?? fallback.entryDate ?? "",
    workDays: parseNum(source?.workDays ?? fallback.workDays),
    dailySalary: parseNum(source?.dailySalary ?? fallback.dailySalary),
    hourlySalary: parseNum(source?.hourlySalary ?? fallback.hourlySalary),
    actualOutput: parseNum(source?.actualOutput ?? fallback.actualOutput),
    standardQuantity: parseNum(source?.standardQuantity ?? fallback.standardQuantity),
    conversionFactor: parseNum(source?.conversionFactor ?? fallback.conversionFactor) || 1,
    convertedWorkHours: parseNum(source?.convertedWorkHours ?? fallback.convertedWorkHours),
    monthTimeTotal: parseNum(source?.monthTimeTotal ?? fallback.monthTimeTotal),
    standardHours: parseNum(source?.standardHours ?? fallback.standardHours),
    conversionRatio: parseNum(source?.conversionRatio ?? fallback.conversionRatio),
    otherIncome: parseNum(source?.otherIncome ?? fallback.otherIncome),
    personalLeave: parseNum(source?.personalLeave ?? fallback.personalLeave),
    sickLeave: parseNum(source?.sickLeave ?? fallback.sickLeave),
    bereavementLeave: parseNum(source?.bereavementLeave ?? fallback.bereavementLeave),
    marriageLeave: parseNum(source?.marriageLeave ?? fallback.marriageLeave),
    allowance: parseNum(source?.allowance ?? fallback.allowance),
    socialPersonal: parseNum(source?.socialPersonal ?? fallback.socialPersonal),
    fundPersonal: parseNum(source?.fundPersonal ?? fallback.fundPersonal),
    otherDeduct: parseNum(source?.otherDeduct ?? fallback.otherDeduct),
@@ -510,8 +570,35 @@
    remark: source?.remark ?? fallback.remark ?? "",
  });
  const applyBackendRow = (targetRow, sourceRow) => {
  const recalculateRowByWorkDays = row => {
    const workDayCount = parseNum(monthWorkDays.value);
    const monthTimeTotal = parseNum(row.monthTimeTotal);
    const standardHours = workDayCount > 0 ? workDayCount * 8 : 0;
    if (workDayCount > 0 && parseNum(row.workDays) > workDayCount) {
      row.workDays = workDayCount;
    }
    if (standardHours > 0) {
      row.conversionRatio = (monthTimeTotal / standardHours).toFixed(4);
    } else {
      row.conversionRatio = 0;
    }
  };
  const applyBackendRow = (targetRow, sourceRow, options = {}) => {
    const preservedConversionRatio = options.preserveConversionRatio
      ? parseNum(targetRow.conversionRatio)
      : null;
    Object.assign(targetRow, normalizeEmployeeRow(sourceRow, targetRow));
    if (options.preserveConversionRatio) {
      targetRow.conversionRatio = preservedConversionRatio;
    }
    recalculateRowByWorkDays(targetRow);
  };
  const recalcRowsByCurrentWorkDays = () => {
    employeeList.value.forEach(row => {
      recalculateRowByWorkDays(row);
    });
  };
  const clearRowRecalcState = row => {
@@ -528,30 +615,36 @@
    recalcVersions.clear();
  };
  const requestEmployeeRecalculate = row => {
  const requestEmployeeRecalculate = (row, options = {}) => {
    const key = String(row?.staffOnJobId ?? row?.id ?? "");
    if (!key || !form.value.salaryMonth) return;
    const version = (recalcVersions.get(key) || 0) + 1;
    recalcVersions.set(key, version);
    const manualWorkDays = parseNum(row.workDays);
    clearTimeout(recalcTimers.get(key));
      const payload = {
      staffOnJobId: row.staffOnJobId ?? row.id,
      salaryMonth: form.value.salaryMonth,
      monthWorkDays: parseNum(monthWorkDays.value),
      staffName: row.staffName ?? "",
      postName: row.postName ?? "",
      deptName: row.deptName ?? "",
      entryDate: row.entryDate ?? row.contractStartTime ?? "",
      basicSalary: parseNum(row.basicSalary),
      attendanceBonus: parseNum(row.attendanceBonus),
      pieceSalary: parseNum(row.pieceSalary),
      workDays: parseNum(row.workDays),
      dailySalary: parseNum(row.dailySalary),
      hourlySalary: parseNum(row.hourlySalary),
      actualOutput: parseNum(row.actualOutput),
      standardQuantity: parseNum(row.standardQuantity),
      conversionFactor: parseNum(row.conversionFactor) || 1,
      convertedWorkHours: parseNum(row.convertedWorkHours),
      monthTimeTotal: parseNum(row.monthTimeTotal),
      standardHours: parseNum(row.standardHours),
      conversionRatio: parseNum(row.conversionRatio),
      otherIncome: parseNum(row.otherIncome),
      personalLeave: parseNum(row.personalLeave),
      sickLeave: parseNum(row.sickLeave),
      bereavementLeave: parseNum(row.bereavementLeave),
      marriageLeave: parseNum(row.marriageLeave),
      allowance: parseNum(row.allowance),
      otherDeduct: parseNum(row.otherDeduct),
      socialSupplementAmount: parseNullableNum(row.socialSupplementAmount),
      remark: row.remark ?? "",
@@ -561,7 +654,12 @@
      setTimeout(() => {
        staffSalaryMainCalculateByEmployeeId(payload).then(res => {
          if (recalcVersions.get(key) !== version) return;
          applyBackendRow(row, res?.data || {});
          applyBackendRow(row, res?.data || {}, options);
          if (manualWorkDays > 0) {
            row.workDays = manualWorkDays;
            row.dailySalary = manualWorkDays > 0 ? parseNum(row.basicSalary) / manualWorkDays : 0;
            recalculateRowByWorkDays(row);
          }
        });
      }, 250)
    );
@@ -572,8 +670,81 @@
    row.otherIncome = parseNum(row.otherIncome);
    row.actualOutput = parseNum(row.actualOutput);
    row.standardQuantity = parseNum(row.standardQuantity);
    row.conversionFactor = parseNum(row.conversionFactor) || 1;
    row.workDays = parseNum(row.workDays);
    row.dailySalary = row.workDays > 0
      ? (parseNum(row.basicSalary) / row.workDays).toFixed(3)
      : 0;
    requestEmployeeRecalculate(row, { preserveConversionRatio: true });
  };
  const handlePieceSalaryInput = row => {
    row.monthTimeTotal = parseNum(row.monthTimeTotal);
    requestEmployeeRecalculate(row);
  };
  const handleWorkDaysInput = row => {
    row.workDays = parseNum(row.workDays);
    const maxWorkDays = parseNum(monthWorkDays.value);
    if (maxWorkDays > 0 && row.workDays > maxWorkDays) {
      row.workDays = maxWorkDays;
      proxy.$modal.msgWarning("出勤天数不能大于工作日");
    }
    enforceLeaveAndWorkDayLimit(row);
    row.dailySalary = row.workDays > 0
      ? (parseNum(row.basicSalary) / row.workDays).toFixed(3)
      : 0;
    requestEmployeeRecalculate(row);
  };
  const handleLeaveInput = row => {
    row.personalLeave = parseNum(row.personalLeave);
    row.sickLeave = parseNum(row.sickLeave);
    row.bereavementLeave = parseNum(row.bereavementLeave);
    row.marriageLeave = parseNum(row.marriageLeave);
    enforceLeaveAndWorkDayLimit(row);
    requestEmployeeRecalculate(row);
  };
  const enforceLeaveAndWorkDayLimit = row => {
    const maxWorkDays = parseNum(monthWorkDays.value);
    if (maxWorkDays <= 0) {
      return true;
    }
    const leaveTotal = parseNum(row.personalLeave)
      + parseNum(row.sickLeave)
      + parseNum(row.bereavementLeave)
      + parseNum(row.marriageLeave)
      + parseNum(row.workDays);
    if (leaveTotal > maxWorkDays) {
      proxy.$modal.msgWarning("请假合计加出勤天数不能超过工作日");
      const allowedWorkDays = Math.max(0, maxWorkDays
        - parseNum(row.personalLeave)
        - parseNum(row.sickLeave)
        - parseNum(row.bereavementLeave)
        - parseNum(row.marriageLeave));
      row.workDays = allowedWorkDays;
      return false;
    }
    return true;
  };
  const handleMonthWorkDaysInput = () => {
    monthWorkDays.value = parseNum(monthWorkDays.value);
    employeeList.value.forEach(row => {
      const standardHours = monthWorkDays.value > 0 ? monthWorkDays.value * 8 : 0;
      if (parseNum(row.workDays) > monthWorkDays.value) {
        row.workDays = monthWorkDays.value;
        row.dailySalary = monthWorkDays.value > 0
          ? (parseNum(row.basicSalary) / monthWorkDays.value).toFixed(3)
          : 0;
      }
      if (parseNum(row.monthTimeTotal) > 0 && standardHours > 0) {
        row.conversionRatio = (parseNum(row.monthTimeTotal) / standardHours).toFixed(4);
      } else {
        row.conversionRatio = 0;
      }
      requestEmployeeRecalculate(row);
    });
  };
  // 基础资料表单
@@ -606,6 +777,26 @@
  const totalSalary = computed(() => {
    return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0);
  });
  const monthWorkDays = ref(0);
  const calculateMonthWorkDays = salaryMonth => {
    if (!salaryMonth) return 0;
    const [yearStr, monthStr] = String(salaryMonth).split("-");
    const year = Number(yearStr);
    const month = Number(monthStr);
    if (!year || !month) return 0;
    const start = new Date(year, month - 1, 1);
    const end = new Date(year, month, 0);
    let workDays = 0;
    for (let day = new Date(start); day <= end; day.setDate(day.getDate() + 1)) {
      const weekday = day.getDay();
      if (weekday !== 0 && weekday !== 6) {
        workDays += 1;
      }
    }
    return workDays;
  };
  // 根据审核人ID获取审核人名称
  const auditUserName = computed(() => {
@@ -712,6 +903,7 @@
      loadUserList();
      loadAllDeptOptions();
      employeeList.value = [];
      monthWorkDays.value = 0;
      Object.assign(form.value, {
        id: undefined,
        salaryTitle: "",
@@ -736,10 +928,19 @@
        form.value.remark = row.remark ?? "";
        form.value.payBank = row.payBank ?? "";
        form.value.auditUserId = row.auditUserId ?? undefined;
        // 如果有员工明细数据,直接反显
        if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) {
          employeeList.value = row.staffSalaryDetailList.map(e => normalizeEmployeeRow(e));
        monthWorkDays.value = parseNum(row.monthWorkDays) || calculateMonthWorkDays(form.value.salaryMonth);
      // 如果有员工明细数据,直接反显
      if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) {
          employeeList.value = row.staffSalaryDetailList.map(e => {
            const normalized = normalizeEmployeeRow(e);
            recalculateRowByWorkDays(normalized);
            return normalized;
          });
        }
        recalcRowsByCurrentWorkDays();
      } else if (form.value.salaryMonth) {
        monthWorkDays.value = calculateMonthWorkDays(form.value.salaryMonth);
        recalcRowsByCurrentWorkDays();
      }
      // 编辑状态且有月份时,加载可用部门列表
      if (type === "edit" && form.value.salaryMonth) {
@@ -753,6 +954,10 @@
    // 切换月份时清空已选择的部门
    if (newMonth !== oldMonth) {
      form.value.deptIds = [];
    }
    if (!monthWorkDays.value || newMonth !== oldMonth) {
      monthWorkDays.value = calculateMonthWorkDays(newMonth);
      recalcRowsByCurrentWorkDays();
    }
    loadAvailableDepts(newMonth);
  });
@@ -786,6 +991,7 @@
        staffName: node.label,
        postName: node.postName ?? node.post ?? "",
        deptName: node.deptName ?? "",
        entryDate: node.contractStartTime ?? node.entryDate ?? "",
        remark: "",
        socialSupplementAmount: null,
        attendanceBonus: node.attendanceBonus,
@@ -873,16 +1079,30 @@
  };
  const saveData = status => {
    const payload = {
      id: form.value.id,
      salaryTitle: form.value.salaryTitle,
      deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "",
      salaryMonth: form.value.salaryMonth,
      remark: form.value.remark,
      payBank: form.value.payBank,
      auditUserId: form.value.auditUserId,
      auditUserName: auditUserName.value,
      totalSalary: totalSalary.value,
      const invalidRow = employeeList.value.find(row => {
        const maxWorkDays = parseNum(monthWorkDays.value);
        const leaveTotal = parseNum(row.personalLeave)
          + parseNum(row.sickLeave)
          + parseNum(row.bereavementLeave)
          + parseNum(row.marriageLeave)
          + parseNum(row.workDays);
        return maxWorkDays > 0 && leaveTotal > maxWorkDays;
      });
      if (invalidRow) {
        proxy.$modal.msgWarning("请假合计加出勤天数不能超过工作日");
        return;
      }
      const payload = {
        id: form.value.id,
        salaryTitle: form.value.salaryTitle,
        deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "",
        salaryMonth: form.value.salaryMonth,
        monthWorkDays: parseNum(monthWorkDays.value),
        remark: form.value.remark,
        payBank: form.value.payBank,
        auditUserId: form.value.auditUserId,
        auditUserName: auditUserName.value,
        totalSalary: totalSalary.value,
      staffSalaryDetailList: employeeList.value.map(e => ({
        staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
        staffName: e.staffName,
@@ -890,14 +1110,17 @@
        deptName: e.deptName ?? "",
        basicSalary: parseNum(e.basicSalary),
        attendanceBonus: parseNum(e.attendanceBonus),
        pieceSalary: parseNum(e.pieceSalary),
        dailySalary: parseNum(e.dailySalary),
        workDays: parseNum(e.workDays),
        personalLeave: parseNum(e.personalLeave),
        sickLeave: parseNum(e.sickLeave),
        bereavementLeave: parseNum(e.bereavementLeave),
        marriageLeave: parseNum(e.marriageLeave),
        allowance: parseNum(e.allowance),
        hourlySalary: parseNum(e.hourlySalary),
        actualOutput: parseNum(e.actualOutput),
        standardQuantity: parseNum(e.standardQuantity),
        conversionFactor: parseNum(e.conversionFactor) || 1,
        convertedWorkHours: parseNum(e.convertedWorkHours),
        monthTimeTotal: parseNum(e.monthTimeTotal),
        standardHours: parseNum(e.standardHours),
        conversionRatio: parseNum(e.conversionRatio),
        otherIncome: parseNum(e.otherIncome),
        socialPersonal: parseNum(e.socialPersonal),