refactor(web): 消除侧边栏硬编码 — iconMap 抽离 + routeTitleFallback 精简

- iconMap 抽离为 utils/iconRegistry.tsx(单一真相源),补齐 10 个后端 seed 使用但前端缺失的图标
- MainLayout import 从 28 个图标减少到 6 个(仅保留布局专用图标)
- routeTitleFallback 从 26 条精简到 10 条(仅保留动态参数路由 + 无后端菜单的静态路由)
- 后端菜单已覆盖的 16 条标题映射移除(由 getTitleFromMenus 从后端数据获取)
- wiki 关键数字更新:迁移 146、权限码 132
This commit is contained in:
iven
2026-05-15 19:27:10 +08:00
parent 2c48bb0f56
commit 41515e5bec
3 changed files with 119 additions and 92 deletions

View File

@@ -1,41 +1,12 @@
import { useCallback, useState, memo, useEffect, useMemo } from 'react';
import { Layout, Avatar, Space, Dropdown, Tooltip, Spin, theme } from 'antd';
import {
HomeOutlined,
UserOutlined,
SafetyOutlined,
ApartmentOutlined,
SettingOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
PartitionOutlined,
LogoutOutlined,
MessageOutlined,
SearchOutlined,
AppstoreOutlined,
TeamOutlined,
TableOutlined,
TagsOutlined,
RightOutlined,
HeartOutlined,
CalendarOutlined,
PhoneOutlined,
CommentOutlined,
MedicineBoxOutlined,
TrophyOutlined,
ShopOutlined,
FileTextOutlined,
DashboardOutlined,
RobotOutlined,
HistoryOutlined,
BarChartOutlined,
AlertOutlined,
BellOutlined,
ControlOutlined,
InboxOutlined,
ApiOutlined,
ReadOutlined,
ExperimentOutlined,
} from '@ant-design/icons';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAppStore } from '../stores/app';
@@ -43,6 +14,7 @@ import { useAuthStore } from '../stores/auth';
import { usePluginStore } from '../stores/plugin';
import type { PluginMenuGroup } from '../stores/plugin';
import { getMenusForUser, type MenuInfo } from '../api/menus';
import { getIcon } from '../utils/iconRegistry';
import NotificationPanel from '../components/NotificationPanel';
import ThemeSwitcher from '../components/ThemeSwitcher';
@@ -54,74 +26,22 @@ interface MenuItem {
label: string;
}
// 完整图标映射表
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 />,
AlertOutlined: <AlertOutlined />,
BellOutlined: <BellOutlined />,
ControlOutlined: <ControlOutlined />,
InboxOutlined: <InboxOutlined />,
ApiOutlined: <ApiOutlined />,
ReadOutlined: <ReadOutlined />,
ExperimentOutlined: <ExperimentOutlined />,
};
function getIcon(name?: string): React.ReactNode {
if (!name) return <AppstoreOutlined />;
return iconMap[name] || <AppstoreOutlined />;
}
// 路由标题 fallback给动态参数路由用
// 路由标题 fallback — 仅保留后端菜单无法覆盖的路由
// 1. 动态参数路由(:id/:id/edit— 菜单表不会存储这些路径
// 2. 无后端菜单记录的静态页面路由
const routeTitleFallback: Record<string, string> = {
// 动态参数路由
'/health/patients/:id': '患者详情',
'/health/follow-up-records': '随访记录',
'/health/consultations/:id': '咨询详情',
'/health/points-rules': '积分规则管理',
'/health/offline-events': '线下活动管理',
'/health/articles': '内容管理',
'/health/articles/new': '新建文章',
'/health/articles/:id/edit': '编辑文章',
'/health/care-plans/:id': '护理计划详情',
'/health/shifts/:id': '班次详情',
'/health/ble-gateways/:id': '网关详情',
// 无后端菜单的静态路由
'/health/follow-up-records': '随访记录',
'/health/article-categories': '分类管理',
'/health/article-tags': '标签管理',
'/health/alerts': '告警列表',
'/health/alert-dashboard': '告警仪表盘',
'/health/alert-rules': '告警规则',
'/health/devices': '设备管理',
'/health/dialysis': '透析管理',
'/health/follow-up-templates': '随访模板管理',
'/health/care-plans': '护理计划',
'/health/care-plans/:id': '护理计划详情',
'/health/shifts': '班次管理',
'/health/shifts/:id': '班次详情',
'/health/medications': '药物记录',
'/health/ble-gateways': 'BLE 网关管理',
'/health/ble-gateways/:id': '网关详情',
'/health/critical-value-thresholds': '危急值阈值',
'/health/diagnoses': '诊断记录',
'/health/family-proxy': '家庭健康代理',
'/health/consents': '知情同意管理',
};
function getTitleFromMenus(path: string, menus: MenuInfo[]): string | undefined {

View File

@@ -0,0 +1,107 @@
/**
* 图标注册表 — 菜单图标名称 → React 组件的单一真相源
*
* 后端 menus.icon 字段存储图标名称字符串(如 "HomeOutlined"
* 前端通过此注册表将其转换为对应的 Ant Design 图标组件。
*
* 新增后端 seed 图标时必须同步在此添加映射,否则侧边栏回退为 AppstoreOutlined。
*/
import {
HomeOutlined,
UserOutlined,
SafetyOutlined,
ApartmentOutlined,
SettingOutlined,
PartitionOutlined,
MessageOutlined,
AppstoreOutlined,
TeamOutlined,
TableOutlined,
TagsOutlined,
HeartOutlined,
CalendarOutlined,
PhoneOutlined,
CommentOutlined,
MedicineBoxOutlined,
TrophyOutlined,
ShopOutlined,
FileTextOutlined,
DashboardOutlined,
RobotOutlined,
HistoryOutlined,
BarChartOutlined,
AlertOutlined,
BellOutlined,
ControlOutlined,
InboxOutlined,
ApiOutlined,
ReadOutlined,
ExperimentOutlined,
// 以下为后端 seed 使用但原 iconMap 缺失的图标
AuditOutlined,
ClockCircleOutlined,
FileSearchOutlined,
FormOutlined,
MonitorOutlined,
PictureOutlined,
SafetyCertificateOutlined,
SolutionOutlined,
SwapOutlined,
WifiOutlined,
} from '@ant-design/icons';
import type { ReactNode } from 'react';
export const iconRegistry: Record<string, ReactNode> = {
// 基础模块
HomeOutlined: <HomeOutlined />,
UserOutlined: <UserOutlined />,
SafetyOutlined: <SafetyOutlined />,
ApartmentOutlined: <ApartmentOutlined />,
SettingOutlined: <SettingOutlined />,
PartitionOutlined: <PartitionOutlined />,
MessageOutlined: <MessageOutlined />,
AppstoreOutlined: <AppstoreOutlined />,
TeamOutlined: <TeamOutlined />,
TableOutlined: <TableOutlined />,
TagsOutlined: <TagsOutlined />,
SearchOutlined: <AppstoreOutlined />, // 搜索无专属侧边栏图标
// 健康模块
HeartOutlined: <HeartOutlined />,
CalendarOutlined: <CalendarOutlined />,
PhoneOutlined: <PhoneOutlined />,
CommentOutlined: <CommentOutlined />,
MedicineBoxOutlined: <MedicineBoxOutlined />,
TrophyOutlined: <TrophyOutlined />,
ShopOutlined: <ShopOutlined />,
FileTextOutlined: <FileTextOutlined />,
DashboardOutlined: <DashboardOutlined />,
RobotOutlined: <RobotOutlined />,
HistoryOutlined: <HistoryOutlined />,
BarChartOutlined: <BarChartOutlined />,
AlertOutlined: <AlertOutlined />,
BellOutlined: <BellOutlined />,
ControlOutlined: <ControlOutlined />,
InboxOutlined: <InboxOutlined />,
ApiOutlined: <ApiOutlined />,
ReadOutlined: <ReadOutlined />,
ExperimentOutlined: <ExperimentOutlined />,
// 健康模块(补充原缺失)
AuditOutlined: <AuditOutlined />,
ClockCircleOutlined: <ClockCircleOutlined />,
FileSearchOutlined: <FileSearchOutlined />,
FormOutlined: <FormOutlined />,
MonitorOutlined: <MonitorOutlined />,
PictureOutlined: <PictureOutlined />,
SafetyCertificateOutlined: <SafetyCertificateOutlined />,
SolutionOutlined: <SolutionOutlined />,
SwapOutlined: <SwapOutlined />,
WifiOutlined: <WifiOutlined />,
};
export function getIcon(name?: string): ReactNode {
if (!name) return <AppstoreOutlined />;
return iconRegistry[name] || <AppstoreOutlined />;
}