gongchunyi
4 天以前 9fae4a56c15392babde83d68db4073762900082d
feat: 设备保养和巡检优化,项目拆分
已添加4个文件
已修改11个文件
7262 ■■■■■ 文件已修改
src/api/equipmentManagement/upkeep.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inspectionManagement/index.js 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/InspectionItemManager.vue 326 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/InspectionItemSelector.vue 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/formDia.vue 775 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue 1167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 967 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/ItemManager.vue 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/MaintenanceItemSelector.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue 534 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/PlanModal.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/RecordDetailDia.vue 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/detailDia.vue 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/formDia.vue 913 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 1103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/upkeep.js
@@ -73,7 +73,7 @@
// æ·»åŠ è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡
export const deviceMaintenanceTaskAdd = (params) => {
  return request({
    url: '/deviceMaintenanceTask/add',
    url: "/deviceMaintenanceTask/add",
    method: "post",
    data: params,
  });
@@ -81,7 +81,7 @@
// ä¿®æ”¹è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡
export const deviceMaintenanceTaskEdit = (params) => {
  return request({
    url: '/deviceMaintenanceTask/update',
    url: "/deviceMaintenanceTask/update",
    method: "post",
    data: params,
  });
@@ -89,7 +89,7 @@
// è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡åˆ—è¡¨
export const deviceMaintenanceTaskList = (params) => {
  return request({
    url: '/deviceMaintenanceTask/listPage',
    url: "/deviceMaintenanceTask/listPage",
    method: "get",
    params: params,
  });
@@ -97,8 +97,54 @@
// è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡åˆ—è¡¨
export const deviceMaintenanceTaskDel = (params) => {
  return request({
    url: '/deviceMaintenanceTask/delete',
    url: "/deviceMaintenanceTask/delete",
    method: "delete",
    data: params,
  });
};
export const getMaintenanceItemList = (params = {}) => {
  return request({
    url: "/deviceMaintenanceItem/list",
    method: "get",
    params,
  });
};
export const getMaintenanceItemPage = (params = {}) => {
  return request({
    url: "/deviceMaintenanceItem/page",
    method: "get",
    params,
  });
};
export const getActiveMaintenanceItems = () => {
  return request({
    url: "/deviceMaintenanceTask/items",
    method: "get",
  });
};
export const addMaintenanceItem = (data) => {
  return request({
    url: "/deviceMaintenanceItem",
    method: "post",
    data,
  });
};
export const editMaintenanceItem = (data) => {
  return request({
    url: "/deviceMaintenanceItem",
    method: "put",
    data,
  });
};
export const deleteMaintenanceItem = (id) => {
  return request({
    url: `/deviceMaintenanceItem/${id}`,
    method: "delete",
  });
};
src/api/inspectionManagement/index.js
@@ -1,61 +1,72 @@
// å·¡æ£€ç®¡ç†
import request from '@/utils/request'
import request from "@/utils/request";
// å·¡æ£€ä»»åŠ¡è¡¨è¡¨æŸ¥è¯¢
export function inspectionTaskList(query) {
    return request({
        url: '/inspectionTask/list',
        method: 'get',
        params: query
    })
  return request({
    url: "/inspectionTask/list",
    method: "get",
    params: query,
  });
}
// å·¡æ£€ä»»åŠ¡è¡¨æ–°å¢žä¿®æ”¹
export function addOrEditInspectionTask(query) {
    return request({
        url: '/inspectionTask/addOrEditInspectionTask',
        method: 'post',
        data: query
    })
  return request({
    url: "/inspectionTask/addOrEditInspectionTask",
    method: "post",
    data: query,
  });
}
// å·¡æ£€ä»»åŠ¡è¡¨åˆ é™¤
export function delInspectionTask(query) {
    return request({
        url: '/inspectionTask/delInspectionTask',
        method: 'delete',
        data: query
    })
  return request({
    url: "/inspectionTask/delInspectionTask",
    method: "delete",
    data: query,
  });
}
// å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨åˆ é™¤
export function delTimingTask(query) {
    return request({
        url: '/timingTask/delTimingTask',
        method: 'delete',
        data: query
    })
  return request({
    url: "/timingTask/delTimingTask",
    method: "delete",
    data: query,
  });
}
// /inspectionTask/addOrEditInspectionTask
// å·¡æ£€ä¸Šä¼ 
export function uploadInspectionTask(query) {
    return request({
        url: '/inspectionTask/addOrEditInspectionTask',
        method: 'post',
        data: query
    })
  return request({
    url: "/inspectionTask/addOrEditInspectionTask",
    method: "post",
    data: query,
  });
}
// å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨æŸ¥è¯¢
export function timingTaskList(query) {
    return request({
        url: '/timingTask/list',
        method: 'get',
        params: query
    })
  return request({
    url: "/timingTask/list",
    method: "get",
    params: query,
  });
}
// å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨æ–°å¢žä¿®æ”¹
export function addOrEditTimingTask(query) {
    return request({
        url: '/timingTask/addOrEditTimingTask',
        method: 'post',
        data: query
    })
}
  return request({
    url: "/timingTask/addOrEditTimingTask",
    method: "post",
    data: query,
  });
}
export const getInspectionItemList = (params = {}) =>
  request({ url: "/inspectionItem/list", method: "get", params });
export const getInspectionItemPage = (params = {}) =>
  request({ url: "/inspectionItem/page", method: "get", params });
export const addInspectionItem = (data) =>
  request({ url: "/inspectionItem", method: "post", data });
export const editInspectionItem = (data) =>
  request({ url: "/inspectionItem", method: "put", data });
export const deleteInspectionItem = (id) =>
  request({ url: `/inspectionItem/${id}`, method: "delete" });
src/views/equipmentManagement/inspectionManagement/components/InspectionItemManager.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,326 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="所属设备">
        <el-select
          v-model="filters.deviceLedgerId"
          clearable
          filterable
          placeholder="请选择设备"
          style="width: 220px"
        >
          <el-option
            v-for="device in deviceOptions"
            :key="device.id"
            :label="device.deviceName"
            :value="device.id"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="巡检频率">
        <el-select
          v-model="filters.frequencyType"
          clearable
          placeholder="请选择频率"
          style="width: 180px"
        >
          <el-option
            v-for="option in frequencyOptions"
            :key="option.value"
            :label="option.label"
            :value="option.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="巡检项目/内容">
        <el-input
          v-model="keyword"
          clearable
          placeholder="请输入项目或内容"
          style="width: 240px"
          @keyup.enter="search"
          @clear="search"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="search">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <el-card>
      <div class="actions">
        <el-text size="large">巡检项目与内容清单</el-text>
        <el-button type="primary" @click="openAdd">新增巡检内容</el-button>
      </div>
      <el-table v-loading="loading" :data="items" border>
        <el-table-column type="index" label="序号" width="80" align="center" :index="indexMethod" />
        <el-table-column
          prop="itemProject"
          label="巡检项目"
          min-width="180"
          show-overflow-tooltip
        />
        <el-table-column
          prop="itemContent"
          label="巡检内容"
          min-width="300"
          show-overflow-tooltip
        />
        <el-table-column label="所属设备" min-width="180" align="center">
          <template #default="{ row }">{{
            row.deviceLedgerId ? deviceNameMap[row.deviceLedgerId] || "-" : "通用"
          }}</template>
        </el-table-column>
        <el-table-column label="频率" width="120" align="center">
          <template #default="{ row }">{{ frequencyLabel(row.frequencyType) }}</template>
        </el-table-column>
        <el-table-column label="操作" width="150" align="center" fixed="right">
          <template #default="{ row }">
            <el-button link type="primary" @click="openEdit(row)">编辑</el-button>
            <el-button link type="danger" @click="removeItem(row)">删除</el-button>
          </template>
        </el-table-column>
        <template #empty>
          <el-empty description="还没有巡检内容,请逐条添加">
            <el-button type="primary" @click="openAdd">新增巡检内容</el-button>
          </el-empty>
        </template>
      </el-table>
      <div class="pagination-wrap">
        <el-pagination
          v-model:current-page="pageNum"
          v-model:page-size="pageSize"
          background
          :page-sizes="[10, 20, 50]"
          :total="total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="load"
        />
      </div>
    </el-card>
    <el-dialog
      v-model="dialogVisible"
      :title="form.id ? '编辑巡检内容' : '新增巡检内容'"
      width="620px"
      align-center
      destroy-on-close
      :close-on-click-modal="false"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="巡检项目" prop="itemProject">
          <el-input
            v-model.trim="form.itemProject"
            maxlength="200"
            show-word-limit
            placeholder="例如:设备外观检查"
          />
        </el-form-item>
        <el-form-item label="巡检内容" prop="itemContent">
          <el-input
            v-model.trim="form.itemContent"
            type="textarea"
            :rows="4"
            resize="vertical"
            maxlength="2000"
            show-word-limit
            placeholder="填写具体检查内容"
          />
        </el-form-item>
        <el-form-item label="所属设备">
          <el-select
            v-model="form.deviceLedgerId"
            clearable
            filterable
            style="width: 100%"
            placeholder="不选择则为通用巡检内容"
          >
            <el-option
              v-for="device in deviceOptions"
              :key="device.id"
              :label="device.deviceName"
              :value="device.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="巡检频率" prop="frequencyType">
          <el-select v-model="form.frequencyType" style="width: 100%" placeholder="请选择频率">
            <el-option
              v-for="option in frequencyOptions"
              :key="option.value"
              :label="option.label"
              :value="option.value"
            />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="saving" @click="save">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
  addInspectionItem,
  deleteInspectionItem,
  editInspectionItem,
  getInspectionItemPage,
} from "@/api/inspectionManagement";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
const items = ref([]);
const loading = ref(false);
const saving = ref(false);
const dialogVisible = ref(false);
const formRef = ref();
const keyword = ref("");
const pageNum = ref(1);
const pageSize = ref(20);
const total = ref(0);
const deviceOptions = ref([]);
const filters = reactive({ deviceLedgerId: undefined, frequencyType: "" });
const frequencyOptions = [
  { label: "每日", value: "DAILY" },
  { label: "每周", value: "WEEKLY" },
  { label: "每月", value: "MONTHLY" },
  { label: "每季度", value: "QUARTERLY" },
  { label: "通用频率", value: "ALL" },
];
const deviceNameMap = computed(() =>
  Object.fromEntries(deviceOptions.value.map((device) => [String(device.id), device.deviceName]))
);
const frequencyLabel = (value) =>
  frequencyOptions.find((option) => option.value === value)?.label || value || "-";
const indexMethod = (index) => (pageNum.value - 1) * pageSize.value + index + 1;
const form = reactive({
  id: undefined,
  itemProject: "",
  itemContent: "",
  deviceLedgerId: undefined,
  frequencyType: "",
  sortNum: 0,
  isActive: 1,
  remarks: "",
});
const rules = {
  itemProject: [{ required: true, message: "请输入巡检项目", trigger: "blur" }],
  itemContent: [{ required: true, message: "请输入巡检内容", trigger: "blur" }],
  frequencyType: [{ required: true, message: "请选择巡检频率", trigger: "change" }],
};
const load = async () => {
  loading.value = true;
  try {
    const { code, data } = await getInspectionItemPage({
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      itemKeyword: keyword.value.trim(),
      isActive: 1,
      deviceLedgerId: filters.deviceLedgerId,
      frequencyType: filters.frequencyType,
      includeCommon: true,
    });
    if (code === 200) {
      items.value = data?.records || [];
      total.value = Number(data?.total || 0);
    }
  } finally {
    loading.value = false;
  }
};
const search = () => {
  pageNum.value = 1;
  load();
};
const resetFilters = () => {
  filters.deviceLedgerId = undefined;
  filters.frequencyType = "";
  keyword.value = "";
  search();
};
const handleSizeChange = () => {
  pageNum.value = 1;
  load();
};
const reset = () =>
  Object.assign(form, {
    id: undefined,
    itemProject: "",
    itemContent: "",
    deviceLedgerId: undefined,
    frequencyType: "",
    sortNum: 0,
    isActive: 1,
    remarks: "",
  });
const openAdd = () => {
  reset();
  dialogVisible.value = true;
};
const openEdit = (row) => {
  Object.assign(form, { ...row });
  dialogVisible.value = true;
};
const save = async () => {
  if (
    !(await formRef.value
      ?.validate()
      .then(() => true)
      .catch(() => false))
  )
    return;
  saving.value = true;
  try {
    const { code } = form.id
      ? await editInspectionItem({ ...form })
      : await addInspectionItem({ ...form });
    if (code === 200) {
      ElMessage.success("保存成功");
      dialogVisible.value = false;
      await load();
    }
  } finally {
    saving.value = false;
  }
};
const removeItem = async (row) => {
  try {
    await ElMessageBox.confirm(`确定删除巡检内容“${row.itemContent}”吗?`, "提示", {
      type: "warning",
    });
    const { code } = await deleteInspectionItem(row.id);
    if (code === 200) {
      ElMessage.success("删除成功");
      await load();
    }
  } catch (_) {
    /* ç”¨æˆ·å–消 */
  }
};
onMounted(async () => {
  const { data } = await getDeviceLedger();
  deviceOptions.value = data || [];
  await load();
});
</script>
<style scoped>
.actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}
.pagination-wrap {
  display: flex;
  justify-content: flex-end;
  padding-top: 12px;
}
</style>
src/views/equipmentManagement/inspectionManagement/components/InspectionItemSelector.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,172 @@
<template>
  <el-dialog
    v-model="dialogVisible"
    title="选择巡检内容"
    width="820px"
    append-to-body
    destroy-on-close
  >
    <div class="selector-toolbar">
      <el-input
        v-model="keyword"
        clearable
        placeholder="按项目或内容搜索"
        @keyup.enter="search"
        @clear="search"
      />
      <el-button type="primary" @click="search">搜索</el-button>
    </div>
    <el-table v-loading="loading" :data="rows" border height="360">
      <el-table-column label="选择" width="70" align="center">
        <template #default="{ row }"
          ><el-checkbox
            :model-value="isSelected(row.id)"
            @change="(checked) => toggleItem(row, checked)"
        /></template>
      </el-table-column>
      <el-table-column type="index" label="序号" width="70" />
      <el-table-column label="类型" width="90" align="center">
        <template #default="{ row }"
          ><el-tag :type="row.deviceLedgerId ? 'primary' : 'info'" size="small">{{
            row.deviceLedgerId ? "本设备" : "通用"
          }}</el-tag></template
        >
      </el-table-column>
      <el-table-column prop="itemProject" label="巡检项目" min-width="170" show-overflow-tooltip />
      <el-table-column prop="itemContent" label="巡检内容" min-width="270" show-overflow-tooltip />
      <el-table-column label="频率" width="110" align="center">
        <template #default="{ row }"
          ><el-tag :type="row.frequencyType === 'ALL' ? 'info' : 'success'" size="small">{{
            frequencyLabel(row.frequencyType)
          }}</el-tag></template
        >
      </el-table-column>
    </el-table>
    <div class="selector-pagination">
      <el-pagination
        v-model:current-page="pageNum"
        v-model:page-size="pageSize"
        :page-sizes="[10, 20, 50]"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="load"
      />
    </div>
    <template #footer>
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="confirm">确定选择({{ selected.size }})</el-button>
    </template>
  </el-dialog>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { getInspectionItemPage } from "@/api/inspectionManagement";
const props = defineProps({
  visible: { type: Boolean, default: false },
  selectedItems: { type: Array, default: () => [] },
  deviceLedgerId: { type: [Number, String], default: undefined },
  frequencyType: { type: String, default: "" },
});
const emit = defineEmits(["update:visible", "confirm"]);
const dialogVisible = computed({
  get: () => props.visible,
  set: (value) => emit("update:visible", value),
});
const rows = ref([]);
const loading = ref(false);
const keyword = ref("");
const pageNum = ref(1);
const pageSize = ref(10);
const total = ref(0);
const selected = ref(new Map());
const frequencyLabel = (value) =>
  ({ DAILY: "每日", WEEKLY: "每周", MONTHLY: "每月", QUARTERLY: "每季度", ALL: "通用频率" }[
    value
  ] ||
  value ||
  "-");
watch(
  () => props.visible,
  (visible) => {
    if (!visible) return;
    keyword.value = "";
    pageNum.value = 1;
    selected.value = new Map(
      (props.selectedItems || []).map((item) => [String(item.inspectionItemId || item.id), item])
    );
    load();
  }
);
watch(
  () => [props.deviceLedgerId, props.frequencyType],
  () => {
    if (!props.visible) return;
    pageNum.value = 1;
    load();
  }
);
const load = async () => {
  if (!props.deviceLedgerId || !props.frequencyType) {
    rows.value = [];
    total.value = 0;
    return;
  }
  loading.value = true;
  try {
    const { code, data } = await getInspectionItemPage({
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      itemKeyword: keyword.value.trim(),
      isActive: 1,
      deviceLedgerId: props.deviceLedgerId,
      frequencyType: props.frequencyType,
      includeCommon: true,
    });
    if (code === 200) {
      rows.value = data?.records || [];
      total.value = Number(data?.total || 0);
    }
  } finally {
    loading.value = false;
  }
};
const search = () => {
  pageNum.value = 1;
  load();
};
const handleSizeChange = () => {
  pageNum.value = 1;
  load();
};
const isSelected = (id) => selected.value.has(String(id));
const toggleItem = (item, checked) => {
  const next = new Map(selected.value);
  if (checked) next.set(String(item.id), item);
  else next.delete(String(item.id));
  selected.value = next;
};
const confirm = () => {
  emit("confirm", Array.from(selected.value.values()));
  dialogVisible.value = false;
};
</script>
<style scoped>
.selector-toolbar {
  display: flex;
  gap: 10px;
  margin-bottom: 14px;
}
.selector-toolbar .el-input {
  max-width: 440px;
}
.selector-pagination {
  display: flex;
  justify-content: flex-end;
  margin-top: 14px;
}
</style>
src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -1,314 +1,503 @@
<template>
  <div>
    <el-dialog :title="operationType === 'add' ? '新增巡检任务' : '编辑巡检任务'"
               v-model="dialogVisitable" width="800px" @close="cancel">
      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
        <el-row>
          <el-col :span="12">
            <el-form-item label="设备名称" prop="taskId">
              <el-select v-model="form.taskId" @change="setDeviceModel">
  <el-dialog
    v-model="visible"
    :title="editing ? '编辑巡检任务' : '新增巡检任务'"
    width="800px"
    align-center
    destroy-on-close
    :close-on-click-modal="false"
    @closed="reset"
  >
    <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
      <el-row :gutter="24">
        <el-col :span="12">
          <el-form-item label="设备名称" prop="taskId">
            <el-select
              v-model="form.taskId"
              filterable
              placeholder="请选择设备"
              style="width: 100%"
              @change="changeDevice"
            >
              <el-option
                v-for="device in deviceOptions"
                :key="device.id"
                :label="device.deviceName"
                :value="device.id"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="巡检人" prop="inspector">
            <el-select
              v-model="form.inspector"
              placeholder="请选择巡检人"
              multiple
              clearable
              filterable
              style="width: 100%"
            >
              <el-option
                v-for="user in userList"
                :key="user.userId"
                :label="user.nickName"
                :value="user.userId"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="任务频率" prop="frequencyType">
            <el-select
              v-model="form.frequencyType"
              placeholder="请选择频率"
              style="width: 100%"
              @change="handleFrequencyChange"
            >
              <el-option label="每日" value="DAILY" />
              <el-option label="每周" value="WEEKLY" />
              <el-option label="每月" value="MONTHLY" />
              <el-option label="每季度" value="QUARTERLY" />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col v-if="form.frequencyType === 'DAILY'" :span="12">
          <el-form-item label="执行时间" prop="frequencyDetail">
            <el-time-picker
              v-model="form.frequencyDetail"
              placeholder="选择时间"
              format="HH:mm"
              value-format="HH:mm"
              style="width: 100%"
            />
          </el-form-item>
        </el-col>
        <el-col v-if="form.frequencyType === 'WEEKLY'" :span="12">
          <el-form-item label="执行日期" prop="frequencyDetail">
            <div class="frequency-fields">
              <el-select v-model="form.week" placeholder="星期" style="width: 50%">
                <el-option
                  v-for="(item, index) in deviceOptions"
                  :key="index"
                  :label="item.deviceName"
                  :value="item.id"
                ></el-option>
                  v-for="day in weekdays"
                  :key="day.value"
                  :label="day.label"
                  :value="day.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="巡检人" prop="inspector">
              <el-select v-model="form.inspector" placeholder="请选择" multiple clearable>
                <el-option v-for="item in userList" :label="item.nickName" :value="item.userId" :key="item.userId"/>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="巡检项目" prop="inspectionProject">
              <el-input v-model="form.inspectionProject" placeholder="请输入巡检项目" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="备注" prop="remarks">
              <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="是否启用" prop="isEnabled">
              <el-radio-group v-model="form.isEnabled">
                <el-radio :value="1">是</el-radio>
                <el-radio :value="0">否</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="任务频率" prop="frequencyType">
              <el-select v-model="form.frequencyType" placeholder="请选择" clearable>
                <el-option label="每日" value="DAILY"/>
                <el-option label="每周" value="WEEKLY"/>
                <el-option label="每月" value="MONTHLY"/>
                <!-- <el-option label="季度" value="QUARTERLY"/> -->
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'DAILY' && form.frequencyType">
            <el-form-item label="日期" prop="frequencyDetail">
              <el-time-picker v-model="form.frequencyDetail" placeholder="选择时间" format="HH:mm"
                              value-format="HH:mm" />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'WEEKLY' && form.frequencyType">
            <el-form-item label="日期" prop="frequencyDetail">
              <el-select v-model="form.week" placeholder="请选择" clearable style="width: 50%">
                <el-option label="周一" value="MON"/>
                <el-option label="周二" value="TUE"/>
                <el-option label="周三" value="WED"/>
                <el-option label="周四" value="THU"/>
                <el-option label="周五" value="FRI"/>
                <el-option label="周六" value="SAT"/>
                <el-option label="周日" value="SUN"/>
              </el-select>
              <el-time-picker v-model="form.time" placeholder="选择时间" format="HH:mm"
                              value-format="HH:mm"  style="width: 50%"/>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'MONTHLY' && form.frequencyType">
            <el-form-item label="日期" prop="frequencyDetail">
              <el-date-picker
                  v-model="form.frequencyDetail"
                  type="datetime"
                  clearable
                  placeholder="选择开始日期"
                  format="DD,HH:mm"
                  value-format="DD,HH:mm"
              <el-time-picker
                v-model="form.time"
                placeholder="时间"
                format="HH:mm"
                value-format="HH:mm"
                style="width: 50%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'QUARTERLY' && form.frequencyType">
            <el-form-item label="日期" prop="frequencyDetail">
              <el-date-picker
                  v-model="form.frequencyDetail"
                  type="datetime"
            </div>
          </el-form-item>
        </el-col>
        <el-col v-if="form.frequencyType === 'MONTHLY'" :span="12">
          <el-form-item label="执行日期" prop="frequencyDetail">
            <el-date-picker
              v-model="form.frequencyDetail"
              type="datetime"
              clearable
              placeholder="选择开始日期"
              format="DD,HH:mm"
              value-format="DD,HH:mm"
              style="width: 100%"
            />
          </el-form-item>
        </el-col>
        <el-col v-if="form.frequencyType === 'QUARTERLY'" :span="12">
          <el-form-item label="执行日期" prop="frequencyDetail">
            <el-date-picker
              v-model="form.frequencyDetail"
              type="datetime"
              clearable
              placeholder="选择开始日期"
              format="MM,DD,HH:mm"
              value-format="MM,DD,HH:mm"
              style="width: 100%"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="是否启用" prop="isEnabled">
            <el-radio-group v-model="form.isEnabled"
              ><el-radio :value="1">启用</el-radio
              ><el-radio :value="0">停用</el-radio></el-radio-group
            >
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="巡检内容" prop="inspectionItemIds">
            <div class="items-field">
              <div class="items-toolbar">
                <el-button
                  type="primary"
                  plain
                  :disabled="!form.taskId || !form.frequencyType"
                  @click="selectorVisible = true"
                  >选择巡检内容</el-button
                >
                <span class="muted">{{ selectionHint }}</span>
              </div>
              <div class="custom-input">
                <el-input
                  v-model="customItemProject"
                  maxlength="200"
                  clearable
                  placeholder="选择开始日期"
                  format="MM,DD,HH:mm"
                  value-format="MM,DD,HH:mm"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">保存</el-button>
          <el-button @click="cancel">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
                  placeholder="输入本任务专用的巡检项目"
                />
                <el-input
                  v-model="customItemContent"
                  maxlength="2000"
                  clearable
                  placeholder="输入项目对应的巡检内容"
                  @keyup.enter="addCustomItem"
                />
                <el-button
                  :disabled="!customItemProject.trim() || !customItemContent.trim()"
                  @click="addCustomItem"
                  >添加</el-button
                >
              </div>
              <div v-if="displayItems.length" class="selected-items">
                <div v-for="(item, index) in displayItems" :key="item.key" class="selected-item">
                  <div class="selected-item-title">
                    <b>{{ index + 1 }}. {{ item.itemProject }}</b>
                    <el-button link type="danger" @click="removeTaskItem(item)">移除</el-button>
                  </div>
                  <div class="selected-item-content">{{ item.itemContent }}</div>
                </div>
              </div>
              <el-empty v-else description="暂未选择巡检内容" :image-size="50" />
            </div>
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="备注"
            ><el-input
              v-model="form.remarks"
              type="textarea"
              :rows="2"
              maxlength="500"
              show-word-limit
          /></el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <template #footer>
      <el-button @click="visible = false">取消</el-button>
      <el-button type="primary" :loading="saving" @click="submit">保存</el-button>
    </template>
  </el-dialog>
  <InspectionItemSelector
    v-model:visible="selectorVisible"
    :selected-items="form.inspectionItems"
    :device-ledger-id="form.taskId"
    :frequency-type="form.frequencyType"
    @confirm="setSelectedItems"
  />
</template>
<script setup>
import {reactive, ref, getCurrentInstance, toRefs} from "vue";
import useUserStore from '@/store/modules/user'
import {addOrEditTimingTask} from "@/api/inspectionManagement/index.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { computed, getCurrentInstance, reactive, ref } from "vue";
import useUserStore from "@/store/modules/user";
import { addOrEditTimingTask, getInspectionItemList } from "@/api/inspectionManagement/index.js";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import InspectionItemSelector from "./InspectionItemSelector.vue";
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const userStore = useUserStore()
const dialogVisitable = ref(false);
const operationType = ref('add');
const { proxy } = getCurrentInstance();
const emit = defineEmits(["closeDia"]);
const userStore = useUserStore();
const visible = ref(false);
const editing = ref(false);
const saving = ref(false);
const selectorVisible = ref(false);
const formRef = ref();
const deviceOptions = ref([]);
const data = reactive({
  form: {
    taskId: undefined,
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    inspectionProject: '',
    isEnabled: 1,
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
    week: '',
    time: ''
  },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请输入巡检人", trigger: "blur" },],
        dateStr: [{ required: true, message: "请选择登记时间", trigger: "change" }],
        frequencyType: [{ required: true, message: "请选择任务频率", trigger: "change" }],
        frequencyDetail: [
            {
                required: true,
                message: "请选择日期",
                trigger: "change",
                validator: (rule, value, callback) => {
                    if (!form.value.frequencyType) {
                        callback()
                        return
                    }
                    if (form.value.frequencyType === 'WEEKLY') {
                        if (!form.value.week || !form.value.time) {
                            callback(new Error("请选择日期和时间"))
                        } else {
                            callback()
                        }
                    } else {
                        if (!value) {
                            callback(new Error("请选择日期"))
                        } else {
                            callback()
                        }
                    }
                }
            }
        ],
        week: [
            {
                required: true,
                message: "请选择星期",
                trigger: "change",
                validator: (rule, value, callback) => {
                    if (form.value.frequencyType === 'WEEKLY' && !value) {
                        callback(new Error("请选择星期"))
                    } else {
                        callback()
                    }
                }
            }
        ],
        time: [
            {
                required: true,
                message: "请选择时间",
                trigger: "change",
                validator: (rule, value, callback) => {
                    if (form.value.frequencyType === 'WEEKLY' && !value) {
                        callback(new Error("请选择时间"))
                    } else {
                        callback()
                    }
                }
            }
        ]
    }
})
const { form, rules } = toRefs(data)
const userList = ref([])
const loadDeviceName = async () => {
  const { data } = await getDeviceLedger();
  deviceOptions.value = data;
const userList = ref([]);
const customItems = ref([]);
const customItemProject = ref("");
const customItemContent = ref("");
let defaultItemsRequestId = 0;
const weekdays = [
  { label: "周一", value: "MON" },
  { label: "周二", value: "TUE" },
  { label: "周三", value: "WED" },
  { label: "周四", value: "THU" },
  { label: "周五", value: "FRI" },
  { label: "周六", value: "SAT" },
  { label: "周日", value: "SUN" },
];
const blankForm = () => ({
  id: undefined,
  taskId: undefined,
  taskName: "",
  inspector: [],
  inspectorIds: "",
  inspectionItemIds: [],
  inspectionItems: [],
  isEnabled: 1,
  remarks: "",
  frequencyType: "",
  frequencyDetail: "",
  week: "",
  time: "",
});
const form = reactive(blankForm());
const rules = {
  taskId: [{ required: true, message: "请选择设备", trigger: "change" }],
  inspector: [{ required: true, message: "请选择巡检人", trigger: "change" }],
  frequencyType: [{ required: true, message: "请选择任务频率", trigger: "change" }],
  frequencyDetail: [
    {
      validator: (_, value, callback) => {
        const valid =
          form.frequencyType === "DAILY"
            ? !!value
            : form.frequencyType === "WEEKLY"
            ? !!form.week && !!form.time
            : form.frequencyType === "MONTHLY"
            ? !!form.frequencyDetail
            : form.frequencyType === "QUARTERLY"
            ? !!form.frequencyDetail
            : false;
        valid ? callback() : callback(new Error("请选择巡检执行日期和时间"));
      },
      trigger: "change",
    },
  ],
  inspectionItemIds: [
    {
      validator: (_, value, callback) =>
        form.inspectionItems.length || customItems.value.length
          ? callback()
          : callback(new Error("请选择或添加至少一个巡检内容")),
      trigger: "change",
    },
  ],
};
const displayItems = computed(() => [
  ...form.inspectionItems.map((item) => ({ ...item, key: `item-${item.id}` })),
  ...customItems.value.map((item) => ({ ...item, key: item.key, isCustom: true })),
]);
const frequencyDetail = computed(() => {
  if (form.frequencyType === "WEEKLY") return `${form.week},${form.time}`;
  if (form.frequencyType === "MONTHLY") return form.frequencyDetail;
  return form.frequencyDetail;
});
const selectionHint = computed(() => {
  if (!form.taskId || !form.frequencyType)
    return "请先选择设备和频率,匹配的设备巡检内容将自动带出";
  return displayItems.value.length
    ? `已选 ${displayItems.value.length} é¡¹ï¼Œå¯ç»§ç»­æ·»åŠ æœ¬ä»»åŠ¡ä¸“ç”¨å†…å®¹æˆ–é€‰æ‹©é€šç”¨å†…å®¹`
    : "该设备和频率暂无默认巡检内容,可手动添加或选择通用内容";
});
const setDeviceModel = (id) => {
  const option = deviceOptions.value.find((item) => item.id === id);
  if (option) {
    form.value.taskName = option.deviceName;
  }
}
// æ‰“开弹框
const openDialog = async (type, row) => {
  dialogVisitable.value = true
  operationType.value = type
  // é‡ç½®è¡¨å•
  resetForm();
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  userListNoPageByTenantId().then((res) => {
    userList.value = res.data;
  });
  // åŠ è½½è®¾å¤‡åˆ—è¡¨
  await loadDeviceName();
  if (type === 'edit' && row) {
    form.value = {...row}
    form.value.inspector = form.value.inspectorIds.split(',').map(Number)
    // å¤„理WEEKLY频率的回显:将frequencyDetail拆分为week和time
    if (form.value.frequencyType === 'WEEKLY' && form.value.frequencyDetail) {
      const parts = form.value.frequencyDetail.split(',')
      form.value.week = parts[0] || ''
      form.value.time = parts[1] || ''
      form.value.frequencyDetail = ''
    }
    // å¦‚果有设备ID,自动设置设备信息
    if (form.value.taskId) {
      setDeviceModel(form.value.taskId);
  editing.value = type === "edit";
  Object.assign(form, blankForm());
  customItems.value = [];
  customItemProject.value = "";
  customItemContent.value = "";
  await Promise.all([
    getDeviceLedger().then(({ data }) => {
      deviceOptions.value = data || [];
    }),
    userListNoPageByTenantId().then(({ data }) => {
      userList.value = data || [];
    }),
  ]);
  if (editing.value && row) {
    Object.assign(form, blankForm(), row);
    form.inspector = String(row.inspectorIds || "")
      .split(",")
      .filter(Boolean)
      .map(Number);
    const taskItems = row.inspectionItems || [];
    form.inspectionItems = taskItems
      .filter((item) => item.inspectionItemId != null)
      .map((item) => ({ ...item, id: item.inspectionItemId }));
    customItems.value = taskItems
      .filter((item) => item.inspectionItemId == null)
      .map((item, index) => ({ ...item, key: `custom-saved-${index}` }));
    if (form.frequencyType === "WEEKLY" && form.frequencyDetail) {
      [form.week, form.time] = form.frequencyDetail.split(",");
    }
  }
}
  visible.value = true;
};
const setDeviceModel = (id) => {
  const device = deviceOptions.value.find((item) => item.id === id);
  if (device) form.taskName = device.deviceName;
};
const syncInspectionItems = (items) => {
  form.inspectionItems = items.map((item) => ({ ...item, id: item.inspectionItemId || item.id }));
  form.inspectionItemIds = form.inspectionItems.map((item) => item.id);
};
const clearInspectionItems = () => syncInspectionItems([]);
const loadDefaultInspectionItems = async () => {
  const requestId = ++defaultItemsRequestId;
  const deviceLedgerId = form.taskId;
  const frequencyType = form.frequencyType;
  clearInspectionItems();
  if (!deviceLedgerId || !frequencyType) return;
// å…³é—­å¯¹è¯æ¡†
const cancel = () => {
  resetForm()
  dialogVisitable.value = false
  emit('closeDia')
}
// é‡ç½®è¡¨å•函数
const resetForm = () => {
  if (proxy.$refs.formRef) {
    proxy.$refs.formRef.resetFields()
  }
  // é‡ç½®è¡¨å•数据确保设备信息正确重置
  form.value = {
    taskId: undefined,
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    inspectionProject: '',
    isEnabled: 1,
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
    week: '',
    time: ''
  }
}
// æäº¤è¡¨å•
const submitForm = () => {
  proxy.$refs["formRef"].validate(async valid => {
    if (valid) {
      try {
        form.value.inspectorIds = form.value.inspector.join(',')
        delete form.value.inspector
        if (form.value.frequencyType === 'WEEKLY') {
          let frequencyDetail = ''
          frequencyDetail = form.value.week + ',' + form.value.time
          form.value.frequencyDetail = frequencyDetail
        }
        let res = await userStore.getInfo()
        form.value.registrantId = res.user.userId
        await addOrEditTimingTask(form.value)
        cancel()
        proxy.$modal.msgSuccess('提交成功')
      } catch (error) {
        proxy.$modal.msgError('提交失败,请重试')
      }
  try {
    const { code, data } = await getInspectionItemList({
      deviceLedgerId,
      frequencyType,
      isActive: 1,
    });
    if (
      requestId !== defaultItemsRequestId ||
      deviceLedgerId !== form.taskId ||
      frequencyType !== form.frequencyType
    )
      return;
    if (code === 200) syncInspectionItems(data || []);
  } catch (error) {
    if (requestId === defaultItemsRequestId) {
      proxy.$modal.msgError(error?.message || "加载默认巡检内容失败");
    }
  })
}
defineExpose({ openDialog })
  }
};
const changeDevice = (id) => {
  setDeviceModel(id);
  loadDefaultInspectionItems();
};
const handleFrequencyChange = () => loadDefaultInspectionItems();
const setSelectedItems = (items) => {
  syncInspectionItems(items);
  formRef.value?.validateField("inspectionItemIds");
};
const addCustomItem = () => {
  const itemProject = customItemProject.value.trim();
  const itemContent = customItemContent.value.trim();
  if (!itemProject || !itemContent) return;
  if (displayItems.value.some((item) => item.itemProject === itemProject)) {
    proxy.$modal.msgWarning("已存在相同的巡检项目");
    return;
  }
  customItems.value = [
    ...customItems.value,
    { key: `custom-${Date.now()}`, itemProject, itemContent },
  ];
  customItemProject.value = "";
  customItemContent.value = "";
  formRef.value?.validateField("inspectionItemIds");
};
const removeTaskItem = (item) => {
  if (item.isCustom)
    customItems.value = customItems.value.filter((custom) => custom.key !== item.key);
  else
    form.inspectionItems = form.inspectionItems.filter(
      (selected) => String(selected.id) !== String(item.id)
    );
  form.inspectionItemIds = form.inspectionItems.map((item) => item.id);
  formRef.value?.validateField("inspectionItemIds");
};
const reset = () => {
  defaultItemsRequestId += 1;
  Object.assign(form, blankForm());
  customItems.value = [];
  customItemProject.value = "";
  customItemContent.value = "";
  formRef.value?.clearValidate();
};
const submit = async () => {
  const valid = await formRef.value
    ?.validate()
    .then(() => true)
    .catch(() => false);
  if (!valid) return;
  saving.value = true;
  try {
    const userInfo = await userStore.getInfo();
    const payload = {
      ...form,
      inspectorIds: form.inspector.join(","),
      frequencyDetail: frequencyDetail.value,
      registrantId: userInfo.user.userId,
      inspectionItemIds: form.inspectionItems.map((item) => item.id),
      customItems: customItems.value.map((item) => ({
        itemProject: item.itemProject,
        itemContent: item.itemContent,
      })),
    };
    delete payload.inspector;
    delete payload.inspectionItems;
    delete payload.week;
    delete payload.time;
    await addOrEditTimingTask(payload);
    proxy.$modal.msgSuccess("提交成功");
    visible.value = false;
    emit("closeDia");
  } catch (error) {
    proxy.$modal.msgError(error?.message || "提交失败,请重试");
  } finally {
    saving.value = false;
  }
};
defineExpose({ openDialog });
</script>
<style scoped>
</style>
.frequency-fields {
  display: flex;
  width: 100%;
  gap: 8px;
}
.items-field {
  width: 100%;
}
.items-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.custom-input {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}
.custom-input .el-input:first-child {
  flex: 0 0 30%;
}
.custom-input .el-input:nth-child(2) {
  flex: 1;
  min-width: 0;
}
.muted {
  color: #909399;
  font-size: 13px;
}
.selected-items {
  max-height: 250px;
  overflow-y: auto;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
}
.selected-item {
  padding: 10px 12px;
  border-bottom: 1px solid #ebeef5;
}
.selected-item:last-child {
  border-bottom: 0;
}
.selected-item-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: #303133;
}
.selected-item-content {
  margin-top: 6px;
  color: #606266;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
</style>
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue
@@ -1,27 +1,29 @@
<template>
  <FormDialog v-model="dialogVisible"
              :title="operationType === 'view' ? '巡检记录详情' : '上传巡检记录'"
              width="980px"
              :operation-type="operationType"
              @close="handleClose"
              @cancel="handleClose">
  <FormDialog
    v-model="dialogVisible"
    :title="operationType === 'view' ? '巡检记录详情' : '上传巡检记录'"
    width="980px"
    :operation-type="operationType"
    @close="handleClose"
    @cancel="handleClose"
  >
    <main class="upload-content">
      <el-card v-if="taskInfo"
               class="section-card">
        <el-descriptions :column="2"
                         border>
      <el-card v-if="taskInfo" class="section-card">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="巡检任务名称">
            {{ taskInfo.taskName || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="巡检项目">
            {{ taskInfo.inspectionProject || "-" }}
            {{ inspectionProjectSummary || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="执行巡检人">
            <template v-if="formattedInspector && formattedInspector.length">
              <el-tag v-for="tag in formattedInspector"
                      :key="tag"
                      size="small"
                      style="margin-right: 4px">
              <el-tag
                v-for="tag in formattedInspector"
                :key="tag"
                size="small"
                style="margin-right: 4px"
              >
                {{ tag }}
              </el-tag>
            </template>
@@ -44,47 +46,66 @@
          </el-descriptions-item>
        </el-descriptions>
      </el-card>
      <el-card v-if="inspectionItems.length" class="section-card inspection-items-card">
        <div class="section-heading">
          <h3>巡检内容</h3>
          <span>已完成 {{ completedItemIds.length }} / {{ inspectionItems.length }} é¡¹</span>
        </div>
        <div v-for="(item, index) in inspectionItems" :key="item.id" class="inspection-item-row">
          <div class="inspection-item-copy">
            <div class="inspection-item-title">{{ index + 1 }}. {{ item.itemProject }}</div>
            <div class="inspection-item-content">{{ item.itemContent }}</div>
          </div>
          <el-checkbox
            :model-value="isItemCompleted(item)"
            :disabled="operationType === 'view'"
            @change="(checked) => toggleItem(item, checked)"
          >
            {{ isItemCompleted(item) ? "已完成" : "未完成" }}
          </el-checkbox>
        </div>
      </el-card>
      <el-card class="section-card">
        <h3>巡检状态</h3>
        <el-radio-group v-model="hasException"
                        :disabled="operationType === 'view'">
        <el-radio-group v-model="hasException" :disabled="operationType === 'view'">
          <el-radio-button :value="false">正常</el-radio-button>
          <el-radio-button :value="true">存在异常</el-radio-button>
        </el-radio-group>
      </el-card>
      <el-card v-if="hasException === true"
               class="section-card">
      <el-card v-if="hasException === true" class="section-card">
        <h3>异常描述</h3>
        <el-input v-model="abnormalDescription"
                  type="textarea"
                  maxlength="500"
                  show-word-limit
                  :rows="4"
                  :disabled="operationType === 'view'"
                  placeholder="请描述异常情况..." />
        <el-input
          v-model="abnormalDescription"
          type="textarea"
          maxlength="500"
          show-word-limit
          :rows="4"
          :disabled="operationType === 'view'"
          placeholder="请描述异常情况..."
        />
      </el-card>
      <el-card v-if="hasException === true"
               class="section-card">
        <div class="upload-buttons"
             v-if="operationType !== 'view'">
          <el-upload :show-file-list="false"
                     :http-request="uploadFile"
                     :disabled="beforeModelValue.length >= uploadConfig.limit || uploading"
                     accept="image/*">
            <el-button type="primary"
                       :loading="uploading">
      <el-card v-if="hasException === true" class="section-card">
        <div class="upload-buttons" v-if="operationType !== 'view'">
          <el-upload
            :show-file-list="false"
            :http-request="uploadFile"
            :disabled="beforeModelValue.length >= uploadConfig.limit || uploading"
            accept="image/*"
          >
            <el-button type="primary" :loading="uploading">
              <el-icon>
                <Camera />
              </el-icon>
              é€‰æ‹©å›¾ç‰‡
            </el-button>
          </el-upload>
          <el-upload :show-file-list="false"
                     :http-request="uploadFile"
                     :disabled="beforeModelValue.length >= uploadConfig.limit || uploading"
                     accept="video/*">
            <el-button type="success"
                       :loading="uploading">
          <el-upload
            :show-file-list="false"
            :http-request="uploadFile"
            :disabled="beforeModelValue.length >= uploadConfig.limit || uploading"
            accept="video/*"
          >
            <el-button type="success" :loading="uploading">
              <el-icon>
                <VideoCamera />
              </el-icon>
@@ -92,35 +113,36 @@
            </el-button>
          </el-upload>
        </div>
        <el-progress v-if="uploading"
                     :percentage="uploadProgress"
                     class="upload-progress" />
        <div v-if="beforeModelValue.length"
             class="file-list">
          <div v-for="(file, index) in beforeModelValue"
               :key="file.uid || file.id || index"
               class="file-item">
        <el-progress v-if="uploading" :percentage="uploadProgress" class="upload-progress" />
        <div v-if="beforeModelValue.length" class="file-list">
          <div
            v-for="(file, index) in beforeModelValue"
            :key="file.uid || file.id || index"
            class="file-item"
          >
            <div class="file-preview-container">
              <el-image v-if="file.type === 'image' || !file.type"
                        :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                        fit="cover"
                        class="file-preview"
                        :preview-src-list="[file.url || file.tempFilePath || file.path || file.downloadUrl]"
                        preview-teleported />
              <div v-else
                   class="video-preview"
                   @click="previewVideo(file)">
              <el-image
                v-if="file.type === 'image' || !file.type"
                :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                fit="cover"
                class="file-preview"
                :preview-src-list="[file.url || file.tempFilePath || file.path || file.downloadUrl]"
                preview-teleported
              />
              <div v-else class="video-preview" @click="previewVideo(file)">
                <el-icon>
                  <VideoCamera />
                </el-icon>
                <span>视频</span>
              </div>
              <el-button class="delete-btn"
                         type="danger"
                         circle
                         size="small"
                         v-if="operationType !== 'view'"
                         @click="removeFile(index)">
              <el-button
                class="delete-btn"
                type="danger"
                circle
                size="small"
                v-if="operationType !== 'view'"
                @click="removeFile(index)"
              >
                <el-icon>
                  <Close />
                </el-icon>
@@ -134,559 +156,614 @@
            </div>
          </div>
        </div>
        <el-empty v-else
                  :description="operationType === 'view' ? '暂无异常照片或视频' : '请选择要上传的巡检图片或视频'" />
        <el-empty
          v-else
          :description="
            operationType === 'view' ? '暂无异常照片或视频' : '请选择要上传的巡检图片或视频'
          "
        />
      </el-card>
      <el-result v-if="hasException === false"
                 icon="success"
                 title="设备运行正常"
                 :sub-title="operationType === 'view' ? '' : '无需上传照片'" />
      <el-result
        v-if="hasException === false"
        icon="success"
        title="设备运行正常"
        :sub-title="operationType === 'view' ? '' : '无需上传照片'"
      />
    </main>
    <template #footer>
      <footer class="footer-buttons">
        <el-button type="primary"
                   v-if="operationType !== 'view'"
                   @click="submitUpload">提交</el-button>
        <el-button v-if="hasException === true && operationType !== 'view'"
                   type="warning"
                   @click="goToRepair">
        <el-button type="primary" v-if="operationType !== 'view'" @click="submitUpload"
          >提交</el-button
        >
        <el-button
          v-if="hasException === true && operationType !== 'view'"
          type="warning"
          @click="goToRepair"
        >
          æ–°å¢žæŠ¥ä¿®
        </el-button>
        <el-button @click="handleClose">{{ operationType === 'view' ? '关闭' : '取消' }}</el-button>
        <el-button @click="handleClose">{{ operationType === "view" ? "关闭" : "取消" }}</el-button>
      </footer>
    </template>
  </FormDialog>
  <el-dialog v-model="showVideoDialog"
             :title="currentVideoFile?.originalFilename || currentVideoFile?.name || '视频预览'"
             width="720px">
    <video v-if="currentVideoFile"
           :src="currentVideoFile.url || currentVideoFile.downloadUrl"
           class="video-player"
           controls
           autoplay />
  <el-dialog
    v-model="showVideoDialog"
    :title="currentVideoFile?.originalFilename || currentVideoFile?.name || '视频预览'"
    width="720px"
  >
    <video
      v-if="currentVideoFile"
      :src="currentVideoFile.url || currentVideoFile.downloadUrl"
      class="video-player"
      controls
      autoplay
    />
  </el-dialog>
</template>
<script setup>
  import { computed, ref } from "vue";
  import { useRouter } from "vue-router";
  import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
  import { Camera, Close, VideoCamera } from "@element-plus/icons-vue";
  import axios from "axios";
  import dayjs from "dayjs";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import { uploadInspectionTask } from "@/api/inspectionManagement/index.js";
  import { getToken } from "@/utils/auth";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
import { Camera, Close, VideoCamera } from "@element-plus/icons-vue";
import axios from "axios";
import dayjs from "dayjs";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { uploadInspectionTask } from "@/api/inspectionManagement/index.js";
import { getToken } from "@/utils/auth";
  const emit = defineEmits(["closeDia", "success"]);
  const router = useRouter();
const emit = defineEmits(["closeDia", "success"]);
const router = useRouter();
  const dialogVisible = ref(false);
  const taskInfo = ref(null);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  const operationType = ref("add"); // add, view
const dialogVisible = ref(false);
const taskInfo = ref(null);
const uploading = ref(false);
const uploadProgress = ref(0);
const operationType = ref("add"); // add, view
  const beforeModelValue = ref([]);
const beforeModelValue = ref([]);
const inspectionItems = ref([]);
const completedItemIds = ref([]);
const inspectionProjectSummary = computed(() =>
  [...new Set(inspectionItems.value.map((item) => item.itemProject).filter(Boolean))].join("、")
);
  const formattedInspector = computed(() => {
    if (!taskInfo.value?.inspector) return [];
    if (Array.isArray(taskInfo.value.inspector)) return taskInfo.value.inspector;
    if (typeof taskInfo.value.inspector === "string") {
      return taskInfo.value.inspector
        .split(",")
        .map(s => s.trim())
        .filter(s => s);
const formattedInspector = computed(() => {
  if (!taskInfo.value?.inspector) return [];
  if (Array.isArray(taskInfo.value.inspector)) return taskInfo.value.inspector;
  if (typeof taskInfo.value.inspector === "string") {
    return taskInfo.value.inspector
      .split(",")
      .map((s) => s.trim())
      .filter((s) => s);
  }
  return [taskInfo.value.inspector];
});
const formatFrequencyType = (type) => {
  const mapping = {
    DAILY: "每日",
    WEEKLY: "每周",
    MONTHLY: "每月",
    QUARTERLY: "季度",
  };
  return mapping[type] || type;
};
const formatFrequencyDetail = (detail) => {
  if (typeof detail !== "string") return detail;
  const replacements = {
    MON: "周一",
    TUE: "周二",
    WED: "周三",
    THU: "周四",
    FRI: "周五",
    SAT: "周六",
    SUN: "周日",
  };
  return detail.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (match) => replacements[match]);
};
const formatDateTime = (date) => {
  if (!date) return "-";
  return dayjs(date).format("YYYY-MM-DD");
};
const hasException = ref(null);
const abnormalDescription = ref("");
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
const uploadConfig = {
  action: "/common/upload",
  limit: 10,
  fileSize: 50,
  fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
};
const uploadFileUrl = computed(() => `${import.meta.env.VITE_APP_BASE_API}${uploadConfig.action}`);
const processFileUrl = (fileUrl) => {
  if (!fileUrl) return "";
  let currentUrl = String(fileUrl);
  if (currentUrl.includes("\\")) {
    const uploadsIndex = currentUrl.toLowerCase().indexOf("uploads");
    if (uploadsIndex > -1) {
      currentUrl = `/${currentUrl.substring(uploadsIndex).replace(/\\/g, "/")}`;
    } else {
      const fileName = currentUrl.split("\\").pop();
      currentUrl = `/uploads/${fileName}`;
    }
    return [taskInfo.value.inspector];
  }
  if (currentUrl && !currentUrl.startsWith("http")) {
    if (!currentUrl.startsWith("/")) {
      currentUrl = `/${currentUrl}`;
    }
    currentUrl = __BASE_API__ + currentUrl;
  }
  return currentUrl;
};
const normalizeList = (list, fileType) => {
  if (!Array.isArray(list)) return [];
  return list.filter(Boolean).map((item) => {
    let currentType = item.type;
    if (!currentType && item.contentType) {
      currentType = item.contentType.startsWith("video") ? "video" : "image";
    } else if (!currentType) {
      currentType = fileType || "image";
    }
    return {
      ...item,
      url: processFileUrl(item.url || item.previewURL || item.downloadUrl || item.path || ""),
      downloadUrl: processFileUrl(
        item.downloadUrl || item.url || item.previewURL || item.path || ""
      ),
      name: item.name || item.originalFilename || item.bucketFilename,
      tempId: item.tempId || item.id || item.tempFileId,
      tempFileId: item.tempFileId || item.tempId || item.id,
      size: item.size || item.byteSize || 0,
      type: currentType,
      status: "success",
      uid: item.uid || `${Date.now()}-${Math.random()}`,
    };
  });
};
  const formatFrequencyType = type => {
    const mapping = {
      DAILY: "每日",
      WEEKLY: "每周",
      MONTHLY: "每月",
      QUARTERLY: "季度",
    };
    return mapping[type] || type;
  };
const resetState = () => {
  taskInfo.value = null;
  beforeModelValue.value = [];
  inspectionItems.value = [];
  completedItemIds.value = [];
  hasException.value = null;
  abnormalDescription.value = "";
  uploading.value = false;
  uploadProgress.value = 0;
  showVideoDialog.value = false;
  currentVideoFile.value = null;
  operationType.value = "add";
};
  const formatFrequencyDetail = detail => {
    if (typeof detail !== "string") return detail;
    const replacements = {
      MON: "周一",
      TUE: "周二",
      WED: "周三",
      THU: "周四",
      FRI: "周五",
      SAT: "周六",
      SUN: "周日",
    };
    return detail.replace(
      /MON|TUE|WED|THU|FRI|SAT|SUN/g,
      match => replacements[match]
    );
  };
const openDialog = (type, row) => {
  operationType.value = type || "add";
  const raw = JSON.parse(JSON.stringify(row?.__raw || row || {}));
  taskInfo.value = raw;
  inspectionItems.value = Array.isArray(raw.inspectionItems) ? raw.inspectionItems : [];
  completedItemIds.value = inspectionItems.value
    .filter((item) => Number(item.itemStatus) === 1)
    .map((item) => item.id);
  const formatDateTime = date => {
    if (!date) return "-";
    return dayjs(date).format("YYYY-MM-DD");
  };
  const hasException = ref(null);
  const abnormalDescription = ref("");
  const showVideoDialog = ref(false);
  const currentVideoFile = ref(null);
  const uploadConfig = {
    action: "/common/upload",
    limit: 10,
    fileSize: 50,
    fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
  };
  const uploadFileUrl = computed(
    () => `${import.meta.env.VITE_APP_BASE_API}${uploadConfig.action}`
  beforeModelValue.value = normalizeList(
    raw.commonFileListBeforeVO || raw.commonFileListBefore || [],
    "image"
  );
  const processFileUrl = fileUrl => {
    if (!fileUrl) return "";
  abnormalDescription.value = raw.abnormalDescription || "";
    let currentUrl = String(fileUrl);
    if (currentUrl.includes("\\")) {
      const uploadsIndex = currentUrl.toLowerCase().indexOf("uploads");
      if (uploadsIndex > -1) {
        currentUrl = `/${currentUrl.substring(uploadsIndex).replace(/\\/g, "/")}`;
      } else {
        const fileName = currentUrl.split("\\").pop();
        currentUrl = `/uploads/${fileName}`;
      }
    }
    if (currentUrl && !currentUrl.startsWith("http")) {
      if (!currentUrl.startsWith("/")) {
        currentUrl = `/${currentUrl}`;
      }
      currentUrl = __BASE_API__ + currentUrl;
    }
    return currentUrl;
  };
  const normalizeList = (list, fileType) => {
    if (!Array.isArray(list)) return [];
    return list.filter(Boolean).map(item => {
      let currentType = item.type;
      if (!currentType && item.contentType) {
        currentType = item.contentType.startsWith("video") ? "video" : "image";
      } else if (!currentType) {
        currentType = fileType || "image";
      }
      return {
        ...item,
        url: processFileUrl(
          item.url || item.previewURL || item.downloadUrl || item.path || ""
        ),
        downloadUrl: processFileUrl(
          item.downloadUrl || item.url || item.previewURL || item.path || ""
        ),
        name: item.name || item.originalFilename || item.bucketFilename,
        tempId: item.tempId || item.id || item.tempFileId,
        tempFileId: item.tempFileId || item.tempId || item.id,
        size: item.size || item.byteSize || 0,
        type: currentType,
        status: "success",
        uid: item.uid || `${Date.now()}-${Math.random()}`,
      };
    });
  };
  const resetState = () => {
    taskInfo.value = null;
    beforeModelValue.value = [];
  if (raw.hasException !== undefined && raw.hasException !== null) {
    hasException.value = raw.hasException;
  } else if (raw.inspectionResult !== undefined && raw.inspectionResult !== null) {
    hasException.value = String(raw.inspectionResult) === "0";
  } else {
    hasException.value = null;
    abnormalDescription.value = "";
    uploading.value = false;
    uploadProgress.value = 0;
    showVideoDialog.value = false;
    currentVideoFile.value = null;
    operationType.value = "add";
  };
  }
  const openDialog = (type, row) => {
    operationType.value = type || "add";
    const raw = JSON.parse(JSON.stringify(row?.__raw || row || {}));
    taskInfo.value = raw;
  if (hasException.value !== true && beforeModelValue.value.length) {
    hasException.value = true;
  }
    beforeModelValue.value = normalizeList(
      raw.commonFileListBeforeVO || raw.commonFileListBefore || [],
      "image"
  dialogVisible.value = true;
};
const isItemCompleted = (item) => completedItemIds.value.includes(item.id);
const toggleItem = (item, checked) => {
  const completed = new Set(completedItemIds.value);
  if (checked) completed.add(item.id);
  else completed.delete(item.id);
  completedItemIds.value = [...completed];
};
const handleClose = () => {
  dialogVisible.value = false;
  resetState();
  emit("closeDia");
};
const previewVideo = (file) => {
  currentVideoFile.value = file;
  showVideoDialog.value = true;
};
const uploadFile = async (uploadRequest) => {
  const rawFile = uploadRequest.file;
  if (beforeModelValue.value.length >= uploadConfig.limit) {
    ElMessage.warning(`最多只能选择${uploadConfig.limit}个文件`);
    return;
  }
  const ext = rawFile.name.split(".").pop()?.toLowerCase();
  if (!uploadConfig.fileType.includes(ext)) {
    ElMessage.warning(`文件格式不支持,请上传 ${uploadConfig.fileType.join("/")} æ ¼å¼`);
    return;
  }
  if (rawFile.size > uploadConfig.fileSize * 1024 * 1024) {
    ElMessage.warning(`文件大小不能超过 ${uploadConfig.fileSize}MB`);
    return;
  }
  const token = getToken();
  if (!token) {
    ElMessage.warning("用户未登录");
    return;
  }
  const formData = new FormData();
  formData.append("files", rawFile);
  formData.append("type", 10); // ç”Ÿäº§å‰å›ºå®šä¸º10
  uploading.value = true;
  uploadProgress.value = 0;
  try {
    const { data } = await axios.post(uploadFileUrl.value, formData, {
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "multipart/form-data",
      },
      onUploadProgress: (event) => {
        if (event.total) {
          uploadProgress.value = Math.round((event.loaded / event.total) * 100);
        }
      },
    });
    if (data.code !== 200) {
      ElMessage.error(data.msg || "上传失败");
      return;
    }
    const resultData = Array.isArray(data.data) ? data.data[0] : data.data;
    const finalUrl = processFileUrl(
      resultData.url || resultData.previewURL || resultData.downloadUrl || ""
    );
    const finalName = resultData.name || resultData.originalFilename || resultData.bucketFilename;
    const finalId = resultData.tempId || resultData.id || resultData.tempFileId;
    abnormalDescription.value = raw.abnormalDescription || "";
    const uploadedFile = {
      ...resultData,
      url: finalUrl,
      downloadUrl: finalUrl,
      name: finalName,
      tempId: finalId,
      tempFileId: resultData.tempFileId || finalId,
      size: rawFile.size || resultData.size || resultData.byteSize || 0,
      type: rawFile.type?.startsWith("video") ? "video" : "image",
      status: "success",
      uid: `${Date.now()}-${Math.random()}`,
    };
    if (raw.hasException !== undefined && raw.hasException !== null) {
      hasException.value = raw.hasException;
    } else if (
      raw.inspectionResult !== undefined &&
      raw.inspectionResult !== null
    ) {
      hasException.value = String(raw.inspectionResult) === "0";
    } else {
      hasException.value = null;
    }
    beforeModelValue.value.push(uploadedFile);
    ElMessage.success("上传成功");
  } catch (error) {
    ElMessage.error(error?.message || "上传失败");
  } finally {
    uploading.value = false;
  }
};
    if (hasException.value !== true && beforeModelValue.value.length) {
      hasException.value = true;
    }
const buildFileItem = (item) => ({
  id: item?.id,
  tempId: item?.tempId,
  tempFileId: item?.tempFileId,
  url: item?.downloadUrl || item?.url || "",
  downloadUrl: item?.downloadUrl || item?.url || "",
  name: item?.name,
  bucketFilename: item?.bucketFilename || item?.name,
  originalFilename: item?.originalFilename || item?.name,
  size: item?.size || 0,
  byteSize: item?.byteSize || item?.size || 0,
  contentType: item?.contentType || "",
  type: item?.type,
});
    dialogVisible.value = true;
  };
const submitUpload = async () => {
  if (hasException.value === null) {
    ElMessage.warning("请选择巡检状态");
    return;
  }
  const handleClose = () => {
    dialogVisible.value = false;
    resetState();
    emit("closeDia");
  };
  if (hasException.value === true) {
    const totalFiles = beforeModelValue.value.length;
  const previewVideo = file => {
    currentVideoFile.value = file;
    showVideoDialog.value = true;
  };
  const uploadFile = async uploadRequest => {
    const rawFile = uploadRequest.file;
    if (beforeModelValue.value.length >= uploadConfig.limit) {
      ElMessage.warning(`最多只能选择${uploadConfig.limit}个文件`);
    if (!totalFiles) {
      ElMessage.warning("请上传异常照片或视频");
      return;
    }
    const ext = rawFile.name.split(".").pop()?.toLowerCase();
    if (!uploadConfig.fileType.includes(ext)) {
      ElMessage.warning(
        `文件格式不支持,请上传 ${uploadConfig.fileType.join("/")} æ ¼å¼`
      );
    if (!abnormalDescription.value.trim()) {
      ElMessage.warning("请填写异常描述");
      return;
    }
  }
    if (rawFile.size > uploadConfig.fileSize * 1024 * 1024) {
      ElMessage.warning(`文件大小不能超过 ${uploadConfig.fileSize}MB`);
      return;
    }
    const token = getToken();
    if (!token) {
      ElMessage.warning("用户未登录");
      return;
    }
    const formData = new FormData();
    formData.append("files", rawFile);
    formData.append("type", 10); // ç”Ÿäº§å‰å›ºå®šä¸º10
    uploading.value = true;
    uploadProgress.value = 0;
    try {
      const { data } = await axios.post(uploadFileUrl.value, formData, {
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "multipart/form-data",
        },
        onUploadProgress: event => {
          if (event.total) {
            uploadProgress.value = Math.round((event.loaded / event.total) * 100);
          }
        },
      });
      if (data.code !== 200) {
        ElMessage.error(data.msg || "上传失败");
        return;
      }
      const resultData = Array.isArray(data.data) ? data.data[0] : data.data;
      const finalUrl = processFileUrl(
        resultData.url || resultData.previewURL || resultData.downloadUrl || ""
      );
      const finalName =
        resultData.name ||
        resultData.originalFilename ||
        resultData.bucketFilename;
      const finalId = resultData.tempId || resultData.id || resultData.tempFileId;
      const uploadedFile = {
        ...resultData,
        url: finalUrl,
        downloadUrl: finalUrl,
        name: finalName,
        tempId: finalId,
        tempFileId: resultData.tempFileId || finalId,
        size: rawFile.size || resultData.size || resultData.byteSize || 0,
        type: rawFile.type?.startsWith("video") ? "video" : "image",
        status: "success",
        uid: `${Date.now()}-${Math.random()}`,
      };
      beforeModelValue.value.push(uploadedFile);
      ElMessage.success("上传成功");
    } catch (error) {
      ElMessage.error(error?.message || "上传失败");
    } finally {
      uploading.value = false;
    }
  };
  const buildFileItem = item => ({
    id: item?.id,
    tempId: item?.tempId,
    tempFileId: item?.tempFileId,
    url: item?.downloadUrl || item?.url || "",
    downloadUrl: item?.downloadUrl || item?.url || "",
    name: item?.name,
    bucketFilename: item?.bucketFilename || item?.name,
    originalFilename: item?.originalFilename || item?.name,
    size: item?.size || 0,
    byteSize: item?.byteSize || item?.size || 0,
    contentType: item?.contentType || "",
    type: item?.type,
  const loading = ElLoading.service({
    text: "提交中...",
    background: "rgba(0, 0, 0, 0.3)",
  });
  const submitUpload = async () => {
    if (hasException.value === null) {
      ElMessage.warning("请选择巡检状态");
      return;
    }
  try {
    const allFiles = [...beforeModelValue.value];
    if (hasException.value === true) {
      const totalFiles = beforeModelValue.value.length;
    const tempFileIds = allFiles
      .map((item) => item?.tempId ?? item?.tempFileId ?? item?.id)
      .filter(Boolean);
      if (!totalFiles) {
        ElMessage.warning("请上传异常照片或视频");
        return;
      }
    const {
      createTime,
      updateTime,
      storageBlobDTO,
      commonFileListAfterVO,
      commonFileListVO,
      commonFileListBeforeVO,
      commonFileListAfter,
      commonFileList,
      commonFileListBefore,
      __raw,
      ...baseTaskInfo
    } = taskInfo.value || {};
      if (!abnormalDescription.value.trim()) {
        ElMessage.warning("请填写异常描述");
        return;
      }
    }
    const loading = ElLoading.service({
      text: "提交中...",
      background: "rgba(0, 0, 0, 0.3)",
    });
    try {
      const allFiles = [...beforeModelValue.value];
      const tempFileIds = allFiles
        .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
        .filter(Boolean);
      const {
        createTime,
        updateTime,
        storageBlobDTO,
        commonFileListAfterVO,
        commonFileListVO,
        commonFileListBeforeVO,
        commonFileListAfter,
        commonFileList,
        commonFileListBefore,
        __raw,
        ...baseTaskInfo
      } = taskInfo.value || {};
      const submitData = {
        ...baseTaskInfo,
        commonFileListBeforeDTO: beforeModelValue.value.map(buildFileItem),
        commonFileListDTO: [],
        commonFileListAfterDTO: [],
        hasException: hasException.value,
        inspectionResult: hasException.value ? 0 : 1,
        abnormalDescription: abnormalDescription.value,
        tempFileIds,
      };
      const result = await uploadInspectionTask(submitData);
      if (result && (result.code === 200 || result.success)) {
        ElMessage.success("提交成功");
        dialogVisible.value = false;
        resetState();
        emit("success");
        emit("closeDia");
      } else {
        ElMessage.error(result?.msg || result?.message || "提交失败");
      }
    } catch (error) {
      ElMessage.error(error?.message || "提交失败");
    } finally {
      loading.close();
    }
  };
  const removeFile = async index => {
    try {
      await ElMessageBox.confirm("确定要删除这个文件吗?", "确认删除", {
        type: "warning",
      });
      beforeModelValue.value.splice(index, 1);
    } catch {}
  };
  const goToRepair = () => {
    const taskData = {
      taskId: taskInfo.value?.taskId || taskInfo.value?.id,
      taskName: taskInfo.value?.taskName,
      inspectionLocation: taskInfo.value?.inspectionLocation,
      inspector: taskInfo.value?.inspector,
      hasException: hasException.value,
      inspectionResult: hasException.value ? 0 : 1,
    const submitData = {
      ...baseTaskInfo,
      inspectionItems: inspectionItems.value.map((item) => ({
        id: item.id,
        itemStatus: completedItemIds.value.includes(item.id) ? 1 : 0,
        remarks: item.remarks || "",
      })),
      commonFileListBeforeDTO: beforeModelValue.value.map(buildFileItem),
      commonFileListDTO: [],
      commonFileListAfterDTO: [],
      uploadedFiles: {
        before: beforeModelValue.value,
        after: [],
        issue: [],
      },
      hasException: hasException.value,
      inspectionResult: hasException.value ? 0 : 1,
      abnormalDescription: abnormalDescription.value,
      tempFileIds,
    };
    sessionStorage.setItem("repairTaskInfo", JSON.stringify(taskData));
    router.push("/equipmentManagement/repair/add");
  };
    const result = await uploadInspectionTask(submitData);
  const formatFileSize = size => {
    if (!size) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    let index = 0;
    let fileSize = size;
    while (fileSize >= 1024 && index < units.length - 1) {
      fileSize /= 1024;
      index += 1;
    if (result && (result.code === 200 || result.success)) {
      ElMessage.success("提交成功");
      dialogVisible.value = false;
      resetState();
      emit("success");
      emit("closeDia");
    } else {
      ElMessage.error(result?.msg || result?.message || "提交失败");
    }
  } catch (error) {
    ElMessage.error(error?.message || "提交失败");
  } finally {
    loading.close();
  }
};
    return `${fileSize.toFixed(2)} ${units[index]}`;
const removeFile = async (index) => {
  try {
    await ElMessageBox.confirm("确定要删除这个文件吗?", "确认删除", {
      type: "warning",
    });
    beforeModelValue.value.splice(index, 1);
  } catch {}
};
const goToRepair = () => {
  const taskData = {
    taskId: taskInfo.value?.taskId || taskInfo.value?.id,
    taskName: taskInfo.value?.taskName,
    inspectionLocation: taskInfo.value?.inspectionLocation,
    inspector: taskInfo.value?.inspector,
    hasException: hasException.value,
    inspectionResult: hasException.value ? 0 : 1,
    commonFileListBeforeDTO: beforeModelValue.value.map(buildFileItem),
    commonFileListDTO: [],
    commonFileListAfterDTO: [],
    uploadedFiles: {
      before: beforeModelValue.value,
      after: [],
      issue: [],
    },
  };
  defineExpose({
    openDialog,
  });
  sessionStorage.setItem("repairTaskInfo", JSON.stringify(taskData));
  router.push("/equipmentManagement/repair/add");
};
const formatFileSize = (size) => {
  if (!size) return "0 B";
  const units = ["B", "KB", "MB", "GB"];
  let index = 0;
  let fileSize = size;
  while (fileSize >= 1024 && index < units.length - 1) {
    fileSize /= 1024;
    index += 1;
  }
  return `${fileSize.toFixed(2)} ${units[index]}`;
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
  .inspection-upload-page {
    min-height: 70vh;
    background: #f5f7fa;
    padding: 20px 20px 90px;
    box-sizing: border-box;
  }
.inspection-upload-page {
  min-height: 70vh;
  background: #f5f7fa;
  padding: 20px 20px 90px;
  box-sizing: border-box;
}
  .upload-content {
    max-width: 960px;
    margin: 20px auto 0;
  }
.upload-content {
  max-width: 960px;
  margin: 20px auto 0;
}
  .section-card {
    margin-bottom: 16px;
  }
.section-card {
  margin-bottom: 16px;
}
  .section-card h3 {
    margin: 0 0 16px;
    font-size: 16px;
  }
.section-card h3 {
  margin: 0 0 16px;
  font-size: 16px;
}
  .upload-buttons {
    display: flex;
    gap: 12px;
    margin: 16px 0;
  }
.section-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  color: #909399;
}
.section-heading h3 {
  margin: 0;
  color: #303133;
}
.inspection-item-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  padding: 14px 8px;
  border-top: 1px solid #ebeef5;
}
.inspection-item-copy {
  min-width: 0;
  flex: 1;
}
.inspection-item-title {
  font-weight: 600;
  color: #303133;
}
.inspection-item-content {
  margin: 6px 0 0 20px;
  color: #606266;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
  .upload-progress {
    margin-bottom: 16px;
  }
.upload-buttons {
  display: flex;
  gap: 12px;
  margin: 16px 0;
}
  .file-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 12px;
  }
.upload-progress {
  margin-bottom: 16px;
}
  .file-preview-container {
    position: relative;
    aspect-ratio: 1;
    border-radius: 8px;
    overflow: hidden;
    background: #f2f3f5;
  }
.file-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 12px;
}
  .file-preview {
    width: 100%;
    height: 100%;
  }
.file-preview-container {
  position: relative;
  aspect-ratio: 1;
  border-radius: 8px;
  overflow: hidden;
  background: #f2f3f5;
}
  .video-preview {
    width: 100%;
    height: 100%;
    background: #303133;
    color: #fff;
    display: flex;
    gap: 6px;
    align-items: center;
    justify-content: center;
    cursor: pointer;
  }
.file-preview {
  width: 100%;
  height: 100%;
}
  .delete-btn {
    position: absolute;
    top: 6px;
    right: 6px;
  }
.video-preview {
  width: 100%;
  height: 100%;
  background: #303133;
  color: #fff;
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
  .file-info {
    margin-top: 6px;
    font-size: 12px;
  }
.delete-btn {
  position: absolute;
  top: 6px;
  right: 6px;
}
  .file-name {
    color: #606266;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
.file-info {
  margin-top: 6px;
  font-size: 12px;
}
  .file-size {
    color: #909399;
    margin-top: 2px;
  }
.file-name {
  color: #606266;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
  .upload-summary {
    margin-top: 16px;
  }
.file-size {
  color: #909399;
  margin-top: 2px;
}
  .footer-buttons {
    position: sticky;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 14px 20px 0;
    background: #f5f7fa;
    display: flex;
    justify-content: center;
    gap: 12px;
  }
.upload-summary {
  margin-top: 16px;
}
  .video-player {
    width: 100%;
    max-height: 70vh;
    background: #000;
  }
.footer-buttons {
  position: sticky;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 14px 20px 0;
  background: #f5f7fa;
  display: flex;
  justify-content: center;
  gap: 12px;
}
.video-player {
  width: 100%;
  max-height: 70vh;
  background: #000;
}
</style>
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -1,461 +1,592 @@
<template>
  <div class="app-container">
    <el-form :inline="true"
             :model="queryParams"
             class="search-form">
      <el-form-item label="巡检任务名称">
        <el-input v-model="queryParams.taskName"
                  placeholder="请输入巡检任务名称"
                  clearable
                  style="width: 200px " />
      </el-form-item>
      <el-form-item>
        <el-button type="primary"
                   @click="handleQuery">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-card>
      <div style="display: flex;flex-direction: row;justify-content: space-between;margin-bottom: 10px;">
        <el-radio-group v-model="activeRadio"
                        @change="radioChange">
          <el-radio-button v-for="tab in radios"
                           :key="tab.name"
                           :label="tab.label"
                           :value="tab.name" />
        </el-radio-group>
        <!-- æ“ä½œæŒ‰é’®åŒº -->
        <el-space v-if="activeRadio !== 'task'">
          <el-button type="primary"
                     :icon="Plus"
                     @click="handleAdd(undefined)">新建</el-button>
          <el-button type="danger"
                     :icon="Delete"
                     @click="handleDelete">删除</el-button>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
        <el-space v-else>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
    <el-tabs v-model="activeRadio" @tab-change="radioChange">
      <el-tab-pane v-for="tab in radios" :key="tab.name" :label="tab.label" :name="tab.name" />
    </el-tabs>
    <InspectionItemManager v-if="activeRadio === 'items'" />
    <template v-else>
      <div class="search_form">
        <el-form :inline="true" :model="queryParams">
          <el-form-item label="巡检任务名称">
            <el-input
              v-model="queryParams.taskName"
              placeholder="请输入巡检任务名称"
              clearable
              style="width: 240px"
              @keyup.enter="handleQuery"
            />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="handleQuery">搜索</el-button>
            <el-button @click="resetQuery">重置</el-button>
          </el-form-item>
        </el-form>
      </div>
      <div>
        <PIMTable :table-loading="tableLoading"
                  :table-data="tableData"
                  :column="tableColumns"
                  @selection-change="handleSelectionChange"
                  @pagination="handlePagination"
                  :is-selection="true"
                  :border="true"
                  :page="{
                  current: pageNum,
                  size: pageSize,
                  total: total,
                  layout: 'total, sizes, prev, pager, next, jumper'
                }"
                  height="calc(100vh - 23em)"
                  :table-style="{ width: '100%' }">
      <div class="table_list">
        <div class="actions">
          <el-text size="large">{{ activeRadio === "task" ? "巡检记录" : "巡检任务" }}</el-text>
          <div class="action-buttons">
            <template v-if="activeRadio !== 'task'">
              <el-button type="primary" :icon="Plus" @click="handleAdd(undefined)">新建</el-button>
              <el-button
                type="danger"
                :icon="Delete"
                :disabled="selectedRows.length === 0"
                @click="handleDelete"
                >删除</el-button
              >
            </template>
            <el-button @click="handleOut">导出</el-button>
          </div>
        </div>
        <PIMTable
          :table-loading="tableLoading"
          :table-data="tableData"
          :column="tableColumns"
          @selection-change="handleSelectionChange"
          @pagination="handlePagination"
          :is-selection="true"
          :border="true"
          :page="{
            current: pageNum,
            size: pageSize,
            total: total,
            layout: 'total, sizes, prev, pager, next, jumper',
          }"
        >
          <template #inspector="{ row }">
            <div class="person-tags">
              <!-- è°ƒè¯•信息,上线时删除 -->
              <!-- {{ console.log('inspector data:', row.inspector) }} -->
              <template v-if="row.inspector && row.inspector.length > 0">
                <el-tag v-for="(person, index) in row.inspector"
                        :key="index"
                        size="small"
                        type="primary"
                        class="person-tag">
                <el-tag
                  v-for="(person, index) in row.inspector"
                  :key="index"
                  size="small"
                  type="primary"
                  class="person-tag"
                >
                  {{ person }}
                </el-tag>
              </template>
              <span v-else
                    class="no-data">--</span>
              <span v-else class="no-data">--</span>
            </div>
          </template>
          <template #isEnabled="{ row }">
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'"
                    size="small">
              {{ row.isEnabled == 1 ? '是' : '否' }}
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'" size="small">
              {{ row.isEnabled == 1 ? "是" : "否" }}
            </el-tag>
          </template>
        </PIMTable>
      </div>
    </el-card>
    <form-dia ref="formDia"
              @closeDia="handleQuery"></form-dia>
    <view-files ref="viewFiles"></view-files>
    <upload-files ref="uploadFiles"
                  @success="handleQuery"
                  @closeDia="handleQuery"></upload-files>
      <form-dia ref="formDia" @closeDia="handleQuery" />
      <view-files ref="viewFiles" />
      <upload-files ref="uploadFiles" @success="handleQuery" @closeDia="handleQuery" />
      <el-dialog v-model="taskDetailVisible" title="巡检任务详情" width="900px">
        <template v-if="taskDetail">
          <el-descriptions :column="2" border>
            <el-descriptions-item label="巡检任务名称">{{
              taskDetail.taskName || "-"
            }}</el-descriptions-item>
            <el-descriptions-item label="执行巡检人">{{
              formatInspector(taskDetail.inspector)
            }}</el-descriptions-item>
            <el-descriptions-item label="频次">{{
              formatFrequencyType(taskDetail.frequencyType)
            }}</el-descriptions-item>
            <el-descriptions-item label="开始日期与时间">{{
              formatFrequencyDetail(taskDetail.frequencyDetail)
            }}</el-descriptions-item>
            <el-descriptions-item label="登记人">{{
              taskDetail.registrant || "-"
            }}</el-descriptions-item>
            <el-descriptions-item label="登记日期">{{
              taskDetail.createTime ? dayjs(taskDetail.createTime).format("YYYY-MM-DD") : "-"
            }}</el-descriptions-item>
            <el-descriptions-item label="备注" :span="2">{{
              taskDetail.remarks || "-"
            }}</el-descriptions-item>
          </el-descriptions>
          <div class="task-detail-items">
            <div class="task-detail-heading">
              å·¡æ£€å†…容({{ taskDetail.inspectionItems?.length || 0 }}项)
            </div>
            <el-empty v-if="!taskDetail.inspectionItems?.length" description="暂无巡检内容" />
            <div
              v-for="(item, index) in taskDetail.inspectionItems"
              :key="item.id || index"
              class="task-detail-item"
            >
              <div class="task-detail-project">
                {{ index + 1 }}. {{ item.itemProject || "未命名项目" }}
              </div>
              <div class="task-detail-content">{{ item.itemContent || "-" }}</div>
            </div>
          </div>
        </template>
        <template #footer>
          <el-button @click="taskDetailVisible = false">关闭</el-button>
        </template>
      </el-dialog>
    </template>
  </div>
</template>
<script setup>
  import { Delete, Plus } from "@element-plus/icons-vue";
  import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
import { Delete, Plus } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue";
import { ElMessageBox } from "element-plus";
import dayjs from "dayjs";
  // ç»„件引入
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
  import UploadFiles from "@/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue";
  import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
// ç»„件引入
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
import UploadFiles from "@/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue";
import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
import InspectionItemManager from "@/views/equipmentManagement/inspectionManagement/components/InspectionItemManager.vue";
  // æŽ¥å£å¼•å…¥
  import {
    delTimingTask,
    inspectionTaskList,
    timingTaskList,
  } from "@/api/inspectionManagement/index.js";
// æŽ¥å£å¼•å…¥
import {
  delTimingTask,
  inspectionTaskList,
  timingTaskList,
} from "@/api/inspectionManagement/index.js";
  // å…¨å±€å˜é‡
  const { proxy } = getCurrentInstance();
  const formDia = ref();
  const viewFiles = ref();
  const uploadFiles = ref();
// å…¨å±€å˜é‡
const { proxy } = getCurrentInstance();
const formDia = ref();
const viewFiles = ref();
const uploadFiles = ref();
const taskDetailVisible = ref(false);
const taskDetail = ref(null);
  // æŸ¥è¯¢å‚æ•°
  const queryParams = reactive({
    taskName: "",
  });
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  taskName: "",
});
  // å•选框配置
  const activeRadio = ref("taskManage");
  const radios = reactive([
    { name: "taskManage", label: "巡检任务" },
    { name: "task", label: "巡检记录" },
  ]);
// å•选框配置
const activeRadio = ref("taskManage");
const radios = reactive([
  { name: "taskManage", label: "巡检任务" },
  { name: "task", label: "巡检记录" },
  { name: "items", label: "巡检内容" },
]);
  // è¡¨æ ¼æ•°æ®
  const selectedRows = ref([]);
  const tableData = ref([]);
  const operationsArr = ref([]);
  const tableColumns = ref([]);
  const tableLoading = ref(false);
  const total = ref(0);
  const pageNum = ref(1);
  const pageSize = ref(10);
// è¡¨æ ¼æ•°æ®
const selectedRows = ref([]);
const tableData = ref([]);
const operationsArr = ref([]);
const tableColumns = ref([]);
const tableLoading = ref(false);
const total = ref(0);
const pageNum = ref(1);
const pageSize = ref(10);
  // åˆ—配置
  const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 200 },
    { prop: "inspectionProject", label: "巡检项目", minWidth: 180 },
    { prop: "remarks", label: "备注", minWidth: 180 },
    { prop: "inspector", label: "执行巡检人", minWidth: 180, slot: "inspector" },
    {
      prop: "isEnabled",
      label: "是否启用",
      minWidth: 100,
      dataType: "slot",
      slot: "isEnabled",
// åˆ—配置
const taskColumns = [
  { prop: "taskName", label: "巡检任务名称", minWidth: 200 },
  { prop: "inspector", label: "执行巡检人", minWidth: 180, slot: "inspector" },
  {
    prop: "isEnabled",
    label: "是否启用",
    minWidth: 100,
    dataType: "slot",
    slot: "isEnabled",
  },
  {
    prop: "frequencyType",
    label: "频次",
    minWidth: 120,
    formatData: (params) => {
      return params === "DAILY"
        ? "每日"
        : params === "WEEKLY"
        ? "每周"
        : params === "MONTHLY"
        ? "每月"
        : params === "QUARTERLY"
        ? "季度"
        : "";
    },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 120,
      formatData: params => {
        return params === "DAILY"
          ? "每日"
          : params === "WEEKLY"
          ? "每周"
          : params === "MONTHLY"
          ? "每月"
          : params === "QUARTERLY"
          ? "季度"
          : "";
      },
  },
  {
    prop: "frequencyDetail",
    label: "开始日期与时间",
    minWidth: 200,
    formatter: (row, column, cellValue) => {
      // å…ˆåˆ¤æ–­æ˜¯å¦æ˜¯å­—符串
      if (typeof cellValue !== "string") return "";
      let val = cellValue;
      const replacements = {
        MON: "周一",
        TUE: "周二",
        WED: "周三",
        THU: "周四",
        FRI: "周五",
        SAT: "周六",
        SUN: "周日",
      };
      // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
      return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (match) => replacements[match]);
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 200,
      formatter: (row, column, cellValue) => {
        // å…ˆåˆ¤æ–­æ˜¯å¦æ˜¯å­—符串
        if (typeof cellValue !== "string") return "";
        let val = cellValue;
        const replacements = {
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
        };
        // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      },
    },
    { prop: "registrant", label: "登记人", minWidth: 120 },
    {
      prop: "createTime",
      label: "登记日期",
      minWidth: 180,
      formatData: cell => {
        if (!cell) return "-";
        try {
          return dayjs(cell).format("YYYY-MM-DD");
        } catch {
          return cell;
        }
      },
    },
    // {
    //   prop: "inspectionResult",
    //   label: "巡检结果",
    //   minWidth: 100,
    //   dataType: "tag",
    //   formatData: val => {
    //     return val == 1 ? "正常" : "异常";
    //   },
    //   formatType: val => {
    //     return val == 1 ? "success" : "danger";
    //   },
    // },
    { prop: "abnormalDescription", label: "异常描述", minWidth: 150 },
  ]);
  // æ“ä½œåˆ—配置
  const getOperationColumn = operations => {
    if (!operations || operations.length === 0) return null;
    const operationConfig = {
      label: "操作",
      width: operations.length > 1 ? 180 : 130,
      fixed: "right",
      align: "center",
      dataType: "action",
      operation: operations
        .map(op => {
          switch (op) {
            case "view":
              return {
                name: "详情",
                clickFun: openViewDialog,
                color: "#409EFF",
              };
            case "edit":
              return {
                name: "编辑",
                clickFun: handleAdd,
                color: "#409EFF",
              };
            case "upload":
              return {
                name: "上传",
                clickFun: openUploadDialog,
                color: "#409EFF",
              };
            case "viewFile":
              return {
                name: "查看附件",
                clickFun: viewFile,
                color: "#67C23A",
              };
            default:
              return null;
          }
        })
        .filter(Boolean),
    };
    return operationConfig;
  };
  onMounted(() => {
    radioChange("taskManage");
  });
  // å•选变化
  const radioChange = value => {
    if (value === "taskManage") {
      const operationColumn = getOperationColumn(["edit"]);
      tableColumns.value = [
        ...columns.value,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["view", "upload", "viewFile"]);
      // å·¡æ£€è®°å½•不展示"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["view", "upload", "viewFile"];
    }
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
  // æŸ¥è¯¢æ“ä½œ
  const handleQuery = () => {
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
  // åˆ†é¡µå¤„理
  const handlePagination = val => {
    pageNum.value = val.page;
    pageSize.value = val.limit;
    getList();
  };
  // èŽ·å–åˆ—è¡¨æ•°æ®
  const getList = () => {
    tableLoading.value = true;
    const params = {
      ...queryParams,
      size: pageSize.value,
      current: pageNum.value,
    };
    let apiCall;
    if (activeRadio.value === "task") {
      apiCall = inspectionTaskList(params);
    } else {
      apiCall = timingTaskList(params);
    }
    apiCall
      .then(res => {
        const rawData = res.data.records || [];
        // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
        tableData.value = rawData.map(item => {
          const processedItem = { ...item };
          processedItem.__raw = { ...item };
          // å¤„理 inspector å­—段
          if (processedItem.inspector) {
            if (typeof processedItem.inspector === "string") {
              // å­—符串按逗号分割
              processedItem.inspector = processedItem.inspector
                .split(",")
                .map(s => s.trim())
                .filter(s => s);
            } else if (!Array.isArray(processedItem.inspector)) {
              // éžæ•°ç»„转为数组
              processedItem.inspector = [processedItem.inspector];
            }
          } else {
            // ç©ºå€¼è®¾ä¸ºç©ºæ•°ç»„
            processedItem.inspector = [];
          }
          return processedItem;
        });
        total.value = res.data.total || 0;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  // é‡ç½®æŸ¥è¯¢
  const resetQuery = () => {
    for (const key in queryParams) {
      if (!["pageNum", "pageSize"].includes(key)) {
        queryParams[key] = "";
  },
  { prop: "registrant", label: "登记人", minWidth: 120 },
  {
    prop: "createTime",
    label: "登记日期",
    minWidth: 180,
    formatData: (cell) => {
      if (!cell) return "-";
      try {
        return dayjs(cell).format("YYYY-MM-DD");
      } catch {
        return cell;
      }
    }
    handleQuery();
  };
    },
  },
  { prop: "remarks", label: "备注", minWidth: 180, showOverflowTooltip: true },
];
const recordColumns = [
  { prop: "taskName", label: "巡检任务名称", minWidth: 200 },
  { prop: "inspector", label: "执行巡检人", minWidth: 180, slot: "inspector" },
  {
    prop: "frequencyType",
    label: "频次",
    minWidth: 120,
    formatData: (params) => formatFrequencyType(params),
  },
  {
    prop: "frequencyDetail",
    label: "开始日期与时间",
    minWidth: 200,
    formatter: (row, column, cellValue) => formatFrequencyDetail(cellValue),
  },
  { prop: "registrant", label: "登记人", minWidth: 120 },
  {
    prop: "createTime",
    label: "登记日期",
    minWidth: 180,
    formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD") : "-"),
  },
  { prop: "abnormalDescription", label: "异常描述", minWidth: 150, showOverflowTooltip: true },
  { prop: "remarks", label: "备注", minWidth: 180, showOverflowTooltip: true },
];
  // æ–°å¢ž / ç¼–辑
  const handleAdd = row => {
    const type = row ? "edit" : "add";
    nextTick(() => {
      formDia.value?.openDialog(type, row);
    });
  };
// æ“ä½œåˆ—配置
const getOperationColumn = (operations) => {
  if (!operations || operations.length === 0) return null;
  // æŸ¥çœ‹é™„ä»¶
  const viewFile = row => {
    nextTick(() => {
      viewFiles.value?.openDialog(row);
    });
  };
  const openUploadDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog("add", row);
    });
  };
  const openViewDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog("view", row);
    });
  };
  // åˆ é™¤æ“ä½œ
  const handleDelete = () => {
    if (!selectedRows.value.length) {
      proxy.$modal.msgWarning("请选择要删除的数据");
      return;
    }
    const deleteIds = selectedRows.value.map(item => item.id);
    proxy.$modal
      .confirm("是否确认删除所选数据项?")
      .then(() => {
        return delTimingTask(deleteIds);
      })
      .then(() => {
        proxy.$modal.msgSuccess("删除成功");
        handleQuery();
      })
      .catch(() => {});
  };
  // å¤šé€‰å˜æ›´
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // æ ¹æ®å½“前选中的标签页调用不同的导出接口
        if (activeRadio.value === "taskManage") {
          // å·¡æ£€ä»»åŠ¡
          proxy.download("/timingTask/export", {}, "巡检任务.xlsx");
        } else if (activeRadio.value === "task") {
          // å·¡æ£€è®°å½•
          proxy.download("/inspectionTask/export", {}, "巡检记录.xlsx");
  const operationConfig = {
    label: "操作",
    width: operations.length > 1 ? 180 : 130,
    fixed: "right",
    align: "center",
    dataType: "action",
    operation: operations
      .map((op) => {
        switch (op) {
          case "view":
            return {
              name: "详情",
              clickFun: openViewDialog,
              color: "#409EFF",
            };
          case "viewTask":
            return {
              name: "详情",
              clickFun: openTaskDetail,
              color: "#409EFF",
            };
          case "edit":
            return {
              name: "编辑",
              clickFun: handleAdd,
              color: "#409EFF",
            };
          case "upload":
            return {
              name: "上传",
              clickFun: openUploadDialog,
              color: "#409EFF",
            };
          case "viewFile":
            return {
              name: "查看附件",
              clickFun: viewFile,
              color: "#67C23A",
            };
          default:
            return null;
        }
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
      .filter(Boolean),
  };
  return operationConfig;
};
onMounted(() => {
  radioChange("taskManage");
});
// å•选变化
const radioChange = (value) => {
  if (value === "taskManage") {
    const operationColumn = getOperationColumn(["viewTask", "edit"]);
    tableColumns.value = [...taskColumns, ...(operationColumn ? [operationColumn] : [])];
    operationsArr.value = ["viewTask", "edit"];
  } else if (value === "task") {
    const operationColumn = getOperationColumn(["view", "upload", "viewFile"]);
    tableColumns.value = [...recordColumns, ...(operationColumn ? [operationColumn] : [])];
    operationsArr.value = ["view", "upload", "viewFile"];
  } else {
    tableColumns.value = [];
    operationsArr.value = [];
    return;
  }
  pageNum.value = 1;
  pageSize.value = 10;
  getList();
};
// æŸ¥è¯¢æ“ä½œ
const handleQuery = () => {
  pageNum.value = 1;
  pageSize.value = 10;
  getList();
};
// åˆ†é¡µå¤„理
const handlePagination = (val) => {
  pageNum.value = val.page;
  pageSize.value = val.limit;
  getList();
};
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  if (activeRadio.value === "items") return;
  tableLoading.value = true;
  const params = {
    ...queryParams,
    size: pageSize.value,
    current: pageNum.value,
  };
  let apiCall;
  if (activeRadio.value === "task") {
    apiCall = inspectionTaskList(params);
  } else {
    apiCall = timingTaskList(params);
  }
  apiCall
    .then((res) => {
      const rawData = res.data.records || [];
      // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
      tableData.value = rawData.map((item) => {
        const processedItem = { ...item };
        processedItem.__raw = { ...item };
        // å¤„理 inspector å­—段
        if (processedItem.inspector) {
          if (typeof processedItem.inspector === "string") {
            // å­—符串按逗号分割
            processedItem.inspector = processedItem.inspector
              .split(",")
              .map((s) => s.trim())
              .filter((s) => s);
          } else if (!Array.isArray(processedItem.inspector)) {
            // éžæ•°ç»„转为数组
            processedItem.inspector = [processedItem.inspector];
          }
        } else {
          // ç©ºå€¼è®¾ä¸ºç©ºæ•°ç»„
          processedItem.inspector = [];
        }
        return processedItem;
      });
      total.value = res.data.total || 0;
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
  for (const key in queryParams) {
    if (!["pageNum", "pageSize"].includes(key)) {
      queryParams[key] = "";
    }
  }
  handleQuery();
};
// æ–°å¢ž / ç¼–辑
const handleAdd = (row) => {
  const type = row ? "edit" : "add";
  nextTick(() => {
    formDia.value?.openDialog(type, row);
  });
};
const openTaskDetail = (row) => {
  taskDetail.value = row;
  taskDetailVisible.value = true;
};
const formatInspector = (value) => {
  if (Array.isArray(value)) return value.join("、") || "-";
  return value || "-";
};
const formatFrequencyType = (type) =>
  ({
    DAILY: "每日",
    WEEKLY: "每周",
    MONTHLY: "每月",
    QUARTERLY: "季度",
  }[type] || "-");
const formatFrequencyDetail = (detail) => {
  if (typeof detail !== "string" || !detail) return "-";
  const replacements = {
    MON: "周一",
    TUE: "周二",
    WED: "周三",
    THU: "周四",
    FRI: "周五",
    SAT: "周六",
    SUN: "周日",
  };
  return detail.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (match) => replacements[match]);
};
// æŸ¥çœ‹é™„ä»¶
const viewFile = (row) => {
  nextTick(() => {
    viewFiles.value?.openDialog(row);
  });
};
const openUploadDialog = (row) => {
  nextTick(() => {
    uploadFiles.value?.openDialog("add", row);
  });
};
const openViewDialog = (row) => {
  nextTick(() => {
    uploadFiles.value?.openDialog("view", row);
  });
};
// åˆ é™¤æ“ä½œ
const handleDelete = () => {
  if (!selectedRows.value.length) {
    proxy.$modal.msgWarning("请选择要删除的数据");
    return;
  }
  const deleteIds = selectedRows.value.map((item) => item.id);
  proxy.$modal
    .confirm("是否确认删除所选数据项?")
    .then(() => {
      return delTimingTask(deleteIds);
    })
    .then(() => {
      proxy.$modal.msgSuccess("删除成功");
      handleQuery();
    })
    .catch(() => {});
};
// å¤šé€‰å˜æ›´
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      // æ ¹æ®å½“前选中的标签页调用不同的导出接口
      if (activeRadio.value === "taskManage") {
        // å·¡æ£€ä»»åŠ¡
        proxy.download("/timingTask/export", {}, "巡检任务.xlsx");
      } else if (activeRadio.value === "task") {
        // å·¡æ£€è®°å½•
        proxy.download("/inspectionTask/export", {}, "巡检记录.xlsx");
      }
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
</script>
<style scoped>
  .person-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
  }
.actions {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}
.action-buttons {
  display: flex;
  gap: 8px;
}
.table_list {
  margin-top: unset;
}
.person-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
  .person-tag {
    margin-right: 4px;
    margin-bottom: 2px;
  }
.person-tag {
  margin-right: 4px;
  margin-bottom: 2px;
}
  .no-data {
    color: #909399;
    font-size: 14px;
  }
.no-data {
  color: #909399;
  font-size: 14px;
}
.task-detail-items {
  margin-top: 20px;
  border: 1px solid var(--el-border-color-lighter);
  border-radius: 4px;
}
.task-detail-heading {
  padding: 12px 16px;
  font-weight: 600;
  border-bottom: 1px solid var(--el-border-color-lighter);
}
.task-detail-item {
  padding: 14px 16px;
  border-bottom: 1px solid var(--el-border-color-lighter);
}
.task-detail-item:last-child {
  border-bottom: 0;
}
.task-detail-project {
  font-weight: 600;
  color: var(--el-text-color-primary);
}
.task-detail-content {
  margin: 6px 0 0 20px;
  color: var(--el-text-color-regular);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
</style>
src/views/equipmentManagement/upkeep/Form/ItemManager.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,331 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="filters" :inline="true">
        <el-form-item label="所属设备">
          <el-select
            v-model="filters.deviceLedgerId"
            clearable
            filterable
            placeholder="请选择设备"
            style="width: 220px"
          >
            <el-option
              v-for="device in deviceOptions"
              :key="device.id"
              :label="device.deviceName"
              :value="device.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="保养频率">
          <el-select
            v-model="filters.frequencyType"
            clearable
            placeholder="请选择频率"
            style="width: 200px"
          >
            <el-option
              v-for="item in frequencyOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="保养内容">
          <el-input
            v-model="keyword"
            clearable
            placeholder="请输入保养内容"
            style="width: 240px"
            :prefix-icon="Search"
            @keyup.enter="search"
            @clear="search"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="search">搜索</el-button>
          <el-button @click="resetFilters">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="table_list">
      <div class="actions">
        <el-text size="large">保养内容清单</el-text>
        <el-button type="primary" icon="Plus" @click="openAdd">新增保养内容</el-button>
      </div>
      <el-table v-loading="loading" :data="items" border>
        <el-table-column type="index" label="序号" width="80" align="center" :index="indexMethod" />
        <el-table-column
          prop="itemProject"
          label="保养项目"
          min-width="160"
          show-overflow-tooltip
        />
        <el-table-column prop="itemName" label="保养内容" min-width="280" show-overflow-tooltip />
        <el-table-column label="所属设备" min-width="180" align="center" show-overflow-tooltip>
          <template #default="{ row }">{{
            row.deviceLedgerId ? deviceNameMap[row.deviceLedgerId] || "-" : "通用"
          }}</template>
        </el-table-column>
        <el-table-column label="频率" width="120" align="center">
          <template #default="{ row }">{{ frequencyLabel(row.frequencyType) }}</template>
        </el-table-column>
        <el-table-column label="操作" width="150" align="center" fixed="right">
          <template #default="{ row }">
            <el-button link type="primary" @click="openEdit(row)">编辑</el-button>
            <el-button link type="danger" @click="removeItem(row)">删除</el-button>
          </template>
        </el-table-column>
        <template #empty>
          <el-empty description="还没有保养内容,请先逐条添加">
            <el-button type="primary" icon="Plus" @click="openAdd">新增保养内容</el-button>
          </el-empty>
        </template>
      </el-table>
      <div class="pagination-wrap">
        <el-pagination
          v-model:current-page="pageNum"
          v-model:page-size="pageSize"
          background
          :page-sizes="[10, 20, 50]"
          :total="total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="load"
        />
      </div>
    </div>
    <el-dialog
      v-model="dialogVisible"
      :title="form.id ? '编辑保养内容' : '新增保养内容'"
      width="520px"
      align-center
      destroy-on-close
      :close-on-click-modal="false"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="90px" label-position="right">
        <el-form-item label="保养项目" prop="itemProject">
          <el-input
            v-model.trim="form.itemProject"
            placeholder="请输入保养项目"
            maxlength="200"
            show-word-limit
          />
        </el-form-item>
        <el-form-item label="保养内容" prop="itemName">
          <el-input
            v-model.trim="form.itemName"
            type="textarea"
            :rows="4"
            resize="vertical"
            placeholder="请输入保养内容"
            maxlength="2000"
            show-word-limit
          />
        </el-form-item>
        <el-form-item label="所属设备">
          <el-select
            v-model="form.deviceLedgerId"
            clearable
            filterable
            placeholder="不选择则为通用保养内容"
            no-data-text="暂无设备,请先在设备台账中添加"
            style="width: 100%"
          >
            <el-option
              v-for="device in deviceOptions"
              :key="device.id"
              :label="device.deviceName"
              :value="device.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="保养频率" prop="frequencyType">
          <el-select v-model="form.frequencyType" placeholder="请选择保养频率" style="width: 100%">
            <el-option
              v-for="item in frequencyOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="saving" @click="save">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from "@element-plus/icons-vue";
import {
  addMaintenanceItem,
  deleteMaintenanceItem,
  editMaintenanceItem,
  getMaintenanceItemPage,
} from "@/api/equipmentManagement/upkeep";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
const items = ref([]);
const loading = ref(false);
const saving = ref(false);
const dialogVisible = ref(false);
const formRef = ref();
const keyword = ref("");
const pageNum = ref(1);
const pageSize = ref(20);
const total = ref(0);
const deviceOptions = ref([]);
const filters = reactive({ deviceLedgerId: undefined, frequencyType: "" });
const frequencyOptions = [
  { label: "每日", value: "DAILY" },
  { label: "每周", value: "WEEKLY" },
  { label: "每月", value: "MONTHLY" },
  { label: "每季度", value: "QUARTERLY" },
  { label: "通用频率", value: "ALL" },
];
const deviceNameMap = computed(() =>
  Object.fromEntries(deviceOptions.value.map((device) => [String(device.id), device.deviceName]))
);
const frequencyLabel = (value) =>
  frequencyOptions.find((item) => item.value === value)?.label || value || "-";
const indexMethod = (index) => (pageNum.value - 1) * pageSize.value + index + 1;
const form = reactive({
  id: undefined,
  itemProject: "",
  itemName: "",
  deviceLedgerId: undefined,
  frequencyType: "",
  sortNum: 0,
  isActive: 1,
  remarks: "",
});
const rules = {
  itemProject: [{ required: true, message: "请输入保养项目", trigger: "blur" }],
  itemName: [{ required: true, message: "请输入保养内容", trigger: "blur" }],
  frequencyType: [{ required: true, message: "请选择保养频率", trigger: "change" }],
};
const load = async () => {
  loading.value = true;
  try {
    const { code, data } = await getMaintenanceItemPage({
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      itemName: keyword.value.trim(),
      isActive: 1,
      deviceLedgerId: filters.deviceLedgerId,
      frequencyType: filters.frequencyType,
      includeCommon: true,
    });
    if (code === 200) {
      items.value = data?.records || [];
      total.value = Number(data?.total || 0);
    }
  } finally {
    loading.value = false;
  }
};
const search = () => {
  pageNum.value = 1;
  load();
};
const resetFilters = () => {
  filters.deviceLedgerId = undefined;
  filters.frequencyType = "";
  keyword.value = "";
  search();
};
const handleSizeChange = () => {
  pageNum.value = 1;
  load();
};
const reset = () =>
  Object.assign(form, {
    id: undefined,
    itemProject: "",
    itemName: "",
    deviceLedgerId: undefined,
    frequencyType: "",
    sortNum: 0,
    isActive: 1,
    remarks: "",
  });
const openAdd = () => {
  reset();
  dialogVisible.value = true;
};
const openEdit = (row) => {
  Object.assign(form, { ...row });
  dialogVisible.value = true;
};
const save = async () => {
  if (
    !(await formRef.value
      ?.validate()
      .then(() => true)
      .catch(() => false))
  )
    return;
  saving.value = true;
  try {
    const { code } = form.id
      ? await editMaintenanceItem({ ...form })
      : await addMaintenanceItem({ ...form });
    if (code === 200) {
      ElMessage.success("保存成功");
      dialogVisible.value = false;
      pageNum.value = 1;
      await load();
    }
  } finally {
    saving.value = false;
  }
};
const removeItem = async (row) => {
  try {
    await ElMessageBox.confirm(`确定删除保养内容“${row.itemName}”吗?`, "提示", {
      type: "warning",
    });
    const { code } = await deleteMaintenanceItem(row.id);
    if (code === 200) {
      ElMessage.success("删除成功");
      await load();
    }
  } catch (_) {
    // ç”¨æˆ·å–消
  }
};
onMounted(async () => {
  const { data } = await getDeviceLedger();
  deviceOptions.value = data || [];
  await load();
});
</script>
<style scoped>
.actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}
.pagination-wrap {
  display: flex;
  justify-content: flex-end;
  padding-top: 12px;
}
</style>
src/views/equipmentManagement/upkeep/Form/MaintenanceItemSelector.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
<template>
  <el-dialog
    v-model="dialogVisible"
    title="选择保养项目"
    width="760px"
    append-to-body
    destroy-on-close
  >
    <div class="selector-toolbar">
      <el-input
        v-model="keyword"
        clearable
        placeholder="按保养项目搜索"
        @keyup.enter="search"
        @clear="search"
      />
      <el-button type="primary" @click="search">搜索</el-button>
    </div>
    <el-table v-loading="loading" :data="rows" border height="360">
      <el-table-column label="选择" width="70" align="center">
        <template #default="{ row }">
          <el-checkbox
            :model-value="isSelected(row.id)"
            @change="(checked) => toggleItem(row, checked)"
          />
        </template>
      </el-table-column>
      <el-table-column type="index" label="序号" width="70" />
      <el-table-column label="类型" width="90" align="center">
        <template #default="{ row }">
          <el-tag :type="row.deviceLedgerId ? 'primary' : 'info'" size="small">{{
            row.deviceLedgerId ? "本设备" : "通用"
          }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="itemProject" label="保养项目" min-width="300" show-overflow-tooltip />
      <el-table-column label="频率" width="110" align="center">
        <template #default="{ row }">
          <el-tag :type="row.frequencyType === 'ALL' ? 'info' : 'success'" size="small">{{
            frequencyLabel(row.frequencyType)
          }}</el-tag>
        </template>
      </el-table-column>
    </el-table>
    <div class="selector-pagination">
      <el-pagination
        v-model:current-page="pageNum"
        v-model:page-size="pageSize"
        :page-sizes="[10, 20, 50]"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="load"
      />
    </div>
    <template #footer>
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="confirm">确定选择</el-button>
    </template>
  </el-dialog>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { getMaintenanceItemPage } from "@/api/equipmentManagement/upkeep";
const props = defineProps({
  visible: { type: Boolean, default: false },
  selectedItems: { type: Array, default: () => [] },
  deviceLedgerId: { type: [Number, String], default: undefined },
  frequencyType: { type: String, default: "" },
});
const emit = defineEmits(["update:visible", "confirm"]);
const dialogVisible = computed({
  get: () => props.visible,
  set: (value) => emit("update:visible", value),
});
const rows = ref([]);
const loading = ref(false);
const keyword = ref("");
const pageNum = ref(1);
const pageSize = ref(10);
const total = ref(0);
const selected = ref(new Map());
const selectedList = computed(() => Array.from(selected.value.values()));
const frequencyLabel = (value) =>
  ({ DAILY: "每日", WEEKLY: "每周", MONTHLY: "每月", QUARTERLY: "每季度", ALL: "通用频率" }[
    value
  ] ||
  value ||
  "通用频率");
watch(
  () => props.visible,
  (visible) => {
    if (!visible) return;
    keyword.value = "";
    pageNum.value = 1;
    selected.value = new Map((props.selectedItems || []).map((item) => [String(item.id), item]));
    if (props.deviceLedgerId && props.frequencyType) load();
    else {
      rows.value = [];
      total.value = 0;
    }
  }
);
watch(
  () => [props.deviceLedgerId, props.frequencyType],
  ([deviceId, frequency]) => {
    if (!props.visible) return;
    selected.value = new Map();
    rows.value = [];
    total.value = 0;
    if (deviceId && frequency) {
      pageNum.value = 1;
      load();
    }
  }
);
const load = async () => {
  loading.value = true;
  try {
    const { code, data } = await getMaintenanceItemPage({
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      itemProject: keyword.value.trim(),
      isActive: 1,
      deviceLedgerId: props.deviceLedgerId,
      frequencyType: props.frequencyType,
      includeCommon: true,
    });
    if (code === 200) {
      rows.value = data?.records || [];
      total.value = Number(data?.total || 0);
    }
  } finally {
    loading.value = false;
  }
};
const search = () => {
  pageNum.value = 1;
  load();
};
const handleSizeChange = () => {
  pageNum.value = 1;
  load();
};
const isSelected = (id) => selected.value.has(String(id));
const toggleItem = (item, checked) => {
  const next = new Map(selected.value);
  if (checked) next.set(String(item.id), item);
  else next.delete(String(item.id));
  selected.value = next;
};
const confirm = () => {
  emit("confirm", selectedList.value);
  dialogVisible.value = false;
};
</script>
<style scoped>
.selector-toolbar {
  display: flex;
  gap: 10px;
  margin-bottom: 14px;
}
.selector-toolbar .el-input {
  max-width: 420px;
}
.selector-pagination {
  display: flex;
  justify-content: flex-end;
  margin-top: 14px;
}
</style>
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue
@@ -1,71 +1,122 @@
<template>
  <FormDialog v-model="visible"
              :title="'设备保养'"
              width="500px"
              @confirm="sendForm"
              @cancel="handleCancel"
              @close="handleClose">
    <el-form :model="form"
             label-width="100px">
  <FormDialog
    v-model="visible"
    :title="'设备保养'"
    width="1080px"
    @confirm="sendForm"
    @cancel="handleCancel"
    @close="handleClose"
  >
    <el-form :model="form" label-width="100px">
      <el-form-item label="实际保养人">
        <el-input v-model="form.maintenanceActuallyName"
                  placeholder="请输入实际保养人"></el-input>
        <el-input v-model="form.maintenanceActuallyName" placeholder="请输入实际保养人"></el-input>
      </el-form-item>
      <el-form-item label="实际保养日期">
        <el-date-picker v-model="form.maintenanceActuallyTime"
                        placeholder="请选择实际保养日期"
                        format="YYYY-MM-DD HH:mm:ss"
                        value-format="YYYY-MM-DD HH:mm:ss"
                        type="datetime"
                        clearable
                        style="width: 100%" />
        <el-date-picker
          v-model="form.maintenanceActuallyTime"
          placeholder="请选择实际保养日期"
          format="YYYY-MM-DD HH:mm:ss"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="datetime"
          clearable
          style="width: 100%"
        />
      </el-form-item>
      <el-form-item label="保养内容" required>
        <div v-if="form.maintenanceItems.length" class="maintenance-checklist">
          <div class="maintenance-checklist__header">
            <span
              >已完成 {{ completedCount }} /
              {{ form.maintenanceItems.length }} é¡¹ï¼Œè¯·å‹¾é€‰å·²ä¿å…»çš„内容</span
            >
            <el-button link type="primary" @click="toggleAll">{{
              completedCount === form.maintenanceItems.length ? "全部取消" : "全部勾选"
            }}</el-button>
          </div>
          <div
            v-for="(item, index) in form.maintenanceItems"
            :key="item.id"
            class="maintenance-checklist__item"
            :class="{ 'is-done': item.itemStatus === 1 }"
          >
            <el-checkbox
              class="maintenance-checklist__check"
              :model-value="item.itemStatus === 1"
              @change="(checked) => (item.itemStatus = checked ? 1 : 0)"
              >已完成</el-checkbox
            >
            <div class="maintenance-checklist__body">
              <div class="maintenance-checklist__content">
                <span class="maintenance-checklist__number">{{ index + 1 }}.</span
                ><span>{{ item.itemName || item.itemProject || "未填写保养内容" }}</span>
              </div>
            </div>
          </div>
        </div>
        <el-empty v-else description="该保养记录没有保养内容" :image-size="60" />
      </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-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-form-item label="保养结果">
        <el-input v-model="form.maintenanceResult"
                  placeholder="请输入保养结果"
                  type="text" />
        <el-input v-model="form.maintenanceResult" placeholder="请输入保养结果" type="text" />
      </el-form-item>
      <el-form-item label="设备备件">
        <el-select v-model="form.sparePartsIds"
                   :loading="loadingSparePartOptions"
                   placeholder="请选择设备备件"
                   multiple
                   filterable>
          <el-option v-for="item in sparePartOptions"
                     :key="item.id"
                     :label="item.name"
                     :value="item.id" />
        <el-select
          v-model="form.sparePartsIds"
          :loading="loadingSparePartOptions"
          placeholder="请选择设备备件"
          multiple
          filterable
        >
          <el-option
            v-for="item in sparePartOptions"
            :key="item.id"
            :label="item.name"
            :value="item.id"
          />
        </el-select>
      </el-form-item>
      <el-form-item v-if="selectedSpareParts.length"
                    label="领用数量">
      <el-form-item v-if="selectedSpareParts.length" label="领用数量">
        <div style="width: 100%">
          <div v-for="item in selectedSpareParts"
               :key="item.id"
               style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
            <div style="flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
          <div
            v-for="item in selectedSpareParts"
            :key="item.id"
            style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px"
          >
            <div
              style="
                flex: 1;
                min-width: 0;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
              "
            >
              {{ item.name }}
              <span v-if="item.quantity !== null && item.quantity !== undefined"
                    style="color: #909399;">
              <span
                v-if="item.quantity !== null && item.quantity !== undefined"
                style="color: #909399"
              >
                ï¼ˆåº“存:{{ item.quantity }})
              </span>
            </div>
            <el-input-number v-model="sparePartQtyMap[item.id]"
                             :min="1"
                             :max="item.quantity !== null && item.quantity !== undefined ? Number(item.quantity) : undefined"
                             :step="1"
                             controls-position="right"
                             style="width: 180px" />
            <el-input-number
              v-model="sparePartQtyMap[item.id]"
              :min="1"
              :max="
                item.quantity !== null && item.quantity !== undefined
                  ? Number(item.quantity)
                  : undefined
              "
              :step="1"
              controls-position="right"
              style="width: 180px"
            />
          </div>
        </div>
      </el-form-item>
@@ -77,177 +128,256 @@
</template>
<script setup>
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import FileUpload from "@/components/AttachmentUpload/file/index.vue";
  import { addMaintenance } from "@/api/equipmentManagement/upkeep";
  import useFormData from "@/hooks/useFormData";
  import dayjs from "dayjs";
  import useUserStore from "@/store/modules/user";
  import { ElMessage } from "element-plus";
  import { computed, ref, nextTick, getCurrentInstance } from "vue";
  import { getSparePartsList } from "@/api/equipmentManagement/spareParts.js";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { addMaintenance } from "@/api/equipmentManagement/upkeep";
import useFormData from "@/hooks/useFormData";
import dayjs from "dayjs";
import useUserStore from "@/store/modules/user";
import { ElMessage } from "element-plus";
import { computed, ref, nextTick, getCurrentInstance } from "vue";
import { getSparePartsList } from "@/api/equipmentManagement/spareParts.js";
  defineOptions({
    name: "保养模态框",
  });
defineOptions({
  name: "保养模态框",
});
  const emits = defineEmits(["ok"]);
const emits = defineEmits(["ok"]);
  const { proxy } = getCurrentInstance();
  // ä¿å­˜è®¡åˆ’保养记录的id
  const planId = ref();
  const visible = ref(false);
  const loading = ref(false);
  const userStore = useUserStore();
const { proxy } = getCurrentInstance();
// ä¿å­˜è®¡åˆ’保养记录的id
const planId = ref();
const visible = ref(false);
const loading = ref(false);
const userStore = useUserStore();
  const { form, resetForm } = useFormData({
    maintenanceActuallyName: undefined, // å®žé™…保养人
    maintenanceActuallyTime: undefined, // å®žé™…保养日期
    maintenanceResult: undefined, // ä¿å…»ç»“æžœ
    status: 0, // ä¿å…»çŠ¶æ€
    sparePartsIds: [],
    storageBlobDTOs: [],
  });
const { form, resetForm } = useFormData({
  maintenanceActuallyName: undefined, // å®žé™…保养人
  maintenanceActuallyTime: undefined, // å®žé™…保养日期
  maintenanceResult: undefined, // ä¿å…»ç»“æžœ
  status: 0, // ä¿å…»çŠ¶æ€
  maintenanceItems: [],
  sparePartsIds: [],
  storageBlobDTOs: [],
});
  const sparePartOptions = ref([]);
  const loadingSparePartOptions = ref(true);
  const sparePartQtyMap = ref({});
const sparePartOptions = ref([]);
const loadingSparePartOptions = ref(true);
const sparePartQtyMap = ref({});
  const selectedSpareParts = computed(() => {
    const ids = Array.isArray(form.sparePartsIds) ? form.sparePartsIds : [];
    const set = new Set(ids.map(i => String(i)));
    return (sparePartOptions.value || []).filter(p => set.has(String(p.id)));
  });
const selectedSpareParts = computed(() => {
  const ids = Array.isArray(form.sparePartsIds) ? form.sparePartsIds : [];
  const set = new Set(ids.map((i) => String(i)));
  return (sparePartOptions.value || []).filter((p) => set.has(String(p.id)));
});
  const setForm = data => {
    form.maintenanceActuallyName =
      data.maintenanceActuallyName ?? userStore.nickName;
    form.maintenanceActuallyTime = data.maintenanceActuallyTime
      ? dayjs(data.maintenanceActuallyTime).format("YYYY-MM-DD HH:mm:ss")
      : dayjs().format("YYYY-MM-DD HH:mm:ss");
    form.maintenanceResult = data.maintenanceResult;
    form.status = 1; // é»˜è®¤çŠ¶æ€ä¸ºå®Œç»“
    // multiple é€‰æ‹©å™¨è¦æ±‚数组;后端常返回 "1,2,3"
    if (Array.isArray(data?.sparePartsIds)) {
      form.sparePartsIds = data.sparePartsIds
        .map(v => Number(v))
        .filter(v => Number.isFinite(v));
    } else if (typeof data?.sparePartsIds === "string") {
      form.sparePartsIds = data.sparePartsIds
        .split(",")
        .map(s => Number(String(s).trim()))
        .filter(v => Number.isFinite(v));
    } else if (typeof data?.sparePartsIds === "number") {
      form.sparePartsIds = [data.sparePartsIds];
    } else {
      form.sparePartsIds = [];
const completedCount = computed(
  () => form.maintenanceItems.filter((item) => item.itemStatus === 1).length
);
const toggleAll = () => {
  const status = completedCount.value === form.maintenanceItems.length ? 0 : 1;
  form.maintenanceItems.forEach((item) => (item.itemStatus = status));
};
const setForm = (data) => {
  form.maintenanceActuallyName = data.maintenanceActuallyName ?? userStore.nickName;
  form.maintenanceActuallyTime = data.maintenanceActuallyTime
    ? dayjs(data.maintenanceActuallyTime).format("YYYY-MM-DD HH:mm:ss")
    : dayjs().format("YYYY-MM-DD HH:mm:ss");
  form.maintenanceResult = data.maintenanceResult;
  form.status = 1; // é»˜è®¤çŠ¶æ€ä¸ºå®Œç»“
  form.maintenanceItems = (data.maintenanceItems || []).map((item) => ({
    ...item,
    itemStatus: Number(item.itemStatus || 0),
  }));
  // multiple é€‰æ‹©å™¨è¦æ±‚数组;后端常返回 "1,2,3"
  if (Array.isArray(data?.sparePartsIds)) {
    form.sparePartsIds = data.sparePartsIds.map((v) => Number(v)).filter((v) => Number.isFinite(v));
  } else if (typeof data?.sparePartsIds === "string") {
    form.sparePartsIds = data.sparePartsIds
      .split(",")
      .map((s) => Number(String(s).trim()))
      .filter((v) => Number.isFinite(v));
  } else if (typeof data?.sparePartsIds === "number") {
    form.sparePartsIds = [data.sparePartsIds];
  } else {
    form.sparePartsIds = [];
  }
  form.storageBlobDTOs = data.storageBlobVOs || [];
};
/**
 * @desc ä¿å­˜ä¿å…»
 */
const sendForm = async () => {
  loading.value = true;
  try {
    if (!form.maintenanceItems.length) {
      ElMessage.warning("当前记录没有保养内容,请先编辑保养任务");
      return;
    }
    form.storageBlobDTOs = data.storageBlobVOs || [];
  };
  /**
   * @desc ä¿å­˜ä¿å…»
   */
  const sendForm = async () => {
    loading.value = true;
    try {
      // é¢†ç”¨æ•°é‡æ ¡éªŒ
      if (Array.isArray(form.sparePartsIds) && form.sparePartsIds.length > 0) {
        for (const partId of form.sparePartsIds) {
          const qty = Number(sparePartQtyMap.value?.[partId]);
          if (!Number.isFinite(qty) || qty <= 0) {
            proxy?.$modal?.msgError?.("请填写备件领用数量");
    // é¢†ç”¨æ•°é‡æ ¡éªŒ
    if (Array.isArray(form.sparePartsIds) && form.sparePartsIds.length > 0) {
      for (const partId of form.sparePartsIds) {
        const qty = Number(sparePartQtyMap.value?.[partId]);
        if (!Number.isFinite(qty) || qty <= 0) {
          proxy?.$modal?.msgError?.("请填写备件领用数量");
          return;
        }
        const part = sparePartOptions.value.find((p) => String(p.id) === String(partId));
        const stock = part?.quantity;
        if (stock !== null && stock !== undefined && Number.isFinite(Number(stock))) {
          if (qty > Number(stock)) {
            proxy?.$modal?.msgError?.(
              `备件「${part?.name || ""}」领用数量不能超过库存(${stock})`
            );
            return;
          }
          const part = sparePartOptions.value.find(
            p => String(p.id) === String(partId)
          );
          const stock = part?.quantity;
          if (
            stock !== null &&
            stock !== undefined &&
            Number.isFinite(Number(stock))
          ) {
            if (qty > Number(stock)) {
              proxy?.$modal?.msgError?.(
                `备件「${part?.name || ""}」领用数量不能超过库存(${stock})`
              );
              return;
            }
          }
        }
      }
      const data = {
        id: planId.value,
        ...form,
        sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
        sparePartsQty: form.sparePartsIds
          ? form.sparePartsIds
              .map(id => sparePartQtyMap.value?.[id] ?? 1)
              .join(",")
          : "",
        sparePartsUseList: form.sparePartsIds
          ? form.sparePartsIds.map(id => ({
              id,
              quantity: sparePartQtyMap.value?.[id] ?? 1,
            }))
          : [],
      };
      const { code } = await addMaintenance(data);
      if (code == 200) {
        ElMessage.success("保养成功");
        emits("ok");
        resetForm();
        sparePartQtyMap.value = {};
        visible.value = false;
      }
    } finally {
      loading.value = false;
    }
  };
    const data = {
      id: planId.value,
      ...form,
      maintenanceItems: form.maintenanceItems.map((item) => ({
        id: item.id,
        itemStatus: item.itemStatus,
      })),
      sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
      sparePartsQty: form.sparePartsIds
        ? form.sparePartsIds.map((id) => sparePartQtyMap.value?.[id] ?? 1).join(",")
        : "",
      sparePartsUseList: form.sparePartsIds
        ? form.sparePartsIds.map((id) => ({
            id,
            quantity: sparePartQtyMap.value?.[id] ?? 1,
          }))
        : [],
    };
    const { code } = await addMaintenance(data);
    if (code == 200) {
      ElMessage.success("保养成功");
      emits("ok");
      resetForm();
      sparePartQtyMap.value = {};
      visible.value = false;
    }
  } finally {
    loading.value = false;
  }
};
  const fetchSparePartOptions = () => {
    loadingSparePartOptions.value = true;
    // å’Œå¤‡ä»¶ç®¡ç†é¡µä¸€è‡´ï¼š/spareParts/listPage â†’ res.data.records
    getSparePartsList({ current: 1, size: 1000 })
      .then(res => {
        if (res.code === 200) {
          sparePartOptions.value = res?.data?.records || [];
        } else {
          sparePartOptions.value = [];
        }
      })
      .catch(() => {
const fetchSparePartOptions = () => {
  loadingSparePartOptions.value = true;
  // å’Œå¤‡ä»¶ç®¡ç†é¡µä¸€è‡´ï¼š/spareParts/listPage â†’ res.data.records
  getSparePartsList({ current: 1, size: 1000 })
    .then((res) => {
      if (res.code === 200) {
        sparePartOptions.value = res?.data?.records || [];
      } else {
        sparePartOptions.value = [];
      })
      .finally(() => {
        loadingSparePartOptions.value = false;
      });
  };
      }
    })
    .catch(() => {
      sparePartOptions.value = [];
    })
    .finally(() => {
      loadingSparePartOptions.value = false;
    });
};
  const handleCancel = () => {
    resetForm();
    sparePartQtyMap.value = {};
    visible.value = false;
  };
const handleCancel = () => {
  resetForm();
  sparePartQtyMap.value = {};
  visible.value = false;
};
  const handleClose = () => {
    resetForm();
    sparePartQtyMap.value = {};
    visible.value = false;
  };
const handleClose = () => {
  resetForm();
  sparePartQtyMap.value = {};
  visible.value = false;
};
  const open = async (id, row) => {
    planId.value = id; // ä¿å­˜è®¡åˆ’保养记录的id
    visible.value = true;
    await nextTick();
    fetchSparePartOptions();
    setForm(row);
  };
const open = async (id, row) => {
  planId.value = id; // ä¿å­˜è®¡åˆ’保养记录的id
  visible.value = true;
  await nextTick();
  fetchSparePartOptions();
  setForm(row);
};
  defineExpose({
    open,
  });
defineExpose({
  open,
});
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.maintenance-checklist {
  width: 100%;
  border: 1px solid #bfc7cf;
  background: #fff;
  overflow: hidden;
}
.maintenance-checklist__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 11px 16px;
  background: #f3f5f7;
  border-bottom: 1px solid #bfc7cf;
  color: #303946;
  font-size: 14px;
  font-weight: 600;
}
.maintenance-checklist__item {
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr) 110px;
  align-items: start;
  gap: 12px;
  padding: 15px 16px;
  border-bottom: 1px solid #d8dde2;
}
.maintenance-checklist__item:last-child {
  border-bottom: none;
}
.maintenance-checklist__item.is-done {
  background: #f6faf5;
}
.maintenance-checklist__check {
  grid-column: 3;
  grid-row: 1;
  justify-self: center;
  margin: 0;
}
.maintenance-checklist__body {
  grid-column: 1 / 3;
  grid-row: 1;
  min-width: 0;
}
.maintenance-checklist__content {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr);
  line-height: 1.75;
  color: #303946;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.maintenance-checklist__number {
  color: #606a75;
  font-weight: 600;
}
:deep(.el-dialog) {
  max-width: calc(100vw - 32px);
}
:deep(.el-dialog__body) {
  max-height: calc(100vh - 190px);
  overflow-y: auto;
}
@media (max-width: 720px) {
  .maintenance-checklist__item {
    grid-template-columns: 30px minmax(0, 1fr) 78px;
    gap: 8px;
    padding: 12px 8px;
  }
}
</style>
src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -2,7 +2,7 @@
  <FormDialog
    v-model="visible"
    :title="id ? '编辑设备保养计划' : '新增设备保养计划'"
    width="500px"
    width="960px"
    @confirm="sendForm"
    @cancel="handleCancel"
    @close="handleClose"
@@ -26,18 +26,46 @@
        </el-select>
      </el-form-item>
      <el-form-item label="规格型号">
        <el-input
          v-model="form.deviceModel"
          placeholder="请输入规格型号"
          disabled
        />
        <el-input v-model="form.deviceModel" placeholder="请输入规格型号" disabled />
      </el-form-item>
      <el-form-item label="保养项目">
        <el-input
            v-model="form.machineryCategory"
            type="textarea"
            placeholder="请输入保养项目"
        />
      <el-form-item label="保养频率">
        <el-select
          v-model="form.frequencyType"
          placeholder="请先选择保养频率"
          @change="clearSelectedItems"
        >
          <el-option label="每日" value="DAILY" />
          <el-option label="每周" value="WEEKLY" />
          <el-option label="每月" value="MONTHLY" />
          <el-option label="每季度" value="QUARTERLY" />
        </el-select>
      </el-form-item>
      <el-form-item label="保养内容">
        <div class="maintenance-selection">
          <el-button
            type="primary"
            plain
            :disabled="!form.deviceLedgerId || !form.frequencyType"
            @click="itemSelectorVisible = true"
            >选择保养内容</el-button
          >
          <div v-if="selectedMaintenanceItems.length" class="paper-list">
            <div class="paper-list__heading">
              <strong>保养项目清单</strong>
              <span>共 {{ selectedMaintenanceItems.length }} é¡¹</span>
            </div>
            <div
              v-for="(item, index) in selectedMaintenanceItems"
              :key="item.id"
              class="paper-list__row"
            >
              <span class="paper-list__number">{{ index + 1 }}</span>
              <span class="paper-list__content">{{ item.itemName }}</span>
              <el-button link type="danger" @click="removeMaintenanceItem(item.id)">移除</el-button>
            </div>
          </div>
          <el-empty v-else description="选择设备和频率后,添加本计划的保养项目" :image-size="54" />
        </div>
      </el-form-item>
      <el-form-item label="录入人">
        <el-select
@@ -64,11 +92,7 @@
        </el-select>
      </el-form-item>
      <el-form-item label="保养人">
        <el-input
          v-model="form.maintenancePerson"
          placeholder="请输入保养人姓名"
          clearable
        />
        <el-input v-model="form.maintenancePerson" placeholder="请输入保养人姓名" clearable />
      </el-form-item>
      <el-form-item label="计划保养日期">
        <el-date-picker
@@ -90,22 +114,26 @@
      </el-row>
    </el-form>
  </FormDialog>
  <MaintenanceItemSelector
    v-model:visible="itemSelectorVisible"
    :selected-items="selectedMaintenanceItems"
    :device-ledger-id="form.deviceLedgerId"
    :frequency-type="form.frequencyType"
    @confirm="applyMaintenanceItems"
  />
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
import {
  addUpkeep,
  editUpkeep,
  getUpkeepById,
} from "@/api/equipmentManagement/upkeep";
import { addUpkeep, editUpkeep, getUpkeepById } from "@/api/equipmentManagement/upkeep";
import { ElMessage } from "element-plus";
import useFormData from "@/hooks/useFormData";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import { onMounted } from "vue";
import { computed, onMounted } from "vue";
import dayjs from "dayjs";
import { userListNoPage } from "@/api/system/user.js";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import MaintenanceItemSelector from "./MaintenanceItemSelector.vue";
defineOptions({
  name: "设备保养新增计划",
@@ -116,8 +144,10 @@
const id = ref();
const visible = ref(false);
const loading = ref(false);
const itemSelectorVisible = ref(false);
const deviceOptions = ref([]);
const maintenanceItemOptions = ref([]);
const loadDeviceName = async () => {
  const { data } = await getDeviceLedger();
  deviceOptions.value = data;
@@ -130,14 +160,39 @@
  maintenancePlanTime: undefined, // è®¡åˆ’保养日期
  createUser: undefined, // å½•入人
  status: 0, //保修状态
  machineryCategory: undefined,
  maintenanceItemIds: [],
  frequencyType: "",
  storageBlobDTOs: [],
  maintenancePerson: undefined, // ä¿å…»äºº
});
const selectedMaintenanceItems = computed(() => {
  const ids = new Set((form.maintenanceItemIds || []).map(String));
  return maintenanceItemOptions.value.filter((item) => ids.has(String(item.id)));
});
const applyMaintenanceItems = (items) => {
  maintenanceItemOptions.value = items;
  form.maintenanceItemIds = items.map((item) => item.id);
};
const clearSelectedItems = () => {
  form.maintenanceItemIds = [];
  maintenanceItemOptions.value = [];
};
const removeMaintenanceItem = (id) => {
  form.maintenanceItemIds = form.maintenanceItemIds.filter(
    (itemId) => String(itemId) !== String(id)
  );
  maintenanceItemOptions.value = maintenanceItemOptions.value.filter(
    (item) => String(item.id) !== String(id)
  );
};
const setDeviceModel = (deviceId) => {
  const option = deviceOptions.value.find((item) => item.id === deviceId);
  form.deviceModel = option.deviceModel;
  if (option) {
    form.deviceModel = option.deviceModel;
    clearSelectedItems();
  }
};
/**
@@ -146,16 +201,21 @@
 */
const setForm = (data) => {
  form.deviceLedgerId = data.deviceLedgerId;
  form.frequencyType = data.frequencyType;
  form.deviceName = data.deviceName;
  form.deviceModel = data.deviceModel;
  form.createUser = Number(data.createUser);
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.maintenanceItemIds =
    data.maintenanceItemIds || data.maintenanceItems?.map((item) => item.maintenanceItemId) || [];
  maintenanceItemOptions.value = (data.maintenanceItems || []).map((item) => ({
    id: item.maintenanceItemId,
    itemName: item.itemName,
    isActive: item.isActive,
  }));
  form.maintenancePerson = data.maintenancePerson;
  if (data.maintenancePlanTime) {
    form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
      "YYYY-MM-DD HH:mm:ss"
    );
    form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD HH:mm:ss");
  }
  form.storageBlobDTOs = data.storageBlobVOs || [];
};
@@ -179,6 +239,14 @@
};
const sendForm = async () => {
  if (!form.deviceLedgerId || !form.frequencyType) {
    ElMessage.warning("请选择设备和保养频率");
    return;
  }
  if (!form.maintenanceItemIds?.length) {
    ElMessage.warning("请选择至少一个保养项目");
    return;
  }
  loading.value = true;
  try {
    const { code } = id.value
@@ -195,11 +263,13 @@
};
const handleCancel = () => {
  itemSelectorVisible.value = false;
  resetForm();
  visible.value = false;
};
const handleClose = () => {
  itemSelectorVisible.value = false;
  resetForm();
  visible.value = false;
};
@@ -207,6 +277,7 @@
const openModal = () => {
  id.value = undefined;
  visible.value = true;
  maintenanceItemOptions.value = [];
};
defineExpose({
@@ -215,4 +286,61 @@
});
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.maintenance-selection {
  width: 100%;
}
.paper-list {
  margin-top: 12px;
  border: 1px solid #cbd2d9;
  background: #fff;
}
.paper-list__heading {
  display: flex;
  justify-content: space-between;
  padding: 10px 14px;
  border-bottom: 1px solid #cbd2d9;
  background: #f5f7fa;
  color: #303946;
}
.paper-list__heading span {
  color: #606a75;
  font-size: 13px;
  font-weight: normal;
}
.paper-list__row {
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr) 52px;
  align-items: start;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid #e1e5e9;
}
.paper-list__row:last-child {
  border-bottom: 0;
}
.paper-list__number {
  color: #596575;
  text-align: center;
}
.paper-list__content {
  color: #303946;
  line-height: 1.7;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
:deep(.el-dialog) {
  max-width: calc(100vw - 32px);
}
:deep(.el-dialog__body) {
  max-height: calc(100vh - 180px);
  overflow-y: auto;
}
@media (max-width: 720px) {
  .paper-list__row {
    grid-template-columns: 28px minmax(0, 1fr) 42px;
    gap: 6px;
    padding: 10px 8px;
  }
}
</style>
src/views/equipmentManagement/upkeep/Form/RecordDetailDia.vue
@@ -1,99 +1,191 @@
<template>
    <FormDialog
        v-model="dialogVisitable"
        title="保养记录详情"
        width="800px"
        operation-type="detail"
        @close="cancel"
    >
        <el-descriptions border :column="2">
            <el-descriptions-item label="设备名称">
                {{ detailData.deviceName || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="规格型号">
                {{ detailData.deviceModel || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="计划保养日期">
                {{ formatDate(detailData.maintenancePlanTime) }}
            </el-descriptions-item>
            <el-descriptions-item label="录入人">
                {{ detailData.createUserName || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="保养项目" :span="2">
                {{ detailData.machineryCategory || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="实际保养人">
                {{ detailData.maintenanceActuallyName || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="实际保养日期">
                {{ formatDate(detailData.maintenanceActuallyTime) }}
            </el-descriptions-item>
            <el-descriptions-item label="保养结果" :span="2">
                {{ detailData.maintenanceResult || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="状态" :span="2">
                <el-tag v-if="detailData.status === 2" type="danger">失败</el-tag>
                <el-tag v-else-if="detailData.status === 1" type="success">完结</el-tag>
                <el-tag v-else type="warning">待保养</el-tag>
            </el-descriptions-item>
        </el-descriptions>
  <FormDialog
    v-model="dialogVisitable"
    title="保养记录详情"
    width="860px"
    operation-type="detail"
    @close="cancel"
  >
    <el-descriptions border :column="2" label-width="110px">
      <el-descriptions-item label="设备名称">
        {{ detailData.deviceName || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="规格型号">
        {{ detailData.deviceModel || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="计划保养日期">
        {{ formatDate(detailData.maintenancePlanTime) }}
      </el-descriptions-item>
      <el-descriptions-item label="录入人">
        {{ detailData.createUserName || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="实际保养人">
        {{ detailData.maintenanceActuallyName || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="实际保养日期">
        {{ formatDate(detailData.maintenanceActuallyTime) }}
      </el-descriptions-item>
      <el-descriptions-item label="保养结果" :span="2">
        {{ detailData.maintenanceResult || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="状态" :span="2">
        <el-tag v-if="detailData.status === 2" type="danger" size="small">失败</el-tag>
        <el-tag v-else-if="detailData.status === 1" type="success" size="small">完结</el-tag>
        <el-tag v-else type="warning" size="small">待保养</el-tag>
      </el-descriptions-item>
    </el-descriptions>
        <div v-if="fileList && fileList.length > 0" class="image-section">
            <div class="image-title">保养附件(图片):</div>
            <AttachmentUploadImage
                v-model:fileList="fileList"
                :disabled="true"
            />
        </div>
    </FormDialog>
    <div class="detail-section">
      <div class="section-title">
        <span>保养内容</span>
        <span class="section-count">共 {{ itemCount }} é¡¹ï¼Œå·²å®Œæˆ {{ doneCount }} é¡¹</span>
      </div>
      <div v-if="itemCount" class="item-list">
        <div
          v-for="(item, index) in detailData.maintenanceItems"
          :key="item.id || index"
          class="item-row"
        >
          <span class="item-index">{{ index + 1 }}</span>
          <div class="item-body">
            <div class="item-head">
              <span class="item-project">{{ item.itemProject || "未填写保养项目" }}</span>
              <el-tag size="small" effect="light" :type="statusMeta(item.itemStatus).type">{{
                statusMeta(item.itemStatus).label
              }}</el-tag>
            </div>
            <div class="item-content">{{ item.itemName }}</div>
          </div>
        </div>
      </div>
      <el-empty v-else :image-size="48" description="暂无保养内容" />
    </div>
    <div v-if="fileList && fileList.length > 0" class="image-section">
      <div class="image-title">保养附件(图片):</div>
      <AttachmentUploadImage v-model:fileList="fileList" :disabled="true" />
    </div>
  </FormDialog>
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ref } from "vue";
import { computed, ref } from "vue";
import dayjs from "dayjs";
import { attachmentList } from "@/api/basicData/storageAttachment.js";
import AttachmentUploadImage from '@/components/AttachmentUpload/image/index.vue';
import AttachmentUploadImage from "@/components/AttachmentUpload/image/index.vue";
const dialogVisitable = ref(false);
const detailData = ref({});
const fileList = ref([]);
const itemCount = computed(() => detailData.value.maintenanceItems?.length || 0);
const doneCount = computed(
  () => (detailData.value.maintenanceItems || []).filter((item) => item.itemStatus === 1).length
);
const formatDate = (date) => {
    return date ? dayjs(date).format("YYYY-MM-DD") : "--";
  return date ? dayjs(date).format("YYYY-MM-DD") : "--";
};
const statusMeta = (status) => {
  if (status === 1) return { label: "已完成", type: "success" };
  if (status === 2) return { label: "异常", type: "danger" };
  if (Number(detailData.value.status) === 1) return { label: "未完成", type: "warning" };
  return { label: "待完成", type: "info" };
};
const openDialog = (row) => {
    dialogVisitable.value = true;
    detailData.value = { ...row };
    fileList.value = [];
  dialogVisitable.value = true;
  detailData.value = { ...row };
  fileList.value = [];
    if (row.id) {
        attachmentList({
            recordType: 'device_maintenance',
            recordId: row.id,
        }).then(res => {
            if (res && res.data) {
                fileList.value = res.data || [];
            }
        });
    }
  if (row.id) {
    attachmentList({
      recordType: "device_maintenance",
      recordId: row.id,
    }).then((res) => {
      if (res && res.data) {
        fileList.value = res.data || [];
      }
    });
  }
};
const cancel = () => {
    dialogVisitable.value = false;
  dialogVisitable.value = false;
};
defineExpose({ openDialog });
</script>
<style scoped>
.detail-section {
  margin-top: 16px;
}
.section-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  font-weight: 600;
  color: #303133;
}
.section-count {
  font-size: 12px;
  font-weight: normal;
  color: #909399;
}
.item-list {
  border: 1px solid #ebeef5;
  border-radius: 4px;
  overflow: hidden;
}
.item-row {
  display: flex;
  gap: 12px;
  padding: 12px 14px;
  border-bottom: 1px solid #ebeef5;
}
.item-row:last-child {
  border-bottom: none;
}
.item-index {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  line-height: 24px;
  text-align: center;
  border-radius: 50%;
  background: #ecf5ff;
  color: #409eff;
  font-size: 12px;
}
.item-body {
  flex: 1;
  min-width: 0;
}
.item-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 4px;
}
.item-project {
  font-weight: 600;
  color: #303133;
}
.item-content {
  line-height: 1.6;
  color: #606266;
  white-space: pre-wrap;
  word-break: break-all;
}
.image-section {
    margin-top: 20px;
  margin-top: 20px;
}
.image-title {
    font-weight: bold;
    margin-bottom: 10px;
    color: #606266;
  font-weight: bold;
  margin-bottom: 10px;
  color: #606266;
}
</style>
src/views/equipmentManagement/upkeep/Form/detailDia.vue
@@ -1,118 +1,190 @@
<template>
    <FormDialog
        v-model="dialogVisitable"
        title="保养任务详情"
        width="800px"
        operation-type="detail"
        @close="cancel"
    >
        <el-descriptions border :column="2">
            <el-descriptions-item label="设备名称">
                {{ detailData.taskName || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="规格型号">
                {{ detailData.deviceModel || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="录入人">
                {{ detailData.registrant || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="登记时间">
                {{ detailData.registrationDate || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="保养项目" :span="2">
                {{ detailData.machineryCategory || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="保养人">
                {{ detailData.maintenancePerson || '--' }}
            </el-descriptions-item>
            <el-descriptions-item label="任务频率">
                {{ formatFrequencyType(detailData.frequencyType) }}
            </el-descriptions-item>
            <el-descriptions-item label="执行时间">
                {{ formatFrequencyDetail(detailData.frequencyDetail) }}
            </el-descriptions-item>
            <el-descriptions-item label="定时任务">
                <el-tag :type="detailData.isActive === 1 ? 'success' : 'info'">
                    {{ detailData.isActive === 1 ? '开启' : '关闭' }}
                </el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="备注" :span="2">
                {{ detailData.remarks || '--' }}
            </el-descriptions-item>
        </el-descriptions>
  <FormDialog
    v-model="dialogVisitable"
    title="保养任务详情"
    width="860px"
    operation-type="detail"
    @close="cancel"
  >
    <el-descriptions border :column="2" label-width="100px">
      <el-descriptions-item label="设备名称">
        {{ detailData.taskName || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="规格型号">
        {{ detailData.deviceModel || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="录入人">
        {{ detailData.registrant || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="登记时间">
        {{ detailData.registrationDate || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="保养人">
        {{ detailData.maintenancePerson || "--" }}
      </el-descriptions-item>
      <el-descriptions-item label="任务频率">
        {{ formatFrequencyType(detailData.frequencyType) }}
      </el-descriptions-item>
      <el-descriptions-item label="执行时间">
        {{ formatFrequencyDetail(detailData.frequencyDetail) }}
      </el-descriptions-item>
      <el-descriptions-item label="定时任务">
        <el-tag :type="detailData.isActive === 1 ? 'success' : 'info'" size="small">
          {{ detailData.isActive === 1 ? "开启" : "关闭" }}
        </el-tag>
      </el-descriptions-item>
      <el-descriptions-item label="备注" :span="2">
        {{ detailData.remarks || "--" }}
      </el-descriptions-item>
    </el-descriptions>
        <div v-if="fileList && fileList.length > 0" class="image-section">
            <div class="image-title">设备图片:</div>
            <AttachmentUploadImage
                v-model:fileList="fileList"
                :disabled="true"
            />
        </div>
    </FormDialog>
    <div class="detail-section">
      <div class="section-title">
        <span>保养内容</span>
        <span class="section-count">共 {{ itemCount }} é¡¹</span>
      </div>
      <div v-if="itemCount" class="item-list">
        <div
          v-for="(item, index) in detailData.maintenanceItems"
          :key="item.id || index"
          class="item-row"
        >
          <span class="item-index">{{ index + 1 }}</span>
          <div class="item-body">
            <div class="item-project">{{ item.itemProject || "未填写保养项目" }}</div>
            <div class="item-content">{{ item.itemName }}</div>
          </div>
        </div>
      </div>
      <el-empty v-else :image-size="48" description="暂无保养内容" />
    </div>
    <div v-if="fileList && fileList.length > 0" class="image-section">
      <div class="image-title">设备图片:</div>
      <AttachmentUploadImage v-model:fileList="fileList" :disabled="true" />
    </div>
  </FormDialog>
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ref } from "vue";
import { computed, ref } from "vue";
import { getLedgerById } from "@/api/equipmentManagement/ledger";
import AttachmentUploadImage from '@/components/AttachmentUpload/image/index.vue';
import AttachmentUploadImage from "@/components/AttachmentUpload/image/index.vue";
const dialogVisitable = ref(false);
const detailData = ref({});
const fileList = ref([]);
const itemCount = computed(() => detailData.value.maintenanceItems?.length || 0);
const formatFrequencyType = (type) => {
    const map = {
        DAILY: "每日",
        WEEKLY: "每周",
        MONTHLY: "每月",
        QUARTERLY: "季度",
    };
    return map[type] || "--";
  const map = {
    DAILY: "每日",
    WEEKLY: "每周",
    MONTHLY: "每月",
    QUARTERLY: "季度",
  };
  return map[type] || "--";
};
const formatFrequencyDetail = (detail) => {
    if (!detail) return "--";
    const replacements = {
        MON: "周一",
        TUE: "周二",
        WED: "周三",
        THU: "周四",
        FRI: "周五",
        SAT: "周六",
        SUN: "周日",
    };
    return detail.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => replacements[match]);
  if (!detail) return "--";
  const replacements = {
    MON: "周一",
    TUE: "周二",
    WED: "周三",
    THU: "周四",
    FRI: "周五",
    SAT: "周六",
    SUN: "周日",
  };
  return detail.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (match) => replacements[match]);
};
const openDialog = (row) => {
    dialogVisitable.value = true;
    detailData.value = { ...row };
    fileList.value = [];
  dialogVisitable.value = true;
  detailData.value = { ...row };
  fileList.value = [];
    if (row.taskId) {
        getLedgerById(row.taskId).then(res => {
            if (res.code === 200 && res.data) {
                fileList.value = res.data.storageBlobVOs || [];
            }
        });
    }
  if (row.taskId) {
    getLedgerById(row.taskId).then((res) => {
      if (res.code === 200 && res.data) {
        fileList.value = res.data.storageBlobVOs || [];
      }
    });
  }
};
const cancel = () => {
    dialogVisitable.value = false;
  dialogVisitable.value = false;
};
defineExpose({ openDialog });
</script>
<style scoped>
.detail-section {
  margin-top: 16px;
}
.section-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  font-weight: 600;
  color: #303133;
}
.section-count {
  font-size: 12px;
  font-weight: normal;
  color: #909399;
}
.item-list {
  border: 1px solid #ebeef5;
  border-radius: 4px;
  overflow: hidden;
}
.item-row {
  display: flex;
  gap: 12px;
  padding: 12px 14px;
  border-bottom: 1px solid #ebeef5;
}
.item-row:last-child {
  border-bottom: none;
}
.item-index {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  line-height: 24px;
  text-align: center;
  border-radius: 50%;
  background: #ecf5ff;
  color: #409eff;
  font-size: 12px;
}
.item-body {
  flex: 1;
  min-width: 0;
}
.item-project {
  margin-bottom: 4px;
  font-weight: 600;
  color: #303133;
}
.item-content {
  line-height: 1.6;
  color: #606266;
  white-space: pre-wrap;
  word-break: break-all;
}
.image-section {
    margin-top: 20px;
  margin-top: 20px;
}
.image-title {
    font-weight: bold;
    margin-bottom: 10px;
    color: #606266;
  font-weight: bold;
  margin-bottom: 10px;
  color: #606266;
}
</style>
src/views/equipmentManagement/upkeep/Form/formDia.vue
@@ -1,349 +1,640 @@
<template>
    <FormDialog
        v-model="dialogVisitable"
        :title="operationType === 'add' ? '新增保养任务' : '编辑保养任务'"
        width="800px"
        :operation-type="operationType"
        @confirm="submitForm"
        @cancel="cancel"
        @close="cancel"
    >
        <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
            <el-row>
                <el-col :span="12">
                    <el-form-item label="设备名称" prop="taskId">
                        <el-select v-model="form.taskId" @change="setDeviceModel" filterable>
                            <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="录入人" prop="inspector">
                        <el-select
                            v-model="form.inspector"
                            filterable
                            default-first-option
                            :reserve-keyword="false"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option
                                v-for="item in userList"
                                :label="item.nickName"
                                :value="item.userId"
                                :key="item.userId"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="登记时间" prop="registrationDate">
                        <el-date-picker
                            v-model="form.registrationDate"
                            type="date"
                            placeholder="选择登记日期"
                            format="YYYY-MM-DD"
                            value-format="YYYY-MM-DD"
                            style="width: 100%"
                        />
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="12">
                    <el-form-item label="保养项目" prop="machineryCategory">
                        <el-input
                            v-model.trim="form.machineryCategory"
                            type="textarea"
                            placeholder="请输入保养项目"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="保养人" prop="maintenancePerson">
                        <el-input
                            v-model.trim="form.maintenancePerson"
                            placeholder="请输入保养人姓名"
                            maxlength="100"
                            clearable
                        />
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="12">
                    <el-form-item label="任务频率" prop="frequencyType">
                        <el-select v-model="form.frequencyType" placeholder="请选择" clearable>
                            <el-option label="每日" value="DAILY"/>
                            <el-option label="每周" value="WEEKLY"/>
                            <el-option label="每月" value="MONTHLY"/>
                            <el-option label="季度" value="QUARTERLY"/>
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12" v-if="form.frequencyType === 'DAILY' && form.frequencyType">
                    <el-form-item label="日期" prop="frequencyDetail">
                        <el-time-picker v-model="form.frequencyDetail" placeholder="选择时间" format="HH:mm"
                                                    value-format="HH:mm" />
                    </el-form-item>
                </el-col>
                <el-col :span="12" v-if="form.frequencyType === 'WEEKLY' && form.frequencyType">
                    <el-form-item label="日期" prop="frequencyDetail">
                        <el-select v-model="form.week" placeholder="请选择" clearable style="width: 50%">
                            <el-option label="周一" value="MON"/>
                            <el-option label="周二" value="TUE"/>
                            <el-option label="周三" value="WED"/>
                            <el-option label="周四" value="THU"/>
                            <el-option label="周五" value="FRI"/>
                            <el-option label="周六" value="SAT"/>
                            <el-option label="周日" value="SUN"/>
                        </el-select>
                        <el-time-picker v-model="form.time" placeholder="选择时间" format="HH:mm"
                                                    value-format="HH:mm"  style="width: 50%"/>
                    </el-form-item>
                </el-col>
                <el-col :span="12" v-if="form.frequencyType === 'MONTHLY' && form.frequencyType">
                    <el-form-item label="日期" prop="frequencyDetail">
                        <el-date-picker
                            v-model="form.frequencyDetail"
                            type="datetime"
                            clearable
                            placeholder="选择开始日期"
                            format="DD,HH:mm"
                            value-format="DD,HH:mm"
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12" v-if="form.frequencyType === 'QUARTERLY' && form.frequencyType">
                    <el-form-item label="日期" prop="frequencyDetail">
                        <el-date-picker
                            v-model="form.frequencyDetail"
                            type="datetime"
                            clearable
                            placeholder="选择开始日期"
                            format="MM,DD,HH:mm"
                            value-format="MM,DD,HH:mm"
                        />
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="24">
                    <el-form-item label="定时任务">
                        <el-switch
                            v-model="form.isActive"
                            :active-value="1"
                            :inactive-value="0"
                            active-text="开启"
                            inactive-text="关闭"
                            inline-prompt
                        />
                        <span style="margin-left: 12px; color: #909399; font-size: 12px;">
                            {{ form.isActive === 1 ? '任务将按计划执行' : '任务暂停执行' }}
                        </span>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="12">
                    <el-form-item label="备注" prop="remarks">
                        <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
    </FormDialog>
  <FormDialog
    v-model="dialogVisitable"
    :title="operationType === 'add' ? '新增保养任务' : '编辑保养任务'"
    width="800px"
    :operation-type="operationType"
    @confirm="submitForm"
    @cancel="cancel"
    @close="cancel"
  >
    <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
      <el-row>
        <el-col :span="12">
          <el-form-item label="设备名称" prop="taskId">
            <el-select v-model="form.taskId" @change="setDeviceModel" filterable>
              <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="录入人" prop="inspector">
            <el-select
              v-model="form.inspector"
              filterable
              default-first-option
              :reserve-keyword="false"
              placeholder="请选择"
              clearable
            >
              <el-option
                v-for="item in userList"
                :label="item.nickName"
                :value="item.userId"
                :key="item.userId"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="登记时间" prop="registrationDate">
            <el-date-picker
              v-model="form.registrationDate"
              type="date"
              placeholder="选择登记日期"
              format="YYYY-MM-DD"
              value-format="YYYY-MM-DD"
              style="width: 100%"
            />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="保养人" prop="maintenancePerson">
            <el-input
              v-model.trim="form.maintenancePerson"
              placeholder="请输入保养人姓名"
              maxlength="100"
              clearable
            />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="任务频率" prop="frequencyType">
            <el-select
              v-model="form.frequencyType"
              placeholder="请选择(日、周、月、季度)"
              clearable
              @change="onFrequencyChange"
            >
              <el-option label="每日" value="DAILY" />
              <el-option label="每周" value="WEEKLY" />
              <el-option label="每月" value="MONTHLY" />
              <el-option label="季度" value="QUARTERLY" />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12" v-if="form.frequencyType === 'DAILY' && form.frequencyType">
          <el-form-item label="日期" prop="frequencyDetail">
            <el-time-picker
              v-model="form.frequencyDetail"
              placeholder="选择时间"
              format="HH:mm"
              value-format="HH:mm"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12" v-if="form.frequencyType === 'WEEKLY' && form.frequencyType">
          <el-form-item label="日期" prop="frequencyDetail">
            <el-select v-model="form.week" placeholder="请选择" clearable style="width: 50%">
              <el-option label="周一" value="MON" />
              <el-option label="周二" value="TUE" />
              <el-option label="周三" value="WED" />
              <el-option label="周四" value="THU" />
              <el-option label="周五" value="FRI" />
              <el-option label="周六" value="SAT" />
              <el-option label="周日" value="SUN" />
            </el-select>
            <el-time-picker
              v-model="form.time"
              placeholder="选择时间"
              format="HH:mm"
              value-format="HH:mm"
              style="width: 50%"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12" v-if="form.frequencyType === 'MONTHLY' && form.frequencyType">
          <el-form-item label="日期" prop="frequencyDetail">
            <el-date-picker
              v-model="form.frequencyDetail"
              type="datetime"
              clearable
              placeholder="选择开始日期"
              format="DD,HH:mm"
              value-format="DD,HH:mm"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12" v-if="form.frequencyType === 'QUARTERLY' && form.frequencyType">
          <el-form-item label="日期" prop="frequencyDetail">
            <el-date-picker
              v-model="form.frequencyDetail"
              type="datetime"
              clearable
              placeholder="选择开始日期"
              format="MM,DD,HH:mm"
              value-format="MM,DD,HH:mm"
            />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="24">
          <el-form-item label="保养项目" prop="maintenanceItemIds">
            <div class="maintenance-selection">
              <div class="selection-toolbar">
                <el-button
                  type="primary"
                  plain
                  :disabled="!form.taskId || !form.frequencyType"
                  @click="itemSelectorVisible = true"
                  >选择保养项目</el-button
                >
                <span v-if="!form.taskId || !form.frequencyType" class="selection-hint"
                  >请先选择设备和任务频率,设备绑定的保养项目将自动带出</span
                >
                <span v-else class="selection-hint"
                  >设备绑定项目已自动带出,可额外选择通用项目,或添加本任务专用项目</span
                >
              </div>
              <div class="custom-input">
                <el-input
                  v-model="customItemProject"
                  maxlength="200"
                  clearable
                  placeholder="输入本任务专用的保养项目"
                />
                <el-input
                  v-model="customItemName"
                  maxlength="2000"
                  clearable
                  placeholder="输入项目对应的保养内容"
                  @keyup.enter="addCustomItem"
                />
                <el-button
                  :disabled="!customItemProject.trim() || !customItemName.trim()"
                  @click="addCustomItem"
                  >添加</el-button
                >
              </div>
              <div class="selected-panel">
                <div class="selected-header">
                  <span class="selected-title">已选 {{ selectedList.length }} é¡¹</span>
                  <span v-if="sourceStats.length" class="selected-stats">
                    <span v-for="stat in sourceStats" :key="stat.source" class="selected-stat"
                      >{{ stat.label }} {{ stat.count }}</span
                    >
                  </span>
                </div>
                <ol v-if="selectedList.length" class="selected-list">
                  <li v-for="(item, index) in selectedList" :key="item.key" class="selected-row">
                    <span class="row-index">{{ index + 1 }}</span>
                    <el-tag size="small" effect="light" :type="sourceMeta[item.source].type">{{
                      sourceMeta[item.source].label
                    }}</el-tag>
                    <span class="row-text" :title="item.itemProject">{{ item.itemProject }}</span>
                    <el-button link type="danger" @click="removeSelectedItem(item)">移除</el-button>
                  </li>
                </ol>
                <el-empty v-else :image-size="56" description="尚未选择保养项目" />
              </div>
            </div>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="24">
          <el-form-item label="定时任务">
            <el-switch
              v-model="form.isActive"
              :active-value="1"
              :inactive-value="0"
              active-text="开启"
              inactive-text="关闭"
              inline-prompt
            />
            <span style="margin-left: 12px; color: #909399; font-size: 12px">
              {{ form.isActive === 1 ? "任务将按计划执行" : "任务暂停执行" }}
            </span>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="备注" prop="remarks">
            <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  </FormDialog>
  <MaintenanceItemSelector
    v-model:visible="itemSelectorVisible"
    :selected-items="selectedItems"
    :device-ledger-id="form.taskId"
    :frequency-type="form.frequencyType"
    @confirm="applyMaintenanceItems"
  />
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { reactive, ref, getCurrentInstance, toRefs } from "vue";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { computed, reactive, ref, getCurrentInstance, toRefs } from "vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import { deviceMaintenanceTaskAdd, deviceMaintenanceTaskEdit } from "@/api/equipmentManagement/upkeep";
import {
  deviceMaintenanceTaskAdd,
  deviceMaintenanceTaskEdit,
  getMaintenanceItemList,
} from "@/api/equipmentManagement/upkeep";
import { getCurrentDate } from "@/utils/index.js";
import useUserStore from "@/store/modules/user.js";
import MaintenanceItemSelector from "./MaintenanceItemSelector.vue";
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const dialogVisitable = ref(false);
const operationType = ref('add');
const operationType = ref("add");
const deviceOptions = ref([]);
const itemSelectorVisible = ref(false);
const userStore = useUserStore();
const data = reactive({
    form: {
        taskId: undefined,
        taskName: undefined,
        // å½•入人:单选一个用户 id
        inspector: undefined,
        machineryCategory: "",
        remarks: '',
        frequencyType: '',
        frequencyDetail: '',
        week: '',
        time: '',
        deviceModel: undefined, // è§„格型号
        registrationDate: '',
        maintenancePerson: '', // ä¿å…»äºº
        isActive: 0 // å®šæ—¶ä»»åŠ¡å¼€å…³ï¼š0=关闭,1=开启
    },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请选择录入人", trigger: "blur" },],
        registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }],
        machineryCategory: [{ required: true, message: "请输入保养项目", trigger: "blur" }]
    }
})
const { form, rules } = toRefs(data)
const userList = ref([])
  form: {
    taskId: undefined,
    taskName: undefined,
    // å½•入人:单选一个用户 id
    inspector: undefined,
    maintenanceItemIds: [],
    remarks: "",
    frequencyType: "",
    frequencyDetail: "",
    week: "",
    time: "",
    deviceModel: undefined, // è§„格型号
    registrationDate: "",
    maintenancePerson: "", // ä¿å…»äºº
    isActive: 0, // å®šæ—¶ä»»åŠ¡å¼€å…³ï¼š0=关闭,1=开启
  },
  rules: {
    taskId: [{ required: true, message: "请选择设备", trigger: "change" }],
    inspector: [{ required: true, message: "请选择录入人", trigger: "blur" }],
    registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }],
    maintenanceItemIds: [
      { type: "array", required: true, min: 1, message: "请勾选保养项目", trigger: "change" },
    ],
    frequencyType: [{ required: true, message: "请选择任务频率", trigger: "change" }],
    frequencyDetail: [
      {
        validator: (_, value, callback) => {
          const valid =
            form.value.frequencyType === "WEEKLY"
              ? Boolean(form.value.week && form.value.time)
              : Boolean(value);
          callback(valid ? undefined : new Error("请选择执行日期和时间"));
        },
        trigger: "change",
      },
    ],
  },
});
const { form, rules } = toRefs(data);
const userList = ref([]);
// ä»Žä¿å…»å†…容清单中选中的项(设备绑定或通用)
const selectedItems = ref([]);
// ä»…属于本任务的自定义内容,不写入保养内容清单
const customItems = ref([]);
const customItemProject = ref("");
const customItemName = ref("");
const sourceMeta = {
  device: { label: "本设备", type: "primary" },
  common: { label: "通用", type: "info" },
  custom: { label: "自定义", type: "warning" },
  other: { label: "其它设备", type: "danger" },
  saved: { label: "已保存", type: "success" },
};
const getSource = (item) => {
  if (item.deviceLedgerId === undefined) return "saved";
  if (item.deviceLedgerId === null) return "common";
  return String(item.deviceLedgerId) === String(form.value.taskId) ? "device" : "other";
};
const selectedList = computed(() => [
  ...selectedItems.value.map((item) => ({
    key: `item-${item.id}`,
    id: item.id,
    itemProject: item.itemProject,
    source: getSource(item),
  })),
  ...customItems.value.map((item) => ({
    key: item.key,
    id: null,
    itemProject: item.itemProject,
    itemName: item.itemName,
    source: "custom",
  })),
]);
const sourceStats = computed(() =>
  Object.keys(sourceMeta)
    .map((source) => ({
      source,
      label: sourceMeta[source].label,
      count: selectedList.value.filter((item) => item.source === source).length,
    }))
    .filter((stat) => stat.count > 0)
);
const syncItemIds = () => {
  form.value.maintenanceItemIds = [
    ...selectedItems.value.map((item) => item.id),
    ...customItems.value.map((item) => item.key),
  ];
  proxy.$refs.formRef?.clearValidate("maintenanceItemIds");
};
const applyMaintenanceItems = (items) => {
  selectedItems.value = items;
  syncItemIds();
};
const addCustomItem = () => {
  const project = customItemProject.value.trim();
  const name = customItemName.value.trim();
  if (!project || !name) return;
  if (selectedList.value.some((item) => item.itemProject === project)) {
    proxy.$modal.msgWarning("已存在相同的保养项目");
    return;
  }
  customItems.value = [
    ...customItems.value,
    { key: `custom-${Date.now()}`, itemProject: project, itemName: name },
  ];
  customItemProject.value = "";
  customItemName.value = "";
  syncItemIds();
};
const removeSelectedItem = (item) => {
  if (item.source === "custom")
    customItems.value = customItems.value.filter((custom) => custom.key !== item.key);
  else
    selectedItems.value = selectedItems.value.filter(
      (selected) => String(selected.id) !== String(item.id)
    );
  syncItemIds();
};
const loadDeviceName = async () => {
    const { data } = await getDeviceLedger();
    deviceOptions.value = data;
  const { data } = await getDeviceLedger();
  deviceOptions.value = data;
};
// é€‰æ‹©è®¾å¤‡æ—¶ï¼Œå›žå¡«è®¾å¤‡åç§°(taskName)和规格型号(deviceModel)
const setDeviceModel = (id) => {
    const option = deviceOptions.value.find((item) => item.id === id);
    if (option) {
        form.value.taskId = option.id;
        form.value.taskName = option.deviceName;
        form.value.deviceModel = option.deviceModel;
    }
}
const setDeviceModel = (id, preserveItems = false) => {
  const option = deviceOptions.value.find((item) => item.id === id);
  if (option) {
    form.value.taskId = option.id;
    form.value.taskName = option.deviceName;
    form.value.deviceModel = option.deviceModel;
    if (!preserveItems) autoFillBoundItems(id);
  }
};
const clearSelectedItems = () => {
  selectedItems.value = [];
  syncItemIds();
};
const onFrequencyChange = () => autoFillBoundItems();
// æŒ‰è®¾å¤‡å’Œé¢‘率自动带出该设备绑定的保养内容;通用内容(无设备)需用户手动额外选择
const autoFillBoundItems = async (deviceId) => {
  const id = deviceId ?? form.value.taskId;
  const frequency = form.value.frequencyType;
  if (!id || !frequency) return clearSelectedItems();
  const { code, data } = await getMaintenanceItemList({
    deviceLedgerId: id,
    frequencyType: frequency,
    isActive: 1,
  });
  if (code !== 200 || id !== form.value.taskId || frequency !== form.value.frequencyType) return;
  applyMaintenanceItems(data || []);
};
// æ‰“开弹框
const openDialog = async (type, row) => {
    dialogVisitable.value = true
    operationType.value = type
    // é‡ç½®è¡¨å•
    resetForm();
    // åŠ è½½ç”¨æˆ·åˆ—è¡¨
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    // åŠ è½½è®¾å¤‡åˆ—è¡¨
    await loadDeviceName();
    if (type === 'edit' && row) {
        form.value = { ...row }
        // ç¼–辑时用接口返回的 registrantId å›žæ˜¾å½•入人
        if (row.registrantId) {
            form.value.inspector = row.registrantId
        }
  dialogVisitable.value = true;
  operationType.value = type;
        // å¦‚果有设备ID,自动设置设备信息
        if (form.value.taskId) {
            setDeviceModel(form.value.taskId);
        }
    } else if (type === 'add') {
        // æ–°å¢žæ—¶è®¾ç½®ç™»è®°æ—¥æœŸä¸ºå½“天
        form.value.registrationDate = getCurrentDate();
        // æ–°å¢žæ—¶è®¾ç½®å½•入人为当前登录账户
        form.value.inspector = userStore.id;
    }
}
  // é‡ç½®è¡¨å•
  resetForm();
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  userListNoPageByTenantId().then((res) => {
    userList.value = res.data;
  });
  // åŠ è½½è®¾å¤‡åˆ—è¡¨
  await loadDeviceName();
  if (type === "edit" && row) {
    form.value = { ...row };
    const savedItems = row.maintenanceItems || [];
    selectedItems.value = savedItems
      .filter((item) => item.maintenanceItemId != null)
      .map((item) => ({
        id: item.maintenanceItemId,
        itemProject: item.itemProject,
        itemName: item.itemName,
      }));
    customItems.value = savedItems
      .filter((item) => item.maintenanceItemId == null)
      .map((item, index) => ({
        key: `custom-saved-${index}`,
        itemProject: item.itemProject,
        itemName: item.itemName,
      }));
    syncItemIds();
    // ç¼–辑时用接口返回的 registrantId å›žæ˜¾å½•入人
    if (row.registrantId) {
      form.value.inspector = row.registrantId;
    }
    // å¦‚果有设备ID,自动设置设备信息
    if (form.value.taskId) {
      setDeviceModel(form.value.taskId, true);
    }
  } else if (type === "add") {
    // æ–°å¢žæ—¶è®¾ç½®ç™»è®°æ—¥æœŸä¸ºå½“天
    form.value.registrationDate = getCurrentDate();
    // æ–°å¢žæ—¶è®¾ç½®å½•入人为当前登录账户
    form.value.inspector = userStore.id;
  }
};
// å…³é—­å¯¹è¯æ¡†
const cancel = () => {
    resetForm()
    dialogVisitable.value = false
    emit('closeDia')
}
  itemSelectorVisible.value = false;
  resetForm();
  dialogVisitable.value = false;
  emit("closeDia");
};
// é‡ç½®è¡¨å•函数
const resetForm = () => {
    if (proxy.$refs.formRef) {
        proxy.$refs.formRef.resetFields()
    }
    // é‡ç½®è¡¨å•数据确保设备信息正确重置
    form.value = {
        taskId: undefined,
        taskName: undefined,
        inspector: undefined,
        machineryCategory: "",
        remarks: '',
        frequencyType: '',
        frequencyDetail: '',
        week: '',
        time: '',
        deviceModel: undefined,
        registrationDate: '',
        maintenancePerson: '',
        isActive: 0
    }
}
  itemSelectorVisible.value = false;
  selectedItems.value = [];
  customItems.value = [];
  customItemProject.value = "";
  customItemName.value = "";
  if (proxy.$refs.formRef) {
    proxy.$refs.formRef.resetFields();
  }
  // é‡ç½®è¡¨å•数据确保设备信息正确重置
  form.value = {
    taskId: undefined,
    taskName: undefined,
    inspector: undefined,
    maintenanceItemIds: [],
    remarks: "",
    frequencyType: "",
    frequencyDetail: "",
    week: "",
    time: "",
    deviceModel: undefined,
    registrationDate: "",
    maintenancePerson: "",
    isActive: 0,
  };
};
// æäº¤è¡¨å•
const submitForm = () => {
    proxy.$refs["formRef"].validate(async valid => {
        if (valid) {
            try {
                const payload = { ...form.value }
                // ä¸å†å‘后端传保养人字段,仅使用接口要求的 registrant / registrantId
                // æ ¹æ®é€‰æ‹©çš„"录入人"设置 registrant / registrantId
                if (payload.inspector) {
                    const selectedUser = userList.value.find(
                        (u) => String(u.userId) === String(payload.inspector)
                    )
                    if (selectedUser) {
                        payload.registrantId = selectedUser.userId
                        payload.registrant = selectedUser.nickName
                    }
                }
                delete payload.inspector
                delete payload.inspectorIds
                delete payload.active
  proxy.$refs["formRef"].validate(async (valid) => {
    if (valid) {
      try {
        const payload = { ...form.value };
        payload.maintenanceItemIds = selectedItems.value.map((item) => item.id);
        payload.customItems = customItems.value.map((item) => ({
          itemProject: item.itemProject,
          itemName: item.itemName,
        }));
        // ä¸å†å‘后端传保养人字段,仅使用接口要求的 registrant / registrantId
        // æ ¹æ®é€‰æ‹©çš„"录入人"设置 registrant / registrantId
        if (payload.inspector) {
          const selectedUser = userList.value.find(
            (u) => String(u.userId) === String(payload.inspector)
          );
          if (selectedUser) {
            payload.registrantId = selectedUser.userId;
            payload.registrant = selectedUser.nickName;
          }
        }
        delete payload.inspector;
        delete payload.inspectorIds;
        delete payload.active;
                if (payload.frequencyType === 'WEEKLY') {
                    let frequencyDetail = ''
                    frequencyDetail = payload.week + ',' + payload.time
                    payload.frequencyDetail = frequencyDetail
                }
        if (payload.frequencyType === "WEEKLY") {
          let frequencyDetail = "";
          frequencyDetail = payload.week + "," + payload.time;
          payload.frequencyDetail = frequencyDetail;
        }
                // å½•入日期:直接使用表单里的 registrationDate å­—段
                // ä¸€äº›é»˜è®¤çŠ¶æ€å­—æ®µ
                if (payload.status === undefined || payload.status === null || payload.status === '') {
                    payload.status = '0' // é»˜è®¤çŠ¶æ€ï¼Œå¯æŒ‰å®žé™…æžšä¸¾è°ƒæ•´
                }
                payload.deleted = 0
        // å½•入日期:直接使用表单里的 registrationDate å­—段
        // ä¸€äº›é»˜è®¤çŠ¶æ€å­—æ®µ
        if (payload.status === undefined || payload.status === null || payload.status === "") {
          payload.status = "0"; // é»˜è®¤çŠ¶æ€ï¼Œå¯æŒ‰å®žé™…æžšä¸¾è°ƒæ•´
        }
        payload.deleted = 0;
                if (operationType.value === 'edit') {
                    await deviceMaintenanceTaskEdit(payload)
                } else {
                    await deviceMaintenanceTaskAdd(payload)
                }
                cancel()
                proxy.$modal.msgSuccess('提交成功')
            } catch (error) {
                proxy.$modal.msgError('提交失败,请重试')
            }
        }
    })
}
defineExpose({ openDialog })
        if (operationType.value === "edit") {
          await deviceMaintenanceTaskEdit(payload);
        } else {
          await deviceMaintenanceTaskAdd(payload);
        }
        cancel();
        proxy.$modal.msgSuccess("提交成功");
      } catch (error) {
        proxy.$modal.msgError("提交失败,请重试");
      }
    }
  });
};
defineExpose({ openDialog });
</script>
<style scoped>
.maintenance-selection {
  width: 100%;
}
.selection-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
}
.selection-hint {
  color: #909399;
  font-size: 12px;
}
.custom-input {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}
.custom-input .el-input:first-child {
  flex: 0 0 30%;
}
.custom-input .el-input:nth-child(2) {
  flex: 1;
  min-width: 0;
}
.selected-panel {
  margin-top: 10px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background: #fafafa;
}
.selected-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  border-bottom: 1px solid #ebeef5;
}
.selected-title {
  font-weight: 600;
  color: #303133;
}
.selected-stats {
  display: flex;
  gap: 12px;
  color: #909399;
  font-size: 12px;
}
.selected-list {
  max-height: 220px;
  margin: 0;
  padding: 4px 0;
  overflow-y: auto;
  list-style: none;
}
.selected-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 12px;
}
.selected-row:hover {
  background: #f0f7ff;
}
.row-index {
  width: 24px;
  flex-shrink: 0;
  color: #909399;
  text-align: right;
}
.row-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: #303133;
}
</style>
src/views/equipmentManagement/upkeep/index.vue
@@ -1,192 +1,173 @@
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab"
             @tab-change="handleTabChange">
    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
      <!-- ä¿å…»ä»»åŠ¡tab -->
      <el-tab-pane label="保养任务"
                   name="scheduled">
      <el-tab-pane label="保养任务" name="scheduled">
        <div class="search_form">
          <el-form :model="scheduledFilters"
                   :inline="true">
          <el-form :model="scheduledFilters" :inline="true">
            <el-form-item label="任务名称">
              <el-input v-model="scheduledFilters.taskName"
                        style="width: 240px"
                        placeholder="请输入任务名称"
                        clearable
                        :prefix-icon="Search"
                        @change="getScheduledTableData" />
              <el-input
                v-model="scheduledFilters.taskName"
                style="width: 240px"
                placeholder="请输入任务名称"
                clearable
                :prefix-icon="Search"
                @change="getScheduledTableData"
              />
            </el-form-item>
            <el-form-item label="任务状态">
              <el-select v-model="scheduledFilters.status"
                         placeholder="请选择任务状态"
                         clearable
                         style="width: 200px">
                <el-option label="启用"
                           value="1" />
                <el-option label="停用"
                           value="0" />
              <el-select
                v-model="scheduledFilters.status"
                placeholder="请选择任务状态"
                clearable
                style="width: 200px"
              >
                <el-option label="启用" value="1" />
                <el-option label="停用" value="0" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary"
                         @click="getScheduledTableData">搜索</el-button>
              <el-button type="primary" @click="getScheduledTableData">搜索</el-button>
              <el-button @click="resetScheduledFilters">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1"
                     size="large">保养任务</el-text>
            <el-text class="mx-1" size="large">保养任务</el-text>
            <div>
              <el-button type="primary"
                         icon="Plus"
                         @click="addScheduledTask">
                æ–°å¢žä»»åŠ¡
              </el-button>
              <el-button type="danger"
                         icon="Delete"
                         :disabled="scheduledMultipleList.length <= 0"
                         @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))">
              <el-button type="primary" icon="Plus" @click="addScheduledTask"> æ–°å¢žä»»åŠ¡ </el-button>
              <el-button
                type="danger"
                icon="Delete"
                :disabled="scheduledMultipleList.length <= 0"
                @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))"
              >
                æ‰¹é‡åˆ é™¤
              </el-button>
            </div>
          </div>
          <PIMTable rowKey="id"
                    isSelection
                    :column="scheduledColumns"
                    :tableData="scheduledDataList"
                    :page="{
          <PIMTable
            rowKey="id"
            isSelection
            :column="scheduledColumns"
            :tableData="scheduledDataList"
            :page="{
              current: scheduledPagination.currentPage,
              size: scheduledPagination.pageSize,
              total: scheduledPagination.total,
            }"
                    @selection-change="handleScheduledSelectionChange"
                    @pagination="changeScheduledPage">
            @selection-change="handleScheduledSelectionChange"
            @pagination="changeScheduledPage"
          >
            <template #isActiveRef="{ row }">
              <el-tag v-if="row.isActive === 1"
                      type="success">开启</el-tag>
              <el-tag v-else
                      type="info">关闭</el-tag>
              <el-tag v-if="row.isActive === 1" type="success">开启</el-tag>
              <el-tag v-else type="info">关闭</el-tag>
            </template>
            <template #statusRef="{ row }">
              <el-tag v-if="row.status === 1"
                      type="success">启用</el-tag>
              <el-tag v-if="row.status === 0"
                      type="danger">停用</el-tag>
              <el-tag v-if="row.status === 1" type="success">启用</el-tag>
              <el-tag v-if="row.status === 0" type="danger">停用</el-tag>
            </template>
            <template #operation="{ row }">
              <el-button type="primary"
                         link
                         @click="handleDetail(row)">
                è¯¦æƒ…
              </el-button>
              <el-button type="primary"
                         link
                         @click="editScheduledTask(row)">
                ç¼–辑
              </el-button>
              <el-button type="danger"
                         link
                         @click="delScheduledTaskByIds(row.id)">
              <el-button type="primary" link @click="handleDetail(row)"> è¯¦æƒ… </el-button>
              <el-button type="primary" link @click="editScheduledTask(row)"> ç¼–辑 </el-button>
              <el-button type="danger" link @click="delScheduledTaskByIds(row.id)">
                åˆ é™¤
              </el-button>
            </template>
          </PIMTable>
        </div>
      </el-tab-pane>
      <el-tab-pane label="保养内容" name="items">
        <ItemManager />
      </el-tab-pane>
      <!-- ä¿å…»è®°å½•tab(原设备保养页面) -->
      <el-tab-pane label="保养记录"
                   name="record">
      <el-tab-pane label="保养记录" name="record">
        <div class="search_form">
          <el-form :model="filters"
                   :inline="true">
          <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-input
                v-model="filters.deviceName"
                style="width: 240px"
                placeholder="请输入设备名称"
                clearable
                :prefix-icon="Search"
                @change="getTableData"
              />
            </el-form-item>
            <el-form-item label="计划保养日期">
              <el-date-picker v-model="filters.maintenancePlanTime"
                              type="date"
                              placeholder="请选择计划保养日期"
                              size="default"
                              @change="(date) => handleDateChange(date,2)" />
              <el-date-picker
                v-model="filters.maintenancePlanTime"
                type="date"
                placeholder="请选择计划保养日期"
                size="default"
                @change="(date) => handleDateChange(date, 2)"
              />
            </el-form-item>
            <el-form-item label="实际保养日期">
              <el-date-picker v-model="filters.maintenanceActuallyTime"
                              type="date"
                              placeholder="请选择实际保养日期"
                              size="default"
                              @change="(date) => handleDateChange(date,1)" />
              <el-date-picker
                v-model="filters.maintenanceActuallyTime"
                type="date"
                placeholder="请选择实际保养日期"
                size="default"
                @change="(date) => handleDateChange(date, 1)"
              />
            </el-form-item>
            <el-form-item label="实际保养人">
              <el-input v-model="filters.maintenanceActuallyName"
                        style="width: 240px"
                        placeholder="请输入实际保养人"
                        clearable
                        :prefix-icon="Search"
                        @change="getTableData" />
              <el-input
                v-model="filters.maintenanceActuallyName"
                style="width: 240px"
                placeholder="请输入实际保养人"
                clearable
                :prefix-icon="Search"
                @change="getTableData"
              />
            </el-form-item>
            <el-form-item>
              <el-button type="primary"
                         @click="getTableData">搜索</el-button>
              <el-button type="primary" @click="getTableData">搜索</el-button>
              <el-button @click="resetFilters">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1"
                     size="large">保养记录</el-text>
            <el-text class="mx-1" size="large">保养记录</el-text>
            <div>
              <el-button type="success"
                         icon="Van"
                         @click="addPlan">
                æ–°å¢žè®¡åˆ’
              </el-button>
              <el-button @click="handleOut">
                å¯¼å‡º
              </el-button>
              <el-button type="danger"
                         icon="Delete"
                         :disabled="multipleList.length <= 0 || hasFinishedStatus"
                         @click="delRepairByIds(multipleList.map((item) => item.id))">
              <el-button type="success" icon="Van" @click="addPlan"> æ–°å¢žè®¡åˆ’ </el-button>
              <el-button @click="handleOut"> å¯¼å‡º </el-button>
              <el-button
                type="danger"
                icon="Delete"
                :disabled="multipleList.length <= 0 || hasFinishedStatus"
                @click="delRepairByIds(multipleList.map((item) => item.id))"
              >
                æ‰¹é‡åˆ é™¤
              </el-button>
            </div>
          </div>
          <PIMTable rowKey="id"
                    isSelection
                    :column="columns"
                    :tableData="dataList"
                    :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
                    @selection-change="handleSelectionChange"
                    @pagination="changePage">
          <PIMTable
            rowKey="id"
            isSelection
            :column="columns"
            :tableData="dataList"
            :page="{
              current: pagination.currentPage,
              size: pagination.pageSize,
              total: pagination.total,
            }"
            @selection-change="handleSelectionChange"
            @pagination="changePage"
          >
            <template #maintenanceResultRef="{ row }">
              <div>{{ row.maintenanceResult || '-' }}</div>
              <div>{{ row.maintenanceResult || "-" }}</div>
            </template>
            <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-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>
            </template>
            <template #operation="{ row }">
              <el-button type="primary"
                         link
                         @click="handleRecordDetail(row)">
                è¯¦æƒ…
              </el-button>
              <el-button type="primary" link @click="handleRecordDetail(row)"> è¯¦æƒ… </el-button>
              <!-- è¿™ä¸ªåŠŸèƒ½è·Ÿæ–°å¢žä¿å…»åŠŸèƒ½ä¸€æ¨¡ä¸€æ ·ï¼Œæœ‰å•¥æ„ä¹‰ï¼Ÿ -->
              <!-- <el-button
              type="primary"
@@ -195,509 +176,475 @@
          >
            æ–°å¢žä¿å…»
          </el-button> -->
              <el-button type="primary"
                         link
                         :disabled="row.status === 1"
                         @click="editPlan(row.id)">
              <el-button type="primary" link :disabled="row.status === 1" @click="editPlan(row.id)">
                ç¼–辑
              </el-button>
              <el-button type="success"
                         link
                         :disabled="row.status === 1"
                         @click="addMaintain(row)">
              <el-button type="success" link :disabled="row.status === 1" @click="addMaintain(row)">
                ä¿å…»
              </el-button>
              <el-button type="danger"
                         link
                         :disabled="row.status === 1"
                         @click="delRepairByIds(row.id)">
              <el-button
                type="danger"
                link
                :disabled="row.status === 1"
                @click="delRepairByIds(row.id)"
              >
                åˆ é™¤
              </el-button>
              <el-button type="primary"
                         link
                         @click="openFileDialog(row)">
                é™„ä»¶
              </el-button>
              <el-button type="primary" link @click="openFileDialog(row)"> é™„ä»¶ </el-button>
            </template>
          </PIMTable>
        </div>
      </el-tab-pane>
    </el-tabs>
    <PlanModal ref="planModalRef"
               @ok="getTableData" />
    <MaintenanceModal ref="maintainModalRef"
                      @ok="getTableData" />
    <FormDia ref="formDiaRef"
             @closeDia="getScheduledTableData" />
    <PlanModal ref="planModalRef" @ok="getTableData" />
    <MaintenanceModal ref="maintainModalRef" @ok="getTableData" />
    <FormDia ref="formDiaRef" @closeDia="getScheduledTableData" />
    <DetailDia ref="detailDiaRef" />
    <RecordDetailDia ref="recordDetailDiaRef" />
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              :record-type="'device_maintenance'"
              :record-id="currentMaintenanceTaskId" />
    <FileList
      v-if="fileDialogVisible"
      v-model:visible="fileDialogVisible"
      :record-type="'device_maintenance'"
      :record-id="currentMaintenanceTaskId"
    />
  </div>
</template>
<script setup>
  import {
    ref,
    onMounted,
    reactive,
    getCurrentInstance,
    nextTick,
    computed,
    defineAsyncComponent,
  } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import PlanModal from "./Form/PlanModal.vue";
  import MaintenanceModal from "./Form/MaintenanceModal.vue";
  import FormDia from "./Form/formDia.vue";
  import DetailDia from "./Form/detailDia.vue";
  import RecordDetailDia from "./Form/RecordDetailDia.vue";
  import {
    getUpkeepPage,
    delUpkeep,
    deviceMaintenanceTaskList,
    deviceMaintenanceTaskDel,
  } from "@/api/equipmentManagement/upkeep";
  import dayjs from "dayjs";
import {
  ref,
  onMounted,
  reactive,
  getCurrentInstance,
  nextTick,
  computed,
  defineAsyncComponent,
} from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PlanModal from "./Form/PlanModal.vue";
import MaintenanceModal from "./Form/MaintenanceModal.vue";
import FormDia from "./Form/formDia.vue";
import DetailDia from "./Form/detailDia.vue";
import RecordDetailDia from "./Form/RecordDetailDia.vue";
import ItemManager from "./Form/ItemManager.vue";
import {
  getUpkeepPage,
  delUpkeep,
  deviceMaintenanceTaskList,
  deviceMaintenanceTaskDel,
} from "@/api/equipmentManagement/upkeep";
import dayjs from "dayjs";
  const { proxy } = getCurrentInstance();
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
const { proxy } = getCurrentInstance();
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
  // Tab相关
  const activeTab = ref("scheduled");
// Tab相关
const activeTab = ref("scheduled");
  // è®¡åˆ’弹窗控制器
  const planModalRef = ref();
  // ä¿å…»å¼¹çª—控制器
  const maintainModalRef = ref();
  // å®šæ—¶ä»»åŠ¡å¼¹çª—æŽ§åˆ¶å™¨
  const formDiaRef = ref();
  // å®šæ—¶ä»»åŠ¡è¯¦æƒ…å¼¹çª—æŽ§åˆ¶å™¨
  const detailDiaRef = ref();
  // ä¿å…»è®°å½•详情弹窗控制器
  const recordDetailDiaRef = ref();
  // é™„件弹窗
  const fileListDialogRef = ref(null);
  const fileDialogVisible = ref(false);
  const currentMaintenanceTaskId = ref(null);
// è®¡åˆ’弹窗控制器
const planModalRef = ref();
// ä¿å…»å¼¹çª—控制器
const maintainModalRef = ref();
// å®šæ—¶ä»»åŠ¡å¼¹çª—æŽ§åˆ¶å™¨
const formDiaRef = ref();
// å®šæ—¶ä»»åŠ¡è¯¦æƒ…å¼¹çª—æŽ§åˆ¶å™¨
const detailDiaRef = ref();
// ä¿å…»è®°å½•详情弹窗控制器
const recordDetailDiaRef = ref();
// é™„件弹窗
const fileListDialogRef = ref(null);
const fileDialogVisible = ref(false);
const currentMaintenanceTaskId = ref(null);
  // ä¿å…»è®°å½•tab(原设备保养页面)相关变量
  const filters = reactive({
    deviceName: "",
    maintenancePlanTime: "",
    maintenanceActuallyTime: "",
    maintenanceActuallyName: "",
  });
// ä¿å…»è®°å½•tab(原设备保养页面)相关变量
const filters = reactive({
  deviceName: "",
  maintenancePlanTime: "",
  maintenanceActuallyTime: "",
  maintenanceActuallyName: "",
});
  const dataList = ref([]);
  const pagination = ref({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const multipleList = ref([]);
const dataList = ref([]);
const pagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const multipleList = ref([]);
  // ä¿å…»ä»»åŠ¡tab相关变量
  const scheduledFilters = reactive({
    taskName: "",
    status: "",
  });
// ä¿å…»ä»»åŠ¡tab相关变量
const scheduledFilters = reactive({
  taskName: "",
  status: "",
});
  const scheduledDataList = ref([]);
  const scheduledPagination = reactive({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const scheduledMultipleList = ref([]);
const scheduledDataList = ref([]);
const scheduledPagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const scheduledMultipleList = ref([]);
  // ä¿å…»ä»»åŠ¡è¡¨æ ¼åˆ—é…ç½®
  const scheduledColumns = ref([
    { prop: "taskName", label: "设备名称" },
    {
      label: "规格型号",
      prop: "deviceModel",
    },
    {
      label: "保养项目",
      prop: "machineryCategory",
      minWidth: 120,
      formatData: cell => cell || "--",
    },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 80,
      // PIMTable ä½¿ç”¨çš„æ˜¯ formatData,而不是 Element-Plus çš„ formatter
      formatData: cell =>
        ({
          DAILY: "每日",
          WEEKLY: "每周",
          MONTHLY: "每月",
          QUARTERLY: "季度",
        }[cell] || ""),
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 130,
      // åŒæ ·æ”¹ç”¨ formatData,PIMTable å†…部会把单元格值传进来
      formatData: cell => {
        if (typeof cell !== "string") return "";
        let val = cell;
        const replacements = {
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
        };
        // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      },
    },
    {
      prop: "isActive",
      label: "定时任务",
      dataType: "slot",
      slot: "isActiveRef",
      align: "center",
      minWidth: 100,
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    {
      prop: "registrationDate",
      label: "登记日期",
      minWidth: 100,
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD") : "-",
    },
    {
      fixed: "right",
      label: "操作",
      dataType: "slot",
      slot: "operation",
      align: "center",
      width: "160px",
    },
  ]);
  // ä¿å…»è®°å½•表格列配置(原设备保养表格列)
  const columns = ref([
    {
      label: "设备名称",
      align: "center",
      prop: "deviceName",
    },
    {
      label: "规格型号",
      align: "center",
      prop: "deviceModel",
    },
    {
      label: "计划保养日期",
      align: "center",
      prop: "maintenancePlanTime",
      formatData: cell => {
        return cell == null ? "-" : dayjs(cell).format("YYYY-MM-DD");
      },
    },
    {
      label: "录入人",
      align: "center",
      prop: "createUserName",
    },
    {
      label: "保养项目",
      align: "center",
      prop: "machineryCategory",
      formatData: cell => cell || "--",
    },
    // {
    //   label: "录入日期",
    //   align: "center",
    //   prop: "createTime",
    //   formatData: (cell) => dayjs(cell).format("YYYY-MM-DD HH:mm:ss"),
    //   width: 200,
    // },
    {
      label: "实际保养人",
      align: "center",
      prop: "maintenanceActuallyName",
    },
    {
      label: "实际保养日期",
      align: "center",
      prop: "maintenanceActuallyTime",
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD") : "-",
    },
    {
      label: "保养结果",
      align: "center",
      prop: "maintenanceResult",
      dataType: "slot",
      slot: "maintenanceResultRef",
    },
    {
      label: "状态",
      align: "center",
      prop: "status",
      dataType: "slot",
      slot: "statusRef",
    },
    {
      fixed: "right",
      label: "操作",
      dataType: "slot",
      slot: "operation",
      align: "center",
      width: "250px",
    },
  ]);
  // Tab切换处理
  const handleTabChange = tabName => {
    if (tabName === "record") {
      getTableData();
    } else if (tabName === "scheduled") {
      getScheduledTableData();
    }
  };
  // ä¿å…»ä»»åŠ¡ç›¸å…³æ–¹æ³•
  const getScheduledTableData = async () => {
    try {
      const params = {
        current: scheduledPagination.currentPage,
        size: scheduledPagination.pageSize,
        taskName: scheduledFilters.taskName || undefined,
        status: scheduledFilters.status || undefined,
// ä¿å…»ä»»åŠ¡è¡¨æ ¼åˆ—é…ç½®
const scheduledColumns = ref([
  { prop: "taskName", label: "设备名称" },
  {
    label: "规格型号",
    prop: "deviceModel",
  },
  {
    prop: "frequencyType",
    label: "频次",
    minWidth: 80,
    // PIMTable ä½¿ç”¨çš„æ˜¯ formatData,而不是 Element-Plus çš„ formatter
    formatData: (cell) =>
      ({
        DAILY: "每日",
        WEEKLY: "每周",
        MONTHLY: "每月",
        QUARTERLY: "季度",
      }[cell] || ""),
  },
  {
    prop: "frequencyDetail",
    label: "开始日期与时间",
    minWidth: 130,
    // åŒæ ·æ”¹ç”¨ formatData,PIMTable å†…部会把单元格值传进来
    formatData: (cell) => {
      if (typeof cell !== "string") return "";
      let val = cell;
      const replacements = {
        MON: "周一",
        TUE: "周二",
        WED: "周三",
        THU: "周四",
        FRI: "周五",
        SAT: "周六",
        SUN: "周日",
      };
      const { code, data } = await deviceMaintenanceTaskList(params);
      if (code === 200) {
        scheduledDataList.value = data?.records || [];
        scheduledPagination.total = data?.total || 0;
      }
    } catch (error) {
      ElMessage.error("获取定时任务列表失败");
      // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
      return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (match) => replacements[match]);
    },
  },
  {
    prop: "isActive",
    label: "定时任务",
    dataType: "slot",
    slot: "isActiveRef",
    align: "center",
    minWidth: 100,
  },
  { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
  { prop: "registrant", label: "登记人", minWidth: 100 },
  {
    prop: "registrationDate",
    label: "登记日期",
    minWidth: 100,
    formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD") : "-"),
  },
  {
    fixed: "right",
    label: "操作",
    dataType: "slot",
    slot: "operation",
    align: "center",
    width: "160px",
  },
]);
// ä¿å…»è®°å½•表格列配置(原设备保养表格列)
const columns = ref([
  {
    label: "设备名称",
    align: "center",
    prop: "deviceName",
  },
  {
    label: "规格型号",
    align: "center",
    prop: "deviceModel",
  },
  {
    label: "计划保养日期",
    align: "center",
    prop: "maintenancePlanTime",
    formatData: (cell) => {
      return cell == null ? "-" : dayjs(cell).format("YYYY-MM-DD");
    },
  },
  {
    label: "录入人",
    align: "center",
    prop: "createUserName",
  },
  // {
  //   label: "录入日期",
  //   align: "center",
  //   prop: "createTime",
  //   formatData: (cell) => dayjs(cell).format("YYYY-MM-DD HH:mm:ss"),
  //   width: 200,
  // },
  {
    label: "实际保养人",
    align: "center",
    prop: "maintenanceActuallyName",
  },
  {
    label: "实际保养日期",
    align: "center",
    prop: "maintenanceActuallyTime",
    formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD") : "-"),
  },
  {
    label: "保养结果",
    align: "center",
    prop: "maintenanceResult",
    dataType: "slot",
    slot: "maintenanceResultRef",
  },
  {
    label: "状态",
    align: "center",
    prop: "status",
    dataType: "slot",
    slot: "statusRef",
  },
  {
    fixed: "right",
    label: "操作",
    dataType: "slot",
    slot: "operation",
    align: "center",
    width: "250px",
  },
]);
// Tab切换处理
const handleTabChange = (tabName) => {
  if (tabName === "record") {
    getTableData();
  } else if (tabName === "scheduled") {
    getScheduledTableData();
  }
};
// ä¿å…»ä»»åŠ¡ç›¸å…³æ–¹æ³•
const getScheduledTableData = async () => {
  try {
    const params = {
      current: scheduledPagination.currentPage,
      size: scheduledPagination.pageSize,
      taskName: scheduledFilters.taskName || undefined,
      status: scheduledFilters.status || undefined,
    };
    const { code, data } = await deviceMaintenanceTaskList(params);
    if (code === 200) {
      scheduledDataList.value = data?.records || [];
      scheduledPagination.total = data?.total || 0;
    }
  };
  } catch (error) {
    ElMessage.error("获取定时任务列表失败");
  }
};
  const resetScheduledFilters = () => {
    scheduledFilters.taskName = "";
    scheduledFilters.status = "";
    getScheduledTableData();
  };
const resetScheduledFilters = () => {
  scheduledFilters.taskName = "";
  scheduledFilters.status = "";
  getScheduledTableData();
};
  const handleScheduledSelectionChange = selection => {
    scheduledMultipleList.value = selection;
  };
const handleScheduledSelectionChange = (selection) => {
  scheduledMultipleList.value = selection;
};
  const changeScheduledPage = page => {
    scheduledPagination.currentPage = page.page;
    scheduledPagination.pageSize = page.limit;
    getScheduledTableData();
  };
const changeScheduledPage = (page) => {
  scheduledPagination.currentPage = page.page;
  scheduledPagination.pageSize = page.limit;
  getScheduledTableData();
};
  const addScheduledTask = () => {
const addScheduledTask = () => {
  nextTick(() => {
    formDiaRef.value?.openDialog("add");
  });
};
const editScheduledTask = (row) => {
  if (row) {
    nextTick(() => {
      formDiaRef.value?.openDialog("add");
      formDiaRef.value?.openDialog("edit", row);
    });
  };
  }
};
  const editScheduledTask = row => {
    if (row) {
      nextTick(() => {
        formDiaRef.value?.openDialog("edit", row);
      });
const handleDetail = (row) => {
  if (row) {
    nextTick(() => {
      detailDiaRef.value?.openDialog(row);
    });
  }
};
const delScheduledTaskByIds = async (ids) => {
  try {
    await ElMessageBox.confirm("确定删除选中的定时任务吗?", "提示", {
      type: "warning",
    });
    const payload = Array.isArray(ids) ? ids : [ids];
    await deviceMaintenanceTaskDel(payload);
    ElMessage.success("删除定时任务成功");
    getScheduledTableData();
  } catch (error) {
    // ç”¨æˆ·å–消删除
  }
};
const handleScheduledOut = () => {
  ElMessage.info("导出定时任务功能待实现");
};
// ä¿å…»è®°å½•相关方法(原设备保养页面方法)
const getTableData = async () => {
  try {
    const params = {
      current: pagination.value.currentPage,
      size: pagination.value.pageSize,
      deviceName: filters.deviceName || undefined,
      maintenancePlanTime: filters.maintenancePlanTime
        ? dayjs(filters.maintenancePlanTime).format("YYYY-MM-DD")
        : undefined,
      maintenanceActuallyTime: filters.maintenanceActuallyTime
        ? dayjs(filters.maintenanceActuallyTime).format("YYYY-MM-DD")
        : undefined,
      maintenanceActuallyName: filters.maintenanceActuallyName || undefined,
    };
    const { code, data } = await getUpkeepPage(params);
    if (code === 200) {
      dataList.value = data.records;
      pagination.value.total = data.total;
    }
  };
  } catch (error) {
    console.log(error);
  }
};
  const handleDetail = row => {
    if (row) {
      nextTick(() => {
        detailDiaRef.value?.openDialog(row);
      });
    }
  };
const resetFilters = () => {
  filters.deviceName = "";
  filters.maintenancePlanTime = "";
  filters.maintenanceActuallyTime = "";
  filters.maintenanceActuallyName = "";
  getTableData();
};
  const delScheduledTaskByIds = async ids => {
    try {
      await ElMessageBox.confirm("确定删除选中的定时任务吗?", "提示", {
        type: "warning",
      });
      const payload = Array.isArray(ids) ? ids : [ids];
      await deviceMaintenanceTaskDel(payload);
      ElMessage.success("删除定时任务成功");
      getScheduledTableData();
    } catch (error) {
      // ç”¨æˆ·å–消删除
    }
  };
const handleSelectionChange = (selection) => {
  multipleList.value = selection;
};
  const handleScheduledOut = () => {
    ElMessage.info("导出定时任务功能待实现");
  };
// æ£€æŸ¥é€‰ä¸­çš„记录中是否有完结状态的
const hasFinishedStatus = computed(() => {
  return multipleList.value.some((item) => item.status === 1);
});
  // ä¿å…»è®°å½•相关方法(原设备保养页面方法)
  const getTableData = async () => {
    try {
      const params = {
        current: pagination.value.currentPage,
        size: pagination.value.pageSize,
        deviceName: filters.deviceName || undefined,
        maintenancePlanTime: filters.maintenancePlanTime
          ? dayjs(filters.maintenancePlanTime).format("YYYY-MM-DD")
          : undefined,
        maintenanceActuallyTime: filters.maintenanceActuallyTime
          ? dayjs(filters.maintenanceActuallyTime).format("YYYY-MM-DD")
          : undefined,
        maintenanceActuallyName: filters.maintenanceActuallyName || undefined,
      };
const changePage = (page) => {
  pagination.value.currentPage = page.page;
  pagination.value.pageSize = page.limit;
  getTableData();
};
      const { code, data } = await getUpkeepPage(params);
      if (code === 200) {
        dataList.value = data.records;
        pagination.value.total = data.total;
      }
    } catch (error) {
      console.log(error);
    }
  };
const addMaintain = (row) => {
  maintainModalRef.value.open(row.id, row);
};
  const resetFilters = () => {
    filters.deviceName = "";
    filters.maintenancePlanTime = "";
    filters.maintenanceActuallyTime = "";
    filters.maintenanceActuallyName = "";
    getTableData();
  };
const handleRecordDetail = (row) => {
  if (row) {
    nextTick(() => {
      recordDetailDiaRef.value?.openDialog(row);
    });
  }
};
  const handleSelectionChange = selection => {
    multipleList.value = selection;
  };
const addPlan = () => {
  planModalRef.value.openModal();
};
  // æ£€æŸ¥é€‰ä¸­çš„记录中是否有完结状态的
  const hasFinishedStatus = computed(() => {
    return multipleList.value.some(item => item.status === 1);
  });
const editPlan = (id) => {
  planModalRef.value.openEdit(id);
};
  const changePage = page => {
    pagination.value.currentPage = page.page;
    pagination.value.pageSize = page.limit;
    getTableData();
  };
const delRepairByIds = async (ids) => {
  // æ£€æŸ¥æ˜¯å¦æœ‰å®Œç»“状态的记录
  const hasFinished = multipleList.value.some((item) => item.status === 1);
  if (hasFinished) {
    ElMessage.warning("不能删除状态为完结的记录");
    return;
  }
  const addMaintain = row => {
    maintainModalRef.value.open(row.id, row);
  };
  const handleRecordDetail = row => {
    if (row) {
      nextTick(() => {
        recordDetailDiaRef.value?.openDialog(row);
      });
    }
  };
  const addPlan = () => {
    planModalRef.value.openModal();
  };
  const editPlan = id => {
    planModalRef.value.openEdit(id);
  };
  const delRepairByIds = async ids => {
    // æ£€æŸ¥æ˜¯å¦æœ‰å®Œç»“状态的记录
    const hasFinished = multipleList.value.some(item => item.status === 1);
    if (hasFinished) {
      ElMessage.warning("不能删除状态为完结的记录");
      return;
    }
    try {
      await ElMessageBox.confirm("确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      });
      const { code } = await delUpkeep(ids);
      if (code === 200) {
        ElMessage.success("删除成功");
        getTableData();
      }
    } catch (error) {
      // ç”¨æˆ·å–消删除
    }
  };
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
  try {
    await ElMessageBox.confirm("确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?", "警告", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download("/device/maintenance/export", {}, "设备保养.xlsx");
      })
      .catch(() => {
        ElMessage.info("已取消");
      });
  };
    });
  const handleDateChange = (date, type) => {
    if (type === 1) {
      filters.maintenanceActuallyTime = date
        ? dayjs(date).format("YYYY-MM-DD")
        : "";
    } else {
      filters.maintenancePlanTime = date ? dayjs(date).format("YYYY-MM-DD") : "";
    }
    getTableData();
  };
  // æ‰“开附件弹窗
  const openFileDialog = async row => {
    currentMaintenanceTaskId.value = row.id;
    fileDialogVisible.value = true;
  };
  onMounted(() => {
    // æ ¹æ®é»˜è®¤æ¿€æ´»çš„ Tab è°ƒç”¨å¯¹åº”的查询接口
    if (activeTab.value === "scheduled") {
      getScheduledTableData();
    } else {
    const { code } = await delUpkeep(ids);
    if (code === 200) {
      ElMessage.success("删除成功");
      getTableData();
    }
  });
  } catch (error) {
    // ç”¨æˆ·å–消删除
  }
};
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download("/device/maintenance/export", {}, "设备保养.xlsx");
    })
    .catch(() => {
      ElMessage.info("已取消");
    });
};
const handleDateChange = (date, type) => {
  if (type === 1) {
    filters.maintenanceActuallyTime = date ? dayjs(date).format("YYYY-MM-DD") : "";
  } else {
    filters.maintenancePlanTime = date ? dayjs(date).format("YYYY-MM-DD") : "";
  }
  getTableData();
};
// æ‰“开附件弹窗
const openFileDialog = async (row) => {
  currentMaintenanceTaskId.value = row.id;
  fileDialogVisible.value = true;
};
onMounted(() => {
  // æ ¹æ®é»˜è®¤æ¿€æ´»çš„ Tab è°ƒç”¨å¯¹åº”的查询接口
  if (activeTab.value === "scheduled") {
    getScheduledTableData();
  } else {
    getTableData();
  }
});
</script>
<style lang="scss" scoped>
  .table_list {
    margin-top: unset;
  }
  .actions {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
  }
.table_list {
  margin-top: unset;
}
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
}
</style>