From bad87029d3a18ccd87d75952374220c70e85472a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A4=E5=AD=A6=E4=BC=9A?= Date: Mon, 15 Jun 2026 02:38:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BB=84=E7=BB=87=E6=A0=91?= =?UTF-8?q?=E4=B8=8A=E7=BA=A7=E8=8A=82=E7=82=B9=E4=BA=BA=E5=91=98=E6=B1=87?= =?UTF-8?q?=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/OrgPermissionTree.vue | 27 ++------ frontend/src/utils/orgTreeEmployees.js | 68 +++++++++++++++++++ frontend/src/utils/orgTreeEmployees.test.js | 68 +++++++++++++++++++ 3 files changed, 141 insertions(+), 22 deletions(-) create mode 100644 frontend/src/utils/orgTreeEmployees.js create mode 100644 frontend/src/utils/orgTreeEmployees.test.js diff --git a/frontend/src/components/OrgPermissionTree.vue b/frontend/src/components/OrgPermissionTree.vue index 65a16c8..c883a96 100644 --- a/frontend/src/components/OrgPermissionTree.vue +++ b/frontend/src/components/OrgPermissionTree.vue @@ -95,6 +95,7 @@ import { computed, defineComponent, h, ref, watch } from "vue"; import PaginationBar from "./PaginationBar.vue"; import StatusBadge from "./StatusBadge.vue"; +import { collectOrganizationEmployees, countOrganizationEmployees } from "../utils/orgTreeEmployees"; import { usePagination } from "../utils/pagination"; const props = defineProps({ @@ -228,7 +229,7 @@ const requestedNode = computed(() => findNodeById(organizationNodes.value, props const selectedNode = computed(() => requestedNode.value || firstRootNode.value); const effectiveSelectedNodeId = computed(() => getNodeId(selectedNode.value) ?? ""); const selectedNodeLabel = computed(() => selectedNode.value ? nodeLabel(selectedNode.value) : "未选择组织"); -const selectedEmployees = computed(() => Array.isArray(selectedNode.value?.employees) ? selectedNode.value.employees : []); +const selectedEmployees = computed(() => collectOrganizationEmployees(selectedNode.value)); const canAddEmployee = computed(() => ["DEPARTMENT", "GROUP"].includes(selectedNode.value?.node_type)); const filteredEmployees = computed(() => { const keyword = searchKeyword.value.trim().toLowerCase(); @@ -358,25 +359,7 @@ function nodeTypeShortLabel(type) { } function employeeCount(node) { - const employeeKeys = new Set(); - collectEmployeeKeys(node, employeeKeys); - return employeeKeys.size; -} - -function collectEmployeeKeys(node, employeeKeys) { - if (!node) { - return; - } - for (const employee of Array.isArray(node.employees) ? node.employees : []) { - employeeKeys.add(String(employeeKey(employee))); - } - for (const child of Array.isArray(node.children) ? node.children : []) { - if (isOrganizationNode(child)) { - collectEmployeeKeys(child, employeeKeys); - } else if (child?.employee_id) { - employeeKeys.add(String(employeeKey(child))); - } - } + return countOrganizationEmployees(node); } function toggleNode(node) { @@ -437,8 +420,8 @@ function employeeKey(employee) { function employeeWithParent(employee) { return { ...employee, - parent_node_id: getNodeId(selectedNode.value), - parent_node_label: nodeLabel(selectedNode.value) + parent_node_id: employee?.parent_node_id ?? getNodeId(selectedNode.value), + parent_node_label: employee?.parent_node_label ?? nodeLabel(selectedNode.value) }; } diff --git a/frontend/src/utils/orgTreeEmployees.js b/frontend/src/utils/orgTreeEmployees.js new file mode 100644 index 0000000..1e49faa --- /dev/null +++ b/frontend/src/utils/orgTreeEmployees.js @@ -0,0 +1,68 @@ +export function collectOrganizationEmployees(node) { + const employees = []; + const seenKeys = new Set(); + + collectFromNode(node, node, employees, seenKeys); + return employees; +} + +export function countOrganizationEmployees(node) { + return collectOrganizationEmployees(node).length; +} + +function collectFromNode(node, parentNode, employees, seenKeys) { + if (!node) { + return; + } + + if (isEmployeeNode(node)) { + appendEmployee(node, parentNode, employees, seenKeys); + return; + } + + for (const employee of Array.isArray(node.employees) ? node.employees : []) { + appendEmployee(employee, node, employees, seenKeys); + } + + for (const child of Array.isArray(node.children) ? node.children : []) { + if (isOrganizationNode(child)) { + collectFromNode(child, child, employees, seenKeys); + } else if (isEmployeeNode(child)) { + appendEmployee(child, node, employees, seenKeys); + } + } +} + +function appendEmployee(employee, parentNode, employees, seenKeys) { + const key = employeeKey(employee); + if (!key || seenKeys.has(key)) { + return; + } + + seenKeys.add(key); + employees.push({ + ...employee, + parent_node_id: employee.parent_node_id ?? nodeId(parentNode), + parent_node_label: employee.parent_node_label ?? nodeLabel(parentNode) + }); +} + +function isOrganizationNode(node) { + return Boolean(node) && node.node_type !== "EMPLOYEE" && !node.employee_id; +} + +function isEmployeeNode(node) { + return Boolean(node) && (node.node_type === "EMPLOYEE" || Boolean(node.employee_id)); +} + +function employeeKey(employee) { + return String(employee?.employee_id ?? employee?.user_id ?? employee?.username ?? employee?.mobile ?? employee?.phone ?? ""); +} + +function nodeId(node) { + return node?.node_id ?? node?.id ?? node?.value ?? ""; +} + +function nodeLabel(node) { + return node?.node_label || node?.label || node?.name || ""; +} diff --git a/frontend/src/utils/orgTreeEmployees.test.js b/frontend/src/utils/orgTreeEmployees.test.js new file mode 100644 index 0000000..258cc85 --- /dev/null +++ b/frontend/src/utils/orgTreeEmployees.test.js @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { collectOrganizationEmployees, countOrganizationEmployees } from "./orgTreeEmployees.js"; + +describe("organization tree employee aggregation", () => { + it("collects employees from the selected organization and all descendant organizations", () => { + const branch = { + node_id: 10, + node_type: "BRANCH", + node_label: "旭升车间", + employees: [ + { employee_id: 1, employee_name: "直属主管", mobile: "13000000001" } + ], + children: [ + { + node_id: 11, + node_type: "DEPARTMENT", + node_label: "旭升加工部", + employees: [ + { employee_id: 2, employee_name: "张三", mobile: "13000000002" }, + { employee_id: 3, employee_name: "李四", mobile: "13000000003" } + ], + children: [ + { + node_id: 12, + node_type: "GROUP", + node_label: "冲压小组", + employees: [ + { employee_id: 4, employee_name: "王五", mobile: "13000000004" } + ] + } + ] + } + ] + }; + + const employees = collectOrganizationEmployees(branch); + + assert.deepEqual(employees.map((employee) => employee.employee_name), ["直属主管", "张三", "李四", "王五"]); + assert.equal(employees[1].parent_node_id, 11); + assert.equal(employees[1].parent_node_label, "旭升加工部"); + assert.equal(countOrganizationEmployees(branch), 4); + }); + + it("deduplicates employees that appear in multiple descendant branches", () => { + const employee = { employee_id: 9, employee_name: "重复人员", mobile: "13000000009" }; + const branch = { + node_id: 20, + node_type: "BRANCH", + node_label: "嘉恒车间", + employees: [employee], + children: [ + { + node_id: 21, + node_type: "DEPARTMENT", + node_label: "嘉恒生产部", + employees: [employee] + } + ] + }; + + const employees = collectOrganizationEmployees(branch); + + assert.equal(employees.length, 1); + assert.equal(countOrganizationEmployees(branch), 1); + }); +});