npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gsp-sys/common-personnel-select-workbench

v0.5.0

Published

用户/岗位选择弹窗组件,提供组织树导航 + 分页列表选择能力

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 / businessdomainrolegroup 实验性) | 输入框 + 弹窗选择,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-workbench

2. 全局注册 Farris UI(main.ts)

组件依赖 @farris/ui-vue 的全局组件(f-modalf-tree-gridf-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" />

优先级httpService prop > 宿主 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-lookupTreeSelectField 会在空选确认时提示“请选择一条记录”且不回调,两者此处行为不同。)

组织树换根(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,打开时自动选中该分组节点) |

授权口径(navTreeFilterlistFilter 成对设置)

老代码 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/treeusergroup/treepositiongroup/treebusiness-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 链路(会触发 clearafterConfirm([])) |

关于“允许不选”:f-lookup 弹窗确认对空集合有硬拦(use-dialog.tshandleSubmitValidationselectedItems 为空即提示“请选择一条记录”), 该判断早于 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 为选中节点的原始数据对象(含 idcodenameparentId
  • 不适用的 Props(navTypelistFilterpagination 等)静默忽略

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 所需的取数与节点转换配置。

  • sourceTypeorg / usergroup / positiongroup / businessdomain(业务域为整树一次加载,其余为懒加载)。
  • filter.rootOrgId(仅 org):组织树换根——以该组织为唯一根(GET /sysOrgs/{id}),可选范围收敛到「该组织及其下级」;子节点仍按 filter 懒加载并做权限裁剪。该参数为前端私有,请求前剔除;不传则根加载走 GET /sysOrgslayer=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 处理

LookupDetailDialogsourceTypeorg / usergroup 时采用单面板布局,以下 Props 静默忽略: navTypelistFilterpaginationpageIndexpageSizepageListtotalgroupIddisableRowFn

单面板模式支持 multiSelect 控制多选/单选,搜索为本地过滤(仅覆盖已加载节点)。

7. 支持的数据源与 rolegroup

TreeSelectDialog 支持 org / usergroup / positiongroup / businessdomainrolegroup 本包暂无对应 API。 TreeSelectFieldsourceType 额外接受 rolegroup(实验性透传,映射 rolegroup/tree,需后端先提供该接口)。


📄 License

Internal use only.