|
|
@@ -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>
|