diff --git a/software/api-server/src/routes/cabinet_tree.rs b/software/api-server/src/routes/cabinet_tree.rs index 36ec075..06690aa 100644 --- a/software/api-server/src/routes/cabinet_tree.rs +++ b/software/api-server/src/routes/cabinet_tree.rs @@ -1,7 +1,7 @@ //! 组织树 API //! //! 两个独立接口: -//! 1. /api/organization-tree — 只返回组织->项目树(不含设备) +//! 1. /api/organization-tree — 返回多级组织->项目树 //! 2. /api/cabinets/filter — 根据组织/项目筛选设备列表 use axum::extract::{State, Query}; @@ -14,7 +14,7 @@ use crate::error::AppError; use crate::middleware::auth::{self, CurrentUser}; use crate::commands::AppState; -/// 组织树接口 — 只返回组织->项目(不含设备) +/// 组织树接口 — 返回多级组织->项目树 pub async fn organization_tree( user: CurrentUser, State(state): State, @@ -23,19 +23,12 @@ pub async fn organization_tree( let db = &state.mysql; - // 根据角色过滤组织 - let orgs: Vec = if user.role_level >= 2 { - sqlx::query_as("SELECT id, name FROM organizations ORDER BY id") - .fetch_all(db) - .await? - } else if let Some(org_id) = user.organization_id { - sqlx::query_as("SELECT id, name FROM organizations WHERE id = ? ORDER BY id") - .bind(org_id) - .fetch_all(db) - .await? - } else { - return Ok(Json(json!([]))); - }; + // 获取所有组织(带parent_id) + let orgs: Vec = sqlx::query_as( + "SELECT id, name, parent_id FROM organizations ORDER BY id" + ) + .fetch_all(db) + .await?; // 获取所有项目 let projects: Vec = sqlx::query_as( @@ -44,38 +37,50 @@ pub async fn organization_tree( .fetch_all(db) .await?; - // 构建树(只到项目节点) - let tree: Vec = orgs - .into_iter() - .map(|org| { - let org_projects: Vec = projects - .iter() - .filter(|p| p.organization_id == Some(org.id)) - .cloned() - .collect(); + // 递归构建树 + fn build_tree( + orgs: &[OrganizationRow], + projects: &[ProjectRow], + parent_id: Option, + ) -> Vec { + orgs + .iter() + .filter(|o| o.parent_id == parent_id) + .map(|org| { + // 递归获取子组织 + let children = build_tree(orgs, projects, Some(org.id)); + + // 获取该组织下的项目 + let org_projects: Vec = projects + .iter() + .filter(|p| p.organization_id == Some(org.id)) + .map(|p| TreeNode { + id: p.id, + name: p.name.clone(), + children: None, + abstract_id: None, + imei: None, + status: None, + }) + .collect(); - let children: Vec = org_projects - .into_iter() - .map(|proj| TreeNode { - id: proj.id, - name: proj.name, - children: None, + // 合并子组织和项目 + let mut all_children = children; + all_children.extend(org_projects); + + TreeNode { + id: org.id, + name: org.name.clone(), + children: if all_children.is_empty() { None } else { Some(all_children) }, abstract_id: None, imei: None, status: None, - }) - .collect(); + } + }) + .collect() + } - TreeNode { - id: org.id, - name: org.name, - children: Some(children), - abstract_id: None, - imei: None, - status: None, - } - }) - .collect(); + let tree = build_tree(&orgs, &projects, None); Ok(Json(json!(tree))) } @@ -107,14 +112,17 @@ pub async fn filter_cabinets( .fetch_all(db) .await? } else if let Some(org_id) = params.organization_id { - // 按组织筛选(通过项目关联) + // 按组织筛选(包括下级组织) sqlx::query_as::<_, CabinetRow>( "SELECT c.id, c.project_id, c.abstract_id, c.imei, c.iccid, c.name, c.address, c.status FROM cabinets c JOIN projects p ON c.project_id = p.id - WHERE p.organization_id = ? ORDER BY c.id" + WHERE p.organization_id IN ( + SELECT id FROM organizations WHERE id = ? OR parent_id = ? + ) ORDER BY c.id" ) .bind(org_id) + .bind(org_id) .fetch_all(db) .await? } else if user.role_level >= 2 { diff --git a/software/api-server/src/routes/organizations.rs b/software/api-server/src/routes/organizations.rs index e12bf34..af7f68d 100644 --- a/software/api-server/src/routes/organizations.rs +++ b/software/api-server/src/routes/organizations.rs @@ -45,6 +45,8 @@ pub struct UpdateCabinetBody { pub struct OrganizationRow { pub id: i64, pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_id: Option, } /// 项目表行 diff --git a/software/web/src/pages/devices/OrganizationTree.tsx b/software/web/src/pages/devices/OrganizationTree.tsx index f3d5e96..98eed80 100644 --- a/software/web/src/pages/devices/OrganizationTree.tsx +++ b/software/web/src/pages/devices/OrganizationTree.tsx @@ -17,7 +17,6 @@ import { } from '@arco-design/web-react/icon'; import { type TreeNode, - type Cabinet, } from '@/api/devices'; const Sider = Layout.Sider; @@ -35,19 +34,19 @@ export interface TreeInternalNode { _id: number; } -/** 构建 Arco Tree 数据(只到项目节点,不显示柜子) */ +/** 构建 Arco Tree 数据(支持多级组织 -> 项目) */ export function buildTreeData(nodes: TreeNode[], parentKey?: string): TreeInternalNode[] { const result: TreeInternalNode[] = []; for (const n of nodes) { const key = parentKey ? `${parentKey}-${n.id}` : `${n.id}`; - const nodeType: NodeType = parentKey ? 'project' : 'organization'; - // 只显示组织和项目,不显示柜子 - const hasChildren = n.children && n.children.some(c => c.children !== undefined); - const children = hasChildren ? buildTreeData(n.children!, key) : undefined; + // 有children的是组织,没有children的是项目 + const isOrg = n.children && n.children.length > 0; + const nodeType: NodeType = isOrg ? 'organization' : 'project'; + const children = isOrg ? buildTreeData(n.children!, key) : undefined; result.push({ key, title: n.name, - isLeaf: !hasChildren, + isLeaf: !isOrg, children, _type: nodeType, _id: n.id, @@ -56,37 +55,6 @@ export function buildTreeData(nodes: TreeNode[], parentKey?: string): TreeIntern return result; } -/** 从树中收集所有柜子 */ -export function collectCabinets(nodes: TreeNode[]): Cabinet[] { - const result: Cabinet[] = []; - for (const n of nodes) { - if (n.children) { - result.push(...collectCabinets(n.children)); - } else if (n.abstract_id) { - result.push({ - id: n.id, - abstract_id: n.abstract_id || '', - imei: n.imei || '', - name: n.name, - status: n.status || 0, - }); - } - } - return result; -} - -/** 在原始树中按 ID 查找组织节点 */ -export function findOrgInTree(nodes: TreeNode[], id: number): TreeNode | undefined { - for (const n of nodes) { - if (n.id === id) return n; - if (n.children) { - const found = findOrgInTree(n.children, id); - if (found) return found; - } - } - return undefined; -} - interface OrganizationTreeProps { treeData: TreeInternalNode[]; loading: boolean;