Quellcode durchsuchen

Merge branch 'feat-position-grouping' into 'dev'

feat: 人员分组

See merge request product-group-fe/sfy-safety-group/sfy-safety!323
毕欣怡 vor 2 Monaten
Ursprung
Commit
e26e7d327f

+ 3 - 1
src/App.vue

@@ -44,7 +44,8 @@
     height: 100%;
   }
   .content {
-    height: 100%;
+    // height: 100%;
+    height: calc(100vh - 78cpx);
     flex: 1;
     overflow-y: auto;
     overflow-x: hidden;
@@ -53,6 +54,7 @@
     justify-content: center;
 
     &.fixed-screen {
+      height: 100%;
       overflow-y: hidden;
     }
   }

+ 24 - 0
src/api/system/person-group.ts

@@ -4,6 +4,7 @@ import {
   EditPersonGroupParams,
   QueryPersonGroupPageParams,
   QueryAvailablePersonPageParams,
+  QueryAvailablePersonPageParamsAll,
   QueryPersonGroupPageRes,
   QueryPersonGroupDetailRes,
   QueryAvailablePersonPageRes,
@@ -89,6 +90,7 @@ export const getUserGroupDetailByIds = (userGroupList: number[]) => {
 /**
  * @description: 查询可添加到用户组的用户
  */
+// 分页
 export function queryAvailableUserList(params: QueryAvailablePersonPageParams): Promise<QueryAvailablePersonPageRes> {
   return http.request({
     url: `/admin/user/queryAvailableUserList`,
@@ -96,6 +98,14 @@ export function queryAvailableUserList(params: QueryAvailablePersonPageParams):
     data: params,
   });
 }
+// 不分页
+export function queryAvailableUserListAll(params: QueryAvailablePersonPageParamsAll): Promise<PersonGroupItem[]> {
+  return http.request<PersonGroupItem[]>({
+    url: `/admin/user/queryUserListByFilter`,
+    method: 'post',
+    data: params,
+  });
+}
 
 /**
  * 查询用户所在一级部门的组织树
@@ -126,3 +136,17 @@ export const queryUserInfoByUserName = (username: string) => {
     method: 'post',
   });
 };
+
+/**
+ * 导出用户组名单
+ */
+export const exportUserGroupList = (userGroupId: number) => {
+  return http.request(
+    {
+      url: `/userGroup/exportGroupUserList?userGroupId=${userGroupId}`,
+      method: 'get',
+      responseType: 'blob',
+    },
+    { isTransformResponse: false },
+  );
+};

+ 10 - 0
src/api/system/user-operate.ts

@@ -31,6 +31,15 @@ interface AdminUserType {
   tenantId: number;
 }
 
+export interface JobListItem {
+  createdAt: string;
+  id: number;
+  idtJobCode: string;
+  idtJobId: string;
+  idtJobName: string;
+  isDeleted: number;
+  updatedAt: string;
+}
 export interface DeptListItem {
   children: [];
   createdAt: string;
@@ -79,6 +88,7 @@ export interface UserLisItem {
   updatedBy: null;
   username: string;
   certify: string;
+  jobList: JobListItem[];
 }
 
 /** root用户给某个租户添加管理员 */

+ 1 - 1
src/components/Nav.vue

@@ -229,7 +229,7 @@
   .header.use-px .header__nav--item {
     height: 45px;
     padding: 10px 20px;
-    font-size: 18px;
+    font-size: 16px;
     border-radius: 4px;
   }
 

+ 82 - 49
src/components/PersonSelector/PersonGroupFilter.vue

@@ -18,25 +18,16 @@
         </el-form-item>
       </div>
       <!-- 搜索结果展示 -->
-      <div v-if="personFilterList?.records?.length" class="filter-result" ref="filterResult">
-        <div class="filter-result-item" v-for="person in personFilterList.records" :key="person.id">
+      <div v-if="personFilterList?.length" class="filter-result" ref="filterResult">
+        <el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" @change="handleCheckAllChange">
+          全选
+        </el-checkbox>
+        <div class="filter-result-item" v-for="person in personFilterList" :key="person.id">
           <el-checkbox
             v-model="person.checked"
-            :label="person.staffNo + '-' + person.realname + '&nbsp' + '(' + person.deptName + ')'"
+            :label="formatPersonDisplay(person)"
             @change="handleSelect($event, person)"
-          ></el-checkbox>
-          <!-- <div style="margin-left: 8px">
-            {{ person.staffNo + '-' + person.nickname + '&nbsp' + '(' + person.deptName + ')' }}
-          </div> -->
-        </div>
-        <div
-          id="next-loading"
-          style="text-align: center"
-          v-if="personFilterList.totalRow > personFilterList.records.length"
-        >
-          <el-icon class="el-input__icon" :size="24">
-            <Loading />
-          </el-icon>
+          />
         </div>
       </div>
       <div v-else-if="!personFilterList" class="filter-result-empty">
@@ -58,20 +49,19 @@
           closable
           @close="handleRemoveSelectedPerson(person.id)"
         >
-          {{ person.staffNo + '-' + person.realname }}
+          {{ formatPersonDisplay(person) }}
         </el-tag>
       </div>
     </div>
   </div>
   <div class="footer">
-    <el-button @click="handleCancle"> 取消 </el-button>
+    <el-button @click="handleCancel"> 取消 </el-button>
     <el-button type="primary" @click="handleSubmit">提交</el-button>
   </div>
 </template>
 
 <script lang="ts" setup>
-  import { ref, onMounted, onBeforeUnmount } from 'vue';
-  import { Loading } from '@element-plus/icons-vue';
+  import { ref, onMounted, watch } from 'vue';
   import { usePersonGroupFilter } from './hooks/usePersonGroupFilter';
   import { PersonGroupItem } from '@/types/person-group/type';
   import { debounce } from 'lodash-es';
@@ -83,9 +73,10 @@
     personFilterList,
     selectedPersonList,
     getPersonFilterList,
-    getNextPersonFilterList,
     handleAddSelectedPerson,
     handleRemoveSelectedPerson,
+    handleBatchAddSelectedPerson,
+    handleBatchRemoveSelectedPerson,
   } = usePersonGroupFilter();
 
   const emit = defineEmits(['cancel', 'submit']);
@@ -94,54 +85,95 @@
     initSelected?: PersonGroupItem[];
   }>();
 
-  onMounted(() => {
-    if (props.initSelected) selectedPersonList.value = [...props.initSelected];
-  });
-
-  let observer: IntersectionObserver;
+  const checkAll = ref(false);
+  const isIndeterminate = ref(false);
 
   const filterResult = ref();
   const handleFilter = () => {
     getPersonFilterList().then(() => {
-      filterResult.value.scrollTop = 0;
-      const loading = document.getElementById('next-loading');
-      if (loading) {
-        if (observer) observer.unobserve(loading);
-        observer = new IntersectionObserver(
-          (entries) => {
-            if (entries[0].isIntersecting) {
-              getNextPersonFilterList();
-            }
-          },
-          {
-            threshold: 0.9,
-          },
-        );
-        observer.observe(loading);
+      if (filterResult.value) {
+        filterResult.value.scrollTop = 0;
       }
     });
   };
 
   const debouncedHandleFilter = debounce(handleFilter, 500);
 
+  // 格式化人员显示文本,过滤空值
+  const formatPersonDisplay = (person: any) => {
+    const values = [person.staffNo, person.realname, person.deptName, person.jobName].filter(
+      (value) => value != null && value !== undefined && value !== '',
+    );
+    return values.join('-');
+  };
+
   const handleSelect = (v: boolean, person: any) => {
     if (v) {
       handleAddSelectedPerson(person);
     } else {
       handleRemoveSelectedPerson(person.id);
     }
+    // 更新全选状态
+    updateCheckAllStatus();
+  };
+
+  const handleCheckAllChange = () => {
+    if (!personFilterList.value || personFilterList.value.length === 0) return;
+
+    isIndeterminate.value = false;
+    if (checkAll.value) {
+      // 全选:批量添加所有当前列表中的项
+      handleBatchAddSelectedPerson(personFilterList.value);
+    } else {
+      // 取消全选:批量移除当前列表中的所有项
+      const personIds = personFilterList.value.map((item) => item.id);
+      handleBatchRemoveSelectedPerson(personIds);
+    }
+  };
+
+  // 更新全选状态
+  const updateCheckAllStatus = () => {
+    if (!personFilterList.value || personFilterList.value.length === 0) {
+      checkAll.value = false;
+      isIndeterminate.value = false;
+      return;
+    }
+    const checkedCount = personFilterList.value.filter((item) => item.checked).length;
+    const totalCount = personFilterList.value.length;
+    checkAll.value = checkedCount === totalCount && totalCount > 0;
+    isIndeterminate.value = checkedCount > 0 && checkedCount < totalCount;
   };
-  const handleCancle = () => {
+
+  watch(
+    () => selectedPersonList.value,
+    () => {
+      updateCheckAllStatus();
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  watch(
+    () => personFilterList.value,
+    () => {
+      updateCheckAllStatus();
+    },
+    {
+      deep: true,
+    },
+  );
+
+  const handleCancel = () => {
     emit('cancel');
   };
+
   const handleSubmit = () => {
     emit('submit', selectedPersonList.value);
   };
 
-  onBeforeUnmount(() => {
-    if (observer) {
-      observer.disconnect();
-    }
+  onMounted(() => {
+    if (props.initSelected) selectedPersonList.value = [...props.initSelected];
   });
 </script>
 
@@ -198,14 +230,14 @@
         font-size: 16px;
         color: rgba(0, 0, 0, 0.88);
         line-height: 22px;
-        margin: 6px 0 6px;
+        margin: 6px 0 18px;
       }
       .selected {
         display: flex;
         flex-wrap: wrap;
         gap: 8px;
-        overflow-y: auto;
-        max-height: calc(100% - 120px);
+        overflow: auto;
+        max-height: 392px;
       }
     }
   }
@@ -214,5 +246,6 @@
     gap: 6px;
     justify-content: flex-end;
     padding-right: 16px;
+    margin-top: 18px;
   }
 </style>

+ 58 - 45
src/components/PersonSelector/hooks/usePersonGroupFilter.ts

@@ -1,52 +1,35 @@
-import {
-  QueryAvailablePersonPageParams,
-  QueryAvailablePersonPageRes,
-  PersonGroupItem,
-} from '@/types/person-group/type';
-import { queryAvailableUserList } from '@/api/system/person-group';
 import { ref } from 'vue';
+import { QueryAvailablePersonPageParamsAll, PersonGroupItem } from '@/types/person-group/type';
+import { queryAvailableUserListAll } from '@/api/system/person-group';
 
 export const usePersonGroupFilter = () => {
   const FILTER_TYPES = [
     { type: 'realname', label: '姓名' },
     { type: 'staffNo', label: '工号' },
     { type: 'deptName', label: '部门' },
+    { type: 'jobName', label: '岗位' },
   ];
 
   // 复用输入框类型及标签
   const personFilterType = ref({ type: 'realname', label: '姓名' });
   // 输入框绑定数据
   const personFilterValue = ref<string>();
-  // 查询参数
-  const personFilterParams = ref<QueryAvailablePersonPageParams>({
-    pageNumber: 1,
-    pageSize: 10,
-    queryParam: {},
-  });
-  // 查询事件
+  // 查询结果(改为直接使用数组,不再使用分页结构)
+  const personFilterList = ref<PersonGroupItem[]>([]);
+
+  // 查询事件(一次性加载全部数据)
   const getPersonFilterList = async () => {
-    personFilterParams.value = {
-      pageNumber: 1,
-      pageSize: 10,
-      queryParam: {},
-    };
-    personFilterParams.value.queryParam[personFilterType.value.type] = personFilterValue.value;
-    const res = await queryAvailableUserList(personFilterParams.value);
-    personFilterList.value = res;
+    const res = await queryAvailableUserListAll({
+      [personFilterType.value.type as keyof QueryAvailablePersonPageParamsAll]: personFilterValue.value,
+    });
     // 如果有返回且搜索到则添加选中标记
-    if (personFilterList.value && personFilterList.value.records)
-      personFilterList.value.records = checkPersonList(personFilterList.value.records);
-  };
-  // 查询翻页事件
-  const getNextPersonFilterList = async () => {
-    personFilterParams.value.pageNumber++;
-    const res = await queryAvailableUserList(personFilterParams.value);
-    res.records = checkPersonList(res.records);
-    personFilterList.value!.totalRow = res.totalRow;
-    personFilterList.value!.records.push(...res.records);
+    if (res && res.length > 0) {
+      personFilterList.value = checkPersonList(res);
+    } else {
+      personFilterList.value = [];
+    }
   };
-  // 查询结果
-  const personFilterList = ref<QueryAvailablePersonPageRes>();
+
   // 已选择人员
   const selectedPersonList = ref<PersonGroupItem[]>([]);
   // 查询结果添加选中标记
@@ -61,36 +44,66 @@ export const usePersonGroupFilter = () => {
 
   // 查询结果刷新选中标记
   const refreshPersonFilterCheckedStatus = (personId: number) => {
-    const person = personFilterList.value?.records.find((item) => item.id === personId);
-    if (person) person.checked = selectedPersonList.value.some((person) => person.id === personId);
+    const person = personFilterList.value.find((item) => item.id === personId);
+    if (person) {
+      person.checked = selectedPersonList.value.some((person) => person.id === personId);
+    }
   };
 
   // 添加已选择人员
   const handleAddSelectedPerson = (person: PersonGroupItem) => {
-    selectedPersonList.value.push(person);
-    // refreshPersonFilterCheckedStatus(person.id);
+    // 检查是否已存在,避免重复添加
+    if (!selectedPersonList.value.some((item) => item.id === person.id)) {
+      selectedPersonList.value.push(person);
+    }
+    // 更新选中状态
+    refreshPersonFilterCheckedStatus(person.id);
   };
 
   // 移除已选择人员
   const handleRemoveSelectedPerson = (personId: number) => {
-    // selectedPersonList.value = selectedPersonList.value.filter((item) => item.id !== personId);
-    selectedPersonList.value.splice(
-      selectedPersonList.value.findIndex((item) => item.id === personId),
-      1,
-    );
-    refreshPersonFilterCheckedStatus(personId);
+    const index = selectedPersonList.value.findIndex((item) => item.id === personId);
+    if (index > -1) {
+      selectedPersonList.value.splice(index, 1);
+      refreshPersonFilterCheckedStatus(personId);
+    }
+  };
+
+  // 批量添加已选择人员
+  const handleBatchAddSelectedPerson = (persons: PersonGroupItem[]) => {
+    persons.forEach((person) => {
+      if (!selectedPersonList.value.some((item) => item.id === person.id)) {
+        selectedPersonList.value.push(person);
+      }
+    });
+    // 批量更新选中状态
+    personFilterList.value.forEach((item) => {
+      item.checked = selectedPersonList.value.some((person) => person.id === item.id);
+    });
+  };
+
+  // 批量移除已选择人员
+  const handleBatchRemoveSelectedPerson = (personIds: number[]) => {
+    personIds.forEach((personId) => {
+      const index = selectedPersonList.value.findIndex((item) => item.id === personId);
+      if (index > -1) {
+        selectedPersonList.value.splice(index, 1);
+      }
+      refreshPersonFilterCheckedStatus(personId);
+    });
   };
+
   return {
     FILTER_TYPES,
     personFilterType,
     personFilterValue,
-    personFilterParams,
     personFilterList,
     selectedPersonList,
     getPersonFilterList,
-    getNextPersonFilterList,
     handleAddSelectedPerson,
     handleRemoveSelectedPerson,
+    handleBatchAddSelectedPerson,
+    handleBatchRemoveSelectedPerson,
   };
 };
 

+ 142 - 25
src/components/batch-import/BatchImport.vue

@@ -4,7 +4,7 @@
     <el-card class="pop-card">
       <template #header>
         <div style="font-size: 16px; font-weight: 600">批量导入</div>
-        <el-icon :size="18" @click="handleClose" style="cursor: pointer">
+        <el-icon :size="18" @click="handleMainClose" style="cursor: pointer">
           <Close />
         </el-icon>
       </template>
@@ -17,6 +17,7 @@
           :limit="1"
           drag
           :action="importApiUrl"
+          :data="props.data"
           :with-credentials="true"
           :auto-upload="false"
           :before-upload="beforeUpload"
@@ -49,21 +50,28 @@
           <WarnTriangleFilled />
         </el-icon>
         <div class="header-text">导入提示</div>
-        <el-icon class="close-icon" :size="18" @click="handleClose"><Close /></el-icon>
+        <el-icon class="close-icon" :size="18" @click="handleErrorDialogClose"><Close /></el-icon>
       </template>
-      <div class="sum-count">
-        成功导入 <span class="succ-sum">{{ sucCount }}</span> 条, 失败 <span class="err-sum">{{ errCount }}</span> 条
+      <!-- 简单模式或 ImportResponseDataPerson 类型:显示字符串消息 -->
+      <div v-if="props.responseType === 'simple' || isPersonResponseType" class="simple-message">
+        {{ simpleMessage }}
       </div>
-      <el-table :data="errDetail" style="width: 100%" height="190">
-        <el-table-column prop="rowNum" label="行数" width="100" fixed align="center" />
-        <el-table-column
-          prop="failReason"
-          label="失败原因"
-          show-overflow-tooltip
-          class-name="error-reason"
-          label-class-name="error-reason-label"
-        />
-      </el-table>
+      <!-- 详细模式:显示统计和错误详情表格 -->
+      <template v-else>
+        <div class="sum-count">
+          成功导入 <span class="succ-sum">{{ sucCount }}</span> 条, 失败 <span class="err-sum">{{ errCount }}</span> 条
+        </div>
+        <el-table :data="errDetail" style="width: 100%" height="190">
+          <el-table-column prop="rowNum" label="行数" width="100" fixed align="center" />
+          <el-table-column
+            prop="failReason"
+            label="失败原因"
+            show-overflow-tooltip
+            class-name="error-reason"
+            label-class-name="error-reason-label"
+          />
+        </el-table>
+      </template>
       <template #footer>
         <el-button type="primary" @click="handleBackToImport">重新导入 </el-button>
       </template>
@@ -78,16 +86,17 @@
   import { genFileId, ElMessage } from 'element-plus';
   import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
   import { Close, Document, WarnTriangleFilled } from '@element-plus/icons-vue';
-  import type { BatchImportProps, ErrorDetailItem, ImportResponseData } from './types';
+  import type { BatchImportProps, ErrorDetailItem, ImportResponseData, ImportResponseDataPerson } from './types';
 
   const props = withDefaults(defineProps<BatchImportProps>(), {
     templateName: '下载模板',
     showTemplate: true,
+    responseType: 'detailed',
   });
 
   const emit = defineEmits<{
     (e: 'close'): void;
-    (e: 'update'): void;
+    (e: 'update', data?: ImportResponseData | ImportResponseDataPerson | string): void;
   }>();
 
   const upload = ref<UploadInstance>();
@@ -96,6 +105,9 @@
   const sucCount = ref<number>(0);
   const errCount = ref<number>(0);
   const errDetail = ref<ErrorDetailItem[]>([]);
+  const simpleMessage = ref<string>('');
+  const lastResponseData = ref<ImportResponseData | ImportResponseDataPerson | string | undefined>(undefined);
+  const isPersonResponseType = ref<boolean>(false);
 
   // 监听visible属性变化,重置状态
   watch(
@@ -107,6 +119,9 @@
         sucCount.value = 0;
         errCount.value = 0;
         errDetail.value = [];
+        simpleMessage.value = '';
+        lastResponseData.value = undefined;
+        isPersonResponseType.value = false;
         // 清空上传文件
         if (upload.value) {
           upload.value.clearFiles();
@@ -178,29 +193,111 @@
     }));
   }
 
-  const handleUploadSuccess = (response: { data: ImportResponseData }) => {
-    sucCount.value = response.data.successCount;
-    errCount.value = response.data.failCount;
-    errDetail.value = response.data.failInfoList;
+  const handleUploadSuccess = (response: { data: ImportResponseData | ImportResponseDataPerson | string } | string) => {
+    // 简单模式:处理字符串返回
+    if (props.responseType === 'simple') {
+      try {
+        // 提取字符串消息
+        let message = '';
+        if (typeof response === 'string') {
+          message = response;
+          simpleMessage.value = response;
+          lastResponseData.value = response;
+        } else if (typeof response.data === 'string') {
+          message = response.data;
+          simpleMessage.value = response.data;
+          lastResponseData.value = response.data;
+        } else if (response.data && typeof response.data === 'object' && 'message' in response.data) {
+          message = (response.data as { message: string }).message;
+          simpleMessage.value = message;
+          lastResponseData.value = message;
+        } else {
+          message = '导入完成';
+          simpleMessage.value = message;
+          lastResponseData.value = message;
+        }
+        // 显示错误弹窗
+        dialogVisibleErr.value = true;
+        // 通知父组件更新数据(但不关闭主弹窗)
+        emit('update', lastResponseData.value);
+      } catch (error) {
+        ElMessage({
+          message: '导入失败',
+          type: 'error',
+        });
+        emit('update', lastResponseData.value);
+      }
+      return;
+    }
+
+    // 详细模式:兼容字符串和对象返回
+    let detailedData: ImportResponseData | ImportResponseDataPerson | undefined;
+    if (typeof response === 'string') {
+      // 如果后端返回纯字符串(理论不应出现,兜底)
+      detailedData = undefined;
+    } else if (response && typeof response.data === 'object') {
+      const responseData = response.data;
+      // 判断是 ImportResponseDataPerson 类型(有 successMembers 属性)
+      if ('successMembers' in responseData) {
+        detailedData = responseData as ImportResponseDataPerson;
+        isPersonResponseType.value = true;
+        // ImportResponseDataPerson 类型:在 dialog 中只显示 message
+        simpleMessage.value = detailedData.message || '导入完成';
+        // 保存返回值
+        lastResponseData.value = detailedData;
+        // 显示错误弹窗
+        dialogVisibleErr.value = true;
+        // 触发 update 事件,通知父组件更新数据(但不关闭主弹窗)
+        emit('update', detailedData);
+        return;
+      } else {
+        // 否则是 ImportResponseData 类型(有 successCount, failCount, failInfoList)
+        detailedData = responseData as ImportResponseData;
+        isPersonResponseType.value = false;
+      }
+    } else {
+      // 如果 response.data 是字符串(异常情况,兜底)
+      detailedData = undefined;
+      isPersonResponseType.value = false;
+    }
+
+    // 保存返回值
+    lastResponseData.value = detailedData;
+
+    // 处理 ImportResponseData 类型(有 successCount, failCount, failInfoList)
+    if (detailedData && 'successCount' in detailedData) {
+      const importData = detailedData as ImportResponseData;
+      sucCount.value = importData.successCount;
+      errCount.value = importData.failCount;
+      errDetail.value = importData.failInfoList;
+    } else {
+      sucCount.value = 0;
+      errCount.value = 0;
+      errDetail.value = [];
+    }
 
     try {
       if (sucCount.value != 0 && errCount.value === 0 && errDetail.value.length === 0) {
+        // 完全成功:通知父组件更新数据,然后关闭弹窗
         ElMessage({
           message: `导入成功!共导入${sucCount.value}条记录`,
           type: 'success',
         });
-        emit('update');
+        emit('update', detailedData);
         emit('close');
       } else {
-        dialogVisibleErr.value = true; // 显示错误dialog
+        // 有错误:显示错误弹窗,通知父组件更新数据(可选)
+        dialogVisibleErr.value = true;
         errDetail.value = mergeFailReasons(errDetail.value);
+        // 可选:如果父组件需要知道有错误,可以触发 update
+        // emit('update', detailedData);
       }
     } catch (error) {
       ElMessage({
         message: '导入失败',
         type: 'error',
       });
-      emit('update');
+      emit('update', lastResponseData.value);
     }
   };
 
@@ -225,8 +322,15 @@
     isImportDisable.value = true;
   };
 
-  const handleClose = () => {
-    emit('update');
+  // 主弹窗关闭:直接关闭,不触发 update(因为可能没有数据更新)
+  const handleMainClose = () => {
+    emit('close');
+  };
+
+  // 错误弹窗关闭:关闭错误弹窗,并关闭主弹窗
+  const handleErrorDialogClose = () => {
+    dialogVisibleErr.value = false;
+    emit('close');
   };
 </script>
 
@@ -297,6 +401,19 @@
     .el-dialog__body {
       padding: 20px;
 
+      .simple-message {
+        font-size: 16px;
+        line-height: 1.5;
+        text-align: center;
+        padding: 40px 20px;
+        min-height: 190px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        white-space: pre-wrap;
+        word-break: break-word;
+      }
+
       .sum-count {
         font-size: 20px;
         display: flex;

+ 23 - 0
src/components/batch-import/types.ts

@@ -22,6 +22,17 @@ export interface BatchImportProps {
    * 是否显示下载模板按钮,默认为 true
    */
   showTemplate?: boolean;
+  /**
+   * 响应数据类型
+   * - 'detailed': 详细模式,返回 { successCount, failCount, failInfoList }
+   * - 'simple': 简单模式,返回字符串
+   * @default 'detailed'
+   */
+  responseType?: 'detailed' | 'simple';
+  /**
+   * 上传时附带的额外参数
+   */
+  data?: Record<string, any>;
 }
 
 /**
@@ -40,3 +51,15 @@ export interface ImportResponseData {
   failCount: number;
   failInfoList: ErrorDetailItem[];
 }
+
+export interface ImportResponseDataPerson {
+  message: string;
+  successMembers: {
+    id: number;
+    realname: string;
+    deptId: number;
+    deptName: string;
+    staffNo: string;
+    jobName: string;
+  }[];
+}

+ 11 - 0
src/types/person-group/type.ts

@@ -45,6 +45,7 @@ export interface QueryPersonGroupPageParams extends PaginationRequest {
 }
 
 /** 查询可选用户列表请求参数 */
+// 分页
 export interface QueryAvailablePersonPageParams extends PaginationRequest {
   /*查询参数 */
   queryParam: {
@@ -52,8 +53,16 @@ export interface QueryAvailablePersonPageParams extends PaginationRequest {
     realname?: string;
     deptName?: string;
     staffNo?: string;
+    jobName?: string;
   };
 }
+// 不分页
+export interface QueryAvailablePersonPageParamsAll {
+  realname?: string;
+  deptName?: string;
+  staffNo?: string;
+  jobName?: string;
+}
 
 /** 用户组列表展示信息 */
 export interface PersonGroupListItem {
@@ -87,6 +96,8 @@ export interface PersonGroupItem {
   deptName?: string;
   /*工号 */
   staffNo: string;
+  /*岗位 */
+  jobName?: string;
 }
 
 /** 查询用户组详情后端返回data */

+ 1 - 0
src/views/emergency/overview/PageOverview.vue

@@ -29,6 +29,7 @@
     width: 100%;
     height: 100%;
     display: flex;
+    overflow-y: hidden;
   }
 
   .left-container {

+ 1 - 1
src/views/security-confidentiality/overview/components/ConfidentialityPosition.vue

@@ -561,11 +561,11 @@
       }
 
       .empty-state {
+        height: calc(100% - 45px);
         display: flex;
         flex-direction: column;
         align-items: center;
         justify-content: center;
-        padding: 40px 20px;
         color: #999;
         gap: 12px;
 

+ 2 - 2
src/views/system/approval/PageApproval.vue

@@ -11,8 +11,8 @@
         <BasicTable
           :tableConfig="tableConfig"
           :tableData="tableData"
-          @update:pageSize="handleSizeChange"
-          @update:pageNumber="handleCurrentChange"
+          @update:page-size="handleSizeChange"
+          @update:page-number="handleCurrentChange"
         >
           <template #action="scope">
             <ActionButton text="设置审批流程" @click="handleEditApproval(scope.row.id)" />

+ 36 - 26
src/views/system/dictionary/dictionary.vue

@@ -8,27 +8,29 @@
         <el-button type="primary" @click="handleAddDialogShow" :icon="Plus">新增字典项</el-button>
       </div>
 
-      <el-table v-loading="loading" :data="dataSource" style="width: 100%; margin-top: 16px" :max-height="'calc(100vh - 300px)'">
-        <el-table-column prop="dictName" label="字典名称" width="360" />
-        <el-table-column prop="dictCode" label="字典编码" width="360" />
-        <el-table-column prop="description" label="字典描述" width="680" />
-        <el-table-column prop="dictType" label="分类" width="180">
-          <template #default="{ row }">
-            {{ typeLabelMap[row.dictType] || '' }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="status" label="状态" width="180">
-          <template #default="{ row }">
-            {{ statusLabelMap[row.status] || '' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="200" fixed="right">
-          <template #default="{ row }">
-            <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
-            <el-button type="primary" link @click="handleDelete(row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
+      <div class="table-wrapper">
+        <el-table v-loading="loading" :data="dataSource" height="100%">
+          <el-table-column prop="dictName" label="字典名称" width="360" />
+          <el-table-column prop="dictCode" label="字典编码" width="360" />
+          <el-table-column prop="description" label="字典描述" />
+          <el-table-column prop="dictType" label="分类" width="180">
+            <template #default="{ row }">
+              {{ typeLabelMap[row.dictType] || '' }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="status" label="状态" width="180">
+            <template #default="{ row }">
+              {{ statusLabelMap[row.status] || '' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="160" fixed="right">
+            <template #default="{ row }">
+              <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
+              <el-button type="primary" link @click="handleDelete(row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
 
       <div class="paginationPosition">
         <el-pagination
@@ -170,13 +172,21 @@
 
 <style lang="scss" scoped>
   .dictionary-container {
-    margin: 20px;
-    .search-form {
-      margin-bottom: 16px;
-    }
+    padding: 20px;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow: hidden;
 
     .table-operations {
-      margin-bottom: 16px;
+      margin-bottom: 20px;
+      flex-shrink: 0;
     }
   }
+
+  .table-wrapper {
+    flex: 1;
+    overflow: hidden;
+    min-height: 0;
+  }
 </style>

+ 91 - 26
src/views/system/person-group/PersonGroup.vue

@@ -3,8 +3,9 @@
     <template #static>
       <Breadcrumb />
     </template>
-    <div>
-      <div style="margin: 20px">
+
+    <div class="content">
+      <div class="toolbar">
         <el-button type="primary" @click="openEditDrawer()" v-permission="PERM_NOTICE.PERSONNEL_GROUP">
           <template #icon>
             <el-icon>
@@ -13,14 +14,22 @@
           </template>
           新建人员分组
         </el-button>
+        <el-button color="#1890FF" plain @click="batchImportVisible = true" v-permission="PERM_NOTICE.PERSONNEL_GROUP">
+          <template #icon>
+            <el-icon><DocumentAdd /></el-icon>
+          </template>
+          导入人员分组
+        </el-button>
+      </div>
 
-        <el-table :data="personGroupList" style="margin-top: 20px">
+      <div class="table-wrapper">
+        <el-table :data="personGroupList" height="100%">
           <el-table-column label="分组名" prop="name" width="240" />
-          <el-table-column label="分组描述" prop="description" width="580" />
+          <el-table-column label="分组描述" prop="description" min-width="200" />
           <el-table-column label="人员数量" prop="total" width="200" />
           <el-table-column label="创建人" prop="operatorName" width="240" />
           <el-table-column label="创建时间" prop="operationTime" width="240" />
-          <el-table-column label="操作" width="300" fixed="right">
+          <el-table-column label="操作" width="250" fixed="right">
             <template #default="{ row }">
               <section class="actions">
                 <el-button type="primary" link @click="openCheckDrawer(row)">查看</el-button>
@@ -34,46 +43,75 @@
             </template>
           </el-table-column>
         </el-table>
-
-        <section class="paginationPosition">
-          <el-pagination
-            background
-            :layout="DEFAULT_PAGINATION_LAYOUT"
-            :page-sizes="PAGE_SIZE_CONFIG"
-            :total="total"
-            v-model:page-size="personGroupListRequestParams.pageSize"
-            v-model:current-page="personGroupListRequestParams.pageNumber"
-            @change="queryPersonGroupList"
-          />
-        </section>
-        <PersonGroupEditDrawer :title="drawerTitle" ref="editDrawerInstance" @submitted="onSubmit" />
-        <PersonGroupExhibitionDrawer :title="drawerTitle" ref="exhibitionDrawerInstance" />
       </div>
+
+      <section class="paginationPosition">
+        <el-pagination
+          background
+          :layout="DEFAULT_PAGINATION_LAYOUT"
+          :page-sizes="PAGE_SIZE_CONFIG"
+          :total="total"
+          v-model:page-size="personGroupListRequestParams.pageSize"
+          v-model:current-page="personGroupListRequestParams.pageNumber"
+          @change="queryPersonGroupList"
+        />
+      </section>
+      <PersonGroupEditDrawer :title="drawerTitle" ref="editDrawerInstance" @submitted="onSubmit" />
+      <PersonGroupExhibitionDrawer :title="drawerTitle" ref="exhibitionDrawerInstance" />
+      <BatchImport
+        :visible="batchImportVisible"
+        :importApiUrl="importApiUrl"
+        :templateUrl="templateUrl"
+        :templateName="'导入创建分组模版'"
+        responseType="simple"
+        @close="handleClose"
+        @update="handleUpdate"
+      />
     </div>
   </VerticalFlexLayout>
 </template>
 
 <script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import { ElMessage } from 'element-plus';
   import { PlusOutlined } from '@vicons/antd';
+  import { DocumentAdd } from '@element-plus/icons-vue';
+  import { msgConfirm } from '@/utils/element-plus/messageBox';
+  import { DEFAULT_PAGINATION_LAYOUT, PAGE_SIZE_CONFIG } from '@/constant/pagination';
+  import { PERM_NOTICE } from '@/types/permission/constants';
   import { PersonGroupListItem } from '@/types/person-group/type';
-  import { deleteUserGroup } from '@/api/system/person-group';
   import usePersonGroupListQuery from './hooks/usePersonGroupListQuery';
   import PersonGroupEditDrawer from './components/PersonGroupEditDrawer.vue';
-  import { onMounted, ref } from 'vue';
   import PersonGroupExhibitionDrawer from './components/PersonGroupExhibitionDrawer.vue';
-  import { ElMessage } from 'element-plus';
-  import { DEFAULT_PAGINATION_LAYOUT, PAGE_SIZE_CONFIG } from '@/constant/pagination';
-  import { msgConfirm } from '@/utils/element-plus/messageBox';
-
-  import { PERM_NOTICE } from '@/types/permission/constants';
   import VerticalFlexLayout from '@/components/VerticalFlexLayout.vue';
   import Breadcrumb from '@/components/Breadcrumb.vue';
+  import { deleteUserGroup } from '@/api/system/person-group';
+  import { BatchImport } from '@/components/batch-import';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
 
   const { personGroupListRequestParams, personGroupList, total, queryPersonGroupList } = usePersonGroupListQuery();
 
   const drawerTitle = ref('');
   const editDrawerInstance = ref();
   const exhibitionDrawerInstance = ref();
+
+  // 批量导入
+  const batchImportVisible = ref(false);
+  const { urlPrefix } = useGlobSetting();
+  const importApiUrl = ref(urlJoin(urlPrefix, '/userGroup/importGroupUserList'));
+  const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-addUserGroup-template.xlsx');
+
+  const handleUpdate = () => {
+    // update 事件只用于更新数据,不关闭弹窗
+    queryPersonGroupList();
+  };
+
+  const handleClose = () => {
+    // close 事件用于关闭弹窗
+    batchImportVisible.value = false;
+  };
+
   function openEditDrawer(row?: PersonGroupListItem) {
     drawerTitle.value = row ? '编辑人员分组' : '新建人员分组';
     editDrawerInstance.value?.open(row);
@@ -83,6 +121,7 @@
     drawerTitle.value = '查看人员分组';
     exhibitionDrawerInstance.value?.open(row);
   }
+
   function deleteGroup(row: PersonGroupListItem) {
     msgConfirm('确认删除,删除后无法恢复,确认删除吗?', '提示', {
       type: 'warning',
@@ -106,3 +145,29 @@
     queryPersonGroupList();
   });
 </script>
+
+<style lang="scss" scoped>
+  .content {
+    padding: 20px;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow: hidden;
+  }
+
+  .toolbar {
+    margin-bottom: 20px;
+    flex-shrink: 0;
+  }
+
+  .table-wrapper {
+    flex: 1;
+    overflow: hidden;
+    min-height: 0;
+  }
+
+  .paginationPosition {
+    flex-shrink: 0;
+    margin-top: 20px;
+  }
+</style>

+ 80 - 8
src/views/system/person-group/components/PersonGroupEditDrawer.vue

@@ -8,7 +8,7 @@
           :autosize="{ minRows: 1, maxRows: 2 }"
           autocomplete="off"
           placeholder="请输入15字以内分组名称"
-        ></el-input>
+        />
       </el-form-item>
       <el-form-item label="分组描述" prop="description">
         <el-input
@@ -19,7 +19,7 @@
           show-word-limit
           autocomplete="off"
           placeholder="请输入50字以内分组描述"
-        ></el-input>
+        />
       </el-form-item>
       <el-form-item label="组内成员" prop="userList" :rules="[{ required: true, message: '组内成员不能为空' }]">
         <el-select
@@ -36,9 +36,10 @@
             :value="user"
           />
         </el-select>
-        <p
-          >共<span>&nbsp;{{ total }}&nbsp;</span>人</p
-        >
+        <div class="member-count">
+          <span>共 {{ total }} 人</span>
+          <span v-if="isEditing" class="import-member-list" @click="batchImportVisible = true">导入成员名单</span>
+        </div>
       </el-form-item>
       <el-form-item label="操作人" prop="operator">
         <el-input v-model="operater" disabled="true" />
@@ -56,17 +57,28 @@
     align-center
     :close-on-click-modal="false"
     style="height: 583px"
-    :width="731"
+    :width="950"
     :destroy-on-close="true"
     class="workShopDialog"
   >
     <PersonGroupFilter
       ref="dialogInstance"
       :init-selected="selectedUser"
-      @cancel="handleDialogCancle"
+      @cancel="handleDialogCancel"
       @submit="handleDialogSubmit"
     />
   </el-dialog>
+  <BatchImport
+    :visible="batchImportVisible"
+    :importApiUrl="importApiUrl"
+    :templateUrl="templateUrl"
+    :templateName="'导入名单模版'"
+    :data="{ userGroupId: formData.id }"
+    responseType="detailed"
+    @close="() => (batchImportVisible = false)"
+    @update="handleUpdate"
+    style="z-index: 10000"
+  />
 </template>
 
 <script setup lang="ts">
@@ -83,6 +95,10 @@
   import { useUserStore } from '@/store/modules/user';
   import PersonGroupFilter from '@/components/PersonSelector/PersonGroupFilter.vue';
   import { storeToRefs } from 'pinia';
+  import { BatchImport } from '@/components/batch-import';
+  import type { ImportResponseDataPerson, ImportResponseData } from '@/components/batch-import/types';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
 
   defineProps<{
     title: string;
@@ -97,6 +113,47 @@
 
   const selectedUser = ref<PersonGroupItem[]>([]);
 
+  // 批量导入
+  const batchImportVisible = ref(false);
+  const { urlPrefix } = useGlobSetting();
+  const importApiUrl = ref(urlJoin(urlPrefix, '/userGroup/importUserGroupMembers'));
+  const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-groupUserList-template.xlsx');
+
+  const handleUpdate = (data?: ImportResponseData | ImportResponseDataPerson | string) => {
+    // 处理批量导入返回的 successMembers
+    if (data && typeof data === 'object' && 'successMembers' in data) {
+      const importData = data as ImportResponseDataPerson;
+      if (importData.successMembers && importData.successMembers.length > 0) {
+        // 将 successMembers 转换为 PersonGroupItem 格式
+        const newMembers: PersonGroupItem[] = importData.successMembers.map((member) => ({
+          checked: true,
+          id: member.id,
+          realname: member.realname,
+          deptId: member.deptId,
+          deptName: member.deptName,
+          staffNo: member.staffNo,
+          jobName: member.jobName,
+        }));
+
+        // 获取现有成员的 id 集合,用于去重
+        const existingIds = new Set(selectedUser.value.map((user) => user.id));
+
+        // 过滤掉已存在的成员,只添加新成员
+        const membersToAdd = newMembers.filter((member) => !existingIds.has(member.id));
+
+        if (membersToAdd.length > 0) {
+          // 合并到现有列表
+          selectedUser.value = [...selectedUser.value, ...membersToAdd];
+          formData.userList = [...formData.userList, ...membersToAdd];
+          total.value = formData.userList.length;
+          ElMessage.success(`成功导入 ${membersToAdd.length} 名成员`);
+        } else {
+          ElMessage.info('导入的成员已全部存在于当前列表中');
+        }
+      }
+    }
+  };
+
   // 表单相关
   let defaultFormData: PersonGroupForm = {
     id: null,
@@ -161,7 +218,7 @@
     reset();
   };
 
-  const handleDialogCancle = () => {
+  const handleDialogCancel = () => {
     dialogOpened.value = false;
   };
   const handleDialogSubmit = (selectedData: PersonGroupItem[]) => {
@@ -185,6 +242,7 @@
             realname: x.realname,
             deptName: x.deptName,
             staffNo: x.staffNo,
+            jobName: x.jobName,
           } as PersonGroupItem;
         }),
       };
@@ -207,3 +265,17 @@
     open,
   });
 </script>
+
+<style scoped lang="scss">
+  .member-count {
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .import-member-list {
+      color: $primary-color;
+      cursor: pointer;
+    }
+  }
+</style>

+ 55 - 14
src/views/system/person-group/components/PersonGroupExhibitionDrawer.vue

@@ -1,21 +1,33 @@
 <template>
   <el-drawer :title="title" v-model="drawerOpened" destroy-on-close>
-    <div v-for="(item, index) in personList" :key="item.id">
-      <div class="person-group-item">{{ `${item.realname}   (${item.staffNo})     ${item.deptName}` }}</div>
+    <div class="drawer-content">
+      <div class="export-button-wrapper">
+        <el-button class="export-button" type="primary" @click="handleExport">导出分组名单</el-button>
+      </div>
+      <el-table :data="personList" style="width: 100%" height="600">
+        <el-table-column prop="staffNo" label="工号" show-overflow-tooltip />
+        <el-table-column prop="realname" label="姓名" show-overflow-tooltip />
+        <el-table-column prop="deptName" label="部门" show-overflow-tooltip>
+          <template #default="{ row }">
+            {{ row.deptName ? row.deptName : '--' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="jobName" label="岗位" show-overflow-tooltip>
+          <template #default="{ row }">
+            {{ row.jobName ? row.jobName : '--' }}
+          </template>
+        </el-table-column>
+      </el-table>
     </div>
   </el-drawer>
 </template>
 
 <script setup lang="ts">
-  import { reactive, ref, computed, watch } from 'vue';
-  import {
-    PersonGroupForm,
-    AddPersonGroupParams,
-    EditPersonGroupParams,
-    PersonGroupItem,
-    PersonGroupListItem,
-  } from '@/types/person-group/type';
-  import { addUserGroup, modifyUserGroup, queryUserGroupDetail } from '@/api/system/person-group';
+  import { ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { downloadByData } from '@/utils/file/download';
+  import { PersonGroupItem, PersonGroupListItem } from '@/types/person-group/type';
+  import { queryUserGroupDetail, exportUserGroupList } from '@/api/system/person-group';
 
   defineProps<{
     title: string;
@@ -23,22 +35,51 @@
 
   const drawerOpened = ref(false);
 
+  const userGroupId = ref<number>(0);
   const personList = ref<PersonGroupItem[]>([]);
 
   const open = async (row: PersonGroupListItem) => {
+    userGroupId.value = row.id;
     const res = await queryUserGroupDetail(row.id);
     personList.value = res.userList;
     drawerOpened.value = true;
   };
 
+  // 导出
+  const handleExport = () => {
+    exportUserGroupList(userGroupId.value).then(async (responnse) => {
+      if (!responnse) {
+        throw new Error('下载文件失败');
+      }
+      downloadByData(responnse, '导出分组名单.xlsx');
+      ElMessage.success('下载文件成功');
+    });
+  };
+
   defineExpose({
     open,
   });
 </script>
 
 <style lang="scss" scoped>
-  .person-group-item {
-    margin-bottom: 10px;
-    white-space: pre-wrap;
+  .drawer-content {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+  }
+
+  .export-button-wrapper {
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    background: #fff;
+    margin-bottom: 20px;
+    display: flex;
+    justify-content: flex-end;
+  }
+
+  :deep(.el-table) {
+    flex: 1;
+    overflow: auto;
   }
 </style>

+ 41 - 18
src/views/system/role/role.vue

@@ -20,24 +20,26 @@
         <el-button type="primary" @click="openDrawer()" v-permission="PERM_USER.ROLE"> 添加角色 </el-button>
       </div>
 
-      <el-table :data="roleList">
-        <el-table-column label="角色ID" width="100" prop="id" />
-        <el-table-column label="角色名称" prop="roleName" width="480" />
-        <el-table-column label="备注" prop="remark" width="800" />
-        <el-table-column label="创建时间" width="240" prop="createdAt" />
-        <el-table-column label="操作" width="200" fixed="right">
-          <template #default="{ row }">
-            <section class="actions">
-              <img src="@/assets/icons/edit.png" @click="openDrawer(row)" v-permission="PERM_USER.ROLE" />
-              <img
-                src="@/assets/icons/delete.png"
-                @click="deleteRole(row.id)"
-                v-permission="{ action: [PERM_USER.ROLE] }"
-              />
-            </section>
-          </template>
-        </el-table-column>
-      </el-table>
+      <div class="table-wrapper">
+        <el-table :data="roleList" height="100%" class="role-table">
+          <el-table-column label="角色ID" width="100" prop="id" />
+          <el-table-column label="角色名称" prop="roleName" width="480" />
+          <el-table-column label="备注" prop="remark" width="800" />
+          <el-table-column label="创建时间" width="240" prop="createdAt" />
+          <el-table-column label="操作" min-width="120" fixed="right">
+            <template #default="{ row }">
+              <section class="actions">
+                <img src="@/assets/icons/edit.png" @click="openDrawer(row)" v-permission="PERM_USER.ROLE" />
+                <img
+                  src="@/assets/icons/delete.png"
+                  @click="deleteRole(row.id)"
+                  v-permission="{ action: [PERM_USER.ROLE] }"
+                />
+              </section>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
 
       <section class="paginationPosition">
         <el-pagination
@@ -117,10 +119,31 @@
 
   .content {
     padding: 20px;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow: hidden;
   }
+
   .toolbar {
     margin-bottom: 20px;
     display: flex;
     justify-content: space-between;
+    flex-shrink: 0;
+  }
+
+  .table-wrapper {
+    flex: 1;
+    overflow: hidden;
+    min-height: 0;
+  }
+
+  .role-table {
+    height: 100%;
+  }
+
+  .paginationPosition {
+    flex-shrink: 0;
+    margin-top: 20px;
   }
 </style>

+ 0 - 4
src/views/system/user/component/SearchForm.vue

@@ -41,7 +41,6 @@
 
 <script setup lang="ts">
   import { ref } from 'vue';
-  import { Filter, Refresh } from '@element-plus/icons-vue';
   import { FormInstance } from 'element-plus';
   import { queryTypeSelect } from '../constant';
   import { OptionsProps } from '../types';
@@ -118,7 +117,4 @@
   .el-form--inline .el-form-item {
     margin-right: 40px;
   }
-  .el-form {
-    padding: 20px 0 0 20px;
-  }
 </style>

+ 75 - 62
src/views/system/user/user.vue

@@ -12,35 +12,28 @@
         @get-table-data="onSearchCommit"
         @reset-form="onResetForm"
       />
-      <div class="content-wrapper">
-        <div v-permission="PERM_USER.ACCOUNT_MANAGE">
-          <el-button type="primary" @click="openAddSingleDrawer">
-            <template #icon>
-              <el-icon>
-                <Plus />
-              </el-icon>
-            </template>
-            添加用户
-          </el-button>
-
-          <el-button color="#1890FF" @click="openAddMultipleDrawer" style="margin-left: 18px" plain>
-            <template #icon>
-              <el-icon>
-                <DocumentAdd />
-              </el-icon>
-            </template>
-            批量导入
-          </el-button>
-        </div>
+      <div v-permission="PERM_USER.ACCOUNT_MANAGE" class="toolbar">
+        <el-button type="primary" @click="openAddSingleDrawer">
+          <template #icon>
+            <el-icon>
+              <Plus />
+            </el-icon>
+          </template>
+          添加用户
+        </el-button>
+
+        <el-button color="#1890FF" @click="openAddMultipleDrawer" style="margin-left: 18px" plain>
+          <template #icon>
+            <el-icon>
+              <DocumentAdd />
+            </el-icon>
+          </template>
+          批量导入
+        </el-button>
+      </div>
 
-        <el-table :data="userList" row-key="id" style="margin-top: 10px">
-          <el-table-column label="工号" prop="staffNo" width="200">
-            <template #default="scope">
-              <div>
-                <span>{{ scope.row.staffNo }}</span>
-              </div>
-            </template>
-          </el-table-column>
+      <div class="table-wrapper">
+        <el-table :data="userList" row-key="id" height="100%">
           <el-table-column label="登录账号" prop="username" width="240">
             <template #default="scope">
               <div class="account">
@@ -59,9 +52,23 @@
               </div>
             </template>
           </el-table-column>
+          <el-table-column label="工号" prop="staffNo" width="200">
+            <template #default="scope">
+              <div>
+                <span>{{ scope.row.staffNo }}</span>
+              </div>
+            </template>
+          </el-table-column>
           <el-table-column label="姓名" prop="realname" width="200" />
+          <el-table-column label="岗位" prop="jobList" width="200">
+            <template #default="scope">
+              <div>
+                {{ tranformJobList(scope.row.jobList) }}
+              </div>
+            </template>
+          </el-table-column>
           <el-table-column label="手机" prop="mobile" width="200" />
-          <el-table-column label="状态" prop="isDisabled" width="200">
+          <el-table-column label="状态" prop="isDisabled" width="120">
             <template #default="scope">
               <div>
                 <el-tag :type="!scope.row.isDisabled ? 'success' : 'danger'">
@@ -70,7 +77,7 @@
               </div>
             </template>
           </el-table-column>
-          <el-table-column label="角色" prop="roleList" width="240">
+          <el-table-column label="角色" prop="roleList" min-width="400">
             <template #default="scope">
               <div>
                 {{ tranformRoleList(scope.row.roleList) }}
@@ -90,19 +97,9 @@
             <template #default="scope">
               <el-space v-if="scope.row.roleType !== RoleTypeEnum.SUPER_ADMIN">
                 <div class="el-space el-space--horizontal">
-                  <!-- <div
-                  class="el-space__item"
-                  @click="handleEdit(scope.row)"
-                  v-permission="{ action: [PERM_USER.ACCOUNT_EDIT] }"
-                > -->
                   <div class="el-space__item" @click="handleEdit(scope.row)" v-permission="PERM_USER.ACCOUNT_MANAGE">
                     <div><img :src="editIcon" class="el-tooltip__trigger" /></div>
                   </div>
-                  <!-- <div
-                  class="el-space__item"
-                  @click="handleDelete(scope.row)"
-                  v-permission="{ action: [PERM_USER.ACCOUNT_DELETE] }"
-                > -->
                   <div class="el-space__item" @click="handleDelete(scope.row)" v-permission="PERM_USER.ACCOUNT_MANAGE">
                     <div><img :src="deleteIcon" class="el-tooltip__trigger" /></div>
                   </div>
@@ -118,20 +115,20 @@
             </template>
           </el-table-column>
         </el-table>
-
-        <section class="paginationPosition">
-          <el-pagination
-            background
-            :layout="DEFAULT_PAGINATION_LAYOUT"
-            :page-sizes="PAGE_SIZE_CONFIG"
-            :total="total"
-            v-model:page-size="params.pageSize"
-            v-model:current-page="params.pageNumber"
-            @change="loadPageData"
-          />
-        </section>
       </div>
 
+      <section class="paginationPosition">
+        <el-pagination
+          background
+          :layout="DEFAULT_PAGINATION_LAYOUT"
+          :page-sizes="PAGE_SIZE_CONFIG"
+          :total="total"
+          v-model:page-size="params.pageSize"
+          v-model:current-page="params.pageNumber"
+          @change="loadPageData"
+        />
+      </section>
+
       <CreateDrawer
         ref="createDrawerRef"
         :title="drawerTitle"
@@ -167,7 +164,7 @@
   import { ResultEnum } from '@/enums/httpEnum';
   import { useTargetTenantIdSetting } from '@/utils/useTargetTenantIdSetting';
   import { useUserStore } from '@/store/modules/user';
-  import type { RoleListItem } from '@/api/system/user-operate';
+  import type { JobListItem, RoleListItem } from '@/api/system/user-operate';
   import SearchForm from './component/SearchForm.vue';
   import AddUser from './component/AddUser.vue';
   import CreateDrawer from './CreateDrawer.vue';
@@ -333,6 +330,11 @@
     loadPageData();
   };
 
+  const tranformJobList = (arr: JobListItem[]) => {
+    if (arr && arr.length === 0) return '--';
+    return arr?.map((item) => item.idtJobName).join(',');
+  };
+
   const tranformRoleList = (arr: RoleListItem[]) => {
     if (arr && arr.length === 0) return '--';
     return arr?.map((item) => item.roleName).join(',');
@@ -346,25 +348,36 @@
 <style lang="scss" scoped>
   .user-page {
     position: relative;
-    /* height: calc(100vh - 64px - 12px); */
-    background-color: #ffffff;
+    padding: 20px;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow: hidden;
   }
 
-  .content-wrapper {
-    padding: 20px;
-    padding-top: 0;
+  .toolbar {
+    margin-bottom: 20px;
+    display: flex;
+    flex-shrink: 0;
+  }
+
+  .table-wrapper {
+    flex: 1;
+    overflow: hidden;
+    min-height: 0;
   }
 
-  .user-list {
-    padding: 0 21px;
+  .paginationPosition {
+    flex-shrink: 0;
+    margin-top: 20px;
   }
 
   .add-popover {
     position: absolute;
     width: 593px;
     height: 435px;
-    left: 50%;
-    top: 50%;
+    left: 45%;
+    top: 45%;
     margin-top: -218px;
     margin-left: -297px;
     z-index: 99;

+ 7 - 2
src/views/traffic/accident/Accident.vue

@@ -116,7 +116,7 @@
     :importApiUrl="importApiUrl"
     :templateUrl="templateUrl"
     :templateName="'交通事故记录-批量导入模版'"
-    @close="() => (batchImportVisible = false)"
+    @close="handleClose"
     @update="handleUpdate"
   />
 </template>
@@ -294,10 +294,15 @@
   const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-traffic-template.xlsx');
 
   const handleUpdate = () => {
-    batchImportVisible.value = false;
+    // update 事件只用于更新数据,不关闭弹窗
     getTableData();
   };
 
+  const handleClose = () => {
+    // close 事件用于关闭弹窗
+    batchImportVisible.value = false;
+  };
+
   const getTableData = () => {
     tableConfig.loading = true;
     accidentTableQuery.queryParam = {

+ 7 - 2
src/views/traffic/vehicle/Vehicle.vue

@@ -99,7 +99,7 @@
     :importApiUrl="importApiUrl"
     :templateUrl="templateUrl"
     :templateName="'车辆信息管理-批量导入模版'"
-    @close="() => (batchImportVisible = false)"
+    @close="handleClose"
     @update="handleUpdate"
   />
 </template>
@@ -277,10 +277,15 @@
   const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-vehicle-template.xlsx');
 
   const handleUpdate = () => {
-    batchImportVisible.value = false;
+    // update 事件只用于更新数据,不关闭弹窗
     getTableData();
   };
 
+  const handleClose = () => {
+    // close 事件用于关闭弹窗
+    batchImportVisible.value = false;
+  };
+
   const getTableData = () => {
     tableConfig.loading = true;
     vehicleTableQuery.queryParam = {

+ 10 - 6
src/views/traffic/violation/act/Act.vue

@@ -87,8 +87,8 @@
             ref="basicTableRef"
             :tableData="tableData"
             :tableConfig="tableConfig"
-            @update:pageSize="handleSizeChange"
-            @update:pageNumber="handleCurrentChange"
+            @update:page-size="handleSizeChange"
+            @update:page-number="handleCurrentChange"
             @update:selection="handleSelectionChange"
           >
             <template #violateName="scope">
@@ -156,7 +156,7 @@
       :importApiUrl="importApiUrl"
       :templateUrl="templateUrl"
       :templateName="'违规行为记录-批量导入模版'"
-      @close="() => (batchImportVisible = false)"
+      @close="handleClose"
       @update="handleUpdate"
     />
   </div>
@@ -172,7 +172,6 @@
   import { TABLE_OPTIONS, VIOLATION_ACT_TABLE_COLUMNS, VIOLATION_ACT_TABLE_COLUMNS_CHECKONLY } from './configs/tables';
   import {
     ACT_NOTICE_DATA_SOURCE_LABEL,
-    ACT_VIOLATION_TYPE,
     ACT_VIOLATION_TYPE_LABEL,
     ACT_TABLE_SEARCH_OPTIONS,
     ACT_VIOLATION_TYPE_OPTIONS,
@@ -187,7 +186,7 @@
   import { useRouter } from 'vue-router';
   import { openMessageBox } from '@/utils/element-plus/messageBox';
   import type { QueryPageRequest } from '@/types/basic-query';
-  import type { ActTableSearch, ActTableQuery, ActTableData, UpdateActQuery } from './types';
+  import type { ActTableSearch, ActTableQuery, ActTableData } from './types';
   import {
     getActTableList,
     noticeActData,
@@ -348,10 +347,15 @@
   const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-violation-template.xlsx');
 
   const handleUpdate = () => {
-    batchImportVisible.value = false;
+    // update 事件只用于更新数据,不关闭弹窗
     getTableData();
   };
 
+  const handleClose = () => {
+    // close 事件用于关闭弹窗
+    batchImportVisible.value = false;
+  };
+
   onMounted(async () => {
     await getTableData();
   });