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.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-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/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)
  • 搜索栏对已加载节点做本地过滤(按 codename 匹配,保留匹配节点及其祖先路径)
  • 不适用的 Props(navTypeshowStopBtnmultiSelectkeepSelectlistFilter 等)静默忽略

confirm 返回的 items 为选中节点的原始数据对象(含 idcodenameparentId)。

用户分组模式(sourceType="usergroup")

单面板布局:弹窗内仅包含搜索栏和用户分组树表格(f-tree-grid),无左侧导航面板、无分页列表。

  • 通过 getUserGroupByLayer(1) 加载根节点,点击展开节点时懒加载子节点
  • 仅支持单选(点击行选中,无 checkbox)
  • 搜索栏对已加载节点做本地过滤(按 codename 匹配,保留匹配节点及其祖先路径)
  • 不适用的 Props(navTypeshowStopBtnmultiSelectkeepSelectlistFilter 等)静默忽略

confirm 返回的 items 为选中节点的原始数据对象(含 idcodenameparentId)。


典型场景

场景一:选择用户(多选)

<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>

单面板模式特点:

  • 仅显示树表格,无左侧导航和右侧列表
  • 单选(点击行选中,再次点击其他行切换选中)
  • 搜索为本地过滤,仅覆盖已加载节点
  • multiSelectnavType 等 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 处理

sourceTypeorgusergroup 时,组件采用单面板布局,以下 Props 被静默忽略(不报错、不生效):

  • navTypeshowStopBtnmultiSelectkeepSelectlistFilter
  • paginationpageIndexpageSizepageListtotal
  • groupIddisableRowFntreeFiltertreeColsgridCols

单面板模式强制单选,搜索为本地过滤(仅覆盖已加载节点)。