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:
@@ -33,6 +33,11 @@ export async function getMenus() {
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function getMenusForUser() {
|
||||
const { data } = await client.get<{ success: boolean; data: MenuInfo[] }>('/menus/user');
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function batchSaveMenus(menus: MenuItemReq[]) {
|
||||
await client.put('/config/menus', { menus });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState, memo, useEffect } from 'react';
|
||||
import { Layout, Avatar, Space, Dropdown, Tooltip, theme } from 'antd';
|
||||
import { useCallback, useState, memo, useEffect, useMemo } from 'react';
|
||||
import { Layout, Avatar, Space, Dropdown, Tooltip, Spin, theme } from 'antd';
|
||||
import {
|
||||
HomeOutlined,
|
||||
UserOutlined,
|
||||
@@ -37,6 +37,7 @@ import { useAppStore } from '../stores/app';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { usePluginStore } from '../stores/plugin';
|
||||
import type { PluginMenuGroup } from '../stores/plugin';
|
||||
import { getMenusForUser, type MenuInfo } from '../api/menus';
|
||||
import NotificationPanel from '../components/NotificationPanel';
|
||||
|
||||
const { Header, Sider, Content, Footer } = Layout;
|
||||
@@ -47,68 +48,59 @@ interface MenuItem {
|
||||
label: string;
|
||||
}
|
||||
|
||||
const mainMenuItems: MenuItem[] = [
|
||||
{ key: '/', icon: <HomeOutlined />, label: '工作台' },
|
||||
{ key: '/users', icon: <UserOutlined />, label: '用户管理' },
|
||||
{ key: '/roles', icon: <SafetyOutlined />, label: '权限管理' },
|
||||
{ key: '/organizations', icon: <ApartmentOutlined />, label: '组织架构' },
|
||||
];
|
||||
// 完整图标映射表
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
HomeOutlined: <HomeOutlined />,
|
||||
UserOutlined: <UserOutlined />,
|
||||
SafetyOutlined: <SafetyOutlined />,
|
||||
ApartmentOutlined: <ApartmentOutlined />,
|
||||
SettingOutlined: <SettingOutlined />,
|
||||
PartitionOutlined: <PartitionOutlined />,
|
||||
MessageOutlined: <MessageOutlined />,
|
||||
AppstoreOutlined: <AppstoreOutlined />,
|
||||
TeamOutlined: <TeamOutlined />,
|
||||
TableOutlined: <TableOutlined />,
|
||||
TagsOutlined: <TagsOutlined />,
|
||||
HeartOutlined: <HeartOutlined />,
|
||||
CalendarOutlined: <CalendarOutlined />,
|
||||
PhoneOutlined: <PhoneOutlined />,
|
||||
CommentOutlined: <CommentOutlined />,
|
||||
MedicineBoxOutlined: <MedicineBoxOutlined />,
|
||||
TrophyOutlined: <TrophyOutlined />,
|
||||
ShopOutlined: <ShopOutlined />,
|
||||
FileTextOutlined: <FileTextOutlined />,
|
||||
DashboardOutlined: <DashboardOutlined />,
|
||||
RobotOutlined: <RobotOutlined />,
|
||||
HistoryOutlined: <HistoryOutlined />,
|
||||
BarChartOutlined: <BarChartOutlined />,
|
||||
};
|
||||
|
||||
const bizMenuItems: MenuItem[] = [
|
||||
{ key: '/workflow', icon: <PartitionOutlined />, label: '工作流' },
|
||||
{ key: '/messages', icon: <MessageOutlined />, label: '消息中心' },
|
||||
];
|
||||
function getIcon(name?: string): React.ReactNode {
|
||||
if (!name) return <AppstoreOutlined />;
|
||||
return iconMap[name] || <AppstoreOutlined />;
|
||||
}
|
||||
|
||||
const healthMenuItems: MenuItem[] = [
|
||||
{ key: '/health/statistics', icon: <DashboardOutlined />, label: '统计报表' },
|
||||
{ key: '/health/patients', icon: <TeamOutlined />, label: '患者管理' },
|
||||
{ key: '/health/doctors', icon: <MedicineBoxOutlined />, label: '医护管理' },
|
||||
{ key: '/health/appointments', icon: <CalendarOutlined />, label: '预约排班' },
|
||||
{ key: '/health/schedules', icon: <HeartOutlined />, label: '排班管理' },
|
||||
{ key: '/health/follow-up-tasks', icon: <PhoneOutlined />, label: '随访管理' },
|
||||
{ key: '/health/consultations', icon: <CommentOutlined />, label: '咨询管理' },
|
||||
{ key: '/health/tags', icon: <TagsOutlined />, label: '标签管理' },
|
||||
{ key: '/health/points-rules', icon: <TrophyOutlined />, label: '积分规则' },
|
||||
{ key: '/health/points-products', icon: <ShopOutlined />, label: '商品管理' },
|
||||
{ key: '/health/points-orders', icon: <FileTextOutlined />, label: '订单管理' },
|
||||
{ key: '/health/offline-events', icon: <CalendarOutlined />, label: '线下活动' },
|
||||
{ key: '/health/ai-prompts', icon: <RobotOutlined />, label: 'AI Prompt 管理' },
|
||||
{ key: '/health/ai-analysis', icon: <HistoryOutlined />, label: 'AI 分析历史' },
|
||||
{ key: '/health/ai-usage', icon: <BarChartOutlined />, label: 'AI 用量统计' },
|
||||
];
|
||||
|
||||
const sysMenuItems: MenuItem[] = [
|
||||
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
|
||||
{ key: '/plugins/admin', icon: <AppstoreOutlined />, label: '插件管理' },
|
||||
];
|
||||
|
||||
const routeTitleMap: Record<string, string> = {
|
||||
'/': '工作台',
|
||||
'/users': '用户管理',
|
||||
'/roles': '权限管理',
|
||||
'/organizations': '组织架构',
|
||||
'/workflow': '工作流',
|
||||
'/messages': '消息中心',
|
||||
'/settings': '系统设置',
|
||||
'/plugins/admin': '插件管理',
|
||||
'/health/statistics': '统计报表',
|
||||
'/health/patients': '患者管理',
|
||||
// 路由标题 fallback(给动态参数路由用)
|
||||
const routeTitleFallback: Record<string, string> = {
|
||||
'/health/patients/:id': '患者详情',
|
||||
'/health/tags': '标签管理',
|
||||
'/health/doctors': '医护管理',
|
||||
'/health/appointments': '预约排班',
|
||||
'/health/schedules': '排班管理',
|
||||
'/health/follow-up-tasks': '随访管理',
|
||||
'/health/follow-up-records': '随访记录',
|
||||
'/health/consultations': '咨询管理',
|
||||
'/health/consultations/:id': '咨询详情',
|
||||
'/health/points-rules': '积分规则管理',
|
||||
'/health/points-products': '商品管理',
|
||||
'/health/points-orders': '订单管理',
|
||||
'/health/offline-events': '线下活动管理',
|
||||
};
|
||||
|
||||
// 侧边栏菜单项 - 提取为独立组件避免重复渲染
|
||||
function getTitleFromMenus(path: string, menus: MenuInfo[]): string | undefined {
|
||||
for (const m of menus) {
|
||||
if (m.path === path) return m.title;
|
||||
if (m.children) {
|
||||
const found = getTitleFromMenus(path, m.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 侧边栏菜单项
|
||||
const SidebarMenuItem = memo(function SidebarMenuItem({
|
||||
item,
|
||||
isActive,
|
||||
@@ -135,28 +127,7 @@ const SidebarMenuItem = memo(function SidebarMenuItem({
|
||||
);
|
||||
});
|
||||
|
||||
// 动态图标映射
|
||||
const pluginIconMap: Record<string, React.ReactNode> = {
|
||||
AppstoreOutlined: <AppstoreOutlined />,
|
||||
team: <TeamOutlined />,
|
||||
TeamOutlined: <TeamOutlined />,
|
||||
user: <UserOutlined />,
|
||||
UserOutlined: <UserOutlined />,
|
||||
message: <MessageOutlined />,
|
||||
MessageOutlined: <MessageOutlined />,
|
||||
tags: <TagsOutlined />,
|
||||
TagsOutlined: <TagsOutlined />,
|
||||
apartment: <ApartmentOutlined />,
|
||||
ApartmentOutlined: <ApartmentOutlined />,
|
||||
TableOutlined: <TableOutlined />,
|
||||
DashboardOutlined: <AppstoreOutlined />,
|
||||
};
|
||||
|
||||
function getPluginIcon(iconName: string): React.ReactNode {
|
||||
return pluginIconMap[iconName] || <AppstoreOutlined />;
|
||||
}
|
||||
|
||||
// 插件子菜单组 — 可折叠二级标题 + 三级菜单项
|
||||
// 插件子菜单组
|
||||
const SidebarSubMenu = memo(function SidebarSubMenu({
|
||||
group,
|
||||
collapsed,
|
||||
@@ -172,7 +143,6 @@ const SidebarSubMenu = memo(function SidebarSubMenu({
|
||||
const hasActive = group.items.some((item) => currentPath === item.key);
|
||||
|
||||
if (collapsed) {
|
||||
// 折叠模式:显示插件图标,Tooltip 列出所有子项
|
||||
const tooltipContent = group.items.map((item) => item.label).join(' / ');
|
||||
return (
|
||||
<Tooltip title={tooltipContent} placement="right">
|
||||
@@ -207,7 +177,7 @@ const SidebarSubMenu = memo(function SidebarSubMenu({
|
||||
key={item.key}
|
||||
item={{
|
||||
key: item.key,
|
||||
icon: getPluginIcon(item.icon),
|
||||
icon: getIcon(item.icon),
|
||||
label: item.label,
|
||||
}}
|
||||
isActive={currentPath === item.key}
|
||||
@@ -220,6 +190,66 @@ const SidebarSubMenu = memo(function SidebarSubMenu({
|
||||
);
|
||||
});
|
||||
|
||||
// 动态菜单渲染
|
||||
const DynamicMenuSection = memo(function DynamicMenuSection({
|
||||
menus,
|
||||
collapsed,
|
||||
currentPath,
|
||||
onNavigate,
|
||||
}: {
|
||||
menus: MenuInfo[];
|
||||
collapsed: boolean;
|
||||
currentPath: string;
|
||||
onNavigate: (key: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{menus.map((menu) => {
|
||||
if (menu.menu_type === 'directory') {
|
||||
return (
|
||||
<div key={menu.id}>
|
||||
{!collapsed && <div className="erp-sidebar-group">{menu.title}</div>}
|
||||
<div className="erp-sidebar-menu">
|
||||
{menu.children
|
||||
?.filter((child) => child.visible !== false)
|
||||
.map((child) => (
|
||||
<SidebarMenuItem
|
||||
key={child.id}
|
||||
item={{
|
||||
key: child.path || child.id,
|
||||
icon: getIcon(child.icon),
|
||||
label: child.title,
|
||||
}}
|
||||
isActive={currentPath === (child.path || child.id)}
|
||||
collapsed={collapsed}
|
||||
onClick={() => onNavigate(child.path || child.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (menu.menu_type === 'menu' && menu.visible !== false) {
|
||||
return (
|
||||
<SidebarMenuItem
|
||||
key={menu.id}
|
||||
item={{
|
||||
key: menu.path || menu.id,
|
||||
icon: getIcon(menu.icon),
|
||||
label: menu.title,
|
||||
}}
|
||||
isActive={currentPath === (menu.path || menu.id)}
|
||||
collapsed={collapsed}
|
||||
onClick={() => onNavigate(menu.path || menu.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||
const { sidebarCollapsed, toggleSidebar, theme: themeMode, setTheme } = useAppStore();
|
||||
const { user, logout } = useAuthStore();
|
||||
@@ -231,6 +261,24 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
||||
const location = useLocation();
|
||||
const currentPath = location.pathname || '/';
|
||||
|
||||
// 动态菜单状态
|
||||
const [dynamicMenus, setDynamicMenus] = useState<MenuInfo[]>([]);
|
||||
const [menuLoading, setMenuLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const menus = await getMenusForUser();
|
||||
if (!cancelled) setDynamicMenus(menus);
|
||||
} catch {
|
||||
// fallback: 使用空数组,保留插件菜单
|
||||
}
|
||||
if (!cancelled) setMenuLoading(false);
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
// 加载插件菜单
|
||||
useEffect(() => {
|
||||
fetchPlugins(1, 'running');
|
||||
@@ -241,6 +289,14 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
||||
navigate('/login');
|
||||
}, [logout, navigate]);
|
||||
|
||||
// 标题查找:先从动态菜单查找,再 fallback
|
||||
const headerTitle = useMemo(() => {
|
||||
return getTitleFromMenus(currentPath, dynamicMenus)
|
||||
|| routeTitleFallback[currentPath]
|
||||
|| pluginMenuItems.find((p) => p.key === currentPath)?.label
|
||||
|| '页面';
|
||||
}, [currentPath, dynamicMenus, pluginMenuItems]);
|
||||
|
||||
const userMenuItems = [
|
||||
{
|
||||
key: 'profile',
|
||||
@@ -280,49 +336,21 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 菜单组:基础模块 */}
|
||||
{!sidebarCollapsed && <div className="erp-sidebar-group">基础模块</div>}
|
||||
<div className="erp-sidebar-menu">
|
||||
{mainMenuItems.map((item) => (
|
||||
<SidebarMenuItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
isActive={currentPath === item.key}
|
||||
collapsed={sidebarCollapsed}
|
||||
onClick={() => navigate(item.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* 动态菜单 */}
|
||||
{menuLoading ? (
|
||||
<div style={{ padding: 16, textAlign: 'center' }}>
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
) : (
|
||||
<DynamicMenuSection
|
||||
menus={dynamicMenus}
|
||||
collapsed={sidebarCollapsed}
|
||||
currentPath={currentPath}
|
||||
onNavigate={(key) => navigate(key)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 菜单组:业务模块 */}
|
||||
{!sidebarCollapsed && <div className="erp-sidebar-group">业务模块</div>}
|
||||
<div className="erp-sidebar-menu">
|
||||
{bizMenuItems.map((item) => (
|
||||
<SidebarMenuItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
isActive={currentPath === item.key}
|
||||
collapsed={sidebarCollapsed}
|
||||
onClick={() => navigate(item.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 菜单组:健康管理 */}
|
||||
{!sidebarCollapsed && <div className="erp-sidebar-group">健康管理</div>}
|
||||
<div className="erp-sidebar-menu">
|
||||
{healthMenuItems.map((item) => (
|
||||
<SidebarMenuItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
isActive={currentPath === item.key}
|
||||
collapsed={sidebarCollapsed}
|
||||
onClick={() => navigate(item.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 菜单组:插件 */}
|
||||
{/* 插件菜单 */}
|
||||
{pluginMenuGroups.length > 0 && (
|
||||
<>
|
||||
{!sidebarCollapsed && <div className="erp-sidebar-group">插件</div>}
|
||||
@@ -339,20 +367,6 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 菜单组:系统 */}
|
||||
{!sidebarCollapsed && <div className="erp-sidebar-group">系统</div>}
|
||||
<div className="erp-sidebar-menu">
|
||||
{sysMenuItems.map((item) => (
|
||||
<SidebarMenuItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
isActive={currentPath === item.key}
|
||||
collapsed={sidebarCollapsed}
|
||||
onClick={() => navigate(item.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Sider>
|
||||
|
||||
{/* 右侧主区域 */}
|
||||
@@ -368,9 +382,7 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
||||
{sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</div>
|
||||
<span className={`erp-header-title ${isDark ? 'erp-text-dark' : 'erp-text-light'}`}>
|
||||
{routeTitleMap[currentPath] ||
|
||||
pluginMenuItems.find((p) => p.key === currentPath)?.label ||
|
||||
'页面'}
|
||||
{headerTitle}
|
||||
</span>
|
||||
</Space>
|
||||
|
||||
|
||||
@@ -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