新疆大罗素
1.设备保养、设备维修、设备巡检新增时可以多选设备
2.设备台账添加区域维护字段
3.添加环境页面实时展示设备数据
已添加3个文件
已修改11个文件
3697 ■■■■■ 文件已修改
src/api/equipmentManagement/deviceArea.js 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/environmentalMonitoring.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 183 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/formDia.vue 595 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 677 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Modal.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 631 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/RepairModal.vue 227 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/PlanModal.vue 245 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/formDia.vue 734 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/environmentalMonitoring/index.vue 277 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/deviceArea.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import request from "@/utils/request";
export function getDeviceAreaTree(params) {
  return request({
    url: "/device/area/tree",
    method: "get",
    params,
  });
}
export function getDeviceAreaTreeWithDevices(params) {
  return request({
    url: "/device/area/treeWithDevices",
    method: "get",
  });
}
export function getDeviceAreaPage(params) {
  return request({
    url: "/device/area/page",
    method: "get",
    params,
  });
}
export function getDeviceAreaDetail(id) {
  return request({
    url: `/device/area/${id}`,
    method: "get",
  });
}
export function addDeviceArea(data) {
  return request({
    url: "/device/area",
    method: "post",
    data,
  });
}
export function updateDeviceArea(data) {
  return request({
    url: "/device/area",
    method: "put",
    data,
  });
}
export function deleteDeviceArea(ids) {
  return request({
    url: "/device/area",
    method: "delete",
    data: ids,
  });
}
src/api/inventoryManagement/environmentalMonitoring.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
import request from "@/utils/request";
export const getEnvironmentalRealData = () => {
  return request({
    url: "/iot/getRealData",
    method: "get",
  });
};
src/layout/index.vue
@@ -1,8 +1,14 @@
<template>
  <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
    <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
    <sidebar v-if="!sidebar.hide" class="sidebar-container" />
    <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
  <div :class="classObj"
       class="app-wrapper"
       :style="{ '--current-color': theme }">
    <div v-if="device === 'mobile' && sidebar.opened"
         class="drawer-bg"
         @click="handleClickOutside" />
    <sidebar v-if="!sidebar.hide"
             class="sidebar-container" />
    <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }"
         class="main-container">
      <div :class="{ 'fixed-header': fixedHeader }">
        <navbar @setLayout="setLayout" />
        <tags-view v-if="needTagsView" />
@@ -14,105 +20,112 @@
</template>
<script setup>
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings'
  import { useWindowSize } from "@vueuse/core";
  import Sidebar from "./components/Sidebar/index.vue";
  import { AppMain, Navbar, Settings, TagsView } from "./components";
  import defaultSettings from "@/settings";
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
  import useAppStore from "@/store/modules/app";
  import useSettingsStore from "@/store/modules/settings";
const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme)
const sideTheme = computed(() => settingsStore.sideTheme)
const sidebar = computed(() => useAppStore().sidebar)
const device = computed(() => useAppStore().device)
const needTagsView = computed(() => settingsStore.tagsView)
const fixedHeader = computed(() => settingsStore.fixedHeader)
  const settingsStore = useSettingsStore();
  const theme = computed(() => settingsStore.theme);
  const sideTheme = computed(() => settingsStore.sideTheme);
  const sidebar = computed(() => useAppStore().sidebar);
  const device = computed(() => useAppStore().device);
  const needTagsView = computed(() => settingsStore.tagsView);
  const fixedHeader = computed(() => settingsStore.fixedHeader);
const classObj = computed(() => ({
  hideSidebar: !sidebar.value.opened,
  openSidebar: sidebar.value.opened,
  withoutAnimation: sidebar.value.withoutAnimation,
  mobile: device.value === 'mobile'
}))
  const classObj = computed(() => ({
    hideSidebar: !sidebar.value.opened,
    openSidebar: sidebar.value.opened,
    withoutAnimation: sidebar.value.withoutAnimation,
    mobile: device.value === "mobile",
  }));
const { width, height } = useWindowSize()
const WIDTH = 992 // refer to Bootstrap's responsive design
  const { width, height } = useWindowSize();
  const WIDTH = 992; // refer to Bootstrap's responsive design
watch(() => device.value, () => {
  if (device.value === 'mobile' && sidebar.value.opened) {
    useAppStore().closeSideBar({ withoutAnimation: false })
  watch(
    () => device.value,
    () => {
      if (device.value === "mobile" && sidebar.value.opened) {
        useAppStore().closeSideBar({ withoutAnimation: false });
      }
    }
  );
  watchEffect(() => {
    if (width.value - 1 < WIDTH) {
      useAppStore().toggleDevice("mobile");
      useAppStore().closeSideBar({ withoutAnimation: true });
    } else {
      useAppStore().toggleDevice("desktop");
    }
  });
  function handleClickOutside() {
    useAppStore().closeSideBar({ withoutAnimation: false });
  }
})
watchEffect(() => {
  if (width.value - 1 < WIDTH) {
    useAppStore().toggleDevice('mobile')
    useAppStore().closeSideBar({ withoutAnimation: true })
  } else {
    useAppStore().toggleDevice('desktop')
  const settingRef = ref(null);
  function setLayout() {
    settingRef.value.openSetting();
  }
})
function handleClickOutside() {
  useAppStore().closeSideBar({ withoutAnimation: false })
}
const settingRef = ref(null)
function setLayout() {
  settingRef.value.openSetting()
}
</script>
<style lang="scss" scoped>
  @import "@/assets/styles/mixin.scss";
  @import "@/assets/styles/variables.module.scss";
.app-wrapper {
  @include clearfix;
  position: relative;
  height: 100%;
  width: 100%;
  background:
    radial-gradient(circle at top, rgba(223, 232, 226, 0.95), transparent 32%),
    linear-gradient(180deg, #f7faf8 0%, var(--app-bg) 100%);
  .app-wrapper {
    @include clearfix;
    position: relative;
    height: 100%;
    width: 100%;
    background: radial-gradient(
        circle at top,
        rgba(223, 232, 226, 0.95),
        transparent 32%
      ),
      linear-gradient(180deg, #f7faf8 0%, var(--app-bg) 100%);
  &.mobile.openSidebar {
    position: fixed;
    top: 0;
    &.mobile.openSidebar {
      position: fixed;
      top: 0;
    }
  }
}
.drawer-bg {
  background: #000;
  opacity: 0.3;
  width: 100%;
  top: 0;
  height: 100%;
  position: absolute;
  z-index: 999;
}
  .drawer-bg {
    background: #000;
    opacity: 0.3;
    width: 100%;
    top: 0;
    height: 100%;
    position: absolute;
    z-index: 999;
  }
.fixed-header {
  position: fixed;
  top: 12px;
  right: 16px;
  z-index: 9;
  width: calc(100% - #{$base-sidebar-width} - 32px);
  transition: width 0.28s, right 0.28s;
  padding-bottom: 8px;
}
  .fixed-header {
    position: fixed;
    top: 0px;
    padding-top: 12px;
    right: 16px;
    z-index: 9;
    width: calc(100% - #{$base-sidebar-width} - 32px);
    transition: width 0.28s, right 0.28s;
    padding-bottom: 8px;
    background-color: #f3f6f4;
  }
  .hideSidebar .fixed-header {
    width: calc(100% - 100px);
  }
.hideSidebar .fixed-header {
  width: calc(100% - 100px);
}
  .sidebarHide .fixed-header {
    width: calc(100% - 32px);
  }
.sidebarHide .fixed-header {
  width: calc(100% - 32px);
}
.mobile .fixed-header {
  width: 100%;
}
  .mobile .fixed-header {
    width: 100%;
  }
</style>
src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -1,26 +1,73 @@
<template>
  <div>
    <el-dialog :title="operationType === 'add' ? '新增巡检任务' : '编辑巡检任务'"
               v-model="dialogVisitable" width="800px" @close="cancel">
    <el-dialog
      v-model="dialogVisitable"
      :title="operationType === 'add' ? '新增巡检任务' : '编辑巡检任务'"
      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-form-item label="所属区域" prop="areaId">
              <el-tree-select
                v-model="form.areaId"
                :data="areaOptions"
                :props="areaTreeProps"
                node-key="id"
                value-key="id"
                check-strictly
                clearable
                filterable
                placeholder="请选择所属区域"
                style="width: 100%"
                @change="handleAreaChange"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="设备名称" prop="deviceLedgerIds">
              <el-select
                v-model="form.deviceLedgerIds"
                multiple
                collapse-tags
                collapse-tags-tooltip
                clearable
                filterable
                placeholder="请选择设备"
                style="width: 100%"
                @change="setDeviceModels"
              >
                <el-option
                  v-for="(item, index) in deviceOptions"
                  :key="index"
                  v-for="item in deviceOptions"
                  :key="item.id"
                  :label="item.deviceName"
                  :value="item.id"
                ></el-option>
                />
              </el-select>
            </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" placeholder="请选择" multiple clearable>
                <el-option
                  v-for="item in userList"
                  :key="item.userId"
                  :label="item.nickName"
                  :value="item.userId"
                />
              </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 label="规格型号">
              <el-input
                v-model="form.deviceModel"
                placeholder="自动带出规格型号"
                disabled
              />
            </el-form-item>
          </el-col>
        </el-row>
@@ -35,55 +82,51 @@
          <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-option label="每日" value="DAILY" />
                <el-option label="每周" value="WEEKLY" />
                <el-option label="每月" value="MONTHLY" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'DAILY' && form.frequencyType">
          <el-col :span="12" v-if="form.frequencyType === 'DAILY'">
            <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.frequencyDetail"
                placeholder="选择时间"
                format="HH:mm"
                value-format="HH:mm"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'QUARTERLY' && form.frequencyType">
          <el-col :span="12" v-if="form.frequencyType === 'WEEKLY'">
            <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'">
            <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"
                v-model="form.frequencyDetail"
                type="datetime"
                clearable
                placeholder="选择开始日期"
                format="DD,HH:mm"
                value-format="DD,HH:mm"
              />
            </el-form-item>
          </el-col>
@@ -100,188 +143,308 @@
</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 { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import { getCurrentInstance, reactive, ref, toRefs } from "vue";
import useUserStore from "@/store/modules/user";
import { addOrEditTimingTask } from "@/api/inspectionManagement/index.js";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import {
  getDeviceAreaTree,
  getDeviceAreaTreeWithDevices,
} from "@/api/equipmentManagement/deviceArea";
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const userStore = useUserStore()
const { proxy } = getCurrentInstance();
const emit = defineEmits(["closeDia"]);
const userStore = useUserStore();
const dialogVisitable = ref(false);
const operationType = ref('add');
const operationType = ref("add");
const areaOptions = ref([]);
const deviceOptions = ref([]);
const data = reactive({
  form: {
    taskId: undefined,
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    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 areaTreeProps = {
  label: "areaName",
  children: "children",
};
const setDeviceModel = (id) => {
  const option = deviceOptions.value.find((item) => item.id === id);
  if (option) {
    form.value.taskName = option.deviceName;
  }
}
const data = reactive({
  form: {
    areaId: undefined,
    taskId: undefined,
    taskIds: [],
    taskIdsStr: undefined,
    deviceLedgerIds: [],
    deviceLedgerIdsStr: undefined,
    taskName: undefined,
    deviceModel: undefined,
    inspector: [],
    inspectorIds: "",
    remarks: "",
    frequencyType: "",
    frequencyDetail: "",
    week: "",
    time: "",
  },
  rules: {
    areaId: [{ required: true, message: "请选择所属区域", trigger: "change" }],
    deviceLedgerIds: [{ required: true, message: "请选择设备", trigger: "change" }],
    inspector: [{ required: true, message: "请选择巡检人", trigger: "change" }],
    frequencyType: [{ required: true, message: "请选择任务频率", trigger: "change" }],
    frequencyDetail: [
      {
        required: true,
        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();
            }
            return;
          }
          if (!value) {
            callback(new Error("请选择日期"));
            return;
          }
          callback();
        },
      },
    ],
  },
});
// æ‰“开弹框
const { form, rules } = toRefs(data);
const loadAreaTree = async () => {
  const { data } = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(data) ? data : [];
};
const normalizeIdList = (value) => {
  if (Array.isArray(value)) {
    return value
      .map((item) => Number(item))
      .filter((item) => Number.isFinite(item));
  }
  if (typeof value === "string") {
    return value
      .split(",")
      .map((item) => Number(item.trim()))
      .filter((item) => Number.isFinite(item));
  }
  if (value !== undefined && value !== null && value !== "") {
    const numericValue = Number(value);
    return Number.isFinite(numericValue) ? [numericValue] : [];
  }
  return [];
};
const getNodeDevices = (node) => {
  const candidates = [
    node?.deviceList,
    node?.devices,
    node?.deviceLedgerList,
    node?.deviceLedgers,
    node?.ledgerList,
    node?.ledgers,
  ];
  return candidates.find((item) => Array.isArray(item)) || [];
};
const normalizeDevice = (item) => ({
  ...item,
  id: item.id ?? item.deviceLedgerId,
  deviceName: item.deviceName ?? item.name,
  deviceModel: item.deviceModel ?? item.model,
});
const collectDevices = (node) => {
  const currentDevices = getNodeDevices(node).map(normalizeDevice);
  const childDevices = (node?.children || []).flatMap((child) =>
    collectDevices(child)
  );
  const deviceMap = new Map();
  [...currentDevices, ...childDevices].forEach((item) => {
    if (item?.id !== undefined && item?.id !== null) {
      deviceMap.set(Number(item.id), item);
    }
  });
  return Array.from(deviceMap.values());
};
const findAreaNode = (nodes, areaId) => {
  for (const node of nodes || []) {
    if (Number(node.id) === Number(areaId)) {
      return node;
    }
    const target = findAreaNode(node.children, areaId);
    if (target) {
      return target;
    }
  }
  return null;
};
const loadDevicesByArea = async (areaId) => {
  if (!areaId) {
    deviceOptions.value = [];
    return;
  }
  const { data } = await getDeviceAreaTreeWithDevices();
  const treeData = Array.isArray(data) ? data : [];
  const currentNode = findAreaNode(treeData, areaId);
  deviceOptions.value = currentNode ? collectDevices(currentNode) : [];
};
const syncDeviceFields = (deviceIds) => {
  const selectedIds = normalizeIdList(deviceIds);
  const selectedDevices = selectedIds
    .map((deviceId) =>
      deviceOptions.value.find((item) => Number(item.id) === Number(deviceId))
    )
    .filter(Boolean);
  form.value.deviceLedgerIds = selectedIds;
  form.value.deviceLedgerIdsStr = selectedIds.join(",");
  form.value.taskIds = [...selectedIds];
  form.value.taskIdsStr = selectedIds.join(",");
  form.value.taskId = selectedIds[0];
  form.value.taskName = selectedDevices
    .map((item) => item.deviceName)
    .filter(Boolean)
    .join(",");
  form.value.deviceModel = selectedDevices
    .map((item) => item.deviceModel || "-")
    .join(",");
};
const setDeviceModels = (deviceIds) => {
  syncDeviceFields(deviceIds);
};
const handleAreaChange = async (areaId) => {
  form.value.taskId = undefined;
  form.value.taskIds = [];
  form.value.taskIdsStr = undefined;
  form.value.deviceLedgerIds = [];
  form.value.deviceLedgerIdsStr = undefined;
  form.value.taskName = undefined;
  form.value.deviceModel = undefined;
  await loadDevicesByArea(areaId);
};
const resetForm = () => {
  if (proxy.$refs.formRef) {
    proxy.$refs.formRef.resetFields();
  }
  form.value = {
    areaId: undefined,
    taskId: undefined,
    taskIds: [],
    taskIdsStr: undefined,
    deviceLedgerIds: [],
    deviceLedgerIdsStr: undefined,
    taskName: undefined,
    deviceModel: undefined,
    inspector: [],
    inspectorIds: "",
    remarks: "",
    frequencyType: "",
    frequencyDetail: "",
    week: "",
    time: "",
  };
};
const openDialog = async (type, row) => {
  dialogVisitable.value = true
  operationType.value = type
  // é‡ç½®è¡¨å•
  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)
    // å¦‚果有设备ID,自动设置设备信息
    if (form.value.taskId) {
      setDeviceModel(form.value.taskId);
  await loadAreaTree();
  if (type === "edit" && row) {
    form.value = {
      ...form.value,
      ...row,
      inspector: row.inspectorIds
        ? String(row.inspectorIds)
            .split(",")
            .map((item) => Number(item))
            .filter((item) => Number.isFinite(item))
        : [],
    };
    form.value.deviceLedgerIds = normalizeIdList(
      row.deviceLedgerIds ??
        row.deviceLedgerIdsStr ??
        row.taskIds ??
        row.taskIdsStr ??
        row.taskId
    );
    form.value.deviceLedgerIdsStr = form.value.deviceLedgerIds.join(",");
    form.value.taskIds = [...form.value.deviceLedgerIds];
    form.value.taskIdsStr = form.value.deviceLedgerIds.join(",");
    form.value.taskId = form.value.deviceLedgerIds[0];
    if (form.value.areaId) {
      await loadDevicesByArea(form.value.areaId);
      syncDeviceFields(form.value.deviceLedgerIds);
    }
  }
}
};
// å…³é—­å¯¹è¯æ¡†
const cancel = () => {
  resetForm()
  dialogVisitable.value = false
  emit('closeDia')
}
  resetForm();
  dialogVisitable.value = false;
  emit("closeDia");
};
// é‡ç½®è¡¨å•函数
const resetForm = () => {
  if (proxy.$refs.formRef) {
    proxy.$refs.formRef.resetFields()
  }
  // é‡ç½®è¡¨å•数据确保设备信息正确重置
  form.value = {
    taskId: undefined,
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    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('提交失败,请重试')
      }
  proxy.$refs.formRef.validate(async (valid) => {
    if (!valid) {
      return;
    }
  })
}
defineExpose({ openDialog })
    try {
      syncDeviceFields(form.value.deviceLedgerIds);
      const payload = { ...form.value };
      payload.inspectorIds = Array.isArray(form.value.inspector)
        ? form.value.inspector.join(",")
        : "";
      delete payload.inspector;
      if (payload.frequencyType === "WEEKLY") {
        payload.frequencyDetail = `${payload.week},${payload.time}`;
      }
      const userInfo = await userStore.getInfo();
      payload.registrantId = userInfo.user.userId;
      payload.taskId = form.value.deviceLedgerIds[0];
      payload.taskIds = [...form.value.deviceLedgerIds];
      payload.taskIdsStr = form.value.deviceLedgerIds.join(",");
      payload.deviceLedgerIds = [...form.value.deviceLedgerIds];
      payload.deviceLedgerIdsStr = form.value.deviceLedgerIds.join(",");
      payload.taskName = form.value.taskName;
      payload.deviceModel = form.value.deviceModel || "-";
      await addOrEditTimingTask(payload);
      cancel();
      proxy.$modal.msgSuccess("提交成功");
    } catch (error) {
      proxy.$modal.msgError("提交失败,请重试");
    }
  });
};
defineExpose({ openDialog });
</script>
<style scoped>
</style>
<style scoped></style>
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -1,393 +1,360 @@
<template>
  <div class="app-container">
    <el-form :inline="true"
             :model="queryParams"
             class="search-form">
    <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-input
          v-model="queryParams.taskName"
          placeholder="请输入巡检任务名称"
          clearable
          style="width: 200px"
        />
      </el-form-item>
      <el-form-item label="所属区域">
        <el-tree-select
          v-model="queryParams.areaId"
          :data="areaOptions"
          :props="areaTreeProps"
          node-key="id"
          value-key="id"
          check-strictly
          clearable
          filterable
          placeholder="请选择所属区域"
          style="width: 220px"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary"
                   @click="handleQuery">查询</el-button>
        <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" />
      <div class="toolbar">
        <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 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>
      </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'
                }"
                  :table-style="{ width: '100%', height: 'calc(100vh - 23em)' }">
          <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">
                  {{ person }}
                </el-tag>
              </template>
              <span v-else
                    class="no-data">--</span>
            </div>
          </template>
        </PIMTable>
      </div>
      <PIMTable
        :table-loading="tableLoading"
        :table-data="tableData"
        :column="tableColumns"
        :is-selection="true"
        :border="true"
        :page="{
          current: pageNum,
          size: pageSize,
          total,
          layout: 'total, sizes, prev, pager, next, jumper',
        }"
        :table-style="{ width: '100%', height: 'calc(100vh - 23em)' }"
        @selection-change="handleSelectionChange"
        @pagination="handlePagination"
      >
        <template #inspector="{ row }">
          <div class="person-tags">
            <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"
              >
                {{ person }}
              </el-tag>
            </template>
            <span v-else class="no-data">--</span>
          </div>
        </template>
      </PIMTable>
    </el-card>
    <form-dia ref="formDia"
              @closeDia="handleQuery"></form-dia>
    <view-files ref="viewFiles"></view-files>
    <form-dia ref="formDia" @closeDia="handleQuery" />
    <view-files ref="viewFiles" />
  </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 { Delete, Plus } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
import { getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
import {
  delTimingTask,
  inspectionTaskList,
  timingTaskList,
} from "@/api/inspectionManagement/index.js";
import { getDeviceAreaTree } from "@/api/equipmentManagement/deviceArea";
  // ç»„件引入
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
  import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
const { proxy } = getCurrentInstance();
const formDia = ref();
const viewFiles = ref();
  // æŽ¥å£å¼•å…¥
  import {
    delTimingTask,
    inspectionTaskList,
    timingTaskList,
  } from "@/api/inspectionManagement/index.js";
const queryParams = reactive({
  taskName: "",
  areaId: undefined,
});
  // å…¨å±€å˜é‡
  const { proxy } = getCurrentInstance();
  const formDia = ref();
  const viewFiles = ref();
const areaOptions = ref([]);
const areaTreeProps = {
  label: "areaName",
  children: "children",
};
  // æŸ¥è¯¢å‚æ•°
  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: "定时任务记录" },
  ]);
const selectedRows = ref([]);
const tableData = 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: 160 },
    { prop: "remarks", label: "备注", minWidth: 150 },
    { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 150,
      // formatter: (_, __, val) => ({
      //   DAILY: "每日",
      //   WEEKLY: "每周",
      //   MONTHLY: "每月",
      //   QUARTERLY: "季度"
      // }[val] || "")
      formatData: params => {
        return params === "DAILY"
          ? "每日"
          : params === "WEEKLY"
          ? "每周"
          : params === "MONTHLY"
          ? "每月"
          : params === "QUARTERLY"
          ? "季度"
          : "";
      },
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 150,
      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: 100 },
    { prop: "createTime", label: "登记日期", minWidth: 100 },
  ]);
  // æ“ä½œåˆ—配置
  const getOperationColumn = operations => {
    if (!operations || operations.length === 0) return null;
    const operationConfig = {
      label: "操作",
      width: 130,
      fixed: "right",
      dataType: "action",
      operation: operations
        .map(op => {
          switch (op) {
            case "edit":
              return {
                name: "编辑",
                clickFun: handleAdd,
                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(["viewFile"]);
      tableColumns.value = [
        ...columns.value,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["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 };
          // å¤„理 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] = "";
const columns = ref([
    {
        label: "所在区域",
        prop: "areaName",
    },
  { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
  { prop: "remarks", label: "备注", minWidth: 150 },
  { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
  {
    prop: "frequencyType",
    label: "频次",
    minWidth: 150,
    formatData: (value) =>
      ({
        DAILY: "每日",
        WEEKLY: "每周",
        MONTHLY: "每月",
        QUARTERLY: "季度",
      }[value] || ""),
  },
  {
    prop: "frequencyDetail",
    label: "开始日期与时间",
    minWidth: 150,
    formatter: (row, column, cellValue) => {
      if (typeof cellValue !== "string") {
        return "";
      }
    }
    handleQuery();
  };
      const replacements = {
        MON: "周一",
        TUE: "周二",
        WED: "周三",
        THU: "周四",
        FRI: "周五",
        SAT: "周六",
        SUN: "周日",
      };
      return cellValue.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (match) => replacements[match]);
    },
  },
  { prop: "registrant", label: "登记人", minWidth: 100 },
  { prop: "createTime", label: "登记日期", minWidth: 100 },
]);
  // æ–°å¢ž / ç¼–辑
  const handleAdd = row => {
    const type = row ? "edit" : "add";
    nextTick(() => {
      formDia.value?.openDialog(type, row);
    });
  };
  // æŸ¥çœ‹é™„ä»¶
  const viewFile = row => {
    nextTick(() => {
      viewFiles.value?.openDialog(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 getOperationColumn = (operations) => {
  if (!operations || operations.length === 0) {
    return null;
  }
  return {
    label: "操作",
    width: 130,
    fixed: "right",
    dataType: "action",
    operation: operations
      .map((op) => {
        switch (op) {
          case "edit":
            return {
              name: "编辑",
              clickFun: handleAdd,
              color: "#409EFF",
            };
          case "viewFile":
            return {
              name: "查看附件",
              clickFun: viewFile,
              color: "#67C23A",
            };
          default:
            return null;
        }
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
      .filter(Boolean),
  };
};
const loadAreaTree = async () => {
  const { data } = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(data) ? data : [];
};
onMounted(() => {
  loadAreaTree();
  radioChange("taskManage");
});
const radioChange = (value) => {
  if (value === "taskManage") {
    const operationColumn = getOperationColumn(["edit"]);
    tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
  } else {
    const operationColumn = getOperationColumn(["viewFile"]);
    tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
  }
  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,
  };
  const apiCall =
    activeRadio.value === "task" ? inspectionTaskList(params) : timingTaskList(params);
  apiCall
    .then((res) => {
      const rawData = res?.data?.records || [];
      tableData.value = rawData.map((item) => {
        const processedItem = { ...item };
        if (processedItem.inspector) {
          if (typeof processedItem.inspector === "string") {
            processedItem.inspector = processedItem.inspector
              .split(",")
              .map((text) => text.trim())
              .filter(Boolean);
          } 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 = () => {
  queryParams.taskName = "";
  queryParams.areaId = undefined;
  handleQuery();
};
const handleAdd = (row) => {
  const type = row ? "edit" : "add";
  nextTick(() => {
    formDia.value?.openDialog(type, row);
  });
};
const viewFile = (row) => {
  nextTick(() => {
    viewFiles.value?.openDialog(row);
  });
};
const handleDelete = () => {
  if (!selectedRows.value.length) {
    proxy.$modal.msgWarning("请选择要删除的数据");
    return;
  }
  const deleteIds = selectedRows.value.map((item) => item.id);
  proxy.$modal
    .confirm("是否确认删除所选数据项?")
    .then(() => 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 {
        proxy.download("/inspectionTask/export", {}, "定时任务记录.xlsx");
      }
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
</script>
<style scoped>
  .person-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
  }
.toolbar {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  margin-bottom: 10px;
}
  .person-tag {
    margin-right: 4px;
    margin-bottom: 2px;
  }
.person-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
  .no-data {
    color: #909399;
    font-size: 14px;
  }
</style>
.person-tag {
  margin-right: 4px;
  margin-bottom: 2px;
}
.no-data {
  color: #909399;
  font-size: 14px;
}
</style>
src/views/equipmentManagement/ledger/Form.vue
@@ -2,6 +2,22 @@
  <el-form :model="form" label-width="120px" :rules="formRules" ref="formRef">
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="所属区域" prop="areaId">
          <el-tree-select
            v-model="form.areaId"
            :data="areaOptions"
            :props="areaTreeProps"
            node-key="id"
            value-key="id"
            check-strictly
            clearable
            filterable
            placeholder="请选择所属区域"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="设备名称" prop="deviceName">
          <el-input v-model="form.deviceName" placeholder="请输入设备名称" />
        </el-form-item>
@@ -72,7 +88,6 @@
        <el-form-item label="数量" prop="number">
          <el-input-number :min="1" style="width: 100%"
            v-model="form.number"
                                                     disabled
            placeholder="请输入数量"
            @change="mathNum"
          />
@@ -168,19 +183,25 @@
import useFormData from "@/hooks/useFormData";
// import useUserStore from "@/store/modules/user";
import { getLedgerById } from "@/api/equipmentManagement/ledger";
import { getDeviceAreaTree } from "@/api/equipmentManagement/deviceArea";
import dayjs from "dayjs";
import {
  calculateTaxIncludeTotalPrice,
  calculateTaxExclusiveTotalPrice,
} from "@/utils/summarizeTable";
import { ElMessage } from "element-plus";
import {ref} from "vue";
import { ref, onMounted } from "vue";
defineOptions({
  name: "设备台账表单",
});
const formRef = ref(null);
const operationType = ref('');
const areaOptions = ref([]);
const areaTreeProps = {
  label: "areaName",
  children: "children",
};
// è®¾å¤‡ç±»åž‹å›ºå®šé€‰é¡¹
const deviceTypeOptions = ref([
  '生产设备',
@@ -190,6 +211,7 @@
  '其他设备'
]);
const formRules = {
    areaId: [{ required: true, trigger: "change", message: "请选择所属区域" }],
    deviceName: [{ required: true, trigger: "blur", message: "请输入" }],
    deviceModel: [{ required: true, trigger: "blur", message: "请输入" }],
    type: [{ required: true, trigger: "change", message: "请选择或输入设备类型" }],
@@ -214,6 +236,7 @@
}
const { form, resetForm } = useFormData({
  areaId: undefined, // åŒºåŸŸID
  deviceName: undefined, // è®¾å¤‡åç§°
  deviceModel: undefined, // è§„格型号
  deviceBrand: undefined, // è®¾å¤‡å“ç‰Œ
@@ -239,6 +262,7 @@
    }
  const { code, data } = await getLedgerById(id);
  if (code == 200) {
    form.areaId = data.areaId;
    form.deviceName = data.deviceName;
    form.deviceModel = data.deviceModel;
    form.deviceBrand = data.deviceBrand;
@@ -261,6 +285,15 @@
      form.planRuntimeTime = undefined;
    }
  }
};
const setAreaId = (areaId) => {
  form.areaId = areaId;
};
const loadAreaOptions = async () => {
  const res = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
};
const handleDeviceTypeChange = (value) => {
@@ -298,9 +331,14 @@
  clearValidate();
};
onMounted(() => {
  loadAreaOptions();
});
defineExpose({
  form,
  loadForm,
  setAreaId,
  resetForm,
  clearValidate,
  resetFormAndValidate,
src/views/equipmentManagement/ledger/Modal.vue
@@ -62,8 +62,15 @@
  formRef.value.loadForm(id);
};
const openCreateModal = async (areaId) => {
  openModal();
  await nextTick();
  formRef.value.setAreaId(areaId);
};
defineExpose({
  openModal,
  loadForm,
  openCreateModal,
});
</script>
src/views/equipmentManagement/ledger/index.vue
@@ -1,85 +1,191 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="设备名称">
  <div class="app-container ledger-view">
    <div class="left-panel">
      <div class="tree-toolbar">
        <el-input
          v-model="filters.deviceName"
          style="width: 200px"
          placeholder="请输入设备名称"
          v-model="treeKeyword"
          style="width: calc(100% - 102px)"
          placeholder="请输入区域名称"
          clearable
          @change="getTableData"
          prefix-icon="Search"
          @input="filterTree"
          @clear="filterTree"
        />
      </el-form-item>
      <el-form-item label="规格型号">
        <el-input
        <el-button type="primary" @click="openAreaDialog('addRoot')">新增区域</el-button>
      </div>
      <div class="tree-actions">
        <el-button link type="primary" @click="resetTreeSelection">全部区域</el-button>
      </div>
      <el-tree
        ref="treeRef"
        v-loading="treeLoading"
        :data="treeData"
        :props="treeProps"
        node-key="id"
        highlight-current
        default-expand-all
        :expand-on-click-node="false"
        :filter-node-method="filterTreeNode"
        class="ledger-tree"
        @node-click="handleTreeNodeClick"
      >
        <template #default="{ node, data }">
          <div class="tree-node">
            <span class="tree-node-content">
              <el-icon class="tree-node-icon">
                <component
                  :is="
                    data.children && data.children.length > 0
                      ? node.expanded
                        ? 'FolderOpened'
                        : 'Folder'
                      : 'Tickets'
                  "
                />
              </el-icon>
              <span class="tree-node-label">{{ data.areaName }}</span>
            </span>
            <div class="tree-node-actions">
              <el-button link type="primary" @click.stop="openAreaDialog('edit', data)">编辑</el-button>
              <el-button link type="primary" @click.stop="openAreaDialog('addChild', data)">新增</el-button>
              <el-button
                v-if="!hasChildren(data)"
                link
                type="danger"
                @click.stop="handleDeleteArea(data)"
              >
                åˆ é™¤
              </el-button>
            </div>
          </div>
        </template>
      </el-tree>
    </div>
    <div class="right-panel">
      <el-form :model="filters" :inline="true">
        <el-form-item label="设备名称">
          <el-input
            v-model="filters.deviceName"
            style="width: 200px"
            placeholder="请输入设备名称"
            clearable
            @change="getTableData"
          />
        </el-form-item>
        <el-form-item label="规格型号">
          <el-input
            v-model="filters.deviceModel"
            style="width: 200px"
            placeholder="请输入规格型号"
            clearable
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="供应商">
        <el-input
          />
        </el-form-item>
        <el-form-item label="供应商">
          <el-input
            v-model="filters.supplierName"
            style="width: 200px"
            placeholder="请输入供应商"
            clearable
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="录入日期:">
        <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                        placeholder="请选择" clearable @change="changeDaterange" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <div class="actions">
        <div></div>
        <div>
          <el-button type="primary" @click="add" icon="Plus"> æ–°å¢ž </el-button>
          <el-button type="info" @click="handleImport" icon="Upload">导入</el-button>
          <el-button @click="handleOut" icon="download">导出</el-button>
          <el-button
            type="danger"
            icon="Delete"
            :disabled="multipleList.length <= 0"
            @click="deleteRow(multipleList.map((item) => item.id))"
          >
            æ‰¹é‡åˆ é™¤
          </el-button>
          />
        </el-form-item>
        <el-form-item label="录入日期">
          <el-date-picker
            v-model="filters.entryDate"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            type="daterange"
            placeholder="请选择"
            clearable
            @change="changeDaterange"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="getTableData">搜索</el-button>
          <el-button @click="handleResetFilters">重置</el-button>
        </el-form-item>
      </el-form>
      <div class="table_list">
        <div class="actions">
          <div class="actions-tip">
            <span v-if="selectedAreaName">当前区域:{{ selectedAreaName }}</span>
          </div>
          <div>
            <el-button type="primary" icon="Plus" @click="add">新增</el-button>
            <el-button type="info" icon="Upload" @click="handleImport">导入</el-button>
            <el-button icon="download" @click="handleOut">导出</el-button>
            <el-button
              type="danger"
              icon="Delete"
              :disabled="multipleList.length <= 0"
              @click="deleteRow(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"
        />
      </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>
    </div>
    <Modal ref="modalRef" @success="getTableData"></Modal>
    <Modal ref="modalRef" @success="getTableData" />
    <el-dialog
      v-model="areaDialogVisible"
      :title="areaDialogTitle"
      width="480px"
      @close="closeAreaDialog"
    >
      <el-form ref="areaFormRef" :model="areaForm" :rules="areaRules" label-width="88px">
        <el-form-item label="区域名称" prop="areaName">
          <el-input v-model="areaForm.areaName" placeholder="请输入区域名称" />
        </el-form-item>
        <el-form-item label="排序" prop="sort">
          <el-input-number v-model="areaForm.sort" :min="0" :step="1" style="width: 100%" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input
            v-model="areaForm.remark"
            type="textarea"
            :rows="4"
            maxlength="200"
            show-word-limit
            placeholder="请输入备注"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitAreaForm">确定</el-button>
          <el-button @click="closeAreaDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog v-model="qrDialogVisible" title="二维码" width="300px" draggable>
      <div style="text-align:center;">
        <img :src="qrCodeUrl" alt="二维码" style="width:200px;height:200px;" />
        <div style="margin:10px 0;">
      <div class="qr-dialog">
        <img :src="qrCodeUrl" alt="二维码" class="qr-image" />
        <div class="qr-footer">
          <el-button type="primary" @click="downloadQRCode">下载二维码图片</el-button>
        </div>
      </div>
    </el-dialog>
    <!-- å¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
      <el-upload
        ref="uploadRef"
@@ -97,15 +203,22 @@
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">下载模板</el-link>
            <span>仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶ã€‚</span>
            <el-link
              type="primary"
              :underline="false"
              style="font-size: 12px; vertical-align: baseline; margin-left: 5px"
              @click="importTemplate"
            >
              ä¸‹è½½æ¨¡æ¿
            </el-link>
          </div>
        </template>
      </el-upload>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitFileForm">ç¡® å®š</el-button>
          <el-button @click="upload.open = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitFileForm">确定</el-button>
          <el-button @click="upload.open = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
@@ -114,8 +227,14 @@
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
// import { Search } from "@element-plus/icons-vue";
import { getLedgerPage, delLedger } from "@/api/equipmentManagement/ledger";
import {
  getDeviceAreaTree,
  getDeviceAreaDetail,
  addDeviceArea,
  updateDeviceArea,
  deleteDeviceArea,
} from "@/api/equipmentManagement/deviceArea";
import { onMounted, getCurrentInstance, ref, reactive } from "vue";
import Modal from "./Modal.vue";
import { ElMessageBox, ElMessage } from "element-plus";
@@ -128,28 +247,47 @@
  name: "设备台账",
});
// è¡¨æ ¼å¤šé€‰æ¡†é€‰ä¸­é¡¹
const multipleList = ref([]);
const { proxy } = getCurrentInstance();
const modalRef = ref();
const treeRef = ref();
const areaFormRef = ref();
const treeKeyword = ref("");
const treeLoading = ref(false);
const treeData = ref([]);
const selectedAreaName = ref("");
const areaDialogVisible = ref(false);
const areaDialogTitle = ref("新增区域");
const areaDialogMode = ref("addRoot");
const qrDialogVisible = ref(false);
const qrCodeUrl = ref("");
const qrRowData = ref(null);
const uploadRef = ref(null);
// å¯¼å…¥ç›¸å…³
const uploadRef = ref(null)
const treeProps = {
  children: "children",
  label: "areaName",
};
const upload = reactive({
  // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚
  open: false,
  // å¼¹å‡ºå±‚标题
  title: "",
  // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
  isUploading: false,
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import"
})
  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import",
});
const areaForm = reactive({
  id: undefined,
  areaName: "",
  parentId: undefined,
  sort: 0,
  remark: "",
});
const areaRules = {
  areaName: [{ required: true, message: "请输入区域名称", trigger: "blur" }],
};
const {
  filters,
@@ -157,7 +295,6 @@
  dataList,
  pagination,
  getTableData,
  resetFilters,
  onCurrentChange,
} = usePaginationApi(
  getLedgerPage,
@@ -165,10 +302,17 @@
    deviceName: undefined,
    deviceModel: undefined,
    supplierName: undefined,
    entryDate: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
    areaId: undefined,
    areaName: undefined,
  },
  [
    {
      label: "所在区域",
      prop: "areaName",
    },
    {
      label: "设备名称",
      prop: "deviceName",
@@ -205,39 +349,151 @@
      label: "录入日期",
      prop: "createTime",
      formatData: (v) => {
        if (!v) return '';
        // å¦‚果包含时分秒,只取日期部分
        if (v.includes(' ')) {
          return v.split(' ')[0];
        }
        return v;
        if (!v) return "";
        return v.includes(" ") ? v.split(" ")[0] : v;
      },
    },
        {
            dataType: "action",
            label: "操作",
            align: "center",
            fixed: 'right',
            width: 150,
            operation: [
                {
                    name: "编辑",
                    clickFun: (row) => {
                        edit(row.id)
                    },
                },
                {
                    name: "生成二维码",
                    clickFun: (row) => {
                        showQRCode(row)
                    },
                },
            ],
        },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 150,
      operation: [
        {
          name: "编辑",
          clickFun: (row) => {
            edit(row.id);
          },
        },
        {
          name: "生成二维码",
          clickFun: (row) => {
            showQRCode(row);
          },
        },
      ],
    },
  ]
);
// å¤šé€‰åŽåšä»€ä¹ˆ
const loadTreeData = async () => {
  treeLoading.value = true;
  try {
    const res = await getDeviceAreaTree();
    treeData.value = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
  } finally {
    treeLoading.value = false;
  }
};
const resetAreaForm = () => {
  areaForm.id = undefined;
  areaForm.areaName = "";
  areaForm.parentId = undefined;
  areaForm.sort = 0;
  areaForm.remark = "";
};
const filterTree = () => {
  treeRef.value?.filter(treeKeyword.value);
};
const filterTreeNode = (value, data) => {
  if (!value) {
    return true;
  }
  return String(data.areaName || "").includes(value);
};
const handleTreeNodeClick = (data) => {
  filters.areaId = data.id;
  filters.areaName = data.areaName;
  selectedAreaName.value = data.areaName || "";
  getTableData();
};
const openAreaDialog = async (mode, row) => {
  areaDialogMode.value = mode;
  areaDialogTitle.value =
    mode === "edit" ? "编辑区域" : mode === "addChild" ? "新增子区域" : "新增区域";
  resetAreaForm();
  areaDialogVisible.value = true;
  if (mode === "addChild") {
    areaForm.parentId = row.id;
    areaForm.sort = 0;
    return;
  }
  if (mode === "edit" && row?.id) {
    const res = await getDeviceAreaDetail(row.id);
    const detail = res?.data || {};
    areaForm.id = detail.id;
    areaForm.areaName = detail.areaName || "";
    areaForm.parentId = detail.parentId;
    areaForm.sort = detail.sort ?? 0;
    areaForm.remark = detail.remark || "";
  }
};
const closeAreaDialog = () => {
  areaDialogVisible.value = false;
  areaFormRef.value?.resetFields();
  resetAreaForm();
};
const submitAreaForm = () => {
  areaFormRef.value?.validate(async (valid) => {
    if (!valid) {
      return;
    }
    const submitData = {
      id: areaForm.id,
      areaName: areaForm.areaName,
      parentId: areaForm.parentId,
      sort: areaForm.sort,
      remark: areaForm.remark,
    };
    const request = areaDialogMode.value === "edit" ? updateDeviceArea : addDeviceArea;
    const { code } = await request(submitData);
    if (code === 200) {
      ElMessage.success(areaDialogMode.value === "edit" ? "修改成功" : "新增成功");
      closeAreaDialog();
      await loadTreeData();
    }
  });
};
const handleDeleteArea = (row) => {
  if (hasChildren(row)) {
    ElMessage.warning("当前区域存在下级区域,不能删除");
    return;
  }
  ElMessageBox.confirm("此操作将删除该设备区域,是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    const { code } = await deleteDeviceArea([row.id]);
    if (code === 200) {
      ElMessage.success("删除成功");
      if (filters.areaId === row.id) {
        resetTreeSelection();
      }
      await loadTreeData();
    }
  });
};
const hasChildren = (row) => Array.isArray(row?.children) && row.children.length > 0;
const resetTreeSelection = () => {
  treeRef.value?.setCurrentKey(null);
  selectedAreaName.value = "";
  filters.areaId = undefined;
  filters.areaName = undefined;
  getTableData();
};
const handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
@@ -245,16 +501,19 @@
const add = () => {
  modalRef.value.openModal();
};
const edit = (id) => {
  modalRef.value.loadForm(id);
};
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
    pagination.pageSize = limit;
  pagination.pageSize = limit;
  onCurrentChange(page);
};
const deleteRow = (id) => {
  ElMessageBox.confirm("此操作将永久删除该文件, æ˜¯å¦ç»§ç»­?", "提示", {
  ElMessageBox.confirm("此操作将永久删除该数据,是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
@@ -281,14 +540,24 @@
  getTableData();
};
const handleResetFilters = () => {
  filters.deviceName = undefined;
  filters.deviceModel = undefined;
  filters.supplierName = undefined;
  filters.entryDate = undefined;
  filters.entryDateStart = undefined;
  filters.entryDateEnd = undefined;
  getTableData();
};
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
  ElMessageBox.confirm("当前查询结果将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download(`/device/ledger/export`, {}, "设备台账档案.xlsx");
      proxy.download("/device/ledger/export", {}, "设备台账档案.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
@@ -296,8 +565,7 @@
};
const showQRCode = async (row) => {
  // ç›´æŽ¥ä½¿ç”¨URL,不要用JSON.stringify包装
  const qrContent = proxy.javaApi + '/device-info?deviceId=' + row.id;
  const qrContent = proxy.javaApi + "/device-info?deviceId=" + row.id;
  qrCodeUrl.value = await QRCode.toDataURL(qrContent);
  qrRowData.value = row;
  qrDialogVisible.value = true;
@@ -310,48 +578,141 @@
  a.click();
};
// å¯¼å…¥æŒ‰é’®æ“ä½œ
const handleImport = () => {
  upload.title = "设备台账导入"
  upload.open = true
}
  upload.title = "设备台账导入";
  upload.open = true;
};
// ä¸‹è½½æ¨¡æ¿æ“ä½œ
const importTemplate = () => {
  proxy.download("/device/ledger/downloadTemplate", {}, `设备台账导入模板_${new Date().getTime()}.xlsx`)
}
  proxy.download("/device/ledger/downloadTemplate", {}, `设备台账导入模板_${new Date().getTime()}.xlsx`);
};
// æ–‡ä»¶ä¸Šä¼ ä¸­å¤„理
const handleFileUploadProgress = (event, file, fileList) => {
  upload.isUploading = true
}
const handleFileUploadProgress = () => {
  upload.isUploading = true;
};
// æ–‡ä»¶ä¸Šä¼ æˆåŠŸå¤„ç†
const handleFileSuccess = (response, file, fileList) => {
  upload.open = false
  upload.isUploading = false
  proxy.$refs["uploadRef"].handleRemove(file)
  proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
  getTableData()
}
const handleFileSuccess = (response, file) => {
  upload.open = false;
  upload.isUploading = false;
  uploadRef.value?.handleRemove(file);
  proxy.$alert(
    "<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" +
      response.msg +
      "</div>",
    "导入结果",
    { dangerouslyUseHTMLString: true }
  );
  getTableData();
};
// æäº¤ä¸Šä¼ æ–‡ä»¶
const submitFileForm = () => {
  proxy.$refs["uploadRef"].submit()
}
  uploadRef.value?.submit();
};
onMounted(() => {
onMounted(async () => {
  await loadTreeData();
  getTableData();
});
</script>
<style lang="scss" scoped>
.table_list {
  margin-top: unset;
.ledger-view {
  display: flex;
  gap: 20px;
}
.left-panel {
  width: 320px;
  min-width: 320px;
  padding: 16px;
  background: #fff;
  border-radius: 4px;
}
.right-panel {
  flex: 1;
  min-width: 0;
  padding: 16px;
  background: #fff;
  border-radius: 4px;
}
.tree-toolbar {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 8px;
}
.tree-actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 8px;
}
.ledger-tree {
  height: calc(100vh - 230px);
  overflow-y: auto;
}
.tree-node {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.tree-node-content {
  display: flex;
  align-items: center;
  min-width: 0;
}
.tree-node-icon {
  color: #e6a23c;
  margin-right: 8px;
  font-size: 18px;
}
.tree-node-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tree-node-actions {
  flex-shrink: 0;
}
.table_list {
  margin-top: 0;
}
.actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
  gap: 12px;
}
.actions-tip {
  color: #606266;
  font-size: 14px;
}
.qr-dialog {
  text-align: center;
}
.qr-image {
  width: 200px;
  height: 200px;
}
.qr-footer {
  margin: 10px 0;
}
</style>
src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -10,14 +10,41 @@
    <el-form :model="form" label-width="100px">
      <el-row>
        <el-col :span="12">
          <el-form-item label="所属区域">
            <el-tree-select
              v-model="form.areaId"
              :data="areaOptions"
              :props="areaTreeProps"
              node-key="id"
              value-key="id"
              check-strictly
              clearable
              filterable
              placeholder="请选择所属区域"
              style="width: 100%"
              @change="handleAreaChange"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="设备名称">
            <el-select v-model="form.deviceLedgerId" @change="setDeviceModel" filterable>
            <el-select
              v-model="form.deviceLedgerIds"
              filterable
              clearable
              multiple
              collapse-tags
              collapse-tags-tooltip
              placeholder="请先选择区域,再选择设备"
              style="width: 100%"
              @change="setDeviceModels"
            >
              <el-option
                v-for="(item, index) in deviceOptions"
                :key="index"
                v-for="item in deviceOptions"
                :key="item.id"
                :label="item.deviceName"
                :value="item.id"
              ></el-option>
              />
            </el-select>
          </el-form-item>
        </el-col>
@@ -25,7 +52,7 @@
          <el-form-item label="规格型号">
            <el-input
              v-model="form.deviceModel"
              placeholder="请输入规格型号"
              placeholder="自动带出规格型号"
              disabled
            />
          </el-form-item>
@@ -53,9 +80,9 @@
        <el-col :span="12">
          <el-form-item label="报修状态">
            <el-select v-model="form.status">
              <el-option label="待维修" :value="0"></el-option>
              <el-option label="完结" :value="1"></el-option>
              <el-option label="失败" :value="2"></el-option>
              <el-option label="待维修" :value="0" />
              <el-option label="完结" :value="1" />
              <el-option label="失败" :value="2" />
            </el-select>
          </el-form-item>
        </el-col>
@@ -77,17 +104,21 @@
</template>
<script setup>
import { nextTick, ref, unref } from "vue";
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import useFormData from "@/hooks/useFormData";
import useUserStore from "@/store/modules/user";
import {
  addRepair,
  editRepair,
  getRepairById,
} from "@/api/equipmentManagement/repair";
import { ElMessage } from "element-plus";
import dayjs from "dayjs";
import useFormData from "@/hooks/useFormData";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import useUserStore from "@/store/modules/user";
import {
  getDeviceAreaTree,
  getDeviceAreaTreeWithDevices,
} from "@/api/equipmentManagement/deviceArea";
defineOptions({
  name: "设备报修弹窗",
@@ -98,32 +129,140 @@
const id = ref();
const visible = ref(false);
const loading = ref(false);
const userStore = useUserStore();
const areaOptions = ref([]);
const deviceOptions = ref([]);
const loadDeviceName = async () => {
  const { data } = await getDeviceLedger();
  deviceOptions.value = data;
const areaTreeProps = {
  label: "areaName",
  children: "children",
};
const { form, resetForm } = useFormData({
  deviceLedgerId: undefined, // è®¾å¤‡Id
  deviceName: undefined, // è®¾å¤‡åç§°
  deviceModel: undefined, // è§„格型号
  repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸï¼Œé»˜è®¤å½“天
  repairName: userStore.nickName, // æŠ¥ä¿®äºº
  remark: undefined, // æ•…障现象
  status: 0, // æŠ¥ä¿®çŠ¶æ€
  areaId: undefined,
  deviceLedgerId: undefined,
  deviceLedgerIds: [],
  deviceLedgerIdsStr: undefined,
  deviceName: undefined,
  deviceModel: undefined,
  repairTime: dayjs().format("YYYY-MM-DD"),
  repairName: userStore.nickName,
  remark: undefined,
  status: 0,
});
const setDeviceModel = (deviceId) => {
  const option = deviceOptions.value.find((item) => item.id === deviceId);
  form.deviceModel = option.deviceModel;
const loadAreaTree = async () => {
  const { data } = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(data) ? data : [];
};
const normalizeIdList = (value) => {
  if (Array.isArray(value)) {
    return value
      .map((item) => Number(item))
      .filter((item) => Number.isFinite(item));
  }
  if (typeof value === "string") {
    return value
      .split(",")
      .map((item) => Number(item.trim()))
      .filter((item) => Number.isFinite(item));
  }
  if (value !== undefined && value !== null && value !== "") {
    const numericValue = Number(value);
    return Number.isFinite(numericValue) ? [numericValue] : [];
  }
  return [];
};
const getNodeDevices = (node) => {
  const candidates = [
    node?.deviceList,
    node?.devices,
    node?.deviceLedgerList,
    node?.deviceLedgers,
    node?.ledgerList,
    node?.ledgers,
  ];
  return candidates.find((item) => Array.isArray(item)) || [];
};
const normalizeDevice = (item) => ({
  ...item,
  id: item.id ?? item.deviceLedgerId,
  deviceName: item.deviceName ?? item.name,
  deviceModel: item.deviceModel ?? item.model,
});
const collectDevices = (node) => {
  const currentDevices = getNodeDevices(node).map(normalizeDevice);
  const childDevices = (node?.children || []).flatMap((child) =>
    collectDevices(child)
  );
  const deviceMap = new Map();
  [...currentDevices, ...childDevices].forEach((item) => {
    if (item?.id !== undefined && item?.id !== null) {
      deviceMap.set(Number(item.id), item);
    }
  });
  return Array.from(deviceMap.values());
};
const findAreaNode = (nodes, areaId) => {
  for (const node of nodes || []) {
    if (Number(node.id) === Number(areaId)) {
      return node;
    }
    const target = findAreaNode(node.children, areaId);
    if (target) {
      return target;
    }
  }
  return null;
};
const loadDevicesByArea = async (areaId) => {
  if (!areaId) {
    deviceOptions.value = [];
    return;
  }
  const { data } = await getDeviceAreaTreeWithDevices();
  const treeData = Array.isArray(data) ? data : [];
  const currentNode = findAreaNode(treeData, areaId);
  deviceOptions.value = currentNode ? collectDevices(currentNode) : [];
};
const syncDeviceFields = (deviceIds) => {
  const selectedIds = normalizeIdList(deviceIds);
  const selectedDevices = selectedIds
    .map((deviceId) =>
      deviceOptions.value.find((item) => Number(item.id) === Number(deviceId))
    )
    .filter(Boolean);
  form.deviceLedgerIds = selectedIds;
  form.deviceLedgerId = selectedIds[0];
  form.deviceLedgerIdsStr = selectedIds.join(",");
  form.deviceName = selectedDevices
    .map((item) => item.deviceName)
    .filter(Boolean)
    .join(",");
  form.deviceModel = selectedDevices
    .map((item) => item.deviceModel || "-")
    .join(",");
};
const setDeviceModels = (deviceIds) => {
  syncDeviceFields(deviceIds);
};
const setForm = (data) => {
  form.deviceLedgerId = data.deviceLedgerId;
  form.areaId = data.areaId;
  form.deviceLedgerIds = normalizeIdList(
    data.deviceLedgerIds ?? data.deviceLedgerIdsStr ?? data.deviceLedgerId
  );
  form.deviceLedgerId = form.deviceLedgerIds[0];
  form.deviceLedgerIdsStr =
    data.deviceLedgerIdsStr ?? form.deviceLedgerIds.join(",");
  form.deviceName = data.deviceName;
  form.deviceModel = data.deviceModel;
  form.repairTime = data.repairTime;
@@ -132,13 +271,30 @@
  form.status = data.status;
};
const handleAreaChange = async (areaId) => {
  form.deviceLedgerId = undefined;
  form.deviceLedgerIds = [];
  form.deviceLedgerIdsStr = undefined;
  form.deviceName = undefined;
  form.deviceModel = undefined;
  await loadDevicesByArea(areaId);
};
const sendForm = async () => {
  loading.value = true;
  try {
    syncDeviceFields(form.deviceLedgerIds);
    const payload = {
      ...form,
      deviceLedgerId: form.deviceLedgerIds[0],
      deviceLedgerIds: [...form.deviceLedgerIds],
      deviceLedgerIdsStr: form.deviceLedgerIds.join(","),
      deviceModel: form.deviceModel || "-",
    };
    const { code } = id.value
      ? await editRepair({ id: unref(id), ...form })
      : await addRepair(form);
    if (code == 200) {
      ? await editRepair({ id: unref(id), ...payload })
      : await addRepair(payload);
    if (code === 200) {
      ElMessage.success(`${id.value ? "编辑" : "新增"}报修成功`);
      visible.value = false;
      emits("ok");
@@ -162,7 +318,8 @@
  id.value = undefined;
  visible.value = true;
  await nextTick();
  await loadDeviceName();
  await loadAreaTree();
  deviceOptions.value = [];
};
const openEdit = async (editId) => {
@@ -170,8 +327,10 @@
  id.value = editId;
  visible.value = true;
  await nextTick();
  await loadDeviceName();
  await loadAreaTree();
  setForm(data);
  await loadDevicesByArea(form.areaId);
  syncDeviceFields(form.deviceLedgerIds);
};
defineExpose({
src/views/equipmentManagement/repair/index.vue
@@ -7,7 +7,6 @@
            style="width: 240px"
            placeholder="请输入设备名称"
            clearable
            :prefix-icon="Search"
            @change="getTableData"
        />
      </el-form-item>
@@ -17,7 +16,6 @@
            style="width: 240px"
            placeholder="请选择规格型号"
            clearable
            :prefix-icon="Search"
            @change="getTableData"
        />
      </el-form-item>
@@ -27,7 +25,6 @@
            style="width: 240px"
            placeholder="请输入故障现象"
            clearable
            :prefix-icon="Search"
            @change="getTableData"
        />
      </el-form-item>
@@ -37,7 +34,6 @@
            style="width: 240px"
            placeholder="请输入维修人"
            clearable
            :prefix-icon="Search"
            @change="getTableData"
        />
      </el-form-item>
@@ -177,6 +173,10 @@
      maintenanceTimeStr: undefined,
    },
    [
            {
                label: "所在区域",
                prop: "areaName",
            },
      {
        label: "设备名称",
        align: "center",
src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -8,27 +8,45 @@
    @close="handleClose"
  >
    <el-form :model="form" label-width="100px">
      <el-form-item label="所属区域">
        <el-tree-select
          v-model="form.areaId"
          :data="areaOptions"
          :props="areaTreeProps"
          node-key="id"
          value-key="id"
          check-strictly
          clearable
          filterable
          placeholder="请选择所属区域"
          style="width: 100%"
          @change="handleAreaChange"
        />
      </el-form-item>
      <el-form-item label="设备名称">
        <el-select
          v-model="form.deviceLedgerId"
          @change="setDeviceModel"
          placeholder="请选择设备"
          v-model="form.deviceLedgerIds"
          filterable
          default-first-option
          :reserve-keyword="false"
          clearable
          multiple
          collapse-tags
          collapse-tags-tooltip
          placeholder="请选择设备"
          style="width: 100%"
          @change="setDeviceModels"
        >
          <el-option
            v-for="(item, index) in deviceOptions"
            :key="index"
            v-for="item in deviceOptions"
            :key="item.id"
            :label="item.deviceName"
            :value="item.id"
          ></el-option>
          />
        </el-select>
      </el-form-item>
      <el-form-item label="规格型号">
        <el-input
          v-model="form.deviceModel"
          placeholder="请输入规格型号"
          placeholder="自动带出规格型号"
          disabled
        />
      </el-form-item>
@@ -51,19 +69,19 @@
      </el-form-item>
      <el-form-item v-if="id" 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 label="完结" :value="1" />
          <el-option label="失败" :value="2" />
        </el-select>
      </el-form-item>
      <el-form-item label="计划保养日期">
        <el-date-picker
          style="width: 100%"
          v-model="form.maintenancePlanTime"
          style="width: 100%"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="date"
          placeholder="请选择计划保养日期日期"
          placeholder="请选择计划保养日期"
          clearable
        />
      </el-form-item>
@@ -72,18 +90,21 @@
</template>
<script setup>
import { nextTick, onMounted, ref, unref } from "vue";
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import useFormData from "@/hooks/useFormData";
import { userListNoPage } from "@/api/system/user.js";
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 dayjs from "dayjs";
import { userListNoPage } from "@/api/system/user.js";
import {
  getDeviceAreaTree,
  getDeviceAreaTreeWithDevices,
} from "@/api/equipmentManagement/deviceArea";
defineOptions({
  name: "设备保养新增计划",
@@ -94,47 +115,159 @@
const id = ref();
const visible = ref(false);
const loading = ref(false);
const areaOptions = ref([]);
const deviceOptions = ref([]);
const loadDeviceName = async () => {
  const { data } = await getDeviceLedger();
  deviceOptions.value = data;
const userList = ref([]);
const areaTreeProps = {
  label: "areaName",
  children: "children",
};
const { form, resetForm } = useFormData({
  deviceLedgerId: undefined, // è®¾å¤‡Id
  deviceName: undefined, // è®¾å¤‡åç§°
  deviceModel: undefined, // è§„格型号
  maintenancePlanTime: undefined, // è®¡åˆ’保养日期
  createUser: undefined, // å½•入人
  status: 0, //保修状态
  areaId: undefined,
  deviceLedgerId: undefined,
  deviceLedgerIds: [],
  deviceLedgerIdsStr: undefined,
  deviceName: undefined,
  deviceModel: undefined,
  maintenancePlanTime: undefined,
  createUser: undefined,
  status: 0,
});
const setDeviceModel = (deviceId) => {
  const option = deviceOptions.value.find((item) => item.id === deviceId);
  form.deviceModel = option.deviceModel;
const loadAreaTree = async () => {
  const { data } = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(data) ? data : [];
};
/**
 * @desc è®¾ç½®è¡¨å•内容
 * @param data è®¾å¤‡ä¿¡æ¯
 */
const normalizeIdList = (value) => {
  if (Array.isArray(value)) {
    return value
      .map((item) => Number(item))
      .filter((item) => Number.isFinite(item));
  }
  if (typeof value === "string") {
    return value
      .split(",")
      .map((item) => Number(item.trim()))
      .filter((item) => Number.isFinite(item));
  }
  if (value !== undefined && value !== null && value !== "") {
    const numericValue = Number(value);
    return Number.isFinite(numericValue) ? [numericValue] : [];
  }
  return [];
};
const getNodeDevices = (node) => {
  const candidates = [
    node?.deviceList,
    node?.devices,
    node?.deviceLedgerList,
    node?.deviceLedgers,
    node?.ledgerList,
    node?.ledgers,
  ];
  return candidates.find((item) => Array.isArray(item)) || [];
};
const normalizeDevice = (item) => ({
  ...item,
  id: item.id ?? item.deviceLedgerId,
  deviceName: item.deviceName ?? item.name,
  deviceModel: item.deviceModel ?? item.model,
});
const collectDevices = (node) => {
  const currentDevices = getNodeDevices(node).map(normalizeDevice);
  const childDevices = (node?.children || []).flatMap((child) =>
    collectDevices(child)
  );
  const deviceMap = new Map();
  [...currentDevices, ...childDevices].forEach((item) => {
    if (item?.id !== undefined && item?.id !== null) {
      deviceMap.set(Number(item.id), item);
    }
  });
  return Array.from(deviceMap.values());
};
const findAreaNode = (nodes, areaId) => {
  for (const node of nodes || []) {
    if (Number(node.id) === Number(areaId)) {
      return node;
    }
    const target = findAreaNode(node.children, areaId);
    if (target) {
      return target;
    }
  }
  return null;
};
const loadDevicesByArea = async (areaId) => {
  if (!areaId) {
    deviceOptions.value = [];
    return;
  }
  const { data } = await getDeviceAreaTreeWithDevices();
  const treeData = Array.isArray(data) ? data : [];
  const currentNode = findAreaNode(treeData, areaId);
  deviceOptions.value = currentNode ? collectDevices(currentNode) : [];
};
const syncDeviceFields = (deviceIds) => {
  const selectedIds = normalizeIdList(deviceIds);
  const selectedDevices = selectedIds
    .map((deviceId) =>
      deviceOptions.value.find((item) => Number(item.id) === Number(deviceId))
    )
    .filter(Boolean);
  form.deviceLedgerIds = selectedIds;
  form.deviceLedgerId = selectedIds[0];
  form.deviceLedgerIdsStr = selectedIds.join(",");
  form.deviceName = selectedDevices
    .map((item) => item.deviceName)
    .filter(Boolean)
    .join(",");
  form.deviceModel = selectedDevices
    .map((item) => item.deviceModel || "-")
    .join(",");
};
const setDeviceModels = (deviceIds) => {
  syncDeviceFields(deviceIds);
};
const handleAreaChange = async (areaId) => {
  form.deviceLedgerId = undefined;
  form.deviceLedgerIds = [];
  form.deviceLedgerIdsStr = undefined;
  form.deviceName = undefined;
  form.deviceModel = undefined;
  await loadDevicesByArea(areaId);
};
const setForm = (data) => {
  form.deviceLedgerId = data.deviceLedgerId;
  form.areaId = data.areaId;
  form.deviceLedgerIds = normalizeIdList(
    data.deviceLedgerIds ?? data.deviceLedgerIdsStr ?? data.deviceLedgerId
  );
  form.deviceLedgerId = form.deviceLedgerIds[0];
  form.deviceLedgerIdsStr =
    data.deviceLedgerIdsStr ?? form.deviceLedgerIds.join(",");
  form.deviceName = data.deviceName;
  form.deviceModel = data.deviceModel;
  form.createUser = Number(data.createUser);
  form.status = data.status;
  form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
    "YYYY-MM-DD HH:mm:ss"
  );
  form.maintenancePlanTime = data.maintenancePlanTime
    ? dayjs(data.maintenancePlanTime).format("YYYY-MM-DD HH:mm:ss")
    : undefined;
};
// ç”¨æˆ·åˆ—表
const userList = ref([]);
onMounted(() => {
  loadDeviceName();
  loadAreaTree();
  userListNoPage().then((res) => {
    userList.value = res.data;
  });
@@ -145,16 +278,27 @@
  id.value = editId;
  visible.value = true;
  await nextTick();
  await loadAreaTree();
  setForm(data);
  await loadDevicesByArea(form.areaId);
  syncDeviceFields(form.deviceLedgerIds);
};
const sendForm = async () => {
  loading.value = true;
  try {
    syncDeviceFields(form.deviceLedgerIds);
    const payload = {
      ...form,
      deviceLedgerId: form.deviceLedgerIds[0],
      deviceLedgerIds: [...form.deviceLedgerIds],
      deviceLedgerIdsStr: form.deviceLedgerIds.join(","),
      deviceModel: form.deviceModel || "-",
    };
    const { code } = id.value
      ? await editUpkeep({ id: unref(id), ...form })
      : await addUpkeep(form);
    if (code == 200) {
      ? await editUpkeep({ id: unref(id), ...payload })
      : await addUpkeep(payload);
    if (code === 200) {
      ElMessage.success(`${id.value ? "编辑" : "新增"}计划成功`);
      visible.value = false;
      emits("ok");
@@ -174,9 +318,12 @@
  visible.value = false;
};
const openModal = () => {
const openModal = async () => {
  id.value = undefined;
  visible.value = true;
  await nextTick();
  await loadAreaTree();
  deviceOptions.value = [];
};
defineExpose({
src/views/equipmentManagement/upkeep/Form/formDia.vue
@@ -1,304 +1,476 @@
<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="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="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="areaId">
            <el-tree-select
              v-model="form.areaId"
              :data="areaOptions"
              :props="areaTreeProps"
              node-key="id"
              value-key="id"
              check-strictly
              clearable
              filterable
              placeholder="请选择所属区域"
              style="width: 100%"
              @change="handleAreaChange"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="设备名称" prop="deviceLedgerIds">
            <el-select
              v-model="form.deviceLedgerIds"
              filterable
              clearable
              multiple
              collapse-tags
              collapse-tags-tooltip
              placeholder="请选择设备"
              style="width: 100%"
              @change="setDeviceModels"
            >
              <el-option
                v-for="item in deviceOptions"
                :key="item.id"
                :label="item.deviceName"
                :value="item.id"
              />
            </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"
                :key="item.userId"
                :label="item.nickName"
                :value="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="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'">
          <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'">
          <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'">
          <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'">
          <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="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>
</template>
<script setup>
import { getCurrentInstance, reactive, ref, toRefs } from "vue";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { 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 { userListNoPageByTenantId } from "@/api/system/user.js";
import {
  getDeviceAreaTree,
  getDeviceAreaTreeWithDevices,
} from "@/api/equipmentManagement/deviceArea";
import {
  deviceMaintenanceTaskAdd,
  deviceMaintenanceTaskEdit,
} from "@/api/equipmentManagement/upkeep";
import { getCurrentDate } from "@/utils/index.js";
import useUserStore from "@/store/modules/user.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const { proxy } = getCurrentInstance();
const emit = defineEmits(["closeDia"]);
const dialogVisitable = ref(false);
const operationType = ref('add');
const operationType = ref("add");
const areaOptions = ref([]);
const deviceOptions = ref([]);
const userList = ref([]);
const areaTreeProps = {
  label: "areaName",
  children: "children",
};
const userStore = useUserStore();
const data = reactive({
    form: {
        taskId: undefined,
        taskName: undefined,
        // å½•入人:单选一个用户 id
        inspector: undefined,
        remarks: '',
        frequencyType: '',
        frequencyDetail: '',
        week: '',
        time: '',
        deviceModel: undefined, // è§„格型号
        registrationDate: ''
    },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请选择录入人", trigger: "blur" },],
        registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }]
    }
})
const { form, rules } = toRefs(data)
const userList = ref([])
const loadDeviceName = async () => {
    const { data } = await getDeviceLedger();
    deviceOptions.value = data;
const data = reactive({
  form: {
    areaId: undefined,
    taskId: undefined,
    taskIds: [],
    taskIdsStr: undefined,
    deviceLedgerIds: [],
    deviceLedgerIdsStr: undefined,
    taskName: undefined,
    inspector: undefined,
    remarks: "",
    frequencyType: "",
    frequencyDetail: "",
    week: "",
    time: "",
    deviceModel: undefined,
    registrationDate: "",
  },
  rules: {
    areaId: [{ required: true, message: "请选择所属区域", trigger: "change" }],
    deviceLedgerIds: [{ required: true, message: "请选择设备", trigger: "change" }],
    inspector: [{ required: true, message: "请选择录入人", trigger: "change" }],
    registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }],
  },
});
const { form, rules } = toRefs(data);
const loadAreaTree = async () => {
  const { data } = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(data) ? 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 normalizeIdList = (value) => {
  if (Array.isArray(value)) {
    return value
      .map((item) => Number(item))
      .filter((item) => Number.isFinite(item));
  }
  if (typeof value === "string") {
    return value
      .split(",")
      .map((item) => Number(item.trim()))
      .filter((item) => Number.isFinite(item));
  }
  if (value !== undefined && value !== null && value !== "") {
    const numericValue = Number(value);
    return Number.isFinite(numericValue) ? [numericValue] : [];
  }
  return [];
};
// æ‰“开弹框
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
        }
const getNodeDevices = (node) => {
  const candidates = [
    node?.deviceList,
    node?.devices,
    node?.deviceLedgerList,
    node?.deviceLedgers,
    node?.ledgerList,
    node?.ledgers,
  ];
  return candidates.find((item) => Array.isArray(item)) || [];
};
        // å¦‚果有设备ID,自动设置设备信息
        if (form.value.taskId) {
            setDeviceModel(form.value.taskId);
        }
    } else if (type === 'add') {
        // æ–°å¢žæ—¶è®¾ç½®ç™»è®°æ—¥æœŸä¸ºå½“天
        form.value.registrationDate = getCurrentDate();
        // æ–°å¢žæ—¶è®¾ç½®å½•入人为当前登录账户
        form.value.inspector = userStore.id;
    }
}
const normalizeDevice = (item) => ({
  ...item,
  id: item.id ?? item.deviceLedgerId,
  deviceName: item.deviceName ?? item.name,
  deviceModel: item.deviceModel ?? item.model,
});
// å…³é—­å¯¹è¯æ¡†
const cancel = () => {
    resetForm()
    dialogVisitable.value = false
    emit('closeDia')
}
const collectDevices = (node) => {
  const currentDevices = getNodeDevices(node).map(normalizeDevice);
  const childDevices = (node?.children || []).flatMap((child) =>
    collectDevices(child)
  );
  const deviceMap = new Map();
  [...currentDevices, ...childDevices].forEach((item) => {
    if (item?.id !== undefined && item?.id !== null) {
      deviceMap.set(Number(item.id), item);
    }
  });
  return Array.from(deviceMap.values());
};
// é‡ç½®è¡¨å•函数
const findAreaNode = (nodes, areaId) => {
  for (const node of nodes || []) {
    if (Number(node.id) === Number(areaId)) {
      return node;
    }
    const target = findAreaNode(node.children, areaId);
    if (target) {
      return target;
    }
  }
  return null;
};
const loadDevicesByArea = async (areaId) => {
  if (!areaId) {
    deviceOptions.value = [];
    return;
  }
  const { data } = await getDeviceAreaTreeWithDevices();
  const treeData = Array.isArray(data) ? data : [];
  const currentNode = findAreaNode(treeData, areaId);
  deviceOptions.value = currentNode ? collectDevices(currentNode) : [];
};
const syncDeviceFields = (deviceIds) => {
  const selectedIds = normalizeIdList(deviceIds);
  const selectedDevices = selectedIds
    .map((deviceId) =>
      deviceOptions.value.find((item) => Number(item.id) === Number(deviceId))
    )
    .filter(Boolean);
  form.value.deviceLedgerIds = selectedIds;
  form.value.deviceLedgerIdsStr = selectedIds.join(",");
  form.value.taskIds = [...selectedIds];
  form.value.taskIdsStr = selectedIds.join(",");
  form.value.taskId = selectedIds[0];
  form.value.taskName = selectedDevices
    .map((item) => item.deviceName)
    .filter(Boolean)
    .join(",");
  form.value.deviceModel = selectedDevices
    .map((item) => item.deviceModel || "-")
    .join(",");
};
const setDeviceModels = (deviceIds) => {
  syncDeviceFields(deviceIds);
};
const handleAreaChange = async (areaId) => {
  form.value.taskId = undefined;
  form.value.taskIds = [];
  form.value.taskIdsStr = undefined;
  form.value.deviceLedgerIds = [];
  form.value.deviceLedgerIdsStr = undefined;
  form.value.taskName = undefined;
  form.value.deviceModel = undefined;
  await loadDevicesByArea(areaId);
};
const resetForm = () => {
    if (proxy.$refs.formRef) {
        proxy.$refs.formRef.resetFields()
    }
    // é‡ç½®è¡¨å•数据确保设备信息正确重置
    form.value = {
        taskId: undefined,
        taskName: undefined,
        inspector: undefined,
        inspector: undefined,
        remarks: '',
        frequencyType: '',
        frequencyDetail: '',
        week: '',
        time: '',
        deviceModel: undefined,
        registrationDate: ''
    }
}
  if (proxy.$refs.formRef) {
    proxy.$refs.formRef.resetFields();
  }
  form.value = {
    areaId: undefined,
    taskId: undefined,
    taskIds: [],
    taskIdsStr: undefined,
    deviceLedgerIds: [],
    deviceLedgerIdsStr: undefined,
    taskName: undefined,
    inspector: undefined,
    remarks: "",
    frequencyType: "",
    frequencyDetail: "",
    week: "",
    time: "",
    deviceModel: undefined,
    registrationDate: "",
  };
};
// æäº¤è¡¨å•
const openDialog = async (type, row) => {
  dialogVisitable.value = true;
  operationType.value = type;
  resetForm();
  userListNoPageByTenantId().then((res) => {
    userList.value = res.data;
  });
  await loadAreaTree();
  if (type === "edit" && row) {
    form.value = {
      ...form.value,
      ...row,
      inspector: row.registrantId || row.inspector,
    };
    form.value.deviceLedgerIds = normalizeIdList(
      row.deviceLedgerIds ??
        row.deviceLedgerIdsStr ??
        row.taskIds ??
        row.taskIdsStr ??
        row.taskId
    );
    form.value.deviceLedgerIdsStr = form.value.deviceLedgerIds.join(",");
    form.value.taskIds = [...form.value.deviceLedgerIds];
    form.value.taskIdsStr = form.value.deviceLedgerIds.join(",");
    form.value.taskId = form.value.deviceLedgerIds[0];
    if (form.value.areaId) {
      await loadDevicesByArea(form.value.areaId);
      syncDeviceFields(form.value.deviceLedgerIds);
    }
  } else {
    form.value.registrationDate = getCurrentDate();
    form.value.inspector = userStore.id;
    deviceOptions.value = [];
  }
};
const cancel = () => {
  resetForm();
  dialogVisitable.value = false;
  emit("closeDia");
};
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
                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.active = true
                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 })
  proxy.$refs.formRef.validate(async (valid) => {
    if (!valid) {
      return;
    }
    try {
      syncDeviceFields(form.value.deviceLedgerIds);
      const payload = { ...form.value };
      if (payload.inspector) {
        const selectedUser = userList.value.find(
          (item) => String(item.userId) === String(payload.inspector)
        );
        if (selectedUser) {
          payload.registrantId = selectedUser.userId;
          payload.registrant = selectedUser.nickName;
        }
      }
      delete payload.inspector;
      delete payload.inspectorIds;
      if (payload.frequencyType === "WEEKLY") {
        payload.frequencyDetail = `${payload.week},${payload.time}`;
      }
      if (
        payload.status === undefined ||
        payload.status === null ||
        payload.status === ""
      ) {
        payload.status = "0";
      }
      payload.deviceLedgerIds = [...form.value.deviceLedgerIds];
      payload.deviceLedgerIdsStr = form.value.deviceLedgerIds.join(",");
      payload.taskIds = [...form.value.deviceLedgerIds];
      payload.taskIdsStr = form.value.deviceLedgerIds.join(",");
      payload.taskId = form.value.deviceLedgerIds[0];
      payload.taskName = form.value.taskName;
      payload.deviceModel = form.value.deviceModel || "-";
      payload.active = true;
      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 });
</script>
<style scoped>
</style>
<style scoped></style>
src/views/equipmentManagement/upkeep/index.vue
@@ -299,6 +299,10 @@
// å®šæ—¶ä»»åŠ¡ç®¡ç†è¡¨æ ¼åˆ—é…ç½®
const scheduledColumns = ref([
    {
        label: "所在区域",
        prop: "areaName",
    },
    { prop: "taskName", label: "设备名称"},
    {
        label: "规格型号",
@@ -352,6 +356,10 @@
// ä»»åŠ¡è®°å½•è¡¨æ ¼åˆ—é…ç½®ï¼ˆåŽŸè®¾å¤‡ä¿å…»è¡¨æ ¼åˆ—ï¼‰
const columns = ref([
    {
        label: "所在区域",
        prop: "areaName",
    },
    {
        label: "设备名称",
        align: "center",
        prop: "deviceName",
src/views/inventoryManagement/environmentalMonitoring/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,277 @@
<template>
  <div class="environment-monitoring-page">
    <section class="chart-panel">
      <h3 class="panel-title">环境实时柱状图</h3>
      <Echarts
        :series="barSeries"
        :x-axis="xAxis"
        :y-axis="yAxis"
        :tooltip="tooltip"
        :grid="grid"
        :legend="legend"
        :options="chartTheme"
        :chart-style="chartStyle"
      />
    </section>
    <section class="table-panel">
      <h3 class="panel-title">设备环境数据列表</h3>
      <div class="sensor-table">
        <div class="sensor-table__head">
          <span>设备</span>
          <span>温度</span>
          <span>湿度</span>
          <span>二氧化碳</span>
          <span>光照</span>
        </div>
        <div v-for="item in deviceRows" :key="item.name" class="sensor-table__row">
          <span>{{ item.name }}</span>
          <span>{{ item.temperature }}</span>
          <span>{{ item.humidity }}</span>
          <span>{{ item.co2 }}</span>
          <span>{{ item.light }}</span>
        </div>
        <div v-if="!deviceRows.length" class="sensor-table__empty">
          æš‚无环境数据
        </div>
      </div>
    </section>
  </div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import Echarts from "@/components/Echarts/echarts.vue";
import { getEnvironmentalRealData } from "@/api/inventoryManagement/environmentalMonitoring";
const POLL_INTERVAL = 30000;
const latestDevices = ref([]);
let pollTimer = null;
const metricConfig = [
  { key: "temperature", label: "温度", color: "#ff7a59" },
  { key: "humidity", label: "湿度", color: "#1ea7fd" },
  { key: "co2", label: "二氧化碳", color: "#12c48b" },
  { key: "light", label: "光照", color: "#8b5cf6" },
];
const chartTheme = {
  backgroundColor: "transparent",
  textStyle: { color: "#6c7c96" },
};
const chartStyle = {
  width: "100%",
  height: "360px",
};
const grid = {
  left: "4%",
  right: "4%",
  top: "16%",
  bottom: "10%",
  containLabel: true,
};
const tooltip = {
  trigger: "axis",
  axisPointer: { type: "shadow" },
  backgroundColor: "rgba(12, 20, 34, 0.88)",
  borderColor: "rgba(126, 164, 255, 0.18)",
  textStyle: { color: "#e8edf7" },
};
const legend = {
  top: 0,
  right: 0,
  textStyle: { color: "#6c7c96" },
};
const extractNumericValue = (rawValue) => {
  const matched = String(rawValue ?? "").match(/-?\d+(\.\d+)?/);
  return matched ? Number(matched[0]) : 0;
};
const normalizeMetricObject = (source, index) => {
  const normalized = {
    name: source.deviceName || source.name || source.deviceNo || `设备${index + 1}`,
    temperature: 0,
    humidity: 0,
    co2: 0,
    light: 0,
  };
  Object.entries(source || {}).forEach(([key, value]) => {
    const rawText = String(value ?? "");
    if (rawText.includes("℃")) {
      normalized.temperature = extractNumericValue(rawText);
      return;
    }
    if (rawText.includes("%RH")) {
      normalized.humidity = extractNumericValue(rawText);
      return;
    }
    if (rawText.includes("ppm")) {
      normalized.co2 = extractNumericValue(rawText);
      return;
    }
    if (rawText.includes("Lux")) {
      normalized.light = extractNumericValue(rawText);
      return;
    }
    if (key === "temperature") {
      normalized.temperature = extractNumericValue(rawText);
    } else if (key === "humidity") {
      normalized.humidity = extractNumericValue(rawText);
    } else if (key === "co2") {
      normalized.co2 = extractNumericValue(rawText);
    } else if (key === "light") {
      normalized.light = extractNumericValue(rawText);
    }
  });
  return normalized;
};
const xAxis = computed(() => [
  {
    type: "category",
    data: latestDevices.value.map((item) => item.name),
    axisLine: { lineStyle: { color: "rgba(79, 110, 148, 0.55)" } },
    axisLabel: { color: "#6c7c96" },
    axisTick: { show: false },
  },
]);
const yAxis = [
  {
    type: "value",
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { lineStyle: { color: "rgba(110, 131, 160, 0.12)" } },
    axisLabel: { color: "#6c7c96" },
  },
];
const barSeries = computed(() =>
  metricConfig.map((item) => ({
    name: item.label,
    type: "bar",
    barMaxWidth: 28,
    itemStyle: {
      color: item.color,
      borderRadius: [8, 8, 0, 0],
    },
    data: latestDevices.value.map((device) => Number(device[item.key] || 0)),
  }))
);
const deviceRows = computed(() =>
  latestDevices.value.map((item) => ({
    name: item.name,
    temperature: `${Number(item.temperature || 0).toFixed(2)}℃`,
    humidity: `${Number(item.humidity || 0).toFixed(2)}%RH`,
    co2: `${Number(item.co2 || 0).toFixed(2)}ppm`,
    light: `${Number(item.light || 0).toFixed(2)}Lux`,
  }))
);
const fetchRealData = async () => {
  try {
    const res = await getEnvironmentalRealData();
    const dataList = Array.isArray(res?.data) ? res.data : [];
    latestDevices.value = dataList.map((item, index) => normalizeMetricObject(item, index));
  } catch (error) {
    latestDevices.value = [];
  }
};
onMounted(() => {
  fetchRealData();
  pollTimer = window.setInterval(fetchRealData, POLL_INTERVAL);
});
onBeforeUnmount(() => {
  if (pollTimer) {
    window.clearInterval(pollTimer);
    pollTimer = null;
  }
});
</script>
<style scoped lang="scss">
.environment-monitoring-page {
  min-height: 100%;
  padding: 20px;
}
.chart-panel,
.table-panel {
  padding: 20px;
  border-radius: 16px;
  background: #fff;
}
.table-panel {
  margin-top: 20px;
}
.panel-title {
  margin: 0 0 16px;
  color: #1d344f;
  font-size: 18px;
  font-weight: 600;
}
.sensor-table {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.sensor-table__head,
.sensor-table__row {
  display: grid;
  grid-template-columns: 1.1fr 1fr 1fr 1fr 1fr;
  gap: 12px;
  align-items: center;
}
.sensor-table__head {
  padding: 0 6px 10px;
  color: #8393a8;
  font-size: 13px;
}
.sensor-table__row {
  padding: 14px 16px;
  border-radius: 12px;
  background: #f6f9fc;
  color: #1d344f;
}
.sensor-table__empty {
  padding: 32px 0;
  color: #8393a8;
  text-align: center;
}
@media (max-width: 768px) {
  .environment-monitoring-page {
    padding: 12px;
  }
  .chart-panel,
  .table-panel {
    padding: 16px;
  }
  .sensor-table__head,
  .sensor-table__row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
</style>