Kaynağa Gözat

feat(doorSecurity): 新增区域树筛选及所属区域字段

- 门禁台账新增左侧区域树筛选,支持按区域查询
- 新增/编辑弹窗增加所属区域选择
- 监控页面新增区域树筛选,按区域加载设备列表
yusheng 5 gün önce
ebeveyn
işleme
e73328489a

+ 52 - 11
src/views/doorSecurity/doorLedger/components/addOrEditDialog.vue

@@ -11,6 +11,14 @@
       :rules="formRules"
       label-width="100px"
     >
+      <el-form-item label="所属区域" prop="areaId">
+        <area-select
+          v-model="formData.areaId"
+          @checkedKeys="getAreaInfo"
+          :data="areaTreeList"
+          placeholder="请选择所属区域"
+        />
+      </el-form-item>
       <el-form-item label="设备名称" prop="deviceName">
         <el-input v-model="formData.deviceName" placeholder="请输入设备名称" clearable />
       </el-form-item>
@@ -48,8 +56,13 @@
 
 <script>
 import { getDeviceDetail, saveDevice } from '@/api/‌doorSecurity‌';
+import { basicAreaPageAPI } from '@/api/factoryModel';
+import areaSelect from '@/components/area-select.vue';
 
   export default {
+    components: {
+      areaSelect
+    },
     props: {
       visible: {
         type: Boolean,
@@ -65,8 +78,12 @@ import { getDeviceDetail, saveDevice } from '@/api/‌doorSecurity‌';
       return {
         isEdit: false,
         submitLoading: false,
+        areaTreeList: [],
         formData: {
           id: null,
+          areaId: '',
+          areaName: '',
+          areaCode: '',
           deviceName: '',
           ipAddress: '',
           port: '',
@@ -78,6 +95,9 @@ import { getDeviceDetail, saveDevice } from '@/api/‌doorSecurity‌';
           // backendUrl: '',
         },
         formRules: {
+          areaId: [
+            { required: true, message: '请选择所属区域', trigger: 'change' }
+          ],
           deviceName: [
             { required: true, message: '请输入设备名称', trigger: 'blur' }
           ],
@@ -125,24 +145,18 @@ import { getDeviceDetail, saveDevice } from '@/api/‌doorSecurity‌';
         console.log('data~~~', this.data);
         const row = this.data;
         this.isEdit = !!row?.id;
+        // 加载区域树
+        this.getAreaTreeList();
         if (row?.id) {
-          // this.formData = {
-          //   id: row.id,
-          //   deviceName: row.deviceName || '',
-          //   ipAddress: row.ipAddress || '',
-          //   port: row.port || '',
-          //   model: row.model || '',
-          //   account: row.account || '',
-          //   password: row.password || '',
-          //   doorCount: row.doorCount ?? 0,
-          //   stream: row.stream || '',
-          // };
           const res = await getDeviceDetail(row.id);
           console.log(res, 'res');
           this.formData = res;
         } else {
           this.formData = {
             id: null,
+            areaId: row?.areaId || '',
+            areaName: row?.areaName || '',
+            areaCode: row?.areaCode || '',
             deviceName: '',
             ipAddress: '',
             port: '',
@@ -159,6 +173,33 @@ import { getDeviceDetail, saveDevice } from '@/api/‌doorSecurity‌';
         });
       },
 
+      /* 获取区域树 */
+      getAreaTreeList() {
+        basicAreaPageAPI({
+          pageNum: 1,
+          size: 9999
+        })
+          .then((list) => {
+            this.areaTreeList = this.$util.toTreeData({
+              data: list || [],
+              idField: 'id',
+              parentIdField: 'parentId'
+            });
+          })
+          .catch((e) => {
+            console.error('getAreaTreeList error', e);
+          });
+      },
+
+      /* 区域选择回调 */
+      getAreaInfo(nodeInfo) {
+        if (nodeInfo) {
+          this.formData.areaId = nodeInfo.id;
+          this.formData.areaName = nodeInfo.name;
+          this.formData.areaCode = nodeInfo.areaCode;
+        }
+      },
+
       /* 弹窗关闭 */
       handleClose() {
         this.$refs.editForm?.clearValidate();

+ 96 - 2
src/views/doorSecurity/doorLedger/index.vue

@@ -1,6 +1,28 @@
 <template>
   <div class="ele-body">
     <el-card shadow="never" v-loading="loading" style="width: 100%">
+      <ele-split-layout
+        width="266px"
+        allow-collapse
+        :right-style="{ overflow: 'hidden' }"
+      >
+        <div>
+          <div class="ele-border-lighter sys-organization-list">
+            <div class="tree-title">区域列表</div>
+            <el-tree
+              ref="areaTree"
+              :data="areaTreeList"
+              :props="defaultProps"
+              node-key="id"
+              highlight-current
+              default-expand-all
+              :expand-on-click-node="false"
+              @node-click="handleAreaClick"
+            >
+            </el-tree>
+          </div>
+        </div>
+        <template v-slot:content>
       <!-- 搜索表单 -->
       <seekPage :seekList="seekList" :formLength="3" @search="reload"></seekPage>
       <!-- 数据表格 -->
@@ -100,6 +122,8 @@
           </el-popconfirm>
         </template>
       </ele-pro-table>
+        </template>
+      </ele-split-layout>
     </el-card>
 
     <!-- 新增、修改弹窗 -->
@@ -120,6 +144,7 @@
 <script>
   import seekPage from '@/BIZComponents/seekPage.vue';
   import tabMixins from '@/mixins/tableColumnsMixin';
+  import { basicAreaPageAPI } from '@/api/factoryModel';
   import { listDoorLedger, removeDoorLedger, enableDoorLedger, loginHaiKan } from '@/api/‌doorSecurity‌';
   import addOrEditDialog from './components/addOrEditDialog.vue';
   import detailDialog from './components/detailDialog.vue';
@@ -142,6 +167,13 @@
         columnsVersion: 1,
         statusMap: {
           0: '在线', 1: '离线', 2: '异常'
+        },
+        // 区域树
+        currentArea: null,
+        areaTreeList: [],
+        defaultProps: {
+          children: 'children',
+          label: 'name'
         }
       };
     },
@@ -171,6 +203,13 @@
             showOverflowTooltip: true,
             slot: 'deviceName'
           },
+          {
+            minWidth: 140,
+            prop: 'areaName',
+            label: '所属区域',
+            align: 'center',
+            showOverflowTooltip: true
+          },
           {
             minWidth: 140,
             prop: 'ipAddress',
@@ -293,7 +332,34 @@
         ];
       }
     },
+    created() {
+      this.getAreaTreeList();
+    },
     methods: {
+      /* 获取区域树 */
+      getAreaTreeList() {
+        basicAreaPageAPI({
+          pageNum: 1,
+          size: 9999
+        })
+          .then((list) => {
+            this.areaTreeList = this.$util.toTreeData({
+              data: list || [],
+              idField: 'id',
+              parentIdField: 'parentId'
+            });
+          })
+          .catch((e) => {
+            console.error('getAreaTreeList error', e);
+          });
+      },
+
+      /* 点击区域节点查询 */
+      handleAreaClick(data) {
+        this.currentArea = data;
+        this.reload({ areaId: data.id });
+      },
+
       openJump(row) {
         loginHaiKan(row.id).then((res) => {
           console.log(res, 'res');
@@ -307,7 +373,8 @@
         return listDoorLedger({
           pageNum: page,
           size: limit,
-          ...where
+          ...where,
+          areaId: this.currentArea?.id
         });
       },
 
@@ -319,7 +386,9 @@
       /* 打开新增/修改弹窗 */
       openEdit(row) {
         console.log(row);
-        this.currentRow = row || null;
+        // 新增时带入左侧选中的区域
+        const areaInfo = this.currentArea;
+        this.currentRow = row || (areaInfo ? { areaId: areaInfo.id, areaName: areaInfo.name, areaCode: areaInfo.areaCode } : null);
         this.editVisible = true;
       },
 
@@ -364,4 +433,29 @@
   :deep(.el-link--inner) {
     margin-left: 0px !important;
   }
+
+  .sys-organization-list {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    box-sizing: border-box;
+    border-width: 1px;
+    border-style: solid;
+    overflow: auto;
+  }
+
+  .tree-title {
+    padding: 10px 12px;
+    font-weight: bold;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  .sys-organization-list :deep(.el-tree-node__content) {
+    height: 36px;
+    & > .el-tree-node__expand-icon {
+      margin-left: 10px;
+    }
+  }
 </style>

+ 194 - 59
src/views/monitor/index.vue

@@ -1,68 +1,89 @@
 <template>
   <div class="monitor-page" v-loading="pageLoading">
-    <!-- 视频网格区域 -->
-    <div
-      class="grid-area"
-      ref="gridArea"
-      @scroll="handleScroll"
-    >
-      <!-- 视频网格 -->
+    <!-- 左侧区域树 -->
+    <div class="area-panel">
+      <div class="tree-title">区域列表</div>
+      <div class="area-tree-wrap">
+        <el-tree
+          ref="areaTree"
+          :data="areaTreeList"
+          :props="defaultProps"
+          node-key="id"
+          highlight-current
+          default-expand-all
+          :expand-on-click-node="false"
+          @node-click="handleAreaClick"
+        >
+        </el-tree>
+      </div>
+    </div>
+
+    <!-- 右侧内容区 -->
+    <div class="content-panel">
+      <!-- 视频网格区域 -->
       <div
-        class="video-grid"
-        :style="gridStyle"
+        class="grid-area"
+        ref="gridArea"
+        @scroll="handleScroll"
       >
-        <VideoCell
-          v-for="(item, index) in deviceList"
-          :key="item.deviceCode || item.code || index"
-          ref="cells"
-          :device="item"
-          :isActive="selectedIndex === index"
-          :url="playingUrls[item.deviceCode || item.code] || ''"
-          @click="onCellClick(item, index)"
-        />
-      </div>
+        <!-- 视频网格 -->
+        <div
+          class="video-grid"
+          :style="gridStyle"
+        >
+          <VideoCell
+            v-for="(item, index) in deviceList"
+            :key="item.deviceCode || item.code || index"
+            ref="cells"
+            :device="item"
+            :isActive="selectedIndex === index"
+            :url="playingUrls[item.deviceCode || item.code] || ''"
+            @click="onCellClick(item, index)"
+          />
+        </div>
 
-      <!-- 加载提示 -->
-      <div class="load-tip" v-if="loadingMore">
-        <i class="el-icon-loading"></i> 加载中...
-      </div>
-      <div class="load-tip" v-else-if="!hasMore && deviceList.length">
-        没有更多设备了
-      </div>
-      <div class="load-tip" v-else-if="!deviceList.length && !pageLoading">
-        暂无设备
+        <!-- 加载提示 -->
+        <div class="load-tip" v-if="loadingMore">
+          <i class="el-icon-loading"></i> 加载中...
+        </div>
+        <div class="load-tip" v-else-if="!hasMore && deviceList.length">
+          没有更多设备了
+        </div>
+        <div class="load-tip" v-else-if="!deviceList.length && !pageLoading">
+          暂无设备
+        </div>
       </div>
-    </div>
 
-    <!-- 底部工具栏 -->
-    <div class="toolbar">
-      <div class="toolbar-left">
-        <div class="layout-group">
-          <div
-            v-for="size in layoutOptions"
-            :key="size"
-            class="layout-btn"
-            :class="{ active: gridSize === size }"
-            :title="size + '分屏'"
-            @click="changeLayout(size)"
-          >
-            <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
-              <rect v-for="r in getLayoutRects(size)" :key="r.i" :x="r.x" :y="r.y" :width="r.w" :height="r.h" rx="0.5" fill="currentColor"/>
-            </svg>
+      <!-- 底部工具栏 -->
+      <div class="toolbar">
+        <div class="toolbar-left">
+          <div class="layout-group">
+            <div
+              v-for="size in layoutOptions"
+              :key="size"
+              class="layout-btn"
+              :class="{ active: gridSize === size }"
+              :title="size + '分屏'"
+              @click="changeLayout(size)"
+            >
+              <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
+                <rect v-for="r in getLayoutRects(size)" :key="r.i" :x="r.x" :y="r.y" :width="r.w" :height="r.h" rx="0.5" fill="currentColor"/>
+              </svg>
+            </div>
           </div>
         </div>
-      </div>
 
-      <div class="toolbar-center">
-        <el-button type="text" icon="el-icon-video-play" class="tool-btn" @click="batchPlay">批量播放</el-button>
-        <el-button type="text" icon="el-icon-video-pause" class="tool-btn" @click="batchStop">取消拉流</el-button>
-      </div>
+        <div class="toolbar-center">
+          <el-button type="text" icon="el-icon-video-play" class="tool-btn" @click="batchPlay">批量播放</el-button>
+          <el-button type="text" icon="el-icon-video-pause" class="tool-btn" @click="batchStop">取消拉流</el-button>
+        </div>
 
-      <div class="toolbar-right">
-        <el-button type="text" icon="el-icon-rank" class="tool-btn" @click="togglePanel">
-          {{ panelVisible ? '隐藏控制台' : '控制台' }}
-        </el-button>
-        <el-button type="text" icon="el-icon-full-screen" class="tool-btn" @click="toggleFullscreen"></el-button>
+        <div class="toolbar-right">
+          <el-button type="text" icon="el-icon-rank" class="tool-btn" @click="togglePanel">
+            {{ panelVisible ? '隐藏控制台' : '控制台' }}
+          </el-button>
+          <el-button type="text" icon="el-icon-full-screen" class="tool-btn" @click="toggleFullscreen"></el-button>
+        </div>
       </div>
     </div>
 
@@ -153,6 +174,8 @@ import direction from './components/direction.vue';
 import PresetPositions from './components/Preset-positions.vue';
 import * as realTime from '@/api/isp/ispRealtime/monitor/index';
 import * as deviceApi from '@/api/isp/deviceManage/robot';
+import * as CameraApi from '@/api/ledgerAssets/camera';
+import { basicAreaPageAPI } from '@/api/factoryModel';
 
 export default {
   name: 'MonitorPage',
@@ -173,7 +196,14 @@ export default {
       slider: 1,
       screenshotVal: false,
       PresetList: [],
-      presetHeight: 200
+      presetHeight: 200,
+      // 区域树
+      currentArea: null,
+      areaTreeList: [],
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      }
     };
   },
   computed: {
@@ -203,11 +233,37 @@ export default {
   },
   mounted() {
     this.loadCameraData();
+    this.getAreaTreeList();
   },
   beforeDestroy() {
     this.batchStop();
   },
   methods: {
+    // ========== 区域树 ==========
+    /* 获取区域树 */
+    getAreaTreeList() {
+      basicAreaPageAPI({
+        pageNum: 1,
+        size: 9999
+      })
+        .then((list) => {
+          this.areaTreeList = this.$util.toTreeData({
+            data: list || [],
+            idField: 'id',
+            parentIdField: 'parentId'
+          });
+        })
+        .catch((e) => {
+          console.error('getAreaTreeList error', e);
+        });
+    },
+
+    /* 点击区域节点查询 */
+    handleAreaClick(data) {
+      this.currentArea = data;
+      this.loadCameraData(false);
+    },
+
     // ========== 布局相关 ==========
     changeLayout(size) {
       this.gridSize = size;
@@ -248,12 +304,16 @@ export default {
       }
 
       try {
-        const res = await realTime.cameraPage({
+        const params = {
           size: this.gridSize,
           pageNum: this.dataPageNum
-        });
-        const list = Array.isArray(res.records) ? res.records : [];
-        const total = res.total || 0;
+        };
+        if (this.currentArea && this.currentArea.id) {
+          params.areaId = this.currentArea.id;
+        }
+        const res = await CameraApi.getDevicePage(params);
+        const list = Array.isArray(res.data?.records) ? res.data.records : [];
+        const total = res.data?.total || 0;
         this.deviceList = more ? this.deviceList.concat(list) : list;
         this.dataPageNum++;
         this.hasMore = this.deviceList.length < total;
@@ -455,6 +515,81 @@ export default {
   background-color: #0b1a28;
   overflow: hidden;
   display: flex;
+  flex-direction: row;
+}
+
+// ====== 左侧区域树 ======
+.area-panel {
+  flex-shrink: 0;
+  width: 220px;
+  height: 100%;
+  background-color: #0d1f30;
+  border-right: 1px solid #1a3047;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.tree-title {
+  flex-shrink: 0;
+  height: 46px;
+  padding: 0 16px;
+  display: flex;
+  align-items: center;
+  font-size: 15px;
+  font-weight: 600;
+  color: #e0ecf7;
+  border-bottom: 1px solid #1a3047;
+}
+
+.area-tree-wrap {
+  flex: 1;
+  overflow-y: auto;
+  padding: 8px 0;
+
+  &::-webkit-scrollbar {
+    width: 5px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background-color: #2c3e5a;
+    border-radius: 3px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+
+  ::v-deep .el-tree {
+    background: transparent;
+    color: rgba(255, 255, 255, 0.75);
+  }
+
+  ::v-deep .el-tree-node__content {
+    height: 32px;
+    color: rgba(255, 255, 255, 0.75);
+
+    &:hover {
+      background: rgba(44, 138, 224, 0.1);
+    }
+  }
+
+  ::v-deep .el-tree-node.is-current > .el-tree-node__content {
+    background: rgba(64, 200, 255, 0.15);
+    color: #40c8ff;
+  }
+
+  ::v-deep .el-tree-node__expand-icon {
+    color: rgba(255, 255, 255, 0.5);
+  }
+}
+
+// ====== 右侧内容区 ======
+.content-panel {
+  flex: 1;
+  min-width: 0;
+  height: 100%;
+  display: flex;
   flex-direction: column;
 }