@gsp-sys/common-personnel-select-workbench
v0.2.0
Published
通用人员选择工作台组件,支持选人、选组织、选岗位、选用户分组四种模式
Readme
@gsp-sys/common-personnel-select-workbench 使用手册
用户/岗位/组织/用户分组选择弹窗组件,提供双面板(树导航+分页列表)和单面板(树表格)两种交互模式。 基于 Vue 3 + @farris/ui-vue,支持 ESM / CJS / TypeScript 类型声明。
目录
安装
1. 安装依赖
# peer 依赖(宿主项目必须已安装)
pnpm add vue @farris/ui-vue
# 安装本包
pnpm add @gsp-sys/common-personnel-select-workbench2. 全局注册 Farris UI(main.ts)
本组件依赖 @farris/ui-vue 的全局组件(f-modal、f-tree-grid、f-data-grid 等),必须在应用入口全局注册:
import { createApp } from 'vue'
import FarrisUI from '@farris/ui-vue'
import '@farris/ui-vue/dist/style.css'
import App from './App.vue'
const app = createApp(App)
app.use(FarrisUI)
app.mount('#app')3. 引入组件样式
在使用的页面或全局引入组件 CSS:
import '@gsp-sys/common-personnel-select-workbench/dist/style.css'快速开始
<template>
<f-button @click="visible = true">选择用户</f-button>
<LookupDetailDialog
v-model:visible="visible"
source-type="user"
@confirm="handleConfirm"
@cancel="visible = false"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LookupDetailDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/dist/style.css'
const visible = ref(false)
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
console.log('选中用户:', data.items)
visible.value = false
}
</script>组件:LookupDetailDialog
Props
| Prop | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|:----:|------|
| visible | boolean | — | ✅ | 控制弹窗显隐,支持 v-model:visible |
| sourceType | 'user' \| 'position' \| 'org' \| 'usergroup' | — | ✅ | 数据源类型:user=选择用户,position=选择岗位,org=选择组织,usergroup=选择用户分组 |
| navType | 'org' \| 'group' \| 'all' | user→all,position→group | | 左侧导航类型。all=组织+分组双 tab,org=仅组织,group=仅分组 |
| title | string | 自动生成 | | 弹窗标题。默认:user→「选择用户」,position→「选择岗位」,org→「选择组织」,usergroup→「选择用户分组」 |
| multiSelect | boolean | true | | 是否多选 |
| keepSelect | boolean | true | | 单击行选中后再次点击是否取消选中 |
| initSelectedIds | string[] | [] | | 初始选中 ID 列表,打开弹窗时自动标记为选中 |
| selectedIdList | string[] | [] | | 已选中数据 ID 列表(与 initSelectedIds 合并使用) |
| initSelectedItems | any[] | [] | | 初始选中项的完整对象列表,确保确认时不丢失未加载的已选项 |
| listFilter | Record<string, any> | {} | | 额外的列表过滤条件(如权限过滤) |
| pagination | boolean | true | | 是否分页 |
| pageIndex | number | 1 | | 首次加载页码 |
| pageSize | number | 20 | | 每页条数 |
| pageList | number[] | [20, 30, 50, 100, 200, 500] | | 可选每页条数列表 |
| total | number | 0 | | 总记录数(外部预置) |
| groupId | string | '' | | 指定初始分组 ID(打开时自动选中该分组节点) |
| isShowSearch | boolean | true | | 是否显示搜索栏 |
| disableRowFn | (row: any, index: number) => boolean | — | | 禁用某些行的选择 |
| treeFilter | Record<string, any> | {} | | 树过滤条件 |
| treeCols | any[] | — | | 自定义树列定义 |
| gridCols | any[] | — | | 自定义表格列定义 |
| isOnSecurity | boolean | false | | 是否开启三员 |
| showStopBtn | boolean | false | | 是否显示停用/启用按钮(用户模式) |
| allRoot | boolean | true | | 组织是否显示所有根节点 |
| allRootGroup | boolean | true | | 分组是否显示所有根节点 |
Events
| 事件 | Payload | 说明 |
|------|---------|------|
| update:visible | (val: boolean) | 弹窗显隐变化,用于 v-model:visible |
| confirm | (data: { items: any[]; hasLoadList: any[] }) | 点击确认时触发。items=选中的对象数组,hasLoadList=所有已加载的数据 |
| cancel | — | 点击取消时触发 |
confirm payload 说明:
interface ConfirmPayload {
/** 本次选中的完整对象数组 */
items: Array<{ id: string; code: string; name: string; [key: string]: any }>
/** 弹窗期间所有加载过的数据(含未选中),用于后续差分提交 */
hasLoadList: Array<{ id: string; code: string; name: string; [key: string]: any }>
}用户模式(sourceType="user")
左侧显示组织导航和用户分组导航两个 tab,右侧显示用户分页列表。
- 点击组织节点 → 加载该组织下的用户
- 点击用户分组节点 → 加载该分组下的用户
- 支持搜索(编号或名称)
- 支持分页
- 跨页选中保持
默认列:用户编号、用户名称、隶属组织
岗位模式(sourceType="position")
左侧仅显示岗位分组导航一个 tab,右侧显示岗位分页列表。
默认列:岗位编号、岗位名称、岗位分组、隶属组织
组织模式(sourceType="org")
单面板布局:弹窗内仅包含搜索栏和组织树表格(f-tree-grid),无左侧导航面板、无分页列表。
- 通过
getOrgByLayer(1, 'org')加载根节点,点击展开节点时懒加载子节点 - 仅支持单选(点击行选中,无 checkbox)
- 搜索栏对已加载节点做本地过滤(按
code或name匹配,保留匹配节点及其祖先路径) - 不适用的 Props(
navType、showStopBtn、multiSelect、keepSelect、listFilter等)静默忽略
confirm 返回的 items 为选中节点的原始数据对象(含 id、code、name、parentId)。
用户分组模式(sourceType="usergroup")
单面板布局:弹窗内仅包含搜索栏和用户分组树表格(f-tree-grid),无左侧导航面板、无分页列表。
- 通过
getUserGroupByLayer(1)加载根节点,点击展开节点时懒加载子节点 - 仅支持单选(点击行选中,无 checkbox)
- 搜索栏对已加载节点做本地过滤(按
code或name匹配,保留匹配节点及其祖先路径) - 不适用的 Props(
navType、showStopBtn、multiSelect、keepSelect、listFilter等)静默忽略
confirm 返回的 items 为选中节点的原始数据对象(含 id、code、name、parentId)。
典型场景
场景一:选择用户(多选)
<template>
<f-button @click="visible = true">选择用户</f-button>
<LookupDetailDialog
v-model:visible="visible"
source-type="user"
:multi-select="true"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LookupDetailDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/dist/style.css'
const visible = ref(false)
const selectedUsers = ref<any[]>([])
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
selectedUsers.value = data.items
visible.value = false
}
</script>场景二:选择岗位(多选)
<template>
<f-button @click="visible = true">选择岗位</f-button>
<LookupDetailDialog
v-model:visible="visible"
source-type="position"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LookupDetailDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/dist/style.css'
const visible = ref(false)
const selectedPositions = ref<any[]>([])
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
selectedPositions.value = data.items
visible.value = false
}
</script>场景三:编辑时回显已选
打开弹窗时需要恢复之前选中的项。传入 initSelectedIds(ID 列表)和 initSelectedItems(完整对象):
<template>
<f-button @click="openDialog">编辑成员</f-button>
<LookupDetailDialog
v-model:visible="visible"
source-type="user"
:init-selected-ids="selectedIds"
:init-selected-items="selectedItems"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LookupDetailDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/dist/style.css'
const visible = ref(false)
// 已选中的 ID 列表
const selectedIds = ref<string[]>(['user-001', 'user-002'])
// 已选中的完整对象(确保未加载到列表中的项也不会丢失)
const selectedItems = ref<any[]>([
{ id: 'user-001', code: 'U001', name: '张三' },
{ id: 'user-002', code: 'U002', name: '李四' }
])
function openDialog() {
visible.value = true
}
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
selectedIds.value = data.items.map(item => item.id)
selectedItems.value = data.items
visible.value = false
}
</script>为什么需要
initSelectedItems?弹窗分页加载时,某些已选用户可能不在当前页的数据中。
initSelectedItems将这些对象预注入内部hasLoadList,确保点击确认时能正确匹配到对应数据,不会丢失。
场景四:单选模式
<LookupDetailDialog
v-model:visible="visible"
source-type="user"
:multi-select="false"
@confirm="handleConfirm"
/>单选模式下,表格不显示 checkbox,点击行即选中。
场景五:自定义过滤条件
通过 listFilter 传入权限过滤等条件:
<LookupDetailDialog
v-model:visible="visible"
source-type="user"
:list-filter="userFilter"
@confirm="handleConfirm"
/>// 对标 Angular userFilter
const userFilter: Record<string, any> = {
withPermission: true, // 启用权限过滤
authOp: 'UserManage', // 权限操作标识
notGetInit: true, // 排除初始数据
notGetCurrentUser: true // 排除当前登录用户
}岗位模式的过滤条件:
const positionFilter: Record<string, any> = {
withPermission: true,
authOp: 'positionManage',
notGetInit: true
}场景六:禁用特定行
通过 disableRowFn 禁用某些行的选择:
<LookupDetailDialog
v-model:visible="visible"
source-type="user"
:disable-row-fn="checkDisabled"
@confirm="handleConfirm"
/>// 禁用状态为「停用」的行
function checkDisabled(row: any, index: number): boolean {
return row.state === 0 // state=0 表示停用
}场景七:选择组织(单面板)
<template>
<f-button @click="visible = true">选择组织</f-button>
<LookupDetailDialog
v-model:visible="visible"
source-type="org"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LookupDetailDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/dist/style.css'
const visible = ref(false)
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
if (data.items.length > 0) {
console.log('选中组织:', data.items[0])
// { id, code, name, parentId }
}
visible.value = false
}
</script>单面板模式特点:
- 仅显示树表格,无左侧导航和右侧列表
- 单选(点击行选中,再次点击其他行切换选中)
- 搜索为本地过滤,仅覆盖已加载节点
multiSelect、navType等 Props 被静默忽略
场景八:选择用户分组(单面板)
<template>
<f-button @click="visible = true">选择用户分组</f-button>
<LookupDetailDialog
v-model:visible="visible"
source-type="usergroup"
:init-selected-ids="selectedId"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LookupDetailDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/dist/style.css'
const visible = ref(false)
const selectedId = ref<string[]>(['group-001'])
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
if (data.items.length > 0) {
console.log('选中用户分组:', data.items[0])
selectedId.value = [data.items[0].id]
}
visible.value = false
}
</script>API 函数
包内嵌了以下 API 函数,可直接导入使用,也可供消费方做差分提交等操作。
导入
import {
getOrgByLayer,
getOrgByParentId,
getUserGroupByLayer,
getUserGroupByParentId,
getPositionGroupByLayer,
getPositionGroupByParentId,
getAllUsers,
getPositionsByGroupId
} from '@gsp-sys/common-personnel-select-workbench'组织 API
// 按层级获取组织树节点
getOrgByLayer(layer: number, type?: string, filter?: Record<string, any>): Promise<any>
// 按父 ID 获取子组织节点
getOrgByParentId(parentId: string, type?: string, filter?: Record<string, any>): Promise<any>示例:
// 加载第一层组织
const orgs = await getOrgByLayer(1, 'org')
// 加载某组织的子节点
const children = await getOrgByParentId('org-001', 'org')用户 API
// 获取用户列表(分页 + 过滤)
getAllUsers(filter?: Record<string, any>): Promise<any>示例:
const res = await getAllUsers({
pageIndex: 1,
pageSize: 20,
orgId: 'org-001',
withPermission: true,
codeOrName: '张' // 搜索关键词
})
// res 可能是数组或 { data: [], totalCount: 100 }
const users = Array.isArray(res) ? res : res?.data || []
const total = res?.totalCount || 0用户分组 API
// 按层级获取用户分组树节点
getUserGroupByLayer(layer: number, filter?: Record<string, any>): Promise<any>
// 按父 ID 获取子分组节点
getUserGroupByParentId(parentId: string, filter?: Record<string, any>): Promise<any>岗位分组 API
// 按层级获取岗位分组树节点
getPositionGroupByLayer(layer: number, filter?: Record<string, any>): Promise<any>
// 按父 ID 获取子分组节点
getPositionGroupByParentId(parentId: string, filter?: Record<string, any>): Promise<any>
// 根据分组 ID 获取岗位列表
getPositionsByGroupId(filter: Record<string, any>): Promise<any>示例:
// 加载岗位分组树(带权限过滤)
const groups = await getPositionGroupByLayer(1, {
withPermission: true,
authOp: 'positionManage'
})
// 获取某分组下的岗位列表
const positions = await getPositionsByGroupId({
groupId: 'group-001',
pageIndex: 1,
pageSize: 20,
withPermission: true,
authOp: 'positionManage'
})Composable:useFTreeGridLoader
用于 f-tree-grid 组件的懒加载数据管理,封装了根节点加载、子节点懒加载、节点 ID 解析、防重复加载等逻辑。
导入
import { useFTreeGridLoader } from '@gsp-sys/common-personnel-select-workbench'
import type { FTreeGridLoaderOptions, FTreeGridLoaderReturn } from '@gsp-sys/common-personnel-select-workbench'类型定义
interface FTreeGridLoaderOptions<T = any> {
/** 加载根节点(第一层) */
fetchRootNodes: () => Promise<T[] | { data: T[] }>
/** 根据 parentId 加载子节点 */
fetchChildNodes: (parentId: string) => Promise<T[] | { data: T[] }>
/** 将原始数据项转换为 f-tree-grid 节点格式 */
convertToTreeNode: (item: T) => any
}
interface FTreeGridLoaderReturn {
/** 树数据源 */
treeData: Ref<any[]>
/** 树组件引用(绑定到 f-tree-grid 的 ref) */
treeRef: Ref<any>
/** f-tree-grid 的 :loadData 回调 */
loadData: (treeNode: any) => Promise<void>
/** 初始化/重新加载根节点 */
initTree: () => Promise<void>
/** 懒加载子节点(兼容旧调用模式) */
loadChildren: (parentId: string) => Promise<any[]>
/** 解析树节点 ID(兼容 f-tree-grid 嵌套数据结构) */
resolveTreeNodeId: (treeNode: any) => string
/** 在嵌套树中递归查找节点 */
findNodeInTree: (nodes: any[], nodeId: string) => any | null
/** 将加载到的子节点同步回本地 treeData 缓存 */
syncChildrenToTreeData: (parentId: string, children: any[]) => void
}用法
<template>
<f-tree-grid
ref="treeRef"
:data="treeData"
:hierarchy="{ hasChildrenField: 'hasChildren', collapseField: 'collapse' }"
:columns="columns"
:loadData="loadData"
:fit="true"
:fitColumns="true"
@selectItem="handleSelect"
/>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useFTreeGridLoader, getOrgByLayer, getOrgByParentId } from '@gsp-sys/common-personnel-select-workbench'
const columns = [
{ field: 'code', title: '编号', dataType: 'string', width: '40%' },
{ field: 'name', title: '名称', dataType: 'string', width: '60%' }
]
function convertToTreeNode(item: any) {
return {
id: item.id,
data: {
id: item.id,
code: item.code || '',
name: item.name || '',
parentId: item.parentId || null,
layer: item.layer
},
hasChildren: item.isDetail !== true && item.leaf !== true,
children: []
}
}
const { treeRef, treeData, loadData, initTree } = useFTreeGridLoader({
fetchRootNodes: () => getOrgByLayer(1, 'org'),
fetchChildNodes: (parentId) => getOrgByParentId(parentId, 'org'),
convertToTreeNode
})
onMounted(() => {
initTree()
})
function handleSelect(event: any) {
if (event?.raw?.data) {
console.log('选中节点:', event.raw.data)
}
}
</script>注意事项
1. 必须引入 CSS
组件使用 scoped 样式 + :deep() 穿透 Farris UI。如果不引入 CSS,弹窗布局会错乱:
import '@gsp-sys/common-personnel-select-workbench/dist/style.css'2. baseURL 为相对路径
API 层的 baseURL 为 /api/runtime/sys/v1.0(相对路径)。宿主项目需配置代理将 /api 转发到后端网关。
3. SessionId 认证
API 拦截器会自动从 sessionStorage.getItem('sessionId') 读取会话 ID,并在请求头中添加 SessionId 字段。宿主项目需确保登录后将 sessionId 写入 sessionStorage。
4. 跨页选中保持
组件内置了跨页选中保持逻辑:
- 维护全局
selectedIds(Set) - 分页切换时,当前页已选中的行会自动恢复选中状态
updateDataSource触发的空选中事件会被抑制,防止误清
5. hasLoadList 与差分提交
confirm 事件的 payload 包含 hasLoadList(所有已加载过的数据)。可用于差分提交场景:
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
// items: 本次选中的完整对象
const selectedIds = new Set(data.items.map(item => item.id))
// 差分:从 hasLoadList 中找出新增和移除的
const addList = data.hasLoadList.filter(item => selectedIds.has(item.id))
const removeList = data.hasLoadList.filter(item => !selectedIds.has(item.id))
// 调用后端差分接口
await modifyMembersGroup({
addList,
removeList
})
}6. TypeScript 类型导入
import type { LookupDetailDialogProps } from '@gsp-sys/common-personnel-select-workbench'
import type { FTreeGridLoaderOptions, FTreeGridLoaderReturn } from '@gsp-sys/common-personnel-select-workbench'7. 单面板模式(org / usergroup)的 Props 处理
当 sourceType 为 org 或 usergroup 时,组件采用单面板布局,以下 Props 被静默忽略(不报错、不生效):
navType、showStopBtn、multiSelect、keepSelect、listFilterpagination、pageIndex、pageSize、pageList、totalgroupId、disableRowFn、treeFilter、treeCols、gridCols
单面板模式强制单选,搜索为本地过滤(仅覆盖已加载节点)。
