| src/api/equipmentManagement/repair.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/equipmentManagement/repairFileType.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/equipmentManagement/repair/Modal/MaintainModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/equipmentManagement/repair/Modal/RepairDetailModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/equipmentManagement/repair/Modal/RepairModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/equipmentManagement/repair/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/equipmentManagement/repair.js
@@ -26,7 +26,7 @@ /** * @desc è®¾å¤æ¥ä¿®-ä¸ä¼ éä»¶ * @param {FormData} formData file + deviceRepairId * @param {FormData} formData file + deviceRepairId + fileType(14设å¤é®é¢/15ç»´ä¿®å®æ) */ export const uploadRepairFile = (formData) => { return request({ @@ -108,3 +108,14 @@ data, }); }; /** * @desc è®¾å¤æ¥ä¿®éªæ¶ */ export const acceptRepair = (data) => { return request({ url: `/device/repair/acceptance`, method: "post", data, }); }; src/api/equipmentManagement/repairFileType.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,11 @@ /** è®¾å¤æ¥ä¿®éä»¶ç±»å */ export const REPAIR_FILE_TYPE_LEGACY = 13; export const REPAIR_FILE_TYPE_PROBLEM = 14; export const REPAIR_FILE_TYPE_MAINTAIN = 15; /** æ¯å¦è®¾å¤é®é¢ç±»å¾ç */ export const isProblemRepairFile = (type) => type === REPAIR_FILE_TYPE_PROBLEM || type === REPAIR_FILE_TYPE_LEGACY; /** æ¯å¦ç»´ä¿®å®æç±»å¾ç */ export const isMaintainRepairFile = (type) => type === REPAIR_FILE_TYPE_MAINTAIN; src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,125 @@ <template> <FormDialog v-model="visible" title="éªæ¶å®¡æ¹" width="520px" :loading="loading" @confirm="sendForm" @cancel="handleCancel" @close="handleClose" > <el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> <el-form-item label="éªæ¶äºº" prop="acceptanceName"> <el-input v-model="form.acceptanceName" disabled placeholder="æ¥ä¿®æ¶æå®çéªæ¶äºº" /> </el-form-item> <el-form-item label="éªæ¶æ¶é´" prop="acceptanceTime"> <el-date-picker v-model="form.acceptanceTime" type="datetime" placeholder="è¯·éæ©éªæ¶æ¶é´" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" /> </el-form-item> <el-form-item label="éªæ¶å¤æ³¨" prop="acceptanceRemark"> <el-input v-model="form.acceptanceRemark" type="textarea" :rows="3" placeholder="请è¾å ¥éªæ¶å¤æ³¨" /> </el-form-item> </el-form> </FormDialog> </template> <script setup> import FormDialog from "@/components/Dialog/FormDialog.vue"; import { acceptRepair } from "@/api/equipmentManagement/repair"; import useFormData from "@/hooks/useFormData"; import useUserStore from "@/store/modules/user"; import dayjs from "dayjs"; import { ElMessage } from "element-plus"; defineOptions({ name: "è®¾å¤æ¥ä¿®éªæ¶å¼¹çª", }); const emits = defineEmits(["ok"]); const repairId = ref(); const visible = ref(false); const loading = ref(false); const formRef = ref(); const userStore = useUserStore(); const rules = { acceptanceName: [{ required: true, message: "éªæ¶äººä¸è½ä¸ºç©º", trigger: "blur" }], acceptanceTime: [{ required: true, message: "è¯·éæ©éªæ¶æ¶é´", trigger: "change" }], acceptanceRemark: [{ required: true, message: "请è¾å ¥éªæ¶å¤æ³¨", trigger: "blur" }], }; const { form, resetForm } = useFormData({ acceptanceName: undefined, acceptanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), acceptanceRemark: undefined, }); const setForm = (row) => { form.acceptanceName = row.acceptanceName; form.acceptanceTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); form.acceptanceRemark = undefined; }; const sendForm = async () => { const valid = await formRef.value?.validate().catch(() => false); if (!valid) return; if (form.acceptanceName !== userStore.nickName) { ElMessage.warning("ä» æå®çéªæ¶äººå¯è¿è¡éªæ¶"); return; } loading.value = true; try { const { code, msg } = await acceptRepair({ id: repairId.value, acceptanceTime: form.acceptanceTime, acceptanceRemark: form.acceptanceRemark, }); if (code === 200) { ElMessage.success("éªæ¶æå"); visible.value = false; emits("ok"); } else if (msg) { ElMessage.error(msg); } } finally { loading.value = false; } }; const handleCancel = () => { resetForm(); visible.value = false; }; const handleClose = () => { resetForm(); visible.value = false; }; const open = async (id, row) => { if (!row?.acceptanceName || row.acceptanceName !== userStore.nickName) { ElMessage.warning("ä» æå®çéªæ¶äººå¯è¿è¡éªæ¶"); return; } repairId.value = id; visible.value = true; await nextTick(); setForm(row); }; defineExpose({ open, }); </script> src/views/equipmentManagement/repair/Modal/MaintainModal.vue
@@ -1,27 +1,27 @@ <template> <FormDialog v-model="visible" :title="'设å¤ç»´ä¿®'" width="500px" title="设å¤ç»´ä¿®" width="640px" :loading="loading" @confirm="sendForm" @cancel="handleCancel" @close="handleClose" > <el-form :model="form" label-width="80px"> <el-form-item label="维修人"> <el-input v-model="form.maintenanceName" placeholder="请è¾å ¥ç»´ä¿®äºº" /> <el-form ref="formRef" :model="form" :rules="rules" label-width="90px"> <el-form-item label="维修人" prop="maintenanceName"> <el-input v-model="form.maintenanceName" disabled placeholder="æ¥ä¿®æ¶æå®ç维修人" /> </el-form-item> <el-form-item label="ç»´ä¿®ç»æ"> <el-form-item label="ç»´ä¿®ç»æ" prop="maintenanceResult"> <el-input v-model="form.maintenanceResult" placeholder="请è¾å ¥ç»´ä¿®ç»æ" /> </el-form-item> <el-form-item label="ç»´ä¿®ç¶æ"> <el-select v-model="form.status"> <el-option label="å¾ æ¥ä¿®" :value="0"></el-option> <el-option label="å®ç»" :value="1"></el-option> <el-option label="失败" :value="2"></el-option> <el-form-item label="ç»´ä¿®ç¶æ" prop="status"> <el-select v-model="form.status" style="width: 100%"> <el-option label="å¾ éªæ¶" :value="3" /> <el-option label="维修失败" :value="2" /> </el-select> </el-form-item> <el-form-item label="ç»´ä¿®æ¥æ"> <el-form-item label="ç»´ä¿®æ¥æ" prop="maintenanceTime"> <el-date-picker v-model="form.maintenanceTime" placeholder="è¯·éæ©ç»´ä¿®æ¥æ" @@ -32,17 +32,39 @@ style="width: 100%" /> </el-form-item> <el-form-item label="éä»¶"> <el-upload class="repair-attachment-upload" v-model:file-list="attachmentFileList" drag multiple :auto-upload="false" :limit="9" accept="image/png,image/jpeg,image/jpg" :before-upload="beforeAttachmentUpload" > <el-icon class="repair-upload-icon"><UploadFilled /></el-icon> <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ <em>ç¹å»éæ©æä»¶</em></div> <template #tip> <div class="el-upload__tip"> æ¯æ png / jpg / jpegï¼å个ä¸è¶ è¿ 50MBï¼ä¿ååä¸å表ãéä»¶ã忥 </div> </template> </el-upload> </el-form-item> </el-form> </FormDialog> </template> <script setup> import FormDialog from "@/components/Dialog/FormDialog.vue"; import { addMaintain } from "@/api/equipmentManagement/repair"; import { addMaintain, uploadRepairFile } from "@/api/equipmentManagement/repair"; import { REPAIR_FILE_TYPE_MAINTAIN } from "@/api/equipmentManagement/repairFileType.js"; import useFormData from "@/hooks/useFormData"; import useUserStore from "@/store/modules/user"; import dayjs from "dayjs"; import { ElMessage } from "element-plus"; import { UploadFilled } from "@element-plus/icons-vue"; defineOptions({ name: "ç»´ä¿®æ¨¡ææ¡", @@ -50,39 +72,95 @@ const emits = defineEmits(["ok"]); // ä¿åæ¥ä¿®è®°å½çid const repairId = ref(); const visible = ref(false); const loading = ref(false); const formRef = ref(); const attachmentFileList = ref([]); const ATTACH_MAX_MB = 50; const userStore = useUserStore(); const rules = { maintenanceName: [{ required: true, message: "è¯·éæ©ç»´ä¿®äºº", trigger: "change" }], maintenanceResult: [{ required: true, message: "请è¾å ¥ç»´ä¿®ç»æ", trigger: "blur" }], maintenanceTime: [{ required: true, message: "è¯·éæ©ç»´ä¿®æ¥æ", trigger: "change" }], }; const { form, resetForm } = useFormData({ maintenanceName: undefined, // ç»´ä¿®åç§° maintenanceResult: undefined, // ç»´ä¿®ç»æ maintenanceTime: undefined, // ç»´ä¿®æ¥æ status: 0, maintenanceName: undefined, maintenanceResult: undefined, maintenanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), status: 3, }); const syncMaintenanceNameFromRow = (row) => { form.maintenanceName = row?.maintenanceName || ""; }; const beforeAttachmentUpload = (rawFile) => { const okType = ["image/jpeg", "image/jpg", "image/png"].includes(rawFile.type); if (!okType) { ElMessage.error("åªè½ä¸ä¼ png / jpg / jpeg å¾ç"); return false; } if (rawFile.size > ATTACH_MAX_MB * 1024 * 1024) { ElMessage.error(`å个æä»¶ä¸è½è¶ è¿ ${ATTACH_MAX_MB}MB`); return false; } return true; }; const clearAttachmentQueue = () => { attachmentFileList.value = []; }; const uploadQueuedImages = async (id) => { const files = (attachmentFileList.value || []).map((f) => f.raw).filter(Boolean); for (const file of files) { const fd = new FormData(); fd.append("file", file); fd.append("deviceRepairId", String(id)); fd.append("fileType", String(REPAIR_FILE_TYPE_MAINTAIN)); const res = await uploadRepairFile(fd); if (res.code !== 200) { throw new Error(res.msg || "éä»¶ä¸ä¼ 失败"); } } }; const setForm = (data) => { form.maintenanceName = data.maintenanceName ?? userStore.nickName; syncMaintenanceNameFromRow(data); form.maintenanceResult = data.maintenanceResult; form.maintenanceTime = data.maintenanceTime form.maintenanceTime = data.maintenanceTime ? dayjs(data.maintenanceTime).format("YYYY-MM-DD HH:mm:ss") : dayjs().format("YYYY-MM-DD HH:mm:ss"); form.status = 1; // é»è®¤ç¶æä¸ºå®ç» form.status = 3; }; const sendForm = async () => { const valid = await formRef.value?.validate().catch(() => false); if (!valid) return; if (form.maintenanceName !== userStore.nickName) { ElMessage.warning("ä» æå®ç维修人å¯è¿è¡ç»´ä¿®"); return; } loading.value = true; try { const { code } = await addMaintain({ id: repairId.value, ...form }); if (code == 200) { ElMessage.success("ç»´ä¿®æå"); emits("ok"); resetForm(); visible.value = false; const { code, msg } = await addMaintain({ id: repairId.value, ...form }); if (code !== 200) { if (msg) ElMessage.error(msg); return; } if (attachmentFileList.value.length) { await uploadQueuedImages(repairId.value); } ElMessage.success("ç»´ä¿®æå"); clearAttachmentQueue(); visible.value = false; emits("ok"); } catch (e) { ElMessage.error(e?.message || "ä¿åæä¸ä¼ é件失败"); } finally { loading.value = false; } @@ -90,19 +168,26 @@ const handleCancel = () => { resetForm(); clearAttachmentQueue(); visible.value = false; }; const handleClose = () => { resetForm(); clearAttachmentQueue(); visible.value = false; }; const open = async (id, row) => { repairId.value = id; // ä¿åæ¥ä¿®è®°å½çid repairId.value = id; clearAttachmentQueue(); visible.value = true; await nextTick(); setForm(row); if (row?.maintenanceName && row.maintenanceName !== userStore.nickName) { ElMessage.warning("ä» æå®ç维修人å¯è¿è¡ç»´ä¿®"); visible.value = false; } }; defineExpose({ @@ -110,4 +195,20 @@ }); </script> <style lang="scss" scoped></style> <style lang="scss" scoped> .repair-attachment-upload { width: 100%; :deep(.el-upload) { width: 100%; } :deep(.el-upload-dragger) { width: 100%; padding: 20px 16px; } } .repair-upload-icon { font-size: 42px; color: var(--el-color-primary); margin-bottom: 8px; } </style> src/views/equipmentManagement/repair/Modal/RepairDetailModal.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,208 @@ <template> <el-dialog v-model="visible" title="æ¥ä¿®å ¨è¿ç¨" width="820px" destroy-on-close @closed="onClosed" > <div v-loading="loading" class="repair-detail"> <template v-if="detail"> <div class="phase-block"> <div class="phase-title">1. æ¥ä¿®ç»è®°</div> <el-descriptions :column="2" border size="small"> <el-descriptions-item label="设å¤åç§°">{{ detail.deviceName || "â" }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼åå·">{{ detail.deviceModel || "â" }}</el-descriptions-item> <el-descriptions-item label="æ¥ä¿®æ¥æ">{{ fmtDate(detail.repairTime) }}</el-descriptions-item> <el-descriptions-item label="æ¥ä¿®äºº">{{ detail.repairName || "â" }}</el-descriptions-item> <el-descriptions-item label="éªæ¶äºº">{{ detail.acceptanceName || "â" }}</el-descriptions-item> <el-descriptions-item label="维修人">{{ detail.maintenanceName || "â" }}</el-descriptions-item> <el-descriptions-item label="æ éç°è±¡" :span="2">{{ detail.remark || "â" }}</el-descriptions-item> <el-descriptions-item label="å½åç¶æ" :span="2"> <el-tag v-if="detail.status === 0" type="warning" size="small">å¾ ç»´ä¿®</el-tag> <el-tag v-else-if="detail.status === 3" type="info" size="small">å¾ éªæ¶</el-tag> <el-tag v-else-if="detail.status === 1" type="success" size="small">宿</el-tag> <el-tag v-else-if="detail.status === 2" type="danger" size="small">维修失败</el-tag> <span v-else>â</span> </el-descriptions-item> </el-descriptions> <div class="img-row-label">设å¤é®é¢å¾ç</div> <div v-if="problemImages.length" class="img-grid"> <el-image v-for="img in problemImages" :key="img.id" :src="img.url" fit="cover" class="thumb" :preview-src-list="problemImages.map((i) => i.url)" preview-teleported /> </div> <el-empty v-else description="ææ è®¾å¤é®é¢å¾ç" :image-size="56" /> </div> <div v-if="hasMaintenanceStep" class="phase-block"> <div class="phase-title">2. ç»´ä¿®å¤ç</div> <el-descriptions :column="2" border size="small"> <el-descriptions-item label="维修人">{{ detail.maintenanceName || "â" }}</el-descriptions-item> <el-descriptions-item label="ç»´ä¿®æ¶é´">{{ fmtDateTime(detail.maintenanceTime) }}</el-descriptions-item> <el-descriptions-item label="ç»´ä¿®ç»æ" :span="2">{{ detail.maintenanceResult || "â" }}</el-descriptions-item> </el-descriptions> <div v-if="maintainImages.length" class="img-row-label">ç»´ä¿®å®æå¾ç</div> <div v-if="maintainImages.length" class="img-grid"> <el-image v-for="img in maintainImages" :key="img.id" :src="img.url" fit="cover" class="thumb" :preview-src-list="maintainImages.map((i) => i.url)" preview-teleported /> </div> </div> <div v-if="detail.status === 1" class="phase-block"> <div class="phase-title">3. éªæ¶</div> <el-descriptions :column="2" border size="small"> <el-descriptions-item label="éªæ¶äºº">{{ detail.acceptanceName || "â" }}</el-descriptions-item> <el-descriptions-item label="éªæ¶æ¶é´">{{ fmtDateTime(detail.acceptanceTime) }}</el-descriptions-item> <el-descriptions-item label="éªæ¶å¤æ³¨" :span="2">{{ detail.acceptanceRemark || "â" }}</el-descriptions-item> </el-descriptions> </div> </template> </div> <template #footer> <el-button type="primary" @click="visible = false">å ³é</el-button> </template> </el-dialog> </template> <script setup> import { computed } from "vue"; import dayjs from "dayjs"; import { getRepairById, getRepairFileList } from "@/api/equipmentManagement/repair"; import { isProblemRepairFile, isMaintainRepairFile } from "@/api/equipmentManagement/repairFileType.js"; defineOptions({ name: "RepairDetailModal" }); const props = defineProps({ /** ä¸å表页ãéä»¶å¼¹çªä¸è´ï¼ç¨äºæ¼éæèµæºå®æ´å°åï¼åç»ä»¶æ å ¨å± proxy æ¶éç±ç¶é¡µä¼ å ¥ï¼ */ javaApi: { type: String, default: "", }, }); const visible = ref(false); const loading = ref(false); const detail = ref(null); const problemImages = ref([]); const maintainImages = ref([]); const apiBase = computed( () => props.javaApi || import.meta.env.VITE_APP_BASE_API || "" ); const hasMaintenanceStep = computed(() => { const s = detail.value?.status; return s === 2 || s === 3 || s === 1; }); const fmtDate = (v) => (v ? dayjs(v).format("YYYY-MM-DD") : "â"); const fmtDateTime = (v) => (v ? dayjs(v).format("YYYY-MM-DD HH:mm:ss") : "â"); /** ä¸è®¾å¤æ¥ä¿® index.vue ä¸ getFileAccessUrl + normalizeFileUrl é»è¾ä¸è´ */ const buildFileUrl = (file = {}) => { let raw = ""; if (file.link) { if (String(file.link).startsWith("http")) return file.link; raw = file.link; } else { raw = file.url || ""; } if (!raw) return ""; if (String(raw).startsWith("http")) return raw; let fileUrl = raw; if (fileUrl && fileUrl.indexOf("\\") > -1) { const lowerPath = fileUrl.toLowerCase(); const uploadPathIndex = lowerPath.indexOf("uploadpath"); if (uploadPathIndex > -1) { fileUrl = fileUrl.substring(uploadPathIndex).replace(/\\/g, "/"); } else { fileUrl = fileUrl.replace(/\\/g, "/"); } } fileUrl = fileUrl.replace(/^\/?uploadPath/, "/profile"); if (!fileUrl.startsWith("http")) { if (!fileUrl.startsWith("/")) fileUrl = "/" + fileUrl; fileUrl = apiBase.value + fileUrl; } return fileUrl; }; const open = async (id) => { if (!id) return; visible.value = true; loading.value = true; detail.value = null; problemImages.value = []; maintainImages.value = []; try { const { data } = await getRepairById(id); detail.value = data || null; const fileRes = await getRepairFileList(id); const list = Array.isArray(fileRes?.data) ? fileRes.data : []; problemImages.value = list .filter((f) => isProblemRepairFile(f.type)) .map((f) => ({ id: f.id, url: buildFileUrl(f) })); maintainImages.value = list .filter((f) => isMaintainRepairFile(f.type)) .map((f) => ({ id: f.id, url: buildFileUrl(f) })); } finally { loading.value = false; } }; const onClosed = () => { detail.value = null; problemImages.value = []; maintainImages.value = []; }; defineExpose({ open }); </script> <style scoped> .repair-detail { min-height: 120px; } .phase-block { margin-bottom: 20px; } .phase-title { font-size: 15px; font-weight: 600; color: #303133; margin-bottom: 10px; padding-left: 8px; border-left: 3px solid var(--el-color-primary); } .img-row-label { font-size: 13px; color: #606266; margin: 14px 0 8px; font-weight: 500; } .img-grid { display: flex; flex-wrap: wrap; gap: 8px; } .thumb { width: 88px; height: 88px; border-radius: 6px; border: 1px solid var(--el-border-color); } </style> src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -2,7 +2,7 @@ <FormDialog v-model="visible" :title="id ? 'ç¼è¾è®¾å¤æ¥ä¿®' : 'æ°å¢è®¾å¤æ¥ä¿®'" width="800px" width="880px" @confirm="sendForm" @cancel="handleCancel" @close="handleClose" @@ -11,7 +11,7 @@ <el-row> <el-col :span="12"> <el-form-item label="设å¤åç§°"> <el-select v-model="form.deviceLedgerId" @change="setDeviceModel" filterable> <el-select v-model="form.deviceLedgerId" placeholder="è¯·éæ©" @change="setDeviceModel" filterable style="width: 100%"> <el-option v-for="(item, index) in deviceOptions" :key="index" @@ -30,6 +30,8 @@ /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="æ¥ä¿®æ¥æ"> <el-date-picker @@ -45,7 +47,45 @@ </el-col> <el-col :span="12"> <el-form-item label="æ¥ä¿®äºº"> <el-input v-model="form.repairName" placeholder="请è¾å ¥æ¥ä¿®äºº" /> <el-input v-model="form.repairName" disabled placeholder="å½åç»å½äºº" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="éªæ¶äºº"> <el-select v-model="form.acceptanceName" placeholder="è¯·éæ©éªæ¶äºº" filterable clearable style="width: 100%" > <el-option v-for="item in userList" :key="item.userId ?? item.nickName" :label="item.nickName" :value="item.nickName" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="维修人" required> <el-select v-model="form.maintenanceName" placeholder="è¯·éæ©ç»´ä¿®äºº" filterable clearable style="width: 100%" > <el-option v-for="item in userList" :key="'m-' + (item.userId ?? item.nickName)" :label="item.nickName" :value="item.nickName" /> </el-select> </el-form-item> </el-col> </el-row> @@ -72,22 +112,63 @@ </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="设å¤é®é¢å¾ç"> <div v-if="id" class="problem-existing-wrap"> <div v-for="img in displayedExistingProblems" :key="img.id" class="problem-thumb" > <el-image :src="img.url" fit="cover" class="problem-thumb-img" @click="previewProblem(img.url)" /> <el-icon class="problem-thumb-remove" @click.stop="markProblemRemove(img.id)"><Close /></el-icon> </div> </div> <el-upload class="repair-attachment-upload" v-model:file-list="attachmentFileList" drag multiple :auto-upload="false" :limit="9" accept="image/png,image/jpeg,image/jpg" :before-upload="beforeAttachmentUpload" > <el-icon class="repair-upload-icon"><UploadFilled /></el-icon> <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ <em>ç¹å»éæ©æä»¶</em></div> <template #tip> <div class="el-upload__tip"> 设å¤é®é¢å¾ï¼{{ id ? 'ç¼è¾æ¶å é¤éç¹å¼¹çªåºé¨ã确认ãåæçæï¼' : '' }}æ¯æ png / jpg / jpegï¼åå¼ ä¸è¶ è¿ 50MBï¼æå¤ 9 å¼ </div> </template> </el-upload> </el-form-item> </el-col> </el-row> </el-form> </FormDialog> </template> <script setup> import { getCurrentInstance, computed } from "vue"; import FormDialog from "@/components/Dialog/FormDialog.vue"; import { addRepair, editRepair, getRepairById, getRepairFileList, uploadRepairFile, deleteRepairFile, } from "@/api/equipmentManagement/repair"; import { REPAIR_FILE_TYPE_PROBLEM, isProblemRepairFile } from "@/api/equipmentManagement/repairFileType.js"; import { ElMessage } from "element-plus"; import { UploadFilled, Close } from "@element-plus/icons-vue"; import dayjs from "dayjs"; import useFormData from "@/hooks/useFormData"; import { getDeviceLedger } from "@/api/equipmentManagement/ledger"; import useUserStore from "@/store/modules/user"; import { userListNoPage } from "@/api/system/user.js"; defineOptions({ name: "è®¾å¤æ¥ä¿®å¼¹çª", @@ -101,10 +182,133 @@ const userStore = useUserStore(); const deviceOptions = ref([]); const userList = ref([]); /** å¾ æäº¤çæ°å¢è®¾å¤é®é¢å¾ï¼ç¡®è®¤ä¿ååä¸ä¼ ï¼ */ const attachmentFileList = ref([]); /** ç¼è¾æ¶ï¼å·²åå¨ç设å¤é®é¢å¾ï¼æå¡ç«¯ï¼ */ const existingProblemImages = ref([]); /** ç¼è¾æ¶ï¼æ è®°å¾ å é¤çå¾ç idï¼ä» ç¹ã确认ãåè°æ¥å£å é¤ */ const pendingRemoveProblemIds = ref([]); const displayedExistingProblems = computed(() => existingProblemImages.value.filter((img) => !pendingRemoveProblemIds.value.includes(img.id)) ); const getFileAccessUrlForModal = (file = {}) => { let raw = file?.link || file?.url || ""; if (!raw) return ""; if (String(raw).startsWith("http")) return raw; const javaApi = getCurrentInstance()?.proxy?.javaApi || ""; let fileUrl = raw; if (fileUrl.indexOf("\\") > -1) { const lowerPath = fileUrl.toLowerCase(); const uploadPathIndex = lowerPath.indexOf("uploadpath"); if (uploadPathIndex > -1) { fileUrl = fileUrl.substring(uploadPathIndex).replace(/\\/g, "/"); } else { fileUrl = fileUrl.replace(/\\/g, "/"); } } fileUrl = fileUrl.replace(/^\/?uploadPath/, "/profile"); if (!fileUrl.startsWith("http")) { if (!fileUrl.startsWith("/")) fileUrl = "/" + fileUrl; fileUrl = javaApi + fileUrl; } return fileUrl; }; const markProblemRemove = (fileId) => { if (!pendingRemoveProblemIds.value.includes(fileId)) { pendingRemoveProblemIds.value.push(fileId); } }; const previewProblem = (url) => { window.open(url, "_blank"); }; const loadExistingProblemImages = async (repairId) => { existingProblemImages.value = []; pendingRemoveProblemIds.value = []; if (!repairId) return; try { const res = await getRepairFileList(repairId); const list = Array.isArray(res?.data) ? res.data : []; existingProblemImages.value = list .filter((item) => isProblemRepairFile(item.type)) .map((item) => ({ id: item.id, name: item.name, url: getFileAccessUrlForModal(item), })); } catch { existingProblemImages.value = []; } }; const resetProblemAttachmentState = () => { existingProblemImages.value = []; pendingRemoveProblemIds.value = []; }; const ATTACH_MAX_MB = 50; const loadDeviceName = async () => { const { data } = await getDeviceLedger(); deviceOptions.value = data; }; const loadUserList = async () => { const res = await userListNoPage(); userList.value = Array.isArray(res?.data) ? res.data : []; }; /** æ¥ä¿®äººåºå®ä¸ºå½åç»å½ç¨æ·ï¼ä¸å¯æ¹ï¼ */ const syncRepairNameToLoginUser = () => { form.repairName = userStore.nickName || ""; }; const beforeAttachmentUpload = (rawFile) => { const okType = ["image/jpeg", "image/jpg", "image/png"].includes(rawFile.type); if (!okType) { ElMessage.error("åªè½ä¸ä¼ png / jpg / jpeg å¾ç"); return false; } const maxBytes = ATTACH_MAX_MB * 1024 * 1024; if (rawFile.size > maxBytes) { ElMessage.error(`å个æä»¶ä¸è½è¶ è¿ ${ATTACH_MAX_MB}MB`); return false; } return true; }; const clearAttachmentQueue = () => { attachmentFileList.value = []; }; const applyPendingProblemDeletes = async () => { for (const fileId of pendingRemoveProblemIds.value) { const { code } = await deleteRepairFile(fileId); if (code !== 200) { throw new Error("å é¤è®¾å¤é®é¢å¾ç失败"); } } pendingRemoveProblemIds.value = []; }; const uploadQueuedRepairImages = async (repairId) => { const rows = attachmentFileList.value || []; const files = rows.map((f) => f.raw).filter(Boolean); if (!files.length || repairId == null) return; for (const file of files) { const fd = new FormData(); fd.append("file", file); fd.append("deviceRepairId", String(repairId)); fd.append("fileType", String(REPAIR_FILE_TYPE_PROBLEM)); const res = await uploadRepairFile(fd); if (res.code !== 200) { throw new Error(res.msg || "éä»¶ä¸ä¼ 失败"); } } }; const { form, resetForm } = useFormData({ @@ -113,13 +317,15 @@ deviceModel: undefined, // è§æ ¼åå· repairTime: dayjs().format("YYYY-MM-DD"), // æ¥ä¿®æ¥æï¼é»è®¤å½å¤© repairName: userStore.nickName, // æ¥ä¿®äºº acceptanceName: undefined, // éªæ¶äºº maintenanceName: undefined, // 维修人ï¼å¯éï¼ä¸åè¡¨ç»´ä¿®äººåæºï¼ remark: undefined, // æ éç°è±¡ status: 0, // æ¥ä¿®ç¶æ }); const setDeviceModel = (deviceId) => { const option = deviceOptions.value.find((item) => item.id === deviceId); form.deviceModel = option.deviceModel; form.deviceModel = option?.deviceModel; }; const setForm = (data) => { @@ -127,22 +333,53 @@ form.deviceName = data.deviceName; form.deviceModel = data.deviceModel; form.repairTime = data.repairTime; form.repairName = data.repairName; form.acceptanceName = data.acceptanceName; form.maintenanceName = data.maintenanceName; form.remark = data.remark; form.status = data.status; syncRepairNameToLoginUser(); }; const sendForm = async () => { syncRepairNameToLoginUser(); if (!form.deviceLedgerId) { ElMessage.warning("è¯·éæ©è®¾å¤åç§°"); return; } if (!form.acceptanceName) { ElMessage.warning("è¯·éæ©éªæ¶äºº"); return; } if (!form.maintenanceName) { ElMessage.warning("è¯·éæ©ç»´ä¿®äºº"); return; } loading.value = true; try { const { code } = id.value ? await editRepair({ id: unref(id), ...form }) : await addRepair(form); if (code == 200) { let repairId = id.value ? unref(id) : undefined; if (repairId) { const { code } = await editRepair({ id: repairId, ...form }); if (code !== 200) return; await applyPendingProblemDeletes(); } else { const res = await addRepair(form); if (res.code !== 200) return; repairId = res.data; if (repairId == null && attachmentFileList.value.length) { ElMessage.error("ä¿åæå使ªè¿åæ¥ä¿®åç¼å·ï¼æ æ³ä¸ä¼ éä»¶"); return; } } if (attachmentFileList.value.length && repairId != null) { await uploadQueuedRepairImages(repairId); } ElMessage.success(`${id.value ? "ç¼è¾" : "æ°å¢"}æ¥ä¿®æå`); clearAttachmentQueue(); resetProblemAttachmentState(); visible.value = false; emits("ok"); } } catch (e) { ElMessage.error(e?.message || "ä¿åæä¸ä¼ é件失败"); } finally { loading.value = false; } @@ -150,27 +387,38 @@ const handleCancel = () => { resetForm(); syncRepairNameToLoginUser(); clearAttachmentQueue(); resetProblemAttachmentState(); visible.value = false; }; const handleClose = () => { resetForm(); syncRepairNameToLoginUser(); clearAttachmentQueue(); resetProblemAttachmentState(); visible.value = false; }; const openAdd = async () => { id.value = undefined; clearAttachmentQueue(); resetProblemAttachmentState(); visible.value = true; await nextTick(); await loadDeviceName(); await Promise.all([loadDeviceName(), loadUserList()]); syncRepairNameToLoginUser(); }; const openEdit = async (editId) => { clearAttachmentQueue(); resetProblemAttachmentState(); const { data } = await getRepairById(editId); id.value = editId; visible.value = true; await nextTick(); await loadDeviceName(); await Promise.all([loadDeviceName(), loadUserList(), loadExistingProblemImages(editId)]); setForm(data); }; @@ -180,4 +428,54 @@ }); </script> <style lang="scss" scoped></style> <style lang="scss" scoped> .repair-attachment-upload { width: 100%; :deep(.el-upload) { width: 100%; } :deep(.el-upload-dragger) { width: 100%; padding: 24px 16px; } } .repair-upload-icon { font-size: 48px; color: var(--el-color-primary); margin-bottom: 8px; } .problem-existing-wrap { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; } .problem-thumb { position: relative; width: 88px; height: 88px; border-radius: 6px; overflow: hidden; border: 1px solid var(--el-border-color); } .problem-thumb-img { width: 100%; height: 100%; cursor: pointer; } .problem-thumb-remove { position: absolute; top: 2px; right: 2px; font-size: 16px; color: #fff; background: rgba(0, 0, 0, 0.45); border-radius: 50%; padding: 2px; cursor: pointer; } </style> src/views/equipmentManagement/repair/index.vue
@@ -98,74 +98,109 @@ @pagination="changePage" > <template #statusRef="{ row }"> <el-tag v-if="row.status === 2" type="danger">失败</el-tag> <el-tag v-if="row.status === 1" type="success">å®ç»</el-tag> <el-tag v-if="row.status === 0" type="warning">å¾ ç»´ä¿®</el-tag> <el-tag v-else-if="row.status === 3" type="info">å¾ éªæ¶</el-tag> <el-tag v-else-if="row.status === 1" type="success">宿</el-tag> <el-tag v-else-if="row.status === 2" type="danger">维修失败</el-tag> </template> <template #operation="{ row }"> <el-button type="primary" link :disabled="row.status === 1" @click="editRepair(row.id)" > <el-button type="info" link @click="openRepairDetail(row.id)"> 详æ </el-button> <el-button type="primary" link :disabled="!canEdit(row)" @click="editRepair(row.id)"> ç¼è¾ </el-button> <el-button type="info" link :disabled="row.status === 1" @click="openAttachment(row)" > éä»¶ </el-button> <el-button type="success" link :disabled="row.status === 1" @click="addMaintain(row)" > <el-button type="success" link :disabled="!canMaintain(row)" @click="addMaintain(row)"> ç»´ä¿® </el-button> <el-button type="danger" link :disabled="row.status === 1" @click="delRepairByIds(row.id)" > <el-button type="warning" link :disabled="!canAccept(row)" @click="openAcceptance(row)"> éªæ¶ </el-button> <el-button type="danger" link :disabled="!canDelete(row)" @click="delRepairByIds(row.id)"> å é¤ </el-button> <el-button type="primary" link @click="openAttachment(row)"> éä»¶ </el-button> </template> </PIMTable> </div> <RepairModal ref="repairModalRef" @ok="getTableData"/> <MaintainModal ref="maintainModalRef" @ok="getTableData"/> <AcceptanceModal ref="acceptanceModalRef" @ok="getTableData"/> <RepairDetailModal ref="repairDetailModalRef" :java-api="javaApi" /> <el-dialog v-model="attachment.visible" title="éä»¶" width="860px" @closed="onAttachmentClosed"> <el-dialog v-model="attachment.visible" title="éä»¶" width="900px" @closed="onAttachmentClosed"> <div v-loading="attachment.loading" class="attachment-wrap"> <div class="attachment-actions"> <p v-if="attachmentReadOnly" class="attachment-readonly-tip"> å·²å¼å§ç»´ä¿®ï¼æ¤å¤ä» 坿¥çï¼è®¾å¤é®é¢å¾è¯·å¨æ¥ä¿®ãå¾ ç»´ä¿®ãæ¶äºãç¼è¾ãä¸ç»´æ¤ï¼ç»´ä¿®å®æå¾è¯·å¨ãç»´ä¿®ãæ¶ä¸ä¼ ã </p> <div class="attachment-section"> <div class="attachment-section-title">设å¤é®é¢å¾ç</div> <template v-if="attachmentReadOnly"> <div v-if="attachment.problemFileList.length" class="attachment-readonly-grid"> <div v-for="f in attachment.problemFileList" :key="f.id" class="attachment-readonly-item" @click="handleAttachmentPreview(f)" > <el-image :src="f.url" fit="cover" class="attachment-readonly-img" /> </div> </div> <el-empty v-else description="ææ è®¾å¤é®é¢å¾ç" :image-size="60" /> </template> <template v-else> <el-upload v-model:file-list="attachment.fileList" v-model:file-list="attachment.problemFileList" :action="upload.url" multiple ref="attachmentFileUpload" auto-upload :headers="upload.headers" :data="{ deviceRepairId: attachment.deviceRepairId }" :data="uploadDataProblem" :before-upload="handleAttachmentBeforeUpload" :on-error="handleAttachmentUploadError" :on-success="handleAttachmentUploadSuccess" :on-success="() => handleAttachmentUploadSuccess('problem')" :on-preview="handleAttachmentPreview" :on-remove="handleAttachmentRemove" :on-remove="(file) => handleAttachmentRemove(file, 'problem')" list-type="picture-card" :limit="9" accept="image/png,image/jpeg,image/jpg" > + </el-upload> <el-empty v-if="!attachment.loading && attachment.problemFileList.length === 0" description="ææ è®¾å¤é®é¢å¾ç" :image-size="60" /> </template> </div> <el-empty v-if="!attachment.loading && attachment.files.length === 0" description="ææ éä»¶" /> <div class="attachment-section"> <div class="attachment-section-title">ç»´ä¿®å®æå¾ç</div> <p v-if="!attachmentReadOnly" class="attachment-readonly-tip" style="margin-top: 0"> ç»´ä¿®å®æå¾è¯·å¨å表ãç»´ä¿®ãæä½ä¸ä¸ä¼ ï¼æ¤å¤ä» æ¥çã </p> <div v-if="attachment.maintainFileList.length" class="attachment-readonly-grid"> <div v-for="f in attachment.maintainFileList" :key="f.id" class="attachment-readonly-item" @click="handleAttachmentPreview(f)" > <el-image :src="f.url" fit="cover" class="attachment-readonly-img" /> </div> </div> <el-empty v-else description="ææ ç»´ä¿®å®æå¾ç" :image-size="60" /> </div> </div> <template #footer> <el-button @click="attachment.visible = false">å ³é</el-button> @@ -203,7 +238,15 @@ import {ElMessageBox, ElMessage} from "element-plus"; import dayjs from "dayjs"; import MaintainModal from "./Modal/MaintainModal.vue"; import AcceptanceModal from "./Modal/AcceptanceModal.vue"; import RepairDetailModal from "./Modal/RepairDetailModal.vue"; import { getToken } from "@/utils/auth"; import useUserStore from "@/store/modules/user"; import { REPAIR_FILE_TYPE_PROBLEM, isProblemRepairFile, isMaintainRepairFile, } from "@/api/equipmentManagement/repairFileType.js"; defineOptions({ name: "è®¾å¤æ¥ä¿®", @@ -215,6 +258,9 @@ // æ¨¡ææ¡å®ä¾ const repairModalRef = ref(); const maintainModalRef = ref(); const acceptanceModalRef = ref(); const repairDetailModalRef = ref(); const userStore = useUserStore(); // è¡¨æ ¼å¤éæ¡éä¸é¡¹ const multipleList = ref([]); @@ -223,10 +269,30 @@ visible: false, loading: false, deviceRepairId: undefined, /** æ¥ä¿®åç¶æï¼éå¾ ç»´ä¿®(0)æ¶éä»¶å¼¹çªåªè¯» */ repairStatus: undefined, files: [], fileList: [], problemFileList: [], maintainFileList: [], previewVisible: false, previewUrl: "", }); /** å·²å¼å§ç»´ä¿®åï¼å表ãéä»¶ãä» æ¥çï¼ä¸å¯å¢å */ const attachmentReadOnly = computed( () => attachment.repairStatus != null && attachment.repairStatus !== 0 ); const uploadDataProblem = computed(() => ({ deviceRepairId: attachment.deviceRepairId, fileType: REPAIR_FILE_TYPE_PROBLEM, })); const toUploadFileItem = (item) => ({ id: item.id, name: item.name, url: getFileAccessUrl(item), fileType: item.type, }); const getFileAccessUrl = (file = {}) => { @@ -312,6 +378,11 @@ prop: "repairName", }, { label: "éªæ¶äºº", align: "center", prop: "acceptanceName", }, { label: "æ éç°è±¡", align: "center", prop: "remark", @@ -345,10 +416,27 @@ dataType: "slot", slot: "operation", align: "center", width: "300px", width: "420px", }, ] ); const isCurrentUser = (name) => !!name && name === userStore.nickName; const canEdit = (row) => row.status === 0; /** ä» æ¥ä¿®æ¶æå®ç维修人å¯ç»´ä¿® */ const canMaintain = (row) => { if (row.status !== 0) return false; if (!row.maintenanceName) return false; return isCurrentUser(row.maintenanceName); }; const canDelete = (row) => row.status === 0; /** ä» æ¥ä¿®æ¶æå®çéªæ¶äººå¯éªæ¶ */ const canAccept = (row) => { if (row.status !== 3) return false; if (!row.acceptanceName) return false; return isCurrentUser(row.acceptanceName); }; // type === 1 ç»´ä¿® 2æ¥ä¿®é´ const handleDateChange = (value, type) => { @@ -371,10 +459,14 @@ multipleList.value = selectionList; }; // æ£æ¥éä¸çè®°å½ä¸æ¯å¦æå®ç»ç¶æç // æ¹éå é¤ï¼ä» å è®¸å¾ ç»´ä¿® const hasFinishedStatus = computed(() => { return multipleList.value.some(item => item.status === 1) }) return multipleList.value.some((item) => item.status !== 0); }); const openRepairDetail = (id) => { repairDetailModalRef.value?.open(id); }; // æ°å¢æ¥ä¿® const addRepair = () => { @@ -386,9 +478,22 @@ repairModalRef.value.openEdit(id); }; // æ°å¢ç»´ä¿® // ç»´ä¿®ï¼ä» æå®ç»´ä¿®äººï¼ const addMaintain = (row) => { if (!canMaintain(row)) { ElMessage.warning("ä» æå®ç维修人å¯è¿è¡ç»´ä¿®"); return; } maintainModalRef.value.open(row.id, row); }; // éªæ¶ï¼ä» æ¥ä¿®æ¶æå®çéªæ¶äººãä¸å¾ éªæ¶ç¶æå¯ç¹ï¼ const openAcceptance = (row) => { if (!canAccept(row)) { ElMessage.warning("ä» æå®çéªæ¶äººå¯è¿è¡éªæ¶"); return; } acceptanceModalRef.value.open(row.id, row); }; const changePage = ({page, limit}) => { @@ -401,13 +506,13 @@ const delRepairByIds = async (ids) => { // æ£æ¥æ¯å¦æå®ç»ç¶æçè®°å½ const idsArray = Array.isArray(ids) ? ids : [ids]; const hasFinished = idsArray.some(id => { const record = dataList.value.find(item => item.id === id); return record && record.status === 1; const cannotDelete = idsArray.some((id) => { const record = dataList.value.find((item) => item.id === id); return record && record.status !== 0; }); if (hasFinished) { ElMessage.warning('ä¸è½å é¤ç¶æä¸ºå®ç»çè®°å½'); if (cannotDelete) { ElMessage.warning("ä» å¾ ç»´ä¿®ç¶æå¯å é¤"); return; } @@ -440,7 +545,9 @@ }; const openAttachment = async (row) => { attachment.fileList = []; attachment.problemFileList = []; attachment.maintainFileList = []; attachment.repairStatus = row?.status; attachment.visible = true; attachment.deviceRepairId = row?.id; await refreshAttachmentList(); @@ -452,11 +559,12 @@ try { const res = await getRepairFileList(attachment.deviceRepairId); attachment.files = Array.isArray(res?.data) ? res.data : []; attachment.fileList = attachment.files.map((item) => ({ id: item.id, name: item.name, url: getFileAccessUrl(item), })); attachment.problemFileList = attachment.files .filter((item) => isProblemRepairFile(item.type)) .map(toUploadFileItem); attachment.maintainFileList = attachment.files .filter((item) => isMaintainRepairFile(item.type)) .map(toUploadFileItem); } finally { attachment.loading = false; } @@ -465,13 +573,18 @@ const onAttachmentClosed = () => { attachment.loading = false; attachment.deviceRepairId = undefined; attachment.repairStatus = undefined; attachment.files = []; attachment.fileList = []; attachment.problemFileList = []; attachment.maintainFileList = []; attachment.previewVisible = false; attachment.previewUrl = ""; }; const handleAttachmentBeforeUpload = (file) => { if (attachmentReadOnly.value) { return false; } const isImage = ["image/png", "image/jpeg", "image/jpg"].includes(file.type); if (!isImage) { ElMessage.error("åªè½ä¸ä¼ png/jpg/jpeg å¾ç"); @@ -480,11 +593,9 @@ return true; }; const handleAttachmentUploadSuccess = async (res) => { if (res?.code === 200) { const handleAttachmentUploadSuccess = async () => { ElMessage.success("ä¸ä¼ æå"); await refreshAttachmentList(); } }; const handleAttachmentUploadError = () => { @@ -501,15 +612,19 @@ attachment.previewVisible = true; }; const handleAttachmentRemove = async (file) => { // ä» ç§»é¤å端æªå ¥åºæä»¶æ¶ï¼ä¸è°ç¨å 餿¥å£ const handleAttachmentRemove = async (file, category) => { if (attachmentReadOnly.value) { return false; } const matched = attachment.files.find((item) => item.id === file?.id) || attachment.files.find((item) => item.name === file?.name); if (!matched) return; const expectProblem = category === "problem"; if (expectProblem && !isProblemRepairFile(matched.type)) return; if (!expectProblem && !isMaintainRepairFile(matched.type)) return; try { await confirmDeleteAttachment(matched); } finally { // åæ¶å 餿¶ï¼el-upload å·²å ç§»é¤ï¼å·æ°ä¸æ¬¡ä¿æä¸å端ä¸è´ await refreshAttachmentList(); } }; @@ -548,11 +663,43 @@ min-height: 240px; } .attachment-actions { display: flex; align-items: center; justify-content: space-between; .attachment-section { margin-bottom: 20px; } .attachment-section-title { font-size: 14px; font-weight: 600; color: #303133; margin-bottom: 12px; padding-left: 8px; border-left: 3px solid var(--el-color-primary); } .attachment-readonly-tip { font-size: 13px; color: var(--el-text-color-secondary); margin-bottom: 16px; line-height: 1.5; } .attachment-readonly-grid { display: flex; flex-wrap: wrap; gap: 10px; } .attachment-readonly-item { width: 100px; height: 100px; border-radius: 6px; overflow: hidden; cursor: pointer; border: 1px solid var(--el-border-color); } .attachment-readonly-img { width: 100%; height: 100%; } .attachment-preview-wrap {