From 94ee31388ed0012d2c65437bd164e6878f4c635d Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 10 四月 2026 13:31:48 +0800
Subject: [PATCH] 新疆大罗素 1.设备保养、设备维修、设备巡检新增时可以多选设备 2.设备台账添加区域维护字段 3.添加环境页面实时展示设备数据

---
 src/views/equipmentManagement/upkeep/index.vue                            |    8 
 src/views/equipmentManagement/upkeep/Form/PlanModal.vue                   |  245 +++
 src/views/equipmentManagement/repair/Modal/RepairModal.vue                |  227 +++
 src/views/equipmentManagement/inspectionManagement/components/formDia.vue |  595 ++++++---
 src/layout/index.vue                                                      |  183 +-
 src/views/equipmentManagement/repair/index.vue                            |    8 
 src/views/equipmentManagement/inspectionManagement/index.vue              |  677 +++++-----
 src/views/equipmentManagement/ledger/Form.vue                             |   42 
 src/views/equipmentManagement/ledger/Modal.vue                            |    7 
 src/api/equipmentManagement/deviceArea.js                                 |   55 
 src/views/equipmentManagement/upkeep/Form/formDia.vue                     |  734 +++++++----
 src/views/inventoryManagement/environmentalMonitoring/index.vue           |  277 ++++
 src/views/equipmentManagement/ledger/index.vue                            |  631 ++++++++--
 src/api/inventoryManagement/environmentalMonitoring.js                    |    8 
 14 files changed, 2,536 insertions(+), 1,161 deletions(-)

diff --git a/src/api/equipmentManagement/deviceArea.js b/src/api/equipmentManagement/deviceArea.js
new file mode 100644
index 0000000..2671840
--- /dev/null
+++ b/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,
+  });
+}
diff --git a/src/api/inventoryManagement/environmentalMonitoring.js b/src/api/inventoryManagement/environmentalMonitoring.js
new file mode 100644
index 0000000..f0b7187
--- /dev/null
+++ b/src/api/inventoryManagement/environmentalMonitoring.js
@@ -0,0 +1,8 @@
+import request from "@/utils/request";
+
+export const getEnvironmentalRealData = () => {
+  return request({
+    url: "/iot/getRealData",
+    method: "get",
+  });
+};
diff --git a/src/layout/index.vue b/src/layout/index.vue
index 2d4bf01..d3580d0 100644
--- a/src/layout/index.vue
+++ b/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>
diff --git a/src/views/equipmentManagement/inspectionManagement/components/formDia.vue b/src/views/equipmentManagement/inspectionManagement/components/formDia.vue
index 9f509b1..6432617 100644
--- a/src/views/equipmentManagement/inspectionManagement/components/formDia.vue
+++ b/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="瀛e害" 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)
-    
-    // 濡傛灉鏈夎澶嘔D锛岃嚜鍔ㄨ缃澶囦俊鎭�
-    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()
-  }
-  // 閲嶇疆琛ㄥ崟鏁版嵁纭繚璁惧淇℃伅姝g‘閲嶇疆
-  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>
\ No newline at end of file
+<style scoped></style>
diff --git a/src/views/equipmentManagement/inspectionManagement/index.vue b/src/views/equipmentManagement/inspectionManagement/index.vue
index 35f82d5..ef15b75 100644
--- a/src/views/equipmentManagement/inspectionManagement/index.vue
+++ b/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: "瀛e害"
-      // }[val] || "")
-      formatData: params => {
-        return params === "DAILY"
-          ? "姣忔棩"
-          : params === "WEEKLY"
-          ? "姣忓懆"
-          : params === "MONTHLY"
-          ? "姣忔湀"
-          : params === "QUARTERLY"
-          ? "瀛e害"
-          : "";
-      },
-    },
-    {
-      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: "鍛ㄦ棩",
-        };
-        // 浣跨敤姝e垯涓�娆℃�ф浛鎹㈡墍鏈夊尮閰嶉」
-        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: "瀛e害",
+      }[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>
\ No newline at end of file
+.person-tag {
+  margin-right: 4px;
+  margin-bottom: 2px;
+}
+
+.no-data {
+  color: #909399;
+  font-size: 14px;
+}
+</style>
diff --git a/src/views/equipmentManagement/ledger/Form.vue b/src/views/equipmentManagement/ledger/Form.vue
index 72d594e..5cfa309 100644
--- a/src/views/equipmentManagement/ledger/Form.vue
+++ b/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,
diff --git a/src/views/equipmentManagement/ledger/Modal.vue b/src/views/equipmentManagement/ledger/Modal.vue
index 16166c6..3870862 100644
--- a/src/views/equipmentManagement/ledger/Modal.vue
+++ b/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>
diff --git a/src/views/equipmentManagement/ledger/index.vue b/src/views/equipmentManagement/ledger/index.vue
index 7ba9401..8e3fae5 100644
--- a/src/views/equipmentManagement/ledger/index.vue
+++ b/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>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
-            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">涓嬭浇妯℃澘</el-link>
+            <span>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢銆�</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`, {}, "璁惧鍙拌处妗f.xlsx");
+      proxy.download("/device/ledger/export", {}, "璁惧鍙拌处妗f.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>
diff --git a/src/views/equipmentManagement/repair/Modal/RepairModal.vue b/src/views/equipmentManagement/repair/Modal/RepairModal.vue
index 1aa82ec..022ae6f 100644
--- a/src/views/equipmentManagement/repair/Modal/RepairModal.vue
+++ b/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({
diff --git a/src/views/equipmentManagement/repair/index.vue b/src/views/equipmentManagement/repair/index.vue
index 1e7af53..1699cff 100644
--- a/src/views/equipmentManagement/repair/index.vue
+++ b/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",
diff --git a/src/views/equipmentManagement/upkeep/Form/PlanModal.vue b/src/views/equipmentManagement/upkeep/Form/PlanModal.vue
index 19095b9..e44d6af 100644
--- a/src/views/equipmentManagement/upkeep/Form/PlanModal.vue
+++ b/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({
diff --git a/src/views/equipmentManagement/upkeep/Form/formDia.vue b/src/views/equipmentManagement/upkeep/Form/formDia.vue
index 66bf067..57d7b11 100644
--- a/src/views/equipmentManagement/upkeep/Form/formDia.vue
+++ b/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="瀛e害" 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="瀛e害" 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)) || [];
+};
 
-		// 濡傛灉鏈夎澶嘔D锛岃嚜鍔ㄨ缃澶囦俊鎭�
-		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()
-	}
-	// 閲嶇疆琛ㄥ崟鏁版嵁纭繚璁惧淇℃伅姝g‘閲嶇疆
-	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 }
-				// 涓嶅啀鍚戝悗绔紶淇濆吇浜哄瓧娈碉紝浠呬娇鐢ㄦ帴鍙h姹傜殑 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>
diff --git a/src/views/equipmentManagement/upkeep/index.vue b/src/views/equipmentManagement/upkeep/index.vue
index 543e37b..e5d358a 100644
--- a/src/views/equipmentManagement/upkeep/index.vue
+++ b/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",
diff --git a/src/views/inventoryManagement/environmentalMonitoring/index.vue b/src/views/inventoryManagement/environmentalMonitoring/index.vue
new file mode 100644
index 0000000..bda17b8
--- /dev/null
+++ b/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>

--
Gitblit v1.9.3