huminmin
6 天以前 8e4840cbd4e026e783c9a9b1b8aefe0f43c67a29
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
已添加6个文件
已修改12个文件
2255 ■■■■ 文件已修改
src/api/personnelManagement/bank.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/socialSecuritySet.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/staffSalaryMain.js 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/projectManagement/projectType.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/FormDialog.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue 74 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/index.vue 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/components/formDia.vue 186 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/index.vue 266 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/components/formDia.vue 166 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/index.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/components/ProjectTypeDialog.vue 385 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/index.vue 462 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/bank.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
import request from "@/utils/request";
// é“¶è¡Œç®¡ç†
export function bankList() {
  return request({
    url: "/bank/list",
    method: "get",
  });
}
export function bankAdd(data) {
  return request({
    url: "/bank/add",
    method: "post",
    data,
  });
}
export function bankUpdate(data) {
  return request({
    url: "/bank/update",
    method: "post",
    data,
  });
}
export function bankDelete(ids) {
  return request({
    url: "/bank/delete",
    method: "delete",
    data: ids,
  });
}
src/api/personnelManagement/socialSecuritySet.js
@@ -4,7 +4,7 @@
// åˆ†é¡µæŸ¥è¯¢åˆ—表
export function socialSecurityListPage(query) {
  return request({
    url: "/socialSecurity/plan/listPage",
    url: "/schemeApplicableStaff/listPage",
    method: "get",
    params: query,
  });
@@ -13,7 +13,7 @@
// æŸ¥è¯¢è¯¦æƒ…
export function socialSecurityInfo(id) {
  return request({
    url: "/socialSecurity/plan/" + id,
    url: "/schemeApplicableStaff/" + id,
    method: "get",
  });
}
@@ -21,7 +21,7 @@
// æ–°å¢ž
export function socialSecurityAdd(data) {
  return request({
    url: "/socialSecurity/plan/add",
    url: "/schemeApplicableStaff/add",
    method: "post",
    data,
  });
@@ -30,7 +30,7 @@
// ä¿®æ”¹
export function socialSecurityUpdate(data) {
  return request({
    url: "/socialSecurity/plan/update",
    url: "/schemeApplicableStaff/updateSchemeApplicableStaff",
    method: "post",
    data,
  });
@@ -39,7 +39,7 @@
// åˆ é™¤
export function socialSecurityDelete(ids) {
  return request({
    url: "/socialSecurity/plan/delete",
    url: "/schemeApplicableStaff/delete",
    method: "delete",
    data: ids,
  });
src/api/personnelManagement/staffSalaryMain.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
import request from "@/utils/request";
// å‘˜å·¥å·¥èµ„主表
export function staffSalaryMainListPage(params) {
  return request({
    url: "/staffSalaryMain/listPage",
    method: "get",
    params,
  });
}
export function staffSalaryMainCalculateSalary(ids) {
  return request({
    url: "/staffSalaryMain/calculateSalary",
    method: "post",
    data: ids,
  });
}
export function staffSalaryMainAdd(data) {
  return request({
    url: "/staffSalaryMain/add",
    method: "post",
    data,
  });
}
export function staffSalaryMainUpdate(data) {
  return request({
    url: "/staffSalaryMain/update",
    method: "post",
    data,
  });
}
export function staffSalaryMainDelete(ids) {
  return request({
    url: "/staffSalaryMain/delete",
    method: "delete",
    data: ids,
  });
}
src/api/projectManagement/projectType.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import request from '@/utils/request'
// æŸ¥è¯¢é¡¹ç›®ç±»åž‹åˆ—表
export function listPlan(data) {
  return request({
    url: '/projectManagement/plan/listPage',
    method: 'post',
    data: data
  })
}
// ä¿å­˜é¡¹ç›®ç±»åž‹ï¼ˆæ–°å¢ž/修改)
export function savePlan(data) {
  return request({
    url: '/projectManagement/plan/save',
    method: 'post',
    data: data
  })
}
// åˆ é™¤é¡¹ç›®ç±»åž‹
export function deletePlan(id) {
  return request({
    url: `/projectManagement/plan/delete/${id}`,
    method: 'post'
  })
}
src/components/Dialog/FormDialog.vue
@@ -8,7 +8,13 @@
    <slot></slot>
    <template #footer>
      <div class="dialog-footer">
        <el-button type="primary" @click="handleConfirm">确认</el-button>
        <el-button
          v-if="showConfirm"
          type="primary"
          @click="handleConfirm"
        >
          ç¡®è®¤
        </el-button>
        <el-button @click="handleCancel">取消</el-button>
      </div>
    </template>
@@ -44,6 +50,9 @@
  set: (val) => emit('update:modelValue', val)
})
// è¯¦æƒ…模式不展示“确认”按钮,其它类型正常显示
const showConfirm = computed(() => props.operationType !== 'detail')
const computedTitle = computed(() => {
  if (typeof props.title === 'function') {
    return props.title(props.operationType)
src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue
@@ -32,9 +32,9 @@
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="别名" prop="aliasName">
        <el-form-item label="别名" prop="alias">
          <el-input
            v-model="form.aliasName"
            v-model="form.alias"
            placeholder="请输入"
            clearable
            maxlength="50"
@@ -135,10 +135,9 @@
    <el-row :gutter="24">
      <el-col :span="10">
        <el-form-item label="角色" prop="roleIds">
        <el-form-item label="角色" prop="roleId">
          <el-select
            v-model="form.roleIds"
            multiple
            v-model="form.roleId"
            placeholder="请选择"
            clearable
            style="width: 100%"
src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue
@@ -8,11 +8,11 @@
          æ•™è‚²ç»åކ
        </span>
      </template>
      <el-table :data="form.educationList" border>
        <el-table-column label="学历" prop="degree" width="120">
      <el-table :data="form.staffEducationList" border>
        <el-table-column label="学历" prop="education" width="120">
          <template #default="{ row }">
            <el-select
              v-model="row.degree"
              v-model="row.education"
              placeholder="请选择"
              clearable
              style="width: 100%"
@@ -25,10 +25,10 @@
            </el-select>
          </template>
        </el-table-column>
        <el-table-column label="毕业院校" prop="school" min-width="160">
        <el-table-column label="毕业院校" prop="schoolName" min-width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.school"
              v-model="row.schoolName"
              placeholder="请输入"
              clearable
              maxlength="30"
@@ -36,10 +36,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="入学时间" prop="admissionDate" width="150">
        <el-table-column label="入学时间" prop="enrollTime" width="150">
          <template #default="{ row }">
            <el-date-picker
              v-model="row.admissionDate"
              v-model="row.enrollTime"
              type="date"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
@@ -49,10 +49,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="毕业时间" prop="graduationDate" width="150">
        <el-table-column label="毕业时间" prop="graduateTime" width="150">
          <template #default="{ row }">
            <el-date-picker
              v-model="row.graduationDate"
              v-model="row.graduateTime"
              type="date"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
@@ -73,10 +73,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="学位" prop="academicDegree" width="140">
        <el-table-column label="学位" prop="degree" width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.academicDegree"
              v-model="row.degree"
              placeholder="请输入"
              clearable
              maxlength="20"
@@ -87,7 +87,7 @@
        <el-table-column label="操作" width="80" align="center">
          <template #default="scope">
            <el-button
              v-if="form.educationList.length > 1"
              v-if="form.staffEducationList.length > 1"
              type="primary"
              link
              @click="removeEducationRow(scope.$index)"
@@ -108,11 +108,11 @@
          å·¥ä½œç»åކ
        </span>
      </template>
      <el-table :data="form.workExperienceList" border>
        <el-table-column label="前公司" prop="company" min-width="180">
      <el-table :data="form.staffWorkExperienceList" border>
        <el-table-column label="前公司" prop="formerCompany" min-width="180">
          <template #default="{ row }">
            <el-input
              v-model="row.company"
              v-model="row.formerCompany"
              placeholder="请输入"
              clearable
              maxlength="30"
@@ -120,10 +120,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="前公司部门" prop="department" min-width="140">
        <el-table-column label="前公司部门" prop="formerDept" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.department"
              v-model="row.formerDept"
              placeholder="请输入"
              clearable
              maxlength="20"
@@ -131,10 +131,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="前公司职位" prop="position" min-width="140">
        <el-table-column label="前公司职位" prop="formerPosition" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.position"
              v-model="row.formerPosition"
              placeholder="请输入"
              clearable
              maxlength="20"
@@ -168,10 +168,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="工作描述" prop="description" min-width="220">
        <el-table-column label="工作描述" prop="workDesc" min-width="220">
          <template #default="{ row }">
            <el-input
              v-model="row.description"
              v-model="row.workDesc"
              type="textarea"
              :rows="2"
              placeholder="请输入"
@@ -184,7 +184,7 @@
        <el-table-column label="操作" width="80" align="center">
          <template #default="scope">
            <el-button
              v-if="form.workExperienceList.length > 1"
              v-if="form.staffWorkExperienceList.length > 1"
              type="primary"
              link
              @click="removeWorkRow(scope.$index)"
@@ -211,35 +211,35 @@
const { form } = toRefs(props);
const addEducationRow = () => {
  form.value.educationList.push({
    degree: "",
    school: "",
    admissionDate: "",
    graduationDate: "",
  form.value.staffEducationList.push({
    education: "",
    schoolName: "",
    enrollTime: "",
    graduateTime: "",
    major: "",
    academicDegree: "",
    degree: "",
  });
};
const removeEducationRow = (index) => {
  if (form.value.educationList.length <= 1) return;
  form.value.educationList.splice(index, 1);
  if (form.value.staffEducationList.length <= 1) return;
  form.value.staffEducationList.splice(index, 1);
};
const addWorkRow = () => {
  form.value.workExperienceList.push({
    company: "",
    department: "",
    position: "",
  form.value.staffWorkExperienceList.push({
    formerCompany: "",
    formerDept: "",
    formerPosition: "",
    startDate: "",
    endDate: "",
    description: "",
    workDesc: "",
  });
};
const removeWorkRow = (index) => {
  if (form.value.workExperienceList.length <= 1) return;
  form.value.workExperienceList.splice(index, 1);
  if (form.value.staffWorkExperienceList.length <= 1) return;
  form.value.staffWorkExperienceList.splice(index, 1);
};
</script>
src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue
@@ -8,11 +8,11 @@
          ç´§æ€¥è”系人
        </span>
      </template>
      <el-table :data="form.emergencyContacts" border>
        <el-table-column label="紧急联系人姓名" prop="name" min-width="160">
      <el-table :data="form.staffEmergencyContactList" border>
        <el-table-column label="紧急联系人姓名" prop="contactName" min-width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.name"
              v-model="row.contactName"
              placeholder="请输入"
              clearable
              maxlength="50"
@@ -20,10 +20,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人关系" prop="relation" min-width="140">
        <el-table-column label="紧急联系人关系" prop="contactRelation" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.relation"
              v-model="row.contactRelation"
              placeholder="请输入"
              clearable
              maxlength="20"
@@ -31,10 +31,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人手机" prop="phone" width="160">
        <el-table-column label="紧急联系人手机" prop="contactPhone" width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.phone"
              v-model="row.contactPhone"
              placeholder="请输入"
              clearable
              maxlength="11"
@@ -42,10 +42,10 @@
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人住址" prop="address" min-width="220">
        <el-table-column label="紧急联系人住址" prop="contactAddress" min-width="220">
          <template #default="{ row }">
            <el-input
              v-model="row.address"
              v-model="row.contactAddress"
              placeholder="请输入"
              clearable
              maxlength="50"
@@ -56,7 +56,7 @@
        <el-table-column label="操作" width="80" align="center">
          <template #default="scope">
            <el-button
              v-if="form.emergencyContacts.length > 1"
              v-if="form.staffEmergencyContactList.length > 1"
              type="primary"
              link
              @click="removeEmergencyRow(scope.$index)"
@@ -108,17 +108,17 @@
const { form } = toRefs(props);
const addEmergencyRow = () => {
  form.value.emergencyContacts.push({
    name: "",
    relation: "",
    phone: "",
    address: "",
  form.value.staffEmergencyContactList.push({
    contactName: "",
    contactRelation: "",
    contactPhone: "",
    contactAddress: "",
  });
};
const removeEmergencyRow = (index) => {
  if (form.value.emergencyContacts.length <= 1) return;
  form.value.emergencyContacts.splice(index, 1);
  if (form.value.staffEmergencyContactList.length <= 1) return;
  form.value.staffEmergencyContactList.splice(index, 1);
};
</script>
src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue
@@ -7,38 +7,12 @@
      </span>
    </template>
    <!-- ç¬¬ä¸€è¡Œï¼šåˆåŒå¼€å§‹ / åˆåŒç»“束 / è¯•用期 / è½¬æ­£ -->
    <el-row :gutter="24">
      <el-col :span="5">
        <el-form-item label="工号" prop="jobNo">
          <el-input
            v-model="form.jobNo"
            placeholder="请输入"
            clearable
            maxlength="20"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="员工类型" prop="staffType">
          <el-select
            v-model="form.staffType"
            placeholder="请选择"
            clearable
            style="width: 100%"
          >
            <el-option label="正式员工" value="official" />
            <el-option label="试用员工" value="probation" />
            <el-option label="实习生" value="intern" />
            <el-option label="兼职" value="part_time" />
            <el-option label="劳务/外包" value="outsourcing" />
          </el-select>
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="入职日期" prop="entryDate">
      <el-col :span="6">
        <el-form-item label="入职日期" prop="contractStartTime">
          <el-date-picker
            v-model="form.entryDate"
            v-model="form.contractStartTime"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
@@ -48,10 +22,34 @@
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
      <el-col :span="6">
        <el-form-item
          label="合同结束日期"
          prop="contractEndTime"
          required
          :rules="[
            {
              required: true,
              message: '请选择合同结束日期',
              trigger: 'change',
            },
          ]"
        >
          <el-date-picker
            v-model="form.contractEndTime"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="请选择"
            style="width: 100%"
            clearable
          />
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="试用期(月)" prop="probationPeriod">
          <el-input-number
            v-model="form.probationPeriod"
            v-model="form.proTerm"
            :min="0"
            :max="24"
            :precision="0"
@@ -60,10 +58,10 @@
          />
        </el-form-item>
      </el-col>
      <el-col :span="4">
        <el-form-item label="转正日期" prop="regularDate">
      <el-col :span="6">
        <el-form-item label="转正日期" prop="positiveDate">
          <el-date-picker
            v-model="form.regularDate"
            v-model="form.positiveDate"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
@@ -75,6 +73,7 @@
      </el-col>
    </el-row>
    <!-- ç¬¬äºŒè¡Œï¼šéƒ¨é—¨ / å²—位 / åŸºæœ¬å·¥èµ„ -->
    <el-row :gutter="24">
      <el-col :span="8">
        <el-form-item label="部门" prop="sysDeptId">
@@ -85,17 +84,6 @@
            :render-after-expand="false"
            placeholder="请选择"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col :span="8">
        <el-form-item label="直接上级" prop="directLeader">
          <el-input
            v-model="form.directLeader"
            placeholder="请输入"
            clearable
            maxlength="50"
            show-word-limit
          />
        </el-form-item>
      </el-col>
@@ -117,21 +105,7 @@
          </el-select>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="24">
      <el-col :span="5">
        <el-form-item label="职级" prop="jobLevel">
          <el-input
            v-model="form.jobLevel"
            placeholder="请输入"
            clearable
            maxlength="10"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="4">
      <el-col :span="8">
        <el-form-item label="基本工资" prop="basicSalary">
          <el-input-number
            v-model="form.basicSalary"
src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue
@@ -67,28 +67,28 @@
  operationType.value === "add" ? "新增入职" : "编辑人员";
const createEmptyEducation = () => ({
  degree: "",
  school: "",
  admissionDate: "",
  graduationDate: "",
  education: "",
  schoolName: "",
  enrollTime: "",
  graduateTime: "",
  major: "",
  academicDegree: "",
  degree: "",
});
const createEmptyWork = () => ({
  company: "",
  department: "",
  position: "",
  formerCompany: "",
  formerDept: "",
  formerPosition: "",
  startDate: "",
  endDate: "",
  description: "",
  workDesc: "",
});
const createEmptyEmergency = () => ({
  name: "",
  relation: "",
  phone: "",
  address: "",
  contactName: "",
  contactRelation: "",
  contactPhone: "",
  contactAddress: "",
});
const createDefaultForm = () => ({
@@ -96,7 +96,7 @@
  // åŸºæœ¬ä¿¡æ¯
  staffNo: "",
  staffName: "",
  aliasName: "",
  alias: "",
  phone: "",
  sex: "",
  birthDate: "",
@@ -112,29 +112,24 @@
  email: "",
  currentAddress: "",
  // åœ¨èŒä¿¡æ¯
  jobNo: "",
  staffType: "",
  entryDate: "",
  probationPeriod: undefined,
  regularDate: "",
  contractStartTime: "",
  contractEndTime: "",
  proTerm: undefined,
  positiveDate: "",
  sysDeptId: undefined,
  directLeader: "",
  sysPostId: undefined,
  jobLevel: "",
  basicSalary: undefined,
  // é“¶è¡Œå¡ä¿¡æ¯
  bankName: "",
  bankCardNo: "",
  // æ•™è‚²ç»åކ
  educationList: [createEmptyEducation()],
  staffEducationList: [createEmptyEducation()],
  // å·¥ä½œç»åކ
  workExperienceList: [createEmptyWork()],
  staffWorkExperienceList: [createEmptyWork()],
  // ç´§æ€¥è”系人
  emergencyContacts: [createEmptyEmergency()],
  emergencyContact: "",
  emergencyContactPhone: "",
  // è§’色
  roleIds: [],
  staffEmergencyContactList: [createEmptyEmergency()],
  // è§’色(单选)
  roleId: undefined,
  // ææ–™é™„件(仅前端展示)
  attachments: [],
});
@@ -149,25 +144,16 @@
    birthDate: [
      { required: true, message: "请选择出生日期", trigger: "change" },
    ],
    jobNo: [{ required: true, message: "请输入工号", trigger: "blur" }],
    staffType: [
      { required: true, message: "请选择员工类型", trigger: "change" },
    ],
    entryDate: [
    contractStartTime: [
      { required: true, message: "请选择入职日期", trigger: "change" },
    ],
    contractEndTime: [
      { required: true, message: "请选择合同结束日期", trigger: "change" },
    ],
    sysDeptId: [
      { required: true, message: "请选择部门", trigger: "change" },
    ],
    roleIds: [
      {
        required: true,
        type: "array",
        min: 1,
        message: "请至少选择一个角色",
        trigger: "change",
      },
    ],
    roleId: [{ required: true, message: "请选择角色", trigger: "change" }],
  },
  postOptions: [],
  deptOptions: [],
@@ -215,12 +201,6 @@
  });
}
const syncEmergencyToLegacyField = () => {
  const first = form.value.emergencyContacts?.[0];
  form.value.emergencyContact = first?.name || "";
  form.value.emergencyContactPhone = first?.phone || "";
};
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
@@ -236,20 +216,23 @@
        ...form.value,
        ...d,
      });
      if (!Array.isArray(form.value.educationList) || !form.value.educationList.length) {
        form.value.educationList = [createEmptyEducation()];
      if (
        !Array.isArray(form.value.staffEducationList) ||
        !form.value.staffEducationList.length
      ) {
        form.value.staffEducationList = [createEmptyEducation()];
      }
      if (
        !Array.isArray(form.value.workExperienceList) ||
        !form.value.workExperienceList.length
        !Array.isArray(form.value.staffWorkExperienceList) ||
        !form.value.staffWorkExperienceList.length
      ) {
        form.value.workExperienceList = [createEmptyWork()];
        form.value.staffWorkExperienceList = [createEmptyWork()];
      }
      if (
        !Array.isArray(form.value.emergencyContacts) ||
        !form.value.emergencyContacts.length
        !Array.isArray(form.value.staffEmergencyContactList) ||
        !form.value.staffEmergencyContactList.length
      ) {
        form.value.emergencyContacts = [createEmptyEmergency()];
        form.value.staffEmergencyContactList = [createEmptyEmergency()];
      }
      if (form.value.sysPostId === 0) {
        form.value.sysPostId = undefined;
@@ -273,7 +256,8 @@
  if (!form.value.sysDeptId) {
    form.value.sysDeptId = undefined;
  }
  syncEmergencyToLegacyField();
  // å…¼å®¹åŽç«¯å¯èƒ½ä»ä½¿ç”¨ roleIds æ•°ç»„
  form.value.roleIds = form.value.roleId ? [form.value.roleId] : [];
  formRef.value?.validate((valid) => {
    if (valid) {
      if (operationType.value === "add") {
src/views/personnelManagement/employeeRecord/index.vue
@@ -102,67 +102,40 @@
    prop: "staffName",
  },
  {
    label: "别名",
    prop: "alias",
  },
  {
    label: "手机",
    prop: "phone",
    width: 150,
  },
  {
    label: "性别",
    prop: "sex",
  },
  {
    label: "户籍住址",
    prop: "nativePlace",
  },
  {
    label: "部门",
    prop: "deptName",
  },
  {
    label: "岗位",
    prop: "postJob",
  },
  {
    label: "现住址",
    prop: "adress",
    width:200
  },
  {
    label: "第一学历",
    prop: "firstStudy",
  },
  {
    label: "专业",
    prop: "profession",
    width:100
    label: "出生日期",
    prop: "birthDate",
    width: 120,
  },
  {
    label: "年龄",
    prop: "age",
  },
  {
    label: "联系电话",
    prop: "phone",
    width:150
    label: "籍贯",
    prop: "nativePlace",
  },
  {
    label: "紧急联系人",
    prop: "emergencyContact",
    width: 120
    label: "民族",
    prop: "nation",
    width: 100,
  },
  {
    label: "紧急联系人电话",
    prop: "emergencyContactPhone",
    width:150
  },
  // {
  //   label: "合同年限",
  //   prop: "contractTerm",
  // },
  // {
  //   label: "合同开始日期",
  //   prop: "contractStartTime",
  //   width: 120
  // },
  {
    label: "合同结束日期",
    prop: "contractExpireTime",
    width: 120
    label: "婚姻状况",
    prop: "maritalStatus",
    width: 100,
  },
  {
    dataType: "action",
src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
<template>
  <FormDialog
    v-model="dialogVisible"
    operation-type="edit"
    title="设置发放银行下拉数据"
    width="640px"
    @close="handleClose"
    @confirm="handleConfirm"
    @cancel="handleCancel"
  >
    <el-form ref="formRef" :model="form" label-position="top">
      <el-row :gutter="16">
        <el-col :span="24" style="display: flex; justify-content: end; gap: 10px;margin-bottom: 10px">
          <el-button type="primary" @click="addBank">新增银行</el-button>
          <el-button @click="resetToEmpty">清空</el-button>
        </el-col>
      </el-row>
      <el-table :data="form.banks" border style="width: 100%">
        <el-table-column label="银行名称" min-width="260">
          <template #default="{ row }">
            <el-input
              v-model="row.bankName"
              placeholder="例如:中国工商银行"
              clearable
              maxlength="50"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="90" align="center">
          <template #default="{ $index }">
            <el-button type="danger" link @click="removeBank($index)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div style="margin-top: 10px; color: #909399; font-size: 12px">
        æç¤ºï¼šè¿™é‡Œç»´æŠ¤çš„æ˜¯â€œå‘放银行”下拉框选项数据;保存后在新建/编辑工资表中可选择。
      </div>
    </el-form>
  </FormDialog>
</template>
<script setup>
import { computed, reactive, ref, toRefs, watch, getCurrentInstance } from "vue";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { bankAdd, bankDelete, bankList, bankUpdate } from "@/api/personnelManagement/bank.js";
const emit = defineEmits(["update:modelValue", "close", "saved"]);
const props = defineProps({
  modelValue: { type: Boolean, default: false },
});
const { proxy } = getCurrentInstance();
const dialogVisible = computed({
  get: () => props.modelValue,
  set: (val) => emit("update:modelValue", val),
});
const formRef = ref(null);
const data = reactive({
  form: {
    banks: [],
  },
});
const { form } = toRefs(data);
function newKey() {
  return Math.random().toString(36).slice(2);
}
const addBank = () => {
  form.value.banks.push({
    _key: newKey(),
    id: undefined,
    bankName: "",
    _originBankName: "",
  });
};
const removeBank = (index) => {
  const row = form.value.banks?.[index];
  if (!row) return;
  // æœªè½åº“的行:直接移除
  if (!row.id) {
    form.value.banks.splice(index, 1);
    return;
  }
  // å·²è½åº“:调用后端删除
  bankDelete([row.id]).then(() => {
    proxy?.$modal?.msgSuccess?.("删除成功");
    form.value.banks.splice(index, 1);
    emit("saved");
  });
};
const resetToEmpty = () => {
  if (!form.value.banks?.length) return;
  const ids = form.value.banks.map((b) => b?.id).filter(Boolean);
  // è‹¥å…¨éƒ¨æ˜¯æœªä¿å­˜è¡Œï¼Œåˆ™ä»…清空本地
  if (!ids.length) {
    form.value.banks = [];
    return;
  }
  proxy?.$modal
    ?.confirm?.("确定清空所有银行吗?")
    .then(() => bankDelete(ids))
    .then(() => {
      proxy?.$modal?.msgSuccess?.("清空成功");
      form.value.banks = [];
      emit("saved");
    })
    .catch(() => {});
};
const loadSetting = () => {
  return bankList().then((res) => {
    const list = Array.isArray(res?.data) ? res.data : [];
    form.value.banks = list.map((b) => ({
      _key: newKey(),
      id: b?.id,
      bankName: b?.bankName ?? "",
      _originBankName: b?.bankName ?? "",
    }));
  });
};
const openDialog = () => {
  loadSetting();
};
watch(
  () => dialogVisible.value,
  (val) => {
    if (val) openDialog();
  }
);
const handleConfirm = () => {
  const names = (form.value.banks || [])
    .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
    .filter((n) => n !== "");
  const unique = Array.from(new Set(names));
  if (!unique.length) {
    proxy?.$modal?.msgWarning?.("请至少新增一个银行选项");
    return;
  }
  if (unique.length !== names.length) {
    proxy?.$modal?.msgWarning?.("银行名称不可重复");
    return;
  }
  const rows = form.value.banks.map((b) => ({
    ...b,
    bankName: b?.bankName == null ? "" : String(b.bankName).trim(),
  }));
  const toAdd = rows.filter((b) => !b.id && b.bankName);
  const toUpdate = rows.filter((b) => b.id && b.bankName && b.bankName !== (b._originBankName ?? ""));
  Promise.all([
    ...toAdd.map((b) => bankAdd({ bankName: b.bankName })),
    ...toUpdate.map((b) => bankUpdate({ id: b.id, bankName: b.bankName })),
  ])
    .then(() => loadSetting())
    .then(() => {
      proxy?.$modal?.msgSuccess?.("保存成功");
      dialogVisible.value = false;
      emit("saved", { options: unique });
    });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
const handleClose = () => {
  emit("close");
};
defineExpose({ openDialog });
</script>
<style scoped></style>
src/views/personnelManagement/monthlyStatistics/components/formDia.vue
@@ -17,9 +17,9 @@
        <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
          <el-row :gutter="24">
            <el-col :span="6">
              <el-form-item label="工资主题" prop="title">
              <el-form-item label="工资主题" prop="salaryTitle">
                <el-input
                  v-model="form.title"
                  v-model="form.salaryTitle"
                  placeholder="请输入"
                  clearable
                  maxlength="20"
@@ -45,9 +45,9 @@
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="选择工资月份" prop="payMonth">
              <el-form-item label="选择工资月份" prop="salaryMonth">
                <el-date-picker
                  v-model="form.payMonth"
                  v-model="form.salaryMonth"
                  type="month"
                  value-format="YYYY-MM"
                  format="YYYY-MM"
@@ -64,6 +64,26 @@
                  placeholder="请输入"
                  clearable
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="24">
            <el-col :span="6">
              <el-form-item label="支付银行" prop="payBank">
                <el-select
                  v-model="form.payBank"
                  placeholder="请选择"
                  clearable
                  filterable
                  style="width: 100%"
                >
                  <el-option
                    v-for="b in bankOptions"
                    :key="b"
                    :label="b"
                    :value="b"
                  />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
@@ -92,7 +112,6 @@
        >
          <el-table-column type="selection" width="55" align="center" />
          <el-table-column label="员工姓名" prop="staffName" minWidth="100" />
          <el-table-column label="角色" prop="roleName" minWidth="100" />
          <el-table-column label="部门" prop="deptName" minWidth="100" />
          <el-table-column label="基本工资" minWidth="110">
            <template #default="{ row }">
@@ -108,11 +127,11 @@
          <el-table-column label="计件工资" minWidth="110">
            <template #default="{ row }">
              <el-input
                v-model.number="row.pieceworkSalary"
                v-model.number="row.pieceSalary"
                type="number"
                placeholder="0"
                size="small"
                @input="row.pieceworkSalary = parseNum(row.pieceworkSalary)"
                @input="row.pieceSalary = parseNum(row.pieceSalary)"
              />
            </template>
          </el-table-column>
@@ -141,22 +160,22 @@
          <el-table-column label="社保个人" minWidth="110">
            <template #default="{ row }">
              <el-input
                v-model.number="row.socialSecurityIndividuals"
                v-model.number="row.socialPersonal"
                type="number"
                placeholder="0"
                size="small"
                @input="row.socialSecurityIndividuals = parseNum(row.socialSecurityIndividuals)"
                @input="row.socialPersonal = parseNum(row.socialPersonal)"
              />
            </template>
          </el-table-column>
          <el-table-column label="公积金个人" minWidth="120">
            <template #default="{ row }">
              <el-input
                v-model.number="row.providentFundIndividuals"
                v-model.number="row.fundPersonal"
                type="number"
                placeholder="0"
                size="small"
                @input="row.providentFundIndividuals = parseNum(row.providentFundIndividuals)"
                @input="row.fundPersonal = parseNum(row.fundPersonal)"
              />
            </template>
          </el-table-column>
@@ -238,11 +257,12 @@
import { ArrowUp } from "@element-plus/icons-vue";
import { listDept } from "@/api/system/dept.js";
import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js";
import { bankList } from "@/api/personnelManagement/bank.js";
import {
  monthlyStatisticsAdd,
  monthlyStatisticsUpdate,
  monthlyStatisticsGet,
} from "@/api/personnelManagement/monthlyStatistics.js";
  staffSalaryMainAdd,
  staffSalaryMainUpdate,
  staffSalaryMainCalculateSalary,
} from "@/api/personnelManagement/staffSalaryMain.js";
const emit = defineEmits(["update:modelValue", "close"]);
const props = defineProps({
@@ -267,6 +287,7 @@
const deptStaffTree = ref([]);
const employeeList = ref([]);
const selectedEmployees = ref([]);
const bankOptions = ref([]);
const taxTableData = ref([
  { level: 1, range: "不超过36000元", rate: 3, quickDeduction: 0 },
  { level: 2, range: "超过36000-144000元", rate: 10, quickDeduction: 2520 },
@@ -287,18 +308,28 @@
const data = reactive({
  form: {
    id: undefined,
    title: "",
    salaryTitle: "",
    deptId: undefined,
    payMonth: "",
    salaryMonth: "",
    remark: "",
    payBank: "",
  },
  rules: {
    title: [{ required: true, message: "请输入工资主题", trigger: "blur" }],
    salaryTitle: [{ required: true, message: "请输入工资主题", trigger: "blur" }],
    deptId: [{ required: true, message: "请选择部门", trigger: "change" }],
    payMonth: [{ required: true, message: "请选择工资月份", trigger: "change" }],
    salaryMonth: [{ required: true, message: "请选择工资月份", trigger: "change" }],
  },
});
const { form, rules } = toRefs(data);
const loadBankOptions = () => {
  return bankList().then((res) => {
    const list = Array.isArray(res?.data) ? res.data : [];
    bankOptions.value = list
      .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
      .filter((v) => v !== "");
  });
};
// æ‰å¹³åŒ–部门树供下拉使用
function flattenDept(tree, list = []) {
@@ -358,32 +389,25 @@
const openDialog = (type, row) => {
  nextTick(() => {
    loadDeptOptions();
    loadBankOptions();
    employeeList.value = [];
    Object.assign(form.value, {
      id: undefined,
      title: "",
      salaryTitle: "",
      deptId: undefined,
      payMonth: "",
      salaryMonth: "",
      remark: "",
      payBank: "",
    });
    // ç¼–辑:列表页已返回主表字段;这里只做回显(明细由“生成工资表/计算工资”得到)
    if (type === "edit" && row?.id) {
      monthlyStatisticsGet(row.id).then((res) => {
        const d = res.data || {};
        form.value.id = d.id;
        form.value.title = d.title ?? d.payDateStr ?? "";
        form.value.deptId = d.deptId;
        form.value.payMonth = d.payMonth ?? d.payDate ?? d.payDateStr ?? "";
        form.value.remark = d.remark ?? "";
        employeeList.value = (d.detailList || d.employeeList || []).map((e) => ({
          ...e,
          basicSalary: parseNum(e.basicSalary),
          pieceworkSalary: parseNum(e.pieceworkSalary),
          hourlySalary: parseNum(e.hourlySalary),
          otherIncome: parseNum(e.otherIncome),
          socialSecurityIndividuals: parseNum(e.socialSecurityIndividuals),
          providentFundIndividuals: parseNum(e.providentFundIndividuals),
        }));
      });
      form.value.id = row.id;
      form.value.salaryTitle = row.salaryTitle ?? "";
      // deptIds åŽç«¯æ˜¯å­—符串(多个用逗号分隔);当前表单仍是单选 deptId
      form.value.deptId = row.deptIds ? Number(String(row.deptIds).split(",")[0]) : undefined;
      form.value.salaryMonth = row.salaryMonth ?? "";
      form.value.remark = row.remark ?? "";
      form.value.payBank = row.payBank ?? "";
    }
  });
};
@@ -412,17 +436,23 @@
    if (existIds.has(id)) return;
    existIds.add(id);
    employeeList.value.push({
      staffId: id,
      id: id,
      staffOnJobId: id,
      id,
      staffName: node.label,
      roleName: node.roleName ?? node.role ?? "",
      postName: node.postName ?? node.post ?? "",
      deptName: node.deptName ?? "",
      basicSalary: 0,
      pieceworkSalary: 0,
      pieceSalary: 0,
      hourlySalary: 0,
      otherIncome: 0,
      socialSecurityIndividuals: 0,
      providentFundIndividuals: 0,
      socialPersonal: 0,
      fundPersonal: 0,
      otherDeduct: 0,
      salaryTax: 0,
      grossSalary: 0,
      deductSalary: 0,
      netSalary: 0,
      remark: "",
    });
  });
  addPersonVisible.value = false;
@@ -430,7 +460,7 @@
const removeEmployee = (row) => {
  employeeList.value = employeeList.value.filter(
    (e) => (e.staffId || e.id) !== (row.staffId || row.id)
    (e) => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id)
  );
};
@@ -443,14 +473,55 @@
    proxy.$modal.msgWarning("请先勾选要删除的员工");
    return;
  }
  const ids = new Set(selectedEmployees.value.map((e) => e.staffId || e.id));
  const ids = new Set(selectedEmployees.value.map((e) => e.staffOnJobId || e.id));
  employeeList.value = employeeList.value.filter(
    (e) => !ids.has(e.staffId || e.id)
    (e) => !ids.has(e.staffOnJobId || e.id)
  );
};
const handleGenerate = () => {
  proxy.$modal.msgInfo("生成工资表功能需对接后端");
  if (!form.value.deptId) {
    proxy.$modal.msgWarning("请先选择部门");
    return;
  }
  if (!form.value.salaryMonth) {
    proxy.$modal.msgWarning("请先选择工资月份");
    return;
  }
  if (!employeeList.value?.length) {
    proxy.$modal.msgWarning("请先新增人员");
    return;
  }
  const ids = employeeList.value
    .map((e) => e.staffOnJobId ?? e.staffId ?? e.id)
    .filter(Boolean);
  staffSalaryMainCalculateSalary(ids).then((res) => {
    const list = Array.isArray(res?.data) ? res.data : [];
    if (!list.length) {
      proxy.$modal.msgWarning("未计算到工资数据");
      return;
    }
    employeeList.value = list.map((e) => ({
      ...e,
      staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
      staffName: e.staffName,
      postName: e.postName,
      deptName: e.deptName,
      basicSalary: parseNum(e.basicSalary),
      pieceSalary: parseNum(e.pieceSalary),
      hourlySalary: parseNum(e.hourlySalary),
      otherIncome: parseNum(e.otherIncome),
      socialPersonal: parseNum(e.socialPersonal),
      fundPersonal: parseNum(e.fundPersonal),
      otherDeduct: parseNum(e.otherDeduct),
      salaryTax: parseNum(e.salaryTax),
      grossSalary: parseNum(e.grossSalary),
      deductSalary: parseNum(e.deductSalary),
      netSalary: parseNum(e.netSalary),
      remark: e.remark ?? "",
    }));
    proxy.$modal.msgSuccess("生成成功");
  });
};
const handleExport = () => {
@@ -476,24 +547,31 @@
    if (!valid) return;
    const payload = {
      ...form.value,
      deptIds: form.value.deptId ? String(form.value.deptId) : "",
      detailList: employeeList.value.map((e) => ({
        staffId: e.staffId ?? e.id,
        staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
        staffName: e.staffName,
        basicSalary: parseNum(e.basicSalary),
        pieceworkSalary: parseNum(e.pieceworkSalary),
        pieceSalary: parseNum(e.pieceSalary),
        hourlySalary: parseNum(e.hourlySalary),
        otherIncome: parseNum(e.otherIncome),
        socialSecurityIndividuals: parseNum(e.socialSecurityIndividuals),
        providentFundIndividuals: parseNum(e.providentFundIndividuals),
        socialPersonal: parseNum(e.socialPersonal),
        fundPersonal: parseNum(e.fundPersonal),
        otherDeduct: parseNum(e.otherDeduct),
        salaryTax: parseNum(e.salaryTax),
        grossSalary: parseNum(e.grossSalary),
        deductSalary: parseNum(e.deductSalary),
        netSalary: parseNum(e.netSalary),
        remark: e.remark ?? "",
      })),
    };
    if (props.operationType === "add") {
      monthlyStatisticsAdd(payload).then(() => {
      staffSalaryMainAdd({ ...payload, status: 1 }).then(() => {
        proxy.$modal.msgSuccess("新增成功");
        closeDia();
      });
    } else {
      monthlyStatisticsUpdate(payload).then(() => {
      staffSalaryMainUpdate(payload).then(() => {
        proxy.$modal.msgSuccess("修改成功");
        closeDia();
      });
src/views/personnelManagement/monthlyStatistics/index.vue
@@ -10,20 +10,17 @@
          clearable
          @keyup.enter="handleQuery"
        />
        <span class="search_title ml10">单据状态:</span>
        <el-select
          v-model="searchForm.documentStatus"
          placeholder="请选择单据状态"
          clearable
          style="width: 180px"
        >
          <el-option label="草稿" value="draft" />
          <el-option label="已提交" value="submitted" />
          <el-option label="已审核" value="approved" />
        <span class="search_title ml10">状态:</span>
        <el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 180px">
          <el-option label="草稿" :value="1" />
          <el-option label="审核未通过" :value="2" />
          <el-option label="待审核" :value="3" />
          <el-option label="待发放" :value="4" />
          <el-option label="已发放" :value="5" />
        </el-select>
        <span class="search_title ml10">工资月份:</span>
        <el-date-picker
          v-model="searchForm.payMonth"
          v-model="searchForm.salaryMonth"
          type="month"
          value-format="YYYY-MM"
          format="YYYY-MM"
@@ -32,17 +29,6 @@
          clearable
          @change="handleQuery"
        />
        <span class="search_title ml10">审核状态:</span>
        <el-select
          v-model="searchForm.approvalStatus"
          placeholder="请选择审核状态"
          clearable
          style="width: 180px"
        >
          <el-option label="待审核" value="pending" />
          <el-option label="已通过" value="passed" />
          <el-option label="已驳回" value="rejected" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
@@ -53,6 +39,7 @@
      <div style="margin-bottom: 10px">
        <el-button type="primary" @click="openForm('add')">新建工资表</el-button>
        <el-button @click="handleDelete">删除</el-button>
        <el-button @click="openBankSetting">设置银行</el-button>
        <el-button @click="handleExport">导出</el-button>
      </div>
      <PIMTable
@@ -74,12 +61,55 @@
      ref="formDiaRef"
      @close="handleQuery"
    />
    <bank-setting-dia
      v-model="bankDialogVisible"
      ref="bankDiaRef"
      @saved="handleBankSaved"
    />
    <el-dialog v-model="issueDialogVisible" title="工资发放" width="720px">
      <el-form label-position="top">
        <el-form-item label="发放银行" required>
          <el-select
            v-model="issueForm.bank"
            placeholder="请选择发放银行"
            clearable
            filterable
            style="width: 100%"
          >
            <el-option v-for="b in issueBankOptions" :key="b" :label="b" :value="b" />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="issueDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="issueLoading" @click="confirmIssue">
          ç¡®å®š
        </el-button>
      </template>
    </el-dialog>
    <el-dialog v-model="auditDialogVisible" title="工资审核" width="720px">
      <el-form label-position="top">
        <el-form-item label="审核结果" required>
          <el-radio-group v-model="auditForm.result">
            <el-radio :value="4">通过</el-radio>
            <el-radio :value="2">不通过</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="auditDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="auditLoading" @click="confirmAudit">
          ç¡®å®š
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {
  onMounted,
  computed,
  ref,
  reactive,
  toRefs,
@@ -87,44 +117,58 @@
  nextTick,
} from "vue";
import { ElMessageBox } from "element-plus";
import Cookies from "js-cookie";
import FormDia from "./components/formDia.vue";
import BankSettingDia from "./components/bankSettingDia.vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { bankList } from "@/api/personnelManagement/bank.js";
import {
  monthlyStatisticsListPage,
  monthlyStatisticsDelete,
} from "@/api/personnelManagement/monthlyStatistics.js";
  staffSalaryMainListPage,
  staffSalaryMainDelete,
  staffSalaryMainUpdate,
} from "@/api/personnelManagement/staffSalaryMain.js";
const data = reactive({
  searchForm: {
    title: "",
    documentStatus: "",
    payMonth: "",
    approvalStatus: "",
    status: "",
    salaryMonth: "",
  },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
  { label: "工资主题", prop: "title", minWidth: 140 },
  { label: "工资月份", prop: "payMonth", width: 120 },
  { label: "单据状态", prop: "documentStatusName", width: 100 },
  { label: "审核状态", prop: "approvalStatusName", width: 100 },
  { label: "工资总额", prop: "totalAmount", width: 120 },
  { label: "支付银行", prop: "paymentBank", width: 120 },
  { label: "发放时间", prop: "issueTime", width: 160 },
  { label: "审批人员", prop: "approver", width: 100 },
  { label: "工资主题", prop: "salaryTitle", minWidth: 140 },
  { label: "工资月份", prop: "salaryMonth", width: 120 },
  { label: "状态", prop: "statusName", width: 110 },
  { label: "工资总额", prop: "totalSalary", width: 120 },
  { label: "支付银行", prop: "payBank", width: 120 },
  { label: "审核人", prop: "auditUserName", width: 110 },
  { label: "备注", prop: "remark", minWidth: 120 },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 120,
    width: 180,
    operation: [
      {
        name: "编辑",
        type: "text",
        showHide: (row) => Number(row?.status) === 1 || Number(row?.status) === 2,
        clickFun: (row) => openForm("edit", row),
      },
      {
        name: "审核",
        type: "text",
        showHide: (row) => Number(row?.status) === 3,
        clickFun: (row) => openAudit(row),
      },
      {
        name: "发放",
        type: "text",
        showHide: (row) => Number(row?.status) === 4,
        clickFun: (row) => openIssue(row),
      },
    ],
  },
@@ -143,6 +187,47 @@
const operationType = ref("add");
const currentRow = ref({});
const { proxy } = getCurrentInstance();
const bankSetting = ref({});
const bankDialogVisible = ref(false);
const bankDiaRef = ref(null);
const issueDialogVisible = ref(false);
const issueLoading = ref(false);
const issueRow = ref(null);
const issueForm = reactive({ bank: "" });
const auditDialogVisible = ref(false);
const auditLoading = ref(false);
const auditRow = ref(null);
const auditForm = reactive({ result: 4 }); // 4=通过(待发放) 2=不通过
const issueBankOptions = computed(() => {
  const options = Array.isArray(bankSetting.value?.options) ? bankSetting.value.options : [];
  return options
    .map((v) => (v == null ? "" : String(v).trim()))
    .filter((v) => v !== "");
});
const statusName = (s) => {
  const n = Number(s);
  return (
    {
      1: "草稿",
      2: "审核未通过",
      3: "待审核",
      4: "待发放",
      5: "已发放",
    }[n] || "-"
  );
};
const loadBankSetting = () => {
  return bankList().then((res) => {
    const list = Array.isArray(res?.data) ? res.data : [];
    const options = list
      .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
      .filter((v) => v !== "");
    bankSetting.value = { options, defaultBank: "" };
  });
};
const handleQuery = () => {
  page.current = 1;
@@ -151,9 +236,8 @@
const handleReset = () => {
  searchForm.value.title = "";
  searchForm.value.documentStatus = "";
  searchForm.value.payMonth = "";
  searchForm.value.approvalStatus = "";
  searchForm.value.status = "";
  searchForm.value.salaryMonth = "";
  page.current = 1;
  getList();
};
@@ -166,27 +250,25 @@
const getList = () => {
  tableLoading.value = true;
  monthlyStatisticsListPage({
  staffSalaryMainListPage({
    ...searchForm.value,
    current: page.current,
    size: page.size,
  })
    .then((res) => {
      tableLoading.value = false;
      const records = res.data?.records ?? [];
      const records = res.data?.records ?? res.data?.list ?? [];
      // å…¼å®¹åŽç«¯å­—段:若接口仍返回台账结构,可在此做映射
      tableData.value = records.map((item) => ({
        ...item,
        title: item.title ?? item.payDateStr ?? "-",
        payMonth: item.payMonth ?? item.payDateStr ?? item.payDate ?? "-",
        documentStatusName: item.documentStatusName ?? "-",
        approvalStatusName: item.approvalStatusName ?? "-",
        totalAmount: item.totalAmount ?? item.actualWages ?? "-",
        paymentBank: item.paymentBank ?? "-",
        issueTime: item.issueTime ?? item.createTime ?? "-",
        approver: item.approver ?? "-",
        salaryTitle: item.salaryTitle ?? "-",
        salaryMonth: item.salaryMonth ?? "-",
        statusName: statusName(item.status),
        totalSalary: item.totalSalary ?? "-",
        payBank: (item.payBank == null ? "" : String(item.payBank).trim()) || "-",
        auditUserName: item.auditUserName ?? "-",
      }));
      page.total = res.data?.total ?? 0;
      page.total = res.data?.total ?? res.data?.count ?? 0;
    })
    .catch(() => {
      tableLoading.value = false;
@@ -206,6 +288,85 @@
  });
};
const openBankSetting = () => {
  bankDialogVisible.value = true;
};
const openAudit = (row) => {
  auditRow.value = row || null;
  auditForm.result = 4;
  auditDialogVisible.value = true;
};
const openIssue = (row) => {
  if (!issueBankOptions.value?.length) {
    proxy?.$modal?.msgWarning?.("请先在“设置银行”中维护发放银行选项");
    return;
  }
  issueRow.value = row || null;
  const current = row?.payBank && row.payBank !== "-" ? String(row.payBank).trim() : "";
  issueForm.bank = current;
  issueDialogVisible.value = true;
};
const confirmAudit = () => {
  const row = auditRow.value;
  if (!row?.id) {
    auditDialogVisible.value = false;
    return;
  }
  const username = Cookies.get("username") || "";
  const userIdRaw = Cookies.get("userId");
  const auditUserId = userIdRaw ? Number(userIdRaw) : undefined;
  auditLoading.value = true;
  staffSalaryMainUpdate({
    id: row.id,
    status: Number(auditForm.result) === 2 ? 2 : 4,
    auditUserId,
    auditUserName: username,
  })
    .then(() => {
      proxy?.$modal?.msgSuccess?.("审核成功");
      auditDialogVisible.value = false;
      getList();
    })
    .finally(() => {
      auditLoading.value = false;
    });
};
const confirmIssue = () => {
  const bank = issueForm.bank ? String(issueForm.bank).trim() : "";
  if (!bank) {
    proxy?.$modal?.msgWarning?.("请选择发放银行");
    return;
  }
  const row = issueRow.value;
  if (!row?.id) {
    issueDialogVisible.value = false;
    return;
  }
  issueLoading.value = true;
  staffSalaryMainUpdate({
    id: row.id,
    payBank: bank,
    status: 5,
  })
    .then(() => {
      proxy?.$modal?.msgSuccess?.("发放成功");
      issueDialogVisible.value = false;
      getList();
    })
    .finally(() => {
      issueLoading.value = false;
    });
};
const handleBankSaved = () => {
  loadBankSetting();
  getList();
};
const handleDelete = () => {
  if (!selectedRows.value?.length) {
    proxy.$modal.msgWarning("请选择要删除的数据");
@@ -218,7 +379,7 @@
    type: "warning",
  })
    .then(() => {
      monthlyStatisticsDelete(ids).then(() => {
      staffSalaryMainDelete(ids).then(() => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
@@ -235,6 +396,7 @@
};
onMounted(() => {
  loadBankSetting();
  getList();
});
</script>
src/views/personnelManagement/socialSecuritySet/components/formDia.vue
@@ -15,7 +15,10 @@
          <el-col :span="8">
            <el-form-item label="适用人员:" prop="deptIds">
              <div class="dept-checkbox-wrap">
                <el-checkbox-group v-model="form.deptIds">
                <el-checkbox-group
                  v-model="form.deptIds"
                  :disabled="isDetail"
                >
                  <div
                    v-for="dept in deptList"
                    :key="dept.deptId"
@@ -41,7 +44,12 @@
                <el-icon class="card-collapse"><ArrowUp /></el-icon>
              </template>
              <el-form-item label="方案标题:" prop="title">
                <el-input v-model="form.title" placeholder="请输入" clearable />
                <el-input
                  v-model="form.title"
                  placeholder="请输入"
                  clearable
                  :disabled="isDetail"
                />
              </el-form-item>
              <el-form-item label="备注:" prop="remark">
                <el-input
@@ -50,6 +58,7 @@
                  :rows="2"
                  placeholder="请输入"
                  clearable
                  :disabled="isDetail"
                />
              </el-form-item>
            </el-card>
@@ -58,7 +67,12 @@
            <el-card class="form-card" shadow="never">
              <template #header>
                <span class="card-title"><span class="card-title-line">|</span> ä¿é™©ç±»åž‹</span>
                <el-button type="primary" size="small" @click="addInsuranceBenefit">
                <el-button
                  v-if="!isDetail"
                  type="primary"
                  size="small"
                  @click="addInsuranceBenefit"
                >
                  æ·»åŠ ä¿é™©ç¦åˆ©
                </el-button>
              </template>
@@ -72,7 +86,7 @@
                    <div class="insurance-benefit-title">
                      ä¿é™©ç¦åˆ©{{ index + 1 }}
                      <el-button
                        v-if="form.insuranceBenefits.length > 1"
                        v-if="!isDetail && form.insuranceBenefits.length > 1"
                        type="danger"
                        link
                        size="small"
@@ -92,6 +106,7 @@
                        placeholder="请选择"
                        clearable
                        style="width: 100%"
                        :disabled="isDetail"
                      >
                        <el-option
                          v-for="opt in insuranceTypeOptions"
@@ -102,9 +117,23 @@
                      </el-select>
                    </el-form-item>
                    <el-form-item label="缴费基数:" label-width="100px">
                      <div class="checkbox-group-inline">
                        <el-checkbox v-model="item.baseOnSalary">根据基本工资缴纳</el-checkbox>
                        <el-checkbox v-model="item.useBasicSalary">调用基本工资</el-checkbox>
                      <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-checkbox>
                      </div>
                    </el-form-item>
                    <el-form-item label="个人缴费比例:" label-width="100px">
@@ -115,6 +144,7 @@
                          clearable
                          style="width: 100px"
                          type="number"
                          :disabled="isDetail"
                        />
                        <span class="ratio-unit">(%)</span>
                        <span class="ratio-plus">+</span>
@@ -124,6 +154,7 @@
                          clearable
                          style="width: 100px"
                          type="number"
                          :disabled="isDetail"
                        />
                      </div>
                    </el-form-item>
@@ -139,15 +170,11 @@
</template>
<script setup>
import { ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
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 {
  socialSecurityInfo,
  socialSecurityAdd,
  socialSecurityUpdate,
} from "@/api/personnelManagement/socialSecuritySet.js";
import { socialSecurityAdd, socialSecurityUpdate } from "@/api/personnelManagement/socialSecuritySet.js";
const emit = defineEmits(["close"]);
const { proxy } = getCurrentInstance();
@@ -157,22 +184,28 @@
const formRef = ref(null);
const deptList = ref([]);
const isDetail = computed(() => operationType.value === "detail");
const dialogTitle = () =>
  operationType.value === "add" ? "新增方案" : "编辑方案";
  operationType.value === "add"
    ? "新增方案"
    : operationType.value === "edit"
    ? "编辑方案"
    : "方案详情";
// ä¿é™©ç±»åž‹é€‰é¡¹ï¼ˆå¯æŒ‰å­—典替换)
const insuranceTypeOptions = [
  { label: "养老保险", value: "pension" },
  { label: "医疗保险", value: "medical" },
  { label: "失业保险", value: "unemployment" },
  { label: "工伤保险", value: "work_injury" },
  { label: "生育保险", value: "maternity" },
  { label: "养老保险", value: "养老保险" },
  { label: "医疗保险", value: "医疗保险" },
  { label: "失业保险", value: "失业保险" },
  { label: "工伤保险", value: "工伤保险" },
  { label: "生育保险", value: "生育保险" },
];
const defaultBenefit = () => ({
  _key: Math.random().toString(36).slice(2),
  insuranceType: "",
  baseOnSalary: false,
  paymentBase: "",
  useBasicSalary: false,
  personalRatio: "",
  personalFixed: "",
@@ -231,6 +264,18 @@
  form.value.insuranceBenefits.splice(index, 1);
};
const handleUseBasicSalaryChange = (item) => {
  if (item.useBasicSalary) {
    item.paymentBase = "";
  }
};
const handlePaymentBaseInput = (item) => {
  if (item.paymentBase !== "" && item.paymentBase != null) {
    item.useBasicSalary = false;
  }
};
const resetForm = () => {
  form.value = {
    id: undefined,
@@ -246,32 +291,77 @@
  dialogFormVisible.value = true;
  loadDeptList();
  resetForm();
  if (type === "edit" && row?.id) {
    socialSecurityInfo(row.id).then((res) => {
      const d = res.data || {};
  if ((type === "edit" || type === "detail") && row) {
    const d = row || {};
      form.value.id = d.id;
      form.value.title = d.title;
      form.value.remark = d.remark ?? "";
      form.value.deptIds = d.deptIds ?? [];
    // 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 =
        (d.insuranceBenefits && d.insuranceBenefits.length)
          ? d.insuranceBenefits.map((b) => ({
              ...b,
              _key: b._key || Math.random().toString(36).slice(2),
      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 = {
      ...form.value,
      insuranceBenefits: form.value.insuranceBenefits.map(
        ({ _key, ...rest }) => rest
      ),
      id: form.value.id,
      title: form.value.title,
      remark: form.value.remark ?? "",
      deptIds,
      insuranceTypes,
      deptNames,
      schemeInsuranceDetailList,
    };
    if (operationType.value === "add") {
      socialSecurityAdd(submitData).then(() => {
@@ -356,6 +446,16 @@
  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;
src/views/personnelManagement/socialSecuritySet/index.vue
@@ -17,8 +17,15 @@
      </div>
    </div>
    <div class="table_list">
      <div style="margin-bottom: 10px">
      <div style="margin-bottom: 10px; display: flex; gap: 10px">
        <el-button type="primary" @click="openForm('add')">新增方案</el-button>
        <el-button
          type="danger"
          @click="handleBatchDelete"
          :disabled="selectedRows.length === 0"
        >
          æ‰¹é‡åˆ é™¤
        </el-button>
      </div>
      <PIMTable
        rowKey="id"
@@ -26,8 +33,10 @@
        :tableData="tableData"
        :page="page"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        @pagination="pagination"
      />
    </div>
    <form-dia ref="formDiaRef" @close="handleQuery" />
@@ -36,9 +45,13 @@
<script setup>
import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import { ElMessageBox, ElMessage } from "element-plus";
import FormDia from "./components/formDia.vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { socialSecurityListPage } from "@/api/personnelManagement/socialSecuritySet.js";
import {
  socialSecurityListPage,
  socialSecurityDelete,
} from "@/api/personnelManagement/socialSecuritySet.js";
const data = reactive({
  searchForm: {
@@ -49,22 +62,35 @@
const tableColumn = ref([
  { label: "主题", prop: "title", minWidth: 120 },
  { label: "保险类型", prop: "insuranceTypeName", width: 120 },
  { label: "使用范围", prop: "scopeName", width: 120 },
  { label: "保险类型", prop: "insuranceTypes", width: 120 },
  { label: "使用范围", prop: "deptNames", width: 120 },
  { label: "备注", prop: "remark", minWidth: 120 },
  { label: "创建时间", prop: "createTime", width: 160 },
  { label: "创建人", prop: "createBy", width: 100 },
  { label: "创建人", prop: "createUserName", width: 100 },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 120,
    width: 180,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => openForm("edit", row),
      },
      {
        name: "详情",
        type: "text",
        clickFun: (row) => openForm("detail", row),
      },
      {
        name: "删除",
        type: "text",
        style: {
          color: "#F56C6C",
        },
        clickFun: (row) => handleDelete(row),
      },
    ],
  },
@@ -72,6 +98,7 @@
const tableData = ref([]);
const tableLoading = ref(false);
const selectedRows = ref([]);
const page = reactive({
  current: 1,
  size: 10,
@@ -113,12 +140,70 @@
    });
};
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
const openForm = (type, row) => {
  nextTick(() => {
    formDiaRef.value?.openDialog(type, row);
  });
};
// åˆ é™¤æ–¹æ¡ˆï¼Œé€»è¾‘与其它页面保持一致(确认弹窗 + è°ƒç”¨åˆ é™¤æŽ¥å£ + åˆ·æ–°åˆ—表)
const handleDelete = (row) => {
  ElMessageBox.confirm(
    `确认删除方案"${row.title}"吗?`,
    "删除确认",
    {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    }
  )
    .then(() => {
      socialSecurityDelete([row.id])
        .then(() => {
          ElMessage.success("删除成功");
          getList();
        })
        .catch(() => {
          ElMessage.error("删除失败");
        });
    })
    .catch(() => {
      ElMessage.info("已取消删除");
    });
};
// æ‰¹é‡åˆ é™¤
const handleBatchDelete = () => {
  if (!selectedRows.value.length) return;
  ElMessageBox.confirm(
    `确定要删除选中的 ${selectedRows.value.length} æ¡æ–¹æ¡ˆå—?`,
    "批量删除确认",
    {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    }
  )
    .then(() => {
      const ids = selectedRows.value.map((item) => item.id);
      socialSecurityDelete(ids)
        .then(() => {
          ElMessage.success("删除成功");
          getList();
        })
        .catch(() => {
          ElMessage.error("删除失败");
        });
    })
    .catch(() => {
      ElMessage.info("已取消删除");
    });
};
onMounted(() => {
  getList();
});
src/views/projectManagement/projectType/components/ProjectTypeDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,385 @@
<template>
  <el-dialog
    :title="title"
    v-model="visible"
    width="1000px"
    append-to-body
    @close="handleClose"
  >
    <el-form ref="formRef" :model="form" :rules="rules" label-width="0">
      <!-- é¡¶éƒ¨åŸºç¡€ä¿¡æ¯ -->
      <div class="base-info-row">
        <div class="info-item">
          <span class="item-label required">名称</span>
          <el-input
            v-model="form.name"
            placeholder="请输入名称"
            maxlength="10"
            show-word-limit
            style="width: 220px"
          />
        </div>
        <div class="info-item">
          <span class="item-label">备注</span>
          <el-input
            v-model="form.description"
            placeholder="请输入备注"
            maxlength="20"
            show-word-limit
            style="width: 220px"
          />
        </div>
        <div class="info-item">
          <span class="item-label">附件</span>
          <el-upload
            action="#"
            :auto-upload="false"
            :show-file-list="false"
            @change="handleFileChange"
          >
            <el-button type="primary">上传附件</el-button>
          </el-upload>
        </div>
      </div>
      <!-- æ­¥éª¤é…ç½®è¡¨æ ¼ -->
      <div class="step-table-container">
        <el-table :data="form.savePlanNodeList" border style="width: 100%">
          <el-table-column label="步骤" width="80" align="center">
            <template #default="scope">
              {{ scope.$index + 1 }}
            </template>
          </el-table-column>
          <el-table-column label="阶段名称" min-width="150">
            <template #header>
              <span class="required-star">*</span> é˜¶æ®µåç§°
            </template>
            <template #default="scope">
              <el-form-item
                :prop="'savePlanNodeList.' + scope.$index + '.name'"
                :rules="[{ required: true, message: '请输入阶段名称', trigger: 'blur' }]"
              >
                <el-input v-model="scope.row.name" placeholder="请输入" />
              </el-form-item>
            </template>
          </el-table-column>
          <el-table-column label="负责人" width="180">
            <template #header>
              <span class="required-star">*</span> è´Ÿè´£äºº
            </template>
            <template #default="scope">
              <el-form-item
                :prop="'savePlanNodeList.' + scope.$index + '.leaderId'"
                :rules="[{ required: true, message: '请选择负责人', trigger: 'change' }]"
              >
                <el-select
                  v-model="scope.row.leaderId"
                  placeholder="测试"
                  @change="(val) => handleLeaderChange(val, scope.row)"
                >
                  <el-option
                    v-for="item in userOptions"
                    :key="item.userId"
                    :label="item.nickName"
                    :value="item.userId"
                  />
                </el-select>
              </el-form-item>
            </template>
          </el-table-column>
          <el-table-column label="预计工期 (天)" width="150">
            <template #header>
              é¢„计工期 (天)
              <el-tooltip content="完成该阶段预计需要的天数" placement="top">
                <el-icon class="info-icon"><QuestionFilled /></el-icon>
              </el-tooltip>
            </template>
            <template #default="scope">
              <el-input-number
                v-model="scope.row.estimatedDuration"
                :min="0"
                controls-position="right"
                style="width: 100%"
              />
            </template>
          </el-table-column>
          <el-table-column label="工时单价" width="120">
            <template #default="scope">
              <el-input v-model="scope.row.hourlyRate" placeholder="请输入" />
            </template>
          </el-table-column>
          <el-table-column label="作业内容" min-width="150">
            <template #default="scope">
              <el-input v-model="scope.row.workContent" placeholder="请输入" />
            </template>
          </el-table-column>
          <el-table-column label="操作" width="180" align="center">
            <template #default="scope">
              <el-button
                link
                type="primary"
                :disabled="scope.$index === 0"
                @click="moveStep(scope.$index, -1)"
              >上移</el-button>
              <el-button
                link
                type="primary"
                :disabled="scope.$index === form.savePlanNodeList.length - 1"
                @click="moveStep(scope.$index, 1)"
              >下移</el-button>
              <el-button
                link
                type="danger"
                @click="removeStep(scope.$index)"
              >删除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <div class="add-row-btn" @click="addStep">
          <el-icon><Plus /></el-icon> æ–°å¢žä¸€è¡Œ
        </div>
      </div>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="visible = false">取消</el-button>
        <el-button type="primary" @click="submitForm">提交</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { Plus, QuestionFilled } from '@element-plus/icons-vue';
import { userListNoPageByTenantId } from '@/api/system/user';
import { ElMessage } from 'element-plus';
const props = defineProps({
  modelValue: Boolean,
  title: String,
  data: Object
});
const emit = defineEmits(['update:modelValue', 'submit']);
const visible = ref(false);
const formRef = ref(null);
const userOptions = ref([]);
const form = ref({
  id: undefined,
  name: '',
  description: '',
  attachmentIds: [],
  savePlanNodeList: []
});
const rules = {
  name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
};
// ç›‘听弹窗显示/隐藏
watch(() => props.modelValue, (val) => {
  visible.value = val;
  if (val) {
    if (props.data) {
      // ç¼–辑模式
      form.value = JSON.parse(JSON.stringify(props.data));
      if (!form.value.savePlanNodeList || form.value.savePlanNodeList.length === 0) {
        form.value.savePlanNodeList = [createDefaultNode()];
      }
    } else {
      // æ–°å¢žæ¨¡å¼
      resetForm();
    }
  }
});
watch(visible, (val) => {
  emit('update:modelValue', val);
});
/** åˆ›å»ºé»˜è®¤èŠ‚ç‚¹å¯¹è±¡ */
function createDefaultNode() {
  return {
    name: '',
    leaderId: null,
    leaderName: null,
    estimatedDuration: null,
    hourlyRate: null,
    workContent: null
  };
}
/** é‡ç½®è¡¨å• */
function resetForm() {
  form.value = {
    id: undefined,
    name: '',
    description: '',
    attachmentIds: [],
    savePlanNodeList: [createDefaultNode()]
  };
  if (formRef.value) {
    formRef.value.resetFields();
  }
}
/** èŽ·å–ç”¨æˆ·åˆ—è¡¨ */
async function getUserList() {
  try {
    const res = await userListNoPageByTenantId();
    if (res.code === 200) {
      userOptions.value = res.data || [];
    }
  } catch (error) {
    console.error('获取用户列表失败:', error);
  }
}
/** å¤„理负责人变化 */
function handleLeaderChange(val, row) {
  const user = userOptions.value.find(u => u.userId === val);
  if (user) {
    row.leaderName = user.nickName;
  }
}
/** å¤„理文件变化 */
function handleFileChange(file) {
  // è¿™é‡Œå®žçŽ°æ–‡ä»¶ä¸Šä¼ é€»è¾‘ï¼ŒèŽ·å– attachmentId
  ElMessage.info('正在上传: ' + file.name);
  // æ¨¡æ‹Ÿä¸Šä¼ æˆåŠŸ
  // form.value.attachmentIds.push(newId);
}
/** æ·»åŠ æ­¥éª¤ */
function addStep() {
  form.value.savePlanNodeList.push(createDefaultNode());
}
/** ç§»é™¤æ­¥éª¤ */
function removeStep(index) {
  if (form.value.savePlanNodeList.length <= 1) {
    ElMessage.warning('至少保留一个步骤');
    return;
  }
  form.value.savePlanNodeList.splice(index, 1);
}
/** ç§»åŠ¨æ­¥éª¤ */
function moveStep(index, direction) {
  const targetIndex = index + direction;
  if (targetIndex < 0 || targetIndex >= form.value.savePlanNodeList.length) return;
  const list = form.value.savePlanNodeList;
  const temp = list[index];
  list[index] = list[targetIndex];
  list[targetIndex] = temp;
}
/** æäº¤è¡¨å• */
async function submitForm() {
  if (!formRef.value) return;
  try {
    const valid = await formRef.value.validate();
    if (valid) {
      emit('submit', form.value);
    }
  } catch (error) {
    console.error('表单验证失败:', error);
  }
}
/** å…³é—­å¼¹çª— */
function handleClose() {
  resetForm();
}
onMounted(() => {
  getUserList();
});
</script>
<style scoped lang="scss">
.base-info-row {
  display: flex;
  gap: 40px;
  margin-bottom: 25px;
  padding: 0 10px;
  .info-item {
    display: flex;
    align-items: center;
    gap: 12px;
    .item-label {
      font-size: 14px;
      color: #606266;
      white-space: nowrap;
      &.required::before {
        content: '*';
        color: #f56c6c;
        margin-right: 4px;
      }
    }
  }
}
.step-table-container {
  padding: 0 10px;
  :deep(.el-form-item) {
    margin-bottom: 0;
  }
  .required-star {
    color: #f56c6c;
    margin-right: 4px;
  }
  .info-icon {
    font-size: 14px;
    color: #909399;
    margin-left: 4px;
    cursor: pointer;
  }
  .add-row-btn {
    margin-top: 15px;
    height: 40px;
    border: 1px dashed #dcdfe6;
    border-radius: 4px;
    display: flex;
    justify-content: center;
    align-items: center;
    color: #409eff;
    cursor: pointer;
    font-size: 14px;
    transition: all 0.3s;
    &:hover {
      border-color: #409eff;
      background-color: #f0f7ff;
    }
  }
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 15px;
  padding-top: 10px;
}
</style>
src/views/projectManagement/projectType/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,462 @@
<template>
  <div class="app-container">
    <div class="header-section">
      <div class="title-container">
        <span class="blue-bar"></span>
        <span class="title-text">项目类型</span>
      </div>
      <el-button type="primary" class="add-btn" @click="handleAdd">新增</el-button>
    </div>
    <div class="content-section" v-loading="loading">
      <div v-for="item in projectTypeList" :key="item.id" class="project-type-card">
        <div class="card-header">
          <div class="info-group">
            <span class="label">类型名称:</span>
            <span class="value">{{ item.name }}</span>
          </div>
          <div class="info-group">
            <span class="label">备注:</span>
            <span class="value">{{ item.description || '--' }}</span>
          </div>
          <div class="info-group">
            <span class="label">附件:</span>
            <div class="attachment-info" v-if="item.attachment" @click="handleExpand(item)">
              <el-icon class="file-icon"><Document /></el-icon>
              <span class="file-name">{{ item.attachment.name }}</span>
              <el-icon class="download-icon" @click.stop="handleDownload(item.attachment)"><Download /></el-icon>
              <span class="expand-link">{{ item.expanded ? '收起' : '展开' }}</span>
              <el-icon class="arrow-icon" :class="{ 'is-reverse': item.expanded }"><ArrowDown /></el-icon>
            </div>
            <span class="value" v-else>--</span>
          </div>
          <div class="actions">
            <el-button link type="primary" @click="handleUpdate(item)">编辑</el-button>
            <el-button link type="primary" @click="handleCopy(item)">复制</el-button>
            <el-button link type="danger" @click="handleDelete(item)">删除</el-button>
          </div>
        </div>
        <el-collapse-transition>
          <div v-show="item.expanded" class="expanded-content">
            <div class="attachment-list">
              <div class="attachment-item">
                <el-icon><Document /></el-icon>
                <span>{{ item.attachment?.name }}</span>
                <el-button link type="primary" size="small" @click="handleDownload(item.attachment)">下载</el-button>
              </div>
            </div>
          </div>
        </el-collapse-transition>
        <div class="card-body">
          <div class="workflow-container">
            <div v-for="(step, index) in item.steps" :key="index" class="workflow-step">
              <div class="step-main">
                <div class="step-circle">{{ index + 1 }}</div>
                <div v-if="index < item.steps.length - 1" class="step-line"></div>
              </div>
              <div class="step-label">{{ step.label }}</div>
            </div>
          </div>
        </div>
      </div>
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="queryParams.current"
          v-model:page-size="queryParams.size"
          :page-sizes="[10, 20, 30, 50]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
          @size-change="getList"
          @current-change="getList"
        />
      </div>
    </div>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹é¡¹ç›®ç±»åž‹å¯¹è¯æ¡† -->
    <ProjectTypeDialog
      v-model="open"
      :title="title"
      :data="editData"
      @submit="handleDialogSubmit"
    />
  </div>
</template>
<script setup name="ProjectType">
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
import { Document, Download, ArrowDown } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { listPlan, savePlan, deletePlan } from '@/api/projectManagement/projectType';
import ProjectTypeDialog from './components/ProjectTypeDialog.vue';
const { proxy } = getCurrentInstance();
// é®ç½©å±‚
const loading = ref(false);
// æ€»æ¡æ•°
const total = ref(0);
// é¡¹ç›®ç±»åž‹è¡¨æ ¼æ•°æ®
const projectTypeList = ref([]);
// å¼¹å‡ºå±‚标题
const title = ref("");
// æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚
const open = ref(false);
// ç¼–辑数据
const editData = ref(null);
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10,
});
/** æŸ¥è¯¢é¡¹ç›®ç±»åž‹åˆ—表 */
async function getList() {
  loading.value = true;
  try {
    const res = await listPlan(queryParams);
    if (res.code === 200) {
      projectTypeList.value = res.data.records.map(item => ({
        ...item,
        expanded: false,
        // åŽç«¯è¿”回的节点列表可能是 planNodeList æˆ– savePlanNodeList
        steps: (item.planNodeList || item.savePlanNodeList || []).map(node => ({
          label: node.name
        }))
      }));
      total.value = res.data.total;
    } else {
      ElMessage.error(res.msg || "获取列表失败");
    }
  } catch (error) {
    console.error("获取列表失败:", error);
    // å¦‚果接口不通,暂时保留模拟数据供演示
    if (projectTypeList.value.length === 0) {
      projectTypeList.value = [
        {
          id: 1,
          name: 'A项目',
          description: '',
          attachment: { name: 'precaution...' },
          steps: [{ label: '立项' }, { label: '设计' }, { label: '采购' }, { label: '生产' }, { label: '出货' }],
          expanded: false
        }
      ];
      total.value = 1;
    }
  } finally {
    loading.value = false;
  }
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
  editData.value = null;
  open.value = true;
  title.value = "添加项目类型";
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
  editData.value = row;
  open.value = true;
  title.value = "修改项目类型";
}
/** å¼¹çª—提交处理 */
async function handleDialogSubmit(formData) {
  try {
    const res = await savePlan(formData);
    if (res.code === 200) {
      ElMessage.success(formData.id !== undefined ? "修改成功" : "新增成功");
      open.value = false;
      getList();
    } else {
      ElMessage.error(res.msg || "保存失败");
    }
  } catch (error) {
    console.error("保存失败:", error);
  }
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  ElMessageBox.confirm('是否确认删除项目类型编号为"' + row.id + '"的数据项?', "系统提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning"
  }).then(async function() {
    try {
      const res = await deletePlan(row.id);
      if (res.code === 200) {
        ElMessage.success("删除成功");
        getList();
      } else {
        ElMessage.error(res.msg || "删除失败");
      }
    } catch (error) {
      console.error("删除失败:", error);
    }
  }).catch(() => {});
}
/** å¤åˆ¶æŒ‰é’®æ“ä½œ */
async function handleCopy(row) {
  const copyData = {
    name: row.name + " - å‰¯æœ¬",
    description: row.description,
    attachmentIds: row.attachmentIds || [],
    savePlanNodeList: (row.planNodeList || row.savePlanNodeList || []).map(node => ({
      name: node.name,
      leaderId: node.leaderId,
      leaderName: node.leaderName,
      estimatedDuration: node.estimatedDuration,
      hourlyRate: node.hourlyRate,
      workContent: node.workContent
    }))
  };
  try {
    const res = await savePlan(copyData);
    if (res.code === 200) {
      ElMessage.success("复制成功");
      getList();
    } else {
      ElMessage.error(res.msg || "复制失败");
    }
  } catch (error) {
    console.error("复制失败:", error);
  }
}
/** å±•å¼€/收起附件 */
function handleExpand(item) {
  item.expanded = !item.expanded;
}
/** ä¸‹è½½é™„ä»¶ */
function handleDownload(attachment) {
  // å®žçŽ°ä¸‹è½½é€»è¾‘
  ElMessage.info("开始下载: " + (attachment.name || "文件"));
}
onMounted(() => {
  getList();
});
</script>
<style scoped lang="scss">
.app-container {
  background-color: #f5f7fa;
  min-height: calc(100vh - 84px);
  padding: 20px;
}
.header-section {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background-color: #fff;
  padding: 15px 20px;
  border-radius: 8px;
  margin-bottom: 20px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
  .title-container {
    display: flex;
    align-items: center;
    .blue-bar {
      width: 4px;
      height: 18px;
      background-color: #409eff;
      margin-right: 10px;
      border-radius: 2px;
    }
    .title-text {
      font-size: 16px;
      font-weight: bold;
      color: #333;
    }
  }
  .add-btn {
    padding: 8px 20px;
  }
}
.project-type-card {
  background-color: #fff;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 20px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
  .card-header {
    display: flex;
    align-items: center;
    margin-bottom: 25px;
    position: relative;
    .info-group {
      margin-right: 40px;
      display: flex;
      align-items: center;
      font-size: 14px;
      .label {
        color: #666;
        margin-right: 8px;
      }
      .value {
        color: #333;
      }
      .attachment-info {
        display: flex;
        align-items: center;
        background-color: #f0f4ff;
        padding: 4px 10px;
        border-radius: 4px;
        color: #409eff;
        cursor: pointer;
        .file-icon {
          margin-right: 5px;
        }
        .file-name {
          margin-right: 5px;
          max-width: 100px;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
        .download-icon {
          font-size: 12px;
          margin-right: 10px;
        }
        .expand-link {
          color: #409eff;
          margin-right: 5px;
        }
        .arrow-icon {
          color: #409eff;
          font-size: 12px;
        }
      }
    }
    .actions {
    margin-left: auto;
  }
}
.expanded-content {
  padding: 0 20px 20px;
  border-bottom: 1px dashed #ebeef5;
  margin-bottom: 20px;
  .attachment-list {
    background-color: #f8f9fa;
    padding: 15px;
    border-radius: 4px;
    .attachment-item {
      display: flex;
      align-items: center;
      gap: 10px;
      font-size: 14px;
      color: #606266;
      .el-icon {
        font-size: 16px;
        color: #409eff;
      }
    }
  }
}
.card-body {
    .workflow-container {
      display: flex;
      align-items: flex-start;
      padding: 10px 0;
      overflow-x: auto;
      .workflow-step {
        display: flex;
        flex-direction: column;
        align-items: center;
        min-width: 120px;
        flex-shrink: 0;
        .step-main {
          display: flex;
          align-items: center;
          width: 100%;
          position: relative;
          .step-circle {
            width: 24px;
            height: 24px;
            background-color: #409eff;
            color: #fff;
            border-radius: 50%;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 12px;
            font-weight: bold;
            z-index: 2;
            margin: 0 auto;
          }
          .step-line {
            position: absolute;
            height: 2px;
            background-color: #d9e6ff;
            left: calc(50% + 12px);
            right: calc(-50% + 12px);
            top: 50%;
            transform: translateY(-50%);
            z-index: 1;
          }
        }
        .step-label {
          margin-top: 10px;
          font-size: 13px;
          color: #333;
          text-align: center;
        }
      }
    }
  }
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  margin-top: 20px;
}
.step-config-item {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.content-section{
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
</style>