فهرست منبع

修改内容 前端页面显示美化和问题修复

bzd_lxf 2 ماه پیش
والد
کامیت
b84754cf83
27فایلهای تغییر یافته به همراه4826 افزوده شده و 63 حذف شده
  1. 11 0
      pm_ui/src/api/meeting/meeting.js
  2. 5 1
      pm_ui/src/api/system/menu.js
  3. 5 5
      pm_ui/src/assets/styles/variables.module.scss
  4. 1 1
      pm_ui/src/components/Breadcrumb/index.vue
  5. 2 1
      pm_ui/src/components/Editor/index.vue
  6. 1 1
      pm_ui/src/layout/components/AppMain.vue
  7. 1 1
      pm_ui/src/layout/components/Settings/index.vue
  8. 3 3
      pm_ui/src/layout/components/Sidebar/index.vue
  9. 1 0
      pm_ui/src/utils/errorCode.js
  10. 452 0
      pm_ui/src/views/device/alarmManagement/index.vue
  11. 80 47
      pm_ui/src/views/device/basic/index.vue
  12. 199 0
      pm_ui/src/views/device/monitor/index.vue
  13. 496 0
      pm_ui/src/views/device/scheduleManagement/index.vue
  14. 252 0
      pm_ui/src/views/device/spaceScene/index.vue
  15. 375 0
      pm_ui/src/views/device/systemMonitor/index.vue
  16. 1 1
      pm_ui/src/views/device/waring/index.vue
  17. 577 0
      pm_ui/src/views/energyManagement/EnergyConsumptionRanking/index.vue
  18. 567 0
      pm_ui/src/views/energyManagement/analysisReport/index.vue
  19. 424 0
      pm_ui/src/views/energyManagement/dataAcquisition/index.vue
  20. 246 0
      pm_ui/src/views/energyManagement/dataComparison/index.vue
  21. 237 0
      pm_ui/src/views/energyManagement/layerManage/index.vue
  22. 265 0
      pm_ui/src/views/energyManagement/layerManage/index2.vue
  23. 255 0
      pm_ui/src/views/energyManagement/quotaManagement/index.vue
  24. 345 0
      pm_ui/src/views/meeting/index.vue
  25. 1 1
      pm_ui/src/views/system/menu/index.vue
  26. 3 1
      pm_ui/src/views/system/notice/index.vue
  27. 21 0
      pm_ui/src/views/zutai/index.vue

+ 11 - 0
pm_ui/src/api/meeting/meeting.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// 查询设备告警列表
+export function listMeeting(query) {
+    return request({
+        url: '/meeting/info/list',
+        method: 'get',
+        params: query
+    })
+}
+

+ 5 - 1
pm_ui/src/api/system/menu.js

@@ -62,7 +62,11 @@ export function delMenu(menuId) {
 export function listNoticeRead() {
   return request({
     url: '/system/menu/listNoticeRead',
-    method: 'get'
+    method: 'get',
+    params: {
+      pageNum: 1,
+      pageSize: 10000
+    }
   })
 }
 // 查询统计信息

+ 5 - 5
pm_ui/src/assets/styles/variables.module.scss

@@ -71,10 +71,10 @@ $--color-info: #909399;
   --sidebar-bg: #{$menuBg};
   --sidebar-text: #{$menuText};
   --menu-hover: #{$menuHover};
-  
+
   --navbar-bg: #ffffff;
   --navbar-text: #303133;
-  
+
   /* splitpanes default-theme 变量 */
   --splitpanes-default-bg: #ffffff;
 
@@ -119,7 +119,7 @@ html.dark {
   --blockquote-bg: #1d1e1f;
   --blockquote-border: #303030;
   --blockquote-text: #d0d0d0;
-  
+
   /* Cron 时间表达式 模式变量 */
   --cron-border: #303030;
 
@@ -199,7 +199,7 @@ html.dark {
       background-color: var(--el-bg-color-overlay);
     }
   }
-  
+
   /* 下拉菜单样式覆盖 */
   .el-dropdown-menu__item:not(.is-disabled):focus, .el-dropdown-menu__item:not(.is-disabled):hover{
     background-color: var(--navbar-hover) !important;
@@ -211,7 +211,7 @@ html.dark {
     border-left-color: var(--blockquote-border) !important;
     color: var(--blockquote-text) !important;
   }
-  
+
   /* 时间表达式标题样式覆盖 */
   .popup-result .title {
     background: var(--cron-border);

+ 1 - 1
pm_ui/src/components/Breadcrumb/index.vue

@@ -95,4 +95,4 @@ getBreadcrumb()
     cursor: text;
   }
 }
-</style>
+</style>

+ 2 - 1
pm_ui/src/components/Editor/index.vue

@@ -152,7 +152,8 @@ function handleUploadSuccess(res, file) {
   // 如果上传成功
   if (res.code == 200) {
     // 获取富文本实例
-    let quill = toRaw(quillEditorRef.value).getQuill();
+    //let quill = toRaw(quillEditorRef.value).getQuill();
+    let quill = quillEditorRef.value.getQuill();
     // 获取光标位置
     let length = quill.selection.savedRange.index;
     // 插入图片,res.url为服务器返回的图片链接地址

+ 1 - 1
pm_ui/src/layout/components/AppMain.vue

@@ -22,7 +22,7 @@ onMounted(() => {
   addIframe()
 })
 
-watch((route) => {
+watchEffect((route) => {
   addIframe()
 })
 

+ 1 - 1
pm_ui/src/layout/components/Settings/index.vue

@@ -201,4 +201,4 @@ defineExpose({
     margin: -3px 8px 0px 0px;
   }
 }
-</style>
+</style>

+ 3 - 3
pm_ui/src/layout/components/Sidebar/index.vue

@@ -71,7 +71,7 @@ const activeMenu = computed(() => {
 <style lang="scss" scoped>
 .sidebar-container {
   background-color: v-bind(getMenuBackground);
-  
+
   .scrollbar-wrapper {
     background-color: v-bind(getMenuBackground);
   }
@@ -80,7 +80,7 @@ const activeMenu = computed(() => {
     border: none;
     height: 100%;
     width: 100% !important;
-    
+
     .el-menu-item, .el-sub-menu__title {
       &:hover {
         background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
@@ -89,7 +89,7 @@ const activeMenu = computed(() => {
 
     .el-menu-item {
       color: v-bind(getMenuTextColor);
-      
+
       &.is-active {
         color: var(--menu-active-text, #409eff);
         background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;

+ 1 - 0
pm_ui/src/utils/errorCode.js

@@ -2,5 +2,6 @@ export default {
   '401': '认证失败,无法访问系统资源',
   '403': '当前操作没有权限',
   '404': '访问资源不存在',
+  '1203': '获取会议室列表失败',
   'default': '系统未知错误,请反馈给管理员'
 }

+ 452 - 0
pm_ui/src/views/device/alarmManagement/index.vue

@@ -0,0 +1,452 @@
+<template>
+  <div class="alarm-management-container">
+    <!-- 筛选条件 -->
+    <el-card class="filter-card">
+      <el-form :model="queryParams" label-width="80px" @submit.prevent="loadAlarmRecords">
+        <el-row :gutter="20">
+          <el-col :span="6">
+            <el-form-item label="设备名称">
+              <el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="报警类型">
+              <el-select v-model="queryParams.alarmType" placeholder="请选择报警类型">
+                <el-option label="全部" value="" />
+                <el-option label="故障报警" value="fault" />
+                <el-option label="异常报警" value="abnormal" />
+                <el-option label="离线报警" value="offline" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="时间范围">
+              <el-date-picker
+                  v-model="queryParams.dateRange"
+                  type="daterange"
+                  range-separator="至"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  style="width: 100%;"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-button type="primary" native-type="submit">查询</el-button>
+            <el-button @click="resetQuery">重置</el-button>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
+
+    <!-- 报警记录表格 -->
+    <el-card class="table-card">
+      <el-table :data="alarmRecords" border style="width: 100%">
+        <el-table-column prop="deviceId" label="设备ID" align="center" />
+        <el-table-column prop="deviceName" label="设备名称" align="center" />
+        <el-table-column prop="alarmType" label="报警类型" align="center">
+          <template #default="{ row }">
+            {{ formatAlarmType(row.alarmType) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="alarmTime" label="报警时间" align="center" />
+        <el-table-column prop="status" label="状态" align="center">
+          <template #default="{ row }">
+            <el-tag :type="getStatusTagType(row.status)">
+              {{ formatStatus(row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center">
+          <template #default="{ row }">
+            <el-button size="small" @click="viewDetail(row)">查看详情</el-button>
+            <el-button size="small" type="warning" @click="forwardToMaintenance(row)">转发报警</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 分页组件 -->
+    <el-pagination
+        class="pagination"
+        background
+        layout="total, prev, pager, next, sizes"
+        :total="total"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        @size-change="handleSizeChange"
+        @current-change="handlePageChange"
+    />
+
+    <!-- 查看详情对话框 -->
+    <el-dialog v-model="detailDialogVisible" title="报警详情" width="50%">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="设备ID">{{ selectedAlarm.deviceId }}</el-descriptions-item>
+        <el-descriptions-item label="设备名称">{{ selectedAlarm.deviceName }}</el-descriptions-item>
+        <el-descriptions-item label="报警类型">{{ formatAlarmType(selectedAlarm.alarmType) }}</el-descriptions-item>
+        <el-descriptions-item label="报警时间">{{ selectedAlarm.alarmTime }}</el-descriptions-item>
+        <el-descriptions-item label="状态">{{ formatStatus(selectedAlarm.status) }}</el-descriptions-item>
+        <el-descriptions-item label="描述">{{ selectedAlarm.description || '-' }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+
+    <!-- 转发报警对话框 -->
+    <el-dialog v-model="forwardDialogVisible" title="转发报警" width="50%">
+      <el-form :model="forwardForm" label-width="120px">
+        <el-form-item label="维保商">
+          <el-input v-model="forwardForm.maintenanceProvider" placeholder="请输入维保商名称" />
+        </el-form-item>
+        <el-form-item label="联系方式">
+          <el-input v-model="forwardForm.contactInfo" placeholder="请输入维保商联系方式" />
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="forwardForm.remark" type="textarea" placeholder="请输入备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="forwardDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitForward">提交</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+
+// 查询参数
+const queryParams = ref({
+  deviceName: '',
+  alarmType: '',
+  dateRange: []
+})
+
+// 报警记录数据
+const alarmRecords = ref([])
+const total = ref(0)
+const pageSize = ref(10)
+const currentPage = ref(1)
+
+// 模拟报警记录数据
+const mockAlarmData = [
+  {
+    deviceId: 'D1001',
+    deviceName: '温度传感器A',
+    alarmType: 'fault',
+    alarmTime: '2025-04-05 14:30',
+    status: 'pending',
+    description: '设备故障,请尽快维修。'
+  },
+  {
+    deviceId: 'D1002',
+    deviceName: '空调控制器B',
+    alarmType: 'abnormal',
+    alarmTime: '2025-04-04 09:10',
+    status: 'resolved',
+    description: '异常报警,已修复。'
+  },
+  {
+    deviceId: 'D2003',
+    deviceName: '湿度传感器C',
+    alarmType: 'warning',
+    alarmTime: '2025-04-06 08:45',
+    status: 'pending',
+    description: '湿度过高,请检查通风系统。'
+  },
+  {
+    deviceId: 'D3004',
+    deviceName: '智能电表D',
+    alarmType: 'fault',
+    alarmTime: '2025-04-03 16:20',
+    status: 'resolved',
+    description: '电表通信故障,已恢复。'
+  },
+  {
+    deviceId: 'D4005',
+    deviceName: '烟雾探测器E',
+    alarmType: 'emergency',
+    alarmTime: '2025-04-07 10:15',
+    status: 'pending',
+    description: '检测到烟雾,请立即处理!'
+  },
+  {
+    deviceId: 'D5006',
+    deviceName: '门禁控制器F',
+    alarmType: 'abnormal',
+    alarmTime: '2025-04-02 13:50',
+    status: 'resolved',
+    description: '门禁异常,已修复。'
+  },
+  {
+    deviceId: 'D6007',
+    deviceName: '摄像头G',
+    alarmType: 'fault',
+    alarmTime: '2025-04-08 11:30',
+    status: 'pending',
+    description: '摄像头离线,请检查网络连接。'
+  },
+  {
+    deviceId: 'D7008',
+    deviceName: '照明控制器H',
+    alarmType: 'warning',
+    alarmTime: '2025-04-01 18:05',
+    status: 'resolved',
+    description: '照明亮度异常,已调整。'
+  },
+  {
+    deviceId: 'D8009',
+    deviceName: '通风系统I',
+    alarmType: 'fault',
+    alarmTime: '2025-04-09 09:25',
+    status: 'pending',
+    description: '通风系统故障,需维修。'
+  },
+  {
+    deviceId: 'D9010',
+    deviceName: '水浸传感器J',
+    alarmType: 'emergency',
+    alarmTime: '2025-04-10 14:40',
+    status: 'pending',
+    description: '检测到漏水,请立即处理!'
+  },
+  {
+    deviceId: 'D1011',
+    deviceName: 'UPS电源K',
+    alarmType: 'abnormal',
+    alarmTime: '2025-04-11 07:15',
+    status: 'resolved',
+    description: '电源异常,已恢复正常。'
+  },
+  {
+    deviceId: 'D2012',
+    deviceName: '服务器机柜L',
+    alarmType: 'fault',
+    alarmTime: '2025-04-12 12:00',
+    status: 'pending',
+    description: '服务器温度过高,请检查散热。'
+  },
+  {
+    deviceId: 'D3013',
+    deviceName: '网络交换机M',
+    alarmType: 'warning',
+    alarmTime: '2025-04-13 15:35',
+    status: 'resolved',
+    description: '网络流量异常,已优化。'
+  },
+  {
+    deviceId: 'D4014',
+    deviceName: '智能插座N',
+    alarmType: 'fault',
+    alarmTime: '2025-04-14 10:50',
+    status: 'pending',
+    description: '插座短路,请维修。'
+  },
+  {
+    deviceId: 'D5015',
+    deviceName: '燃气探测器O',
+    alarmType: 'emergency',
+    alarmTime: '2025-04-15 17:25',
+    status: 'pending',
+    description: '检测到燃气泄漏,紧急处理!'
+  },
+  {
+    deviceId: 'D6016',
+    deviceName: '电梯控制器P',
+    alarmType: 'abnormal',
+    alarmTime: '2025-04-16 08:10',
+    status: 'resolved',
+    description: '电梯运行异常,已修复。'
+  },
+  {
+    deviceId: 'D7017',
+    deviceName: '消防报警器Q',
+    alarmType: 'fault',
+    alarmTime: '2025-04-17 11:45',
+    status: 'pending',
+    description: '报警器故障,需更换。'
+  },
+  {
+    deviceId: 'D8018',
+    deviceName: '空调控制器R',
+    alarmType: 'warning',
+    alarmTime: '2025-04-18 14:20',
+    status: 'resolved',
+    description: '空调温度异常,已调整。'
+  },
+  {
+    deviceId: 'D9019',
+    deviceName: '智能窗帘S',
+    alarmType: 'fault',
+    alarmTime: '2025-04-19 09:55',
+    status: 'pending',
+    description: '窗帘电机故障,请维修。'
+  },
+  {
+    deviceId: 'D1020',
+    deviceName: '安防摄像头T',
+    alarmType: 'emergency',
+    alarmTime: '2025-04-20 16:30',
+    status: 'pending',
+    description: '检测到入侵,请立即查看!'
+  },
+  {
+    deviceId: 'D2021',
+    deviceName: '温湿度传感器U',
+    alarmType: 'abnormal',
+    alarmTime: '2025-04-21 10:05',
+    status: 'resolved',
+    description: '传感器数据异常,已校准。'
+  },
+  {
+    deviceId: 'D3022',
+    deviceName: '智能门锁V',
+    alarmType: 'fault',
+    alarmTime: '2025-04-22 13:40',
+    status: 'pending',
+    description: '门锁电池电量低,请更换。'
+  },
+  {
+    deviceId: 'D4023',
+    deviceName: '配电箱W',
+    alarmType: 'warning',
+    alarmTime: '2025-04-23 08:15',
+    status: 'resolved',
+    description: '电压波动,已稳定。'
+  },
+  {
+    deviceId: 'D5024',
+    deviceName: '新风系统X',
+    alarmType: 'fault',
+    alarmTime: '2025-04-24 11:50',
+    status: 'pending',
+    description: '风机故障,需维修。'
+  }
+]
+
+// 加载报警记录
+const loadAlarmRecords = () => {
+  // 模拟异步请求
+  setTimeout(() => {
+    const filteredData = mockAlarmData.filter(item => {
+      return (
+          (!queryParams.value.deviceName || item.deviceName.includes(queryParams.value.deviceName)) &&
+          (!queryParams.value.alarmType || item.alarmType === queryParams.value.alarmType)
+      )
+    })
+    alarmRecords.value = filteredData.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)
+    total.value = filteredData.length
+  }, 300)
+}
+
+// 重置查询条件
+const resetQuery = () => {
+  queryParams.value = {
+    deviceName: '',
+    alarmType: '',
+    dateRange: []
+  }
+  loadAlarmRecords()
+}
+
+// 格式化报警类型
+const formatAlarmType = (type) => {
+  switch (type) {
+    case 'fault': return '故障报警'
+    case 'abnormal': return '异常报警'
+    case 'offline': return '离线报警'
+    default: return '未知'
+  }
+}
+
+// 格式化状态
+const formatStatus = (status) => {
+  switch (status) {
+    case 'pending': return '待处理'
+    case 'resolved': return '已解决'
+    default: return '未知'
+  }
+}
+
+// 获取标签类型
+const getStatusTagType = (status) => {
+  switch (status) {
+    case 'pending': return 'warning'
+    case 'resolved': return 'success'
+    default: return ''
+  }
+}
+
+// 查看详情
+const detailDialogVisible = ref(false)
+const selectedAlarm = ref({})
+const viewDetail = (row) => {
+  selectedAlarm.value = row
+  detailDialogVisible.value = true
+}
+
+// 转发报警
+const forwardDialogVisible = ref(false)
+const forwardForm = ref({
+  maintenanceProvider: '',
+  contactInfo: '',
+  remark: ''
+})
+const currentAlarm = ref(null)
+const forwardToMaintenance = (row) => {
+  currentAlarm.value = row
+  forwardForm.value = {
+    maintenanceProvider: '',
+    contactInfo: '',
+    remark: ''
+  }
+  forwardDialogVisible.value = true
+}
+
+// 提交转发
+const submitForward = () => {
+  if (!forwardForm.value.maintenanceProvider || !forwardForm.value.contactInfo) {
+    ElMessage.warning('请填写维保商和联系方式')
+    return
+  }
+
+  ElMessage.success(`报警已成功转发给 ${forwardForm.value.maintenanceProvider}`)
+  forwardDialogVisible.value = false
+}
+
+// 分页事件
+const handleSizeChange = (newSize) => {
+  pageSize.value = newSize
+  loadAlarmRecords()
+}
+const handlePageChange = (newPage) => {
+  currentPage.value = newPage
+  loadAlarmRecords()
+}
+
+// 初始化数据
+onMounted(() => {
+  loadAlarmRecords()
+})
+</script>
+
+<style scoped lang="scss">
+.alarm-management-container {
+  padding: 20px;
+
+  .filter-card {
+    margin-bottom: 20px;
+  }
+
+  .table-card {
+    margin-bottom: 20px;
+  }
+
+  .pagination {
+    text-align: right;
+    margin-top: 20px;
+  }
+}
+</style>

+ 80 - 47
pm_ui/src/views/device/basic/index.vue

@@ -127,10 +127,40 @@
           <el-input v-model="form.deviceName" placeholder="请输入设备名称" />
         </el-form-item>
         <el-form-item label="设备类型" prop="deviceType">
-          <el-input v-model="form.deviceType" placeholder="请输入设备类型" />
+          <el-select
+              v-model="form.deviceType"
+              placeholder="请选择设备类型"
+              filterable
+              clearable
+          >
+            <el-option
+                v-for="dict in sys_device_type"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+            ></el-option>
+          </el-select>
         </el-form-item>
-        <el-form-item label="子系统类型" prop="subsystemType">
+<!--        <el-form-item label="设备类型" prop="deviceType">
+          <el-input v-model="form.deviceType" placeholder="请输入设备类型" />
+        </el-form-item>-->
+<!--        <el-form-item label="子系统类型" prop="subsystemType">
           <el-input v-model="form.subsystemType" placeholder="请输入子系统类型" />
+        </el-form-item>-->
+        <el-form-item label="子系统类型" prop="subsystemType">
+          <el-select
+              v-model="form.subsystemType"
+              placeholder="请选择子系统类型"
+              filterable
+              clearable
+          >
+            <el-option
+                v-for="dict in sys_subsystem_type"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+            ></el-option>
+          </el-select>
         </el-form-item>
         <el-form-item label="所属空间ID" prop="spaceId">
           <el-input v-model="form.spaceId" placeholder="请输入所属空间ID" />
@@ -191,50 +221,53 @@
         </div>
       </template>
     </el-dialog>
+    <el-drawer
+        v-model="detailVisible"
+        title="设备监控"
+        direction="rtl"
+        size="50%"
+    >
+      <el-tabs v-model="activeTab">
+        <el-tab-pane label="设备详情" name="detail">
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="设备编码">{{currentDevice.deviceCode}}</el-descriptions-item>
+            <el-descriptions-item label="设备名称">{{currentDevice.deviceName}}</el-descriptions-item>
+            <el-descriptions-item label="设备类型">
+              {{ sys_device_type.find(item => item.value === currentDevice.deviceType)?.label || currentDevice.deviceType }}
+            </el-descriptions-item>
+            <el-descriptions-item label="子系统类型">
+              {{ sys_subsystem_type.find(item => item.value === currentDevice.subsystemType)?.label || currentDevice.subsystemType }}
+            </el-descriptions-item>
+            <el-descriptions-item label="所属空间">{{currentDevice.spaceName}}</el-descriptions-item>
+            <el-descriptions-item label="品牌">{{currentDevice.brand}}</el-descriptions-item>
+            <el-descriptions-item label="型号">{{currentDevice.model}}</el-descriptions-item>
+            <el-descriptions-item label="序列号">{{currentDevice.serialNumber}}</el-descriptions-item>
+          </el-descriptions>
+        </el-tab-pane>
+        <el-tab-pane label="设备组态" name="configuration">
+          <!-- 设备组态内容 -->
+        </el-tab-pane>
+        <el-tab-pane label="设备告警" name="alarm">
+          <!-- 设备告警内容 -->
+        </el-tab-pane>
+        <el-tab-pane label="控制纪录" name="control">
+          <!-- 控制纪录内容 -->
+        </el-tab-pane>
+        <el-tab-pane label="实时数据" name="realtime">
+          <!-- 实时数据内容 -->
+        </el-tab-pane>
+        <el-tab-pane label="历史数据" name="instant">
+          <!-- 历史数据内容 -->
+        </el-tab-pane>
+        <el-tab-pane label="自动抄表" name="meter">
+          <!-- 自动抄表内容 -->
+        </el-tab-pane>
+        <el-tab-pane label="事件记录" name="event">
+          <!-- 事件记录内容 -->
+        </el-tab-pane>
+      </el-tabs>
+    </el-drawer>
   </div>
-  <el-drawer
-      v-model="detailVisible"
-      title="设备监控"
-      direction="rtl"
-      size="50%"
-      :before-close="handleClose"
-  >
-    <el-tabs v-model="activeTab">
-      <el-tab-pane label="设备详情" name="detail">
-        <el-descriptions :column="2" border>
-          <el-descriptions-item label="设备编码">{{currentDevice.deviceCode}}</el-descriptions-item>
-          <el-descriptions-item label="设备名称">{{currentDevice.deviceName}}</el-descriptions-item>
-          <el-descriptions-item label="设备类型">{{currentDevice.deviceType}}</el-descriptions-item>
-          <el-descriptions-item label="子系统类型">{{currentDevice.subsystemType}}</el-descriptions-item>
-          <el-descriptions-item label="所属空间">{{currentDevice.spaceName}}</el-descriptions-item>
-          <el-descriptions-item label="品牌">{{currentDevice.brand}}</el-descriptions-item>
-          <el-descriptions-item label="型号">{{currentDevice.model}}</el-descriptions-item>
-          <el-descriptions-item label="序列号">{{currentDevice.serialNumber}}</el-descriptions-item>
-        </el-descriptions>
-      </el-tab-pane>
-      <el-tab-pane label="设备组态" name="configuration">
-        <!-- 设备组态内容 -->
-      </el-tab-pane>
-      <el-tab-pane label="设备告警" name="alarm">
-        <!-- 设备告警内容 -->
-      </el-tab-pane>
-      <el-tab-pane label="控制纪录" name="control">
-        <!-- 控制纪录内容 -->
-      </el-tab-pane>
-      <el-tab-pane label="实时数据" name="realtime">
-        <!-- 实时数据内容 -->
-      </el-tab-pane>
-      <el-tab-pane label="历史数据" name="instant">
-        <!-- 历史数据内容 -->
-      </el-tab-pane>
-      <el-tab-pane label="自动抄表" name="meter">
-        <!-- 自动抄表内容 -->
-      </el-tab-pane>
-      <el-tab-pane label="事件记录" name="event">
-        <!-- 事件记录内容 -->
-      </el-tab-pane>
-    </el-tabs>
-  </el-drawer>
 </template>
 
 <script setup name="Device">
@@ -242,7 +275,7 @@ import {listBasic, getBasic, delBasic, addBasic, updateBasic} from "@/api/device
 import {show} from "@/api/system/notice.js";
 
 const {proxy} = getCurrentInstance();
-const {sys_drivce_status,sys_yes_no_tf} = proxy.useDict('sys_drivce_status','sys_yes_no_tf');
+const {sys_drivce_status,sys_yes_no_tf,sys_device_type,sys_subsystem_type} = proxy.useDict('sys_drivce_status','sys_yes_no_tf','sys_device_type','sys_subsystem_type');
 
 const deviceList = ref([]);
 const open = ref(false);
@@ -414,7 +447,7 @@ function handleDelete(row) {
 
 /** 导出按钮操作 */
 function handleExport() {
-  proxy.download('driver/device/export', {
+  proxy.download('device/basic/export', {
     ...queryParams.value
   }, `device_${new Date().getTime()}.xlsx`)
 }

+ 199 - 0
pm_ui/src/views/device/monitor/index.vue

@@ -0,0 +1,199 @@
+<template>
+  <div class="device-monitor-container">
+    <!-- 搜索与筛选 -->
+    <el-card class="search-card">
+      <el-form :model="queryParams" label-width="80px" @submit.prevent="loadDeviceList">
+        <el-row :gutter="20">
+          <el-col :span="6">
+            <el-form-item label="设备名称">
+              <el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="设备类型">
+              <el-select v-model="queryParams.deviceType" placeholder="请选择类型">
+                <el-option label="全部" value="" />
+                <el-option label="传感器" value="sensor" />
+                <el-option label="控制器" value="controller" />
+                <el-option label="摄像头" value="camera" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="状态">
+              <el-select v-model="queryParams.status" placeholder="请选择状态">
+                <el-option label="全部" value="" />
+                <el-option label="在线" value="online" />
+                <el-option label="离线" value="offline" />
+                <el-option label="故障" value="fault" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-button type="primary" native-type="submit">查询</el-button>
+            <el-button @click="resetQuery">重置</el-button>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
+
+    <!-- 设备列表 -->
+    <el-card class="table-card">
+      <el-table :data="deviceList" border style="width: 100%" :row-class-name="tableRowClassName">
+        <el-table-column prop="deviceId" label="设备ID" align="center" />
+        <el-table-column prop="name" label="设备名称" align="center" />
+        <el-table-column prop="type" label="设备类型" align="center">
+          <template #default="{ row }">
+            {{ formatDeviceType(row.type) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" align="center">
+          <template #default="{ row }">
+            <el-tag :type="getStatusTagType(row.status)">
+              {{ formatStatus(row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="lastOnlineTime" label="最后在线时间" align="center" />
+        <el-table-column prop="location" label="位置" align="center" />
+        <el-table-column label="操作" align="center">
+          <template #default="{ row }">
+            <el-button size="small" @click="viewDetail(row)">查看详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 设备详情对话框 -->
+    <el-dialog v-model="dialogVisible" title="设备详情" width="50%">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="设备ID">{{ selectedDevice.deviceId }}</el-descriptions-item>
+        <el-descriptions-item label="设备名称">{{ selectedDevice.name }}</el-descriptions-item>
+        <el-descriptions-item label="设备类型">{{ formatDeviceType(selectedDevice.type) }}</el-descriptions-item>
+        <el-descriptions-item label="状态">{{ formatStatus(selectedDevice.status) }}</el-descriptions-item>
+        <el-descriptions-item label="最后在线时间">{{ selectedDevice.lastOnlineTime }}</el-descriptions-item>
+        <el-descriptions-item label="位置">{{ selectedDevice.location }}</el-descriptions-item>
+        <el-descriptions-item label="描述">{{ selectedDevice.description || '-' }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+
+// 查询参数
+const queryParams = ref({
+  deviceName: '',
+  deviceType: '',
+  status: ''
+})
+
+// 设备列表数据
+const deviceList = ref([])
+
+// 控制弹窗显示和内容
+const dialogVisible = ref(false)
+const selectedDevice = ref({})
+
+// 模拟请求接口加载设备数据
+const loadDeviceList = () => {
+  // 模拟异步请求
+  setTimeout(() => {
+    const mockData = [
+      { deviceId: 'D1001', name: '温度传感器A', type: 'sensor', status: 'online', lastOnlineTime: '2025-04-05 14:30', location: '机房A' },
+      { deviceId: 'D1002', name: '空调控制器B', type: 'controller', status: 'offline', lastOnlineTime: '-', location: '办公室B' },
+      { deviceId: 'D1003', name: '摄像头C', type: 'camera', status: 'fault', lastOnlineTime: '2025-04-04 09:10', location: '大厅C' }
+    ]
+    deviceList.value = mockData.filter(item => {
+      return (
+          (!queryParams.value.deviceName || item.name.includes(queryParams.value.deviceName)) &&
+          (!queryParams.value.deviceType || item.type === queryParams.value.deviceType) &&
+          (!queryParams.value.status || item.status === queryParams.value.status)
+      )
+    })
+  }, 300)
+}
+
+// 重置查询条件
+const resetQuery = () => {
+  queryParams.value = {
+    deviceName: '',
+    deviceType: '',
+    status: ''
+  }
+  loadDeviceList()
+}
+
+// 查看设备详情
+const viewDetail = (device) => {
+  selectedDevice.value = device
+  dialogVisible.value = true
+}
+
+// 格式化设备类型
+const formatDeviceType = (type) => {
+  switch (type) {
+    case 'sensor': return '传感器'
+    case 'controller': return '控制器'
+    case 'camera': return '摄像头'
+    default: return '未知'
+  }
+}
+
+// 格式化设备状态
+const formatStatus = (status) => {
+  switch (status) {
+    case 'online': return '在线'
+    case 'offline': return '离线'
+    case 'fault': return '故障'
+    default: return '未知'
+  }
+}
+
+// 表格行样式(用于根据状态设置背景色)
+const tableRowClassName = ({ row }) => {
+  if (row.status === 'fault') {
+    return 'warning-row'
+  } else if (row.status === 'offline') {
+    return 'danger-row'
+  }
+  return ''
+}
+
+// 获取标签类型(用于 el-tag)
+const getStatusTagType = (status) => {
+  switch (status) {
+    case 'online': return 'success'
+    case 'offline': return 'info'
+    case 'fault': return 'danger'
+    default: return ''
+  }
+}
+
+// 页面加载时初始化数据
+onMounted(() => {
+  loadDeviceList()
+})
+</script>
+
+<style scoped lang="scss">
+.device-monitor-container {
+  padding: 20px;
+}
+
+.search-card {
+  margin-bottom: 20px;
+}
+
+.table-card {
+  .el-table ::v-deep(.warning-row) {
+    background-color: #fef0f0 !important;
+  }
+
+  .el-table ::v-deep(.danger-row) {
+    background-color: #fdf6ec !important;
+  }
+}
+</style>

+ 496 - 0
pm_ui/src/views/device/scheduleManagement/index.vue

@@ -0,0 +1,496 @@
+<template>
+  <div class="timetable-management-container">
+    <!-- 顶部导航栏 -->
+    <el-card class="header-card">
+      <el-tabs v-model="viewMode" type="card" @tab-click="handleTabClick">
+        <el-tab-pane label="日历视图" name="calendar"></el-tab-pane>
+        <el-tab-pane label="列表视图" name="list"></el-tab-pane>
+        <el-tab-pane label="执行历史" name="history"></el-tab-pane>
+      </el-tabs>
+      <el-button type="primary" @click="addTimetable">新增时间表</el-button>
+    </el-card>
+
+    <!-- 日历视图 -->
+    <div v-if="viewMode === 'calendar'" class="calendar-view">
+      <el-calendar v-model="currentDate">
+        <template #dateCell="{ data }">
+          <div class="date-cell" @click="handleDateClick(data.day)" @contextmenu.prevent="showContextMenu(data.day, $event)">
+            <p :class="{'highlight': isToday(data.day), 'event-day': hasEvents(data.day)}">
+              {{ data.day.split('-').slice(2).join('-') }}
+            </p>
+            <div v-if="hasEvents(data.day)" class="events">
+              <el-tag
+                  v-for="(event, index) in getEvents(data.day)"
+                  :key="index"
+                  :type="getTagType(event.status)"
+                  size="small"
+                  @click.stop="viewEvent(event)"
+                  style="cursor: pointer;"
+                  :title="getTooltip(event)"
+              >
+                {{ event.title }}
+              </el-tag>
+            </div>
+            <el-button
+                v-if="!hasEvents(data.day)"
+                size="mini"
+                type="text"
+                @click.stop="addTimetableForDay(data.day)"
+            >
+              + 新增
+            </el-button>
+          </div>
+        </template>
+      </el-calendar>
+      <!-- 右键菜单 -->
+      <el-dropdown
+          v-if="contextMenuVisible"
+          trigger="manual"
+          :visible="contextMenuVisible"
+          @command="handleContextCommand"
+          @hide="contextMenuVisible = false"
+          :style="{ position: 'absolute', left: contextMenuX + 'px', top: contextMenuY + 'px' }"
+      >
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item command="add">新增时间表</el-dropdown-item>
+            <el-dropdown-item v-if="selectedDayEvents.length > 0" command="view">查看事件</el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
+
+    <!-- 列表示图 -->
+    <div v-if="viewMode === 'list'" class="list-view">
+      <el-table :data="timetableList" border style="width: 100%" @row-click="viewEvent">
+        <el-table-column prop="id" label="ID" align="center" width="80" />
+        <el-table-column prop="title" label="标题" align="center" />
+        <el-table-column prop="startTime" label="开始时间" align="center" width="180" />
+        <el-table-column prop="endTime" label="结束时间" align="center" width="180" />
+        <el-table-column prop="devices" label="设备" align="center" show-overflow-tooltip />
+        <el-table-column prop="commands" label="指令" align="center" show-overflow-tooltip />
+        <el-table-column label="操作" align="center" width="150">
+          <template #default="{ row }">
+            <el-button size="small" @click.stop="editTimetable(row)">编辑</el-button>
+            <el-button size="small" type="danger" @click.stop="deleteTimetable(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <el-pagination
+          v-if="total > 0"
+          background
+          layout="prev, pager, next"
+          :total="total"
+          :page-size="queryParams.pageSize"
+          :current-page="queryParams.pageNumber"
+          @current-change="handlePageChange"
+      />
+    </div>
+
+    <!-- 执行历史视图 -->
+    <div v-if="viewMode === 'history'" class="history-view">
+      <el-table :data="executionHistory" border style="width: 100%" @row-click="viewExecutionDetail">
+        <el-table-column prop="id" label="ID" align="center" width="80" />
+        <el-table-column prop="timetableId" label="时间表ID" align="center" />
+        <el-table-column prop="executionTime" label="执行时间" align="center" width="180" />
+        <el-table-column prop="status" label="状态" align="center" />
+        <el-table-column prop="result" label="结果" align="center" show-overflow-tooltip />
+      </el-table>
+      <!-- 分页 -->
+      <el-pagination
+          v-if="historyTotal > 0"
+          background
+          layout="prev, pager, next"
+          :total="historyTotal"
+          :page-size="historyQueryParams.pageSize"
+          :current-page="historyQueryParams.pageNumber"
+          @current-change="handleHistoryPageChange"
+      />
+    </div>
+
+    <!-- 新增/编辑时间表对话框 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
+      <el-form :model="timetableForm" label-width="120px">
+        <el-form-item label="标题">
+          <el-input v-model="timetableForm.title" placeholder="请输入标题" />
+        </el-form-item>
+        <el-form-item label="开始时间">
+          <el-date-picker
+              v-model="timetableForm.startTime"
+              type="datetime"
+              placeholder="选择开始时间"
+              value-format="YYYY-MM-DD HH:mm:ss"
+          />
+        </el-form-item>
+        <el-form-item label="结束时间">
+          <el-date-picker
+              v-model="timetableForm.endTime"
+              type="datetime"
+              placeholder="选择结束时间"
+              value-format="YYYY-MM-DD HH:mm:ss"
+          />
+        </el-form-item>
+        <el-form-item label="设备">
+          <el-select v-model="timetableForm.devices" multiple placeholder="请选择设备">
+            <el-option label="设备A" value="deviceA" />
+            <el-option label="设备B" value="deviceB" />
+            <el-option label="设备C" value="deviceC" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="指令">
+          <el-input v-model="timetableForm.commands" type="textarea" :rows="4" placeholder="请输入指令" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveTimetable">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 查看时间表详情对话框 -->
+    <el-dialog v-model="detailDialogVisible" title="时间表详情" width="50%">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="ID">{{ selectedTimetable.id }}</el-descriptions-item>
+        <el-descriptions-item label="标题">{{ selectedTimetable.title }}</el-descriptions-item>
+        <el-descriptions-item label="开始时间">{{ selectedTimetable.startTime }}</el-descriptions-item>
+        <el-descriptions-item label="结束时间">{{ selectedTimetable.endTime }}</el-descriptions-item>
+        <el-descriptions-item label="设备">{{ selectedTimetable.devices.join(', ') }}</el-descriptions-item>
+        <el-descriptions-item label="指令">{{ selectedTimetable.commands }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+
+    <!-- 查看执行历史详情对话框 -->
+    <el-dialog v-model="historyDetailDialogVisible" title="执行历史详情" width="50%">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="ID">{{ selectedExecution.id }}</el-descriptions-item>
+        <el-descriptions-item label="时间表ID">{{ selectedExecution.timetableId }}</el-descriptions-item>
+        <el-descriptions-item label="执行时间">{{ selectedExecution.executionTime }}</el-descriptions-item>
+        <el-descriptions-item label="状态">{{ selectedExecution.status }}</el-descriptions-item>
+        <el-descriptions-item label="结果">{{ selectedExecution.result }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+
+// 视图模式(日历视图或列表视图或执行历史)
+const viewMode = ref('calendar')
+// 当前日期
+const currentDate = ref(new Date())
+// 时间表列表数据
+const timetableList = ref([])
+const total = ref(0)
+// 查询参数
+const queryParams = ref({
+  pageNumber: 1,
+  pageSize: 10
+})
+// 执行历史数据
+const executionHistory = ref([])
+const historyTotal = ref(0)
+// 执行历史查询参数
+const historyQueryParams = ref({
+  pageNumber: 1,
+  pageSize: 10
+})
+// 控制弹窗显示和内容
+const dialogVisible = ref(false)
+const detailDialogVisible = ref(false)
+const historyDetailDialogVisible = ref(false)
+const dialogTitle = ref('新增时间表')
+const timetableForm = ref({
+  id: '',
+  title: '',
+  startTime: '',
+  endTime: '',
+  devices: [],
+  commands: ''
+})
+const selectedTimetable = ref({})
+const selectedExecution = ref({})
+// 模拟时间表数据
+const timetableMap = ref({
+  '2025-04-01': [
+    { id: 'T001', title: '设备A维护', startTime: '2025-04-01 08:00:00', endTime: '2025-04-01 10:00:00', devices: ['deviceA'], commands: '维护指令', status: 'success' },
+    { id: 'T002', title: '设备B检查', startTime: '2025-04-01 12:00:00', endTime: '2025-04-01 14:00:00', devices: ['deviceB'], commands: '检查指令', status: 'failed' }
+  ],
+  '2025-04-02': [
+    { id: 'T003', title: '设备C清洗', startTime: '2025-04-02 09:00:00', endTime: '2025-04-02 11:00:00', devices: ['deviceC'], commands: '清洗指令', status: 'success' }
+  ]
+})
+// 模拟执行历史数据
+const executionHistoryMap = ref([
+  { id: 'H001', timetableId: 'T001', executionTime: '2025-04-01 08:00:00', status: '成功', result: '设备A维护完成' },
+  { id: 'H002', timetableId: 'T002', executionTime: '2025-04-01 12:00:00', status: '失败', result: '设备B检查异常' },
+  { id: 'H003', timetableId: 'T003', executionTime: '2025-04-02 09:00:00', status: '成功', result: '设备C清洗完成' },
+  { id: 'H004', timetableId: 'T001', executionTime: '2025-04-03 10:00:00', status: '成功', result: '设备A测试完成' },
+  { id: 'H005', timetableId: 'T002', executionTime: '2025-04-04 14:00:00', status: '成功', result: '设备B升级完成' }
+])
+// 右键菜单相关变量
+const contextMenuVisible = ref(false)
+const contextMenuX = ref(0)
+const contextMenuY = ref(0)
+const selectedDay = ref('')
+const selectedDayEvents = ref([])
+
+// 获取当前日期的时间表
+const getEvents = (day) => {
+  return timetableMap.value[day] || []
+}
+
+// 判断某天是否有事件
+const hasEvents = (day) => {
+  return timetableMap.value[day] && timetableMap.value[day].length > 0
+}
+
+// 判断是否是今天
+const isToday = (day) => {
+  const today = new Date().toISOString().split('T')[0]
+  return day === today
+}
+
+// 获取事件标签类型
+const getTagType = (status) => {
+  switch (status) {
+    case 'success':
+      return 'success'
+    case 'failed':
+      return 'danger'
+    default:
+      return 'info'
+  }
+}
+
+// 获取事件提示信息
+const getTooltip = (event) => {
+  return `标题: ${event.title}\n开始时间: ${event.startTime}\n结束时间: ${event.endTime}\n设备: ${event.devices.join(', ')}\n指令: ${event.commands}`
+}
+
+// 切换视图模式
+const handleTabClick = () => {
+  if (viewMode.value === 'list') {
+    fetchTimetables()
+  } else if (viewMode.value === 'history') {
+    fetchExecutionHistory()
+  }
+}
+
+// 获取时间表列表
+const fetchTimetables = () => {
+  // 模拟异步请求
+  setTimeout(() => {
+    const mockData = [
+      { id: 'T001', title: '设备A维护', startTime: '2025-04-01 08:00:00', endTime: '2025-04-01 10:00:00', devices: ['deviceA'], commands: '维护指令', status: 'success' },
+      { id: 'T002', title: '设备B检查', startTime: '2025-04-01 12:00:00', endTime: '2025-04-01 14:00:00', devices: ['deviceB'], commands: '检查指令', status: 'failed' },
+      { id: 'T003', title: '设备C清洗', startTime: '2025-04-02 09:00:00', endTime: '2025-04-02 11:00:00', devices: ['deviceC'], commands: '清洗指令', status: 'success' },
+      { id: 'T004', title: '设备A测试', startTime: '2025-04-03 10:00:00', endTime: '2025-04-03 12:00:00', devices: ['deviceA'], commands: '测试指令', status: 'success' },
+      { id: 'T005', title: '设备B升级', startTime: '2025-04-04 14:00:00', endTime: '2025-04-04 16:00:00', devices: ['deviceB'], commands: '升级指令', status: 'success' }
+    ]
+    // 模拟分页
+    const start = (queryParams.value.pageNumber - 1) * queryParams.value.pageSize
+    const end = start + queryParams.value.pageSize
+    timetableList.value = mockData.slice(start, end)
+    total.value = mockData.length
+  }, 300)
+}
+
+// 获取执行历史
+const fetchExecutionHistory = () => {
+  // 模拟异步请求
+  setTimeout(() => {
+    // 模拟分页
+    const start = (historyQueryParams.value.pageNumber - 1) * historyQueryParams.value.pageSize
+    const end = start + historyQueryParams.value.pageSize
+    executionHistory.value = executionHistoryMap.value.slice(start, end)
+    historyTotal.value = executionHistoryMap.value.length
+  }, 300)
+}
+
+// 新增时间表
+const addTimetable = () => {
+  dialogTitle.value = '新增时间表'
+  timetableForm.value = {
+    id: '',
+    title: '',
+    startTime: '',
+    endTime: '',
+    devices: [],
+    commands: ''
+  }
+  dialogVisible.value = true
+}
+
+// 新增某一天的时间表
+const addTimetableForDay = (day) => {
+  dialogTitle.value = '新增时间表'
+  timetableForm.value = {
+    id: '',
+    title: '',
+    startTime: `${day} 08:00:00`,
+    endTime: `${day} 10:00:00`,
+    devices: [],
+    commands: ''
+  }
+  dialogVisible.value = true
+}
+
+// 编辑时间表
+const editTimetable = (timetable) => {
+  dialogTitle.value = '编辑时间表'
+  timetableForm.value = { ...timetable }
+  dialogVisible.value = true
+}
+
+// 删除时间表
+const deleteTimetable = (timetable) => {
+  ElMessage.confirm('确定要删除该时间表吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    // 模拟删除操作
+    setTimeout(() => {
+      ElMessage.success('时间表删除成功')
+      fetchTimetables()
+    }, 300)
+  }).catch(() => {})
+}
+
+// 保存时间表
+const saveTimetable = () => {
+  if (!timetableForm.value.title || !timetableForm.value.startTime || !timetableForm.value.endTime || timetableForm.value.devices.length === 0 || !timetableForm.value.commands) {
+    ElMessage.warning('请填写完整的时间表信息')
+    return
+  }
+  // 模拟保存操作
+  setTimeout(() => {
+    ElMessage.success('时间表保存成功')
+    dialogVisible.value = false
+    fetchTimetables()
+  }, 300)
+}
+
+// 查看时间表详情
+const viewEvent = (event) => {
+  selectedTimetable.value = event
+  detailDialogVisible.value = true
+}
+
+// 查看执行历史详情
+const viewExecutionDetail = (execution) => {
+  selectedExecution.value = execution
+  historyDetailDialogVisible.value = true
+}
+
+// 处理日期点击事件
+const handleDateClick = (day) => {
+  if (!hasEvents(day)) {
+    addTimetableForDay(day)
+  }
+}
+
+// 显示右键菜单
+const showContextMenu = (day, event) => {
+  contextMenuVisible.value = true
+  contextMenuX.value = event.clientX
+  contextMenuY.value = event.clientY
+  selectedDay.value = day
+  selectedDayEvents.value = getEvents(day)
+}
+
+// 处理右键菜单命令
+const handleContextCommand = (command) => {
+  if (command === 'add') {
+    addTimetableForDay(selectedDay.value)
+  } else if (command === 'view') {
+    if (selectedDayEvents.value.length > 0) {
+      viewEvent(selectedDayEvents.value[0]) // 默认查看第一个事件
+    }
+  }
+}
+
+// 分页变化处理
+const handlePageChange = (page) => {
+  queryParams.value.pageNumber = page
+  fetchTimetables()
+}
+
+// 执行历史分页变化处理
+const handleHistoryPageChange = (page) => {
+  historyQueryParams.value.pageNumber = page
+  fetchExecutionHistory()
+}
+
+// 页面加载时初始化数据
+onMounted(() => {
+  fetchTimetables()
+})
+</script>
+
+<style scoped lang="scss">
+.timetable-management-container {
+  padding: 20px;
+}
+.header-card {
+  margin-bottom: 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.calendar-view {
+  .el-calendar {
+    --el-calendar-day-height: 80px;
+  }
+  .date-cell {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+    p {
+      font-size: 14px;
+    }
+    .highlight {
+      font-weight: bold;
+      color: #409EFF;
+    }
+    .event-day {
+      color: #67C23A;
+    }
+    .events {
+      margin-top: 5px;
+      display: flex;
+      flex-wrap: wrap;
+      gap: 5px;
+      .el-tag {
+        max-width: 100%;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+    .el-button--text {
+      padding: 0;
+      font-size: 12px;
+    }
+  }
+}
+.list-view, .history-view {
+  .el-table {
+    margin-bottom: 20px;
+  }
+  .el-pagination {
+    text-align: right;
+  }
+}
+.dialog-footer {
+  text-align: right;
+}
+</style>

+ 252 - 0
pm_ui/src/views/device/spaceScene/index.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="space-scene-container">
+    <!-- 左侧导航树 -->
+    <el-card class="tree-card">
+      <h3>空间层级</h3>
+      <el-tree
+          :data="spaceTree"
+          node-key="id"
+          default-expand-all
+          highlight-current
+          @node-click="handleNodeClick"
+      >
+        <template #default="{ node, data }">
+          <span class="custom-tree-node">
+            <span>{{ node.label }}</span>
+            <el-tag v-if="data.type === 'building'" size="small" type="primary">楼栋</el-tag>
+            <el-tag v-if="data.type === 'floor'" size="small" type="success">楼层</el-tag>
+            <el-tag v-if="data.type === 'room'" size="small" type="info">房间</el-tag>
+          </span>
+        </template>
+      </el-tree>
+    </el-card>
+
+    <!-- 右侧场景展示 -->
+    <el-card class="scene-card">
+      <h3>场景图</h3>
+      <div class="scene-view">
+        <img v-if="currentScene.image" :src="currentScene.image" alt="场景图" class="scene-image" />
+        <p v-else>请选择一个空间层级以查看场景图。</p>
+      </div>
+      <div class="config-info" v-if="currentScene.config">
+        <h4>组态配置</h4>
+        <pre>{{ currentScene.config }}</pre>
+        <el-button type="primary" @click="editConfig">编辑配置</el-button>
+      </div>
+    </el-card>
+
+    <!-- 组态编辑对话框 -->
+    <el-dialog v-model="dialogVisible" title="编辑组态配置" width="50%">
+      <el-form :model="configForm" label-width="120px">
+        <el-form-item label="设备列表">
+          <el-input v-model="configForm.devices" placeholder="逗号分隔的设备列表" />
+        </el-form-item>
+        <el-form-item label="布局">
+          <el-select v-model="configForm.layout" placeholder="选择布局">
+            <el-option label="网格" value="grid" />
+            <el-option label="列表" value="list" />
+            <el-option label="自定义" value="custom" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveConfig">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+
+// 模拟空间层级数据
+const spaceTree = ref([
+  {
+    id: 'b1',
+    label: 'A栋',
+    type: 'building',
+    children: [
+      {
+        id: 'f1',
+        label: '1楼',
+        type: 'floor',
+        children: [
+          { id: 'r1', label: '101室', type: 'room' },
+          { id: 'r2', label: '102室', type: 'room' }
+        ]
+      },
+      {
+        id: 'f2',
+        label: '2楼',
+        type: 'floor',
+        children: [
+          { id: 'r3', label: '201室', type: 'room' },
+          { id: 'r4', label: '202室', type: 'room' }
+        ]
+      }
+    ]
+  },
+  {
+    id: 'b2',
+    label: 'B栋',
+    type: 'building',
+    children: [
+      {
+        id: 'f3',
+        label: '1楼',
+        type: 'floor',
+        children: [
+          { id: 'r5', label: '101室', type: 'room' },
+          { id: 'r6', label: '102室', type: 'room' }
+        ]
+      }
+    ]
+  }
+])
+
+// 当前选中的场景
+const currentScene = ref({
+  image: '',
+  config: null
+})
+
+// 场景图片和组态配置映射(模拟数据)
+const sceneMap = {
+  'r1': {
+    image: 'https://dummyimage.com/600x400/007bff/ffffff&text=Room+101 ',
+    config: { devices: ['light1', 'sensor1'], layout: 'grid' }
+  },
+  'r2': {
+    image: 'https://dummyimage.com/600x400/28a745/ffffff&text=Room+102 ',
+    config: { devices: ['camera1', 'sensor2'], layout: 'list' }
+  },
+  'r3': {
+    image: 'https://dummyimage.com/600x400/ffc107/ffffff&text=Room+201 ',
+    config: { devices: ['ac1', 'sensor3'], layout: 'custom' }
+  },
+  'r4': {
+    image: 'https://dummyimage.com/600x400/fc3549/ffffff&text=Room+202 ',
+    config: { devices: ['light2', 'sensor4'], layout: 'grid' }
+  },
+  'r5': {
+    image: 'https://dummyimage.com/600x400/17a2b8/ffffff&text=Room+101+B ',
+    config: { devices: ['light3', 'sensor5'], layout: 'list' }
+  },
+  'r6': {
+    image: 'https://dummyimage.com/600x400/20c997/ffffff&text=Room+102+B ',
+    config: { devices: ['camera2', 'sensor6'], layout: 'custom' }
+  }
+}
+
+// 组态编辑对话框显示控制
+const dialogVisible = ref(false)
+
+// 当前编辑的配置
+const configForm = ref({
+  devices: '',
+  layout: ''
+})
+
+// 处理树节点点击事件
+const handleNodeClick = (data) => {
+  if (data.type === 'room') {
+    // 加载对应房间的场景图和组态配置
+    const sceneData = sceneMap[data.id]
+    if (sceneData) {
+      currentScene.value = sceneData
+      configForm.value.devices = sceneData.config.devices.join(', ')
+      configForm.value.layout = sceneData.config.layout
+    } else {
+      currentScene.value = { image: '', config: null }
+    }
+  } else {
+    // 如果是楼栋或楼层,清空场景图
+    currentScene.value = { image: '', config: null }
+  }
+}
+
+// 编辑配置
+const editConfig = () => {
+  if (currentScene.value.config) {
+    dialogVisible.value = true
+  } else {
+    ElMessage.warning('请先选择一个房间以编辑配置')
+  }
+}
+
+// 保存配置
+const saveConfig = () => {
+  if (currentScene.value.config) {
+    currentScene.value.config.devices = configForm.value.devices.split(',').map(device => device.trim())
+    currentScene.value.config.layout = configForm.value.layout
+    ElMessage.success('配置保存成功')
+    dialogVisible.value = false
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.space-scene-container {
+  display: flex;
+  height: 100vh;
+  padding: 20px;
+
+  .tree-card {
+    width: 300px;
+    margin-right: 20px;
+
+    h3 {
+      margin-bottom: 10px;
+      font-size: 18px;
+      color: #333;
+    }
+
+    .custom-tree-node {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      width: 100%;
+    }
+  }
+
+  .scene-card {
+    flex: 1;
+
+    h3 {
+      margin-bottom: 10px;
+      font-size: 18px;
+      color: #333;
+    }
+
+    .scene-view {
+      text-align: center;
+      margin-bottom: 20px;
+
+      .scene-image {
+        max-width: 100%;
+        max-height: 400px;
+        border: 1px solid #ddd;
+        border-radius: 5px;
+      }
+    }
+
+    .config-info {
+      pre {
+        background-color: #f5f5f5;
+        padding: 10px;
+        border-radius: 5px;
+        font-size: 14px;
+        overflow-x: auto;
+      }
+
+      .el-button {
+        margin-top: 10px;
+      }
+    }
+  }
+}
+</style>

+ 375 - 0
pm_ui/src/views/device/systemMonitor/index.vue

@@ -0,0 +1,375 @@
+<template>
+  <div class="system-monitor-container">
+    <!-- 左侧导航树 -->
+    <el-card class="tree-card">
+      <h3>系统层级</h3>
+      <el-tree
+          :data="systemTree"
+          node-key="id"
+          default-expand-all
+          highlight-current
+          @node-click="handleNodeClick"
+      >
+        <template #default="{ node, data }">
+          <span class="custom-tree-node">
+            <span>{{ node.label }}</span>
+            <el-tag v-if="data.type === 'system'" size="small" type="primary">系统</el-tag>
+            <el-tag v-if="data.type === 'subsystem'" size="small" type="success">子系统</el-tag>
+          </span>
+        </template>
+      </el-tree>
+    </el-card>
+
+    <!-- 右侧监控展示 -->
+    <el-card class="monitor-card">
+      <h3>监控组态图</h3>
+      <div class="config-info" v-if="currentSystem.config">
+        <h4>组态配置</h4>
+        <pre>{{ currentSystem.config }}</pre>
+        <el-button type="primary" @click="editConfig">编辑配置</el-button>
+      </div>
+      <div class="chart-container" v-if="currentSystem.data">
+        <h4>实时监控数据</h4>
+        <div ref="chart" class="chart"></div>
+      </div>
+    </el-card>
+
+    <!-- 组态编辑对话框 -->
+    <el-dialog v-model="dialogVisible" title="编辑组态配置" width="50%">
+      <el-form :model="configForm" label-width="120px">
+        <el-form-item label="设备列表">
+          <el-input v-model="configForm.devices" placeholder="逗号分隔的设备列表" />
+        </el-form-item>
+        <el-form-item label="布局">
+          <el-select v-model="configForm.layout" placeholder="选择布局">
+            <el-option label="网格" value="grid" />
+            <el-option label="列表" value="list" />
+            <el-option label="自定义" value="custom" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveConfig">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, nextTick } from 'vue'
+import * as echarts from 'echarts'
+import { ElMessage } from 'element-plus'
+
+// 模拟系统层级数据
+const systemTree = ref([
+  {
+    id: 'sys1',
+    label: '暖通空调系统',
+    type: 'system',
+    children: [
+      { id: 'subsys1', label: '空调机组', type: 'subsystem' },
+      { id: 'subsys2', label: '末端设备', type: 'subsystem' }
+    ]
+  },
+  {
+    id: 'sys2',
+    label: '新风系统',
+    type: 'system',
+    children: [
+      { id: 'subsys3', label: '新风机', type: 'subsystem' },
+      { id: 'subsys4', label: '空气过滤器', type: 'subsystem' }
+    ]
+  },
+  {
+    id: 'sys3',
+    label: '制冷机房',
+    type: 'system',
+    children: [
+      { id: 'subsys5', label: '冷水机组', type: 'subsystem' },
+      { id: 'subsys6', label: '冷却塔', type: 'subsystem' }
+    ]
+  },
+  {
+    id: 'sys4',
+    label: '水系统',
+    type: 'system',
+    children: [
+      { id: 'subsys7', label: '水泵', type: 'subsystem' },
+      { id: 'subsys8', label: '水箱', type: 'subsystem' }
+    ]
+  },
+  {
+    id: 'sys5',
+    label: '电梯系统',
+    type: 'system',
+    children: [
+      { id: 'subsys9', label: '电梯A', type: 'subsystem' },
+      { id: 'subsys10', label: '电梯B', type: 'subsystem' }
+    ]
+  },
+  {
+    id: 'sys6',
+    label: '配电系统',
+    type: 'system',
+    children: [
+      { id: 'subsys11', label: '配电柜', type: 'subsystem' },
+      { id: 'subsys12', label: '断路器', type: 'subsystem' }
+    ]
+  },
+  {
+    id: 'sys7',
+    label: '照明系统',
+    type: 'system',
+    children: [
+      { id: 'subsys13', label: '照明A', type: 'subsystem' },
+      { id: 'subsys14', label: '照明B', type: 'subsystem' }
+    ]
+  }
+])
+
+// 当前选中的系统
+const currentSystem = ref({
+  config: null,
+  data: null
+})
+
+// 组态配置映射(模拟数据)
+const systemConfigMap = {
+  'subsys1': {
+    config: { devices: ['ac_unit1', 'fan1'], layout: 'grid' },
+    data: {
+      temperature: [22, 23, 21, 24, 22, 23, 21],
+      humidity: [45, 50, 55, 48, 49, 47, 50]
+    }
+  },
+  'subsys2': {
+    config: { devices: ['end_device1', 'end_device2'], layout: 'list' },
+    data: {
+      temperature: [20, 21, 22, 20, 21, 22, 20],
+      humidity: [55, 60, 65, 62, 63, 61, 64]
+    }
+  },
+  'subsys3': {
+    config: { devices: ['ventilator1', 'filter1'], layout: 'custom' },
+    data: {
+      airflow: [100, 110, 120, 105, 115, 125, 110]
+    }
+  },
+  'subsys4': {
+    config: { devices: ['filter2', 'filter3'], layout: 'grid' },
+    data: {
+      airflow: [90, 95, 100, 92, 97, 102, 95]
+    }
+  },
+  'subsys5': {
+    config: { devices: ['chiller1', 'chiller2'], layout: 'list' },
+    data: {
+      temperature: [18, 19, 17, 18, 19, 17, 18]
+    }
+  },
+  'subsys6': {
+    config: { devices: ['cooling_tower1', 'cooling_tower2'], layout: 'custom' },
+    data: {
+      temperature: [25, 26, 24, 25, 26, 24, 25]
+    }
+  },
+  'subsys7': {
+    config: { devices: ['pump1', 'pump2'], layout: 'grid' },
+    data: {
+      flow_rate: [50, 55, 60, 52, 57, 62, 55]
+    }
+  },
+  'subsys8': {
+    config: { devices: ['tank1', 'tank2'], layout: 'list' },
+    data: {
+      water_level: [80, 85, 90, 82, 87, 92, 85]
+    }
+  },
+  'subsys9': {
+    config: { devices: ['elevator_a1', 'elevator_a2'], layout: 'custom' },
+    data: {
+      status: ['正常', '正常', '正常', '正常', '正常', '正常', '正常']
+    }
+  },
+  'subsys10': {
+    config: { devices: ['elevator_b1', 'elevator_b2'], layout: 'grid' },
+    data: {
+      status: ['正常', '正常', '正常', '正常', '正常', '正常', '正常']
+    }
+  },
+  'subsys11': {
+    config: { devices: ['switchboard1', 'switchboard2'], layout: 'list' },
+    data: {
+      voltage: [220, 225, 230, 222, 227, 232, 225]
+    }
+  },
+  'subsys12': {
+    config: { devices: ['breaker1', 'breaker2'], layout: 'custom' },
+    data: {
+      current: [10, 12, 11, 10, 12, 11, 10]
+    }
+  },
+  'subsys13': {
+    config: { devices: ['light_a1', 'light_a2'], layout: 'grid' },
+    data: {
+      power_usage: [50, 55, 60, 52, 57, 62, 55]
+    }
+  },
+  'subsys14': {
+    config: { devices: ['light_b1', 'light_b2'], layout: 'list' },
+    data: {
+      power_usage: [40, 45, 50, 42, 47, 52, 45]
+    }
+  }
+}
+
+// 组态编辑对话框显示控制
+const dialogVisible = ref(false)
+
+// 当前编辑的配置
+const configForm = ref({
+  devices: '',
+  layout: ''
+})
+
+// ECharts 实例
+const chart = ref(null)
+let chartInstance = null
+
+// 处理树节点点击事件
+const handleNodeClick = (data) => {
+  if (data.type === 'subsystem') {
+    // 加载对应子系统的组态配置和监控数据
+    const systemData = systemConfigMap[data.id]
+    if (systemData) {
+      currentSystem.value = systemData
+      configForm.value.devices = systemData.config.devices.join(', ')
+      configForm.value.layout = systemData.config.layout
+      nextTick(() => {
+        initChart(systemData.data)
+      })
+    } else {
+      currentSystem.value = { config: null, data: null }
+    }
+  } else {
+    // 如果是系统节点,清空监控数据
+    currentSystem.value = { config: null, data: null }
+  }
+}
+
+// 初始化 ECharts 图表
+const initChart = (data) => {
+  if (chartInstance) {
+    chartInstance.dispose()
+  }
+  chartInstance = echarts.init(chart.value)
+  const option = {
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: Object.keys(data)
+    },
+    xAxis: {
+      type: 'category',
+      data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: Object.keys(data).map(key => ({
+      name: key,
+      type: 'line',
+      data: data[key]
+    }))
+  }
+  chartInstance.setOption(option)
+}
+
+// 编辑配置
+const editConfig = () => {
+  if (currentSystem.value.config) {
+    dialogVisible.value = true
+  } else {
+    ElMessage.warning('请先选择一个子系统以编辑配置')
+  }
+}
+
+// 保存配置
+const saveConfig = () => {
+  if (currentSystem.value.config) {
+    currentSystem.value.config.devices = configForm.value.devices.split(',').map(device => device.trim())
+    currentSystem.value.config.layout = configForm.value.layout
+    ElMessage.success('配置保存成功')
+    dialogVisible.value = false
+  }
+}
+
+// 页面加载时初始化图表
+onMounted(() => {
+  // 可以在这里加载默认的子系统数据
+  // 例如:handleNodeClick(systemTree.value[0].children[0])
+})
+</script>
+
+<style scoped lang="scss">
+.system-monitor-container {
+  display: flex;
+  height: 100vh;
+  padding: 20px;
+
+  .tree-card {
+    width: 300px;
+    margin-right: 20px;
+
+    h3 {
+      margin-bottom: 10px;
+      font-size: 18px;
+      color: #333;
+    }
+
+    .custom-tree-node {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      width: 100%;
+    }
+  }
+
+  .monitor-card {
+    flex: 1;
+
+    h3 {
+      margin-bottom: 10px;
+      font-size: 18px;
+      color: #333;
+    }
+
+    .config-info {
+      pre {
+        background-color: #f5f5f5;
+        padding: 10px;
+        border-radius: 5px;
+        font-size: 14px;
+        overflow-x: auto;
+      }
+
+      .el-button {
+        margin-top: 10px;
+      }
+    }
+
+    .chart-container {
+      margin-top: 20px;
+
+      .chart {
+        width: 100%;
+        height: 400px;
+      }
+    }
+  }
+}
+</style>

+ 1 - 1
pm_ui/src/views/device/waring/index.vue

@@ -369,7 +369,7 @@ function handleDelete(row) {
 
 /** 导出按钮操作 */
 function handleExport() {
-  proxy.download('waring/waring/export', {
+  proxy.download('device/waring/export', {
     ...queryParams.value
   }, `waring_${new Date().getTime()}.xlsx`)
 }

+ 577 - 0
pm_ui/src/views/energyManagement/EnergyConsumptionRanking/index.vue

@@ -0,0 +1,577 @@
+<template>
+  <div class="app">
+    <!-- 左侧区域 -->
+    <div class="sidebar">
+
+      <!-- 树形结构 -->
+      <div class="tree-container">
+        <el-table
+            ref="tableRef"
+            v-loading="loading"
+            :data="topologyList"
+            @current-change="handleNodeClick"
+            row-key="itemId"
+            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+            :default-expand-all="true"
+            :indent="24"
+            class="custom-table"
+            :show-header="false"
+            :highlight-current-row="true"
+            :border="false"
+        >
+          <el-table-column align="left" prop="itemName">
+            <template #default="{ row }">
+              <i
+                  v-if="row.children && row.children.length > 0"
+                  :class="row.expanded ? 'el-icon-folder-opened' : 'el-icon-folder'"
+                  style="margin-right: 5px"
+              ></i>
+              <i v-else class="el-icon-document" style="margin-right: 5px"></i>
+              {{ row.itemName }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+
+    <!-- 右侧区域 -->
+    <div class="main-content">
+      <!-- 能耗排名 -->
+      <div class="energy-ranking" v-if="showRanking">
+        <div class="ranking-controls">
+          <el-select v-model="rankingType" placeholder="选择能耗类型">
+            <el-option label="电量" value="electricity"></el-option>
+            <el-option label="水量" value="water"></el-option>
+            <el-option label="气量" value="gas"></el-option>
+          </el-select>
+
+          <el-select v-model="timeRange" placeholder="选择时间范围">
+            <el-option label="日" value="day"></el-option>
+            <el-option label="月" value="month"></el-option>
+            <el-option label="年" value="year"></el-option>
+          </el-select>
+
+          <el-select v-model="department" placeholder="选择科室" v-if="showDepartment">
+            <el-option
+                v-for="dept in departments"
+                :key="dept.id"
+                :label="dept.name"
+                :value="dept.id">
+            </el-option>
+          </el-select>
+
+          <button @click="loadRankingData" class="ranking-btn">查询排名</button>
+        </div>
+
+        <el-table :data="rankingData" border style="width: 100%">
+          <el-table-column prop="rank" label="排名" width="80"></el-table-column>
+          <el-table-column prop="name" label="区域"></el-table-column>
+          <el-table-column prop="value" :label="rankingTypeLabel"></el-table-column>
+          <el-table-column prop="ratio" label="占比(%)"></el-table-column>
+        </el-table>
+        <div class="chart-row">
+          <div ref="pieChartRef" class="chart-container"></div>
+          <div ref="barChartRef" class="chart-container"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, nextTick,watch } from 'vue';
+import * as echarts from 'echarts';
+import { listTopology } from '@/api/device/topology';
+import { ElMessage } from 'element-plus';
+
+// 数据定义
+const startDate = ref('2025-05-01');
+const endDate = ref('2025-05-08');
+const topologyList = ref([]);
+const loading = ref(true);
+const selectedNode = ref(null);
+
+
+// 获取树形数据
+const getList = async () => {
+  try {
+    loading.value = true;
+    const response = await listTopology({ pageNum: 1, pageSize: 10000 });
+    topologyList.value = handleTree(response.rows, 'itemId', 'parentId', 'children');
+    loading.value = false;
+  } catch (error) {
+    ElMessage.error('加载数据失败');
+    loading.value = false;
+  }
+};
+
+// 处理树形结构的方法
+const handleTree = (data, id, parentId, childrenName = 'children') => {
+  const config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenName: childrenName || 'children'
+  };
+  const tree = [];
+  const map = {};
+  data.forEach(item => (map[item[config.id]] = item));
+  data.forEach(item => {
+    const parent = map[item[config.parentId]];
+    if (parent) {
+      (parent[config.childrenName] || (parent[config.childrenName] = [])).push(item);
+    } else {
+      tree.push(item);
+    }
+  });
+
+  return tree;
+};
+
+// 点击节点
+const handleNodeClick = (node) => {
+  selectedNode.value = node;
+  // 默认选择电量+日
+  rankingType.value = 'electricity';
+  timeRange.value = 'day';
+  // 自动触发查询
+  loadRankingData();
+};
+
+// 能耗排名相关数据
+const showRanking = ref(true);
+const rankingType = ref('electricity');
+const timeRange = ref('day');
+const department = ref('');
+const departments = ref([]);
+const rankingData = ref([]);
+const showDepartment = computed(() => selectedNode.value?.type === 'department');
+
+const rankingTypeLabel = computed(() => {
+  switch(rankingType.value) {
+    case 'electricity': return '用电量(kWh)';
+    case 'water': return '用水量(t)';
+    case 'gas': return '用气量(m³)';
+    default: return '';
+  }
+});
+
+// 修改查询按钮点击事件
+const onQueryClick = () => {
+  if (!rankingType.value || !timeRange.value) {
+    ElMessage.warning('请选择能耗类型和时间范围');
+    return;
+  }
+  loadRankingData();
+};
+
+const loadRankingData = async () => {
+  try {
+    if (!selectedNode.value) return;
+
+    // 根据选择类型构建查询参数
+    const params = {
+      type: rankingType.value,
+      range: timeRange.value,
+      nodeId: selectedNode.value.itemId
+    };
+
+    if (showDepartment.value && department.value) {
+      params.departmentId = department.value;
+    }
+
+    // 生成随机区域名称和模拟数据
+    const generateRandomData = () => {
+      const data = [];
+      const regions = ['东区', '西区', '南区', '北区', '中区', 'A栋', 'B栋', 'C栋', 'D栋', 'E栋'];
+      const maxValue = rankingType.value === 'electricity' ? 2000 :
+          rankingType.value === 'water' ? 1000 : 500;
+
+      // 随机打乱区域名称
+      const shuffledRegions = [...regions].sort(() => 0.5 - Math.random());
+
+      for (let i = 0; i < 10; i++) {
+        const value = Math.floor(Math.random() * maxValue);
+        data.push({
+          rank: i + 1,
+          name: shuffledRegions[i],
+          value: value,
+          ratio: `${Math.round((value / maxValue) * 100)}%`
+        });
+      }
+      return data;
+    };
+
+    rankingData.value = generateRandomData();
+  } catch (error) {
+    ElMessage.error('获取排名数据失败');
+  }
+};
+
+const pieChartRef = ref(null);
+let pieChartInstance = null;
+
+// 渲染图表
+const renderChart = () => {
+  if (!pieChartRef.value || !rankingData.value.length) return;
+
+  if (!pieChartInstance) {
+    pieChartInstance = echarts.init(pieChartRef.value);
+  }
+
+  const option = {
+    title: {
+      text: `${rankingTypeLabel.value}排名`,
+      left: 'center'
+    },
+    tooltip: {
+      trigger: 'item',
+      formatter: '{a} <br/>{b}: {c} ({d}%)'
+    },
+    legend: {
+      orient: 'vertical',
+      left: 'left',
+      data: rankingData.value.map(item => item.name)
+    },
+    series: [
+      {
+        name: '占比',
+        type: 'pie',
+        radius: '50%',
+        data: rankingData.value.map(item => ({
+          value: item.value,
+          name: item.name
+        })),
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
+          }
+        }
+      }
+    ]
+  };
+
+  pieChartInstance.setOption(option);
+};
+
+// 监听数据变化重新渲染图表
+watch(rankingData, () => {
+  nextTick(() => {
+    renderChart();
+  });
+}, { deep: true });
+
+// 页面初始化时加载数据
+onMounted(() => {
+  getList();
+  window.addEventListener('resize', () => {
+    if (pieChartInstance) {
+      chartInstance.resize();
+    }
+  });
+});
+
+const barChartRef = ref(null);
+let barChartInstance = null;
+
+// 渲染柱状图
+const renderBarChart = () => {
+  if (!barChartRef.value || !rankingData.value.length) return;
+
+  if (!barChartInstance) {
+    barChartInstance = echarts.init(barChartRef.value);
+  }
+
+  const option = {
+    title: {
+      text: `${rankingTypeLabel.value}排名柱状图`,
+      left: 'center'
+    },
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    xAxis: {
+      type: 'category',
+      data: rankingData.value.map(item => item.name),
+      axisLabel: {
+        rotate: 30
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: rankingTypeLabel.value
+    },
+    series: [{
+      name: '能耗值',
+      type: 'bar',
+      data: rankingData.value.map(item => item.value),
+      itemStyle: {
+        color: function(params) {
+          const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'];
+          return colorList[params.dataIndex % colorList.length];
+        }
+      }
+    }]
+  };
+
+  barChartInstance.setOption(option);
+};
+
+// 监听数据变化重新渲染图表
+watch(rankingData, () => {
+  nextTick(() => {
+    renderBarChart();
+  });
+}, { deep: true });
+
+// 页面初始化时加载数据
+onMounted(() => {
+  getList();
+  window.addEventListener('resize', () => {
+    if (barChartInstance) {
+      barChartInstance.resize();
+    }
+  });
+});
+
+onBeforeUnmount(() => {
+  if (pieChartInstance) {
+    pieChartInstance.dispose();
+    pieChartInstance = null;
+  }
+  if (barChartInstance) {
+    barChartInstance.dispose();
+    barChartInstance = null;
+  }
+  window.removeEventListener('resize', () => {
+    if (pieChartInstance) pieChartInstance.resize();
+    if (barChartInstance) barChartInstance.resize();
+  });
+});
+// 页面初始化时加载数据
+onMounted(() => {
+  getList();
+});
+
+</script>
+
+<style scoped>
+.chart-row {
+  display: flex;
+  gap: 20px;
+  margin-top: 20px;
+}
+
+.chart-container {
+  flex: 1;
+  height: 300px;
+  min-width: 0; /* 防止图表溢出 */
+}
+.energy-ranking {
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+.ranking-controls {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 15px;
+}
+
+.ranking-controls .el-select {
+  width: 150px;
+}
+
+.ranking-btn {
+  padding: 8px 16px;
+  background-color: #2196f3;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+
+
+/* 左侧树形表格高亮样式 */
+:deep(.custom-table .el-table__body tr.current-row > td) {
+  background-color: #a6b9dd !important; /* 可以替换为您想要的颜色 */
+  border-radius: 12px !important;  /* 添加圆角效果 */
+  margin: 8px 0 !important;      /* 添加间距使圆角更明显 */
+}
+
+/* 如果需要同时修改文字颜色 */
+:deep(.custom-table .el-table__body tr.current-row > td .cell) {
+  color: #fff !important;
+}
+.custom-table :deep(.el-table__row) {
+  background: transparent !important;
+}
+.custom-table :deep(.el-table__cell) {
+  padding: 6px 0 !important;  /* 减小上下内边距 */
+  border: none !important;
+}
+.custom-table :deep(.el-table__cell .cell) {
+  line-height: 15px !important;
+  height: 15px !important;
+}
+.custom-table :deep(.el-table__inner-wrapper::before) {
+  display: none;
+}
+.tree-container {
+  height: 990px; /* 可根据需要调整高度 */
+  overflow-y: auto;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+}
+.qy{
+  background-color: white;
+  width: 1000px;
+  margin: 0 auto;
+  border: 1px solid #ddd;
+}
+/* 报告打印按钮*/
+.print-btn {
+  position: absolute;
+  top: 30px;
+  left: 380px;
+  padding: 8px 16px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  z-index: 100;
+}
+
+@media print {
+  .print-btn {
+    display: none;
+  }
+  body * {
+    visibility: hidden;
+  }
+  .qy, .qy * {
+    visibility: visible;
+  }
+  .qy {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+  }
+}
+
+.app {
+  display: flex;
+  background-color: #15203b;
+  color: white;
+  overflow: hidden; /* 防止外层滚动条 */
+}
+
+.sidebar {
+  width: 350px;
+  padding: 10px;
+  background-color: white;
+}
+
+.report-container {
+  text-align: center;
+  height: calc(100vh - 124px);
+  overflow-y: auto;
+  padding: 20px;
+  border: 1px solid #ddd; /* 新增边框 */
+  border-radius: 8px; /* 圆角边框 */
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); /* 阴影效果 */
+  background-color: white; /* 白色背景 */
+}
+
+.chart-section {
+  display: flex;
+  flex-direction: column; /* 改为垂直排列 */
+  align-items: center;
+  gap: 20px;
+  margin: 20px auto;
+  padding-bottom: 20px;
+  width: 100%;
+}
+
+.chart-item {
+  width: 90%; /* 增加宽度 */
+  margin-bottom: 20px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.date-picker input {
+  padding: 6px 10px;
+  border: none;
+  border-radius: 4px;
+  background-color: white;
+  color: #090909;
+  border: 1px solid #ddd;
+  margin-right: 10px;
+}
+
+.generate-report-btn {
+  padding: 10px 20px;
+  background-color: #2196f3;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  margin-top: 20px;
+  width: 100%;
+}
+
+.tree-container {
+  height: 90vh; /* 调整高度计算方式 */
+  overflow-y: auto;
+
+  background-color: white;
+  color: #000000;
+}
+
+.custom-table {
+  background-color: inherit;
+  color: inherit;
+}
+
+.main-content {
+  flex: 1;
+  padding: 20px;
+  background-color: #fff;
+  color: #333;
+  overflow-y: hidden;
+}
+
+.chart-item {
+  width: 48%;
+  margin-bottom: 20px;
+}
+
+.chart {
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+/* 表格样式 */
+.el-table {
+  margin-top: 20px;
+}
+
+.el-table th {
+  background-color: #2196f3;
+  color: white;
+}
+</style>

+ 567 - 0
pm_ui/src/views/energyManagement/analysisReport/index.vue

@@ -0,0 +1,567 @@
+<template>
+  <div class="app">
+    <!-- 左侧区域 -->
+    <div class="sidebar">
+      <!-- 日期选择 -->
+      <div class="date-picker">
+        <input type="date" v-model="startDate" placeholder="开始日期" />
+        <span>-</span>
+        <input type="date" v-model="endDate" placeholder="结束日期" />
+      </div>
+
+      <!-- 树形结构 -->
+      <div class="tree-container">
+        <el-table
+            ref="tableRef"
+            v-loading="loading"
+            :data="topologyList"
+            @current-change="handleNodeClick"
+            row-key="itemId"
+            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+            :default-expand-all="true"
+            :indent="24"
+            class="custom-table"
+            :show-header="false"
+            :highlight-current-row="true"
+            :border="false"
+        >
+          <el-table-column align="left" prop="itemName">
+            <template #default="{ row }">
+              <i
+                  v-if="row.children && row.children.length > 0"
+                  :class="row.expanded ? 'el-icon-folder-opened' : 'el-icon-folder'"
+                  style="margin-right: 5px"
+              ></i>
+              <i v-else class="el-icon-document" style="margin-right: 5px"></i>
+              {{ row.itemName }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <!-- 生成报告按钮 -->
+      <button class="generate-report-btn" @click="generateReport">生成分析报告</button>
+    </div>
+
+    <!-- 右侧区域 -->
+    <div class="main-content">
+      <!-- 分析报告 -->
+      <div class="report-container">
+        <button @click="printReport" class="print-btn">打印</button>
+        <div class="qy">
+          <h1>分析报告</h1>
+          <p>{{ startDate }} ~ {{ endDate }}</p>
+
+          <!-- 用能统计 -->
+          <!-- 用能统计 -->
+          <h3 v-if="energyStatistics.length > 0">1、用能统计</h3>
+          <el-table v-if="energyStatistics.length > 0" :data="energyStatistics" border style="width: 60%;margin: 0 auto">
+            <el-table-column prop="category" label="分类" width="180" />
+            <el-table-column prop="value" label="数值" />
+            <el-table-column prop="ringRatio" label="环比(%)" />
+            <el-table-column prop="yearOnYearRatio" label="同比(%)" />
+          </el-table>
+          <!-- 用电量详细说明 -->
+
+          <p v-if="electricityUsage">
+            <h3>2、用电量</h3>
+            本周期内,共计使用电力 {{ electricityUsage }} kWh,最大用电量 {{ peakElectricity }} kW,最大负荷发生时间 {{ peakTime }}。
+          </p>
+
+          <!-- 图表区域 -->
+          <div class="chart-section">
+            <div v-for="(chartData, index) in chartDatas" :key="index" class="chart-item">
+              <h4>图表 {{ index + 1 }}</h4>
+              <div :id="`chart${index}`" :ref="`chartRef${index}`" class="chart" style="width: 100%; height: 300px;"></div>
+            </div>
+          </div>
+        </div>
+
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, nextTick } from 'vue';
+import * as echarts from 'echarts';
+import { listTopology } from '@/api/device/topology'; // 假设你有这些接口
+import { ElMessage } from 'element-plus';
+
+// 数据定义
+const startDate = ref('2025-05-01');
+const endDate = ref('2025-05-08');
+const topologyList = ref([]);
+const loading = ref(true);
+const selectedNode = ref(null);
+const energyStatistics = ref([]); // 初始化为空数组
+const electricityUsage = ref(''); // 初始化为空
+const peakElectricity = ref(''); // 初始化为空
+const peakTime = ref(''); // 初始化为空
+const generateReport = async () => {
+  if (!selectedNode.value) {
+    alert('请先选择一个节点');
+    return;
+  }
+
+  // 生成用能统计数据
+  energyStatistics.value = [
+    {
+      category: '用电量',
+      value: `${(Math.random() * 1000 + 1000).toFixed(2)} kWh`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    },
+    {
+      category: '用水量',
+      value: `${(Math.random() * 50).toFixed(2)} t`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    },
+    {
+      category: '综合能耗',
+      value: `${(Math.random() * 300).toFixed(2)} kgce`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    },
+    {
+      category: '碳排放量',
+      value: `${(Math.random() * 2000).toFixed(2)} kg`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    }
+  ];
+
+  // 生成用电量数据
+  electricityUsage.value = (Math.random() * 1000 + 1000).toFixed(2);
+  peakElectricity.value = (Math.random() * 100 + 200).toFixed(2);
+  peakTime.value = `${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 30) + 1}`;
+
+  if (!selectedNode.value) {
+    alert('请先选择一个节点');
+    return;
+  }
+
+  // 模拟生成报告内容
+  electricityUsage.value = Math.random() * 1000 + 1000; // 随机生成用电量
+  peakElectricity.value = Math.random() * 100 + 200; // 随机生成最大用电量
+  peakTime.value = `${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 30) + 1}`; // 随机生成时间
+
+  // 更新图表数据 - 改为随机生成
+  chartDatas.value = [
+    {
+      title: '用电量分布',
+      data: Array(7).fill(0).map(() => Math.floor(Math.random() * 200) + 50)
+    },
+    {
+      title: '用水量分布',
+      data: Array(7).fill(0).map(() => Math.floor(Math.random() * 100) + 30)
+    },
+    {
+      title: '综合能耗分布',
+      data: Array(7).fill(0).map(() => Math.floor(Math.random() * 300) + 100)
+    },
+    {
+      title: '碳排放量分布',
+      data: Array(7).fill(0).map(() => Math.floor(Math.random() * 80) + 20)
+    },
+  ];
+
+  // 初始化或更新图表
+  await nextTick();
+  chartDatas.value.forEach((chartData, index) => {
+    const chartInstance = echarts.getInstanceByDom(document.getElementById(`chart${index}`));
+    if (chartInstance) {
+      updateChart(index, chartData);
+    } else {
+      initChart(index, chartData);
+    }
+  });
+};
+
+// 图表数据
+const chartDatas = ref([]);
+
+// 获取树形数据
+const getList = async () => {
+  try {
+    loading.value = true;
+    const response = await listTopology({ pageNum: 1, pageSize: 10000 });
+    topologyList.value = handleTree(response.rows, 'itemId', 'parentId', 'children');
+    loading.value = false;
+  } catch (error) {
+    ElMessage.error('加载数据失败');
+    loading.value = false;
+  }
+};
+
+// 处理树形结构的方法
+const handleTree = (data, id, parentId, childrenName = 'children') => {
+  const config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenName: childrenName || 'children'
+  };
+  const tree = [];
+  const map = {};
+  data.forEach(item => (map[item[config.id]] = item));
+  data.forEach(item => {
+    const parent = map[item[config.parentId]];
+    if (parent) {
+      (parent[config.childrenName] || (parent[config.childrenName] = [])).push(item);
+    } else {
+      tree.push(item);
+    }
+  });
+
+  return tree;
+};
+
+// 点击节点
+const handleNodeClick = (node) => {
+  selectedNode.value = node;
+};
+
+// 生成报告
+/*const generateReport = async () => {
+  if (!selectedNode.value) {
+    alert('请先选择一个节点');
+    return;
+  }
+
+  // 模拟生成报告内容
+  electricityUsage.value = Math.random() * 1000 + 1000; // 随机生成用电量
+  peakElectricity.value = Math.random() * 100 + 200; // 随机生成最大用电量
+  peakTime.value = `${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 30) + 1}`; // 随机生成时间
+
+  // 更新图表数据
+  chartDatas.value = [
+    { title: '用电量分布', data: [120, 200, 150, 80, 70, 110, 130] },
+    { title: '用水量分布', data: [80, 100, 120, 90, 110, 130, 150] },
+    { title: '综合能耗分布', data: [200, 250, 300, 220, 280, 320, 350] },
+    { title: '碳排放量分布', data: [50, 60, 70, 55, 65, 75, 80] },
+  ];
+
+  // 初始化图表
+  await nextTick(); // 确保 DOM 更新完成
+  chartDatas.value.forEach((chartData, index) => {
+    initChart(index, chartData);
+  });
+};*/
+
+// 初始化图表
+const initChart = (index, chartData) => {
+  const chartDom = document.getElementById(`chart${index}`);
+  const chartInstance = echarts.init(chartDom);
+
+  const option = {
+    title: { text: chartData.title, left: 'center' },
+    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+    xAxis: {
+      type: 'category',
+      data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
+      axisLabel: { color: '#333' }
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: { color: '#333' }
+    },
+    series: [
+      {
+        name: chartData.title,
+        type: 'bar',
+        data: chartData.data,
+        itemStyle: { color: getRandomColor() }
+      }
+    ],
+    grid: {
+      top: 30,
+      bottom: 30,
+      left: 60,
+      right: 20
+    }
+  };
+
+  chartInstance.setOption(option);
+};
+
+// 更新图表
+const updateChart = (index, chartData) => {
+  const chartInstance = echarts.getInstanceByDom(document.getElementById(`chart${index}`));
+  if (chartInstance) {
+    chartInstance.setOption({
+      series: [{ data: chartData.data }]
+    });
+  }
+};
+
+// 页面初始化时加载数据
+onMounted(() => {
+  getList();
+});
+
+// 随机颜色生成函数
+function getRandomColor() {
+  const letters = '0123456789ABCDEF';
+  let color = '#';
+  for (let i = 0; i < 6; i++) {
+    color += letters[Math.floor(Math.random() * 16)];
+  }
+  return color;
+}
+
+const printReport = async () => {
+  // 获取所有图表实例并生成图片
+  const chartImages = [];
+  for (let i = 0; i < chartDatas.value.length; i++) {
+    const chartInstance = echarts.getInstanceByDom(document.getElementById(`chart${i}`));
+    if (chartInstance) {
+      const img = new Image();
+      img.src = chartInstance.getDataURL({
+        type: 'png',
+        pixelRatio: 2,
+        backgroundColor: '#fff'
+      });
+      chartImages.push(img);
+    }
+  }
+
+  // 等待所有图片加载完成
+  await Promise.all(chartImages.map(img => new Promise(resolve => {
+    img.onload = resolve;
+  })));
+
+  // 创建打印内容
+  const printWindow = window.open('', '_blank');
+  printWindow.document.write(`
+    <html>
+      <head>
+        <title>分析报告</title>
+        <style>
+          body { font-family: Arial; padding: 20px; }
+          table { width: 60%; margin: 20px auto; border-collapse: collapse; }
+          th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
+          .chart-img { width: 100%; margin: 20px 0; }
+        </style>
+      </head>
+      <body>
+        <h1>分析报告</h1>
+        <p>${startDate.value} ~ ${endDate.value}</p>
+
+        <h3>1、用能统计</h3>
+        <table border="1">
+          <tr>
+            <th>分类</th>
+            <th>数值</th>
+            <th>环比(%)</th>
+            <th>同比(%)</th>
+          </tr>
+          ${energyStatistics.value.map(item => `
+            <tr>
+              <td>${item.category}</td>
+              <td>${item.value}</td>
+              <td>${item.ringRatio}</td>
+              <td>${item.yearOnYearRatio}</td>
+            </tr>
+          `).join('')}
+        </table>
+
+        <h3>2、用电量</h3>
+        <p>
+          本周期内,共计使用电力 ${electricityUsage.value} kWh,最大用电量 ${peakElectricity.value} kW,最大负荷发生时间 ${peakTime.value}。
+        </p>
+
+        <h3>3、图表分析</h3>
+        ${chartImages.map((img, index) => `
+          <h4>图表 ${index + 1}</h4>
+          <img class="chart-img" src="${img.src}" />
+        `).join('')}
+      </body>
+    </html>
+  `);
+  printWindow.document.close();
+  printWindow.focus();
+  setTimeout(() => {
+    printWindow.print();
+    printWindow.close();
+  }, 500);
+};
+
+</script>
+
+<style scoped>
+/* 左侧树形表格高亮样式 */
+:deep(.custom-table .el-table__body tr.current-row > td) {
+  background-color: #a6b9dd !important; /* 可以替换为您想要的颜色 */
+  border-radius: 12px !important;  /* 添加圆角效果 */
+  margin: 8px 0 !important;      /* 添加间距使圆角更明显 */
+}
+
+/* 如果需要同时修改文字颜色 */
+:deep(.custom-table .el-table__body tr.current-row > td .cell) {
+  color: #fff !important;
+}
+.custom-table :deep(.el-table__row) {
+  background: transparent !important;
+}
+.custom-table :deep(.el-table__cell) {
+  padding: 6px 0 !important;  /* 减小上下内边距 */
+  border: none !important;
+}
+.custom-table :deep(.el-table__cell .cell) {
+  line-height: 15px !important;
+  height: 15px !important;
+}
+.custom-table :deep(.el-table__inner-wrapper::before) {
+  display: none;
+}
+.tree-container {
+  height: 990px; /* 可根据需要调整高度 */
+  overflow-y: auto;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+}
+.qy{
+  background-color: white;
+  width: 1000px;
+  margin: 0 auto;
+  border: 1px solid #ddd;
+}
+/* 报告打印按钮*/
+.print-btn {
+  position: absolute;
+  top: 30px;
+  left: 380px;
+  padding: 8px 16px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  z-index: 100;
+}
+
+@media print {
+  .print-btn {
+    display: none;
+  }
+  body * {
+    visibility: hidden;
+  }
+  .qy, .qy * {
+    visibility: visible;
+  }
+  .qy {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+  }
+}
+
+.app {
+  display: flex;
+  background-color: #15203b;
+  color: white;
+  overflow: hidden; /* 防止外层滚动条 */
+}
+
+.sidebar {
+  width: 350px;
+  padding: 10px;
+  background-color: white;
+}
+
+.report-container {
+  text-align: center;
+  height: calc(100vh - 124px);
+  overflow-y: auto;
+  padding: 20px;
+  border: 1px solid #ddd; /* 新增边框 */
+  border-radius: 8px; /* 圆角边框 */
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); /* 阴影效果 */
+  background-color: white; /* 白色背景 */
+}
+
+.chart-section {
+  display: flex;
+  flex-direction: column; /* 改为垂直排列 */
+  align-items: center;
+  gap: 20px;
+  margin: 20px auto;
+  padding-bottom: 20px;
+  width: 100%;
+}
+
+.chart-item {
+  width: 90%; /* 增加宽度 */
+  margin-bottom: 20px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.date-picker input {
+  padding: 6px 10px;
+  border: none;
+  border-radius: 4px;
+  background-color: white;
+  color: #090909;
+  border: 1px solid #ddd;
+  margin-right: 10px;
+}
+
+.generate-report-btn {
+  padding: 10px 20px;
+  background-color: #2196f3;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  margin-top: 20px;
+  width: 100%;
+}
+
+.tree-container {
+  height: 70vh; /* 调整高度计算方式 */
+  overflow-y: auto;
+
+  background-color: white;
+  color: #000000;
+}
+
+.custom-table {
+  background-color: inherit;
+  color: inherit;
+}
+
+.main-content {
+  flex: 1;
+  padding: 20px;
+  background-color: #fff;
+  color: #333;
+  overflow-y: hidden;
+}
+
+.chart-item {
+  width: 48%;
+  margin-bottom: 20px;
+}
+
+.chart {
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+/* 表格样式 */
+.el-table {
+  margin-top: 20px;
+}
+
+.el-table th {
+  background-color: #2196f3;
+  color: white;
+}
+</style>

+ 424 - 0
pm_ui/src/views/energyManagement/dataAcquisition/index.vue

@@ -0,0 +1,424 @@
+<template>
+  <div class="app-container">
+    <el-row :gutter="20">
+      <el-col :span="6">
+        <div class="tree-container">
+          <el-table
+              ref="tableRef"
+              v-loading="loading"
+              :data="topologyList"
+              @current-change="handleCurrentChange"
+              row-key="itemId"
+              :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+              :default-expand-all="true"
+              :indent="24"
+              class="custom-table"
+              :show-header="false"
+              :highlight-current-row="true"
+              :border="false"
+          >
+            <el-table-column align="left" prop="itemName">
+              <template #default="{ row }">
+                <i
+                    v-if="row.children && row.children.length > 0"
+                    :class="row.expanded ? 'el-icon-folder-opened' : 'el-icon-folder'"
+                    style="margin-right: 5px"
+                ></i>
+                <i v-else class="el-icon-document" style="margin-right: 5px"></i>
+                {{ row.itemName }}
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </el-col>
+      <el-col :span="18">
+        <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+          <el-form-item label="项目名称" prop="itemName">
+            <el-input
+                v-model="queryParams.itemName"
+                placeholder="请输入项目名称"
+                clearable
+                @keyup.enter="handleQuery"
+            />
+          </el-form-item>
+          <el-form-item label="部门状态" prop="status">
+            <el-select v-model="queryParams.status" placeholder="请选择部门状态" clearable style="width: 240px">
+              <el-option
+                  v-for="dict in sys_normal_disable"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="项目类型" prop="itemType">
+            <el-select v-model="queryParams.itemType" placeholder="请选择项目类型" clearable style="width: 240px">
+              <el-option
+                  v-for="dict in root_node_type"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+        <el-table v-loading="loading" stripe border :data="deviceOptions" @selection-change="handleSelectionChange">
+          <el-table-column type="selection" width="55" align="center" />
+          <!--      <el-table-column label="设备ID" align="center" prop="id" />-->
+          <el-table-column label="设备编码" align="center" prop="deviceCode" width="90"/>
+          <el-table-column label="设备名称" align="center" prop="deviceName" width="120"/>
+          <el-table-column label="设备类型" align="center" prop="deviceType" />
+          <el-table-column label="子系统类型" align="center" prop="subsystemType" width="120"/>
+          <el-table-column label="所属空间ID" align="center" prop="spaceId" width="100"/>
+          <el-table-column label="所属空间名称" align="center" prop="spaceName" width="110"/>
+          <el-table-column label="品牌" align="center" prop="brand" />
+          <el-table-column label="型号" align="center" prop="model" width="90"/>
+          <el-table-column label="序列号" align="center" prop="serialNumber" width="120"/>
+          <el-table-column label="生产日期" align="center" prop="manufactureDate" width="110" sortable
+          >
+            <template #default="scope">
+              <span>{{ parseTime(scope.row.manufactureDate, '{y}-{m}-{d}') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="安装日期" align="center" prop="installDate" width="110">
+            <template #default="scope">
+              <span>{{ parseTime(scope.row.installDate, '{y}-{m}-{d}') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="保修期(月)" align="center" prop="warrantyPeriod" width="90"/>
+          <el-table-column label="状态" align="center" prop="status">
+            <template #default="scope">
+              <dict-tag :options="sys_drivce_status" :value="scope.row.status"/>
+            </template>
+          </el-table-column>
+          <el-table-column label="是否在线" align="center" prop="isOnline">
+            <template #default="scope">
+              <dict-tag :options="sys_yes_no_tf" :value="scope.row.isOnline"/>
+            </template>
+          </el-table-column>
+          <el-table-column label="设备描述" align="center" prop="description" width="140"/>
+          <el-table-column label="操作" min-width="140" fixed="right" width="200px" align="center" class-name="small-padding fixed-width">
+            <template #default="scope">
+              <el-button link type="primary" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['driver:basic:detail']">详情</el-button>
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['driver:basic:edit']">修改</el-button>
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['driver:basic:remove']">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <pagination
+            v-show="total>0"
+            :total="total"
+            v-model:page="queryParams.pageNum"
+            v-model:limit="queryParams.pageSize"
+            @pagination="getListBasic"
+        />
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup name="Topology">
+import {listTopology, getTopology, delTopology, addTopology, updateTopology} from "@/api/device/topology";
+import {listBasic} from "@/api/device/basic";
+
+const {proxy} = getCurrentInstance();
+const {root_node_type, sys_normal_disable} = proxy.useDict('root_node_type', 'sys_normal_disable');
+
+const deviceList = ref([]);
+const topologyList = ref([]);
+const open = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref("");
+// 新增响应式引用
+const itemTopologyOptions = ref([]);
+const deviceOptions = ref([]);
+const isExpandAll = ref(false);
+const tableRef = ref();
+
+
+const data = reactive({
+  form: {},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    itemName: null,
+    status: null,
+    itemType: null,
+    deviceId: null
+  },
+  rules: {}
+});
+
+const {queryParams, form, rules} = toRefs(data);
+
+/** 查询项目拓扑列表 */
+function getList() {
+  loading.value = true;
+  listTopology({pageNum: 1,pageSize: 10000,}).then(response => {
+    topologyList.value = proxy.handleTree(response.rows, "itemId", "parentId", "children");
+    //total.value = response.total;
+    loading.value = false;
+    topologyList.value = injectDeviceNodes(topologyList.value);
+  });
+}
+// 选中数据后的查询
+const handleCurrentChange = (currentRow) => {
+  if (currentRow && currentRow.itemId) {
+    console.log('当前选中行:', currentRow);
+
+    // 调用(接口)查询方法
+    listBasic(queryParams.value).then(response => {
+      deviceOptions.value = response.rows;
+      total.value = response.total;
+      loading.value = false;
+    });
+  }
+};
+
+function injectDeviceNodes(treeData) {
+  return treeData.map(node => {
+    const newNode = {...node};
+
+    // 递归处理子节点
+    if (newNode.children && newNode.children.length > 0) {
+      newNode.children = injectDeviceNodes(newNode.children);
+    }
+
+    // 处理当前节点的 deviceId
+    if (newNode.deviceId && typeof newNode.deviceId === "string") {
+      const deviceIds = newNode.deviceId.split(",").filter(Boolean);
+      const deviceChildren = deviceIds.map(id => ({
+        itemId: `${newNode.itemId}-${id}`,
+        itemName: `设备: ${id}`,
+        level: (newNode.level || 0) + 1,
+        isDevice: true
+      }));
+
+      // 合并原有子节点和设备子节点
+      newNode.children = (newNode.children || []).concat(deviceChildren);
+    }
+
+    return newNode;
+  });
+}
+
+
+function toggleExpandAll() {
+  isExpandAll.value = !isExpandAll.value;
+
+  const toggleChildren = (items) => {
+    items.forEach(item => {
+      tableRef.value.toggleRowExpansion(item, isExpandAll.value);
+      if (item.children && item.children.length > 0) {
+        toggleChildren(item.children);
+      }
+    });
+  };
+
+  toggleChildren(topologyList.value);
+}
+
+// 取消按钮
+function cancel() {
+  open.value = false;
+  reset();
+}
+
+// 表单重置
+function reset() {
+  form.value = {
+    itemId: null,
+    parentId: null,
+    ancestors: null,
+    itemName: null,
+    orderNum: null,
+    status: null,
+    delFlag: null,
+    itemType: null,
+    createBy: null,
+    createTime: null,
+    updateBy: null,
+    updateTime: null,
+    deviceId: null
+  };
+  proxy.resetForm("topologyRef");
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+  proxy.resetForm("queryRef");
+  handleQuery();
+}
+
+// 多选框选中数据
+function handleSelectionChange(selection) {
+  ids.value = selection.map(item => item.itemId);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+function handleAdd(row = null) {
+  reset();
+  open.value = true;
+  title.value = "添加项目拓扑";
+
+  form.value = {
+    ...form.value,
+    parentId: row ? row.itemId : null,
+    status: '0', // 默认部门状态为"正常"
+    itemType: row ? row.itemType : null,
+    deviceId: null
+  };
+
+  // 并行加载拓扑和设备数据
+  Promise.all([
+    listTopology({pageNum: 1, pageSize: 10000}),
+    listBasic({pageNum: 1, pageSize: 10000})
+  ]).then(([topologyRes, deviceRes]) => {
+    itemTopologyOptions.value = proxy.handleTree(topologyRes.rows, "itemId");
+    deviceOptions.value = deviceRes.rows.map(item => ({
+      id: item.id,
+      deviceName: `${item.deviceCode} - ${item.deviceName}`
+    }));
+  });
+}
+
+/** 修改按钮操作 */
+function handleUpdate(row) {
+  reset();
+  const _itemId = row.itemId;
+
+  getTopology(_itemId).then(response => {
+    // 处理关联设备数据
+    const deviceIds = response.data.deviceId ? response.data.deviceId.split(',').map(id => String(id)) : [];
+
+    // 处理上级项目数据
+    //const parentId = response.data.parentId === "0" ? response.data.itemName : response.data.parentId;
+    form.value = {
+      ...response.data,
+      parentId: response.data.parentId === "0" ? response.data.itemName : response.data.parentId,
+      deviceId: deviceIds
+    };
+
+    Promise.all([
+      listTopology({pageNum: 1, pageSize: 10000}),
+      listBasic({pageNum: 1, pageSize: 10000})
+    ]).then(([topologyRes, deviceRes]) => {
+      itemTopologyOptions.value = proxy.handleTree(topologyRes.rows, "itemId");
+      deviceOptions.value = deviceRes.rows.map(item => ({
+        id: String(item.id),  // 确保ID转为字符串
+        deviceName: `${item.deviceCode} - ${item.deviceName}`
+      }));
+    });
+
+    open.value = true;
+    title.value = "修改项目拓扑";
+  });
+}
+
+/** 提交按钮 */
+function submitForm() {
+  proxy.$refs["topologyRef"].validate(valid => {
+    if (valid) {
+      if (Array.isArray(form.value.deviceId)) {
+        form.value.deviceId = form.value.deviceId.join(',');
+      }
+      if (form.value.itemId != null) {
+        updateTopology(form.value).then(response => {
+          proxy.$modal.msgSuccess("修改成功");
+          open.value = false;
+          getList();
+        });
+      } else {
+        addTopology(form.value).then(response => {
+          proxy.$modal.msgSuccess("新增成功");
+          open.value = false;
+          getList();
+        });
+      }
+    }
+  });
+}
+
+/** 删除按钮操作 */
+function handleDelete(row) {
+  const _itemIds = row.itemId || ids.value;
+  proxy.$modal.confirm('是否确认删除项目拓扑编号为"' + _itemIds + '"的数据项?').then(function () {
+    return delTopology(_itemIds);
+  }).then(() => {
+    getList();
+    proxy.$modal.msgSuccess("删除成功");
+  }).catch(() => {
+  });
+}
+
+/** 导出按钮操作 */
+function handleExport() {
+  proxy.download('topology/topology/export', {
+    ...queryParams.value
+  }, `topology_${new Date().getTime()}.xlsx`)
+}
+
+function getListBasic() {
+  listBasic(queryParams.value).then(response => {
+    deviceOptions.value = response.rows;
+  });
+}
+
+getList();
+//getListBasic();
+</script>
+<style scoped>
+/* 左侧树形表格高亮样式 */
+:deep(.custom-table .el-table__body tr.current-row > td) {
+  background-color: #a6b9dd !important; /* 可以替换为您想要的颜色 */
+  border-radius: 12px !important;  /* 添加圆角效果 */
+  margin: 8px 0 !important;      /* 添加间距使圆角更明显 */
+}
+
+/* 如果需要同时修改文字颜色 */
+:deep(.custom-table .el-table__body tr.current-row > td .cell) {
+  color: #fff !important;
+}
+
+/* 树形表格样式 */
+.custom-table {
+  background: transparent !important;
+}
+.custom-table :deep(.el-table__row) {
+  background: transparent !important;
+}
+.custom-table :deep(.el-table__cell) {
+  padding: 6px 0 !important;  /* 减小上下内边距 */
+  border: none !important;
+}
+.custom-table :deep(.el-table__cell .cell) {
+  line-height: 13px !important;
+  height: 13px !important;
+}
+.custom-table :deep(.el-table__inner-wrapper::before) {
+  display: none;
+}
+.tree-container {
+  height: 990px; /* 可根据需要调整高度 */
+  overflow-y: auto;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+}
+</style>

+ 246 - 0
pm_ui/src/views/energyManagement/dataComparison/index.vue

@@ -0,0 +1,246 @@
+<template>
+  <div class="app">
+    <!-- 上半部分:两个卡片 -->
+    <div class="top-section">
+      <!-- 本月 -->
+      <div class="card">
+        <h3>本月</h3>
+        <div class="data-container">
+          <div class="data-row">
+            <div class="data-item">
+              <span class="value">8.405</span><span class="unit">t</span>
+              <span class="label">当月</span>
+            </div>
+            <div class="data-item">
+              <span class="value">8.477</span><span class="unit">t</span>
+              <span class="label">上月同期</span>
+            </div>
+            <div class="data-item trend">
+              <span class="value" style="color: #00c853;">-0.072</span><span class="unit">↓</span>
+              <span class="label">趋势</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 本年 -->
+      <div class="card">
+        <h3>本年</h3>
+        <div class="data-container">
+          <div class="data-row">
+            <div class="data-item">
+              <span class="value">144.747</span><span class="unit">t</span>
+              <span class="label">当年</span>
+            </div>
+            <div class="data-item">
+              <span class="value">123.812</span><span class="unit">t</span>
+              <span class="label">去年同期</span>
+            </div>
+            <div class="data-item trend">
+              <span class="value" style="color: #d50000;">20.935</span><span class="unit">↑</span>
+              <span class="label">趋势</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 筛选区域 -->
+    <div class="filter-container">
+      <span>分类能耗</span>
+      <select v-model="selectedCategory">
+        <option value="电">电</option>
+        <option value="水">水</option>
+        <option value="天然气">天然气</option>
+      </select>
+      <input type="text" v-model="selectedYear" placeholder="输入年份" />
+      <button @click="fetchData">查询</button>
+    </div>
+
+    <!-- 图表区域 -->
+    <div id="chart" ref="chartRef" style="width: 100%; height: 400px;"></div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts';
+
+export default {
+  data() {
+    return {
+      selectedCategory: '电',
+      selectedYear: '',
+      chartInstance: null,
+    };
+  },
+  mounted() {
+    this.initChart();
+  },
+  methods: {
+    initChart() {
+      const chartDom = this.$refs.chartRef;
+      this.chartInstance = echarts.init(chartDom);
+
+      const option = {
+        title: { text: '能耗分布', left: 'center' },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' }
+        },
+        xAxis: {
+          type: 'category',
+          data: ['1月', '2月', '3月', '4月', '5月'],
+          axisLabel: { color: '#333' }
+        },
+        yAxis: {
+          type: 'value',
+          axisLabel: { color: '#333' }
+        },
+        series: [
+          {
+            name: '能耗',
+            type: 'bar',
+            data: [39, 30, 28, 22, 6],
+            itemStyle: { color: '#2196f3' }
+          }
+        ],
+        grid: {
+          top: 30,
+          bottom: 30,
+          left: 60,
+          right: 20
+        }
+      };
+
+      this.chartInstance.setOption(option);
+    },
+    fetchData() {
+      console.log('查询:', this.selectedCategory, this.selectedYear);
+      // 模拟数据更新
+      this.updateChart([40, 32, 29, 23, 7]);
+    },
+    updateChart(data) {
+      if (this.chartInstance) {
+        this.chartInstance.setOption({
+          series: [{ data: data }]
+        });
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.app {
+  background-color: #fff;
+  color: #333;
+  font-family: "Segoe UI", sans-serif;
+  padding: 30px;
+}
+
+/* 上方两个卡片并列 */
+.top-section {
+  display: flex;
+  justify-content: space-between;
+  gap: 30px;
+  margin-bottom: 30px;
+}
+
+.card {
+  flex: 1;
+  background-color: #f8f9fa;
+  border-radius: 10px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  border: 1px solid #e0e0e0;
+  height: 300px; /* 固定高度 */
+  position: relative; /* 用于绝对定位标题 */
+}
+
+.card h3 {
+  margin: 15px;
+  font-size: 18px;
+  font-weight: 600;
+  color: #212121;
+  position: absolute;
+  top: 15px;
+  left: 15px; /* 标题固定在左上角 */
+}
+
+.data-container {
+  width: 100%;
+  display: flex;
+  justify-content: center; /* 水平居中 */
+  align-items: center; /* 垂直居中 */
+  height: calc(100% - 40px); /* 减去标题的高度 */
+}
+
+.data-row {
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  width: 80%; /* 调整宽度以适应居中 */
+}
+
+.data-item {
+  text-align: center;
+}
+
+.value {
+  font-size: 24px;
+  font-weight: bold;
+  color: #000;
+}
+
+.unit {
+  font-size: 14px;
+  margin-left: 4px;
+  color: #666;
+}
+
+.label {
+  font-size: 12px;
+  color: #999;
+  display: block;
+  margin-top: 4px;
+}
+
+.trend .value {
+  font-size: 20px;
+}
+
+/* 筛选区域 */
+.filter-container {
+  display: flex;
+  align-items: center;
+  margin-bottom: 25px;
+  gap: 15px;
+}
+
+.filter-container span {
+  font-weight: 500;
+  min-width: 70px;
+}
+
+.filter-container select,
+.filter-container input {
+  padding: 8px 12px;
+  border-radius: 6px;
+  border: 1px solid #ccc;
+  outline: none;
+  font-size: 14px;
+}
+
+.filter-container button {
+  padding: 8px 16px;
+  background-color: #2196f3;
+  color: white;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: background-color 0.3s ease;
+}
+
+.filter-container button:hover {
+  background-color: #1976d2;
+}
+</style>

+ 237 - 0
pm_ui/src/views/energyManagement/layerManage/index.vue

@@ -0,0 +1,237 @@
+<template>
+  <div class="app">
+    <!-- 左侧区域 -->
+    <div class="sidebar">
+      <!-- 树形结构 -->
+      <div class="tree-container">
+        <el-table
+            ref="tableRef"
+            v-loading="loading"
+            :data="topologyList"
+            @current-change="handleNodeClick"
+            row-key="itemId"
+            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+            :default-expand-all="true"
+            :indent="24"
+            class="custom-table"
+            :show-header="false"
+            :highlight-current-row="true"
+            :border="false"
+        >
+          <el-table-column align="left" prop="itemName">
+            <template #default="{ row }">
+              <i
+                  v-if="row.children && row.children.length > 0"
+                  :class="row.expanded ? 'el-icon-folder-opened' : 'el-icon-folder'"
+                  style="margin-right: 5px"
+              ></i>
+              <i v-else class="el-icon-document" style="margin-right: 5px"></i>
+              {{ row.itemName }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+    <!-- 右侧区域 -->
+    <div class="main-content">
+      <!-- 总体能耗展示 -->
+      <div class="floor-summary" v-if="selectedNode">
+        <h2>{{ selectedNode.itemName }} 总体能耗</h2>
+        <el-table :data="energyStatistics" border style="width: 100%">
+          <el-table-column prop="category" label="能耗类型"></el-table-column>
+          <el-table-column prop="value" label="数值"></el-table-column>
+          <el-table-column prop="ringRatio" label="环比"></el-table-column>
+          <el-table-column prop="yearOnYearRatio" label="同比"></el-table-column>
+        </el-table>
+        <div class="summary-details">
+          <p>总用电量: {{ electricityUsage }} kWh</p>
+          <p>总用水量: {{ waterUsage }} t</p>
+          <p>综合能耗: {{ totalEnergyConsumption }} kgce</p>
+          <p>峰值用电量: {{ peakElectricity }} kW</p>
+          <p>峰值时间: {{ peakTime }}</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { listTopology } from '@/api/device/topology'; // 假设你有这些接口
+import { ElMessage } from 'element-plus';
+
+// 数据定义
+const topologyList = ref([]);
+const loading = ref(true);
+const selectedNode = ref(null);
+
+// 总体能耗数据
+const energyStatistics = ref([]);
+const electricityUsage = ref('');
+const peakElectricity = ref('');
+const peakTime = ref('');
+const waterUsage = ref('');
+const totalEnergyConsumption = ref('');
+
+// 获取树形数据
+const getList = async () => {
+  try {
+    loading.value = true;
+    const response = await listTopology({ pageNum: 1, pageSize: 10000 });
+    topologyList.value = handleTree(response.rows, 'itemId', 'parentId', 'children');
+    loading.value = false;
+  } catch (error) {
+    ElMessage.error('加载数据失败');
+    loading.value = false;
+  }
+};
+
+// 处理树形结构的方法
+const handleTree = (data, id, parentId, childrenName = 'children') => {
+  const config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenName: childrenName || 'children'
+  };
+  const tree = [];
+  const map = {};
+  data.forEach(item => (map[item[config.id]] = item));
+  data.forEach(item => {
+    const parent = map[item[config.parentId]];
+    if (parent) {
+      (parent[config.childrenName] || (parent[config.childrenName] = [])).push(item);
+    } else {
+      tree.push(item);
+    }
+  });
+  return tree;
+};
+
+// 点击节点
+const handleNodeClick = (node) => {
+  selectedNode.value = node;
+  loadFloorEnergyData();
+};
+
+// 加载楼层总体能耗数据
+const loadFloorEnergyData = () => {
+  // 模拟数据 - 实际项目中应该调用API获取
+  energyStatistics.value = [
+    {
+      category: '用电量',
+      value: `${(Math.random() * 1000 + 1000).toFixed(2)} kWh`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    },
+    {
+      category: '用水量',
+      value: `${(Math.random() * 50).toFixed(2)} t`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    },
+    {
+      category: '综合能耗',
+      value: `${(Math.random() * 300).toFixed(2)} kgce`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    }
+  ];
+  electricityUsage.value = (Math.random() * 1000 + 1000).toFixed(2);
+  peakElectricity.value = (Math.random() * 100 + 200).toFixed(2);
+  peakTime.value = `${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 30) + 1}`;
+  // 新增用水量和综合能耗数据
+  waterUsage.value = (Math.random() * 50).toFixed(2);
+  totalEnergyConsumption.value = (Math.random() * 300).toFixed(2);
+};
+
+// 页面初始化时加载数据
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+/* 左侧树形表格高亮样式 */
+:deep(.custom-table .el-table__body tr.current-row > td) {
+  background-color: #a6b9dd !important; /* 可以替换为您想要的颜色 */
+  border-radius: 12px !important;  /* 添加圆角效果 */
+  margin: 8px 0 !important;      /* 添加间距使圆角更明显 */
+}
+/* 如果需要同时修改文字颜色 */
+:deep(.custom-table .el-table__body tr.current-row > td .cell) {
+  color: #fff !important;
+}
+.custom-table :deep(.el-table__row) {
+  background: transparent !important;
+}
+.custom-table :deep(.el-table__cell) {
+  padding: 6px 0 !important;  /* 减小上下内边距 */
+  border: none !important;
+}
+.custom-table :deep(.el-table__cell .cell) {
+  line-height: 15px !important;
+  height: 15px !important;
+}
+.custom-table :deep(.el-table__inner-wrapper::before) {
+  display: none;
+}
+.tree-container {
+  height: 90vh; /* 调整高度计算方式 */
+  overflow-y: auto;
+  background-color: white;
+  color: #000000;
+}
+.main-content {
+  flex: 1;
+  padding: 20px;
+  background-color: #fff;
+  color: #333;
+  overflow-y: hidden;
+}
+.floor-summary {
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+.summary-details {
+  margin-top: 10px;
+  font-size: 14px;
+  color: #555;
+}
+.el-table {
+  margin-top: 20px;
+}
+.el-table th {
+  background-color: #2196f3;
+  color: white;
+}
+
+.app {
+  display: flex;
+}
+
+.sidebar {
+  width: 300px;
+  height: 100%;
+  overflow: auto;
+  background-color: white;
+  border-right: 1px solid #e6e6e6;
+}
+
+.main-content {
+  flex: 1;
+  padding: 20px;
+  overflow-y: auto;
+  background-color: #fff;
+}
+
+.floor-summary {
+  margin-left: 20px;
+  padding: 15px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+</style>

+ 265 - 0
pm_ui/src/views/energyManagement/layerManage/index2.vue

@@ -0,0 +1,265 @@
+<template>
+  <div class="app">
+    <!-- 左侧区域 -->
+    <div class="sidebar">
+      <!-- 树形结构 -->
+      <div class="tree-container">
+        <el-table
+            ref="tableRef"
+            v-loading="loading"
+            :data="topologyList"
+            @current-change="handleNodeClick"
+            row-key="itemId"
+            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+            :default-expand-all="true"
+            :indent="24"
+            class="custom-table"
+            :show-header="false"
+            :highlight-current-row="true"
+            :border="false"
+        >
+          <el-table-column align="left" prop="itemName">
+            <template #default="{ row }">
+              <i
+                  v-if="row.children && row.children.length > 0"
+                  :class="row.expanded ? 'el-icon-folder-opened' : 'el-icon-folder'"
+                  style="margin-right: 5px"
+              ></i>
+              <i v-else class="el-icon-document" style="margin-right: 5px"></i>
+              {{ row.itemName }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+    <!-- 右侧区域 -->
+    <div class="main-content">
+      <!-- 总体能耗展示 -->
+      <div class="floor-summary" v-if="selectedNode">
+        <h2>{{ selectedNode.itemName }} 总体能耗</h2>
+        <el-table :data="energyStatistics" border style="width: 100%">
+          <el-table-column prop="category" label="能耗类型"></el-table-column>
+          <el-table-column prop="value" label="数值"></el-table-column>
+          <el-table-column prop="ringRatio" label="环比"></el-table-column>
+          <el-table-column prop="yearOnYearRatio" label="同比"></el-table-column>
+        </el-table>
+        <div class="summary-details">
+          <p>总用电量: {{ electricityUsage }} kWh</p>
+          <p>总用水量: {{ waterUsage }} t</p>
+          <p>综合能耗: {{ totalEnergyConsumption }} kgce</p>
+          <p>峰值用电量: {{ peakElectricity }} kW</p>
+          <p>峰值时间: {{ peakTime }}</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { listTopology } from '@/api/device/topology'; // 假设你有这些接口
+import { ElMessage } from 'element-plus';
+
+// 数据定义
+const topologyList = ref([]);
+const loading = ref(true);
+const selectedNode = ref(null);
+
+// 总体能耗数据
+const energyStatistics = ref([]);
+const electricityUsage = ref('');
+const peakElectricity = ref('');
+const peakTime = ref('');
+const waterUsage = ref('');
+const totalEnergyConsumption = ref('');
+
+// 获取树形数据
+const getList = async () => {
+  try {
+    loading.value = true;
+    const response = await listTopology({ pageNum: 1, pageSize: 10000 });
+    topologyList.value = handleTree(response.rows, 'itemId', 'parentId', 'children');
+    loading.value = false;
+    topologyList.value = injectDeviceNodes(topologyList.value);
+  } catch (error) {
+    ElMessage.error('加载数据失败');
+    loading.value = false;
+  }
+};
+
+// 处理树形结构的方法
+const handleTree = (data, id, parentId, childrenName = 'children') => {
+  const config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenName: childrenName || 'children'
+  };
+  const tree = [];
+  const map = {};
+  data.forEach(item => (map[item[config.id]] = item));
+  data.forEach(item => {
+    const parent = map[item[config.parentId]];
+    if (parent) {
+      (parent[config.childrenName] || (parent[config.childrenName] = [])).push(item);
+    } else {
+      tree.push(item);
+    }
+  });
+  return tree;
+};
+
+// 点击节点
+const handleNodeClick = (node) => {
+  selectedNode.value = node;
+  loadFloorEnergyData();
+};
+
+// 加载楼层总体能耗数据
+const loadFloorEnergyData = () => {
+  // 模拟数据 - 实际项目中应该调用API获取
+  energyStatistics.value = [
+    {
+      category: '用电量',
+      value: `${(Math.random() * 1000 + 1000).toFixed(2)} kWh`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    },
+    {
+      category: '用水量',
+      value: `${(Math.random() * 50).toFixed(2)} t`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    },
+    {
+      category: '综合能耗',
+      value: `${(Math.random() * 300).toFixed(2)} kgce`,
+      ringRatio: `${(Math.random() * 10).toFixed(2)}↑`,
+      yearOnYearRatio: `${(Math.random() * 15 + 100).toFixed(2)}↑`
+    }
+  ];
+  electricityUsage.value = (Math.random() * 1000 + 1000).toFixed(2);
+  peakElectricity.value = (Math.random() * 100 + 200).toFixed(2);
+  peakTime.value = `${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 30) + 1}`;
+
+  // 新增用水量和综合能耗数据
+  waterUsage.value = (Math.random() * 50).toFixed(2);
+  totalEnergyConsumption.value = (Math.random() * 300).toFixed(2);
+};
+
+function injectDeviceNodes(treeData) {
+  return treeData.map(node => {
+    const newNode = {...node};
+
+    // 递归处理子节点
+    if (newNode.children && newNode.children.length > 0) {
+      newNode.children = injectDeviceNodes(newNode.children);
+    }
+
+    // 处理当前节点的 deviceId
+    if (newNode.deviceId && typeof newNode.deviceId === "string") {
+      const deviceIds = newNode.deviceId.split(",").filter(Boolean);
+      const deviceChildren = deviceIds.map(id => ({
+        itemId: `${newNode.itemId}-${id}`,
+        itemName: `设备: ${id}`,
+        level: (newNode.level || 0) + 1,
+        isDevice: true
+      }));
+
+      // 合并原有子节点和设备子节点
+      newNode.children = (newNode.children || []).concat(deviceChildren);
+    }
+
+    return newNode;
+  });
+}
+// 页面初始化时加载数据
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+/* 左侧树形表格高亮样式 */
+:deep(.custom-table .el-table__body tr.current-row > td) {
+  background-color: #a6b9dd !important; /* 可以替换为您想要的颜色 */
+  border-radius: 12px !important;  /* 添加圆角效果 */
+  margin: 8px 0 !important;      /* 添加间距使圆角更明显 */
+}
+/* 如果需要同时修改文字颜色 */
+:deep(.custom-table .el-table__body tr.current-row > td .cell) {
+  color: #fff !important;
+}
+.custom-table :deep(.el-table__row) {
+  background: transparent !important;
+}
+.custom-table :deep(.el-table__cell) {
+  padding: 6px 0 !important;  /* 减小上下内边距 */
+  border: none !important;
+}
+.custom-table :deep(.el-table__cell .cell) {
+  line-height: 15px !important;
+  height: 15px !important;
+}
+.custom-table :deep(.el-table__inner-wrapper::before) {
+  display: none;
+}
+.tree-container {
+  height: 90vh; /* 调整高度计算方式 */
+  overflow-y: auto;
+  background-color: white;
+  color: #000000;
+}
+.main-content {
+  flex: 1;
+  padding: 20px;
+  background-color: #fff;
+  color: #333;
+  overflow-y: hidden;
+}
+.floor-summary {
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+.summary-details {
+  margin-top: 10px;
+  font-size: 14px;
+  color: #555;
+}
+.el-table {
+  margin-top: 20px;
+}
+.el-table th {
+  background-color: #2196f3;
+  color: white;
+}
+
+.app {
+  display: flex;
+}
+
+.sidebar {
+  width: 300px;
+  height: 100%;
+  overflow: auto;
+  background-color: white;
+  border-right: 1px solid #e6e6e6;
+}
+
+.main-content {
+  flex: 1;
+  padding: 20px;
+  overflow-y: auto;
+  background-color: #fff;
+}
+
+.floor-summary {
+  margin-left: 20px;
+  padding: 15px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+</style>

+ 255 - 0
pm_ui/src/views/energyManagement/quotaManagement/index.vue

@@ -0,0 +1,255 @@
+<template>
+  <div class="app">
+    <!-- 左侧区域 -->
+    <div class="sidebar">
+      <!-- 树形结构 -->
+      <div class="tree-container">
+        <el-table
+            ref="tableRef"
+            v-loading="loading"
+            :data="topologyList"
+            @current-change="handleNodeClick"
+            row-key="itemId"
+            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+            :default-expand-all="true"
+            :indent="24"
+            class="custom-table"
+            :show-header="false"
+            :highlight-current-row="true"
+            :border="false"
+        >
+          <el-table-column align="left" prop="itemName">
+            <template #default="{ row }">
+              <i
+                  v-if="row.children && row.children.length > 0"
+                  :class="row.expanded ? 'el-icon-folder-opened' : 'el-icon-folder'"
+                  style="margin-right: 5px"
+              ></i>
+              <i v-else class="el-icon-document" style="margin-right: 5px"></i>
+              {{ row.itemName }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+    <!-- 右侧区域:能耗进度条展示 -->
+    <div class="main-content">
+      <!-- 选择能源类型 -->
+      <div class="query-section">
+        <el-select
+            v-model="selectedEnergyType"
+            placeholder="请选择能源类型"
+            style="width: 200px"
+        >
+          <el-option label="用电量 (kWh)" value="electricity" />
+          <el-option label="用水量 (t)" value="water" />
+          <el-option label="用气量 (m³)" value="gas" />
+        </el-select>
+        <el-button
+            type="primary"
+            @click="fetchEnergyData"
+        >
+          查询
+        </el-button>
+      </div>
+      <h2 v-if="selectedNodeName">{{ selectedNodeName }} 能耗使用情况</h2>
+
+      <div class="progress-card" v-if="energyData">
+        <div class="progress-header">
+          <span>总限值:{{ energyData.limit }} {{ unit }}</span>
+          <span>已使用:{{ energyData.used }} {{ unit }}</span>
+          <span>剩余:{{ energyData.remaining }} {{ unit }}</span>
+          <span>使用率:{{ energyData.percentage }}%</span>
+        </div>
+        <el-progress
+            :percentage="energyData.percentage"
+            :status="progressStatus"
+            :stroke-width="20"
+        ></el-progress>
+      </div>
+
+      <div class="tip" v-else>
+        请选择左侧区域和能源类型后点击“查询”按钮
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import { listTopology } from '@/api/device/topology';
+import { ElMessage } from 'element-plus';
+
+const topologyList = ref([]);
+const loading = ref(true);
+const selectedNode = ref(null);
+const selectedNodeName = computed(() => selectedNode.value ? selectedNode.value.itemName : '');
+const selectedEnergyType = ref('electricity');
+const unit = computed(() => {
+  switch (selectedEnergyType.value) {
+    case 'electricity':
+      return 'kWh';
+    case 'water':
+      return 't';
+    case 'gas':
+      return 'm³';
+    default:
+      return '';
+  }
+});
+const energyData = ref(null);
+
+const progressStatus = computed(() => {
+  if (!energyData.value) return '';
+  return energyData.value.percentage >= 100 ? 'exception' : '';
+});
+
+const getList = async () => {
+  try {
+    loading.value = true;
+    const response = await listTopology({ pageNum: 1, pageSize: 10000 });
+    topologyList.value = handleTree(response.rows, 'itemId', 'parentId', 'children');
+    loading.value = false;
+  } catch (error) {
+    ElMessage.error('加载数据失败');
+    loading.value = false;
+  }
+};
+
+const handleTree = (data, id, parentId, childrenName = 'children') => {
+  const config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenName: childrenName || 'children'
+  };
+  const tree = [];
+  const map = {};
+  data.forEach(item => (map[item[config.id]] = item));
+  data.forEach(item => {
+    const parent = map[item[config.parentId]];
+    if (parent) {
+      (parent[config.childrenName] || (parent[config.childrenName] = [])).push(item);
+    } else {
+      tree.push(item);
+    }
+  });
+  return tree;
+};
+
+const handleNodeClick = (node) => {
+  selectedNode.value = node;
+  selectedEnergyType.value = 'electricity'; // 默认选择电
+  fetchEnergyData(); // 自动触发查询
+};
+
+const fetchEnergyData = async () => {
+  if (!selectedNode.value) {
+    alert('请先选择一个节点');
+    return;
+  }
+
+  // 模拟数据 - 实际项目中应该调用API获取
+  const limit = Math.random() * 10000 + 5000; // 总限值
+  const used = Math.random() * limit; // 已使用
+  const remaining = limit - used; // 剩余
+  const percentage = (used / limit) * 100; // 使用率
+
+  energyData.value = {
+    limit: parseFloat(limit.toFixed(2)),
+    used: parseFloat(used.toFixed(2)),
+    remaining: parseFloat(remaining.toFixed(2)),
+    percentage: parseFloat(percentage.toFixed(2))
+  };
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+.app {
+  display: flex;
+  background-color: #f5f7fa;
+  color: #333;
+  overflow: hidden;
+}
+
+.sidebar {
+  width: 300px;
+  padding: 20px;
+  background-color: #fff;
+  border-right: 1px solid #ddd;
+}
+
+.tree-container {
+  height: 84vh;
+  overflow-y: auto;
+}
+
+.custom-table {
+  background-color: inherit;
+  color: inherit;
+}
+
+.main-content {
+  flex: 1;
+  padding: 20px;
+  background-color: #fff;
+}
+
+.query-section {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 20px;
+  align-items: center;
+}
+
+.progress-card {
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: #f9f9f9;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+.progress-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+
+.tip {
+  color: #999;
+  margin-top: 20px;
+}
+
+/* 左侧树形表格高亮样式 */
+:deep(.custom-table .el-table__body tr.current-row > td) {
+  background-color: #a6b9dd !important;
+  border-radius: 12px !important;
+  margin: 8px 0 !important;
+}
+
+:deep(.custom-table .el-table__body tr.current-row > td .cell) {
+  color: #fff !important;
+}
+
+.custom-table :deep(.el-table__row) {
+  background: transparent !important;
+}
+
+.custom-table :deep(.el-table__cell) {
+  padding: 6px 0 !important;
+  border: none !important;
+}
+
+.custom-table :deep(.el-table__cell .cell) {
+  line-height: 15px !important;
+  height: 15px !important;
+}
+
+.custom-table :deep(.el-table__inner-wrapper::before) {
+  display: none;
+}
+</style>

+ 345 - 0
pm_ui/src/views/meeting/index.vue

@@ -0,0 +1,345 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索栏 -->
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="queryParams.status">
+          <el-radio-button label="">全部</el-radio-button>
+          <el-radio-button label="1">当前可用</el-radio-button>
+          <el-radio-button label="2">一小时后可用</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="日期" prop="date">
+        <el-date-picker
+            v-model="queryParams.date"
+            type="date"
+            value-format="YYYY-MM-DD"
+            placeholder="选择日期"
+        />
+      </el-form-item>
+      <el-form-item label="时间范围" prop="timeRange">
+        <el-time-picker style="width: 100px"
+                        v-model="queryParams.start_time"
+                        format="HH:mm"
+                        placeholder="开始时间"
+                        @change="updateAppointTime"
+        />
+        <span style="margin: 0 2px">至</span>
+        <el-time-picker style="width: 100px"
+                        v-model="queryParams.end_time"
+                        format="HH:mm"
+                        placeholder="结束时间"
+                        @change="updateAppointTime"
+        />
+      </el-form-item>
+      <el-form-item label="会议名称" prop="search">
+        <el-input
+            v-model="queryParams.search"
+            placeholder="请输入会议名称"
+            clearable
+            @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 状态筛选 -->
+    <div class="status-filter">
+      <el-tag type="success" size="small">可用</el-tag>
+      <el-tag type="warning" size="small">占用</el-tag>
+      <el-tag type="info" size="small">禁用</el-tag>
+    </div>
+
+    <!-- 会议室列表 -->
+    <div class="meeting-list">
+      <div v-for="meeting in waringList" :key="meeting.id" class="meeting-card">
+        <div class="card-header">
+          <img :src="getImageUrl(meeting.images)" alt="会议室图片" class="meeting-image"/>
+          <div class="meeting-info">
+            <h3>{{ meeting.name }}</h3>
+            <p>
+              {{ meeting.seats === 0 ? '无限制' : meeting.seats }}人 |
+              {{ meeting.room_type_name[0] }}
+            </p>
+            <div v-if="meeting.device_name_arr.length > 0">
+              <el-tag v-for="device in meeting.device_name_arr" :key="device" size="small">{{ device }}</el-tag>
+            </div>
+            <div v-else>暂无设备信息</div>
+            <p></p>
+            <div v-if="meeting.usage && meeting.usage.appointTime && meeting.usage.appointTime.length > 0">
+              已预订时间段:
+              <span v-for="(appointment, index) in meeting.usage.appointTime">
+                  {{ appointment.start_time }} - {{ appointment.end_time }}
+              </span>
+            </div>
+            <div v-else>
+              <span>暂无预订时间段</span>
+            </div>
+          </div>
+          <!--          <el-button type="primary" size="small" @click="handleReserve(meeting)">预定</el-button>-->
+        </div>
+        <div class="time-slots">
+          <div class="slot-container">
+            <div v-for="(slot, index) in timeSlots" :key="index"
+                 :class="['slot', { current: isCurrent(slot), booked: isBooked(meeting, slot) }]">
+              {{ slot }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup name="Waring">
+import {listMeeting} from "@/api/meeting/meeting";
+import {ElMessage} from "element-plus";
+import {ref, reactive, toRefs, onMounted, nextTick} from 'vue';
+
+const waringList = ref([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const total = ref(0);
+
+// 获取当前时间
+const currentTime = getCurrentHour(); // 当前整点时间
+const data = reactive({
+  form: {},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10000,
+    status: '', // 默认选中"全部"
+    date: new Date().toISOString().split('T')[0], // 格式化为YYYY-MM-DD
+    appoint_time: "", // 重置为空
+    start_time: "",
+    end_time: "",
+    search: '' // 搜索文本默认为空
+  },
+  timeSlots: generateTimeSlots(), // 动态生成时间轴
+});
+
+const {queryParams, timeSlots} = toRefs(data);
+
+// 动态生成时间轴,从 00:00 到 23:00
+function generateTimeSlots() {
+  const slots = [];
+  for (let i = 0; i <= 23; i++) {
+    slots.push(`${i.toString().padStart(2, '0')}:00`);
+  }
+  return slots;
+}
+
+// 获取当前整点时间
+function getCurrentHour() {
+  const now = new Date();
+  const hour = now.getHours();
+  return `${hour.toString().padStart(2, '0')}:00`;
+}
+
+// 获取会议室列表
+function getList() {
+  loading.value = true;
+  listMeeting(queryParams.value).then(response => {
+    waringList.value = response.data.list;
+    total.value = response.data.total;
+    loading.value = false;
+  });
+}
+
+// 更新预约时间
+function updateAppointTime() {
+  if (queryParams.value.start_time && queryParams.value.end_time) {
+    queryParams.value.appoint_time = `${queryParams.value.start_time}~${queryParams.value.end_time}`;
+  }
+}
+
+// 搜索按钮操作
+function handleQuery() {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+// 重置按钮操作
+function resetQuery() {
+  queryParams.value = {
+    pageNum: 1,
+    pageSize: 10000,
+    status: '',
+    date: new Date().toISOString().split('T')[0],
+    appoint_time: "",
+    start_time: "",
+    end_time: "",
+    search: ''
+  };
+  handleQuery();
+}
+
+// 判断某个时间段是否被预订
+function isBooked(meeting, timeSlot) {
+  const appointments = meeting.usage?.appointTime || [];
+  return appointments.some(appointment => {
+    return appointment.start_time <= timeSlot && appointment.end_time >= timeSlot;
+  });
+}
+
+// 判断是否是当前时间
+function isCurrent(timeSlot) {
+  return timeSlot === currentTime;
+}
+
+// 预定按钮操作
+function handleReserve(meeting) {
+  proxy.$modal.confirm('是否确认预定会议室 "' + meeting.name + '"?').then(() => {
+    // 这里可以添加预定逻辑
+    ElMessage.success('预定成功');
+  }).catch(() => {
+  });
+}
+
+// 处理图片URL
+function getImageUrl(url) {
+  if (!url || url.length === 0) {
+    return 'http://183.129.224.253:9998/assets/defRoomIng.3e28e55b.png';
+  }
+  return `http://183.129.224.253:9998${url}`;
+}
+
+onMounted(() => {
+  getList();
+
+  const checkReady = () => {
+    if (waringList.value.length > 0) {
+      // 添加双重延迟确保DOM完全渲染
+      setTimeout(() => {
+        scrollToCurrentTime();
+        // 二次检查确保滚动执行
+        //setTimeout(scrollToCurrentTime, 300);
+      }, 100);
+    } else {
+      setTimeout(checkReady, 200);
+    }
+  };
+
+  checkReady();
+});
+
+// 滚动到当前时间
+function scrollToCurrentTime() {
+  const currentSlots = document.querySelectorAll('.slot.current');
+  currentSlots.forEach(slot => {
+    const container = slot.closest('.time-slots');
+    if (container) {
+      const slotRect = slot.getBoundingClientRect();
+      const containerRect = container.getBoundingClientRect();
+      container.scrollLeft = slotRect.left - containerRect.left;// - (container.clientWidth) + (slotRect.width);
+    }
+  });
+}
+</script>
+
+<style scoped>
+.time-slots {
+  overflow-x: auto;
+  width: 100%;
+  max-width: none;
+  margin-top: 10px;
+}
+
+.slot-container {
+  display: inline-flex;
+  /* min-width: calc(64px * 24 + 4px * 23);  24个时间槽+间隙 */
+  padding: 5px 0;
+  gap: 4px;
+}
+
+.app-container {
+  padding: 20px;
+}
+
+.status-filter {
+  margin-bottom: 20px;
+}
+
+.meeting-list {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
+  gap: 20px;
+}
+
+@media (max-width: 768px) {
+  .meeting-list {
+    grid-template-columns: 1fr;
+  }
+}
+
+.meeting-card {
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  padding: 16px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.meeting-image {
+  width: 100px;
+  height: 100px;
+  object-fit: cover;
+  border-radius: 8px;
+}
+
+.meeting-info {
+  flex: 1;
+  margin-left: 16px;
+}
+
+.time-slots {
+  overflow-x: auto; /* 添加水平滚动条 */
+  white-space: nowrap;
+  margin-top: 10px;
+
+  @media (max-width: 768px) {
+    max-width: 300px; /* 小屏幕宽度 */
+  }
+
+  @media (min-width: 769px) and (max-width: 1024px) {
+    max-width: 400px; /* 中等屏幕宽度 */
+  }
+
+  @media (min-width: 1025px) {
+    max-width: 900px; /* 大屏幕宽度 */
+  }
+}
+
+.slot-container {
+  display: inline-flex;
+  gap: 4px;
+}
+
+.slot {
+  width: 60px;
+  height: 20px;
+  background-color: #f0f0f0;
+  text-align: center;
+  line-height: 20px;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.slot.current {
+  background-color: #409EFF; /* 当前时间高亮 */
+  color: #fff;
+}
+
+.slot.booked {
+  background-color: #ff9900; /* 占用状态 */
+}
+</style>

+ 1 - 1
pm_ui/src/views/system/menu/index.vue

@@ -37,7 +37,7 @@
             >新增</el-button>
          </el-col>
          <el-col :span="1.5">
-            <el-button 
+            <el-button
                type="info"
                plain
                icon="Sort"

+ 3 - 1
pm_ui/src/views/system/notice/index.vue

@@ -143,7 +143,9 @@
                </el-col>
                <el-col :span="24">
                   <el-form-item label="内容">
-                    <editor v-model="form.noticeContent" :min-height="192"/>
+                    <div v-if="open">
+                      <editor v-model="form.noticeContent" :min-height="192"/>
+                    </div>
                   </el-form-item>
                </el-col>
             </el-row>

+ 21 - 0
pm_ui/src/views/zutai/index.vue

@@ -0,0 +1,21 @@
+<!--
+ * @Descripttion:
+ * @version: 1.0.0
+ * @Author: htang
+ * @Date: 2023-09-11 08:50:37
+ * @LastEditors: htang
+ * @LastEditTime: 2024-04-03 10:29:55
+-->
+<template>
+  <div class="app-page">
+    <h1>组态图绘制</h1>
+  </div>
+</template>
+
+<script lang="ts" setup>
+
+</script>
+
+<style lang="less" scoped>
+
+</style>