ソースを参照

Merge branch 'dev-wyf' into 'dev'

人员分组组件

See merge request product-group-fe/sfy-safety-group/sfy-safety!19
楼航飞 11 ヶ月 前
コミット
ee3f9c0e18

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

@@ -0,0 +1,74 @@
+import { http } from '@/utils/http/axios';
+import {
+  AddPersonGroupParams,
+  EditPersonGroupParams,
+  QueryPersonGroupPageParams,
+  QueryAvailablePersonPageParams,
+  QueryPersonGroupPageRes,
+  QueryPersonGroupDetailRes,
+  QueryAvailablePersonPageRes,
+} from '@/types/person-group/type';
+
+/**
+ * @description: 新增用户组
+ */
+export function addUserGroup(params: AddPersonGroupParams) {
+  return http.request({
+    url: `/userGroup/addUserGroup`,
+    method: 'POST',
+    data: params,
+  });
+}
+
+/**
+ * @description: 编辑用户组
+ */
+export function modifyUserGroup(params: EditPersonGroupParams) {
+  return http.request({
+    url: `/userGroup/modifyUserGroup`,
+    method: 'PUT',
+    data: params,
+  });
+}
+
+/**
+ * @description: 删除用户组
+ */
+export function deleteUserGroup(userGroupId: number) {
+  return http.request({
+    url: `/userGroup/deleteUserGroup?userGroupId=${userGroupId}`,
+    method: 'DELETE',
+  });
+}
+
+/**
+ * @description: 查询用户组分页
+ */
+export function queryUserGroupPage(params: QueryPersonGroupPageParams): Promise<QueryPersonGroupPageRes> {
+  return http.request({
+    url: `/userGroup/queryUserGroupPage`,
+    method: 'POST',
+    data: params,
+  });
+}
+
+/**
+ * @description: 查询用户组详情
+ */
+export function queryUserGroupDetail(userGroupId: number): Promise<QueryPersonGroupDetailRes> {
+  return http.request({
+    url: `/userGroup/queryUserGroupDetail?userGroupId=${userGroupId}`,
+    method: 'GET',
+  });
+}
+
+/**
+ * @description: 查询可添加到用户组的用户
+ */
+export function queryAvailableUserList(params: QueryAvailablePersonPageParams): Promise<QueryAvailablePersonPageRes> {
+  return http.request({
+    url: `/admin/user/queryAvailableUserList`,
+    method: 'POST',
+    data: params,
+  });
+}

BIN
src/assets/images/person-group-empty.png


+ 0 - 0
src/types/person-group/constant.ts


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

@@ -0,0 +1,104 @@
+import type { PaginationRequest, PaginationResponse } from '@/types/common/type';
+
+/** 用户组编辑表单 */
+export interface PersonGroupForm {
+  id?: number | null;
+  /*用户组名称 */
+  name: string;
+  /*用户组描述 */
+  description?: string;
+  /*用户组成员 */
+  userList: PersonGroupItem[];
+}
+
+/** 用户组编辑提交 */
+export interface PersonGroupSubmitForm {
+  id?: number | null;
+  /*用户组名称 */
+  name: string;
+  /*用户组描述 */
+  description?: string;
+  /*用户组成员 */
+  userIdList: number[];
+}
+
+/** 新增用户组列表请求参数 */
+export interface AddPersonGroupParams extends PersonGroupSubmitForm {
+  /*人数 */
+  total: number;
+}
+
+/** 编辑用户组列表请求参数 */
+export interface EditPersonGroupParams extends AddPersonGroupParams {
+  userGroupId: number;
+}
+
+/** 查询用户组列表请求参数 */
+export interface QueryPersonGroupPageParams extends PaginationRequest {
+  /*查询参数 */
+  queryParam?: string;
+}
+
+/** 查询可选用户列表请求参数 */
+export interface QueryAvailablePersonPageParams extends PaginationRequest {
+  /*查询参数 */
+  queryParam: {
+    [key: string]: string | undefined;
+    realname?: string;
+    deptName?: string;
+    staffNo?: string;
+  };
+}
+
+/** 用户组列表展示信息 */
+export interface PersonGroupListItem {
+  /*用户组id */
+  id: number;
+  /*用户组名称 */
+  name: string;
+  /*分组描述 */
+  description: string;
+  /*人数 */
+  total: number;
+  /*操作人姓名 */
+  operatorName: string;
+  /*操作时间 */
+  operationTime: string;
+}
+
+/** 查询用户组列表后端返回data */
+export type QueryPersonGroupPageRes = PaginationResponse<PersonGroupListItem>;
+
+/** 用户组用户信息 */
+export interface PersonGroupItem {
+  checked?: boolean;
+  /*自增主键 */
+  id: number;
+  /*姓名 */
+  realname: string;
+  /*部门id */
+  deptId?: number;
+  /*部门名称 */
+  deptName?: string;
+  /*工号 */
+  staffNo: string;
+}
+
+/** 查询用户组详情后端返回data */
+export interface QueryPersonGroupDetailRes {
+  /*用户组名称 */
+  name: string;
+  /*用户组描述 */
+  description: string;
+  /*用户列表 */
+  userList: PersonGroupItem[];
+  /*人数 */
+  total: number;
+  /*操作人姓名 */
+  operator: string;
+  /*用户分组id */
+  userGroupId: number;
+}
+
+/** 查询可选用户列表后端返回data */
+export type QueryAvailablePersonPageRes = PaginationResponse<PersonGroupItem>;

+ 97 - 1
src/views/system/person-group/PersonGroup.vue

@@ -1 +1,97 @@
-<template>人员分组 </template>
+<template>
+  <div>
+    <el-card>
+      <template #header>
+        <el-button type="primary" @click="openEditDrawer()">
+          <template #icon>
+            <el-icon>
+              <PlusOutlined />
+            </el-icon>
+          </template>
+          新建人员分组
+        </el-button>
+      </template>
+
+      <el-table height="calc(100vh - 256px)" :data="personGroupList">
+        <el-table-column label="分组名" width="300" prop="name" />
+        <el-table-column label="分组描述" prop="description" />
+        <el-table-column label="人员数量" width="200" prop="total" />
+        <el-table-column label="创建人" width="200" prop="operatorName" />
+        <el-table-column label="创建时间" width="200" prop="operationTime" />
+        <el-table-column label="操作" width="240">
+          <template #default="{ row }">
+            <section class="actions">
+              <el-button type="primary" link @click="openCheckDrawer(row)">查看</el-button>
+              <el-button type="primary" link @click="openEditDrawer(row)">编辑</el-button>
+              <el-button type="primary" link @click="deleteGroup(row)">删除</el-button>
+            </section>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <section class="mt-4 flex justify-end">
+        <el-pagination
+          background
+          layout="total, sizes, prev, pager, next"
+          :page-sizes="[10, 30, 50]"
+          :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" />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { PlusOutlined } from '@vicons/antd';
+  import { QueryPersonGroupPageParams, 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, ElMessageBox } from 'element-plus';
+
+  const { personGroupListRequestParams, personGroupList, total, setRequestParams, queryPersonGroupList } =
+    usePersonGroupListQuery();
+
+  const drawerTitle = ref('');
+  const editDrawerInstance = ref();
+  const exhibitionDrawerInstance = ref();
+  function openEditDrawer(row?: PersonGroupListItem) {
+    drawerTitle.value = row ? '编辑人员分组' : '新建人员分组';
+    editDrawerInstance.value?.open(row);
+  }
+
+  function openCheckDrawer(row: PersonGroupListItem) {
+    drawerTitle.value = '查看人员分组';
+    exhibitionDrawerInstance.value?.open(row);
+  }
+  function deleteGroup(row: PersonGroupListItem) {
+    ElMessageBox.confirm('确认删除,删除后无法恢复,确认删除吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+    })
+      .then(() => {
+        deleteUserGroup(row.id).then(() => {
+          queryPersonGroupList();
+          ElMessage.success('删除成功');
+        });
+      })
+      .catch((e) => {
+        ElMessage.error(e);
+      });
+  }
+
+  const onSubmit = () => {
+    queryPersonGroupList();
+  };
+
+  onMounted(() => {
+    queryPersonGroupList();
+  });
+</script>

+ 196 - 0
src/views/system/person-group/components/PersonGroupEditDrawer.vue

@@ -0,0 +1,196 @@
+<template>
+  <el-drawer :title="title" v-model="drawerOpened" @close="reset">
+    <el-form label-width="80px" :model="formData" :rules="formRules" ref="formInstance">
+      <el-form-item label="分组名称" prop="name">
+        <el-input
+          v-model="formData.name"
+          type="textarea"
+          :autosize="{ minRows: 1, maxRows: 2 }"
+          autocomplete="off"
+          placeholder="请输入15字以内分组名称"
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="分组描述" prop="description">
+        <el-input
+          v-model="formData.description"
+          type="textarea"
+          :rows="4"
+          maxlength="50"
+          show-word-limit
+          autocomplete="off"
+          placeholder="请输入50字以内分组描述"
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="组内成员" prop="userList" :rules="[{ required: true, message: '组内成员不能为空' }]">
+        <el-select
+          placeholder="请添加组内人员"
+          v-model="formData.userList"
+          value-key="id"
+          multiple
+          @click="dialogOpened = true"
+        >
+          <el-option
+            v-for="user in selectedUser"
+            :key="user.id"
+            :label="user.staffNo + '-' + user.realname"
+            :value="user"
+          />
+        </el-select>
+        <p
+          >共<span>&nbsp;{{ total }}&nbsp;</span>人</p
+        >
+      </el-form-item>
+      <el-form-item label="操作人" prop="operator">
+        <el-input v-model="operater" disabled="true" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="reset">重置</el-button>
+      <el-button type="primary" @click="submit">提交</el-button>
+    </template>
+  </el-drawer>
+  <el-dialog
+    v-model="dialogOpened"
+    title="添加人员"
+    align-center
+    :close-on-click-modal="false"
+    style="height: 583px"
+    :width="731"
+    :destroy-on-close="true"
+    class="workShopDialog"
+  >
+    <PersonGroupFilter
+      ref="dialogInstance"
+      :init-selected="selectedUser"
+      @cancel="handleDialogCancle"
+      @submit="handleDialogSubmit"
+    />
+  </el-dialog>
+</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 { FormRules, FormInstance, ElMessage } from 'element-plus';
+  import { useUserStore } from '@/store/modules/user';
+  import PersonGroupFilter from './PersonGroupFilter.vue';
+  import { storeToRefs } from 'pinia';
+
+  defineProps<{
+    title: string;
+  }>();
+
+  const emits = defineEmits<{
+    (e: 'submitted'): void; // 提交之后触发的事件
+  }>();
+
+  const drawerOpened = ref(false);
+  const dialogOpened = ref(false);
+
+  const selectedUser = ref<PersonGroupItem[]>([]);
+
+  // 表单相关
+  const defaultFormData = (): PersonGroupForm => ({
+    id: null,
+    name: '',
+    description: '',
+    userList: [],
+  });
+  const useUser = useUserStore();
+  const { info } = storeToRefs(useUser);
+  const operater = ref(info.value.realname);
+
+  const total = ref<number>(0);
+
+  const formData = reactive<PersonGroupForm>(defaultFormData());
+  const formRules: FormRules = {
+    name: { required: true, trigger: 'blur', message: '请填写分组名称' },
+    userList: { required: true, trigger: 'blur', message: '请选择组内成员' },
+  };
+  const formInstance = ref<FormInstance>();
+  const isEditing = computed(() => formData.id != null);
+
+  /** 重置表单 */
+  const reset = () => {
+    formInstance.value?.resetFields();
+    Object.assign(formData, defaultFormData());
+  };
+  /** 提交表单 */
+  const submit = async () => {
+    try {
+      await formInstance.value?.validate();
+      if (isEditing.value) {
+        await modifyUserGroup({
+          name: formData.name,
+          description: formData.description,
+          userIdList: formData.userList.map((x) => x.id),
+          total: formData.userList.length,
+          userGroupId: formData.id!,
+        } as EditPersonGroupParams);
+      } else {
+        await addUserGroup({
+          name: formData.name,
+          description: formData.description,
+          userIdList: formData.userList.map((x) => x.id),
+          total: formData.userList.length,
+        } as AddPersonGroupParams);
+      }
+      drawerOpened.value = false;
+      ElMessage.success('提交成功');
+      emits('submitted');
+    } catch (e) {
+      console.error(e);
+    }
+  };
+
+  const handleDialogCancle = () => {
+    dialogOpened.value = false;
+  };
+  const handleDialogSubmit = (selectedData: PersonGroupItem[]) => {
+    selectedUser.value = selectedData;
+    formData.userList = selectedData;
+    total.value = formData.userList.length;
+    dialogOpened.value = false;
+  };
+
+  const open = async (row?: PersonGroupListItem) => {
+    if (row) {
+      const res = await queryUserGroupDetail(row.id);
+      formData.id = res.userGroupId;
+      formData.name = res.name;
+      formData.description = res.description;
+      formData.userList = res.userList.map((x) => {
+        return {
+          checked: true,
+          id: x.id,
+          realname: x.realname,
+          deptName: x.deptName,
+          staffNo: x.staffNo,
+        } as PersonGroupItem;
+      });
+      selectedUser.value = formData.userList;
+    }
+    drawerOpened.value = true;
+  };
+
+  watch(
+    () => formData.userList,
+    (newUserList) => {
+      total.value = newUserList.length;
+      const ids = newUserList.map((user) => user.id);
+      selectedUser.value = selectedUser.value.filter((user) => ids.includes(user.id));
+    },
+  );
+
+  defineExpose({
+    open,
+  });
+</script>

+ 44 - 0
src/views/system/person-group/components/PersonGroupExhibitionDrawer.vue

@@ -0,0 +1,44 @@
+<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>
+  </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';
+
+  defineProps<{
+    title: string;
+  }>();
+
+  const drawerOpened = ref(false);
+
+  const personList = ref<PersonGroupItem[]>([]);
+
+  const open = async (row: PersonGroupListItem) => {
+    const res = await queryUserGroupDetail(row.id);
+    personList.value = res.userList;
+    drawerOpened.value = true;
+  };
+
+  defineExpose({
+    open,
+  });
+</script>
+
+<style scoped>
+  .person-group-item {
+    margin-bottom: 10px;
+    white-space: pre-wrap;
+  }
+</style>

+ 218 - 0
src/views/system/person-group/components/PersonGroupFilter.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="person-filter-selection">
+    <div class="left">
+      <div class="filter-title">
+        <el-form-item>
+          <el-input v-model="personFilterValue" style="width: 253px" placeholder="请输入搜索的内容" clearable>
+            <template #prepend>
+              <el-select v-model="personFilterType" style="width: 74px" value-key="type" :validate-event="false">
+                <el-option v-for="item in FILTER_TYPES" :key="item.type" :value="item" :label="item.label" />
+              </el-select>
+            </template>
+          </el-input>
+          <div style="float: right">
+            <el-button type="primary" @click="debouncedHandleFilter" style="width: 65px; height: 32px; margin: 0 11px 0"
+              >搜 索</el-button
+            >
+          </div>
+        </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">
+          <el-checkbox
+            v-model="person.checked"
+            :label="person.staffNo + '-' + person.realname + '&nbsp' + '(' + person.deptName + ')'"
+            @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">
+        <img src="@/assets/images/person-group-empty.png" alt="" />
+        <div style="position: relative; top: -50px">请选择类型并搜索人员</div>
+      </div>
+      <div v-else class="filter-result-empty"> <div>未搜索到相关内容</div><div>请重新搜索</div> </div>
+    </div>
+    <div class="right">
+      <div class="head">
+        <span style="font-weight: 400; font-size: 16px; color: rgba(0, 0, 0, 0.88); line-height: 22px"
+          >已选择:{{ selectedPersonList.length }}人</span
+        >
+      </div>
+      <div class="selected">
+        <el-tag
+          v-for="person in selectedPersonList"
+          :key="person.id"
+          closable
+          @close="handleRemoveSelectedPerson(person.id)"
+        >
+          {{ person.staffNo + '-' + person.realname }}
+        </el-tag>
+      </div>
+    </div>
+  </div>
+  <div class="footer">
+    <el-button @click="handleCancle"> 取消 </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 { usePersonGroupFilter } from '../hooks/usePersonGroupFilter';
+  import { PersonGroupItem } from '@/types/person-group/type';
+  import { debounce } from 'lodash-es';
+
+  const {
+    FILTER_TYPES,
+    personFilterType,
+    personFilterValue,
+    personFilterList,
+    selectedPersonList,
+    getPersonFilterList,
+    getNextPersonFilterList,
+    handleAddSelectedPerson,
+    handleRemoveSelectedPerson,
+  } = usePersonGroupFilter();
+
+  const emit = defineEmits(['cancel', 'submit']);
+
+  const props = defineProps<{
+    initSelected?: PersonGroupItem[];
+  }>();
+
+  onMounted(() => {
+    if (props.initSelected) selectedPersonList.value = [...props.initSelected];
+  });
+
+  let observer: IntersectionObserver;
+
+  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);
+      }
+    });
+  };
+
+  const debouncedHandleFilter = debounce(handleFilter, 500);
+
+  const handleSelect = (v: boolean, person: any) => {
+    if (v) {
+      handleAddSelectedPerson(person);
+    } else {
+      handleRemoveSelectedPerson(person.id);
+    }
+  };
+  const handleCancle = () => {
+    emit('cancel');
+  };
+  const handleSubmit = () => {
+    emit('submit', selectedPersonList.value);
+  };
+
+  onBeforeUnmount(() => {
+    if (observer) {
+      observer.disconnect();
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .person-filter-selection {
+    display: flex;
+    width: 100%;
+    height: 98%;
+    .left {
+      display: flex;
+      flex-direction: column;
+      width: 50%;
+      height: 100%;
+      border-right: 1px solid rgba(0, 0, 0, 0.06);
+      // .filter-title {
+      // }
+      .filter-result {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+        height: 392px;
+        overflow: auto;
+        // .filter-result-item {
+        //   display: flex;
+        //   align-items: center;
+        //   font-weight: 500;
+        //   font-size: 14px;
+        //   color: #303133;
+        //   line-height: 22px;
+        // }
+      }
+      .filter-result-empty {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        height: 392px;
+        font-weight: 400;
+        font-size: 14px;
+        color: #999999;
+      }
+    }
+    .right {
+      display: flex;
+      flex-direction: column;
+      flex: 1;
+      height: 100%;
+      position: relative;
+      margin-left: 16px;
+      .head {
+        display: flex;
+        align-items: center;
+        font-weight: 400;
+        font-size: 16px;
+        color: rgba(0, 0, 0, 0.88);
+        line-height: 22px;
+        margin: 6px 0 6px;
+      }
+      .selected {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+        overflow-y: auto;
+        max-height: calc(100% - 120px);
+      }
+    }
+  }
+  .footer {
+    display: flex;
+    gap: 6px;
+    justify-content: flex-end;
+    padding-right: 16px;
+  }
+</style>

+ 21 - 0
src/views/system/person-group/hooks/usePersonGroupEditQuery.ts

@@ -0,0 +1,21 @@
+import { DEFAULT_PAGE_SIZE } from '@/types/common/constants';
+import {
+  AddPersonGroupParams,
+  EditPersonGroupParams,
+  PersonGroupListItem,
+  QueryPersonGroupDetailRes,
+} from '@/types/person-group/type';
+import { queryUserGroupPage } from '@/api/system/person-group';
+import { ref } from 'vue';
+import { cloneDeep } from 'lodash-es';
+
+const emptyPersonGroupDetail: AddPersonGroupParams = {
+  name: '',
+  description: '',
+  userList: [],
+  total: 0,
+};
+
+export default function usePersonGroupEditQuery() {
+  const requestParams = ref<AddPersonGroupParams | EditPersonGroupParams>(cloneDeep(emptyPersonGroupDetail));
+}

+ 97 - 0
src/views/system/person-group/hooks/usePersonGroupFilter.ts

@@ -0,0 +1,97 @@
+import {
+  QueryAvailablePersonPageParams,
+  QueryAvailablePersonPageRes,
+  PersonGroupItem,
+} from '@/types/person-group/type';
+import { queryAvailableUserList } from '@/api/system/person-group';
+import { ref } from 'vue';
+
+export const usePersonGroupFilter = () => {
+  const FILTER_TYPES = [
+    { type: 'realname', label: '姓名' },
+    { type: 'staffNo', label: '工号' },
+    { type: 'deptName', label: '部门' },
+  ];
+
+  // 复用输入框类型及标签
+  const personFilterType = ref({ type: 'realname', label: '姓名' });
+  // 输入框绑定数据
+  const personFilterValue = ref<string>();
+  // 查询参数
+  const personFilterParams = ref<QueryAvailablePersonPageParams>({
+    pageNumber: 1,
+    pageSize: 10,
+    queryParam: {},
+  });
+  // 查询事件
+  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;
+    // 如果有返回且搜索到则添加选中标记
+    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);
+  };
+  // 查询结果
+  const personFilterList = ref<QueryAvailablePersonPageRes>();
+  // 已选择人员
+  const selectedPersonList = ref<PersonGroupItem[]>([]);
+  // 查询结果添加选中标记
+  const checkPersonList = (list: PersonGroupItem[]) => {
+    return list.map((item) => {
+      return {
+        ...item,
+        checked: selectedPersonList.value.some((person) => person.id === item.id),
+      };
+    });
+  };
+
+  // 查询结果刷新选中标记
+  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 handleAddSelectedPerson = (person: PersonGroupItem) => {
+    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);
+  };
+  return {
+    FILTER_TYPES,
+    personFilterType,
+    personFilterValue,
+    personFilterParams,
+    personFilterList,
+    selectedPersonList,
+    getPersonFilterList,
+    getNextPersonFilterList,
+    handleAddSelectedPerson,
+    handleRemoveSelectedPerson,
+  };
+};
+
+export default usePersonGroupFilter;

+ 46 - 0
src/views/system/person-group/hooks/usePersonGroupListQuery.ts

@@ -0,0 +1,46 @@
+import { DEFAULT_PAGE_SIZE } from '@/types/common/constants';
+import { QueryPersonGroupPageParams, PersonGroupListItem } from '@/types/person-group/type';
+import { queryUserGroupPage } from '@/api/system/person-group';
+import { ref } from 'vue';
+import { cloneDeep } from 'lodash-es';
+
+const defaultPersonGroupListRequest: QueryPersonGroupPageParams = {
+  pageNumber: 1,
+  pageSize: DEFAULT_PAGE_SIZE,
+};
+
+export default function usePersonGroupListQuery() {
+  const personGroupListRequestParams = ref(cloneDeep(defaultPersonGroupListRequest));
+  const personGroupList = ref<PersonGroupListItem[]>();
+  const total = ref(0);
+  const loading = ref(false);
+
+  const setRequestParams = (params: QueryPersonGroupPageParams) => {
+    Object.assign(personGroupListRequestParams.value, params);
+  };
+
+  const resetRequestParams = () => {
+    Object.assign(personGroupListRequestParams.value, defaultPersonGroupListRequest);
+  };
+
+  const queryPersonGroupList = async () => {
+    try {
+      loading.value = true;
+      const result = await queryUserGroupPage(personGroupListRequestParams.value);
+      personGroupList.value = result.records;
+      total.value = result.totalRow;
+    } catch (e) {
+      console.error(e);
+    } finally {
+      loading.value = false;
+    }
+  };
+  return {
+    personGroupListRequestParams,
+    personGroupList,
+    loading,
+    total,
+    setRequestParams,
+    queryPersonGroupList,
+  };
+}