Bläddra i källkod

feat: 保密要害部位完成

bxy 7 månader sedan
förälder
incheckning
6ca3fbe5d2
21 ändrade filer med 1628 tillägg och 59 borttagningar
  1. 114 1
      src/api/security-confidentiality-position/index.ts
  2. 2 1
      src/components/Render.vue
  3. 2 2
      src/components/TableColumn.vue
  4. 8 1
      src/components/position-monitor-camera-edit/UpdatePositionMonitorCamera.vue
  5. 161 0
      src/components/thumbnail/ThumbnailClick.vue
  6. 42 3
      src/views/security-confidentiality/confidentiality-position/monitor-records/MonitorRecords.vue
  7. 251 0
      src/views/security-confidentiality/confidentiality-position/monitor-records/components/AccessRecords.vue
  8. 286 0
      src/views/security-confidentiality/confidentiality-position/monitor-records/components/InvasionSnapshot.vue
  9. 21 0
      src/views/security-confidentiality/confidentiality-position/monitor-records/config/index.ts
  10. 112 0
      src/views/security-confidentiality/confidentiality-position/monitor-records/config/table.ts
  11. 44 0
      src/views/security-confidentiality/confidentiality-position/monitor-records/constant/index.ts
  12. 338 3
      src/views/security-confidentiality/confidentiality-position/position-management/PositionManagement.vue
  13. 15 0
      src/views/security-confidentiality/confidentiality-position/position-management/config/index.ts
  14. 9 0
      src/views/security-confidentiality/confidentiality-position/position-management/config/search.ts
  15. 56 0
      src/views/security-confidentiality/confidentiality-position/position-management/config/table.ts
  16. 31 0
      src/views/security-confidentiality/confidentiality-position/position-management/constant/index.ts
  17. 123 39
      src/views/security-confidentiality/security-position/SecurityPosition.vue
  18. 9 9
      src/views/security-confidentiality/security-position/config/table.ts
  19. 1 0
      utils/devProxy/staff/proxy.ts
  20. 2 0
      utils/devProxy/types.ts
  21. 1 0
      utils/devProxy/utils.ts

+ 114 - 1
src/api/security-confidentiality-position/index.ts

@@ -1,5 +1,6 @@
 import { http } from '@/utils/http/axios';
 import { GetCameraGroupListRes } from '@/api/disaster-overview';
+import type { QueryPageRequest, QueryPageResponse } from '@/types/basic-query';
 
 /**
  * @description: 查询治安重点部位
@@ -59,10 +60,122 @@ export interface UpdateCameraGroupOrderParam {
   id: number; // 分组id
   orderNum: number; // 排序序号
 }
-export const updateCameraGroupOrder = (data: UpdateCameraGroupOrderParam) => {
+export const updateCameraGroupOrder = (data: UpdateCameraGroupOrderParam[]) => {
   return http.request({
     url: '/cameraGroup/updateCameraGroupOrder',
     method: 'post',
     data,
   });
 };
+
+/**
+ * @description: 保密要害部位--监测记录--分页查询入侵抓拍记录
+ */
+export interface QueryInvasionSnapshotParams {
+  location?: string; // 地点
+  startTime?: string; // 开始时间
+  endTime?: string; // 结束时间
+}
+export interface QueryInvasionSnapshotRes {
+  id: number; // 自增主键
+  event: string; // 事件
+  location: string; // 抓拍地点
+  pictures: string; // 抓拍图片
+  eventTime: string; // 抓拍时间
+  createdAt: string; // 创建时间
+  updatedAt: string; // 更新时间
+  isDeleted: number; // 是否删除:0-是,大于0-否
+}
+export const getInvasionSnapshotList = (data: QueryPageRequest<QueryInvasionSnapshotParams>) => {
+  return http.request<QueryPageResponse<QueryInvasionSnapshotRes>>({
+    url: '/detectRecord/queryIntrusionCaptureRecordPage',
+    method: 'post',
+    data,
+  });
+};
+
+/**
+ * @description: 保密要害部位--监测记录--查询抓拍开关
+ */
+export const getInvasionSnapshotSwitch = () => {
+  return http.request({
+    url: '/detectRecord/querySnapshotSwitch',
+    method: 'get',
+  });
+};
+
+/**
+ * @description: 保密要害部位--监测记录--更新抓拍开关
+ */
+export const updateInvasionSnapshotSwitch = (data: { switchStatus: boolean }) => {
+  return http.request({
+    url: '/detectRecord/updateSnapshotSwitch',
+    method: 'post',
+    data,
+  });
+};
+
+/**
+ * @description: 保密要害部位--监测记录--分页查询门禁记录
+ */
+export interface QueryAccessRecordParams {
+  username?: string; // 姓名
+  cardNum?: string; // 卡号
+  deptName?: string; // 部门名称
+  location?: string; // 进出地点
+  startTime?: string; // 开始时间
+  endTime?: string; // 结束时间
+}
+export interface QueryAccessRecordRes {
+  id: number; // 自增主键
+  username: string; // 姓名
+  cardNum: string; // 卡号
+  deptName: string; // 部门名称
+  event: string; // 事件
+  location: string; // 进出地点
+  eventTime: string; // 进出时间
+  createdAt: string; // 创建时间
+  updatedAt: string; // 更新时间
+  isDeleted: number; // 是否删除:0-是,大于0-否
+}
+export const getAccessRecordList = (data: QueryPageRequest<QueryAccessRecordParams>) => {
+  return http.request<QueryPageResponse<QueryAccessRecordRes>>({
+    url: '/detectRecord/queryAccessControlRecordPage',
+    method: 'post',
+    data,
+  });
+};
+
+/**
+ * @description: 保密要害部位--监测记录--导出入侵抓拍记录
+ */
+export const exportInvasionSnapshot = (data: QueryInvasionSnapshotParams) => {
+  return http.request(
+    {
+      url: '/detectRecord/exportIntrusionCaptureRecord',
+      method: 'post',
+      responseType: 'blob',
+      data,
+    },
+    {
+      isTransformResponse: false,
+    },
+  );
+};
+
+/**
+ * @description: 保密要害部位--监测记录--导出门禁记录
+ */
+export const exportAccessRecord = (data: QueryAccessRecordParams) => {
+  return http.request(
+    {
+      url: '/detectRecord/exportAccessControlRecord',
+      method: 'post',
+      responseType: 'blob',
+      data,
+    },
+    {
+      isTransformResponse: false,
+    },
+  );
+};

+ 2 - 1
src/components/Render.vue

@@ -3,6 +3,7 @@
   const props = {
     render: { type: Function, required: true },
     row: { type: Object, required: true },
+    index: { type: Number, required: false },
   };
 
   export default defineComponent({
@@ -11,7 +12,7 @@
     setup(props) {
       return () => {
         const { render } = props;
-        return render && render(props.row);
+        return render && render(props.row, props.index);
       };
     },
   });

+ 2 - 2
src/components/TableColumn.vue

@@ -8,8 +8,8 @@
   </el-table-column>
   <el-table-column v-else :prop="column.prop" :label="column.label" v-bind="getColumnAttrs">
     <template #default="scope">
-      <Render v-if="column.render" :render="column.render" :row="scope.row" />
-      <slot v-else-if="column.slot" :name="column.slot" :row="scope.row" />
+      <Render v-if="column.render" :render="column.render" :row="scope.row" :index="scope.$index" />
+      <slot v-else-if="column.slot" :name="column.slot" :row="scope.row" :index="scope.$index" />
     </template>
   </el-table-column>
 </template>

+ 8 - 1
src/components/position-monitor-camera-edit/UpdatePositionMonitorCamera.vue

@@ -11,7 +11,14 @@
       <div v-loading="loading">
         <div class="position-name">
           <div class="position-name-title">部位名称:</div>
-          <el-input v-model="positionName" style="width: 294px" placeholder="请输入部位名称" />
+          <el-input
+            v-model="positionName"
+            maxlength="200"
+            show-word-limit
+            clearable
+            style="width: 294px"
+            placeholder="请输入部位名称"
+          />
         </div>
         <div class="relate-camera">关联相机:</div>
         <div class="camera-select-section">

+ 161 - 0
src/components/thumbnail/ThumbnailClick.vue

@@ -0,0 +1,161 @@
+<template>
+  <div class="thumb-nail">
+    <ElPopover
+      popper-class="thumb-popover"
+      ref="popoverRef"
+      :placement="position || 'bottom'"
+      :show-after="100"
+      :hide-after="0"
+      @show="handleMouseEnter"
+      @hide="handleMouseLeave"
+      trigger="click"
+    >
+      <template #reference>
+        <div>
+          <slot></slot>
+        </div>
+      </template>
+      <template #default>
+        <ElImage
+          v-loading="isLoading"
+          :src="imgUrl || EmptyImg"
+          fit="contain"
+          class="thumb-img"
+          :preview-src-list="srcList"
+          :zoom-rate="1.2"
+          :max-scale="3"
+          :min-scale="0.1"
+          @close="handleClose"
+        />
+      </template>
+    </ElPopover>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { ElPopover, ElImage } from 'element-plus';
+  import EmptyImg from '@/assets/images/nine-square-grid/empty.png';
+  import { captureFirstPicFromFLV } from '@/utils/flv/captureFirstPicFromFLV';
+  import { getCameraInfoDetail } from '@/api/camera/camera';
+  import { CameraDetailServer } from '@/types/scene-types/scene-types';
+  import { ref } from 'vue';
+  import { useUserStore } from '@/store/modules/user';
+  import { storeToRefs } from 'pinia';
+
+  const userStore = useUserStore();
+  const { token } = storeToRefs(userStore);
+
+  const isLoading = ref(false);
+  const imgUrl = ref();
+  const srcList = ref<string[]>([]);
+  const popoverRef = ref();
+
+  const props = defineProps<{
+    // 图片预览的url
+    imageUrl?: string;
+    code?: string;
+    position?:
+      | 'top'
+      | 'top-start'
+      | 'top-end'
+      | 'bottom'
+      | 'bottom-start'
+      | 'bottom-end'
+      | 'left'
+      | 'left-start'
+      | 'left-end'
+      | 'right'
+      | 'right-start'
+      | 'right-end';
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'mouse-enter', code: string): void;
+    (e: 'mouse-leave'): void;
+  }>();
+
+  const getCameraDetail = async (code: string) => {
+    return getCameraInfoDetail(code);
+  };
+
+  const getPushstreamImg = async (detail: CameraDetailServer) => {
+    return detail.pushStreamDTO?.imageUrl;
+  };
+
+  const getPushstreamIp = async (detail: CameraDetailServer) => {
+    if (/macintosh|mac os x/i.test(navigator.userAgent)) {
+      return detail.pushStreamDTO?.videoUrls?.m3u8PushstreamIp;
+    } else if (detail.render) {
+      return detail.pushStreamDTO?.videoUrls?.pushstreamRenderUrl;
+    }
+    return detail.pushStreamDTO?.videoUrls?.pushstreamIp;
+  };
+
+  const handleMouseEnter = async () => {
+    emit('mouse-enter', props.code || '');
+    // if (imgUrl.value) return;
+    srcList.value = [];
+    isLoading.value = true;
+    const imageUrl = props.imageUrl;
+    if (imageUrl) {
+      imgUrl.value = imageUrl;
+      srcList.value.push(imageUrl);
+      isLoading.value = false;
+      return;
+    }
+
+    if (!props.code) return;
+    const detail = await getCameraDetail(props.code);
+    let url: string | null;
+    url = await getPushstreamImg(detail);
+    if (url) {
+      imgUrl.value = url;
+      srcList.value.push(url);
+      isLoading.value = false;
+      return;
+    }
+    const videoSrc = await getPushstreamIp(detail);
+
+    const urlWithToken = videoSrc + '?token=' + token.value;
+    url = await captureFirstPicFromFLV(urlWithToken);
+    imgUrl.value = url;
+    srcList.value.push(url);
+    isLoading.value = false;
+  };
+
+  const handleMouseLeave = () => {
+    emit('mouse-leave');
+  };
+
+  const handleClose = () => {
+    popoverRef.value.hide();
+  };
+
+  defineExpose({
+    handleClose,
+  });
+</script>
+
+<style lang="scss" scoped>
+  .thumb-nail {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+  }
+</style>
+<style lang="scss">
+  .thumb-popover {
+    display: flex;
+    align-items: center;
+    padding: 0 !important;
+    border-radius: 5px !important;
+    width: 400px !important;
+  }
+
+  .thumb-img {
+    width: 100%;
+    border-radius: 5px;
+  }
+</style>

+ 42 - 3
src/views/security-confidentiality/confidentiality-position/monitor-records/MonitorRecords.vue

@@ -1,7 +1,46 @@
 <template>
-  <div> overview </div>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <div class="breadcrumb-title"> 监测记录 </div>
+      <el-tabs v-model="activeName">
+        <el-tab-pane
+          v-for="item in MONITOR_RECORDS_SUBPAGES"
+          :key="item.value"
+          :label="item.label"
+          :name="item.value"
+        />
+      </el-tabs>
+    </header>
+    <main class="safety-platform-container__main">
+      <component :is="dynamicComponent" ref="dynamicComponentRef" />
+    </main>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { ref, computed, defineAsyncComponent } from 'vue';
+  import { MONITOR_RECORDS_SUBPAGES } from './constant';
 
-<style scoped></style>
+  const activeName = ref(MONITOR_RECORDS_SUBPAGES[0].value);
+
+  const dynamicComponent = computed(() => {
+    return activeName.value === MONITOR_RECORDS_SUBPAGES[1].value
+      ? defineAsyncComponent(() => import('./components/AccessRecords.vue'))
+      : defineAsyncComponent(() => import('./components/InvasionSnapshot.vue'));
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+  .safety-platform-container__header {
+    padding-bottom: 0 !important;
+  }
+  :deep(.el-tabs__header) {
+    margin: 0;
+  }
+  :deep(.el-tabs__item) {
+    font-size: 14px !important;
+  }
+</style>

+ 251 - 0
src/views/security-confidentiality/confidentiality-position/monitor-records/components/AccessRecords.vue

@@ -0,0 +1,251 @@
+<template>
+  <div class="access-records-container">
+    <div class="search-container">
+      <el-input
+        v-model="searchKeyword"
+        :placeholder="`请输入${curSearchTypeLabel}进行搜索`"
+        clearable
+        @input="handleSearch"
+        @keyup.enter="handleSearch"
+        style="width: 400px"
+      >
+        <template #prefix>
+          <el-icon color="#1777ff"><Search /></el-icon>
+        </template>
+        <template #prepend>
+          <el-select
+            v-model="searchSelectedType"
+            placeholder="选择搜索项"
+            @change="handleSelectedTypeChange"
+            style="width: 100px"
+          >
+            <el-option v-for="item in accessQueryOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </template>
+      </el-input>
+      <div class="search-time">
+        <span>进出时间:</span>
+        <el-date-picker
+          v-model="queryTimes"
+          type="datetimerange"
+          range-separator="-"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          @change="handleSearch"
+        />
+      </div>
+      <div class="search-container-btn">
+        <el-button type="primary" @click="handleSearch">查询</el-button>
+        <el-button @click="handleReset">重置</el-button>
+        <el-button @click="handleExport">导出</el-button>
+      </div>
+    </div>
+    <div class="table-container">
+      <BasicTable
+        ref="basicTableRef"
+        :tableData="tableData"
+        :tableConfig="tableConfig"
+        @update:page-number="handleCurrentPageChange"
+        @update:page-size="handlePageSizeChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed, onMounted } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { Search } from '@element-plus/icons-vue';
+  import BasicTable from '@/components/BasicTable.vue';
+  import { downloadByData } from '@/utils/file/download';
+  import { msgConfirm } from '@/utils/element-plus/messageBox';
+  import { getCurrentDateTimeString } from '@/utils/dateUtil';
+  import type { QueryPageRequest } from '@/types/basic-query';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import {
+    getAccessRecordList,
+    exportAccessRecord,
+    type QueryAccessRecordParams,
+    type QueryAccessRecordRes,
+  } from '@/api/security-confidentiality-position';
+  import {
+    ACCESS_RECORD_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+    ACCESS_RECORD_LIST_TABLE_OPTIONS,
+    ACCESS_RECORD_LIST_TABLE_COLUMNS,
+  } from '../config/table';
+  import { ACCESS_FIELDTYPE, ACCESS_FIELD_CONTENT, accessQueryOptions } from '../constant';
+
+  const { tableConfig, pagination } = useTableConfig(
+    ACCESS_RECORD_LIST_TABLE_COLUMNS,
+    ACCESS_RECORD_LIST_TABLE_OPTIONS,
+  );
+
+  const searchSelectedType = ref(ACCESS_FIELDTYPE.USERNAME);
+  const searchKeyword = ref('');
+  const queryTimes = ref(['', '']);
+  const curSearchTypeLabel = computed(() => {
+    const option = accessQueryOptions.find((item) => item.value === searchSelectedType.value);
+    return option ? option.label : ACCESS_FIELD_CONTENT[searchSelectedType.value];
+  });
+
+  const accessRecordTableQuery: QueryPageRequest<QueryAccessRecordParams> = {
+    pageNumber: pagination.pageNumber,
+    pageSize: pagination.pageSize,
+    queryParam: {},
+  };
+
+  const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+  const tableData = ref<QueryAccessRecordRes[]>([]);
+
+  const handleSelectedTypeChange = () => {
+    searchKeyword.value = '';
+  };
+
+  const handleSearch = () => {
+    if (!queryTimes.value) queryTimes.value = ['', ''];
+    getTableData();
+  };
+
+  const handleReset = () => {
+    searchSelectedType.value = ACCESS_FIELDTYPE.USERNAME;
+    searchKeyword.value = '';
+    queryTimes.value = ['', ''];
+    getTableData();
+  };
+
+  // 导出
+  const handleExport = () => {
+    msgConfirm('确定导出所查询数据?', '导出', {
+      confirmButtonText: '确定',
+      showCancelButton: true,
+      type: 'warning',
+    })
+      .then(() => {
+        const exportParams: QueryAccessRecordParams = {
+          startTime: queryTimes.value[0] || undefined,
+          endTime: queryTimes.value[1] || undefined,
+        };
+
+        // 根据搜索类型设置对应的参数
+        if (searchKeyword.value) {
+          switch (searchSelectedType.value) {
+            case ACCESS_FIELDTYPE.USERNAME:
+              exportParams.username = searchKeyword.value;
+              break;
+            case ACCESS_FIELDTYPE.CARD_NUM:
+              exportParams.cardNum = searchKeyword.value;
+              break;
+            case ACCESS_FIELDTYPE.DEPT_NAME:
+              exportParams.deptName = searchKeyword.value;
+              break;
+            case ACCESS_FIELDTYPE.LOCATION:
+              exportParams.location = searchKeyword.value;
+              break;
+          }
+        }
+
+        exportAccessRecord(exportParams).then(async (response) => {
+          if (!response) {
+            throw new Error('下载文件失败');
+          }
+          downloadByData(response, `门禁记录_${getCurrentDateTimeString()}.xlsx`);
+          ElMessage.success('下载文件成功');
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消导出',
+        });
+      });
+  };
+
+  const handleCurrentPageChange = (pageNumber: number) => {
+    pagination.pageNumber = pageNumber;
+    accessRecordTableQuery.pageNumber = pageNumber;
+    getTableData();
+  };
+
+  const handlePageSizeChange = (pageSize: number) => {
+    pagination.pageSize = pageSize;
+    accessRecordTableQuery.pageSize = pageSize;
+    getTableData();
+  };
+
+  const getTableData = () => {
+    tableConfig.loading = true;
+    accessRecordTableQuery.queryParam = {
+      startTime: queryTimes.value[0] || undefined,
+      endTime: queryTimes.value[1] || undefined,
+    };
+
+    // 根据搜索类型设置对应的参数
+    if (searchKeyword.value) {
+      switch (searchSelectedType.value) {
+        case ACCESS_FIELDTYPE.USERNAME:
+          accessRecordTableQuery.queryParam.username = searchKeyword.value;
+          break;
+        case ACCESS_FIELDTYPE.CARD_NUM:
+          accessRecordTableQuery.queryParam.cardNum = searchKeyword.value;
+          break;
+        case ACCESS_FIELDTYPE.DEPT_NAME:
+          accessRecordTableQuery.queryParam.deptName = searchKeyword.value;
+          break;
+        case ACCESS_FIELDTYPE.LOCATION:
+          accessRecordTableQuery.queryParam.location = searchKeyword.value;
+          break;
+      }
+    }
+
+    getAccessRecordList(accessRecordTableQuery).then((res) => {
+      tableData.value = res?.records || [];
+      pagination.total = res?.totalRow || 0;
+    });
+    tableConfig.loading = false;
+  };
+
+  onMounted(() => {
+    getTableData();
+    tableConfig.maxHeight = ACCESS_RECORD_LIST_TABLE_MAX_HEIGHT_DEFAULT;
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+
+  .access-records-container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .search-container {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    margin-bottom: 20px;
+
+    .search-time {
+      color: rgba(0, 0, 0, 0.85);
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+
+    .search-container-btn {
+      margin-left: auto;
+      display: flex;
+      gap: 8px;
+    }
+  }
+
+  .table-container {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 286 - 0
src/views/security-confidentiality/confidentiality-position/monitor-records/components/InvasionSnapshot.vue

@@ -0,0 +1,286 @@
+<template>
+  <div class="invasion-snapshot-container">
+    <div class="switch-container" v-if="monitorManagePermission">
+      <el-tooltip content="开关关闭后,抓拍到的入侵行为将不在此进行记录展示" placement="top">
+        <el-icon color="#1777ff" size="20"><QuestionFilled /></el-icon>
+      </el-tooltip>
+      <span>抓拍启停</span>
+      <el-switch v-model="switchStatus" @change="updateSwitchStatus" />
+    </div>
+    <div class="search-container">
+      <div class="search-location">
+        <span>抓拍地点:</span>
+        <el-input
+          v-model="searchKeyword"
+          placeholder="请输入抓拍地点"
+          clearable
+          @input="handleSearch"
+          @keyup.enter="handleSearch"
+          style="width: 380px"
+        >
+          <template #prefix>
+            <el-icon color="#1777ff"><Search /></el-icon>
+          </template>
+        </el-input>
+      </div>
+      <div class="search-time">
+        <span>抓拍时间:</span>
+        <el-date-picker
+          v-model="queryTimes"
+          type="datetimerange"
+          range-separator="-"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          @change="handleSearch"
+        />
+      </div>
+      <div class="search-container-btn">
+        <el-button type="primary" @click="handleSearch">查询</el-button>
+        <el-button @click="handleReset">重置</el-button>
+        <el-button @click="handleExport">导出</el-button>
+      </div>
+    </div>
+    <div class="table-container">
+      <BasicTable
+        ref="basicTableRef"
+        :tableData="tableData"
+        :tableConfig="tableConfig"
+        @update:page-number="handleCurrentPageChange"
+        @update:page-size="handlePageSizeChange"
+      >
+        <template #pictures="scope">
+          <div class="image-viewer">
+            <div v-if="scope.row.pictures">
+              <el-image
+                fit="cover"
+                :src="scope.row.pictures"
+                :preview-src-list="[scope.row.pictures]"
+                class="image-viewer__image"
+                :preview-teleported="true"
+              />
+              <div class="image-viewer__text">1张</div>
+            </div>
+            <div v-else> - </div>
+          </div>
+        </template>
+      </BasicTable>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { QuestionFilled, Search } from '@element-plus/icons-vue';
+  import BasicTable from '@/components/BasicTable.vue';
+  import { downloadByData } from '@/utils/file/download';
+  import { msgConfirm } from '@/utils/element-plus/messageBox';
+  import { getCurrentDateTimeString } from '@/utils/dateUtil';
+  import type { QueryPageRequest } from '@/types/basic-query';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+  import { SECURITY_CONFIDENTIALITY_PERMISSIONS } from '@/views/security-confidentiality/constant';
+  import {
+    getInvasionSnapshotSwitch,
+    updateInvasionSnapshotSwitch,
+    getInvasionSnapshotList,
+    exportInvasionSnapshot,
+    type QueryInvasionSnapshotParams,
+    type QueryInvasionSnapshotRes,
+  } from '@/api/security-confidentiality-position';
+  import {
+    INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+    INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+    INVASION_SNAPSHOT_LIST_TABLE_OPTIONS,
+    INVASION_SNAPSHOT_LIST_TABLE_COLUMNS,
+  } from '../config';
+
+  const { tableConfig, pagination } = useTableConfig(
+    INVASION_SNAPSHOT_LIST_TABLE_COLUMNS,
+    INVASION_SNAPSHOT_LIST_TABLE_OPTIONS,
+  );
+
+  const { permissions } = useUserInfoHook();
+  const monitorManagePermission = ref<boolean>(false);
+
+  const switchStatus = ref(true);
+  const searchKeyword = ref('');
+  const queryTimes = ref(['', '']);
+
+  const invasionSnapshotTableQuery: QueryPageRequest<QueryInvasionSnapshotParams> = {
+    pageNumber: pagination.pageNumber,
+    pageSize: pagination.pageSize,
+    queryParam: {},
+  };
+
+  const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+  const tableData = ref<QueryInvasionSnapshotRes[]>([]);
+
+  const getSwitchStatus = async () => {
+    switchStatus.value = await getInvasionSnapshotSwitch();
+  };
+
+  const updateSwitchStatus = async () => {
+    await updateInvasionSnapshotSwitch({ switchStatus: switchStatus.value });
+  };
+
+  const handleSearch = () => {
+    if (!queryTimes.value) queryTimes.value = ['', ''];
+    getTableData();
+  };
+
+  const handleReset = () => {
+    searchKeyword.value = '';
+    queryTimes.value = ['', ''];
+    getTableData();
+  };
+
+  // 导出
+  const handleExport = () => {
+    msgConfirm('确定导出所查询数据?', '导出', {
+      confirmButtonText: '确定',
+      showCancelButton: true,
+      type: 'warning',
+    })
+      .then(() => {
+        const exportParams: QueryInvasionSnapshotParams = {
+          location: searchKeyword.value || undefined,
+          startTime: queryTimes.value[0] || undefined,
+          endTime: queryTimes.value[1] || undefined,
+        };
+        exportInvasionSnapshot(exportParams).then(async (response) => {
+          if (!response) {
+            throw new Error('下载文件失败');
+          }
+          downloadByData(response, `入侵抓拍记录_${getCurrentDateTimeString()}.xlsx`);
+          ElMessage.success('下载文件成功');
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消导出',
+        });
+      });
+  };
+
+  const handleCurrentPageChange = (pageNumber: number) => {
+    pagination.pageNumber = pageNumber;
+    invasionSnapshotTableQuery.pageNumber = pageNumber;
+    getTableData();
+  };
+
+  const handlePageSizeChange = (pageSize: number) => {
+    pagination.pageSize = pageSize;
+    invasionSnapshotTableQuery.pageSize = pageSize;
+    getTableData();
+  };
+
+  const getTableData = () => {
+    tableConfig.loading = true;
+    invasionSnapshotTableQuery.queryParam = {
+      location: searchKeyword.value || undefined,
+      startTime: queryTimes.value[0] || undefined,
+      endTime: queryTimes.value[1] || undefined,
+    };
+    getInvasionSnapshotList(invasionSnapshotTableQuery).then((res) => {
+      tableData.value = res?.records || [];
+      pagination.total = res?.totalRow || 0;
+    });
+    tableConfig.loading = false;
+  };
+
+  onMounted(() => {
+    monitorManagePermission.value = Boolean(
+      permissions.find(
+        (item: { code: string }) =>
+          item.code === SECURITY_CONFIDENTIALITY_PERMISSIONS.CONFIDENTIALITY_POSITION_MONITOR_MANAGEMENT,
+      ),
+    );
+
+    if (monitorManagePermission.value) {
+      getSwitchStatus();
+    }
+    getTableData();
+    tableConfig.maxHeight = monitorManagePermission.value
+      ? INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_PERMISSION
+      : INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_DEFAULT;
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+
+  .invasion-snapshot-container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .switch-container {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    line-height: 32px;
+    margin-bottom: 20px;
+  }
+
+  .search-container {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+    margin-bottom: 20px;
+
+    .search-location,
+    .search-time {
+      color: rgba(0, 0, 0, 0.85);
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+
+    .search-container-btn {
+      margin-left: auto;
+      display: flex;
+      gap: 8px;
+    }
+  }
+
+  .table-container {
+    width: 100%;
+    height: 100%;
+  }
+
+  .image-viewer {
+    width: 120px;
+    height: 90px;
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin: auto;
+  }
+  .image-viewer__image {
+    display: block;
+    width: 120px;
+    height: 90px;
+    border-radius: 5px;
+  }
+  .image-viewer__text {
+    position: absolute;
+    bottom: 0px;
+    right: 0px;
+    background-color: rgba(0, 0, 0, 0.6);
+    padding: 3px;
+    color: rgba(255, 255, 255, 0.7);
+    font-size: 14px;
+    border-top-left-radius: 5px;
+    border-bottom-right-radius: 5px;
+    pointer-events: none;
+  }
+</style>

+ 21 - 0
src/views/security-confidentiality/confidentiality-position/monitor-records/config/index.ts

@@ -0,0 +1,21 @@
+import {
+  INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  INVASION_SNAPSHOT_LIST_TABLE_OPTIONS,
+  INVASION_SNAPSHOT_LIST_TABLE_COLUMNS,
+  ACCESS_RECORD_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  ACCESS_RECORD_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  ACCESS_RECORD_LIST_TABLE_OPTIONS,
+  ACCESS_RECORD_LIST_TABLE_COLUMNS,
+} from './table';
+
+export {
+  INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  INVASION_SNAPSHOT_LIST_TABLE_OPTIONS,
+  INVASION_SNAPSHOT_LIST_TABLE_COLUMNS,
+  ACCESS_RECORD_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  ACCESS_RECORD_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  ACCESS_RECORD_LIST_TABLE_OPTIONS,
+  ACCESS_RECORD_LIST_TABLE_COLUMNS,
+};

+ 112 - 0
src/views/security-confidentiality/confidentiality-position/monitor-records/config/table.ts

@@ -0,0 +1,112 @@
+/**
+ * 入侵抓拍表格配置
+ */
+import type { TableColumnProps } from '@/types/basic-table';
+
+export const INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_DEFAULT = 'calc(70vh - 120px)';
+export const INVASION_SNAPSHOT_LIST_TABLE_MAX_HEIGHT_PERMISSION = 'calc(70vh - 170px)';
+
+// 基础表格样式配置
+const TABLE_OPTIONS = {
+  emptyText: '暂无数据',
+  loading: true,
+};
+
+// 入侵抓拍表格样式配置
+export const INVASION_SNAPSHOT_LIST_TABLE_OPTIONS = {
+  ...TABLE_OPTIONS,
+};
+
+// 入侵抓拍表格列配置
+export const INVASION_SNAPSHOT_LIST_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '序号',
+    prop: 'index',
+    width: '80px',
+    type: 'index',
+    align: 'center',
+  },
+  {
+    label: '事件',
+    prop: 'event',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '抓拍地点',
+    prop: 'location',
+    align: 'left',
+    minWidth: '200px',
+  },
+  {
+    label: '抓拍时间',
+    prop: 'eventTime',
+    align: 'left',
+    width: '200px',
+  },
+  {
+    label: '抓拍图片',
+    prop: 'pictures',
+    align: 'center',
+    slot: 'pictures',
+    minWidth: '200px',
+  },
+];
+
+/**
+ * 门禁记录表格配置
+ */
+export const ACCESS_RECORD_LIST_TABLE_MAX_HEIGHT_DEFAULT = 'calc(70vh - 120px)';
+export const ACCESS_RECORD_LIST_TABLE_MAX_HEIGHT_PERMISSION = 'calc(70vh - 170px)';
+
+// 门禁记录表格样式配置
+export const ACCESS_RECORD_LIST_TABLE_OPTIONS = {
+  ...TABLE_OPTIONS,
+};
+
+// 门禁记录表格列配置
+export const ACCESS_RECORD_LIST_TABLE_COLUMNS: TableColumnProps[] = [
+  // {
+  //   label: '序号',
+  //   prop: 'index',
+  //   width: '80px',
+  //   type: 'index',
+  //   align: 'center',
+  // },
+  {
+    label: '姓名',
+    prop: 'username',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '卡号',
+    prop: 'cardNum',
+    align: 'center',
+    minWidth: '150px',
+  },
+  {
+    label: '部门',
+    prop: 'deptName',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '事件',
+    prop: 'event',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '进出地点',
+    prop: 'location',
+    align: 'left',
+    minWidth: '200px',
+  },
+  {
+    label: '进出时间',
+    prop: 'eventTime',
+    align: 'left',
+    width: '200px',
+  },
+];

+ 44 - 0
src/views/security-confidentiality/confidentiality-position/monitor-records/constant/index.ts

@@ -0,0 +1,44 @@
+export const MONITOR_RECORDS_SUBPAGES = [
+  {
+    label: '入侵抓拍',
+    value: 'invasion-snapshot',
+  },
+  {
+    label: '门禁记录',
+    value: 'access-records',
+  },
+];
+
+// 查询字段对应:1-姓名,2-卡号,3-部门,4-进出地点
+export enum ACCESS_FIELDTYPE {
+  USERNAME = '1',
+  CARD_NUM = '2',
+  DEPT_NAME = '3',
+  LOCATION = '4',
+}
+
+export const ACCESS_FIELD_CONTENT = {
+  [ACCESS_FIELDTYPE.USERNAME]: '姓名',
+  [ACCESS_FIELDTYPE.CARD_NUM]: '卡号',
+  [ACCESS_FIELDTYPE.DEPT_NAME]: '部门',
+  [ACCESS_FIELDTYPE.LOCATION]: '进出地点',
+};
+
+export const accessQueryOptions = [
+  {
+    label: ACCESS_FIELD_CONTENT[ACCESS_FIELDTYPE.USERNAME],
+    value: ACCESS_FIELDTYPE.USERNAME,
+  },
+  {
+    label: ACCESS_FIELD_CONTENT[ACCESS_FIELDTYPE.CARD_NUM],
+    value: ACCESS_FIELDTYPE.CARD_NUM,
+  },
+  {
+    label: ACCESS_FIELD_CONTENT[ACCESS_FIELDTYPE.DEPT_NAME],
+    value: ACCESS_FIELDTYPE.DEPT_NAME,
+  },
+  {
+    label: ACCESS_FIELD_CONTENT[ACCESS_FIELDTYPE.LOCATION],
+    value: ACCESS_FIELDTYPE.LOCATION,
+  },
+];

+ 338 - 3
src/views/security-confidentiality/confidentiality-position/position-management/PositionManagement.vue

@@ -1,7 +1,342 @@
 <template>
-  <div> overview </div>
+  <div class="safety-platform-container">
+    <div class="safety-platform-container__header">
+      <div class="breadcrumb-title">保密要害部位</div>
+    </div>
+    <div class="safety-platform-container__main">
+      <div class="search-table-container">
+        <header class="disaster-precaution__header">
+          <el-button
+            v-if="confidentialityPositionManagePermission"
+            class="search-table-container--button"
+            type="primary"
+            :icon="Plus"
+            @click="handleAddConfidentialityPosition"
+          >
+            新建要害监控部位
+          </el-button>
+          <BasicSearch
+            :searchConfig="CONFIDENTIALITY_POSITION_LIST_SEARCH_CONFIG"
+            :searchData="searchData"
+            :custom-reset="true"
+            @update:search-data="handleSearch"
+            @custom-reset="handleReset"
+          >
+            <template #confidentialityPosition>
+              <el-input
+                v-model="searchKeyword"
+                :placeholder="`请输入${curSearchTypeLabel}进行搜索`"
+                clearable
+                @input="handleSearch"
+                @keyup.enter="handleSearch"
+                style="width: 380px"
+              >
+                <template #prefix>
+                  <el-icon color="#1777ff"><Search /></el-icon>
+                </template>
+                <template #prepend>
+                  <el-select
+                    v-model="searchSelectedType"
+                    placeholder="选择搜索项"
+                    @change="handleSelectedTypeChange"
+                    style="width: 100px"
+                  >
+                    <el-option
+                      v-for="item in confidentialityPositionQueryOptions"
+                      :key="item.value"
+                      :label="item.label"
+                      :value="item.value"
+                    />
+                  </el-select>
+                </template>
+              </el-input>
+            </template>
+          </BasicSearch>
+        </header>
+        <BasicTable ref="basicTableRef" :tableData="tableData" :tableConfig="tableConfig">
+          <template #cameraName="scope">
+            <div class="camera-name-container">
+              <div v-for="item in scope.row.children" :key="item.id">
+                <ThumbnailClick
+                  :imageUrl="item.pushStreamDTO.imageUrl"
+                  :code="item.code"
+                  position="right"
+                  @mouse-enter="handleMouseEnter"
+                  @mouse-leave="handleMouseLeave"
+                >
+                  <div
+                    :class="{ active: activeCameraCode === item.code && activePositionId === scope.row.id }"
+                    @click="handleCameraClick(scope.row.id, item.code)"
+                    >{{ item.name }}</div
+                  >
+                </ThumbnailClick>
+              </div>
+            </div>
+          </template>
+          <template #action="{ row, index }">
+            <div class="action-container--div">
+              <ActionButton text="上移" @click="handleUpOne(row, index)" v-if="index > 0" />
+              <ActionButton text="下移" @click="handleDownOne(row, index)" v-if="index < tableData.length - 1" />
+              <ActionButton text="编辑" @click="handleEditConfidentialityPosition(row)" />
+              <ActionButton
+                text="删除"
+                :popconfirm="{
+                  title: '是否删除该保密要害部位?',
+                }"
+                @confirm="handleDeleteConfidentialityPosition(row.id)"
+              />
+            </div>
+          </template>
+        </BasicTable>
+      </div>
+    </div>
+    <UpdatePositionMonitorCamera
+      v-if="updatePositionMonitorCameraVisible"
+      @confirm="handleConfirmPositionMonitorCamera"
+      @close="handleClosePositionMonitorCamera"
+    />
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { ref, onMounted, reactive, computed } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { Plus, Search } from '@element-plus/icons-vue';
+  import { storeToRefs } from 'pinia';
+  import BasicSearch from '@/components/BasicSearch.vue';
+  import BasicTable from '@/components/BasicTable.vue';
+  import ActionButton from '@/components/ActionButton.vue';
+  import UpdatePositionMonitorCamera from '@/components/position-monitor-camera-edit/UpdatePositionMonitorCamera.vue';
+  import ThumbnailClick from '@/components/thumbnail/ThumbnailClick.vue';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+  import { usePositionMonitorCameraEdit } from '@/store/modules/usePositionMonitorCameraEdit';
+  import { SECURITY_CONFIDENTIALITY_PERMISSIONS } from '../../constant';
+  import { FIELDTYPE, FIELD_CONTENT, POSITION_TYPE, confidentialityPositionQueryOptions } from './constant';
+  import {
+    CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+    CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+    CONFIDENTIALITY_POSITION_LIST_TABLE_OPTIONS,
+    CONFIDENTIALITY_POSITION_LIST_TABLE_COLUMNS,
+    CONFIDENTIALITY_POSITION_LIST_SEARCH_CONFIG,
+  } from './config';
+  import {
+    GetPositionListParams,
+    PositionMonitorCameraListRes,
+    getConfidentialityPositionList,
+    AddOrUpdatePositionInfoParams,
+    addOrUpdatePositionInfo,
+    updateCameraGroupOrder,
+  } from '@/api/security-confidentiality-position';
+  import { deleteCameraGroupApi } from '@/api/nine-square-grid';
 
-<style scoped></style>
+  const { tableConfig } = useTableConfig(
+    CONFIDENTIALITY_POSITION_LIST_TABLE_COLUMNS,
+    CONFIDENTIALITY_POSITION_LIST_TABLE_OPTIONS,
+  );
+
+  const { permissions } = useUserInfoHook();
+  const confidentialityPositionManagePermission = ref<boolean>(false);
+
+  const positionMonitorCameraEdit = usePositionMonitorCameraEdit();
+  const {
+    titleOfUpdatePositionMonitorCamera,
+    dataOfPosition,
+    idOfPosition,
+    nameOfPosition,
+    selectedCameraIdsOfPosition,
+  } = storeToRefs(positionMonitorCameraEdit);
+  const { initDataOfPosition, resetPositionMonitorCameraEdit } = positionMonitorCameraEdit;
+
+  const searchData = reactive<GetPositionListParams>({
+    groupName: '',
+    cameraName: '',
+  });
+
+  const searchSelectedType = ref(FIELDTYPE.POSITION_NAME);
+  const searchKeyword = ref('');
+  const curSearchTypeLabel = computed(() => {
+    const option = confidentialityPositionQueryOptions.find((item) => item.value === searchSelectedType.value);
+    return option ? option.label : FIELD_CONTENT[searchSelectedType.value];
+  });
+
+  const tableData = ref<PositionMonitorCameraListRes[]>([]);
+
+  const updatePositionMonitorCameraVisible = ref(false);
+
+  const handleAddConfidentialityPosition = () => {
+    titleOfUpdatePositionMonitorCamera.value = '新建要害监控部位';
+    dataOfPosition.value = undefined;
+    initDataOfPosition();
+    updatePositionMonitorCameraVisible.value = true;
+  };
+
+  const handleSearch = () => {
+    if (searchSelectedType.value === FIELDTYPE.POSITION_NAME) searchData.groupName = searchKeyword.value;
+    else searchData.cameraName = searchKeyword.value;
+    getTableData();
+  };
+
+  const handleReset = () => {
+    searchKeyword.value = '';
+    searchSelectedType.value = FIELDTYPE.POSITION_NAME;
+    searchData.cameraName = '';
+    searchData.groupName = '';
+    getTableData();
+  };
+
+  const handleSelectedTypeChange = () => {
+    searchKeyword.value = '';
+  };
+
+  const handleUpOne = (currentRow: PositionMonitorCameraListRes, currentIndex: number) => {
+    // 获取上一行的数据
+    const prevRow = tableData.value[currentIndex - 1];
+    // 构造交换数据数组
+    const swapData = [
+      { id: prevRow.id, orderNum: currentRow.orderNum }, // 上一行使用当前行的orderNum
+      { id: currentRow.id, orderNum: prevRow.orderNum }, // 当前行使用上一行的orderNum
+    ];
+
+    updateCameraGroupOrder(swapData)
+      .then(() => {
+        ElMessage.success('上移成功');
+        getTableData();
+      })
+      .catch(() => {
+        ElMessage.error('上移失败');
+      });
+  };
+
+  const handleDownOne = (currentRow: PositionMonitorCameraListRes, currentIndex: number) => {
+    // 获取下一行的数据
+    const nextRow = tableData.value[currentIndex + 1];
+    // 构造交换数据数组
+    const swapData = [
+      { id: nextRow.id, orderNum: currentRow.orderNum }, // 下一行使用当前行的orderNum
+      { id: currentRow.id, orderNum: nextRow.orderNum }, // 当前行使用下一行的orderNum
+    ];
+
+    updateCameraGroupOrder(swapData)
+      .then(() => {
+        ElMessage.success('下移成功');
+        getTableData();
+      })
+      .catch(() => {
+        ElMessage.error('下移失败');
+      });
+  };
+
+  const handleEditConfidentialityPosition = (row: PositionMonitorCameraListRes) => {
+    titleOfUpdatePositionMonitorCamera.value = '编辑要害监控部位';
+    dataOfPosition.value = row;
+    initDataOfPosition();
+    updatePositionMonitorCameraVisible.value = true;
+  };
+
+  const handleDeleteConfidentialityPosition = (id: number) => {
+    deleteCameraGroupApi(id).then(() => {
+      ElMessage.success('要害监控部位删除成功');
+      getTableData();
+    });
+  };
+
+  const handleConfirmPositionMonitorCamera = () => {
+    const params: AddOrUpdatePositionInfoParams = {
+      id: idOfPosition.value ?? undefined,
+      groupName: nameOfPosition.value,
+      type: POSITION_TYPE.CONFIDENTIALITY_POSITION,
+      cameraIdList: selectedCameraIdsOfPosition.value.map((item) => item.id),
+    };
+    addOrUpdatePositionInfo(params).then(() => {
+      ElMessage.success(idOfPosition.value ? '要害监控部位编辑成功' : '要害监控部位新建成功');
+      getTableData();
+    });
+    updatePositionMonitorCameraVisible.value = false;
+    resetPositionMonitorCameraEdit();
+  };
+
+  const handleClosePositionMonitorCamera = () => {
+    ElMessage.info('取消操作');
+    updatePositionMonitorCameraVisible.value = false;
+    resetPositionMonitorCameraEdit();
+  };
+
+  const getTableData = () => {
+    tableConfig.loading = true;
+    getConfidentialityPositionList(searchData).then((res) => {
+      tableData.value = res;
+    });
+    tableConfig.loading = false;
+  };
+
+  // 当前选中的保密要害部位id和相机code
+  const activePositionId = ref(0);
+  const activeCameraCode = ref('');
+  const handleCameraClick = (id: number, code: string) => {
+    activePositionId.value = id;
+    activeCameraCode.value = code;
+  };
+  const handleMouseEnter = (code: string) => {
+    activeCameraCode.value = code;
+  };
+  const handleMouseLeave = () => {
+    activeCameraCode.value = '';
+  };
+
+  // 动态生成表格列配置
+  const getTableColumns = () => {
+    if (confidentialityPositionManagePermission.value) {
+      return CONFIDENTIALITY_POSITION_LIST_TABLE_COLUMNS;
+    } else {
+      // 过滤掉操作列
+      return CONFIDENTIALITY_POSITION_LIST_TABLE_COLUMNS.filter(
+        (column) => column.prop !== 'action' && column.type !== 'selection',
+      );
+    }
+  };
+
+  onMounted(() => {
+    getTableData();
+    confidentialityPositionManagePermission.value = Boolean(
+      permissions.find(
+        (item: { code: string }) =>
+          item.code === SECURITY_CONFIDENTIALITY_PERMISSIONS.CONFIDENTIALITY_POSITION_MANAGEMENT,
+      ),
+    );
+    tableConfig.maxHeight = confidentialityPositionManagePermission.value
+      ? CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_PERMISSION
+      : CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_DEFAULT;
+    tableConfig.columns = getTableColumns();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+
+  .action-container--div {
+    justify-content: flex-start;
+  }
+
+  .camera-name-container {
+    display: flex;
+    gap: 10px;
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  .camera-name-container .thumb-nail {
+    color: #1777ff;
+    cursor: pointer;
+  }
+
+  .camera-name-container .thumb-nail:hover {
+    color: #94c1ff;
+  }
+
+  .camera-name-container .active {
+    color: #003f97;
+  }
+</style>

+ 15 - 0
src/views/security-confidentiality/confidentiality-position/position-management/config/index.ts

@@ -0,0 +1,15 @@
+import {
+  CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  CONFIDENTIALITY_POSITION_LIST_TABLE_OPTIONS,
+  CONFIDENTIALITY_POSITION_LIST_TABLE_COLUMNS,
+} from './table';
+import { CONFIDENTIALITY_POSITION_LIST_SEARCH_CONFIG } from './search';
+
+export {
+  CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  CONFIDENTIALITY_POSITION_LIST_TABLE_OPTIONS,
+  CONFIDENTIALITY_POSITION_LIST_TABLE_COLUMNS,
+  CONFIDENTIALITY_POSITION_LIST_SEARCH_CONFIG,
+};

+ 9 - 0
src/views/security-confidentiality/confidentiality-position/position-management/config/search.ts

@@ -0,0 +1,9 @@
+import type { SearchConfig } from '@/types/basic-search';
+
+export const CONFIDENTIALITY_POSITION_LIST_SEARCH_CONFIG: SearchConfig[] = [
+  {
+    label: '',
+    prop: 'confidentialityPosition',
+    slot: 'confidentialityPosition',
+  },
+];

+ 56 - 0
src/views/security-confidentiality/confidentiality-position/position-management/config/table.ts

@@ -0,0 +1,56 @@
+/**
+ * 治安重点部位表格配置
+ */
+import type { TableColumnProps } from '@/types/basic-table';
+
+export const CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_DEFAULT = 'calc(70vh - 30px)';
+export const CONFIDENTIALITY_POSITION_LIST_TABLE_MAX_HEIGHT_PERMISSION = 'calc(70vh - 80px)';
+
+// 基础表格样式配置
+const TABLE_OPTIONS = {
+  emptyText: '暂无数据',
+  loading: true,
+};
+
+// 应急处置表格样式配置
+export const CONFIDENTIALITY_POSITION_LIST_TABLE_OPTIONS = {
+  ...TABLE_OPTIONS,
+};
+
+// 应急处置表格列配置
+export const CONFIDENTIALITY_POSITION_LIST_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '序号',
+    prop: 'index',
+    width: '80px',
+    type: 'index',
+    align: 'center',
+  },
+  {
+    label: '重点部位名称',
+    prop: 'groupName',
+    // align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '关联相机',
+    prop: 'cameraName',
+    slot: 'cameraName',
+    // align: 'center',
+    minWidth: '180px',
+  },
+  {
+    label: '创建时间',
+    prop: 'updatedAt',
+    // align: 'center',
+    width: '200px',
+  },
+  {
+    prop: 'action',
+    label: '操作',
+    // align: 'center',
+    slot: 'action',
+    fixed: 'right',
+    width: '230px',
+  },
+];

+ 31 - 0
src/views/security-confidentiality/confidentiality-position/position-management/constant/index.ts

@@ -0,0 +1,31 @@
+/**
+ * @description: 治安重点部位/保密要害部位 常量对应字段
+ */
+
+// 查询字段对应:1-部位名称,2-相机名称
+export enum FIELDTYPE {
+  POSITION_NAME = 'groupName',
+  CAMERA_NAME = 'cameraName',
+}
+
+export const FIELD_CONTENT = {
+  [FIELDTYPE.POSITION_NAME]: '部位名称',
+  [FIELDTYPE.CAMERA_NAME]: '相机名称',
+};
+
+export const confidentialityPositionQueryOptions = [
+  {
+    label: FIELD_CONTENT[FIELDTYPE.POSITION_NAME],
+    value: FIELDTYPE.POSITION_NAME,
+  },
+  {
+    label: FIELD_CONTENT[FIELDTYPE.CAMERA_NAME],
+    value: FIELDTYPE.CAMERA_NAME,
+  },
+];
+
+// 新增或编辑治安重点部位/保密要害部位type:3-治安重点部位,4-保密要害部位
+export enum POSITION_TYPE {
+  SECURITY_POSITION = 3,
+  CONFIDENTIALITY_POSITION = 4,
+}

+ 123 - 39
src/views/security-confidentiality/security-position/SecurityPosition.vue

@@ -13,12 +13,14 @@
             :icon="Plus"
             @click="handleAddSecurityPosition"
           >
-            新建治安重点部位
+            新建重点监控部位
           </el-button>
           <BasicSearch
             :searchConfig="SECURITY_POSITION_LIST_SEARCH_CONFIG"
             :searchData="searchData"
+            :custom-reset="true"
             @update:search-data="handleSearch"
+            @custom-reset="handleReset"
           >
             <template #securityPosition>
               <el-input
@@ -55,26 +57,33 @@
           <template #cameraName="scope">
             <div class="camera-name-container">
               <div v-for="item in scope.row.children" :key="item.id">
-                {{ item.name }}
+                <ThumbnailClick
+                  :imageUrl="item.pushStreamDTO.imageUrl"
+                  :code="item.code"
+                  position="right"
+                  @mouse-enter="handleMouseEnter"
+                  @mouse-leave="handleMouseLeave"
+                >
+                  <div
+                    :class="{ active: activeCameraCode === item.code && activePositionId === scope.row.id }"
+                    @click="handleCameraClick(scope.row.id, item.code)"
+                    >{{ item.name }}</div
+                  >
+                </ThumbnailClick>
               </div>
             </div>
           </template>
-          <template #action="scope">
+          <template #action="{ row, index }">
             <div class="action-container--div">
-              <ActionButton text="上移" @click="handleUpOne(scope.row.id, scope.row.orderNum)" />
-              <ActionButton text="下移" @click="handleDownOne(scope.row.id, scope.row.orderNum)" />
-              <ActionButton
-                text="编辑"
-                v-if="securityPositionManagePermission"
-                @click="handleEditSecurityPosition(scope.row)"
-              />
+              <ActionButton text="上移" @click="handleUpOne(row, index)" v-if="index > 0" />
+              <ActionButton text="下移" @click="handleDownOne(row, index)" v-if="index < tableData.length - 1" />
+              <ActionButton text="编辑" @click="handleEditSecurityPosition(row)" />
               <ActionButton
-                v-if="securityPositionManagePermission"
                 text="删除"
                 :popconfirm="{
                   title: '是否删除该治安重点部位?',
                 }"
-                @confirm="handleDeleteSecurityPosition(scope.row.id)"
+                @confirm="handleDeleteSecurityPosition(row.id)"
               />
             </div>
           </template>
@@ -98,6 +107,7 @@
   import BasicTable from '@/components/BasicTable.vue';
   import ActionButton from '@/components/ActionButton.vue';
   import UpdatePositionMonitorCamera from '@/components/position-monitor-camera-edit/UpdatePositionMonitorCamera.vue';
+  import ThumbnailClick from '@/components/thumbnail/ThumbnailClick.vue';
   import useTableConfig from '@/hooks/useTableConfigHook';
   import { useUserInfoHook } from '@/hooks/useUserInfoHook';
   import { usePositionMonitorCameraEdit } from '@/store/modules/usePositionMonitorCameraEdit';
@@ -133,7 +143,7 @@
     nameOfPosition,
     selectedCameraIdsOfPosition,
   } = storeToRefs(positionMonitorCameraEdit);
-  const { initDataOfPosition } = positionMonitorCameraEdit;
+  const { initDataOfPosition, resetPositionMonitorCameraEdit } = positionMonitorCameraEdit;
 
   const searchData = reactive<GetPositionListParams>({
     groupName: '',
@@ -152,46 +162,69 @@
   const updatePositionMonitorCameraVisible = ref(false);
 
   const handleAddSecurityPosition = () => {
-    titleOfUpdatePositionMonitorCamera.value = '编辑重点监控部位';
+    titleOfUpdatePositionMonitorCamera.value = '添加重点监控部位';
     dataOfPosition.value = undefined;
+    initDataOfPosition();
     updatePositionMonitorCameraVisible.value = true;
   };
 
   const handleSearch = () => {
-    console.log('查询治安重点部位');
     if (searchSelectedType.value === FIELDTYPE.POSITION_NAME) searchData.groupName = searchKeyword.value;
     else searchData.cameraName = searchKeyword.value;
     getTableData();
   };
 
+  const handleReset = () => {
+    searchKeyword.value = '';
+    searchSelectedType.value = FIELDTYPE.POSITION_NAME;
+    searchData.cameraName = '';
+    searchData.groupName = '';
+    getTableData();
+  };
+
   const handleSelectedTypeChange = () => {
     searchKeyword.value = '';
   };
 
-  const handleUpOne = (id: number, orderNum: number) => {
-    console.log('上移治安重点部位', id);
-    updateCameraGroupOrder({
-      id: id,
-      orderNum: orderNum - 1,
-    }).then((res) => {
-      console.log('上移治安重点部位', res);
-      getTableData();
-    });
+  const handleUpOne = (currentRow: PositionMonitorCameraListRes, currentIndex: number) => {
+    // 获取上一行的数据
+    const prevRow = tableData.value[currentIndex - 1];
+    // 构造交换数据数组
+    const swapData = [
+      { id: prevRow.id, orderNum: currentRow.orderNum }, // 上一行使用当前行的orderNum
+      { id: currentRow.id, orderNum: prevRow.orderNum }, // 当前行使用上一行的orderNum
+    ];
+
+    updateCameraGroupOrder(swapData)
+      .then(() => {
+        ElMessage.success('上移成功');
+        getTableData();
+      })
+      .catch(() => {
+        ElMessage.error('上移失败');
+      });
   };
 
-  const handleDownOne = (id: number, orderNum: number) => {
-    console.log('下移治安重点部位', id);
-    updateCameraGroupOrder({
-      id: id,
-      orderNum: orderNum + 1,
-    }).then((res) => {
-      console.log('下移治安重点部位', res);
-      getTableData();
-    });
+  const handleDownOne = (currentRow: PositionMonitorCameraListRes, currentIndex: number) => {
+    // 获取下一行的数据
+    const nextRow = tableData.value[currentIndex + 1];
+    // 构造交换数据数组
+    const swapData = [
+      { id: nextRow.id, orderNum: currentRow.orderNum }, // 下一行使用当前行的orderNum
+      { id: currentRow.id, orderNum: nextRow.orderNum }, // 当前行使用下一行的orderNum
+    ];
+
+    updateCameraGroupOrder(swapData)
+      .then(() => {
+        ElMessage.success('下移成功');
+        getTableData();
+      })
+      .catch(() => {
+        ElMessage.error('下移失败');
+      });
   };
 
   const handleEditSecurityPosition = (row: PositionMonitorCameraListRes) => {
-    console.log('编辑治安重点部位', row);
     titleOfUpdatePositionMonitorCamera.value = '编辑重点监控部位';
     dataOfPosition.value = row;
     initDataOfPosition();
@@ -199,9 +232,8 @@
   };
 
   const handleDeleteSecurityPosition = (id: number) => {
-    console.log('删除治安重点部位', id);
-    deleteCameraGroupApi(id).then((res) => {
-      console.log('删除治安重点部位', res);
+    deleteCameraGroupApi(id).then(() => {
+      ElMessage.success('治安重点部位删除成功');
       getTableData();
     });
   };
@@ -218,23 +250,51 @@
       getTableData();
     });
     updatePositionMonitorCameraVisible.value = false;
+    resetPositionMonitorCameraEdit();
   };
 
   const handleClosePositionMonitorCamera = () => {
     ElMessage.info('取消操作');
     updatePositionMonitorCameraVisible.value = false;
+    resetPositionMonitorCameraEdit();
   };
 
   const getTableData = () => {
     tableConfig.loading = true;
     getSecurityPositionList(searchData).then((res) => {
-      console.log('获取治安重点部位列表', res);
       tableData.value = res;
     });
     tableConfig.loading = false;
   };
 
+  // 当前选中的治安重点部位id和相机code
+  const activePositionId = ref(0);
+  const activeCameraCode = ref('');
+  const handleCameraClick = (id: number, code: string) => {
+    activePositionId.value = id;
+    activeCameraCode.value = code;
+  };
+  const handleMouseEnter = (code: string) => {
+    activeCameraCode.value = code;
+  };
+  const handleMouseLeave = () => {
+    activeCameraCode.value = '';
+  };
+
+  // 动态生成表格列配置
+  const getTableColumns = () => {
+    if (securityPositionManagePermission.value) {
+      return SECURITY_POSITION_LIST_TABLE_COLUMNS;
+    } else {
+      // 过滤掉操作列
+      return SECURITY_POSITION_LIST_TABLE_COLUMNS.filter(
+        (column) => column.prop !== 'action' && column.type !== 'selection',
+      );
+    }
+  };
+
   onMounted(() => {
+    getTableData();
     securityPositionManagePermission.value = Boolean(
       permissions.find(
         (item: { code: string }) => item.code === SECURITY_CONFIDENTIALITY_PERMISSIONS.SECURITY_POSITION_MANAGEMENT,
@@ -243,12 +303,36 @@
     tableConfig.maxHeight = securityPositionManagePermission.value
       ? SECURITY_POSITION_LIST_TABLE_MAX_HEIGHT_PERMISSION
       : SECURITY_POSITION_LIST_TABLE_MAX_HEIGHT_DEFAULT;
+    tableConfig.columns = getTableColumns();
   });
-  getTableData();
 </script>
 
 <style scoped lang="scss">
   @use '@/styles/page-main-layout.scss' as *;
   @use '@/styles/page-details-layout.scss' as *;
   @use '@/styles/basic-table-action.scss' as *;
+
+  .action-container--div {
+    justify-content: flex-start;
+  }
+
+  .camera-name-container {
+    display: flex;
+    gap: 10px;
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  .camera-name-container .thumb-nail {
+    color: #1777ff;
+    cursor: pointer;
+  }
+
+  .camera-name-container .thumb-nail:hover {
+    color: #94c1ff;
+  }
+
+  .camera-name-container .active {
+    color: #003f97;
+  }
 </style>

+ 9 - 9
src/views/security-confidentiality/security-position/config/table.ts

@@ -3,8 +3,8 @@
  */
 import type { TableColumnProps } from '@/types/basic-table';
 
-export const SECURITY_POSITION_LIST_TABLE_MAX_HEIGHT_DEFAULT = 'calc(100vh - 80px)';
-export const SECURITY_POSITION_LIST_TABLE_MAX_HEIGHT_PERMISSION = 'calc(100vh - 130px)';
+export const SECURITY_POSITION_LIST_TABLE_MAX_HEIGHT_DEFAULT = 'calc(70vh - 30px)';
+export const SECURITY_POSITION_LIST_TABLE_MAX_HEIGHT_PERMISSION = 'calc(70vh - 80px)';
 
 // 基础表格样式配置
 const TABLE_OPTIONS = {
@@ -29,28 +29,28 @@ export const SECURITY_POSITION_LIST_TABLE_COLUMNS: TableColumnProps[] = [
   {
     label: '重点部位名称',
     prop: 'groupName',
-    align: 'center',
-    minWidth: '180px',
+    // align: 'center',
+    minWidth: '120px',
   },
   {
     label: '关联相机',
     prop: 'cameraName',
     slot: 'cameraName',
-    align: 'center',
+    // align: 'center',
     minWidth: '180px',
   },
   {
     label: '创建时间',
     prop: 'updatedAt',
-    align: 'center',
-    width: '250px',
+    // align: 'center',
+    width: '200px',
   },
   {
     prop: 'action',
     label: '操作',
-    align: 'center',
+    // align: 'center',
     slot: 'action',
     fixed: 'right',
-    width: '400px',
+    width: '230px',
   },
 ];

+ 1 - 0
utils/devProxy/staff/proxy.ts

@@ -8,6 +8,7 @@ const proxyStaff: PROXY_TYPE = {
   // serverHost: 'http://192.168.22.146:8802/',
   loginHost: 'http://192.168.13.68:7200/login/#/',
   fileUploadHost: 'http://192.168.13.102:9000/',
+  violation_src: 'http://192.168.13.102:62/violation_src',
   push_stream_host: 'http://192.168.13.68:7000/skyeye-admin/push_stream_host/',
 };
 

+ 2 - 0
utils/devProxy/types.ts

@@ -8,4 +8,6 @@ export interface PROXY_TYPE {
   fileUploadHost: string;
   /** 流媒体服务 */
   push_stream_host: string;
+  /** 报警图片或视频地址 */
+  violation_src: string;
 }

+ 1 - 0
utils/devProxy/utils.ts

@@ -9,6 +9,7 @@ export const createProxyList = (devProxy: PROXY_TYPE) =>
     ['/safety_api/', devProxy.serverHost],
     ['/skyeye-file-upload/', devProxy.fileUploadHost],
     ['/push_stream_host/', devProxy.push_stream_host],
+    ['/violation_src/', devProxy.violation_src],
   ] as ProxyList;
 
 export const createConfig = (appConfigPath: string) => {