Explorar o código

Merge branch 'dev-bxy' into 'dev'

feat: 修改部位管理添加组件必填项逻辑;完成监控调阅记录页面

See merge request product-group-fe/sfy-safety-group/sfy-safety!220
毕欣怡 hai 7 meses
pai
achega
46dabd29f6

+ 91 - 0
src/api/security-confidentiality-surveillance/index.ts

@@ -0,0 +1,91 @@
+/**
+ * @description: 监控调阅记录
+ */
+import { http } from '@/utils/http/axios';
+import type { QueryPageRequest, QueryPageResponse } from '@/types/basic-query';
+
+/**
+ * @description: 分页查询监控调阅记录
+ */
+export interface SurveillanceListQuery {
+  fieldType?: string; // 字段类型:1-姓名,2-工号,3-部门,4-调阅位置
+  fieldContent?: string;
+  isCopy?: number; // 是否拷贝:0-否,1-是
+  accessStatus?: number; // 调阅状态:0-待调阅,1-已调阅
+  startTime?: string; // 开始时间
+  endTime?: string; // 结束时间
+}
+export interface SurveillanceInfoStruct {
+  id?: number; // 自增主键
+  staffNo?: string; // 工号
+  userName?: string; // 姓名
+  deptId?: number; // 部门id
+  deptName?: string; // 部门名称
+  accessLocation?: string; // 调阅位置
+  accessStartTime?: string; // 调阅开始时间
+  accessEndTime?: string; // 调阅结束时间
+  isCopy?: number; // 是否拷贝: 0-否,1-是
+  accessStatus?: number; // 调阅状态: 0-待调阅,1-已调阅
+  approvalFormUrl?: string; // 审批单地址
+  createdById?: number; // 创建人id
+  createdByName?: string; // 创建人姓名
+  createdByStaffNo?: string; // 创建人工号
+  createdAt?: string; // 创建时间
+  updatedAt?: string; // 更新时间
+  isDeleted?: number; // 0-未删除,大于0(时间戳)-已删除
+}
+export const getSurveillanceInfoList = (params: QueryPageRequest<SurveillanceListQuery>) => {
+  return http.request<QueryPageResponse<SurveillanceInfoStruct>>({
+    url: '/MonitorAccessRecord/queryMonitorAccessRecordPage',
+    method: 'post',
+    params,
+  });
+};
+
+/**
+ * @description: 添加监控调阅记录
+ */
+export const addSurveillanceInfo = (params: SurveillanceInfoStruct) => {
+  return http.request({
+    url: '/MonitorAccessRecord/saveMonitorAccessRecord',
+    method: 'post',
+    params,
+  });
+};
+
+/**
+ * @description: 更新监控调阅记录
+ */
+export const updateSurveillanceInfo = (params: SurveillanceInfoStruct) => {
+  return http.request({
+    url: '/MonitorAccessRecord/updateMonitorAccessRecord',
+    method: 'put',
+    params,
+  });
+};
+
+/**
+ * @description: 删除监控调阅记录(单个)
+ */
+export const deleteSurveillanceInfo = (params: { recordId: number }) => {
+  return http.request({
+    url: '/MonitorAccessRecord/deleteMonitorAccessRecord',
+    method: 'delete',
+    params,
+  });
+};
+
+/**
+ * @description: 导出监控调阅记录
+ */
+export function exportSurveillanceInfo(params: SurveillanceListQuery) {
+  return http.request(
+    {
+      url: '/MonitorAccessRecord/exportMonitorAccessRecord',
+      method: 'post',
+      responseType: 'blob',
+      params,
+    },
+    { isTransformResponse: false },
+  );
+}

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

@@ -103,7 +103,7 @@
   import { nextTick, onMounted, ref, watch } from 'vue';
   import { storeToRefs } from 'pinia';
   import type { TreeInstance } from 'element-plus';
-  import { ElDialog, ElInput, ElTree, ElTag, ElButton } from 'element-plus';
+  import { ElDialog, ElInput, ElTree, ElTag, ElButton, ElMessage } from 'element-plus';
   import { VideoCamera, WarningFilled, Search } from '@element-plus/icons-vue';
   import Thumbnail from '@/components/thumbnail/Thumbnail.vue';
   import { usePositionMonitorCameraEdit } from '@/store/modules/usePositionMonitorCameraEdit';
@@ -181,6 +181,18 @@
 
   // 确认按钮点击事件
   function handleConfirm() {
+    // 校验部位名称
+    if (!positionName.value || positionName.value.trim() === '') {
+      ElMessage.error('请输入部位名称');
+      return;
+    }
+
+    // 校验关联相机
+    if (selectedItems.value.length === 0) {
+      ElMessage.error('请至少选择一个关联相机');
+      return;
+    }
+
     selectedCameraIdsOfPosition.value = selectedItems.value;
     nameOfPosition.value = positionName.value;
     emits('confirm');

+ 3 - 41
src/views/security-confidentiality/confidentiality-position/monitor-records/components/InvasionSnapshot.vue

@@ -50,19 +50,7 @@
         @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>
+          <ImageViewer :file-list="scope.row.pictures" />
         </template>
       </BasicTable>
     </div>
@@ -81,6 +69,8 @@
   import useTableConfig from '@/hooks/useTableConfigHook';
   import { useUserInfoHook } from '@/hooks/useUserInfoHook';
   import { SECURITY_CONFIDENTIALITY_PERMISSIONS } from '@/views/security-confidentiality/constant';
+  import ImageViewer from '@/views/traffic/violation/act/components/ImageViewer.vue';
+
   import {
     getInvasionSnapshotSwitch,
     updateInvasionSnapshotSwitch,
@@ -255,32 +245,4 @@
     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>

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

@@ -30,7 +30,7 @@ export const INVASION_SNAPSHOT_LIST_TABLE_COLUMNS: TableColumnProps[] = [
     label: '事件',
     prop: 'event',
     align: 'center',
-    minWidth: '120px',
+    minWidth: '100px',
   },
   {
     label: '抓拍地点',
@@ -49,7 +49,7 @@ export const INVASION_SNAPSHOT_LIST_TABLE_COLUMNS: TableColumnProps[] = [
     prop: 'pictures',
     align: 'center',
     slot: 'pictures',
-    minWidth: '200px',
+    minWidth: '120px',
   },
 ];
 

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

@@ -27,7 +27,7 @@ export const CONFIDENTIALITY_POSITION_LIST_TABLE_COLUMNS: TableColumnProps[] = [
     align: 'center',
   },
   {
-    label: '重点部位名称',
+    label: '要害部位名称',
     prop: 'groupName',
     // align: 'center',
     minWidth: '120px',

+ 369 - 3
src/views/security-confidentiality/surveillance-management/SurveillanceManagement.vue

@@ -1,7 +1,373 @@
 <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="surveillanceManagePermission"
+            class="search-table-container--button"
+            type="primary"
+            :icon="Plus"
+            @click="handleAddSurveillanceInfo"
+          >
+            添加监控调阅记录
+          </el-button>
+          <div class="search-container">
+            <el-input
+              v-model="searchKeyword"
+              :placeholder="`请输入${curSearchTypeLabel}进行搜索`"
+              clearable
+              @input="handleSearch"
+              @clear="handleClear"
+              @keyup.enter="handleSearch"
+              style="width: 340px"
+            >
+              <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 surveillanceQueryOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </template>
+            </el-input>
+            <div class="search-item">
+              <label>是否拷贝:</label>
+              <el-select v-model="searchIsCopy" placeholder="请选择" clearable style="width: 120px">
+                <el-option v-for="item in COPY_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+            </div>
+            <div class="search-item">
+              <label>调阅状态:</label>
+              <el-select v-model="searchAccessStatus" placeholder="请选择" clearable style="width: 120px">
+                <el-option
+                  v-for="item in ACCESS_STATUS_OPTIONS"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </div>
+            <div class="search-item">
+              <label>调阅时段:</label>
+              <el-date-picker
+                v-model="searchTimeRange"
+                type="datetimerange"
+                range-separator="~"
+                start-placeholder="开始时间"
+                end-placeholder="结束时间"
+                format="YYYY-MM-DD HH:mm:ss"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                style="width: 300px"
+              />
+            </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>
+        </header>
+        <div class="table-container">
+          <BasicTable
+            ref="basicTableRef"
+            :tableData="tableData"
+            :tableConfig="tableConfig"
+            @update:page-number="handleCurrentPageChange"
+            @update:page-size="handlePageSizeChange"
+          >
+            <template #accessTimePeriod="scope">
+              <span v-if="scope.row.accessStartTime && scope.row.accessEndTime">
+                {{ scope.row.accessStartTime }} ~ {{ scope.row.accessEndTime }}
+              </span>
+              <span v-else>-</span>
+            </template>
+            <template #isCopy="scope">
+              <el-tag :type="scope.row.isCopy === 1 ? 'success' : 'info'">
+                {{ scope.row.isCopy === 1 ? '是' : '否' }}
+              </el-tag>
+            </template>
+            <template #approvalForm="scope">
+              <div class="image-viewer">
+                <div v-if="scope.row.approvalFormUrl">
+                  <el-image
+                    fit="cover"
+                    :src="scope.row.approvalFormUrl"
+                    :preview-src-list="[scope.row.approvalFormUrl]"
+                    class="image-viewer__image"
+                    :preview-teleported="true"
+                  />
+                </div>
+                <div v-else>-</div>
+              </div>
+            </template>
+            <template #accessStatus="scope">
+              <el-tag :type="scope.row.accessStatus === 1 ? 'success' : 'warning'">
+                {{ scope.row.accessStatus === 1 ? '已调阅' : '待调阅' }}
+              </el-tag>
+            </template>
+            <template #action="scope">
+              <div class="action-container--div">
+                <ActionButton text="编辑" @click="handleEditSurveillanceInfo(scope.row)" />
+                <ActionButton
+                  text="删除"
+                  :popconfirm="{
+                    title: '是否删除该监控调阅记录?',
+                  }"
+                  @confirm="handleDeleteSurveillanceInfo(scope.row)"
+                />
+              </div>
+            </template>
+          </BasicTable>
+        </div>
+      </div>
+    </div>
+  </div>
+  <ManageDrawer v-if="drawerVisible" :type="drawerType" :initial-data="initialData" @close="handleCloseDrawer" />
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { computed, onMounted, ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { Plus, Search } from '@element-plus/icons-vue';
+  import BasicTable from '@/components/BasicTable.vue';
+  import ActionButton from '@/components/ActionButton.vue';
+  import ManageDrawer from './components/ManageDrawer.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 {
+    SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+    SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+    SURVEILLANCE_LIST_TABLE_OPTIONS,
+    SURVEILLANCE_LIST_TABLE_COLUMNS,
+  } from './config';
+  import { FIELDTYPE, FIELD_CONTENT, surveillanceQueryOptions, COPY_OPTIONS, ACCESS_STATUS_OPTIONS } from './constant';
+  import {
+    SurveillanceListQuery,
+    SurveillanceInfoStruct,
+    getSurveillanceInfoList,
+    deleteSurveillanceInfo,
+    exportSurveillanceInfo,
+  } from '@/api/security-confidentiality-surveillance';
 
-<style scoped></style>
+  const { tableConfig, pagination } = useTableConfig(SURVEILLANCE_LIST_TABLE_COLUMNS, SURVEILLANCE_LIST_TABLE_OPTIONS);
+
+  const surveillanceManagePermission = ref<boolean>(false);
+
+  const searchSelectedType = ref(FIELDTYPE.NAME);
+  const searchKeyword = ref('');
+  const searchIsCopy = ref<number | undefined>(undefined);
+  const searchAccessStatus = ref<number | undefined>(undefined);
+  const searchTimeRange = ref<[string, string] | undefined>(undefined);
+
+  const curSearchTypeLabel = computed(() => {
+    const option = surveillanceQueryOptions.find((item) => item.value === searchSelectedType.value);
+    return option ? option.label : FIELD_CONTENT[searchSelectedType.value];
+  });
+
+  const surveillanceTableQuery: QueryPageRequest<SurveillanceListQuery> = {
+    pageNumber: pagination.pageNumber,
+    pageSize: pagination.pageSize,
+    queryParam: {},
+  };
+
+  const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+  const tableData = ref<SurveillanceInfoStruct[]>([]);
+
+  // add or edit
+  const drawerVisible = ref(false);
+  const drawerType = ref('');
+  const initialData = ref<SurveillanceInfoStruct>({});
+
+  const handleSelectedTypeChange = () => {
+    searchKeyword.value = '';
+  };
+
+  const handleSearch = () => {
+    if (!searchKeyword.value.trim()) {
+      handleClear();
+      return;
+    }
+    getTableData();
+  };
+
+  const handleClear = () => {
+    searchKeyword.value = '';
+    getTableData();
+  };
+
+  const handleReset = () => {
+    searchSelectedType.value = FIELDTYPE.NAME;
+    searchKeyword.value = '';
+    searchIsCopy.value = undefined;
+    searchAccessStatus.value = undefined;
+    searchTimeRange.value = undefined;
+    getTableData();
+  };
+
+  // 导出
+  const handleExport = () => {
+    msgConfirm('确定导出所查询数据?', '导出', {
+      confirmButtonText: '确定',
+      showCancelButton: true,
+      type: 'warning',
+    })
+      .then(() => {
+        exportSurveillanceInfo(surveillanceTableQuery.queryParam).then(async (responnse) => {
+          if (!responnse) {
+            throw new Error('下载文件失败');
+          }
+          downloadByData(responnse, `监控调阅记录_${getCurrentDateTimeString()}.xlsx`);
+          ElMessage.success('下载文件成功');
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消导出',
+        });
+      });
+  };
+
+  const handleCurrentPageChange = (pageNumber: number) => {
+    pagination.pageNumber = pageNumber;
+    surveillanceTableQuery.pageNumber = pageNumber;
+    getTableData();
+  };
+
+  const handlePageSizeChange = (pageSize: number) => {
+    pagination.pageSize = pageSize;
+    surveillanceTableQuery.pageSize = pageSize;
+    getTableData();
+  };
+
+  const handleAddSurveillanceInfo = () => {
+    drawerVisible.value = true;
+    drawerType.value = 'add';
+    initialData.value = {};
+  };
+
+  const handleEditSurveillanceInfo = (row: SurveillanceInfoStruct) => {
+    drawerVisible.value = true;
+    drawerType.value = 'edit';
+    initialData.value = row;
+  };
+
+  const handleCloseDrawer = () => {
+    drawerVisible.value = false;
+    getTableData();
+  };
+
+  const handleDeleteSurveillanceInfo = (row: SurveillanceInfoStruct) => {
+    if (!row.id) return;
+    deleteSurveillanceInfo({ recordId: row.id }).then(() => {
+      ElMessage.success('删除成功');
+      getTableData();
+    });
+  };
+
+  const getTableData = () => {
+    tableConfig.loading = true;
+    surveillanceTableQuery.queryParam = {
+      fieldType: searchSelectedType.value,
+      fieldContent: searchKeyword.value,
+      isCopy: searchIsCopy.value,
+      accessStatus: searchAccessStatus.value,
+      startTime: searchTimeRange.value?.[0],
+      endTime: searchTimeRange.value?.[1],
+    };
+    getSurveillanceInfoList(surveillanceTableQuery).then((res) => {
+      tableData.value = res?.records || [];
+      pagination.total = res?.totalRow || 0;
+    });
+    tableConfig.loading = false;
+  };
+
+  // 动态生成表格列配置
+  const getTableColumns = () => {
+    if (surveillanceManagePermission.value) {
+      return SURVEILLANCE_LIST_TABLE_COLUMNS;
+    } else {
+      // 过滤掉操作列
+      return SURVEILLANCE_LIST_TABLE_COLUMNS.filter((column) => column.prop !== 'action');
+    }
+  };
+
+  onMounted(() => {
+    getTableData();
+    // 假设有监控调阅管理权限,实际应该从权限系统获取
+    surveillanceManagePermission.value = true;
+    tableConfig.maxHeight = surveillanceManagePermission.value
+      ? SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_PERMISSION
+      : SURVEILLANCE_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 *;
+
+  .search-container {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 16px;
+
+    .search-container-btn {
+      margin-left: auto;
+    }
+
+    .search-item {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      label {
+        white-space: nowrap;
+        font-size: 14px;
+        color: #606266;
+      }
+    }
+  }
+
+  .table-container {
+    position: relative;
+    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;
+  }
+</style>

+ 420 - 0
src/views/security-confidentiality/surveillance-management/components/ManageDrawer.vue

@@ -0,0 +1,420 @@
+<template>
+  <el-drawer
+    v-model="isDrawer"
+    size="480"
+    :title="props.type === 'add' ? '添加监控调阅记录' : '编辑监控调阅记录'"
+    @close="handleCloseDrawer"
+  >
+    <el-form ref="formRef" :model="formParams" :rules="rules" label-placement="left" :label-width="95">
+      <el-form-item label="工号" prop="staffNo" style="margin-bottom: 8px">
+        <el-input placeholder="请输入工号" v-model="formParams.staffNo" v-if="staffNoHtmlType === 'INPUT'" />
+        <el-tree-select
+          v-model="formParams.staffNo"
+          check-strictly
+          placeholder="请输入工号进行搜索"
+          class="protocal-select"
+          filterable
+          remote
+          clearable
+          :loading="loading"
+          :data="staffNoOptions"
+          :render-after-expand="false"
+          :default-expand-all="true"
+          :remote-method="debouncedRemoteMethod"
+          @clear="handleClearStaffNo"
+          @change="handleChangeStaffNo"
+          v-else
+        />
+        <el-text class="text-mode" type="primary" @click="handleChangeStaff">{{
+          `切换为工号${staffNoHtmlType === 'INPUT' ? '选择' : '输入'}方式`
+        }}</el-text>
+      </el-form-item>
+      <el-form-item label="姓名" prop="userName">
+        <el-input
+          :placeholder="staffNoHtmlType === 'INPUT' ? '请输入姓名' : '请选择工号,此项自动填充'"
+          v-model="formParams.userName"
+          :disabled="staffNoHtmlType === 'SELECT'"
+        />
+      </el-form-item>
+      <el-form-item label="所属部门" prop="deptName">
+        <el-tree-select
+          v-model="formParams.deptName"
+          :data="departmentArr"
+          :render-after-expand="false"
+          :default-expand-all="true"
+          check-strictly
+          :placeholder="staffNoHtmlType === 'INPUT' ? '请选择部门' : '请选择工号,此项自动填充'"
+          class="protocal-select"
+          :filter-node-method="filterDept"
+          filterable
+          clearable
+          :disabled="staffNoHtmlType === 'SELECT'"
+        />
+      </el-form-item>
+      <el-form-item label="调阅位置" prop="accessLocation">
+        <el-input
+          placeholder="请输入调阅位置"
+          v-model="formParams.accessLocation"
+          maxlength="50"
+          show-word-limit
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="调阅时段" prop="accessTimeRange">
+        <el-date-picker
+          v-model="formParams.accessTimeRange"
+          type="datetimerange"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          format="YYYY-MM-DD HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          style="width: 100%"
+          @change="handleChangeAccessTimeRange"
+        />
+      </el-form-item>
+      <el-form-item label="是否拷贝" prop="isCopy">
+        <el-radio-group v-model="formParams.isCopy">
+          <el-radio :value="0">否</el-radio>
+          <el-radio :value="1">是</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="调阅状态" prop="accessStatus">
+        <el-radio-group v-model="formParams.accessStatus">
+          <el-radio :value="0">待调阅</el-radio>
+          <el-radio :value="1">已调阅</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="审批单上传" prop="approvalFormUrl" :rules="approvalFormRules">
+        <UploadImages
+          ref="uploadImagesRef"
+          :maxCount="1"
+          :image-list="approvalImageList"
+          @upload-success="handleApprovalUploadChange"
+        />
+      </el-form-item>
+      <el-form-item label="记录人" prop="createdByName">
+        <el-input placeholder="记录人" v-model="formParams.createdByName" disabled />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-space>
+        <el-button @click="handleCloseDrawer">取消</el-button>
+        <el-button type="primary" @click="handleSubmitForm(formRef)">提交</el-button>
+      </el-space>
+    </template>
+  </el-drawer>
+</template>
+
+<script lang="ts" setup>
+  import { ref, reactive, watch, computed, onMounted } from 'vue';
+  import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
+  import { debounce } from 'lodash-es';
+  import { getAllDepartments } from '@/api/auth/dept';
+  import { queryOrganizationUserTree } from '@/api/system/user';
+  import { OrganizationUserTree } from '@/views/system/user/types';
+  import { calculateTreeData } from '@/utils';
+  import UploadImages from '@/views/disaster/disaster-control/src/components/UploadImages.vue';
+  import { ImageItem } from '@/types/disaster-control';
+  import { UPLOAD_BIZ_TYPE, uploadFileApi } from '@/api/minio';
+  import {
+    findUserByWorkNo,
+    transformTreeData,
+    TransformedTreeNode,
+    findOrgCodeByWorkNo,
+  } from '@/utils/findUserByWorkNo';
+  import {
+    SurveillanceInfoStruct,
+    addSurveillanceInfo,
+    updateSurveillanceInfo,
+  } from '@/api/security-confidentiality-surveillance';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+
+  const props = defineProps<{
+    type: string; // add or edit
+    initialData: SurveillanceInfoStruct;
+  }>();
+
+  const emits = defineEmits<{
+    (e: 'close'): void;
+  }>();
+
+  const { id, realname, staffNo } = useUserInfoHook();
+
+  const formRef = ref<FormInstance>();
+  const formParams = ref<SurveillanceInfoStruct & { accessTimeRange: [string, string] }>({
+    id: 0,
+    staffNo: '',
+    userName: '',
+    deptId: 0,
+    deptName: '',
+    accessLocation: '',
+    accessStartTime: '',
+    accessEndTime: '',
+    accessTimeRange: ['', ''],
+    isCopy: 0,
+    accessStatus: 0,
+    approvalFormUrl: '',
+    createdById: id,
+    createdByName: realname,
+    createdByStaffNo: staffNo,
+    createdAt: '',
+    updatedAt: '',
+    isDeleted: 0,
+  });
+
+  const rules = reactive<FormRules<SurveillanceInfoStruct & { accessTimeRange: [string, string] }>>({
+    staffNo: { required: true, message: '工号不能为空', trigger: 'blur' },
+    userName: { required: true, message: '姓名不能为空', trigger: 'blur' },
+    deptName: { required: true, message: '所属部门不能为空', trigger: 'blur' },
+    accessLocation: { required: true, message: '调阅位置不能为空', trigger: 'blur' },
+    accessTimeRange: { required: true, message: '调阅时段不能为空', trigger: 'change' },
+    isCopy: { required: true, message: '请选择是否拷贝', trigger: 'change' },
+    accessStatus: { required: true, message: '请选择调阅状态', trigger: 'change' },
+  });
+
+  // 审批单上传验证规则
+  const approvalFormRules = [
+    {
+      validator: (_: unknown, value: string, callback: (err?: Error) => void) => {
+        if (!value || value.trim() === '') {
+          return callback(new Error('请上传审批单'));
+        }
+        callback();
+      },
+      trigger: ['blur', 'change'],
+    },
+  ];
+
+  const isDrawer = ref(true);
+  const loading = ref(false);
+  const staffNoOptions = ref<TransformedTreeNode[]>([]); // 工号选择列表
+  const OrganizationSourceData = ref<OrganizationUserTree[]>([]); // 组织结构树原始数据
+  const departmentArr = ref<{ value: string | number; label: string }[]>([]);
+  const sourceDepartArr = ref<{ value: string | number; label: string }[]>([]);
+
+  // 工号输入/选择模式切换
+  type STAFFNO_HTML_TYPE = 'INPUT' | 'SELECT';
+  const staffNoHtmlType = ref<STAFFNO_HTML_TYPE>('SELECT');
+
+  // 审批单上传相关
+  const uploadImagesRef = ref<InstanceType<typeof UploadImages>>();
+  const approvalImages = ref<ImageItem[]>([]);
+
+  const approvalImageList = computed(() => {
+    if (!formParams.value.approvalFormUrl) return [];
+    // 如果是单个URL字符串,转换为数组格式
+    try {
+      const parsed = JSON.parse(formParams.value.approvalFormUrl);
+      return Array.isArray(parsed) ? parsed : [parsed];
+    } catch {
+      // 如果不是JSON格式,直接作为单个URL处理
+      return [{ url: formParams.value.approvalFormUrl }];
+    }
+  });
+
+  // 通过工号查询组织结构树
+  const remoteMethod = (query: string) => {
+    if (query) {
+      loading.value = true;
+      queryOrganizationUserTree(Number(query)).then((res) => {
+        if (res) {
+          loading.value = false;
+          staffNoOptions.value = transformTreeData(res, true);
+          OrganizationSourceData.value = res;
+          departmentArr.value = transformTreeData(OrganizationSourceData.value, false);
+        }
+      });
+    } else {
+      staffNoOptions.value = [];
+    }
+  };
+
+  // 防抖
+  const debouncedRemoteMethod = debounce(remoteMethod, 1000);
+
+  const handleClearStaffNo = () => {
+    formParams.value.staffNo = '';
+    formParams.value.userName = '';
+    formParams.value.deptId = undefined;
+    formParams.value.deptName = '';
+  };
+
+  // 切换工号输入/选择模式
+  const handleChangeStaff = () => {
+    staffNoHtmlType.value = staffNoHtmlType.value === 'INPUT' ? 'SELECT' : 'INPUT';
+    if (staffNoHtmlType.value === 'INPUT') {
+      // 切换到输入模式时,清空工号、姓名和部门,并恢复部门列表
+      formParams.value.staffNo = '';
+      formParams.value.userName = '';
+      formParams.value.deptId = undefined;
+      formParams.value.deptName = '';
+      departmentArr.value = sourceDepartArr.value;
+    } else {
+      // 切换到选择模式时,清空工号、姓名和部门
+      formParams.value.staffNo = '';
+      formParams.value.userName = '';
+      formParams.value.deptId = undefined;
+      formParams.value.deptName = '';
+    }
+  };
+
+  // 递归查找树结构中的节点
+  const findNodeInTree = (tree: any[], value: string | number): any => {
+    for (const node of tree) {
+      // 检查当前节点
+      if (node.value === value) {
+        return node;
+      }
+      // 检查子节点(如果有)
+      if (node.children && node.children.length > 0) {
+        const foundNode = findNodeInTree(node.children, value);
+        if (foundNode) {
+          return foundNode;
+        }
+      }
+    }
+    return null;
+  };
+
+  const handleChangeStaffNo = (value) => {
+    const findUser = findUserByWorkNo(OrganizationSourceData.value, value);
+    const deptId = Number(findOrgCodeByWorkNo(OrganizationSourceData.value, value));
+    const dept = findNodeInTree(departmentArr.value, deptId);
+    if (findUser) {
+      formParams.value.staffNo = findUser.idtUserWorkNo;
+      formParams.value.userName = findUser.appAccountAccountName;
+      formParams.value.deptId = deptId;
+      formParams.value.deptName = dept ? dept.label : '';
+    }
+  };
+
+  const handleChangeAccessTimeRange = () => {
+    console.log('accessTimeRange', formParams.value.accessTimeRange);
+    if (formParams.value.accessTimeRange && formParams.value.accessTimeRange.length === 2) {
+      formParams.value.accessStartTime = formParams.value.accessTimeRange[0];
+      formParams.value.accessEndTime = formParams.value.accessTimeRange[1];
+    }
+  };
+
+  // 格式化审批单图片
+  const formatApprovalImage = async (file: File) => {
+    if (!file) return '';
+    const fileName = file.name;
+    const res = await uploadFileApi({ bizType: UPLOAD_BIZ_TYPE.ATTACHMENT, fileName, file });
+    return res.url;
+  };
+
+  const handleApprovalUploadChange = async () => {
+    approvalImages.value = uploadImagesRef.value!.getUploadedImages();
+    if (approvalImages.value && approvalImages.value.length > 0) {
+      const image = approvalImages.value[0]; // 只取第一张图片
+      if (!image.file && image.url) {
+        // 如果已有URL,直接使用
+        formParams.value.approvalFormUrl = image.url;
+      } else if (image.file) {
+        // 如果有新文件,上传后使用新URL
+        const url = await formatApprovalImage(image.file);
+        formParams.value.approvalFormUrl = url;
+      }
+    } else {
+      // 如果没有图片,清空URL
+      formParams.value.approvalFormUrl = '';
+    }
+
+    // 手动触发审批单字段的校验
+    if (formRef.value) {
+      formRef.value.validateField('approvalFormUrl');
+    }
+  };
+
+  // 部门过滤函数
+  const filterDept = (val: string, data: any) => {
+    return data.label.includes(val);
+  };
+
+  // 初始化部门列表
+  const initDepartmentList = () => {
+    const departmentList = ref<{ value: string | number; label: string }[]>([]);
+    getAllDepartments().then((res) => {
+      departmentList.value = calculateTreeData(res, { level: 10, valueKey: 'id', labelKey: 'deptName' }, 1);
+      sourceDepartArr.value = departmentList.value;
+      departmentArr.value = departmentList.value;
+    });
+  };
+
+  const handleCloseDrawer = () => {
+    emits('close');
+  };
+
+  const handleSubmitForm = async (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+
+    // 处理调阅时段
+    if (formParams.value.accessTimeRange && formParams.value.accessTimeRange.length === 2) {
+      formParams.value.accessStartTime = formParams.value.accessTimeRange[0];
+      formParams.value.accessEndTime = formParams.value.accessTimeRange[1];
+    }
+
+    await formEl.validate((valid) => {
+      if (!valid) return;
+
+      if (props.type === 'add') {
+        addSurveillanceInfo(formParams.value).then(() => {
+          ElMessage.success('添加成功');
+          handleCloseDrawer();
+        });
+      } else {
+        updateSurveillanceInfo(formParams.value).then(() => {
+          ElMessage.success('编辑成功');
+          handleCloseDrawer();
+        });
+      }
+    });
+  };
+
+  watch(
+    () => props.initialData,
+    (newData) => {
+      if (newData) {
+        formParams.value = { ...newData, accessTimeRange: ['', ''] };
+        // 处理调阅时段
+        if (newData.accessStartTime && newData.accessEndTime) {
+          formParams.value.accessTimeRange = [newData.accessStartTime, newData.accessEndTime];
+        }
+        // 处理审批单图片 - 直接使用URL字符串
+        // approvalFormUrl 已经是字符串格式,不需要额外处理
+        // 如果有工号,尝试查询用户信息
+        if (newData.staffNo) {
+          remoteMethod(newData.staffNo);
+        }
+        // 编辑模式下,记录人字段保持原有数据,不覆盖
+      }
+    },
+    { immediate: true, deep: true },
+  );
+
+  onMounted(() => {
+    initDepartmentList();
+    // 只有在添加模式下才初始化记录人为当前登录用户
+    if (props.type === 'add') {
+      formParams.value.createdByName = realname;
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .protocal-select:deep(.el-select-dropdown__wrap) {
+    max-height: 600px;
+  }
+
+  .text-mode {
+    cursor: pointer;
+    font-size: 12px;
+    margin-left: 4px;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+</style>

+ 13 - 0
src/views/security-confidentiality/surveillance-management/config/index.ts

@@ -0,0 +1,13 @@
+import {
+  SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  SURVEILLANCE_LIST_TABLE_OPTIONS,
+  SURVEILLANCE_LIST_TABLE_COLUMNS,
+} from './table';
+
+export {
+  SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  SURVEILLANCE_LIST_TABLE_OPTIONS,
+  SURVEILLANCE_LIST_TABLE_COLUMNS,
+};

+ 95 - 0
src/views/security-confidentiality/surveillance-management/config/table.ts

@@ -0,0 +1,95 @@
+/**
+ * 监控调阅记录表格配置
+ */
+import type { TableColumnProps } from '@/types/basic-table';
+
+export const SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_DEFAULT = 'calc(70vh - 80px)';
+export const SURVEILLANCE_LIST_TABLE_MAX_HEIGHT_PERMISSION = 'calc(70vh - 130px)';
+
+// 基础表格样式配置
+const TABLE_OPTIONS = {
+  emptyText: '暂无数据',
+  loading: true,
+};
+
+// 监控调阅记录表格样式配置
+export const SURVEILLANCE_LIST_TABLE_OPTIONS = {
+  ...TABLE_OPTIONS,
+};
+
+// 监控调阅记录表格列配置
+export const SURVEILLANCE_LIST_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '序号',
+    prop: 'index',
+    width: '80px',
+    type: 'index',
+    align: 'center',
+  },
+  {
+    label: '姓名',
+    prop: 'userName',
+    align: 'left',
+    minWidth: '100px',
+  },
+  {
+    label: '工号',
+    prop: 'staffNo',
+    align: 'left',
+    minWidth: '100px',
+  },
+  {
+    label: '部门',
+    prop: 'deptName',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '调阅位置',
+    prop: 'accessLocation',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '调阅时段',
+    prop: 'accessTimePeriod',
+    align: 'left',
+    minWidth: '200px',
+    slot: 'accessTimePeriod',
+  },
+  {
+    label: '是否拷贝',
+    prop: 'isCopy',
+    align: 'center',
+    width: '110px',
+    slot: 'isCopy',
+  },
+  {
+    label: '审批单',
+    prop: 'approvalForm',
+    align: 'center',
+    width: '180px',
+    slot: 'approvalForm',
+  },
+  {
+    label: '调阅状态',
+    prop: 'accessStatus',
+    align: 'center',
+    width: '110px',
+    slot: 'accessStatus',
+  },
+  {
+    label: '记录人',
+    prop: 'createdByName',
+    align: 'left',
+    minWidth: '100px',
+  },
+  {
+    label: '操作',
+    prop: 'action',
+    align: 'left',
+    slot: 'action',
+    fixed: 'right',
+    width: '128px',
+  },
+];

+ 45 - 0
src/views/security-confidentiality/surveillance-management/constant/index.ts

@@ -0,0 +1,45 @@
+// 查询字段对应:1-姓名,2-工号,3-部门,4-调阅位置
+export enum FIELDTYPE {
+  NAME = '1',
+  JOB_NUMBER = '2',
+  DEPARTMENT_NAME = '3',
+  ACCESS_LOCATION = '4',
+}
+
+export const FIELD_CONTENT = {
+  [FIELDTYPE.NAME]: '姓名',
+  [FIELDTYPE.JOB_NUMBER]: '工号',
+  [FIELDTYPE.DEPARTMENT_NAME]: '部门',
+  [FIELDTYPE.ACCESS_LOCATION]: '调阅位置',
+};
+
+export const surveillanceQueryOptions = [
+  {
+    label: FIELD_CONTENT[FIELDTYPE.NAME],
+    value: FIELDTYPE.NAME,
+  },
+  {
+    label: FIELD_CONTENT[FIELDTYPE.JOB_NUMBER],
+    value: FIELDTYPE.JOB_NUMBER,
+  },
+  {
+    label: FIELD_CONTENT[FIELDTYPE.DEPARTMENT_NAME],
+    value: FIELDTYPE.DEPARTMENT_NAME,
+  },
+  {
+    label: FIELD_CONTENT[FIELDTYPE.ACCESS_LOCATION],
+    value: FIELDTYPE.ACCESS_LOCATION,
+  },
+];
+
+// 是否拷贝选项
+export const COPY_OPTIONS = [
+  { label: '否', value: 0 },
+  { label: '是', value: 1 },
+];
+
+// 调阅状态选项
+export const ACCESS_STATUS_OPTIONS = [
+  { label: '待调阅', value: 0 },
+  { label: '已调阅', value: 1 },
+];