gongchunyi
8 天以前 aeabb6a019fbb7e60bd3b6c8cf3e4081abdff80c
src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -1,24 +1,176 @@
<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: "设备报修弹窗",
@@ -26,45 +178,306 @@
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>