| | |
| | | <template> |
| | | <el-dialog v-model="visible" :title="modalOptions.title" @close="close"> |
| | | <RepairForm ref="repairFormRef" /> |
| | | <template #footer> |
| | | <el-button @click="closeModal">{{ modalOptions.cancelText }}</el-button> |
| | | <el-button type="primary" @click="sendForm" :loading="loading"> |
| | | {{ modalOptions.confirmText }} |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <FormDialog |
| | | v-model="visible" |
| | | :title="id ? '编辑设备报修' : '新增设备报修'" |
| | | width="880px" |
| | | @confirm="sendForm" |
| | | @cancel="handleCancel" |
| | | @close="handleClose" |
| | | > |
| | | <el-form :model="form" label-width="100px"> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item label="设备名称"> |
| | | <el-select v-model="form.deviceLedgerId" placeholder="请选择" @change="setDeviceModel" filterable style="width: 100%"> |
| | | <el-option |
| | | v-for="(item, index) in deviceOptions" |
| | | :key="index" |
| | | :label="item.deviceName" |
| | | :value="item.id" |
| | | ></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="规格型号"> |
| | | <el-input |
| | | v-model="form.deviceModel" |
| | | placeholder="请输入规格型号" |
| | | disabled |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item label="报修日期"> |
| | | <el-date-picker |
| | | v-model="form.repairTime" |
| | | placeholder="请选择报修日期" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | type="date" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="报修人"> |
| | | <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 |
| | | allow-create |
| | | default-first-option |
| | | 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> |
| | | <el-row v-if="id"> |
| | | <el-col :span="12"> |
| | | <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-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="故障现象"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | :rows="2" |
| | | type="textarea" |
| | | placeholder="请输入故障现象" |
| | | /> |
| | | </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 { useModal } from "@/hooks/useModal"; |
| | | import RepairForm from "../Form/RepairForm.vue"; |
| | | 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: "设备报修弹窗", |
| | |
| | | |
| | | const emits = defineEmits(["ok"]); |
| | | |
| | | const repairFormRef = ref(); |
| | | const { |
| | | id, |
| | | visible, |
| | | loading, |
| | | openModal, |
| | | modalOptions, |
| | | handleConfirm, |
| | | closeModal, |
| | | } = useModal({ title: "设备报修" }); |
| | | const id = ref(); |
| | | const visible = ref(false); |
| | | const loading = ref(false); |
| | | |
| | | 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({ |
| | | deviceLedgerId: undefined, // 设备Id |
| | | deviceName: undefined, // 设备名称 |
| | | 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; |
| | | }; |
| | | |
| | | const setForm = (data) => { |
| | | form.deviceLedgerId = data.deviceLedgerId; |
| | | form.deviceName = data.deviceName; |
| | | form.deviceModel = data.deviceModel; |
| | | form.repairTime = data.repairTime; |
| | | form.acceptanceName = data.acceptanceName; |
| | | form.maintenanceName = data.maintenanceName; |
| | | form.remark = data.remark; |
| | | form.status = data.status; |
| | | syncRepairNameToLoginUser(); |
| | | }; |
| | | |
| | | const sendForm = async () => { |
| | | loading.value = true; |
| | | const form = await repairFormRef.value.getForm(); |
| | | const { code } = id.value |
| | | ? await editRepair({ id: unref(id), ...form }) |
| | | : await addRepair(form); |
| | | if (code == 200) { |
| | | ElMessage.success(`${id ? "编辑" : "新增"}报修成功`); |
| | | closeModal(); |
| | | emits("ok"); |
| | | syncRepairNameToLoginUser(); |
| | | if (!form.deviceLedgerId) { |
| | | ElMessage.warning("请选择设备名称"); |
| | | return; |
| | | } |
| | | loading.value = false; |
| | | if (!form.acceptanceName) { |
| | | ElMessage.warning("请选择验收人"); |
| | | return; |
| | | } |
| | | if (!form.maintenanceName) { |
| | | ElMessage.warning("请选择或输入维修人"); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | 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; |
| | | } |
| | | }; |
| | | |
| | | const openEdit = async (id) => { |
| | | const { data } = await getRepairById(id); |
| | | openModal(id); |
| | | 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 repairFormRef.value.setForm(data); |
| | | await Promise.all([loadDeviceName(), loadUserList()]); |
| | | syncRepairNameToLoginUser(); |
| | | }; |
| | | |
| | | const close = () => { |
| | | repairFormRef.value.resetForm(); |
| | | closeModal(); |
| | | const openEdit = async (editId) => { |
| | | clearAttachmentQueue(); |
| | | resetProblemAttachmentState(); |
| | | const { data } = await getRepairById(editId); |
| | | id.value = editId; |
| | | visible.value = true; |
| | | await nextTick(); |
| | | await Promise.all([loadDeviceName(), loadUserList(), loadExistingProblemImages(editId)]); |
| | | setForm(data); |
| | | }; |
| | | |
| | | defineExpose({ |
| | | openModal, |
| | | openAdd, |
| | | openEdit, |
| | | }); |
| | | </script> |
| | | |
| | | <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> |