zhangwencui
16 小时以前 d1b00f612f24bbd547e2bd3cad877d55602cd087
个人缴费比例限制输入
已修改1个文件
784 ■■■■ 文件已修改
src/views/personnelManagement/socialSecuritySet/components/formDia.vue 784 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/components/formDia.vue
@@ -1,34 +1,31 @@
<template>
  <div>
    <FormDialog
      v-model="dialogFormVisible"
      :operation-type="operationType"
      :title="dialogTitle"
      width="80%"
      @close="closeDia"
      @confirm="submitForm"
      @cancel="closeDia"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
    <FormDialog v-model="dialogFormVisible"
                :operation-type="operationType"
                :title="dialogTitle"
                width="80%"
                @close="closeDia"
                @confirm="submitForm"
                @cancel="closeDia">
      <el-form ref="formRef"
               :model="form"
               :rules="rules"
               label-position="top">
        <el-row :gutter="24">
          <!-- 左侧:适用人员 -->
          <el-col :span="8">
            <el-form-item label="适用人员:" prop="deptIds">
            <el-form-item label="适用人员:"
                          prop="deptIds">
              <div class="dept-checkbox-wrap">
                <el-checkbox-group
                  v-model="form.deptIds"
                  :disabled="isDetail"
                >
                  <div
                    v-for="dept in deptList"
                    :key="dept.deptId"
                    class="dept-checkbox-item"
                  >
                <el-checkbox-group v-model="form.deptIds"
                                   :disabled="isDetail">
                  <div v-for="dept in deptList"
                       :key="dept.deptId"
                       class="dept-checkbox-item">
                    <el-checkbox :value="dept.deptId">
                      {{ dept.deptName }}
                      <span v-if="dept.personCount != null" class="dept-count"
                        >{{ dept.personCount }}人</span
                      >
                      <span v-if="dept.personCount != null"
                            class="dept-count">{{ dept.personCount }}人</span>
                    </el-checkbox>
                  </div>
                </el-checkbox-group>
@@ -38,124 +35,111 @@
          <!-- 右侧:基础信息 + 保险类型 -->
          <el-col :span="16">
            <!-- 基础信息 -->
            <el-card class="form-card" shadow="never">
            <el-card class="form-card"
                     shadow="never">
              <template #header>
                <span class="card-title"><span class="card-title-line">|</span> 基础信息</span>
                <el-icon class="card-collapse"><ArrowUp /></el-icon>
                <el-icon class="card-collapse">
                  <ArrowUp />
                </el-icon>
              </template>
              <el-form-item label="方案标题:" prop="title">
                <el-input
                  v-model="form.title"
                  placeholder="请输入"
                  clearable
                  :disabled="isDetail"
                />
              <el-form-item label="方案标题:"
                            prop="title">
                <el-input v-model="form.title"
                          placeholder="请输入"
                          clearable
                          :disabled="isDetail" />
              </el-form-item>
              <el-form-item label="备注:" prop="remark">
                <el-input
                  v-model="form.remark"
                  type="textarea"
                  :rows="2"
                  placeholder="请输入"
                  clearable
                  :disabled="isDetail"
                />
              <el-form-item label="备注:"
                            prop="remark">
                <el-input v-model="form.remark"
                          type="textarea"
                          :rows="2"
                          placeholder="请输入"
                          clearable
                          :disabled="isDetail" />
              </el-form-item>
            </el-card>
            <!-- 保险类型 -->
            <el-card class="form-card" shadow="never">
            <el-card class="form-card"
                     shadow="never">
              <template #header>
                <span class="card-title"><span class="card-title-line">|</span> 保险类型</span>
                <el-button
                  v-if="!isDetail"
                  type="primary"
                  size="small"
                  @click="addInsuranceBenefit"
                >
                <el-button v-if="!isDetail"
                           type="primary"
                           size="small"
                           @click="addInsuranceBenefit">
                  添加保险福利
                </el-button>
              </template>
              <el-row :gutter="16">
                <el-col
                  v-for="(item, index) in form.insuranceBenefits"
                  :key="item._key"
                  :span="12"
                >
                <el-col v-for="(item, index) in form.insuranceBenefits"
                        :key="item._key"
                        :span="12">
                  <div class="insurance-benefit-card">
                    <div class="insurance-benefit-title">
                      保险福利{{ index + 1 }}
                      <el-button
                        v-if="!isDetail && form.insuranceBenefits.length > 1"
                        type="danger"
                        link
                        size="small"
                        class="card-delete-btn"
                        @click="removeInsuranceBenefit(index)"
                      >
                      <el-button v-if="!isDetail && form.insuranceBenefits.length > 1"
                                 type="danger"
                                 link
                                 size="small"
                                 class="card-delete-btn"
                                 @click="removeInsuranceBenefit(index)">
                        删除
                      </el-button>
                    </div>
                    <el-form-item
                      :prop="'insuranceBenefits.' + index + '.insuranceType'"
                      label="保险类型:"
                      label-width="100px"
                    >
                      <el-select
                        v-model="item.insuranceType"
                        placeholder="请选择"
                        clearable
                        style="width: 100%"
                        :disabled="isDetail"
                      >
                        <el-option
                          v-for="opt in insuranceTypeOptions"
                          :key="opt.value"
                          :label="opt.label"
                          :value="opt.value"
                        />
                    <el-form-item :prop="'insuranceBenefits.' + index + '.insuranceType'"
                                  label="保险类型:"
                                  label-width="100px">
                      <el-select v-model="item.insuranceType"
                                 placeholder="请选择"
                                 clearable
                                 style="width: 100%"
                                 :disabled="isDetail">
                        <el-option v-for="opt in insuranceTypeOptions"
                                   :key="opt.value"
                                   :label="opt.label"
                                   :value="opt.value" />
                      </el-select>
                    </el-form-item>
                    <el-form-item label="缴费基数:" label-width="100px">
                    <el-form-item label="缴费基数:"
                                  label-width="100px">
                      <div class="base-salary-wrap">
                        <el-input
                          v-model="item.paymentBase"
                          placeholder="根据基本工资缴纳"
                          clearable
                          style="width: 120px"
                          type="number"
                          :disabled="isDetail || item.useBasicSalary"
                          @input="handlePaymentBaseInput(item)"
                        />
                        <el-checkbox
                          v-model="item.useBasicSalary"
                          @change="handleUseBasicSalaryChange(item)"
                          :disabled="isDetail"
                        >
                        <el-input v-model="item.paymentBase"
                                  placeholder="根据基本工资缴纳"
                                  clearable
                                  style="width: 120px"
                                  type="number"
                                  :disabled="isDetail || item.useBasicSalary"
                                  @input="handlePaymentBaseInput(item)" />
                        <el-checkbox v-model="item.useBasicSalary"
                                     @change="handleUseBasicSalaryChange(item)"
                                     :disabled="isDetail">
                          调用基本工资
                        </el-checkbox>
                      </div>
                    </el-form-item>
                    <el-form-item label="个人缴费比例:" label-width="100px">
                    <el-form-item label="个人缴费比例:"
                                  label-width="100px">
                      <div class="personal-ratio-wrap">
                        <el-input
                          v-model="item.personalRatio"
                          placeholder="请输入"
                          clearable
                          style="width: 100px"
                          type="number"
                          :disabled="isDetail"
                        />
                        <el-input v-model="item.personalRatio"
                                  placeholder="请输入"
                                  clearable
                                  style="width: 100px"
                                  type="number"
                                  :disabled="isDetail"
                                  :min="0"
                                  @input="handlePersonalRatioInput(item)" />
                        <span class="ratio-unit">(%)</span>
                        <span class="ratio-plus">+</span>
                        <el-input
                          v-model="item.personalFixed"
                          placeholder="请输入"
                          clearable
                          style="width: 100px"
                          type="number"
                          :disabled="isDetail"
                        />
                        <el-input v-model="item.personalFixed"
                                  placeholder="请输入"
                                  clearable
                                  style="width: 100px"
                                  type="number"
                                  :disabled="isDetail"
                                  :min="0"
                                  @input="handlePersonalFixedInput(item)" />
                      </div>
                    </el-form-item>
                  </div>
@@ -170,301 +154,327 @@
</template>
<script setup>
import { ref, reactive, toRefs, getCurrentInstance, nextTick, computed } from "vue";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ArrowUp } from "@element-plus/icons-vue";
import { listDept } from "@/api/system/dept.js";
import { socialSecurityAdd, socialSecurityUpdate } from "@/api/personnelManagement/socialSecuritySet.js";
  import {
    ref,
    reactive,
    toRefs,
    getCurrentInstance,
    nextTick,
    computed,
  } from "vue";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import { ArrowUp } from "@element-plus/icons-vue";
  import { listDept } from "@/api/system/dept.js";
  import {
    socialSecurityAdd,
    socialSecurityUpdate,
  } from "@/api/personnelManagement/socialSecuritySet.js";
const emit = defineEmits(["close"]);
const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
  const { proxy } = getCurrentInstance();
const dialogFormVisible = ref(false);
const operationType = ref("add");
const formRef = ref(null);
const deptList = ref([]);
  const dialogFormVisible = ref(false);
  const operationType = ref("add");
  const formRef = ref(null);
  const deptList = ref([]);
const isDetail = computed(() => operationType.value === "detail");
  const isDetail = computed(() => operationType.value === "detail");
const dialogTitle = () =>
  operationType.value === "add"
    ? "新增方案"
    : operationType.value === "edit"
    ? "编辑方案"
    : "方案详情";
  const dialogTitle = () =>
    operationType.value === "add"
      ? "新增方案"
      : operationType.value === "edit"
      ? "编辑方案"
      : "方案详情";
// 保险类型选项(可按字典替换)
const insuranceTypeOptions = [
  { label: "养老保险", value: "养老保险" },
  { label: "医疗保险", value: "医疗保险" },
  { label: "失业保险", value: "失业保险" },
  { label: "工伤保险", value: "工伤保险" },
  { label: "生育保险", value: "生育保险" },
  { label: "公积金", value: "公积金" },
];
  // 保险类型选项(可按字典替换)
  const insuranceTypeOptions = [
    { label: "养老保险", value: "养老保险" },
    { label: "医疗保险", value: "医疗保险" },
    { label: "失业保险", value: "失业保险" },
    { label: "工伤保险", value: "工伤保险" },
    { label: "生育保险", value: "生育保险" },
    { label: "公积金", value: "公积金" },
  ];
const defaultBenefit = () => ({
  _key: Math.random().toString(36).slice(2),
  insuranceType: "",
  paymentBase: "",
  useBasicSalary: false,
  personalRatio: "",
  personalFixed: "",
});
  const defaultBenefit = () => ({
    _key: Math.random().toString(36).slice(2),
    insuranceType: "",
    paymentBase: "",
    useBasicSalary: false,
    personalRatio: "",
    personalFixed: "",
  });
const data = reactive({
  form: {
    id: undefined,
    title: "",
    remark: "",
    deptIds: [],
    insuranceBenefits: [defaultBenefit()],
  },
  rules: {
    title: [{ required: true, message: "请输入方案标题", trigger: "blur" }],
    deptIds: [
      {
        required: true,
        type: "array",
        min: 1,
        message: "请至少选择一个适用部门",
        trigger: "change",
      },
    ],
  },
});
const { form, rules } = toRefs(data);
  const data = reactive({
    form: {
      id: undefined,
      title: "",
      remark: "",
      deptIds: [],
      insuranceBenefits: [defaultBenefit()],
    },
    rules: {
      title: [{ required: true, message: "请输入方案标题", trigger: "blur" }],
      deptIds: [
        {
          required: true,
          type: "array",
          min: 1,
          message: "请至少选择一个适用部门",
          trigger: "change",
        },
      ],
    },
  });
  const { form, rules } = toRefs(data);
function flattenDept(tree, list = []) {
  if (!tree || !tree.length) return list;
  tree.forEach((node) => {
    list.push({
      deptId: node.deptId,
      deptName: node.deptName,
      personCount: node.personCount ?? null,
  function flattenDept(tree, list = []) {
    if (!tree || !tree.length) return list;
    tree.forEach(node => {
      list.push({
        deptId: node.deptId,
        deptName: node.deptName,
        personCount: node.personCount ?? null,
      });
      if (node.children && node.children.length) {
        flattenDept(node.children, list);
      }
    });
    if (node.children && node.children.length) {
      flattenDept(node.children, list);
    }
  });
  return list;
}
const loadDeptList = () => {
  listDept().then((res) => {
    const tree = res.data ?? [];
    deptList.value = flattenDept(tree);
  });
};
const addInsuranceBenefit = () => {
  form.value.insuranceBenefits.push(defaultBenefit());
};
const removeInsuranceBenefit = (index) => {
  form.value.insuranceBenefits.splice(index, 1);
};
const handleUseBasicSalaryChange = (item) => {
  if (item.useBasicSalary) {
    item.paymentBase = "";
    return list;
  }
};
const handlePaymentBaseInput = (item) => {
  if (item.paymentBase !== "" && item.paymentBase != null) {
    item.useBasicSalary = false;
  }
};
const resetForm = () => {
  form.value = {
    id: undefined,
    title: "",
    remark: "",
    deptIds: [],
    insuranceBenefits: [defaultBenefit()],
  const loadDeptList = () => {
    listDept().then(res => {
      const tree = res.data ?? [];
      deptList.value = flattenDept(tree);
    });
  };
};
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  loadDeptList();
  resetForm();
  if ((type === "edit" || type === "detail") && row) {
    const d = row || {};
    form.value.id = d.id;
    form.value.title = d.title;
    form.value.remark = d.remark ?? "";
    // deptIds 后端可能是逗号分隔字符串或数组,这里统一转为数组并尽量还原数值类型
    if (d.deptIds) {
      form.value.deptIds = String(d.deptIds)
        .split(",")
        .filter((v) => v !== "")
        .map((v) => {
          const num = Number(v);
          return Number.isNaN(num) ? v : num;
        });
    } else {
      form.value.deptIds = [];
  const addInsuranceBenefit = () => {
    form.value.insuranceBenefits.push(defaultBenefit());
  };
  const removeInsuranceBenefit = index => {
    form.value.insuranceBenefits.splice(index, 1);
  };
  const handleUseBasicSalaryChange = item => {
    if (item.useBasicSalary) {
      item.paymentBase = "";
    }
    const detailList = d.schemeInsuranceDetailList || [];
    form.value.insuranceBenefits =
      detailList.length > 0
        ? detailList.map((b) => ({
            _key: Math.random().toString(36).slice(2),
            insuranceType: b.insuranceType || "",
            paymentBase: b.paymentBase ?? "",
            useBasicSalary: b.useBasicSalary === 2,
            personalRatio: b.personalRatio ?? "",
            personalFixed: b.personalFixed ?? "",
          }))
        : [defaultBenefit()];
  }
};
  };
const submitForm = () => {
  // 详情模式下不提交,只关闭弹窗
  if (operationType.value === "detail") {
    closeDia();
    return;
  }
  formRef.value?.validate((valid) => {
    if (!valid) return;
    const deptIds =
      Array.isArray(form.value.deptIds) && form.value.deptIds.length
        ? form.value.deptIds.join(",")
        : "";
    const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map(
      ({ _key, ...rest }) => ({
        ...rest,
        useBasicSalary: rest.useBasicSalary ? 2 : 1,
      })
    );
    const insuranceTypes = schemeInsuranceDetailList
      .map((item) => item.insuranceType)
      .filter((v) => v)
      .join(",");
    // 部门名称,多个使用逗号隔开(根据选中的 deptIds 与 deptList 计算)
    const deptNames = (deptList.value || [])
      .filter((d) =>
        (form.value.deptIds || []).some(
          (id) => String(id) === String(d.deptId)
        )
      )
      .map((d) => d.deptName)
      .join(",");
    const submitData = {
      id: form.value.id,
      title: form.value.title,
      remark: form.value.remark ?? "",
      deptIds,
      insuranceTypes,
      deptNames,
      schemeInsuranceDetailList,
  const handlePaymentBaseInput = item => {
    if (item.paymentBase !== "" && item.paymentBase != null) {
      item.useBasicSalary = false;
    }
  };
  const handlePersonalRatioInput = item => {
    if (item.personalRatio !== "" && item.personalRatio != null) {
      const value = Number(item.personalRatio);
      if (value < 0) {
        item.personalRatio = "";
      }
    }
  };
  const handlePersonalFixedInput = item => {
    if (item.personalFixed !== "" && item.personalFixed != null) {
      const value = Number(item.personalFixed);
      if (value < 0) {
        item.personalFixed = "";
      }
    }
  };
  const resetForm = () => {
    form.value = {
      id: undefined,
      title: "",
      remark: "",
      deptIds: [],
      insuranceBenefits: [defaultBenefit()],
    };
    if (operationType.value === "add") {
      socialSecurityAdd(submitData).then(() => {
        proxy.$modal.msgSuccess("新增成功");
        closeDia();
      });
    } else {
      socialSecurityUpdate(submitData).then(() => {
        proxy.$modal.msgSuccess("修改成功");
        closeDia();
      });
  };
  const openDialog = (type, row) => {
    operationType.value = type;
    dialogFormVisible.value = true;
    loadDeptList();
    resetForm();
    if ((type === "edit" || type === "detail") && row) {
      const d = row || {};
      form.value.id = d.id;
      form.value.title = d.title;
      form.value.remark = d.remark ?? "";
      // deptIds 后端可能是逗号分隔字符串或数组,这里统一转为数组并尽量还原数值类型
      if (d.deptIds) {
        form.value.deptIds = String(d.deptIds)
          .split(",")
          .filter(v => v !== "")
          .map(v => {
            const num = Number(v);
            return Number.isNaN(num) ? v : num;
          });
      } else {
        form.value.deptIds = [];
      }
      const detailList = d.schemeInsuranceDetailList || [];
      form.value.insuranceBenefits =
        detailList.length > 0
          ? detailList.map(b => ({
              _key: Math.random().toString(36).slice(2),
              insuranceType: b.insuranceType || "",
              paymentBase: b.paymentBase ?? "",
              useBasicSalary: b.useBasicSalary === 2,
              personalRatio: b.personalRatio ?? "",
              personalFixed: b.personalFixed ?? "",
            }))
          : [defaultBenefit()];
    }
  });
};
  };
const closeDia = () => {
  proxy.resetForm?.("formRef");
  dialogFormVisible.value = false;
  emit("close");
};
  const submitForm = () => {
    // 详情模式下不提交,只关闭弹窗
    if (operationType.value === "detail") {
      closeDia();
      return;
    }
    formRef.value?.validate(valid => {
      if (!valid) return;
      const deptIds =
        Array.isArray(form.value.deptIds) && form.value.deptIds.length
          ? form.value.deptIds.join(",")
          : "";
      const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map(
        ({ _key, ...rest }) => ({
          ...rest,
          useBasicSalary: rest.useBasicSalary ? 2 : 1,
        })
      );
      const insuranceTypes = schemeInsuranceDetailList
        .map(item => item.insuranceType)
        .filter(v => v)
        .join(",");
      // 部门名称,多个使用逗号隔开(根据选中的 deptIds 与 deptList 计算)
      const deptNames = (deptList.value || [])
        .filter(d =>
          (form.value.deptIds || []).some(id => String(id) === String(d.deptId))
        )
        .map(d => d.deptName)
        .join(",");
      const submitData = {
        id: form.value.id,
        title: form.value.title,
        remark: form.value.remark ?? "",
        deptIds,
        insuranceTypes,
        deptNames,
        schemeInsuranceDetailList,
      };
      if (operationType.value === "add") {
        socialSecurityAdd(submitData).then(() => {
          proxy.$modal.msgSuccess("新增成功");
          closeDia();
        });
      } else {
        socialSecurityUpdate(submitData).then(() => {
          proxy.$modal.msgSuccess("修改成功");
          closeDia();
        });
      }
    });
  };
defineExpose({ openDialog });
  const closeDia = () => {
    proxy.resetForm?.("formRef");
    dialogFormVisible.value = false;
    emit("close");
  };
  defineExpose({ openDialog });
</script>
<style scoped>
.card-title-line {
  color: #f56c6c;
  margin-right: 4px;
}
.form-card {
  margin-bottom: 16px;
}
.form-card :deep(.el-card__header) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
}
.card-title {
  font-weight: 500;
}
.card-collapse {
  color: #999;
  cursor: pointer;
}
.dept-checkbox-wrap {
  max-height: 320px;
  overflow-y: auto;
  padding: 8px 0;
  border: 1px solid var(--el-border-color);
  border-radius: 4px;
  background: #fff;
}
.dept-checkbox-item {
  padding: 6px 12px;
}
.dept-count {
  color: #909399;
  font-size: 12px;
  margin-left: 4px;
}
.insurance-benefit-card {
  border: 1px solid var(--el-border-color-lighter);
  border-radius: 4px;
  padding: 12px 16px;
  margin-bottom: 12px;
  background: #fafafa;
}
.insurance-benefit-title {
  font-size: 14px;
  margin-bottom: 12px;
  font-weight: 500;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.card-delete-btn {
  margin-left: auto;
}
.checkbox-group-inline {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}
.base-salary-wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.base-salary-text {
  color: #606266;
  font-size: 14px;
}
.personal-ratio-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
}
.ratio-unit,
.ratio-plus {
  color: #606266;
  font-size: 14px;
}
  .card-title-line {
    color: #f56c6c;
    margin-right: 4px;
  }
  .form-card {
    margin-bottom: 16px;
  }
  .form-card :deep(.el-card__header) {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
  }
  .card-title {
    font-weight: 500;
  }
  .card-collapse {
    color: #999;
    cursor: pointer;
  }
  .dept-checkbox-wrap {
    max-height: 320px;
    overflow-y: auto;
    padding: 8px 0;
    border: 1px solid var(--el-border-color);
    border-radius: 4px;
    background: #fff;
  }
  .dept-checkbox-item {
    padding: 6px 12px;
  }
  .dept-count {
    color: #909399;
    font-size: 12px;
    margin-left: 4px;
  }
  .insurance-benefit-card {
    border: 1px solid var(--el-border-color-lighter);
    border-radius: 4px;
    padding: 12px 16px;
    margin-bottom: 12px;
    background: #fafafa;
  }
  .insurance-benefit-title {
    font-size: 14px;
    margin-bottom: 12px;
    font-weight: 500;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .card-delete-btn {
    margin-left: auto;
  }
  .checkbox-group-inline {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
  }
  .base-salary-wrap {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
  }
  .base-salary-text {
    color: #606266;
    font-size: 14px;
  }
  .personal-ratio-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .ratio-unit,
  .ratio-plus {
    color: #606266;
    font-size: 14px;
  }
</style>