Bläddra i källkod

Merge branch 'feat/production-safety' of http://14.103.151.10:8888/product-group-fe/sfy-safety-group/sfy-safety into feat/production-safety

sunqijun 1 månad sedan
förälder
incheckning
56a7f25c2b

+ 33 - 0
src/api/employee-training-record-card-management/index.ts

@@ -12,6 +12,7 @@ export interface EmployeeTableType {
   staffAddress: string;
   staffImg?: string;
   deptName: string;
+  deptId?: string;
   dateOfJoining: Date;
   highestDegree: string;
   staffJob: string;
@@ -56,6 +57,28 @@ export interface EducationStaffTrainingCardItem {
   createdAt: string;
   updatedAt: string;
 }
+// 编辑员工记录卡
+export interface FormDataType {
+  id?: string; 
+  staffNo: string;
+  /** 后端存取:部门名称(仅最后一级所选部门) */
+  deptName: string;
+  /** 后端存取:部门 id(单个,最后一级) */
+  deptId?: string;
+  /** 仅前端级联:选中的部门 id(单选,叶子) */
+  deptIdForSelect?: number;
+  staffName: string;
+  staffBirthday: string;
+  staffIdCard: string;
+  staffAddress: string;
+  dateOfJoining: string;
+  staffJob: string;
+  technicalLvl: string;
+  professionalTitle: string;
+  highestDegree: string;
+  jobSeniority: number | string;
+  staffImg?:any
+}
 
 /**
  * 分页查询员工培训记录卡列表
@@ -144,3 +167,13 @@ export function delateEducationStaffTrainingCardScore(id: number) {
     method: 'delete',
   });
 }
+/**
+ * 员工培训记录卡--编辑
+ */
+export function updateEducationStaffTrainingCard(data: any) {
+  return http.request({
+    url: '/educationStaffTrainingCard/updateEducationStaffTrainingCard',
+    method: 'post',
+    data: data,
+  });
+}

+ 14 - 0
src/router/routers/production-safety-router/safetyTrainingAndEducation.ts

@@ -98,6 +98,20 @@ const safetyTrainingAndEducationRoutes: RouteComponent[] = [{
         noCache: false,
       },
     },
+    {
+      id: 90060204,
+      parentId: 9006,
+      name: 'employeeTrainingRecordCardManagementEdit',
+      path: 'employee-training-record-card-management-edit',
+      component: '/production-safety/safetyTrainingAndEducation/employeeTrainingRecordCardManagement/employeeTrainingRecordCardManagementEdit',
+      meta: {
+        title: '员工培训记录卡管理编辑',
+        icon: 'OverviewIcon',
+        isRoot: false,
+        hidden: true,
+        noCache: false,
+      },
+    },
   ],
 }];
 

+ 86 - 0
src/views/production-safety/safetyTrainingAndEducation/employeeTrainingRecordCardManagement/configs/form.ts

@@ -13,3 +13,89 @@ export const EDIT_EMPLOYEE_FORM_RULES = {
   score: [{ required: true, message: '请输入成绩', trigger: 'blur' }],
   operationCertificateNum: [{ required: true, message: '请输入操作证号', trigger: 'blur' }],
 };
+
+const isValidYYYYMMDD = (value: string): boolean => {
+  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
+  if (!match) return false;
+
+  const year = Number(match[1]);
+  const month = Number(match[2]); // 1-12
+  const day = Number(match[3]); // 1-31
+  if (month < 1 || month > 12) return false;
+  if (day < 1 || day > 31) return false;
+
+  // 利用 Date 校验闰年、每月天数
+  const date = new Date(year, month - 1, day);
+  return (
+    date.getFullYear() === year &&
+    date.getMonth() === month - 1 &&
+    date.getDate() === day
+  );
+};
+
+const isValidChinaIdCard = (value: string): boolean => {
+  const id = value.trim().toUpperCase();
+  // 15位:纯数字
+  if (/^\d{15}$/.test(id)) return true;
+
+  // 18位:前17位数字 + 校验位数字或X
+  if (!/^\d{17}[\dX]$/.test(id)) return false;
+
+  const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
+  const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4'];
+
+  let sum = 0;
+  for (let i = 0; i < 17; i++) {
+    sum += Number(id[i]) * weights[i];
+  }
+  const mod = sum % 11;
+  const expected = checkCodes[mod];
+  return id[17] === expected;
+};
+
+export const FORM_CARD_RULES = {
+  staffNo: [{ required: true, message: '请输入工号', trigger: 'blur' }],
+  deptIdForSelect: [{ required: true, message: '请选择所/部/中心', trigger: 'change' }],
+  staffName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  staffBirthday: [
+    { required: true, message: '请输入出生年月', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: any, callback: any) => {
+        const v = String(value ?? '').trim();
+        if (!v) return callback(); // required 负责兜底
+        if (!isValidYYYYMMDD(v)) return callback(new Error('出生年月必须为 YYYY-MM-DD 格式'));
+        return callback();
+      },
+      trigger: 'blur',
+    },
+  ],
+  staffIdCard: [
+    { required: true, message: '请输入身份证号', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: any, callback: any) => {
+        const v = String(value ?? '').trim();
+        if (!v) return callback();
+        if (!isValidChinaIdCard(v)) return callback(new Error('身份证号格式不正确'));
+        return callback();
+      },
+      trigger: 'blur',
+    },
+  ],
+  staffAddress: [{ required: true, message: '请输入家庭住址', trigger: 'blur' }],
+  dateOfJoining: [
+    { required: true, message: '请输入入职日期', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: any, callback: any) => {
+        const v = String(value ?? '').trim();
+        if (!v) return callback();
+        if (!isValidYYYYMMDD(v)) return callback(new Error('入职日期必须为 YYYY-MM-DD 格式'));
+        return callback();
+      },
+      trigger: 'blur',
+    },
+  ],
+  staffJob: [{ required: true, message: '请输入从事岗位', trigger: 'blur' }],
+  jobSeniority: [{ required: true, message: '请输入本岗位工龄', trigger: 'blur' }],
+  technicalLvl: [{ required: true, message: '请输入技术等级', trigger: 'blur' }],
+  professionalTitle: [{ required: true, message: '请输入职称', trigger: 'blur' }],
+};

+ 14 - 1
src/views/production-safety/safetyTrainingAndEducation/employeeTrainingRecordCardManagement/employeeTrainingRecordCardManagement.vue

@@ -63,6 +63,10 @@
           >
             <template #action="scope">
               <div class="action-container--div" style="justify-content: left">
+                <ActionButton
+                  text="编辑"
+                  @click="handleEdit(scope.row.id)"
+                />
                 <ActionButton
                   text="删除"
                   :popconfirm="{
@@ -226,7 +230,16 @@
       },
     });
   };
-
+  
+  const handleEdit = (id: number) => {
+    router.push({
+      name: 'employeeTrainingRecordCardManagementEdit',
+      query: {
+        id,
+        operate: 'employee-training-record-card-management-edit',
+      },
+    });
+  };
   onMounted(() => {
     getTableData();
   });

+ 368 - 0
src/views/production-safety/safetyTrainingAndEducation/employeeTrainingRecordCardManagement/employeeTrainingRecordCardManagementEdit.vue

@@ -0,0 +1,368 @@
+<template>
+  <div>
+    <header class="safety-platform-container__header">
+      <BreadcrumbBack />
+      <span class="breadcrumb-title">编辑员工培训记录卡</span>
+    </header>
+    <main class="safety-platform-container__main">
+      <el-form
+        :model="form"
+        :rules="rules"
+        ref="formRef"
+        label-width="150px"
+        style="max-width: 600px"
+        label-position="left"
+      >
+        <el-form-item label="工号:" prop="staffNo">
+          <el-input v-model="form.staffNo" placeholder="输入工号" />
+        </el-form-item>
+        <el-form-item label="姓名:" prop="staffName">
+          <el-input v-model="form.staffName" placeholder="输入姓名" />
+        </el-form-item>
+        <el-form-item label="出生年月:" prop="staffBirthday">
+          <el-input v-model="form.staffBirthday" placeholder="输入出生年月" />
+        </el-form-item>
+        <el-form-item label="身份证号:" prop="staffIdCard">
+          <el-input v-model="form.staffIdCard" placeholder="输入身份证号" />
+        </el-form-item>
+        <el-form-item label="所/部/中心:" prop="deptIdForSelect">
+          <el-cascader
+            v-model="form.deptIdForSelect"
+            :options="deptTree"
+            :props="cascaderProp"
+            clearable
+            :show-all-levels="false"
+            placeholder="选择所/部/中心"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="家庭住址:" prop="staffAddress">
+          <el-input v-model="form.staffAddress" placeholder="输入家庭住址" />
+        </el-form-item>
+        <el-form-item label="入职日期:" prop="dateOfJoining">
+          <el-input v-model="form.dateOfJoining" placeholder="输入入职日期" />
+        </el-form-item>
+        <el-form-item label="文化程度:" prop="highestDegree">
+          <el-input v-model="form.highestDegree" placeholder="输入文化程度" />
+        </el-form-item>
+        <el-form-item label="从事岗位:" prop="staffJob">
+          <el-input v-model="form.staffJob" placeholder="输入从事岗位" />
+        </el-form-item>
+        <el-form-item label="本岗位工龄:" prop="jobSeniority">
+          <el-input v-model="form.jobSeniority" placeholder="输入本岗位工龄" />
+        </el-form-item>
+        <el-form-item label="技术等级:" prop="technicalLvl">
+          <el-input v-model="form.technicalLvl" placeholder="输入技术等级" />
+        </el-form-item>
+        <el-form-item label="职称:" prop="professionalTitle">
+          <el-input v-model="form.professionalTitle" placeholder="输入职称" />
+        </el-form-item>
+        <el-form-item label="员工头像:" prop="staffImg">
+            <el-upload
+              class="image-uploader"
+              ref="staffImgRef"
+              action="#"
+              :file-list="staffImgList"
+              :disabled="isViewMode"
+              :auto-upload="false"
+              accept="image/*"
+              :on-change="handleImageUploadChange"
+              :on-remove="handlePictureCardDelete"
+              :on-preview="handlePictureCardPreview"
+              :before-upload="validateImage"
+              list-type="picture-card">
+                  <el-icon>
+                      <Plus />
+                  </el-icon>
+              <template #tip>
+                <div class="el-upload__tip"> 支持格式:.jpg .png .jpeg,单个文件不能超过300k,设置一个默认图片。</div>
+              </template>
+            </el-upload>
+            <el-dialog v-model="dialogVisible">
+              <img w-full :src="dialogImageUrl" alt="Preview Image" style="width: 100%;" />
+            </el-dialog>
+          </el-form-item>
+        </el-form>
+  
+  
+  
+  
+      <!-- 提交按钮 -->
+      <footer class="safety-platform-container__footer">
+        <el-button @click="router.back()">返回</el-button>
+        <el-button v-if="!isViewMode" type="primary" @click="handleSubmit">
+          {{ isCreateMode ? '提交' : '保存' }}
+        </el-button>
+      </footer>
+    </main>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, onMounted, ref, reactive } from 'vue';
+  import { useRoute, useRouter } from 'vue-router';
+  import { ElMessage, UploadRawFile } from 'element-plus';
+  import type { FormInstance } from 'element-plus';
+  import { Plus, Delete, ZoomIn } from '@element-plus/icons-vue';
+  import { FORM_CARD_RULES } from './configs/form';
+  import {
+    getEducationStaffTrainingCardDetail,
+    updateEducationStaffTrainingCard,
+    type FormDataType,
+  } from '@/api/employee-training-record-card-management';
+  import { DeptTree } from '@/types/dept/type';
+  import { getAllDepartments } from '@/api/auth/dept';
+  import { debounce } from 'lodash-es';
+  import { uploadFileApi, UPLOAD_BIZ_TYPE } from '@/api/minio';
+  import type { FileItem } from '@/components/UploadFiles/types';
+  const router = useRouter();
+  const route = useRoute();
+
+  const operate = computed(() => (route.query.operate as string) || 'education-training-plan-management-create');
+  const currentId = computed(() => Number(route.query.id));
+
+  const isCreateMode = computed(() => operate.value === 'education-training-plan-management-create');
+  const isEditMode = computed(() => operate.value === 'education-training-plan-management-edit');
+  const isViewMode = computed(() => operate.value === 'education-training-plan-management-view');
+
+  // 表单数据
+  const form = reactive<FormDataType>({
+    staffNo: '',
+    deptName: '',
+    deptId: '',
+    deptIdForSelect: undefined,
+    staffName: '',
+    staffBirthday: '',
+    staffIdCard: '',
+    staffAddress: '',
+    dateOfJoining: '',
+    staffJob: '',
+    technicalLvl: '',
+    professionalTitle: '',
+    highestDegree: '',
+    jobSeniority: 0,
+    staffImg:[]
+  });
+  const cascaderProp = {
+    multiple: false,
+    expandTrigger: 'hover',
+    checkStrictly: true,
+    emitPath: false,
+    value: 'id',
+    label: 'deptName',
+  };
+  // 获取级联部门数据
+  const deptTree = ref<DeptTree[]>();
+  const loadDeptTreeData = async () => {
+    const result = await getAllDepartments();
+    deptTree.value = result[0].children;
+  };
+
+  /** 根据部门 id 从树中取名称(最后一级所选节点) */
+  const getDeptNameById = (nodes: DeptTree[] | undefined, id: number): string => {
+    if (!nodes?.length || id == null || Number.isNaN(id)) return '';
+    const nameById = new Map<number, string>();
+    const walk = (list: DeptTree[]) => {
+      for (const n of list) {
+        if (n.id != null) nameById.set(Number(n.id), n.deptName);
+        if (n.children?.length) walk(n.children);
+      }
+    };
+    walk(nodes);
+    return nameById.get(id) ?? '';
+  };
+  const fileList = ref([])
+  const staffImgList = ref<FileItem[]>([]);
+  // 表单引用
+  const formRef = ref<FormInstance>();
+
+  // 表单验证规则
+  const rules = reactive(FORM_CARD_RULES);
+
+  const handleValidate = async () => {
+    if (!formRef.value) return;
+    const res = await formRef.value.validateField();
+    return res;
+  };
+  const parseDeptIds = (ids: string | string[] | number[] | undefined | null): number[] => {
+    if (!ids) {
+      return [];
+    }
+
+    if (Array.isArray(ids)) {
+      return ids.map((v: any) => Number(v)).filter((id) => !isNaN(id));
+    }
+
+    return String(ids)
+      .split(',')
+      .map(Number)
+      .filter((id) => !isNaN(id));
+  };
+
+  /** 后端若返回逗号分隔 id,只取最后一位(最后一级部门) */
+  const parseLastDeptId = (raw: string | string[] | number[] | undefined | null): number | undefined => {
+    const ids = parseDeptIds(raw);
+    return ids.length ? ids[ids.length - 1] : undefined;
+  };
+
+  const getDetail = async () => {
+    if (!currentId.value) return;
+    try {
+      const res = await getEducationStaffTrainingCardDetail(currentId.value);
+      if (res) {
+        Object.assign(form, {
+          ...res,
+          deptIdForSelect: parseLastDeptId((res as FormDataType).deptId),
+        });
+        if(res.staffImg){
+            form.staffImg = JSON.parse(res.staffImg)
+            staffImgList.value = JSON.parse(res.staffImg) || []
+        }
+      }
+    } catch (e) {
+      ElMessage.error('获取详情失败');
+    }
+  };
+     // 上传文件
+ const formatAttachment = async (data: any) => {
+    if (!data) return data;
+    const uuid = Math.random().toString(36).substring(2, 9);
+    const timestamp = Date.now().toString();
+    const random = Math.random().toString(36).substring(2, 4);
+    const fileName = data.name;
+    const res = await uploadFileApi({
+        bizType: UPLOAD_BIZ_TYPE.ATTACHMENT,
+        fileName: `${uuid}-${timestamp}-${random}`,
+        file: data,
+    });
+    return res;
+};
+    //  上传图片
+  const handleImageUploadChange = async (file: any, fileLists: any) => {
+    if(file.raw){
+        try {
+            const res = await formatAttachment(file.raw);
+            
+            const targetFile = fileLists.find(f => f.uid === file.uid);
+            if (targetFile) {
+                targetFile.url = res.url; 
+                targetFile.contentType = res.contentType
+            }
+            staffImgList.value = fileLists; 
+            form.staffImg = JSON.stringify(fileLists);
+            ElMessage.success('上传成功');
+        } catch (error) {
+            ElMessage.error('上传失败,请重试');
+            // 上传失败时,可以从 fileLists 中移除该文件
+            staffImgList.value = fileLists.filter(f => f.uid !== file.uid);
+        }
+    }
+  };
+  // 替换图片
+  const staffImgRef = ref();
+  const handleImageExceed = (files) => {
+    staffImgRef.value!.clearFiles(); 
+    const file = files[0] as UploadRawFile;
+    console.log(file)
+    if (!validateImage(file)) {
+      return;
+    }
+    staffImgRef.value!.handleStart(file); 
+  };
+// 图片预览
+  const dialogVisible = ref(false);
+  const dialogImageUrl = ref('');
+  const handlePictureCardPreview = (file: any) => {
+    dialogImageUrl.value = file.url;
+    dialogVisible.value = true;
+  };
+
+  const handlePictureCardDelete = (file, fileLists)=>{
+    staffImgRef.value = fileLists.filter(f => f.uid !== file.uid);
+    form.staffImg = JSON.stringify(staffImgRef.value);
+  }
+
+// 图片格式校验
+  const validateImage = (file) => {
+    const validMIME = [
+      'image/jpeg',
+      'image/png',
+      'image/gif',
+      'image/bmp',
+      'image/webp',
+      'image/svg+xml',
+      'image/tiff',
+      'image/heic',
+      'image/heif',
+      'image/avif',
+    ];
+
+    if (!validMIME.includes(file.type)) {
+      ElMessage.error('仅支持图片文件(JPEG/PNG/GIF/BMP/WEBP/SVG/TIFF/HEIC/AVIF等)');
+      return false;
+    }
+
+    if (!validMIME.includes(file.type)) {
+      ElMessage.error('仅支持JPG/PNG格式图片');
+      return false; // 阻止上传
+    }
+
+    // 可选:添加文件大小限制
+    const maxSize = 5 * 1024 * 1024; // 5MB
+    if (file.size > maxSize) {
+      ElMessage.error('图片大小不能超过5MB');
+      return false;
+    }
+
+    return true; // 验证通过
+  };
+  const handleSubmit = debounce(async () => {
+    const res = await handleValidate();
+    if (!res) return;
+    try {
+      const { deptIdForSelect, ...rest } = form;
+      const id = deptIdForSelect;
+      const basePayload = {
+        ...rest,
+        deptId: id != null && !Number.isNaN(id) ? String(id) : '',
+        deptName: id != null ? getDeptNameById(deptTree.value, id) : '',
+      };
+
+      await updateEducationStaffTrainingCard({
+        id: currentId.value,
+        ...basePayload,
+      });
+      ElMessage.success('保存成功');
+
+      router.back();
+    } catch (e) {
+      ElMessage.error('保存失败,请重试');
+    }
+  }, 1000);
+
+  onMounted(() => {
+    loadDeptTreeData();
+    getDetail();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+
+  .safety-platform-container__header {
+    flex-direction: row !important;
+    justify-content: flex-start !important;
+    gap: 8px !important;
+  }
+  .el-form-item {
+    margin-bottom: 25px;
+  }
+  li{
+    list-style: none;
+  }
+  .border-b{
+    border-bottom: 1px solid #efefef;
+    padding-bottom:10px;
+    margin-bottom:10px;
+  }
+</style>