Files
knowledge-governance/web/src/qaRegionSelection.ts
T
姚章浩 154b0b62ad
build / build (push) Successful in 1m20s
feat(qaops): 完成区域统计与部门绑定六项联动
新增北京时间自然周校验与缓存隔离,默认展开到业务二级部门。
选中部门在统计页就地绑定;区域名称列表原位增删改,双版本锁、自动预览、独立读回及删除确认。
筛选和两张区域看板使用同一已保存目录,删除失效筛选回全部。

验证:完整 make verify、全 mirror 隔离 PostgreSQL race、自然周边界与缓存键负例通过;真实恢复库327部门逐行对拍及六项浏览器验收通过。
schema57无迁移,不改真实公司区域绑定或原始问答,不连接DobotAI生产源库;PNG导出本轮不冒充已验证。
2026-10-19 23:41:01 +08:00

123 lines
4.3 KiB
TypeScript

import type { DepartmentRegion, RegionRule } from './qaOrg'
export type DepartmentNode = { id: string; parent_id: string; name: string }
export function defaultDepartmentCollapsed(nodes: DepartmentNode[]): Set<string> {
return new Set(
departmentTree(nodes, new Set())
.filter((r) => r.depth >= 2 && r.hasChildren)
.map((r) => r.department.id)
)
}
export function subtreeIDs(nodes: DepartmentNode[], ids: string[]): string[] {
const children = new Map<string, string[]>(),
known = new Set(nodes.map((n) => n.id))
for (const node of nodes) {
const list = children.get(node.parent_id) ?? []
list.push(node.id)
children.set(node.parent_id, list)
}
const seen = new Set<string>(),
queue = [...ids]
while (queue.length) {
const id = queue.pop()!
if (!known.has(id)) throw Error('部门不在当前通讯录')
if (seen.has(id)) continue
seen.add(id)
queue.push(...(children.get(id) ?? []))
}
return [...seen]
}
export function selectDepartmentBranch(
nodes: DepartmentNode[],
selected: string[],
id: string,
checked: boolean
): string[] {
const ids = subtreeIDs(nodes, [id]),
next = new Set(selected)
if (checked) ids.forEach((x) => next.add(x))
else {
ids.forEach((x) => next.delete(x))
const parents = new Map(nodes.map((n) => [n.id, n.parent_id]))
const seen = new Set<string>()
let parent = parents.get(id)
while (parent && !seen.has(parent)) {
seen.add(parent)
next.delete(parent)
parent = parents.get(parent)
}
}
return [...next]
}
export function departmentTree<T extends DepartmentNode>(
nodes: T[],
collapsed: ReadonlySet<string>,
search = '',
region = ''
): Array<{ department: T; depth: number; hasChildren: boolean }> {
const known = new Map(nodes.map((n) => [n.id, n])),
children = new Map<string, T[]>()
if (known.size !== nodes.length) throw Error('通讯录部门重复')
for (const node of nodes) {
if (node.parent_id && !known.has(node.parent_id)) throw Error('通讯录缺少父部门')
const list = children.get(node.parent_id) ?? []
list.push(node)
children.set(node.parent_id, list)
}
for (const list of children.values())
list.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN') || a.id.localeCompare(b.id))
const all: Array<{ department: T; depth: number; hasChildren: boolean; parents: string[]; path: string }> = [],
seen = new Set<string>()
const walk = (parent: string, parents: string[], names: string[]) => {
for (const node of children.get(parent) ?? []) {
if (seen.has(node.id) || parents.length > 100) throw Error('通讯录部门层级无效')
seen.add(node.id)
all.push({
department: node,
depth: parents.length,
hasChildren: !!children.get(node.id)?.length,
parents,
path: [...names, node.name].join('/'),
})
walk(node.id, [...parents, node.id], [...names, node.name])
}
}
walk('', [], [])
if (all.length !== nodes.length) throw Error('通讯录部门存在循环')
if (search || region) {
const matches = all.filter(
(r) =>
(!search || r.path.toLowerCase().includes(search.toLowerCase())) &&
(!region || (r.department as DepartmentNode & { region?: string }).region === region)
)
const visible = new Set(matches.flatMap((r) => [r.department.id, ...r.parents]))
return all.filter((r) => visible.has(r.department.id))
}
return all.filter((r) => !r.parents.some((id) => collapsed.has(id)))
}
export function applyBulkRegionRules(rules: RegionRule[], ids: string[], region: string): RegionRule[] {
const selected = new Set(ids)
if (!selected.size) return [...rules]
if (!region) throw new Error('请先选择区域')
return [
...rules.filter((r) => !selected.has(r.department_id)),
...Array.from(selected, (department_id) => ({ department_id, region })),
]
}
export function departmentLevels(
departments: DepartmentRegion[],
id: string,
nodes = new Map(departments.map((d) => [d.id, d]))
): string[] {
const seen = new Set<string>(),
names: string[] = []
let current = id
while (current) {
const node = nodes.get(current)
if (!node || seen.has(current) || names.length > 100) throw new Error('通讯录部门层级无效,请重新同步')
seen.add(current)
names.unshift(node.name)
current = node.parent_id
}
return names
}