feat(config): 菜单动态化改造 — 侧边栏从后端 API 加载
- 新增 seed 迁移插入完整菜单树(4 directory + 23 menu = 27 条) - 新增 GET /api/v1/menus/user 端点(仅需登录,无需 menu.list 权限) - MainLayout 从 API 动态获取菜单树替换硬编码数组 - 扩展图标映射表覆盖 22 个 Ant Design 图标 - Header 标题从动态菜单数据查找,保留 fallback
This commit is contained in:
@@ -236,6 +236,39 @@ where
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/menus/user",
|
||||
responses(
|
||||
(status = 200, description = "成功", body = ApiResponse<Vec<MenuResp>>),
|
||||
(status = 401, description = "未授权"),
|
||||
),
|
||||
security(("bearer_auth" = [])),
|
||||
tag = "菜单管理"
|
||||
)]
|
||||
/// GET /api/v1/menus/user
|
||||
///
|
||||
/// 获取当前用户可见的菜单树(无需 menu.list 权限,仅需登录)。
|
||||
pub async fn get_user_menus<S>(
|
||||
State(state): State<ConfigState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
) -> Result<JsonResponse<ApiResponse<Vec<MenuResp>>>, AppError>
|
||||
where
|
||||
ConfigState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
let role_ids: Vec<Uuid> = ctx
|
||||
.roles
|
||||
.iter()
|
||||
.filter_map(|r| Uuid::parse_str(r).ok())
|
||||
.collect();
|
||||
|
||||
// 如果用户有角色关联菜单,按角色过滤;否则返回全部(admin 兜底)
|
||||
let menus = MenuService::get_menu_tree(ctx.tenant_id, &role_ids, &state.db).await?;
|
||||
|
||||
Ok(JsonResponse(ApiResponse::ok(menus)))
|
||||
}
|
||||
|
||||
/// 删除菜单的乐观锁版本号请求体。
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteMenuVersionReq {
|
||||
|
||||
@@ -63,6 +63,11 @@ impl ConfigModule {
|
||||
"/config/menus/{id}",
|
||||
put(menu_handler::update_menu).delete(menu_handler::delete_menu),
|
||||
)
|
||||
// User menu tree (no special permission required)
|
||||
.route(
|
||||
"/menus/user",
|
||||
get(menu_handler::get_user_menus),
|
||||
)
|
||||
// Setting routes
|
||||
.route(
|
||||
"/config/settings/{key}",
|
||||
|
||||
@@ -58,6 +58,7 @@ mod m20260425_000055_points_checkin_standard_fields;
|
||||
mod m20260426_000056_create_diagnosis;
|
||||
mod m20260426_000057_rename_points_transaction_type_column;
|
||||
mod m20260426_000058_merge_daily_monitoring_into_vital_signs;
|
||||
mod m20260426_000059_seed_menus;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -123,6 +124,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260426_000056_create_diagnosis::Migration),
|
||||
Box::new(m20260426_000057_rename_points_transaction_type_column::Migration),
|
||||
Box::new(m20260426_000058_merge_daily_monitoring_into_vital_signs::Migration),
|
||||
Box::new(m20260426_000059_seed_menus::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
128
crates/erp-server/migration/src/m20260426_000059_seed_menus.rs
Normal file
128
crates/erp-server/migration/src/m20260426_000059_seed_menus.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
// 软删除已有菜单数据
|
||||
db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
"UPDATE menus SET deleted_at = NOW() WHERE deleted_at IS NULL".to_string(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
// 获取默认租户 ID
|
||||
let result = db.query_one(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
"SELECT id::text FROM tenant LIMIT 1".to_string(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
let tid = match result {
|
||||
Some(row) => row.try_get_by_index::<String>(0).unwrap_or_default(),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let sys = "00000000-0000-0000-0000-000000000000";
|
||||
let nil = "NULL";
|
||||
|
||||
// === Directory 节点 ===
|
||||
insert_dir(db, &tid, "a0000000-0000-0000-0000-000000000001", "基础模块", 1, sys).await?;
|
||||
insert_dir(db, &tid, "a0000000-0000-0000-0000-000000000002", "业务模块", 2, sys).await?;
|
||||
insert_dir(db, &tid, "a0000000-0000-0000-0000-000000000003", "健康管理", 3, sys).await?;
|
||||
insert_dir(db, &tid, "a0000000-0000-0000-0000-000000000004", "系统", 4, sys).await?;
|
||||
|
||||
// === 基础模块菜单 ===
|
||||
let d1 = "a0000000-0000-0000-0000-000000000001";
|
||||
insert_menu(db, &tid, d1, "b0000001-0000-0000-0000-000000000001", "工作台", "/", "HomeOutlined", 0, sys).await?;
|
||||
insert_menu(db, &tid, d1, "b0000001-0000-0000-0000-000000000002", "用户管理", "/users", "UserOutlined", 1, sys).await?;
|
||||
insert_menu(db, &tid, d1, "b0000001-0000-0000-0000-000000000003", "权限管理", "/roles", "SafetyOutlined", 2, sys).await?;
|
||||
insert_menu(db, &tid, d1, "b0000001-0000-0000-0000-000000000004", "组织架构", "/organizations", "ApartmentOutlined", 3, sys).await?;
|
||||
|
||||
// === 业务模块菜单 ===
|
||||
let d2 = "a0000000-0000-0000-0000-000000000002";
|
||||
insert_menu(db, &tid, d2, "b0000002-0000-0000-0000-000000000001", "工作流", "/workflow", "PartitionOutlined", 0, sys).await?;
|
||||
insert_menu(db, &tid, d2, "b0000002-0000-0000-0000-000000000002", "消息中心", "/messages", "MessageOutlined", 1, sys).await?;
|
||||
|
||||
// === 健康管理菜单 ===
|
||||
let d3 = "a0000000-0000-0000-0000-000000000003";
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000001", "统计报表", "/health/statistics", "DashboardOutlined", 0, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000002", "患者管理", "/health/patients", "TeamOutlined", 1, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000003", "医护管理", "/health/doctors", "MedicineBoxOutlined", 2, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000004", "预约排班", "/health/appointments", "CalendarOutlined", 3, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000005", "排班管理", "/health/schedules", "HeartOutlined", 4, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000006", "随访管理", "/health/follow-up-tasks", "PhoneOutlined", 5, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000007", "咨询管理", "/health/consultations", "CommentOutlined", 6, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000008", "标签管理", "/health/tags", "TagsOutlined", 7, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000009", "积分规则", "/health/points-rules", "TrophyOutlined", 8, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000010", "商品管理", "/health/points-products", "ShopOutlined", 9, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000011", "订单管理", "/health/points-orders", "FileTextOutlined", 10, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000012", "线下活动", "/health/offline-events", "CalendarOutlined", 11, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000013", "AI Prompt 管理", "/health/ai-prompts", "RobotOutlined", 12, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000014", "AI 分析历史", "/health/ai-analysis", "HistoryOutlined", 13, sys).await?;
|
||||
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000015", "AI 用量统计", "/health/ai-usage", "BarChartOutlined", 14, sys).await?;
|
||||
|
||||
// === 系统菜单 ===
|
||||
let d4 = "a0000000-0000-0000-0000-000000000004";
|
||||
insert_menu(db, &tid, d4, "b0000004-0000-0000-0000-000000000001", "系统设置", "/settings", "SettingOutlined", 0, sys).await?;
|
||||
insert_menu(db, &tid, d4, "b0000004-0000-0000-0000-000000000002", "插件管理", "/plugins/admin", "AppstoreOutlined", 1, sys).await?;
|
||||
|
||||
let _ = nil;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
"DELETE FROM menus WHERE id LIKE 'a0000000-0000-0000-0000-00000000000%' OR id LIKE 'b000000%-0000-0000-0000-00000000000%'".to_string(),
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert_dir(
|
||||
db: &sea_orm_migration::SchemaManagerConnection<'_>,
|
||||
tenant_id: &str,
|
||||
id: &str,
|
||||
title: &str,
|
||||
sort: i32,
|
||||
sys: &str,
|
||||
) -> Result<(), DbErr> {
|
||||
db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
format!(
|
||||
"INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order, visible, menu_type, permission, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
VALUES ('{id}', '{tenant_id}', NULL, '{title}', NULL, NULL, {sort}, true, 'directory', NULL, NOW(), NOW(), '{sys}', '{sys}', NULL, 1) ON CONFLICT (id) DO NOTHING"
|
||||
),
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_menu(
|
||||
db: &sea_orm_migration::SchemaManagerConnection<'_>,
|
||||
tenant_id: &str,
|
||||
parent_id: &str,
|
||||
id: &str,
|
||||
title: &str,
|
||||
path: &str,
|
||||
icon: &str,
|
||||
sort: i32,
|
||||
sys: &str,
|
||||
) -> Result<(), DbErr> {
|
||||
db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
format!(
|
||||
"INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order, visible, menu_type, permission, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
VALUES ('{id}', '{tenant_id}', '{parent_id}', '{title}', '{path}', '{icon}', {sort}, true, 'menu', NULL, NOW(), NOW(), '{sys}', '{sys}', NULL, 1) ON CONFLICT (id) DO NOTHING"
|
||||
),
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user