@gsp-sys/common-personnel-select-workbench
v0.5.0
Published
用户/岗位选择弹窗组件,提供组织树导航 + 分页列表选择能力
Keywords
Readme
@gsp-sys/common-personnel-select-workbench
用户 / 岗位 / 组织 / 分组选择组件库,基于 Vue 3 + @farris/ui-vue。 按「交互范式」拆分为多个职责单一的组件,支持 ESM / CJS / TypeScript 类型声明。
✨ 特性
- 按交互范式拆分:纯树选择、双面板(树导航+列表)、字段级帮助各司其职,命名清晰、职责单一。
- 开箱即用:内置组织 / 用户分组 / 岗位分组 / 业务域 / 用户 / 岗位的取数 API,无需自行对接接口。
- 能力完整:懒加载树、分页列表、跨页选中保持、初始选中回显、本地搜索、权限过滤、禁用行。
- TypeScript 友好:提供完整的 Props / Events / 类型声明。
- 历史兼容:保留
LookupDetailDialog作为兼容资源,既有调用方零改动。
📦 组件一览
| 组件 | 交互范式 | 适用数据源 | 说明 |
|------|----------|-----------|------|
| TreeSelectDialog | 纯树选择弹窗 | org / usergroup / positiongroup / businessdomain | 树表格单选/多选,支持回显、过滤、组织树换根(filter.rootOrgId)、自定义列 |
| DualPanelSelectDialog | 双面板弹窗 | user / position | 左树导航 + 右分页列表,跨页选中,组织树可换根(navRootOrgId) |
| TreeSelectField | 字段级帮助 | org / usergroup / positiongroup / businessdomain(rolegroup 实验性) | 输入框 + 弹窗选择,v-model:selectedItems |
| PersonnelSelectDialog | 人员选择弹窗 | user(多页签) | 迁移自 Angular selection,组织 / 用户分组页签已实现 |
| PersonnelSelectField | 人员选择字段 | user(标签形态) | 标签可删除 / 拖拽排序,弹窗配置透传 |
| LookupDetailDialog | 兼容旧组件 | user / position / org / usergroup | 历史组件,双面板 + 单面板两种布局 |
🔧 安装
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/index.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/style.css'4. 注册 f-lookup 取数服务(仅 TreeSelectField 需要)
TreeSelectField 底层是 f-lookup,它自身不发请求,而是通过 Vue 依赖注入向宿主应用索取取数服务。本包已内置默认实现 SysLookupHttpService,因此有三种接入方式:
方式一:零配置(推荐) —— 无需任何额外代码,组件自动使用包内置服务:
<TreeSelectField source-type="org" v-model:selected-items="orgs" />方式二:宿主显式注入 —— 需要自定义取数逻辑,或希望与其他 f-lookup 组件共用同一实例时:
import { createApp } from 'vue'
import { F_LOOKUP_HTTP_SERVICE_TOKEN } from '@farris/ui-vue'
import { SysLookupHttpService } from '@gsp-sys/common-personnel-select-workbench'
import App from './App.vue'
const app = createApp(App)
app.provide(F_LOOKUP_HTTP_SERVICE_TOKEN, new SysLookupHttpService())方式三:单点覆盖 —— 通过 :http-service 为某个字段指定独立服务:
<TreeSelectField source-type="org" :http-service="myLookupService" />优先级:
httpServiceprop > 宿主provide的注入值 > 包内置SysLookupHttpService。服务接口约定(结构化类型
LookupHttpServiceLike;因@farris/ui-vue未从包根导出LookupHttpService类型,本包按方法签名做鸭子类型声明):getData(uri, params)必需,getSettings(id)/updateSettings(id, settings)按需实现。默认服务的请求约定:baseURL
/api/runtime/sys/v1.0,请求头SessionId取自sessionStorage.getItem('sessionId')。若宿主的会话存储方式不同,请用方式二或方式三接管。其余组件(
TreeSelectDialog/DualPanelSelectDialog/PersonnelSelectDialog/PersonnelSelectField)直接调用本包 API 函数,无需任何注入。
🚀 快速开始
<template>
<f-button @click="visible = true">选择用户</f-button>
<DualPanelSelectDialog
v-model:visible="visible"
source-type="user"
@confirm="handleConfirm"
@cancel="visible = false"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DualPanelSelectDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/style.css'
const visible = ref(false)
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
console.log('选中用户:', data.items)
visible.value = false
}
</script>📚 组件详解
1. TreeSelectDialog(纯树选择弹窗)
承接 org / usergroup / positiongroup 的树选择,底层为 f-modal + f-tree-grid,懒加载树节点。
<template>
<f-button @click="visible = true">选择组织</f-button>
<TreeSelectDialog
v-model:visible="visible"
source-type="org"
:multi-select="true"
:init-selected-ids="['org-001']"
:filter="{ withPermission: true }"
@confirm="handleConfirm"
@cancel="visible = false"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { TreeSelectDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/style.css'
const visible = ref(false)
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
console.log('选中节点:', data.items)
visible.value = false
}
</script>Props
| Prop | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|:----:|------|
| visible | boolean | — | ✅ | 控制弹窗显隐,支持 v-model:visible |
| sourceType | 'org' \| 'usergroup' \| 'positiongroup' \| 'businessdomain' | — | ✅ | 数据源类型 |
| title | string | 自动生成 | | 弹窗标题,默认按 sourceType 生成 |
| multiSelect | boolean | false | | 是否多选(单选时无 checkbox) |
| initSelectedIds | string[] | [] | | 初始选中 ID,打开时自动回显;确认时的选中集合以它(与 selectedIdList 合并)为准 |
| selectedIdList | string[] | [] | | 已选中 ID(与 initSelectedIds 合并) |
| filter | Record<string, any> | — | | 树过滤条件,传入各 by-layer/by-parent 接口;rootOrgId(仅 org)为前端私有参数——组织树换根,语义见下文「组织树换根」 |
| treeCols | TreeColumn[] | code/name | | 自定义树列定义 |
| disableRowFn | (row, index) => boolean | — | | 禁用特定节点(接收节点 data 对象) |
| initSelectedItems | SelectedItem[] | [] | | 初始选中完整对象,确保未加载的深层节点不丢失;须与 initSelectedIds 成对传入(只传对象不会建立勾选态,确认将返回空集合) |
行为说明:本弹窗不做级联勾选(勾父不带子,只落明确勾选的节点),需要“含下级”语义请由业务侧规则实现。
确认时不校验最小选择数:取消全部勾选后确认,items 为 [],调用方可据此表达“清空已选”。
(f-lookup 的 TreeSelectField 会在空选确认时提示“请选择一条记录”且不回调,两者此处行为不同。)
组织树换根(filter.rootOrgId)
传 rootOrgId(仅 org 类型)时,组织树以该组织为唯一树根,可选范围收敛到「该组织及其下级」;不传则保持全树根加载(既有行为)。
<TreeSelectDialog
v-model:visible="visible"
source-type="org"
:multi-select="true"
:filter="{ withPermission: true, authOp: 'UserManage', rootOrgId: 'org-001' }"
@confirm="handleConfirm"
/>- 根节点按 ID 直取(
GET /sysOrgs/{id},不经过数据权限过滤,调用方自行保证该组织对当前用户可见);子节点仍走parentId懒加载,并按filter的权限参数(withPermission/authOp)裁剪。 rootOrgId为前端私有参数:取数前会被剔除,不会随请求发给后端。
Events
| 事件 | Payload | 说明 |
|------|---------|------|
| update:visible | (val: boolean) | 弹窗显隐变化 |
| confirm | (data: ConfirmPayload) | 确认时触发,items=选中节点,hasLoadList=所有已加载节点 |
| cancel | — | 取消时触发 |
2. DualPanelSelectDialog(双面板选择弹窗)
承接 user / position 的「左树导航 + 右分页列表」,支持包含下级/仅当前、跨页选中、搜索、分页,以及列表 / 导航树过滤条件透传(如按“是否已分配”等场景参数过滤)。
<template>
<f-button @click="visible = true">选择岗位</f-button>
<DualPanelSelectDialog
v-model:visible="visible"
source-type="position"
nav-type="group"
:multi-select="true"
:position-options="{ groupId: 'group-001' }"
@confirm="handleConfirm"
@cancel="visible = false"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DualPanelSelectDialog } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/style.css'
const visible = ref(false)
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
console.log('选中岗位:', data.items)
visible.value = false
}
</script>Props
| Prop | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|:----:|------|
| visible | boolean | — | ✅ | 控制弹窗显隐,支持 v-model:visible |
| sourceType | 'user' \| 'position' | — | ✅ | 数据源类型 |
| navType | 'org' \| 'group' \| 'all' | 'all' | | 左侧导航类型:all=组织+分组双页签,org=仅组织,group=仅分组 |
| title | string | 自动生成 | | 弹窗标题 |
| multiSelect | boolean | true | | 是否多选 |
| keepSelect | boolean | true | | 点击行选中后再次点击是否取消 |
| pagination | boolean | true | | 是否分页 |
| pageIndex | number | 1 | | 首次加载页码 |
| pageSize | number | 20 | | 每页条数 |
| pageList | number[] | [20,30,50,100,200,500] | | 可选每页条数 |
| total | number | 0 | | 总记录数(外部预置) |
| initSelectedIds | string[] | [] | | 初始选中 ID 列表 |
| selectedIdList | string[] | [] | | 已选中 ID 列表 |
| initSelectedItems | SelectedItem[] | [] | | 初始选中完整对象,确保未加载项不丢失 |
| listFilter | Record<string, any> | {} | | 额外列表过滤条件 |
| navTreeFilter | Record<string, any> | — | | 左侧导航树(组织 / 分组)取数过滤,如 { withPermission: true, authOp: 'xxx' };对标老代码 treeFilter,须与 listFilter 传同一套授权参数;不传保持包内既有口径 |
| navRootOrgId | string | — | | 左侧组织导航树的根组织 ID:传值时组织树以该组织为唯一根(子节点仍懒加载 + 权限裁剪),列表范围同步收敛到「该组织及其下级」(未选节点 / 分组导航场景);不传保持全树根加载 |
| disableRowFn | (row, index) => boolean | — | | 禁用特定行 |
| positionOptions | PositionModeOptions | — | | position 模式专属配置(含 groupId,打开时自动选中该分组节点) |
授权口径(navTreeFilter 与 listFilter 成对设置)
老代码 SysLookupDetailComponent 有两个入参:treeFilter(左树取数)与 listFilter(右侧列表取数),
每个调用点都成对赋同一个授权码,且组织导航与分组导航绑的是同一个 treeFilter 对象:
| 老代码调用点 | treeFilter | listFilter |
|---|---|---|
| sysuser/gspuser-detail 授权岗位 | { withPermission: true, authOp: 'UserAssPosition' } | 同上 + notGetInit / positionType |
| sysposition/position-detail 岗位授权用户 | { withPermission: true, authOp: 'PositionAssUser' } | 同上 + notGetInit / notGetCurrentUser / enableUser |
本包沿用该约定:navTreeFilter 对应 treeFilter,包里不猜也不继承授权码,
调用方只给授权两位(withPermission / authOp),列表专属参数留在 listFilter 里。
两侧给了不同授权码时,用户看到的就是「左边树点得到、右边没数据」。
<!-- 推荐:从 listFilter 派生,两处不会各自漂移 -->
<DualPanelSelectDialog source-type="position" :list-filter="positionFilter" :nav-tree-filter="positionTreeFilter" />
<script setup lang="ts">
const positionFilter = reactive({ withPermission: true, authOp: 'UserAssPosition', notGetInit: true })
const positionTreeFilter = computed(() => ({
withPermission: positionFilter.withPermission,
authOp: positionFilter.authOp
}))
</script>PositionModeOptions
interface PositionModeOptions {
/** 初始选中的分组 ID(打开时自动选中该分组节点) */
groupId?: string
}Events
| 事件 | Payload | 说明 |
|------|---------|------|
| update:visible | (val: boolean) | 弹窗显隐变化 |
| confirm | (data: ConfirmPayload) | 确认时触发 |
| cancel | — | 取消时触发 |
✅ 关闭行为:弹窗底部「确定 / 取消」由
f-modal内置按钮承载(beforeClose默认为放行),点击后弹窗自动关闭并发update:visible(false),同时分别触发confirm/cancel事件,消费方无需手动设置visible = false。 ⚠️ 时序:update:visible(false)可能先于confirm到达。若在 visible 回调里清空了“当前操作上下文”(如当前选择的类型),confirm回调中不要再依赖它。
3. TreeSelectField(字段级帮助)
输入框 + 点击弹出树帮助弹窗,用于表单字段选择。基于 f-lookup(TREELIST 模式)实现,支持 v-model:selectedItems 双向绑定。
取数默认由本包内置的
SysLookupHttpService完成(org/tree、usergroup/tree、positiongroup/tree、business-domains/tree),无需宿主配置即可使用。 若宿主已provide(F_LOOKUP_HTTP_SERVICE_TOKEN, ...)或通过:http-service传入自定义实现,组件会优先使用它们(详见「安装与初始化 - 4. 注册 f-lookup 取数服务」)。
<template>
<TreeSelectField
source-type="org"
:single-select="true"
v-model:selected-items="selectedOrgs"
@after-confirm="onConfirm"
@clear="onClear"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { TreeSelectField } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/style.css'
const selectedOrgs = ref([])
function onConfirm(data: any) {
console.log('选中:', data)
}
function onClear() {
console.log('已清空')
}
</script>Props
| Prop | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|:----:|------|
| sourceType | 'org' \| 'usergroup' \| 'positiongroup' \| 'businessdomain' \| 'rolegroup' | — | ✅ | 数据源类型(rolegroup 为实验性透传,需后端先提供 rolegroup/tree) |
| title | string | 自动生成 | | 弹窗标题,默认按 sourceType 生成 |
| disabled | boolean | false | | 是否只读 |
| singleSelect | boolean | true | | 是否单选 |
| displayTxt | string | '' | | 初始显示文本 |
| selectedItems | SelectedItem[] | [] | | 已选完整数据,支持 v-model:selectedItems |
| filter | Record<string, any> | — | | 过滤条件,传入帮助弹窗 |
| asyncMode | boolean | false | | 是否分层异步加载(false 则全量加载) |
| expandLevel | number | 1 | | 树默认展开层级(0=全部收起,1=展开一级) |
| bindingData | { id; name; code? } | { id: '', name: '' } | | 已废弃:旧接口逗号串绑定,组件会回写该对象;新代码请用 selectedItems |
| withPermission | boolean | false | | 是否开启数据权限过滤 |
| enableToSelect | boolean | true | | 数据加载后是否回填选中现有值(不是“是否可以不选”) |
| enableCascade | boolean | false | | 是否开启级联选择(为 false 时弹窗底部的级联下拉一并隐藏,无法切换) |
| cascadeValue | 'both' \| 'up' \| 'down' \| 'disable' | 'down' | | 缺省级联模式,disable = 仅选择自身;需 enableCascade 为 true 才能在下拉里切换 |
| showCheckAll | boolean | false | | 是否显示全选 |
| showAllPathOrg | boolean | false | | 是否显示全路径组织 |
| useBeforeCloseEvent | boolean | false | | 是否启用关闭前验证事件 |
| beforeClose | (selectData) => Promise<{closeDialog, message?}> | — | | 关闭前验证回调 |
| httpService | LookupHttpServiceLike | — | | 单点覆盖取数服务;不传则用宿主注入值,再回退包内置实现 |
Events / Expose
| 事件 | Payload | 说明 |
|------|---------|------|
| update:selectedItems | (items: SelectedItem[]) | 已选项变化 |
| afterConfirm | (data: SelectedItem \| SelectedItem[]) | 确认后触发(单选返回对象,多选返回数组) |
| clear | (data?) | 清空时触发 |
组件暴露两个方法,可通过 ref 调用:
| 方法 | 说明 |
|------|------|
| showHelp() | 主动打开帮助弹窗 |
| clear() | 以“未选中任何值”清空字段,与用户点输入框清除图标同一条 @clear 链路(会触发 clear 与 afterConfirm([])) |
关于“允许不选”:f-lookup 弹窗确认对空集合有硬拦(
use-dialog.ts的handleSubmitValidation:selectedItems为空即提示“请选择一条记录”), 该判断早于beforeSelectData,没有任何 prop 可关(schema 里的required只供设计器做表单必填校验,运行时 composition 不引用它)。 需要“全部不选 / 清空已选”时,调clear()或改用TreeSelectDialog。
4. LookupDetailDialog(历史兼容)
历史组件,新代码建议按交互范式选用上述新组件。根据 sourceType 自动切换两种布局:
- 双面板模式(
user/position):左侧树导航 + 右侧分页列表 - 单面板模式(
org/usergroup):树表格 + 本地搜索
<LookupDetailDialog
v-model:visible="visible"
source-type="user"
:multi-select="true"
@confirm="handleConfirm"
@cancel="visible = false"
/>Props
| Prop | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|:----:|------|
| visible | boolean | — | ✅ | 控制弹窗显隐,支持 v-model:visible |
| sourceType | 'user' \| 'position' \| 'org' \| 'usergroup' | — | ✅ | 数据源类型 |
| navType | 'org' \| 'group' \| 'all' | user→all,position→group | | 左侧导航类型(仅双面板模式) |
| title | string | 自动生成 | | 弹窗标题 |
| multiSelect | boolean | true | | 是否多选(双面板和单面板模式均生效) |
| keepSelect | boolean | true | | 多选时再次点击已选中行是否保持选中(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(打开时自动选中该分组节点) |
| disableRowFn | (row, index) => boolean | — | | 禁用某些行的选择(仅双面板模式) |
用户模式(sourceType="user")
左侧显示组织导航和用户分组导航两个 tab,右侧显示用户分页列表。
- 点击组织节点 → 加载该组织下的用户
- 点击用户分组节点 → 加载该分组下的用户
- 支持搜索(编号或名称)、分页、跨页选中保持
- 默认列:用户编号、用户名称、隶属组织
岗位模式(sourceType="position")
左侧仅显示岗位分组导航一个 tab,右侧显示岗位分页列表。
- 默认列:岗位编号、岗位名称、岗位分组、隶属组织
组织模式(sourceType="org")/ 用户分组模式(sourceType="usergroup")
单面板布局:弹窗内仅包含搜索栏和树表格,无左侧导航面板、无分页列表。
- 懒加载树节点(点击展开时加载子节点)
multiSelect=true时显示 checkbox 支持多选;multiSelect=false时点击行单选- 搜索栏对已加载节点做本地过滤(按编号或名称匹配,保留匹配节点及其祖先路径)
- confirm 返回的
items为选中节点的原始数据对象(含id、code、name、parentId) - 不适用的 Props(
navType、listFilter、pagination等)静默忽略
Events
| 事件 | Payload | 说明 |
|------|---------|------|
| update:visible | (val: boolean) | 弹窗显隐变化 |
| confirm | (data: ConfirmPayload) | 确认时触发,items=选中对象数组,hasLoadList=所有已加载数据 |
| cancel | — | 取消时触发 |
5. PersonnelSelectDialog / PersonnelSelectField(人员选择,Angular selection 迁移)
迁移自 Angular selection 库:多页签选人,支持组织范围限定、包含本人 / 离职 / 停用开关、岗位过滤漏斗等场景配置。
一期已实现 organization / sysUserGroup 两个页签;recent / candidate / group / favorites 为占位页(二三期实现),配置了也会被忽略。
<template>
<f-button @click="visible = true">选择人员</f-button>
<PersonnelSelectDialog
v-model:visible="visible"
:multi-select="true"
show-tab-ids="organization,sysUserGroup"
:init-user-ids="'user-001,user-002'"
@confirm="handleConfirm"
/>
<!-- 字段形态:标签可删除 / 拖拽排序,弹窗配置同名透传 -->
<PersonnelSelectField v-model:selected-items="picked" :multi-select="true" placeholder="请选择人员" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { PersonnelSelectDialog, PersonnelSelectField } from '@gsp-sys/common-personnel-select-workbench'
import '@gsp-sys/common-personnel-select-workbench/style.css'
const visible = ref(false)
const picked = ref<any[]>([])
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
picked.value = data.items
visible.value = false
}
</script>PersonnelSelectDialog 主要 Props
| Prop | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| visible | boolean | — | 控制弹窗显隐,支持 v-model:visible(必填) |
| title | string | '' | 弹窗标题(未传时展示“选择人员”) |
| showTabIds | string | 'organization' | 展示哪些页签(逗号分隔,取值 recent / candidate / organization / sysUserGroup / group / favorites) |
| activeTabId | PersonnelTabId | 'organization' | 默认激活页签 |
| rememberTab | boolean | true | 是否记忆上次激活页签(localStorage,与 Angular 共享缓存结构) |
| multiSelect | boolean | true | 是否多选 |
| initSelectedItems | PersonnelSelectedItem[] | [] | 初始已选完整对象(优先于 initUserIds) |
| initUserIds | string | '' | 初始已选用户 ID(逗号分隔,兼容 Angular 形态) |
| orgId | string | '' | 限定组织范围 |
| includeChildHierarchy | boolean | true | 组织范围是否包含下级 |
| includeCurrentUser | boolean | true | 是否包含当前用户 |
| includeJobLeavers / includeStopUser / includeStopOrg | boolean | false | 是否包含离职人员 / 停用用户 / 停用组织 |
| filterPosition | boolean | false | 是否显示岗位过滤漏斗 |
| showOrgFilterIcon | boolean | false | 用户分组页签是否显示组织过滤 |
| personnelOrderField | PersonnelOrderField | '' | 人员排序方式 |
| pageSize | number | 20 | 每页条数 |
| getCurrentUserId | () => string | — | 当前用户 ID 提供者(排除本人场景) |
PersonnelSelectField 主要 Props
| Prop | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| selectedItems | PersonnelSelectedItem[] | [] | 已选完整对象,支持 v-model:selectedItems |
| title / placeholder | string | '' / '请选择' | 标题 / 占位文案 |
| multiSelect | boolean | true | 是否多选 |
| readonly / disabled | boolean | false | 只读 / 禁用 |
| draggable | boolean | true | 标签是否可拖拽排序 |
| displayField | string | 'name' | 表单展示字段 |
| formatFn | (item) => string | — | 自定义显示文本(优先级最高) |
| expression | string | '' | 表达式自定义显示文本,如 '{{code}}-{{name}}' |
| multipleChoiceSeparator | string | ',' | 多选 ID 输出分隔符(供调用方拼接) |
其余 props(showTabIds / activeTabId / rememberTab / orgId / includeChildHierarchy / includeCurrentUser / includeJobLeavers / includeStopUser / includeStopOrg / filterPosition / showOrgFilterIcon / personnelOrderField / pageSize)与 Dialog 同名透传。
Events
| 组件 | 事件 | Payload | 说明 |
|------|------|---------|------|
| PersonnelSelectDialog | update:visible | (val: boolean) | 弹窗显隐变化 |
| PersonnelSelectDialog | confirm | (data: ConfirmPayload) | 确认时触发 |
| PersonnelSelectDialog | cancel | — | 取消时触发 |
| PersonnelSelectField | update:selectedItems | (items: PersonnelSelectedItem[]) | 已选项变化 |
| PersonnelSelectField | afterConfirm | (items: PersonnelSelectedItem[]) | 弹窗确认后触发 |
| PersonnelSelectField | remove | (item: PersonnelSelectedItem) | 移除某一已选标签 |
| PersonnelSelectField | clear | — | 清空全部已选 |
💡 典型场景
场景一:选择用户(多选)
<DualPanelSelectDialog
v-model:visible="visible"
source-type="user"
:multi-select="true"
@confirm="handleConfirm"
/>场景二:选择岗位(多选)
<DualPanelSelectDialog
v-model:visible="visible"
source-type="position"
@confirm="handleConfirm"
/>场景三:编辑时回显已选
传入 initSelectedIds(ID 列表)和 initSelectedItems(完整对象):
<DualPanelSelectDialog
v-model:visible="visible"
source-type="user"
:init-selected-ids="selectedIds"
:init-selected-items="selectedItems"
@confirm="handleConfirm"
/>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 handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
selectedIds.value = data.items.map(item => item.id)
selectedItems.value = data.items
visible.value = false
}为什么需要
initSelectedItems? 弹窗分页加载时,某些已选项可能不在当前页数据中。initSelectedItems将对象预注入内部hasLoadList,确保确认时不丢失。
场景四:单选模式
<DualPanelSelectDialog
v-model:visible="visible"
source-type="user"
:multi-select="false"
@confirm="handleConfirm"
/>单选模式下,表格不显示 checkbox,点击行即选中。
场景五:自定义过滤条件
<DualPanelSelectDialog
v-model:visible="visible"
source-type="user"
:list-filter="userFilter"
@confirm="handleConfirm"
/>const userFilter: Record<string, any> = {
withPermission: true,
authOp: 'UserManage',
notGetInit: true,
notGetCurrentUser: true
}场景六:禁用特定行
<DualPanelSelectDialog
v-model:visible="visible"
source-type="user"
:disable-row-fn="(row) => row.state === 0"
@confirm="handleConfirm"
/>场景七:选择组织(单面板多选)
<LookupDetailDialog
v-model:visible="visible"
source-type="org"
:multi-select="true"
@confirm="handleConfirm"
/>场景八:选择用户分组(单面板单选 + 回显)
<LookupDetailDialog
v-model:visible="visible"
source-type="usergroup"
:multi-select="false"
:init-selected-ids="['group-001']"
@confirm="handleConfirm"
/>场景九:组织选择换根(收敛到绑定组织子树)
典型场景如数字助理「服务范围」:可选组织收敛到当前绑定组织及其全部下级,历史勾选回填, 并允许“取消全部勾选后确认”表达清空。
<TreeSelectDialog
v-model:visible="visible"
source-type="org"
:multi-select="true"
:init-selected-ids="orgIds"
:init-selected-items="orgItems"
:filter="orgFilter"
@confirm="handleOrgConfirm"
/>// orgIds / orgItems 由当前已选行投影而来,保证每次打开都按最新状态回填
const orgIds = computed(() => rows.value.map(r => r.id))
const orgItems = computed(() => rows.value.map(r => ({
id: r.id, code: r.code, name: r.name, orgAllPath: r.orgAllPath
})))
// 绑定组织存在时才收敛范围;缺省回退全树口径
const orgFilter = computed(() => ({
withPermission: true,
authOp: 'UserManage',
...(scopeOrgId.value ? { rootOrgId: scopeOrgId.value } : {})
}))
// confirm 的 items 是“组织完整集合”(含初始化时的历史勾选),整体替换即可;
// 空数组同样合法(用户取消全部勾选后确认 = 清空)
function handleOrgConfirm({ items }: ConfirmPayload) {
rows.value = items.map(item => ({ ...item }))
}🧩 共享类型
import type {
SelectedItem,
ConfirmPayload,
TreeSourceType,
OrgResourceSourceType,
TreeColumn,
PositionModeOptions,
LookupHttpServiceLike,
PersonnelTabId,
PersonnelSelectedItem,
PersonnelOrderField
} from '@gsp-sys/common-personnel-select-workbench'/** 选中项数据结构 */
interface SelectedItem {
id: string
name: string
code?: string
[key: string]: any
}
/** 弹窗确认事件 payload */
interface ConfirmPayload {
/** 本次选中的完整对象数组 */
items: SelectedItem[]
/** 弹窗期间所有加载过的数据(含未选中),用于差分提交 */
hasLoadList: SelectedItem[]
}
/** 纯树选择数据源类型 */
type TreeSourceType = 'org' | 'usergroup' | 'positiongroup' | 'businessdomain'
/** 双面板数据源类型 */
type OrgResourceSourceType = 'user' | 'position'
/** 树列定义 */
interface TreeColumn {
field: string
title: string
dataType?: string
width?: string | number
[key: string]: any
}
/** 人员选择(selection 迁移)常用类型 */
type PersonnelTabId = 'organization' | 'sysUserGroup' | 'search' | 'recent' | 'favorites' | 'candidate' | 'group'
type PersonnelOrderField = '' | 'orderby_ordernum' | 'orderby_code' | 'orderby_organduser'
interface PersonnelSelectedItem extends SelectedItem {
/** 数据源类型标记(user/org/usergroup),混选场景用于区分 */
itemType?: 'user' | 'org' | 'usergroup'
}🔌 API 函数
包内嵌以下 API 函数,可直接导入使用,也可供消费方做差分提交等操作。
import {
getOrgByLayer,
getOrgByParentId,
getAllOrgs,
searchOrg,
getUserGroupByLayer,
getUserGroupByParentId,
searchUserGroup,
getPositionGroupByLayer,
getPositionGroupByParentId,
searchPositionGroup,
getAllUsers,
getPositionsByGroupId,
searchPersonnel,
searchUserById,
getFilterPositions,
extractPageData,
getBusinessDomainTree
} from '@gsp-sys/common-personnel-select-workbench'| 函数 | 说明 |
|------|------|
| getOrgByLayer(layer, type?, filter?) | 按层级获取组织树节点 |
| getOrgByParentId(parentId, type?, filter?) | 按父 ID 获取子组织节点 |
| getAllOrgs(filter?) | 获取全部组织 |
| searchOrg(keyword) | 按关键词搜索组织 |
| getUserGroupByLayer(layer, filter?) | 按层级获取用户分组树节点 |
| getUserGroupByParentId(parentId, filter?) | 按父 ID 获取子用户分组节点 |
| searchUserGroup(keyword) | 按关键词搜索用户分组 |
| getPositionGroupByLayer(layer, filter?) | 按层级获取岗位分组树节点 |
| getPositionGroupByParentId(parentId, filter?) | 按父 ID 获取子岗位分组节点 |
| searchPositionGroup(keyword) | 按关键词搜索岗位分组 |
| getAllUsers(filter?) | 获取用户列表(分页 + 过滤) |
| getPositionsByGroupId(filter) | 根据分组 ID 获取岗位列表 |
| searchPersonnel(filter?) | 全局搜索人员(编号 / 名称 / 拼音 / 首字母同时匹配,searchMode=or) |
| searchUserById(param) | 按 ID 批量查询用户(回显场景) |
| getFilterPositions(filter?) | 获取成员所属岗位(岗位过滤漏斗选项;接口路径待后端确认,失败时调用方降级为不显示过滤) |
| extractPageData(res) | 提取响应数据(兼容数组 / { data, totalCount } 两种结构) |
| getBusinessDomainTree() | 获取完整业务域树 |
示例:
// 加载第一层组织
const orgs = await getOrgByLayer(1, 'org')
// 获取用户列表
const res = await getAllUsers({
pageIndex: 1,
pageSize: 20,
orgId: 'org-001',
withPermission: true,
codeOrName: '张'
})
const users = Array.isArray(res) ? res : res?.data || []
const total = res?.totalCount || 0
// 加载岗位分组树(带权限过滤)
const groups = await getPositionGroupByLayer(1, {
withPermission: true,
authOp: 'positionManage'
})🛠 Composables
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[]> // 树数据源
treeRef: Ref<any> // 树组件引用
loadData: (treeNode: any) => Promise<void> // f-tree-grid 的 :loadData 回调
initTree: () => Promise<void> // 初始化/重新加载根节点
loadChildren: (parentId: string) => Promise<any[]>
resolveTreeNodeId: (treeNode: any) => string
findNodeInTree: (nodes: any[], nodeId: string) => any | null
syncChildrenToTreeData: (parentId: string, children: any[]) => void
}useTreeDataSource
输入 sourceType + filter,输出 useFTreeGridLoader 所需的取数与节点转换配置。
sourceType:org/usergroup/positiongroup/businessdomain(业务域为整树一次加载,其余为懒加载)。filter.rootOrgId(仅org):组织树换根——以该组织为唯一根(GET /sysOrgs/{id}),可选范围收敛到「该组织及其下级」;子节点仍按filter懒加载并做权限裁剪。该参数为前端私有,请求前剔除;不传则根加载走GET /sysOrgs(layer=1+filter),保持全树口径。
import { useTreeDataSource } from '@gsp-sys/common-personnel-select-workbench'
const dataSource = useTreeDataSource({
sourceType: () => 'org',
filter: () => ({ withPermission: true, authOp: 'UserManage', rootOrgId: 'org-001' })
})useSelectionState
跨页/跨节点选中态管理,供双面板列表复用。
import { useSelectionState } from '@gsp-sys/common-personnel-select-workbench'
const selection = useSelectionState({ multiSelect: () => true })⚠️ 注意事项
1. 必须引入 CSS
组件使用 scoped 样式 + :deep() 穿透 Farris UI,不引入 CSS 会导致布局错乱:
import '@gsp-sys/common-personnel-select-workbench/style.css'2. baseURL 为相对路径
API 层 baseURL 为 /api/runtime/sys/v1.0(相对路径),宿主项目需配置代理将 /api 转发到后端网关。
3. SessionId 认证
API 拦截器会自动从 sessionStorage.getItem('sessionId') 读取会话 ID 并加入请求头。宿主项目需确保登录后将 sessionId 写入 sessionStorage。
该约定同样适用于
TreeSelectField的默认取数服务;若宿主的会话存储方式不同,请自行实现服务并通过provide或:http-service接管。
4. 跨页选中保持
双面板组件内置跨页选中保持:维护全局 selectedIds(Set),分页切换时自动恢复当前页选中状态,updateDataSource 触发的空选中事件会被抑制以防误清。
5. hasLoadList 与差分提交
confirm 事件 payload 含 hasLoadList(所有已加载过的数据),可用于差分提交:
function handleConfirm(data: { items: any[]; hasLoadList: any[] }) {
const selectedIds = new Set(data.items.map(item => item.id))
const addList = data.hasLoadList.filter(item => selectedIds.has(item.id))
const removeList = data.hasLoadList.filter(item => !selectedIds.has(item.id))
// 调用后端差分接口...
}6. 单面板模式的 Props 处理
LookupDetailDialog 的 sourceType 为 org / usergroup 时采用单面板布局,以下 Props 静默忽略:
navType、listFilter、pagination、pageIndex、pageSize、pageList、total、groupId、disableRowFn
单面板模式支持 multiSelect 控制多选/单选,搜索为本地过滤(仅覆盖已加载节点)。
7. 支持的数据源与 rolegroup
TreeSelectDialog 支持 org / usergroup / positiongroup / businessdomain;rolegroup 本包暂无对应 API。
TreeSelectField 的 sourceType 额外接受 rolegroup(实验性透传,映射 rolegroup/tree,需后端先提供该接口)。
📄 License
Internal use only.
