refactor: 拆分组织树和设备列表为独立接口,支持按组织/项目筛选
This commit is contained in:
parent
e3894cb946
commit
2453f680ac
@ -1,10 +1,12 @@
|
|||||||
//! 组织树 API
|
//! 组织树 API
|
||||||
//!
|
//!
|
||||||
//! 返回组织->项目->设备完整树状结构。
|
//! 两个独立接口:
|
||||||
//! 根据用户角色进行数据隔离:总管理员看全部,企业管理员只看本组织。
|
//! 1. /api/organization-tree — 只返回组织->项目树(不含设备)
|
||||||
|
//! 2. /api/cabinets/filter — 根据组织/项目筛选设备列表
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::{State, Query};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use super::organizations::{CabinetRow, OrganizationRow, ProjectRow, TreeNode};
|
use super::organizations::{CabinetRow, OrganizationRow, ProjectRow, TreeNode};
|
||||||
@ -12,10 +14,7 @@ use crate::error::AppError;
|
|||||||
use crate::middleware::auth::{self, CurrentUser};
|
use crate::middleware::auth::{self, CurrentUser};
|
||||||
use crate::commands::AppState;
|
use crate::commands::AppState;
|
||||||
|
|
||||||
/// GET /api/organization-tree — 返回组织->项目->设备完整树
|
/// 组织树接口 — 只返回组织->项目(不含设备)
|
||||||
///
|
|
||||||
/// 需要 `device:view` 权限。
|
|
||||||
/// 数据隔离:总管理员看所有组织,企业管理员只看本组织下的数据。
|
|
||||||
pub async fn organization_tree(
|
pub async fn organization_tree(
|
||||||
user: CurrentUser,
|
user: CurrentUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@ -24,39 +23,28 @@ pub async fn organization_tree(
|
|||||||
|
|
||||||
let db = &state.mysql;
|
let db = &state.mysql;
|
||||||
|
|
||||||
// 根据角色过滤组织:总管理员看所有,企业管理员只看本组织
|
// 根据角色过滤组织
|
||||||
let orgs: Vec<OrganizationRow> = if user.role_level >= 2 {
|
let orgs: Vec<OrganizationRow> = if user.role_level >= 2 {
|
||||||
sqlx::query_as(
|
sqlx::query_as("SELECT id, name FROM organizations ORDER BY id")
|
||||||
r#"SELECT id, name FROM organizations ORDER BY id"#,
|
.fetch_all(db)
|
||||||
)
|
.await?
|
||||||
.fetch_all(db)
|
|
||||||
.await?
|
|
||||||
} else if let Some(org_id) = user.organization_id {
|
} else if let Some(org_id) = user.organization_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as("SELECT id, name FROM organizations WHERE id = ? ORDER BY id")
|
||||||
r#"SELECT id, name FROM organizations WHERE id = ? ORDER BY id"#,
|
.bind(org_id)
|
||||||
)
|
.fetch_all(db)
|
||||||
.bind(org_id)
|
.await?
|
||||||
.fetch_all(db)
|
|
||||||
.await?
|
|
||||||
} else {
|
} else {
|
||||||
// 无组织关联的用户看不到任何数据
|
|
||||||
return Ok(Json(json!([])));
|
return Ok(Json(json!([])));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取所有项目
|
||||||
let projects: Vec<ProjectRow> = sqlx::query_as(
|
let projects: Vec<ProjectRow> = 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)
|
.fetch_all(db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let cabinets: Vec<CabinetRow> = 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<TreeNode> = orgs
|
let tree: Vec<TreeNode> = orgs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|org| {
|
.map(|org| {
|
||||||
@ -68,33 +56,13 @@ pub async fn organization_tree(
|
|||||||
|
|
||||||
let children: Vec<TreeNode> = org_projects
|
let children: Vec<TreeNode> = org_projects
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|proj| {
|
.map(|proj| TreeNode {
|
||||||
let proj_cabinets: Vec<CabinetRow> = cabinets
|
id: proj.id,
|
||||||
.iter()
|
name: proj.name,
|
||||||
.filter(|c| c.project_id == Some(proj.id))
|
children: None,
|
||||||
.cloned()
|
abstract_id: None,
|
||||||
.collect();
|
imei: None,
|
||||||
|
status: None,
|
||||||
let cab_nodes: Vec<TreeNode> = 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,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -111,3 +79,66 @@ pub async fn organization_tree(
|
|||||||
|
|
||||||
Ok(Json(json!(tree)))
|
Ok(Json(json!(tree)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 设备列表筛选参数
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CabinetFilterParams {
|
||||||
|
pub organization_id: Option<i64>,
|
||||||
|
pub project_id: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设备列表接口 — 根据组织/项目筛选
|
||||||
|
pub async fn filter_cabinets(
|
||||||
|
user: CurrentUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(params): Query<CabinetFilterParams>,
|
||||||
|
) -> Result<Json<Value>, 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)))
|
||||||
|
}
|
||||||
|
|||||||
@ -63,6 +63,7 @@ pub fn build(state: AppState) -> Router {
|
|||||||
.route("/api/cabinets/{id}/regenerate-auth", post(cabinets::regenerate_auth))
|
.route("/api/cabinets/{id}/regenerate-auth", post(cabinets::regenerate_auth))
|
||||||
// 组织树
|
// 组织树
|
||||||
.route("/api/organization-tree", get(cabinet_tree::organization_tree))
|
.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", get(charge_records::list_charge_records))
|
||||||
.route("/api/charge-records/export", get(charge_records::export_charge_records))
|
.route("/api/charge-records/export", get(charge_records::export_charge_records))
|
||||||
|
|||||||
@ -89,4 +89,10 @@ export const cabinetApi = {
|
|||||||
// 组织树 API
|
// 组织树 API
|
||||||
export const treeApi = {
|
export const treeApi = {
|
||||||
get: () => api.get<TreeNode[]>('/organization-tree'),
|
get: () => api.get<TreeNode[]>('/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<Cabinet[]>(`/cabinets/filter?${query.toString()}`);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,7 +29,6 @@ import {
|
|||||||
treeApi,
|
treeApi,
|
||||||
orgApi,
|
orgApi,
|
||||||
projectApi,
|
projectApi,
|
||||||
cabinetApi,
|
|
||||||
type TreeNode,
|
type TreeNode,
|
||||||
type Cabinet,
|
type Cabinet,
|
||||||
type Organization,
|
type Organization,
|
||||||
@ -37,8 +36,6 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
buildTreeData,
|
buildTreeData,
|
||||||
collectCabinets,
|
|
||||||
findOrgInTree,
|
|
||||||
type NodeType,
|
type NodeType,
|
||||||
type TreeInternalNode,
|
type TreeInternalNode,
|
||||||
} from './OrganizationTree';
|
} from './OrganizationTree';
|
||||||
@ -48,7 +45,6 @@ import AddCabinetModal from './AddCabinetModal';
|
|||||||
export default function Devices() {
|
export default function Devices() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [treeData, setTreeData] = useState<TreeInternalNode[]>([]);
|
const [treeData, setTreeData] = useState<TreeInternalNode[]>([]);
|
||||||
const [rawTree, setRawTree] = useState<TreeNode[]>([]);
|
|
||||||
const [cabinets, setCabinets] = useState<Cabinet[]>([]);
|
const [cabinets, setCabinets] = useState<Cabinet[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||||
@ -79,7 +75,6 @@ export default function Devices() {
|
|||||||
try {
|
try {
|
||||||
const res = await treeApi.get();
|
const res = await treeApi.get();
|
||||||
const data = (res as unknown as TreeNode[]) || [];
|
const data = (res as unknown as TreeNode[]) || [];
|
||||||
setRawTree(data);
|
|
||||||
setTreeData(buildTreeData(data));
|
setTreeData(buildTreeData(data));
|
||||||
} catch {
|
} catch {
|
||||||
Message.error('加载组织树失败');
|
Message.error('加载组织树失败');
|
||||||
@ -93,21 +88,21 @@ export default function Devices() {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
let res;
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
setCabinets(collectCabinets(rawTree));
|
res = await treeApi.filterCabinets({});
|
||||||
} else if (type === 'organization') {
|
} else if (type === 'organization') {
|
||||||
const org = findOrgInTree(rawTree, id);
|
res = await treeApi.filterCabinets({ organization_id: id });
|
||||||
setCabinets(org?.children ? collectCabinets(org.children) : []);
|
|
||||||
} else if (type === 'project') {
|
} else if (type === 'project') {
|
||||||
const res = await cabinetApi.list(id);
|
res = await treeApi.filterCabinets({ project_id: id });
|
||||||
setCabinets((res as unknown as Cabinet[]) || []);
|
|
||||||
}
|
}
|
||||||
|
setCabinets((res as unknown as Cabinet[]) || []);
|
||||||
} catch {
|
} catch {
|
||||||
Message.error('加载柜子列表失败');
|
Message.error('加载柜子列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [rawTree, navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTree().then(() => setLoading(false));
|
loadTree().then(() => setLoading(false));
|
||||||
@ -350,7 +345,7 @@ export default function Devices() {
|
|||||||
添加柜子
|
添加柜子
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
<div style={{ flex: 1, overflow: 'auto', padding: '0 8px' }}>
|
||||||
<CabinetGrid cabinets={cabinets} loading={loading} />
|
<CabinetGrid cabinets={cabinets} loading={loading} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user