diff --git a/software/api-server/src/routes/cabinet_tree.rs b/software/api-server/src/routes/cabinet_tree.rs index 42ad87e..36ec075 100644 --- a/software/api-server/src/routes/cabinet_tree.rs +++ b/software/api-server/src/routes/cabinet_tree.rs @@ -1,10 +1,12 @@ //! 组织树 API //! -//! 返回组织->项目->设备完整树状结构。 -//! 根据用户角色进行数据隔离:总管理员看全部,企业管理员只看本组织。 +//! 两个独立接口: +//! 1. /api/organization-tree — 只返回组织->项目树(不含设备) +//! 2. /api/cabinets/filter — 根据组织/项目筛选设备列表 -use axum::extract::State; +use axum::extract::{State, Query}; use axum::Json; +use serde::Deserialize; use serde_json::{json, Value}; use super::organizations::{CabinetRow, OrganizationRow, ProjectRow, TreeNode}; @@ -12,10 +14,7 @@ use crate::error::AppError; use crate::middleware::auth::{self, CurrentUser}; use crate::commands::AppState; -/// GET /api/organization-tree — 返回组织->项目->设备完整树 -/// -/// 需要 `device:view` 权限。 -/// 数据隔离:总管理员看所有组织,企业管理员只看本组织下的数据。 +/// 组织树接口 — 只返回组织->项目(不含设备) pub async fn organization_tree( user: CurrentUser, State(state): State, @@ -24,39 +23,28 @@ pub async fn organization_tree( let db = &state.mysql; - // 根据角色过滤组织:总管理员看所有,企业管理员只看本组织 + // 根据角色过滤组织 let orgs: Vec = if user.role_level >= 2 { - sqlx::query_as( - r#"SELECT id, name FROM organizations ORDER BY id"#, - ) - .fetch_all(db) - .await? + 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( - r#"SELECT id, name FROM organizations WHERE id = ? ORDER BY id"#, - ) - .bind(org_id) - .fetch_all(db) - .await? + 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!([]))); }; + // 获取所有项目 let projects: Vec = sqlx::query_as( - r#"SELECT id, organization_id, name FROM projects ORDER BY id"#, + "SELECT id, organization_id, name FROM projects ORDER BY id" ) .fetch_all(db) .await?; - let cabinets: Vec = sqlx::query_as( - r#"SELECT id, project_id, abstract_id, imei, iccid, name, address, status - FROM cabinets ORDER BY id"#, - ) - .fetch_all(db) - .await?; - - // 构建树 + // 构建树(只到项目节点) let tree: Vec = orgs .into_iter() .map(|org| { @@ -68,33 +56,13 @@ pub async fn organization_tree( let children: Vec = org_projects .into_iter() - .map(|proj| { - let proj_cabinets: Vec = cabinets - .iter() - .filter(|c| c.project_id == Some(proj.id)) - .cloned() - .collect(); - - let cab_nodes: Vec = proj_cabinets - .into_iter() - .map(|c| TreeNode { - id: c.id, - name: c.name.unwrap_or_else(|| c.abstract_id.clone()), - children: None, - abstract_id: Some(c.abstract_id), - imei: Some(c.imei), - status: Some(c.status), - }) - .collect(); - - TreeNode { - id: proj.id, - name: proj.name, - children: Some(cab_nodes), - abstract_id: None, - imei: None, - status: None, - } + .map(|proj| TreeNode { + id: proj.id, + name: proj.name, + children: None, + abstract_id: None, + imei: None, + status: None, }) .collect(); @@ -111,3 +79,66 @@ pub async fn organization_tree( Ok(Json(json!(tree))) } + +/// 设备列表筛选参数 +#[derive(Deserialize)] +pub struct CabinetFilterParams { + pub organization_id: Option, + pub project_id: Option, +} + +/// 设备列表接口 — 根据组织/项目筛选 +pub async fn filter_cabinets( + user: CurrentUser, + State(state): State, + Query(params): Query, +) -> Result, AppError> { + auth::check_permission(&user, "device:view")?; + + let db = &state.mysql; + + let rows = if let Some(proj_id) = params.project_id { + // 按项目筛选 + sqlx::query_as::<_, CabinetRow>( + "SELECT id, project_id, abstract_id, imei, iccid, name, address, status + FROM cabinets WHERE project_id = ? ORDER BY id" + ) + .bind(proj_id) + .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" + ) + .bind(org_id) + .fetch_all(db) + .await? + } else if user.role_level >= 2 { + // 总管理员看所有 + sqlx::query_as::<_, CabinetRow>( + "SELECT id, project_id, abstract_id, imei, iccid, name, address, status + FROM cabinets ORDER BY id" + ) + .fetch_all(db) + .await? + } else if let Some(org_id) = user.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" + ) + .bind(org_id) + .fetch_all(db) + .await? + } else { + vec![] + }; + + Ok(Json(json!(rows))) +} diff --git a/software/api-server/src/routes/mod.rs b/software/api-server/src/routes/mod.rs index ce1607b..a75040a 100644 --- a/software/api-server/src/routes/mod.rs +++ b/software/api-server/src/routes/mod.rs @@ -63,6 +63,7 @@ pub fn build(state: AppState) -> Router { .route("/api/cabinets/{id}/regenerate-auth", post(cabinets::regenerate_auth)) // 组织树 .route("/api/organization-tree", get(cabinet_tree::organization_tree)) + .route("/api/cabinets/filter", get(cabinet_tree::filter_cabinets)) // 充电记录 .route("/api/charge-records", get(charge_records::list_charge_records)) .route("/api/charge-records/export", get(charge_records::export_charge_records)) diff --git a/software/web/src/api/devices.ts b/software/web/src/api/devices.ts index d0516d2..e49601b 100644 --- a/software/web/src/api/devices.ts +++ b/software/web/src/api/devices.ts @@ -89,4 +89,10 @@ export const cabinetApi = { // 组织树 API export const treeApi = { get: () => api.get('/organization-tree'), + filterCabinets: (params: { organization_id?: number; project_id?: number }) => { + const query = new URLSearchParams(); + if (params.organization_id) query.set('organization_id', String(params.organization_id)); + if (params.project_id) query.set('project_id', String(params.project_id)); + return api.get(`/cabinets/filter?${query.toString()}`); + }, }; diff --git a/software/web/src/pages/devices/index.tsx b/software/web/src/pages/devices/index.tsx index 4a22265..72d9a97 100644 --- a/software/web/src/pages/devices/index.tsx +++ b/software/web/src/pages/devices/index.tsx @@ -29,7 +29,6 @@ import { treeApi, orgApi, projectApi, - cabinetApi, type TreeNode, type Cabinet, type Organization, @@ -37,8 +36,6 @@ import { import { buildTreeData, - collectCabinets, - findOrgInTree, type NodeType, type TreeInternalNode, } from './OrganizationTree'; @@ -48,7 +45,6 @@ import AddCabinetModal from './AddCabinetModal'; export default function Devices() { const navigate = useNavigate(); const [treeData, setTreeData] = useState([]); - const [rawTree, setRawTree] = useState([]); const [cabinets, setCabinets] = useState([]); const [loading, setLoading] = useState(true); const [selectedKeys, setSelectedKeys] = useState([]); @@ -79,7 +75,6 @@ export default function Devices() { try { const res = await treeApi.get(); const data = (res as unknown as TreeNode[]) || []; - setRawTree(data); setTreeData(buildTreeData(data)); } catch { Message.error('加载组织树失败'); @@ -93,21 +88,21 @@ export default function Devices() { } setLoading(true); try { + let res; if (type === 'all') { - setCabinets(collectCabinets(rawTree)); + res = await treeApi.filterCabinets({}); } else if (type === 'organization') { - const org = findOrgInTree(rawTree, id); - setCabinets(org?.children ? collectCabinets(org.children) : []); + res = await treeApi.filterCabinets({ organization_id: id }); } else if (type === 'project') { - const res = await cabinetApi.list(id); - setCabinets((res as unknown as Cabinet[]) || []); + res = await treeApi.filterCabinets({ project_id: id }); } + setCabinets((res as unknown as Cabinet[]) || []); } catch { Message.error('加载柜子列表失败'); } finally { setLoading(false); } - }, [rawTree, navigate]); + }, [navigate]); useEffect(() => { loadTree().then(() => setLoading(false)); @@ -350,7 +345,7 @@ export default function Devices() { 添加柜子 -
+