gongchunyi
8 天以前 aeabb6a019fbb7e60bd3b6c8cf3e4081abdff80c
src/views/equipmentManagement/repair/index.vue
@@ -1,24 +1,83 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="设备名称">
        <el-input
            v-model="filters.deviceName"
            style="width: 240px"
            placeholder="请输入设备名称"
            clearable
            :prefix-icon="Search"
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="规格型号">
        <el-input
            v-model="filters.deviceModel"
            style="width: 240px"
            placeholder="请选择规格型号"
            clearable
            :prefix-icon="Search"
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="故障现象">
        <el-input
            v-model="filters.remark"
            style="width: 240px"
            placeholder="请输入故障现象"
            clearable
            :prefix-icon="Search"
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="维修人">
        <el-input
            v-model="filters.maintenanceName"
            style="width: 240px"
            placeholder="请输入维修人"
            clearable
            :prefix-icon="Search"
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="报修日期">
        <el-date-picker
            v-model="filters.repairTimeStr"
            type="date"
            placeholder="请选择报修日期"
            size="default"
            @change="(date) => handleDateChange(date,2)"
        />
      </el-form-item>
      <el-form-item label="维修日期">
        <el-date-picker
            v-model="filters.maintenanceTimeStr"
            type="date"
            placeholder="请选择维修日期"
            size="default"
            @change="(date) => handleDateChange(date,1)"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <div class="actions">
        <el-text class="mx-1" size="large">设备报修</el-text>
        <div>
          <el-button
            type="primary"
            icon="Plus"
            :disabled="multipleList.length !== 1"
            @click="addMaintain"
          >
            新增维修
          </el-button>
          <el-button type="success" icon="Van" @click="addRepair">
            新增报修
          </el-button>
          <el-button @click="handleOut">
            导出
          </el-button>
          <el-button
            type="danger"
            icon="Delete"
            :disabled="multipleList.length <= 0"
            :disabled="multipleList.length <= 0 || hasFinishedStatus"
            @click="delRepairByIds(multipleList.map((item) => item.id))"
          >
            批量删除
@@ -26,72 +85,284 @@
        </div>
      </div>
      <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
          rowKey="id"
          isSelection
          :column="columns"
          :tableData="dataList"
          :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
          @selection-change="handleSelectionChange"
          @pagination="changePage"
      >
        <template #statusRef="{ row }">
          <el-tag v-if="row.status === 1" type="success">完结</el-tag>
          <el-tag v-if="row.status === 0" type="error">待维修</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"
            text
            icon="editPen"
            @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="danger"
            text
            icon="delete"
            @click="delRepairByIds(row.id)"
          >
          <el-button type="success" link :disabled="!canMaintain(row)" @click="addMaintain(row)">
            维修
          </el-button>
          <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" />
    <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="900px" @closed="onAttachmentClosed">
      <div v-loading="attachment.loading" class="attachment-wrap">
        <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.problemFileList"
              :action="upload.url"
              multiple
              auto-upload
              :headers="upload.headers"
              :data="uploadDataProblem"
              :before-upload="handleAttachmentBeforeUpload"
              :on-error="handleAttachmentUploadError"
              :on-success="() => handleAttachmentUploadSuccess('problem')"
              :on-preview="handleAttachmentPreview"
              :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>
        <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>
      </template>
    </el-dialog>
    <el-dialog
      v-model="attachment.previewVisible"
      title="图片预览"
      width="70%"
      append-to-body
    >
      <div class="attachment-preview-wrap">
        <img
          v-if="attachment.previewUrl"
          :src="attachment.previewUrl"
          alt="preview"
          class="attachment-preview-img"
        />
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
import { getRepairPage, delRepair } from "@/api/equipmentManagement/repair";
import { onMounted } from "vue";
import { onMounted, getCurrentInstance, computed } from "vue";
import {usePaginationApi} from "@/hooks/usePaginationApi";
import {
  getRepairPage,
  delRepair,
  getRepairFileList,
  deleteRepairFile,
} from "@/api/equipmentManagement/repair";
import RepairModal from "./Modal/RepairModal.vue";
import { ElMessageBox, ElMessage } from "element-plus";
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 { userListNoPage } from "@/api/system/user.js";
import {
  REPAIR_FILE_TYPE_PROBLEM,
  isProblemRepairFile,
  isMaintainRepairFile,
} from "@/api/equipmentManagement/repairFileType.js";
defineOptions({
  name: "设备报修",
});
const {proxy} = getCurrentInstance();
const javaApi = proxy?.javaApi || "";
// 模态框实例
const repairModalRef = ref();
const maintainModalRef = ref();
const acceptanceModalRef = ref();
const repairDetailModalRef = ref();
const userStore = useUserStore();
const systemUserNames = ref([]);
const loadSystemUsers = async () => {
  const res = await userListNoPage();
  systemUserNames.value = (res?.data || []).map((u) => u.nickName);
};
const isSystemUser = (name) => systemUserNames.value.includes(name);
// 表格多选框选中项
const multipleList = ref([]);
const attachment = reactive({
  visible: false,
  loading: false,
  deviceRepairId: undefined,
  /** 报修单状态:非待维修(0)时附件弹窗只读 */
  repairStatus: undefined,
  files: [],
  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 = {}) => {
  if (file?.link) {
    if (String(file.link).startsWith('http')) return file.link;
    return normalizeFileUrl(file.link);
  }
  return normalizeFileUrl(file?.url || '');
};
const normalizeFileUrl = (rawUrl = '') => {
  let fileUrl = rawUrl || '';
  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 = javaApi + fileUrl;
  }
  return fileUrl;
};
const attachmentUploadAction = "/device/repair/uploadFile";
const upload = reactive({
  url: import.meta.env.VITE_APP_BASE_API + attachmentUploadAction,
  headers: {
    Authorization: "Bearer " + getToken(),
  },
});
// 表格钩子
const { filters, columns, dataList, pagination, getTableData, resetFilters } =
  usePaginationApi(
const {
  filters,
  columns,
  dataList,
  pagination,
  getTableData,
  resetFilters,
  onCurrentChange,
} = usePaginationApi(
    getRepairPage,
    {
      searchText: undefined,
      deviceName: undefined,
      deviceModel: undefined,
      remark: undefined,
      maintenanceName: undefined,
      repairTimeStr: undefined,
      maintenanceTimeStr: undefined,
    },
    [
      {
@@ -114,6 +385,11 @@
        label: "报修人",
        align: "center",
        prop: "repairName",
      },
      {
        label: "验收人",
        align: "center",
        prop: "acceptanceName",
      },
      {
        label: "故障现象",
@@ -149,19 +425,64 @@
        dataType: "slot",
        slot: "operation",
        align: "center",
        width: "200px",
        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;
  if (isSystemUser(row.maintenanceName)) {
    return isCurrentUser(row.maintenanceName);
  }
  return true;
};
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) => {
  filters.maintenanceTimeStr = null
  filters.c = null
  if (type === 1) {
    if (value) {
      filters.maintenanceTimeStr = dayjs(value).format("YYYY-MM-DD");
    }
  } else {
    if (value) {
      filters.repairTimeStr = dayjs(value).format("YYYY-MM-DD");
    }
  }
  getTableData();
};
// 多选后做什么
const handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
// 批量删除:仅允许待维修
const hasFinishedStatus = computed(() => {
  return multipleList.value.some((item) => item.status !== 0);
});
const openRepairDetail = (id) => {
  repairDetailModalRef.value?.open(id);
};
// 新增报修
const addRepair = () => {
  repairModalRef.value.openModal();
  repairModalRef.value.openAdd();
};
// 编辑报修
@@ -169,20 +490,50 @@
  repairModalRef.value.openEdit(id);
};
// 新增维修
const addMaintain = () => {
  const row = multipleList.value[0];
// 维修(仅指定维修人)
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}) => {
  pagination.currentPage = page;
  pagination.pageSize = limit;
  onCurrentChange(page);
};
// 单行删除
const delRepairByIds = async (ids) => {
  // 检查是否有完结状态的记录
  const idsArray = Array.isArray(ids) ? ids : [ids];
  const cannotDelete = idsArray.some((id) => {
    const record = dataList.value.find((item) => item.id === id);
    return record && record.status !== 0;
  });
  if (cannotDelete) {
    ElMessage.warning("仅待维修状态可删除");
    return;
  }
  ElMessageBox.confirm("确认删除报修数据, 此操作不可逆?", "警告", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    const { code } = await delRepair(ids);
    const {code} = await delRepair(ids);
    if (code === 200) {
      ElMessage.success("删除成功");
      getTableData();
@@ -190,8 +541,123 @@
  });
};
// 导出
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        proxy.download("/device/repair/export", {}, "设备报修.xlsx");
      })
      .catch(() => {
        ElMessage.info("已取消");
      });
};
const openAttachment = async (row) => {
  attachment.problemFileList = [];
  attachment.maintainFileList = [];
  attachment.repairStatus = row?.status;
  attachment.visible = true;
  attachment.deviceRepairId = row?.id;
  await refreshAttachmentList();
};
const refreshAttachmentList = async () => {
  if (!attachment.deviceRepairId) return;
  attachment.loading = true;
  try {
    const res = await getRepairFileList(attachment.deviceRepairId);
    attachment.files = Array.isArray(res?.data) ? res.data : [];
    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;
  }
};
const onAttachmentClosed = () => {
  attachment.loading = false;
  attachment.deviceRepairId = undefined;
  attachment.repairStatus = undefined;
  attachment.files = [];
  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 图片");
    return false;
  }
  return true;
};
const handleAttachmentUploadSuccess = async () => {
  ElMessage.success("上传成功");
  await refreshAttachmentList();
};
const handleAttachmentUploadError = () => {
  ElMessage.error("上传失败");
};
const handleAttachmentPreview = (file) => {
  const rawUrl = file?.url || file?.response?.data?.link || file?.response?.data?.url || "";
  if (!rawUrl) {
    ElMessage.warning("图片地址无效,无法预览");
    return;
  }
  attachment.previewUrl = normalizeFileUrl(rawUrl);
  attachment.previewVisible = true;
};
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 {
    await refreshAttachmentList();
  }
};
const confirmDeleteAttachment = (fileRow) => {
  return ElMessageBox.confirm("确认删除该附件?", "警告", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    const { code } = await deleteRepairFile(fileRow.id);
    if (code === 200) {
      ElMessage.success("删除成功");
      await refreshAttachmentList();
    }
  });
};
onMounted(() => {
  getTableData();
  loadSystemUsers();
});
</script>
@@ -199,9 +665,67 @@
.table_list {
  margin-top: unset;
}
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
}
.attachment-wrap {
  min-height: 240px;
}
.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 {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 360px;
}
.attachment-preview-img {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
}
</style>