refactor(batch-position): 批量职务弹窗的部门员工树改用统一数据源
新增 composables/useDeptEmployeeTree.ts(部门树 → 成员的单一实现,数据源 /API/FormAPI/DeptEmployeeSearch,与 EntityPicker(treeGroup) 同源):分页拉全 + 服务端部门树组装 + 统一成员标签,单部门模式返回单个分组节点。BatchPositionDialog 的公司页 / 部门列表页 / 部门详情页三个分支改为调用它,删除各自分页拉 UserRoleSearch 再前端拼树、以及 Get/UserRoleDepartment 的重复实现;工作组相关分支(非部门树)保持不动。顺带修正未分组节点 id 未计入 groupIds 导致勾选它会被当作成员 PK 提交的问题。
This commit is contained in:
1 parent
d83866542e
commit
01d3cd1b9a
2 files changed
+180
-69
No files matched your search
@@ -4,7 +4,11 @@
|
||||
*
|
||||
* 三个入口(部门详情 / 工作组详情 / 公司详情-员工)共用的批量职务操作:
|
||||
* - 多选职务(tree 视图:通用部门树 → 职务)
|
||||
* - 多选员工(tree 视图:公司页=部门树分组员工 UserRoleSearch;部门/工作组页=各自成员树)
|
||||
* - 多选员工(tree 视图):
|
||||
* 公司页 / 部门列表页 / 部门详情页 → 部门树 → 成员,统一走
|
||||
* composables/useDeptEmployeeTree.ts(数据源 /API/FormAPI/DeptEmployeeSearch,
|
||||
* 与表单里的 EntityPicker(treeGroup) 同源,避免"同功能两套代码")
|
||||
* 工作组页 → 工作组树 → 成员(WorkgroupSearch + UserRoleWorkgroup)
|
||||
* - 批量增加 / 批量删除职务(后端 FormService.batch_positions,统一落
|
||||
* UserRoleDepartmentPosition 子表,工作组/公司页解析到成员的"主部门关联")
|
||||
*
|
||||
@@ -16,6 +20,7 @@ import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { formApi } from '@/api/form'
|
||||
import { get } from '@/api/request'
|
||||
import { loadDeptEmployeeTree } from '@/composables/useDeptEmployeeTree'
|
||||
|
||||
/** 批量目标表 */
|
||||
type BatchTargetTable =
|
||||
@@ -151,7 +156,7 @@ async function fetchTreeData(
|
||||
}
|
||||
}
|
||||
|
||||
/** 分页拉全(UserRoleSearch 等按 offset+has_more 翻页;安全上限防失控) */
|
||||
/** 分页拉全(WorkgroupSearch / PositionSearch 等按 offset+has_more 翻页;安全上限防失控) */
|
||||
const PAGE_SIZE = 20
|
||||
const MAX_FETCH = 500
|
||||
|
||||
@@ -159,76 +164,23 @@ const MAX_FETCH = 500
|
||||
async function loadEmployeeTree(): Promise<void> {
|
||||
const ctx = props.context ?? {}
|
||||
selectedEmployeePks.value = new Set()
|
||||
if (props.table === 'UserRole') {
|
||||
// 公司页:UserRoleSearch 部门树 + 员工(user-role 对)按部门分组(翻页拉全)
|
||||
let offset = 0
|
||||
let tree: unknown[] = []
|
||||
const allList: unknown[] = []
|
||||
let hasMore = true
|
||||
while (hasMore) {
|
||||
const params: Record<string, unknown> = { keyword: '', tree: '1', offset }
|
||||
if (ctx.CompanyUUID) params.CompanyUUID = ctx.CompanyUUID
|
||||
const r = await fetchTreeData('/API/FormAPI/UserRoleSearch', params)
|
||||
if (!tree.length) tree = r.tree
|
||||
allList.push(...r.list)
|
||||
hasMore = r.hasMore
|
||||
offset += PAGE_SIZE
|
||||
if (allList.length >= MAX_FETCH) hasMore = false
|
||||
}
|
||||
const leaves = allList
|
||||
.map((r) => r as Record<string, unknown>)
|
||||
.map((r) => ({
|
||||
groupKey: r.DeptUUID ? String(r.DeptUUID) : '',
|
||||
id: String(r.UserRoleUUID ?? ''),
|
||||
// 角色覆盖名优先,空则角色真名(对齐员工列表约定)
|
||||
label: `${String((r.RoleNameCover || r.RoleName) ?? '')}${r.UserName ? `(${String(r.UserName)})` : ''}` || String(r.UserRoleUUID ?? ''),
|
||||
}))
|
||||
.filter((l) => l.id)
|
||||
const built = buildTree(tree as Array<{ uuid: string; name: string; parentUuid?: string }>, leaves)
|
||||
if (props.table === 'UserRole' || props.table === 'Department') {
|
||||
// 公司页 / 部门列表页:部门树 → 成员,取 UserRoleUUID(按"用户-角色对"去重)
|
||||
// 数据源与表单选择器同源,见 composables/useDeptEmployeeTree.ts
|
||||
const built = await loadDeptEmployeeTree({
|
||||
CompanyUUID: ctx.CompanyUUID,
|
||||
DedupUserRole: true,
|
||||
ValueField: 'UserRoleUUID',
|
||||
})
|
||||
employeeTree.value = built.tree
|
||||
employeeGroupIds.value = built.groupIds
|
||||
} else if (props.table === 'UserRoleDepartment') {
|
||||
// 部门页:当前部门成员(UserRoleDepartment 行)
|
||||
const params: Record<string, unknown> = { DeptUUID: ctx.DeptUUID || '', CompanyUUID: ctx.CompanyUUID || '' }
|
||||
const res = await get<unknown>('/API/FormAPI/Get/UserRoleDepartment', params)
|
||||
const list = Array.isArray(res) ? (res as unknown[]) : (res as Record<string, unknown>)?.list || []
|
||||
const leaves = (list as Record<string, unknown>[])
|
||||
.map((r) => ({
|
||||
groupKey: '',
|
||||
id: String(r.UserRoleDepartmentUUID ?? ''),
|
||||
// 角色覆盖名优先,空则角色真名(对齐员工列表约定)
|
||||
label: `${String((r.RoleNameCover || r.UserRoleUUID_label) ?? '')}${r.UserUUID_label ? `(${String(r.UserUUID_label)})` : ''}` || String(r.UserRoleDepartmentUUID ?? ''),
|
||||
}))
|
||||
.filter((l) => l.id)
|
||||
const built = buildTree([], leaves)
|
||||
employeeTree.value = [{ id: '__dept__', label: '本部门员工', children: built.tree }]
|
||||
employeeGroupIds.value = new Set(['__dept__', ...built.groupIds])
|
||||
} else if (props.table === 'Department') {
|
||||
// 部门列表页:以部门为树组织员工(UserRoleSearch 部门树分组,公司键取部门所属公司)
|
||||
let offset = 0
|
||||
let tree: unknown[] = []
|
||||
const allList: unknown[] = []
|
||||
let hasMore = true
|
||||
while (hasMore) {
|
||||
const params: Record<string, unknown> = { keyword: '', tree: '1', offset }
|
||||
if (ctx.CompanyUUID) params.CompanyUUID = ctx.CompanyUUID
|
||||
const r = await fetchTreeData('/API/FormAPI/UserRoleSearch', params)
|
||||
if (!tree.length) tree = r.tree
|
||||
allList.push(...r.list)
|
||||
hasMore = r.hasMore
|
||||
offset += PAGE_SIZE
|
||||
if (allList.length >= MAX_FETCH) hasMore = false
|
||||
}
|
||||
const leaves = allList
|
||||
.map((r) => r as Record<string, unknown>)
|
||||
.map((r) => ({
|
||||
groupKey: r.DeptUUID ? String(r.DeptUUID) : '',
|
||||
id: String(r.UserRoleUUID ?? ''),
|
||||
// 角色覆盖名优先,空则角色真名(对齐员工列表约定)
|
||||
label: `${String((r.RoleNameCover || r.RoleName) ?? '')}${r.UserName ? `(${String(r.UserName)})` : ''}` || String(r.UserRoleUUID ?? ''),
|
||||
}))
|
||||
.filter((l) => l.id)
|
||||
const built = buildTree(tree as Array<{ uuid: string; name: string; parentUuid?: string }>, leaves)
|
||||
// 部门详情页:本部门成员,取 UserRoleDepartmentUUID(单个分组节点)
|
||||
const built = await loadDeptEmployeeTree({
|
||||
CompanyUUID: ctx.CompanyUUID,
|
||||
DeptUUID: ctx.DeptUUID,
|
||||
ValueField: 'UserRoleDepartmentUUID',
|
||||
})
|
||||
employeeTree.value = built.tree
|
||||
employeeGroupIds.value = built.groupIds
|
||||
} else if (props.table === 'Workgroup') {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 部门树 → 成员 两级树数据源(el-tree 自绘场景的单一实现)
|
||||
*
|
||||
* 数据源 /API/FormAPI/DeptEmployeeSearch —— 与表单里的 EntityPicker(treeGroup) 同源:
|
||||
* 一项 = 一条 UserRoleDepartment(部门 + 该用户在本公司的"用户-角色对"),
|
||||
* 响应带 tree(部门树)、DeptEmployeeLabel("部门名 · 用户名")与
|
||||
* DeptUUID / UserName / RoleName / UserRoleUUID / UserRoleDepartmentUUID 等字段。
|
||||
*
|
||||
* 为什么单独抽出:表单内"选人/选成员"请直接用 EntityPicker(config.treeGroup 已封装同一数据源);
|
||||
* 而 el-tree 自绘场景(如批量调整职务弹窗)需要的是"树 + 勾选"形态,无法复用同一个组件,
|
||||
* 但必须复用同一套数据源与节点组装逻辑 —— 否则就会出现"功能相同、两套代码"。
|
||||
*
|
||||
* 对应后端单一来源配置:gvsds/apps/gerp/form_service.py 的 DEPT_TREE_GROUP / DEPT_EMPLOYEE_PICKER*
|
||||
*/
|
||||
import { get } from '@/api/request'
|
||||
|
||||
/** 树节点(el-tree:node-key="id",props.label="label") */
|
||||
export interface DeptEmployeeTreeNode {
|
||||
id: string
|
||||
label: string
|
||||
children?: DeptEmployeeTreeNode[]
|
||||
}
|
||||
|
||||
/** DeptEmployeeSearch 返回行(仅声明用到的字段) */
|
||||
interface DeptEmployeeRow {
|
||||
DeptEmployeeLabel?: string
|
||||
DeptName?: string
|
||||
DeptUUID?: string
|
||||
UserName?: string
|
||||
RoleName?: string
|
||||
UserRoleUUID?: string
|
||||
UserRoleDepartmentUUID?: string
|
||||
}
|
||||
|
||||
/** 部门树节点原始结构(后端 tree 字段) */
|
||||
interface DeptTreeNodeRaw {
|
||||
uuid: string
|
||||
name?: string
|
||||
parentUuid?: string
|
||||
}
|
||||
|
||||
export interface LoadDeptEmployeeTreeOptions {
|
||||
/** 公司上下文(不传则后端回退当前角色公司) */
|
||||
CompanyUUID?: string
|
||||
/** 只取某部门的成员(部门详情页):此时不请求部门树,返回单个分组节点 */
|
||||
DeptUUID?: string
|
||||
/** 按"用户-角色对"去重(同一用户多部门只留一条,供只认 UserRoleUUID 的场景使用) */
|
||||
DedupUserRole?: boolean
|
||||
/** 叶子取值列(默认 UserRoleUUID;部门详情页传 UserRoleDepartmentUUID) */
|
||||
ValueField?: 'UserRoleUUID' | 'UserRoleDepartmentUUID'
|
||||
/** 单部门模式的组节点标题(默认"本部门员工") */
|
||||
SingleGroupTitle?: string
|
||||
}
|
||||
|
||||
/** 每页条数(与后端 Search 接口一致) */
|
||||
const PAGE_SIZE = 20
|
||||
/** 分页拉全的安全上限(防止异常数据把前端拖死) */
|
||||
const MAX_FETCH = 500
|
||||
|
||||
/** 成员叶子标签:角色名(用户名)——与成员列表口径一致;缺失时回退 DeptEmployeeLabel / 主键 */
|
||||
function memberLabel(row: DeptEmployeeRow, id: string): string {
|
||||
const name = `${row.RoleName || ''}${row.UserName ? `(${row.UserName})` : ''}`
|
||||
return name || row.DeptEmployeeLabel || id
|
||||
}
|
||||
|
||||
/** 响应拦截器已解包 {code,data}:兼容纯数组与 {list, tree, has_more} */
|
||||
function parsePage(res: unknown): { list: DeptEmployeeRow[]; tree: DeptTreeNodeRaw[]; hasMore: boolean } {
|
||||
if (Array.isArray(res)) return { list: res as DeptEmployeeRow[], tree: [], hasMore: false }
|
||||
const obj = (res ?? {}) as Record<string, unknown>
|
||||
return {
|
||||
list: Array.isArray(obj.list) ? (obj.list as DeptEmployeeRow[]) : [],
|
||||
tree: Array.isArray(obj.tree) ? (obj.tree as DeptTreeNodeRaw[]) : [],
|
||||
hasMore: typeof obj.has_more === 'boolean' ? obj.has_more : false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页拉全部门树与成员,组装成 el-tree 数据(分组节点为部门,叶子为成员)
|
||||
*
|
||||
* @returns tree(树数据)与 groupIds(分组节点 id 集合,提交时用于过滤掉非成员节点)
|
||||
*/
|
||||
export async function loadDeptEmployeeTree(
|
||||
options: LoadDeptEmployeeTreeOptions = {},
|
||||
): Promise<{ tree: DeptEmployeeTreeNode[]; groupIds: Set<string> }> {
|
||||
const singleDept = options.DeptUUID || ''
|
||||
const rows: DeptEmployeeRow[] = []
|
||||
let rawTree: DeptTreeNodeRaw[] = []
|
||||
let offset = 0
|
||||
let hasMore = true
|
||||
while (hasMore) {
|
||||
const params: Record<string, unknown> = { keyword: '', offset }
|
||||
if (options.CompanyUUID) params.CompanyUUID = options.CompanyUUID
|
||||
if (singleDept) params.DeptUUID = singleDept
|
||||
if (options.DedupUserRole) params.Dedup = 'UserRole'
|
||||
// 部门树:仅"全公司树"模式需要(单部门模式只需成员)
|
||||
if (!singleDept) params.tree = '1'
|
||||
const page = parsePage(await get<unknown>('/API/FormAPI/DeptEmployeeSearch', params))
|
||||
if (!rawTree.length) rawTree = page.tree
|
||||
rows.push(...page.list)
|
||||
hasMore = page.hasMore
|
||||
offset += PAGE_SIZE
|
||||
if (rows.length >= MAX_FETCH) hasMore = false
|
||||
}
|
||||
|
||||
const valueField = options.ValueField || 'UserRoleUUID'
|
||||
const leaves: Array<{ groupKey: string; id: string; label: string }> = []
|
||||
for (const row of rows) {
|
||||
const id = String(row[valueField] ?? '')
|
||||
if (!id) continue
|
||||
leaves.push({
|
||||
groupKey: singleDept || String(row.DeptUUID ?? ''),
|
||||
id,
|
||||
label: memberLabel(row, id),
|
||||
})
|
||||
}
|
||||
|
||||
// 单部门模式:不展示全公司树,只给一个分组节点(与部门详情页语义一致)
|
||||
if (singleDept) {
|
||||
const groupId = '__dept__'
|
||||
return {
|
||||
tree: [{
|
||||
id: groupId,
|
||||
label: options.SingleGroupTitle || '本部门员工',
|
||||
children: leaves.map((l) => ({ id: l.id, label: l.label })),
|
||||
}],
|
||||
groupIds: new Set([groupId]),
|
||||
}
|
||||
}
|
||||
|
||||
// 全公司模式:部门树 + 成员按 DeptUUID 归组,未挂部门的归入"未分组"
|
||||
const nodeMap = new Map<string, DeptEmployeeTreeNode>()
|
||||
for (const n of rawTree) {
|
||||
if (!n.uuid) continue
|
||||
nodeMap.set(n.uuid, { id: n.uuid, label: n.name || n.uuid, children: [] })
|
||||
}
|
||||
const roots: DeptEmployeeTreeNode[] = []
|
||||
for (const n of rawTree) {
|
||||
const node = nodeMap.get(n.uuid)
|
||||
if (!node) continue
|
||||
if (n.parentUuid && nodeMap.has(n.parentUuid)) {
|
||||
nodeMap.get(n.parentUuid)!.children!.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
}
|
||||
const ungrouped: DeptEmployeeTreeNode[] = []
|
||||
for (const leaf of leaves) {
|
||||
const group = leaf.groupKey ? nodeMap.get(leaf.groupKey) : undefined
|
||||
const node: DeptEmployeeTreeNode = { id: leaf.id, label: leaf.label }
|
||||
if (group) group.children!.push(node)
|
||||
else ungrouped.push(node)
|
||||
}
|
||||
const groupIds = new Set<string>(nodeMap.keys())
|
||||
if (ungrouped.length) {
|
||||
roots.push({ id: '__ungrouped__', label: '未分组', children: ungrouped })
|
||||
groupIds.add('__ungrouped__')
|
||||
}
|
||||
return { tree: roots, groupIds }
|
||||
}
|
||||
Reference in new issue
Block a user