Appearance
BizTablePicker 弹窗表格选择器
弹窗表格选择器(筛选区双形态),合并自 BizSelectTable(弹窗表格选择器)与 BizMemberPicker(人员选择器):按钮触发 Dialog,内部表格分页浏览 + 单选 / 多选(跨页保留),弹窗内为独立会话选择,确定后回填 v-model,已选 chips 可逐个删除。
filter-mode="search"(默认):顶部搜索栏 + 表格。本地data自动分页 + 关键词跨列过滤,或request远程契约(loading / 翻页 / 搜索自动管理);选单据(订单 / 合同 / 物料 / 商品 SKU)等多列信息需比对后选择的场景。filter-mode="tree":左侧组织树(org-tree静态全量 /org-load-request懒加载逐层拉取,二选一,均不传则隐藏左栏)+ 右侧表格(关键词 300ms 防抖搜索 + 分页 + 行点击即选);审批选人、任务指派、权限分配等选人场景。
组件零接口:数据全部由业务侧注入(本地 data 或 request),request 契约 { page, pageSize, keyword, deptId? }(tree 形态携带 deptId);本地数据模式支持拼音检索(pinyin-of / initials-of 转换器注入)与头像列(avatar-key)。与 BizRemoteSelect(轻量远程下拉)、BizOrgTreeSelect(字段级选部门)互补。
本地数据单选
暂未选择
当前选中:未选择
36 条本地数据自动分页;单选回填
label-key 字段标签。<script setup lang="ts">
/** 场景:本地数据单选——弹窗表格分页浏览 + 单选回填(选物料) */
import { ref } from 'vue'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'code', label: '物料编码', width: 120 },
{ prop: 'name', label: '物料名称', minWidth: 160 },
{ prop: 'unit', label: '单位', width: 70 },
{ prop: 'stock', label: '库存', width: 90, align: 'right' },
]
const all = Array.from({ length: 36 }, (_, i) => ({
id: i + 1,
code: `MAT-${String(i + 1).padStart(4, '0')}`,
name: ['铝合金外壳', 'M4 螺丝', 'Type-C 模组', '包装纸箱'][i % 4] + `(${i + 1}#)`,
unit: i % 2 === 0 ? '个' : '箱',
stock: (i * 37) % 500,
}))
const selected = ref<BizTablePickerRow | null>(null)
</script>
<template>
<div style="max-width: 640px">
<BizTablePicker
v-model="selected"
:columns="columns"
:data="all"
title="选择物料"
trigger-text="选择物料"
label-key="name"
/>
<p style="margin-top: 10px; font-size: 13px; color: var(--el-text-color-secondary)">
当前选中:{{ selected ? `${selected.name}(${selected.code})` : '未选择' }}
</p>
</div>
</template>
显示代码
本地多选(跨页保留)
暂未选择
翻页勾选互不丢失;已选标签可单个移除,合计联动。
<script setup lang="ts">
/** 场景:本地数据多选——跨页勾选互不丢失、已选标签回显、单个移除与合计联动 */
import { computed, ref } from 'vue'
import { ElTag } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '商品', minWidth: 150 },
{ prop: 'price', label: '价格', width: 90, align: 'right', formatter: (row) => `¥${row.price}` },
]
const all = Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
name: ['蓝牙耳机', '移动电源', '数据线', '手机壳'][i % 4] + ` ${i + 1}#`,
price: ((i * 27) % 300) + 19,
}))
const selected = ref<BizTablePickerRow[]>([])
const totalAmount = computed(() => selected.value.reduce((sum, row) => sum + Number(row.price), 0))
</script>
<template>
<div style="max-width: 640px">
<BizTablePicker
v-model="selected"
:columns="columns"
:data="all"
multiple
title="搭配商品"
trigger-text="选择商品"
label-key="name"
/>
<div v-if="selected.length" style="margin-top: 8px; display: flex; gap: 8px; align-items: center">
<ElTag type="success">已选 {{ selected.length }} 件,合计 ¥{{ totalAmount }}</ElTag>
</div>
</div>
</template>
显示代码
远程数据源(request 分页搜索)
暂未选择
request 注入(400ms 模拟延迟):搜索 / 翻页走接口,loading 自动管理。<script setup lang="ts">
/** 场景:远程 request 分页搜索——搜索 / 翻页走接口,loading 自动管理(关联订单) */
import { ref } from 'vue'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerParams, BizTablePickerResult, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'orderNo', label: '订单号', width: 150 },
{ prop: 'customer', label: '客户', minWidth: 140 },
{ prop: 'amount', label: '金额', width: 100, align: 'right', formatter: (row) => `¥${row.amount}` },
]
// 模拟远程接口:200 条数据,支持关键字过滤与分页(实际项目替换为真实请求)
const DB = Array.from({ length: 200 }, (_, i) => ({
id: i + 1,
orderNo: `SO2026${String(i + 1).padStart(5, '0')}`,
customer: ['杭州某某科技', '上海某某贸易', '深圳某某实业', '北京某某传媒'][i % 4],
amount: ((i * 173) % 50000) / 10,
}))
const selected = ref<BizTablePickerRow | null>(null)
const request = (params: BizTablePickerParams): Promise<BizTablePickerResult> => {
return new Promise((resolve) => {
setTimeout(() => {
const kw = params.keyword.trim()
const hit = DB.filter(
(row) => !kw || row.orderNo.includes(kw) || String(row.customer).includes(kw)
)
const start = (params.page - 1) * params.pageSize
resolve({ list: hit.slice(start, start + params.pageSize), total: hit.length })
}, 400)
})
}
</script>
<template>
<div style="max-width: 720px">
<BizTablePicker
v-model="selected"
:columns="columns"
:request="request"
title="关联订单"
trigger-text="选择订单"
label-key="orderNo"
/>
</div>
</template>
显示代码
选单据多列比对
暂未选择
已选 0 份合同,主键集合:[]
合同多列信息(编号 / 供应商 / 金额 / 日期 / 状态)比对后多选,
confirm 携带主键数组直接提交业务 id。<script setup lang="ts">
/** 场景:选单据多列比对——订单多列信息(编号 / 供应商 / 金额 / 日期 / 状态)比对后多选 */
import { ref } from 'vue'
import { ElTag } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerChangePayload, BizTablePickerColumn, BizTablePickerRow } from 'vue3-ep-biz-lib'
const STATUS: Record<string, { text: string; type: 'success' | 'warning' | 'info' }> = {
done: { text: '已完结', type: 'success' },
doing: { text: '履行中', type: 'warning' },
draft: { text: '草稿', type: 'info' },
}
const columns: BizTablePickerColumn[] = [
{ prop: 'contractNo', label: '合同编号', width: 150 },
{ prop: 'supplier', label: '供应商', minWidth: 140 },
{ prop: 'amount', label: '金额', width: 110, align: 'right', formatter: (row) => `¥${Number(row.amount).toLocaleString()}` },
{ prop: 'signedAt', label: '签订日期', width: 110 },
{ prop: 'statusText', label: '状态', width: 90, align: 'center' },
]
const all = Array.from({ length: 18 }, (_, i) => ({
id: i + 1,
contractNo: `HT-2026-${String(i + 1).padStart(3, '0')}`,
supplier: ['苏州精密机械', '东莞五金塑料', '宁波电器制造', '青岛橡胶制品'][i % 4],
amount: 30000 + ((i * 9137) % 200000),
signedAt: `2026-0${(i % 9) + 1}-${String((i * 3) % 27 + 1).padStart(2, '0')}`,
statusText: STATUS[['done', 'doing', 'draft'][i % 3]]?.text ?? '',
status: ['done', 'doing', 'draft'][i % 3],
}))
const selected = ref<BizTablePickerRow[]>([])
const lastKeys = ref<unknown[]>([])
const onConfirm = (payload: BizTablePickerChangePayload): void => {
lastKeys.value = payload.keys
}
</script>
<template>
<div style="max-width: 720px">
<BizTablePicker
v-model="selected"
:columns="columns"
:data="all"
multiple
title="选择关联合同"
trigger-text="选择合同"
label-key="contractNo"
@confirm="onConfirm"
>
<template #col-statusText="{ row }">
<ElTag size="small" :type="STATUS[String(row.status)]?.type ?? 'info'">
{{ STATUS[String(row.status)]?.text ?? row.statusText }}
</ElTag>
</template>
</BizTablePicker>
<p style="margin-top: 10px; font-size: 13px; color: var(--el-text-color-secondary)">
已选 {{ selected.length }} 份合同,主键集合:<code>{{ JSON.stringify(lastKeys) }}</code>
</p>
</div>
</template>
显示代码
自定义列(col-xxx 插槽)
暂未选择
#col-字段名 作用域插槽自定义单元格(状态标签 / 健康度着色)。<script setup lang="ts">
/** 场景:col-xxx 插槽自定义列——表格内嵌状态标签与颜色块 */
import { ref } from 'vue'
import { ElTag } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '设备', minWidth: 140 },
{ prop: 'status', label: '状态', width: 100 },
{ prop: 'health', label: '健康度', width: 140 },
]
const all = [
{ id: 1, name: '入口网关 A', status: 'online', health: 96 },
{ id: 2, name: '入口网关 B', status: 'online', health: 88 },
{ id: 3, name: '数据库主库', status: 'degraded', health: 61 },
{ id: 4, name: '备份节点', status: 'offline', health: 0 },
]
const selected = ref<BizTablePickerRow | null>(null)
const STATUS: Record<string, { text: string; type: 'success' | 'warning' | 'info' }> = {
online: { text: '在线', type: 'success' },
degraded: { text: '降级', type: 'warning' },
offline: { text: '离线', type: 'info' },
}
</script>
<template>
<div style="max-width: 640px">
<BizTablePicker
v-model="selected"
:columns="columns"
:data="all"
title="选择设备"
trigger-text="选择设备"
label-key="name"
:page-size="10"
>
<template #col-status="{ row }">
<ElTag size="small" :type="STATUS[String(row.status)]?.type ?? 'info'">
{{ STATUS[String(row.status)]?.text ?? row.status }}
</ElTag>
</template>
<template #col-health="{ row }">
<span :style="{ color: Number(row.health) > 80 ? 'var(--el-color-success)' : Number(row.health) > 50 ? 'var(--el-color-warning)' : 'var(--el-color-danger)', fontWeight: 600 }">
{{ row.health }}%
</span>
</template>
</BizTablePicker>
</div>
</template>
显示代码
tree 形态选人(审批选人)
成员03(运维中心)
静态组织树过滤部门、右侧关键词搜索分页勾选(姓名 + 部门 + 岗位列),chips 回显姓名(部门)。
<script setup lang="ts">
/** 场景:tree 形态选人(审批选人)——静态组织树 + 关键词搜索 + 分页成员表格多选 + 已选 chips */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerOrgNode, BizTablePickerParams, BizTablePickerResult, BizTablePickerRow } from 'vue3-ep-biz-lib'
const orgTree: BizTablePickerOrgNode[] = [
{
id: 'dev',
label: '研发中心',
children: [{ id: 'fe', label: '前端组' }, { id: 'be', label: '服务端组' }],
},
{ id: 'ops', label: '运维中心' },
{ id: 'sec', label: '安全管理部' },
]
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'dept', label: '部门', minWidth: 160 },
{ prop: 'post', label: '岗位', width: 120 },
]
const DEPT_OF_NODE: Record<string, string> = {
dev: '研发中心',
fe: '研发中心-前端组',
be: '研发中心-服务端组',
ops: '运维中心',
sec: '安全管理部',
}
/** 模拟成员接口:部门过滤 + 关键词 + 分页(真实项目为服务端接口) */
const request = (params: BizTablePickerParams): Promise<BizTablePickerResult> => {
const all: BizTablePickerRow[] = Array.from({ length: 57 }, (_unused, i) => ({
id: i + 1,
name: `成员${String(i + 1).padStart(2, '0')}`,
dept: ['研发中心-前端组', '研发中心-服务端组', '运维中心', '安全管理部'][i % 4],
post: ['前端工程师', '后端工程师', '运维工程师', '安全专员'][i % 4],
}))
const deptPath = params.deptId === undefined ? '' : DEPT_OF_NODE[String(params.deptId)] ?? ''
const keyword = params.keyword.trim()
const filtered = all.filter(
(m) =>
(deptPath === '' || String(m.dept).includes(deptPath)) &&
(keyword === '' || String(m.name).includes(keyword) || String(m.dept).includes(keyword))
)
const start = (params.page - 1) * params.pageSize
return new Promise((resolve) => {
setTimeout(() => resolve({ list: filtered.slice(start, start + params.pageSize), total: filtered.length }), 250)
})
}
const selected = ref<BizTablePickerRow[]>([{ id: 3, name: '成员03', dept: '运维中心', post: '运维工程师' }])
const onChange = (payload: { selected: BizTablePickerRow[] }): void => {
if (payload.selected.length > 0) ElMessage.success(`已选 ${payload.selected.length} 人:${payload.selected.map((m) => String(m.name)).join('、')}`)
}
</script>
<template>
<div style="max-width: 720px">
<BizTablePicker
v-model="selected"
filter-mode="tree"
:columns="columns"
:org-tree="orgTree"
:request="request"
row-key="id"
label-key="name"
multiple
title="选择审批人"
trigger-text="选择审批人"
empty-text="暂未选择审批人"
@change="onChange"
/>
</div>
</template>
显示代码
tree 形态单选(指派负责人)
未指派
multiple=false 行高亮单选;不传 org-tree 隐藏左栏仅保留搜索分页。<script setup lang="ts">
/** 场景:tree 形态单选指派负责人——multiple=false 行高亮单选;不传 org-tree 隐藏左栏仅保留搜索分页 */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerChangePayload, BizTablePickerColumn, BizTablePickerParams, BizTablePickerResult, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'dept', label: '班组', minWidth: 140 },
]
const request = (params: BizTablePickerParams): Promise<BizTablePickerResult> => {
const all: BizTablePickerRow[] = Array.from({ length: 12 }, (_unused, i) => ({
id: i + 1,
name: `值班员${String(i + 1).padStart(2, '0')}`,
dept: i % 2 === 0 ? '白班' : '夜班',
}))
const keyword = params.keyword.trim()
const filtered = keyword === '' ? all : all.filter((m) => String(m.name).includes(keyword))
const start = (params.page - 1) * params.pageSize
return new Promise((resolve) => {
setTimeout(() => resolve({ list: filtered.slice(start, start + params.pageSize), total: filtered.length }), 200)
})
}
const owner = ref<BizTablePickerRow | null>(null)
const onChange = (payload: BizTablePickerChangePayload): void => {
const first = payload.selected[0]
if (first !== undefined) ElMessage.success(`已指派:${String(first.name)}(${String(first.dept)})`)
}
</script>
<template>
<div style="max-width: 560px; display: flex; flex-direction: column; gap: 10px">
<BizTablePicker
v-model="owner"
filter-mode="tree"
:multiple="false"
:columns="columns"
:request="request"
label-key="name"
title="指派值班负责人"
trigger-text="选择负责人"
empty-text="未指派"
@change="onChange"
/>
<span style="font-size: 13px; color: var(--el-text-color-secondary)">
单选模式行高亮选择(无复选框);不传 org-tree 时隐藏左栏仅保留搜索分页
</span>
</div>
</template>
显示代码
懒加载组织树
暂未选择人员
左侧组织树展开时才请求子级(模拟 300ms 延迟);点击部门按 deptId 过滤右侧成员
org-load-request 逐层拉取子部门(展开才请求);点部门按 deptId 过滤右侧成员。<script setup lang="ts">
/** 场景:懒加载组织树——org-load-request 逐层拉取子部门,点部门按 deptId 过滤成员 */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerChangePayload, BizTablePickerColumn, BizTablePickerOrgLoad, BizTablePickerOrgNode, BizTablePickerParams, BizTablePickerResult, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'dept', label: '所属分支', minWidth: 180 },
{ prop: 'phone', label: '联系电话', width: 140 },
]
/** 组织树结构(懒加载逐层返回):集团 → 大区 → 分公司 */
const ORG: Record<string, BizTablePickerOrgNode[]> = {
root: [{ id: 'hq', label: '集团总部' }],
hq: [{ id: 'east', label: '华东大区' }, { id: 'south', label: '华南大区' }],
east: [{ id: 'sh', label: '上海分公司', isLeaf: true }, { id: 'hz', label: '杭州分公司', isLeaf: true }],
south: [{ id: 'sz', label: '深圳分公司', isLeaf: true }, { id: 'gz', label: '广州分公司', isLeaf: true }],
}
/** 分公司 → 成员部门名映射(用于接口侧过滤) */
const ORG_LABEL: Record<string, string> = {
hq: '集团总部',
east: '华东大区',
south: '华南大区',
sh: '上海分公司',
hz: '杭州分公司',
sz: '深圳分公司',
gz: '广州分公司',
}
const delay = <T,>(data: T, ms = 300): Promise<T> => new Promise((resolve) => setTimeout(() => resolve(data), ms))
/** 模拟组织树懒加载接口:按父节点返回子级(真实项目为服务端接口) */
const orgLoadRequest: BizTablePickerOrgLoad = (node) => delay(ORG[String(node.id)] ?? [])
/** 模拟成员接口:按分公司(deptId)过滤 + 关键词 + 分页 */
const request = (params: BizTablePickerParams): Promise<BizTablePickerResult> => {
const all: BizTablePickerRow[] = Array.from({ length: 64 }, (_unused, i) => ({
id: i + 1,
name: `员工${String(i + 1).padStart(2, '0')}`,
dept: ['集团总部', '上海分公司', '杭州分公司', '深圳分公司', '广州分公司'][i % 5],
phone: `138${String(10000000 + i * 137).slice(0, 8)}`,
}))
const deptLabel = params.deptId === undefined ? '' : ORG_LABEL[String(params.deptId)] ?? ''
const kw = params.keyword.trim()
const filtered = all.filter(
(m) =>
(deptLabel === '' || String(m.dept).includes(deptLabel)) &&
(kw === '' || String(m.name).includes(kw) || String(m.dept).includes(kw))
)
const start = (params.page - 1) * params.pageSize
return delay({ list: filtered.slice(start, start + params.pageSize), total: filtered.length }, 250)
}
const selected = ref<BizTablePickerRow[]>([])
const onConfirm = (payload: BizTablePickerChangePayload): void => {
ElMessage.success(`已确认 ${payload.selected.length} 人`)
}
</script>
<template>
<div style="max-width: 720px">
<BizTablePicker
v-model="selected"
filter-mode="tree"
:columns="columns"
:org-load-request="orgLoadRequest"
:request="request"
label-key="name"
multiple
title="选择分支机构人员"
trigger-text="选择人员"
empty-text="暂未选择人员"
@confirm="onConfirm"
/>
<p style="margin-top: 10px; font-size: 13px; color: var(--el-text-color-secondary)">
左侧组织树展开时才请求子级(模拟 300ms 延迟);点击部门按 deptId 过滤右侧成员
</p>
</div>
</template>
显示代码
已选 chips 管理(项目组上限)
项目组暂无成员
当前 0 / 6 人
项目场景:多选组建团队、chips 移除成员、超上限(6 人)自动截断并提示。
<script setup lang="ts">
/** 场景:已选 chips 管理——多选组建项目组、chips 移除成员、超上限(6 人)自动截断并提示 */
import { ref } from 'vue'
import { ElMessage, ElTag } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerChangePayload, BizTablePickerColumn, BizTablePickerParams, BizTablePickerResult, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'dept', label: '小组', minWidth: 140 },
]
const staff: BizTablePickerRow[] = Array.from({ length: 20 }, (_unused, i) => ({
id: i + 1,
name: `工程师${String(i + 1).padStart(2, '0')}`,
dept: i % 3 === 0 ? '前端组' : i % 3 === 1 ? '服务端组' : '测试组',
}))
const request = (params: BizTablePickerParams): Promise<BizTablePickerResult> => {
const keyword = params.keyword.trim()
const filtered = keyword === '' ? staff : staff.filter((m) => String(m.name).includes(keyword) || String(m.dept ?? '').includes(keyword))
const start = (params.page - 1) * params.pageSize
return new Promise((resolve) => {
setTimeout(() => resolve({ list: filtered.slice(start, start + params.pageSize), total: filtered.length }), 200)
})
}
const team = ref<BizTablePickerRow[]>([])
const onChange = (payload: BizTablePickerChangePayload): void => {
if (payload.selected.length > 6) {
ElMessage.warning('项目组最多 6 人,请先移除成员')
team.value = payload.selected.slice(0, 6)
}
}
</script>
<template>
<div style="max-width: 640px">
<BizTablePicker
v-model="team"
filter-mode="tree"
:columns="columns"
:request="request"
label-key="name"
multiple
title="组建项目组"
trigger-text="添加成员"
empty-text="项目组暂无成员"
@change="onChange"
/>
<div style="margin-top: 10px; display: flex; gap: 8px; align-items: center">
<ElTag v-for="member in team" :key="String(member.id)" size="small" type="info">{{ member.name }} · {{ member.dept }}</ElTag>
<span style="font-size: 13px; color: var(--el-text-color-secondary)">当前 {{ team.length }} / 6 人</span>
</div>
</div>
</template>
显示代码
值班排班(按日指派)
周一
未安排
周二
未安排
周三
未安排
周四
未安排
周五
未安排
排班场景:周一~周五每行一个单选 picker,各自独立 v-model;指派完成即时提示。
<script setup lang="ts">
/** 场景:值班排班——按日期逐格选人(tree 形态单选 + 每格独立 v-model) */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerParams, BizTablePickerResult, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'dept', label: '班组', minWidth: 120 },
]
const request = (params: BizTablePickerParams): Promise<BizTablePickerResult> => {
const all: BizTablePickerRow[] = Array.from({ length: 16 }, (_unused, i) => ({
id: i + 1,
name: `值班员${String(i + 1).padStart(2, '0')}`,
dept: i % 2 === 0 ? '白班组' : '夜班组',
}))
const keyword = params.keyword.trim()
const filtered = keyword === '' ? all : all.filter((m) => String(m.name).includes(keyword))
const start = (params.page - 1) * params.pageSize
return Promise.resolve({ list: filtered.slice(start, start + params.pageSize), total: filtered.length })
}
const days = ['周一', '周二', '周三', '周四', '周五']
const roster = ref<Record<string, BizTablePickerRow | null>>({ 周一: null, 周二: null, 周三: null, 周四: null, 周五: null })
const onChange = (day: string): void => {
const member = roster.value[day]
if (member !== null) ElMessage.success(`${day} 值班:${String(member.name)}`)
}
</script>
<template>
<div style="max-width: 680px; display: flex; flex-direction: column; gap: 8px">
<div v-for="day in days" :key="day" style="display: flex; align-items: center; gap: 12px; padding: 6px 0; border-bottom: 1px dashed var(--el-border-color-lighter)">
<span style="width: 40px; font-weight: 600">{{ day }}</span>
<BizTablePicker
v-model="roster[day]"
filter-mode="tree"
:multiple="false"
:columns="columns"
:request="request"
label-key="name"
:title="`${day} 值班安排`"
trigger-text="指派"
empty-text="未安排"
@change="() => onChange(day)"
/>
</div>
</div>
</template>
显示代码
数据权限分配
敏感权限开通即记录审计日志,授权后次日 0 点生效
暂无授权成员
安全场景:敏感权限授权选人,提交生效并记录审计——人数上限与生效时间提示前置。
<script setup lang="ts">
/** 场景:数据权限分配——tree 形态多选受审成员 + 提交生效 */
import { ref } from 'vue'
import { ElButton, ElMessage, ElTag } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerParams, BizTablePickerResult, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'dept', label: '部门', minWidth: 140 },
]
const request = (params: BizTablePickerParams): Promise<BizTablePickerResult> => {
const all: BizTablePickerRow[] = Array.from({ length: 30 }, (_unused, i) => ({
id: i + 1,
name: `分析员${String(i + 1).padStart(2, '0')}`,
dept: '数据中心',
}))
const filtered = params.keyword.trim() === '' ? all : all.filter((m) => String(m.name).includes(params.keyword.trim()))
const start = (params.page - 1) * params.pageSize
return new Promise((resolve) => {
setTimeout(() => resolve({ list: filtered.slice(start, start + params.pageSize), total: filtered.length }), 200)
})
}
const granted = ref<BizTablePickerRow[]>([])
const onSubmit = (): void => {
if (granted.value.length === 0) {
ElMessage.warning('请先选择授权成员')
return
}
ElMessage.success(`已为 ${granted.value.length} 人开通「经营明细」查看权限`)
}
</script>
<template>
<div style="max-width: 640px; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; gap: 8px; align-items: center">
<ElTag size="small" type="warning">敏感权限</ElTag>
<span style="font-size: 13px; color: var(--el-text-color-secondary)">开通即记录审计日志,授权后次日 0 点生效</span>
</div>
<BizTablePicker
v-model="granted"
filter-mode="tree"
:columns="columns"
:request="request"
label-key="name"
multiple
title="选择授权成员"
trigger-text="选择成员"
empty-text="暂无授权成员"
/>
<ElButton type="primary" style="width: fit-content" @click="onSubmit">提交授权</ElButton>
</div>
</template>
显示代码
表单关联单据
报销单选择关联差旅申请,弹窗确认后回填表单。
<script setup lang="ts">
/** 场景:表单联动回填——报销单选择关联差旅申请,弹窗确认后回填表单 */
import { ref } from 'vue'
import { ElButton, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'applyNo', label: '申请单号', width: 150 },
{ prop: 'summary', label: '差旅事由', minWidth: 160 },
{ prop: 'applicant', label: '申请人', width: 90 },
]
const all = [
{ id: 1, applyNo: 'TRV-2026-018', summary: '深圳客户现场支持(3 天)', applicant: '张三' },
{ id: 2, applyNo: 'TRV-2026-021', summary: '上海供应商年度审核', applicant: '李四' },
{ id: 3, applyNo: 'TRV-2026-025', summary: '北京行业展会观展', applicant: '王五' },
]
const linked = ref<BizTablePickerRow | null>(null)
const expense = ref('')
const submit = (): void => {
if (!linked.value) {
ElMessage.warning('请先选择关联差旅申请')
return
}
ElMessage.success(`已提交报销单,关联 ${linked.value.applyNo}`)
}
</script>
<template>
<div style="max-width: 680px">
<ElForm label-width="110px">
<ElFormItem label="关联差旅申请">
<BizTablePicker
v-model="linked"
:columns="columns"
:data="all"
title="选择差旅申请"
trigger-text="选择申请单"
label-key="applyNo"
dialog-width="640px"
/>
</ElFormItem>
<ElFormItem label="报销金额">
<ElInput v-model="expense" placeholder="元" style="width: 200px" />
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="submit">提交报销单</ElButton>
</ElFormItem>
</ElForm>
</div>
</template>
显示代码
事件日志(confirm / change / cancel)
暂未选择
暂无事件——打开弹窗选择后观察事件流
确认 / 变化 / 取消全事件流实时记录;
change、confirm 同时携带选中行与主键数组。<script setup lang="ts">
/** 场景:事件日志——confirm / change / cancel 全事件流实时记录(含主键数组契约) */
import { ref } from 'vue'
import { ElTag } from 'element-plus'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerChangePayload, BizTablePickerColumn, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '会签人', minWidth: 120 },
{ prop: 'dept', label: '部门', width: 140 },
]
const all = [
{ id: 1, name: '张三', dept: '财务部' },
{ id: 2, name: '李四', dept: '法务部' },
{ id: 3, name: '王五', dept: '运营部' },
]
const selected = ref<BizTablePickerRow[]>([])
interface LogItem {
time: string
name: string
detail: string
}
const logs = ref<LogItem[]>([])
const stamp = (): string => {
const now = new Date()
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
}
const log = (name: string, detail: string): void => {
logs.value = [{ time: stamp(), name, detail }, ...logs.value].slice(0, 8)
}
const payloadText = (payload: BizTablePickerChangePayload): string =>
`keys=${JSON.stringify(payload.keys)},rows=${payload.selected.length}`
const onConfirm = (payload: BizTablePickerChangePayload): void => log('confirm', payloadText(payload))
const onChange = (payload: BizTablePickerChangePayload): void => log('change', payloadText(payload))
const onCancel = (): void => log('cancel', '弹窗未确认关闭')
const onUpdate = (value: BizTablePickerRow[] | BizTablePickerRow | null): void =>
log('update:modelValue', JSON.stringify(Array.isArray(value) ? value.map((row) => row.name) : value?.name ?? null))
</script>
<template>
<div style="max-width: 640px; display: flex; flex-direction: column; gap: 10px">
<BizTablePicker
v-model="selected"
:columns="columns"
:data="all"
multiple
title="选择会签人"
trigger-text="选择会签人"
label-key="name"
@confirm="onConfirm"
@change="onChange"
@cancel="onCancel"
@update:model-value="onUpdate"
/>
<div style="min-height: 120px; padding: 8px 12px; border: 1px solid var(--el-border-color-lighter); border-radius: var(--el-border-radius-base); font-size: 13px; display: flex; flex-direction: column; gap: 4px">
<span v-if="logs.length === 0" style="color: var(--el-text-color-placeholder)">暂无事件——打开弹窗选择后观察事件流</span>
<div v-for="(item, index) in logs" :key="`${item.time}-${index}`" style="display: flex; gap: 8px; align-items: center">
<ElTag size="small" :type="item.name === 'confirm' ? 'success' : item.name === 'cancel' ? 'info' : 'warning'">{{ item.name }}</ElTag>
<span style="color: var(--el-text-color-secondary)">{{ item.time }}</span>
<span>{{ item.detail }}</span>
</div>
</div>
</div>
</template>
显示代码
最近选择页签(本地留痕)
暂未选择
已选 0 人;确认选择后再次打开弹窗,「最近选择」页签会带出上次确认的人(最多留 8 条,可悬停移除单条记录)。
recent.storage-key 开启「全部数据 / 最近选择」双页签:确认选择时所选行自动提到最近列表最前(按 key 去重、截断至上限),再次打开弹窗可直接从最近记录点选——钉钉式选人体验;悬停记录可移除单条。<script setup lang="ts">
/** 场景:最近选择页签(本地存储)——确认后自动留痕,重开弹窗可从「最近选择」快速点选 */
import { ref } from 'vue'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'name', label: '姓名', width: 110 },
{ prop: 'dept', label: '部门', width: 120 },
{ prop: 'role', label: '岗位', minWidth: 120 },
]
const all: BizTablePickerRow[] = Array.from({ length: 30 }, (_, i) => ({
id: i + 1,
name: `员工${i + 1}号`,
dept: ['研发部', '运营部', '财务部', '人事部'][i % 4],
role: ['前端工程师', '产品经理', '会计', '招聘专员'][i % 4],
}))
const selected = ref<BizTablePickerRow[]>([])
</script>
<template>
<div style="max-width: 640px">
<BizTablePicker
v-model="selected"
:columns="columns"
:data="all"
:multiple="true"
:page-size="8"
title="选择抄送人"
trigger-text="选择抄送人"
label-key="name"
:recent="{ storageKey: 'demo-picker-cc', max: 8 }"
/>
<p style="margin-top: 10px; font-size: 13px; color: var(--el-text-color-secondary)">
已选 {{ selected.length }} 人;确认选择后再次打开弹窗,「最近选择」页签会带出上次确认的人(最多留 8 条,可悬停移除单条记录)。
</p>
</div>
</template>
显示代码
最近选择(服务端受控)
暂未选择
受控模式:最近记录来自服务端(跨设备一致),确认选择不写 localStorage;需要服务端留痕时监听 confirm 事件自行持久化。
recent.initial 受控模式:最近记录由服务端下发(跨设备一致),组件不写 localStorage;需要服务端留痕时监听 confirm 事件自行持久化后回写 initial。<script setup lang="ts">
/** 场景:最近选择受控模式——recent.initial 由服务端下发(跨设备同步),不写本地存储 */
import { ref } from 'vue'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerColumn, BizTablePickerRow } from 'vue3-ep-biz-lib'
const columns: BizTablePickerColumn[] = [
{ prop: 'orderNo', label: '订单号', width: 140 },
{ prop: 'customer', label: '客户', minWidth: 140 },
{ prop: 'amount', label: '金额(元)', width: 100, align: 'right' },
]
const all: BizTablePickerRow[] = Array.from({ length: 24 }, (_, i) => ({
id: i + 1,
orderNo: `SO-2026${String(1200 + i)}`,
customer: `客户${String.fromCodePoint(0x4e01 + (i % 20))}`,
amount: 500 + i * 120,
}))
/** 服务端最近浏览订单(真实项目来自接口) */
const serverRecent: BizTablePickerRow[] = [all[3], all[11], all[19]]
const selected = ref<BizTablePickerRow | null>(null)
</script>
<template>
<div style="max-width: 640px">
<BizTablePicker
v-model="selected"
:columns="columns"
:data="all"
title="关联订单"
trigger-text="关联订单"
label-key="orderNo"
:recent="{ initial: serverRecent }"
/>
<p style="margin-top: 10px; font-size: 13px; color: var(--el-text-color-secondary)">
受控模式:最近记录来自服务端(跨设备一致),确认选择不写 localStorage;需要服务端留痕时监听 confirm 事件自行持久化。
</p>
</div>
</template>
显示代码
拼音检索
暂未选择
pinyin-of / initials-of 注入全拼与首字母转换器(本地 data 模式生效,匹配 label-key 指向的回显字段):「张伟」「zhangwei」「zw」三种输入均可命中;search 形态 label-key 默认回退 row-key(如 id),拼音检索需显式指定 label-key="name" 等文本字段;转换器由业务注入(生产建议 pinyin-pro),组件保持零字典依赖。<script setup lang="ts">
/** 拼音检索:注入全拼 / 首字母转换器(生产建议 pinyin-pro),支持「张伟」「zhangwei」「zw」三种输入 */
import { ref } from 'vue'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerRow } from 'vue3-ep-biz-lib'
interface MemberRow extends BizTablePickerRow {
id: number
name: string
dept: string
}
const data: MemberRow[] = [
{ id: 1, name: '张伟', dept: '研发中心' },
{ id: 2, name: '王芳', dept: '产品设计部' },
{ id: 3, name: '李娜', dept: '人力资源部' },
{ id: 4, name: '刘强', dept: '研发中心' },
{ id: 5, name: '陈静', dept: '财务部' },
{ id: 6, name: '杨洋', dept: '市场运营部' },
{ id: 7, name: '赵敏', dept: '研发中心' },
{ id: 8, name: '周杰', dept: '行政部' },
]
/**
* 演示用拼音映射(仅覆盖示例姓名);生产环境注入 pinyin-pro:
* pinyinOf: (t) => pinyin(t, { toneType: 'none' })
* initialsOf: (t) => pinyin(t, { pattern: 'first', toneType: 'none' })
*/
const DICT: Record<string, string> = {
张伟: 'zhangwei|zw',
王芳: 'wangfang|wf',
李娜: 'lina|ln',
刘强: 'liuqiang|lq',
陈静: 'chenjing|cj',
杨洋: 'yangyang|yy',
赵敏: 'zhaomin|zm',
周杰: 'zhoujie|zj',
}
const pinyinOf = (text: string): string => (DICT[text] ?? '').split('|')[0] ?? ''
const initialsOf = (text: string): string => (DICT[text] ?? '').split('|')[1] ?? ''
const selected = ref<BizTablePickerRow[]>([])
</script>
<template>
<div>
<BizTablePicker
v-model="selected"
multiple
:data="data"
:columns="[
{ prop: 'name', label: '姓名' },
{ prop: 'dept', label: '部门' },
]"
row-key="id"
label-key="name"
title="选择成员"
trigger-text="选择成员(支持拼音)"
:pinyin-of="pinyinOf"
:initials-of="initialsOf"
/>
</div>
</template>
显示代码
头像列
暂未选择
avatar-key 传入即在表格最前插入头像列:字段有值渲染图片头像,空值回退回显文本首字。<script setup lang="ts">
/** 头像列:avatar-key 传入即在表格最前插入头像列,字段有值渲染图片、空值回退姓名首字 */
import { ref } from 'vue'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerRow } from 'vue3-ep-biz-lib'
interface MemberRow extends BizTablePickerRow {
id: number
name: string
dept: string
avatar: string
}
const data: MemberRow[] = [
{ id: 1, name: '张伟', dept: '研发中心', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=80&h=80&fit=crop' },
{ id: 2, name: '王芳', dept: '产品设计部', avatar: '' },
{ id: 3, name: '李娜', dept: '人力资源部', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&h=80&fit=crop' },
{ id: 4, name: '刘强', dept: '研发中心', avatar: '' },
]
const selected = ref<BizTablePickerRow | null>(null)
</script>
<template>
<div>
<BizTablePicker
v-model="selected"
:data="data"
:columns="[
{ prop: 'name', label: '姓名' },
{ prop: 'dept', label: '部门' },
]"
row-key="id"
label-key="name"
title="指派负责人"
trigger-text="指派负责人(带头像)"
avatar-key="avatar"
/>
</div>
</template>
显示代码
头像 + 最近选择
暂未选择
钉钉式选人组合:头像列 +
recent 本地留痕页签(最近记录同步带头像)。<script setup lang="ts">
/** 头像 + 最近选择组合:钉钉式选人(头像列 + 最近页签本地留痕 + 勾选态头像同步) */
import { ref } from 'vue'
import { BizTablePicker } from 'vue3-ep-biz-lib'
import type { BizTablePickerRow } from 'vue3-ep-biz-lib'
interface MemberRow extends BizTablePickerRow {
id: number
name: string
dept: string
avatar: string
}
const data: MemberRow[] = [
{ id: 1, name: '张伟', dept: '研发中心', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=80&h=80&fit=crop' },
{ id: 2, name: '王芳', dept: '产品设计部', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&h=80&fit=crop' },
{ id: 3, name: '李娜', dept: '人力资源部', avatar: '' },
{ id: 4, name: '刘强', dept: '研发中心', avatar: '' },
{ id: 5, name: '陈静', dept: '财务部', avatar: '' },
{ id: 6, name: '杨洋', dept: '市场运营部', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=80&h=80&fit=crop' },
]
const selected = ref<BizTablePickerRow[]>([])
</script>
<template>
<div>
<BizTablePicker
v-model="selected"
multiple
:data="data"
:columns="[
{ prop: 'name', label: '姓名' },
{ prop: 'dept', label: '部门' },
]"
row-key="id"
label-key="name"
title="选择评审人"
trigger-text="选择评审人(头像 + 最近)"
avatar-key="avatar"
:recent="{ storageKey: 'demo-avatar-recent', max: 6 }"
/>
</div>
</template>
显示代码
Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| model-value / v-model | BizTablePickerRow[] | BizTablePickerRow | null | null | 多选为行数组,单选为行对象 |
| filter-mode | 'search' | 'tree' | 'search' | 筛选区形态:search 顶部搜索栏;tree 左侧组织树 |
| columns | BizTablePickerColumn[] | [] | 表格列(prop / label / width / minWidth / align / formatter) |
| data | BizTablePickerRow[] | [] | 本地数据源(search 形态与 request 二选一;自动分页 + 关键词跨列过滤) |
| request | (params: { page; pageSize; keyword; deptId? }) => Promise<{ list; total }> | — | 远程数据源契约(tree 形态必填,携带当前 deptId) |
| multiple | boolean | false | 多选 |
| row-key | string | 'id' | 行主键字段(选择判定) |
| label-key | string | tree 形态 'name',search 形态同 row-key | 回显标签字段 |
| recent | BizTablePickerRecentConfig | — | 最近选择配置:{ storageKey, max?, initial? };传入后弹窗内出现「全部数据 / 最近选择」页签,确认自动留痕(storageKey 存本地,initial 受控不写本地),max 默认 10 |
| org-tree | BizTablePickerOrgNode[] | — | 静态组织树(tree 形态;与 org-load-request 二选一) |
| org-load-request | (node) => Promise<BizTablePickerOrgNode[]> | — | 组织树懒加载(展开拉子级;根级传入 id 为 'root' 的占位节点) |
| title | string | '选择数据' | 弹窗标题 |
| trigger-text | string | '选择数据' | 触发按钮文案 |
| trigger-type | 'primary' | 'success' | 'warning' | 'danger' | 'info' | '' | 'primary' | 触发按钮类型(透传 el-button,plain 风格) |
| dialog-width | string | number | search 'min(760px, 94vw)',tree 'min(880px, 94vw)' | 弹窗宽度(窄屏自动收缩) |
| page-size | number | 10 | 每页条数 |
| clearable | boolean | true | 已选标签可单个移除 |
| disabled | boolean | false | 禁用触发 |
| empty-text | string | '暂未选择' | 已选区空占位 |
| pinyin-of | (text: string) => string | — | 拼音全拼转换注入(本地 data 模式):关键词额外按回显字段(label-key,search 形态默认回退 row-key,需指向姓名等文本字段)全拼匹配,如 pinyin-pro (t) => pinyin(t, { toneType: 'none' }) |
| initials-of | (text: string) => string | — | 拼音首字母转换注入:支持 zhangwei / zw 式检索(与 pinyin-of 可只传其一) |
| avatar-key | string | — | 头像字段名(表格最前插入头像列 + 最近记录头像位;空值回退首字) |
其余属性透传触发按钮(el-button)。
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
| update:model-value | (rows | row | null) | 确认或移除标签后回发 |
| change | (payload: { selected: rows; keys: unknown[] }) | 选中集合变化(含主键数组) |
| confirm | (payload: { selected: rows; keys: unknown[] }) | 弹窗内点击确定(search 形态多选空选时阻止并提示) |
| cancel | () | 弹窗未确认关闭(取消按钮 / 右上角关闭 / ESC) |
Slots
| 插槽名 | 作用域 | 说明 |
|---|---|---|
| trigger | — | 自定义触发按钮内容 |
col-字段名 | { row, index } | 弹窗表格自定义单元格 |
Expose
| 名称 | 类型 | 说明 |
|---|---|---|
| open | () => void | 主动打开选择弹窗 |
| close | () => void | 关闭弹窗(未确认关闭触发 cancel) |
| clear | () => void | 清空已选并回发 v-model 与 change |
| clearRecent | () => void | 清空最近选择记录(含本地存储) |
| getSelected | () => rows | 读取弹窗会话内当前选中行集合(单选为长度 ≤ 1 的数组) |