- ChatPage 组件: 左侧会话列表(260px) + 右侧聊天区 + RichMessage 富消息 - 新建/选择/重命名/关闭会话,session_id 模式消息持久化 - aiChatApi 新增 createSession/listSessions/renameSession/closeSession - 路由 /ai/chat 注册,支持 display_hints 富消息渲染 - App.tsx 路由权限校验覆盖
376 lines
18 KiB
TypeScript
376 lines
18 KiB
TypeScript
import { useEffect, lazy, Suspense, useMemo } from 'react';
|
|
import { HashRouter, Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
|
import { ConfigProvider, theme as antdTheme, Spin, Result } from 'antd';
|
|
import zhCN from 'antd/locale/zh_CN';
|
|
import MainLayout from './layouts/MainLayout';
|
|
import Login from './pages/Login';
|
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
|
import { useAuthStore } from './stores/auth';
|
|
import { useAppStore } from './stores/app';
|
|
import type { ThemeName } from './stores/app';
|
|
import { ROUTE_PERMISSIONS, FROZEN_ROUTES, validateRouteCoverage } from './routeConfig';
|
|
|
|
const Home = lazy(() => import('./pages/Home'));
|
|
const Users = lazy(() => import('./pages/Users'));
|
|
const Roles = lazy(() => import('./pages/Roles'));
|
|
const Organizations = lazy(() => import('./pages/Organizations'));
|
|
const Workflow = lazy(() => import('./pages/Workflow'));
|
|
const Messages = lazy(() => import('./pages/Messages'));
|
|
const Settings = lazy(() => import('./pages/Settings'));
|
|
const PluginAdmin = lazy(() => import('./pages/PluginAdmin'));
|
|
const PluginMarket = lazy(() => import('./pages/PluginMarket'));
|
|
const PluginCRUDPage = lazy(() => import('./pages/PluginCRUDPage'));
|
|
const PluginTabsPage = lazy(() => import('./pages/PluginTabsPage').then((m) => ({ default: m.PluginTabsPage })));
|
|
const PluginTreePage = lazy(() => import('./pages/PluginTreePage').then((m) => ({ default: m.PluginTreePage })));
|
|
const PluginGraphPage = lazy(() => import('./pages/PluginGraphPage').then((m) => ({ default: m.PluginGraphPage })));
|
|
const PluginDashboardPage = lazy(() => import('./pages/PluginDashboardPage').then((m) => ({ default: m.PluginDashboardPage })));
|
|
const PluginKanbanPage = lazy(() => import('./pages/PluginKanbanPage'));
|
|
|
|
// 健康管理模块
|
|
const PatientList = lazy(() => import('./pages/health/PatientList'));
|
|
const PatientDetail = lazy(() => import('./pages/health/PatientDetail'));
|
|
const PatientTagManage = lazy(() => import('./pages/health/PatientTagManage'));
|
|
const DoctorList = lazy(() => import('./pages/health/DoctorList'));
|
|
const AppointmentList = lazy(() => import('./pages/health/AppointmentList'));
|
|
const DoctorSchedule = lazy(() => import('./pages/health/DoctorSchedule'));
|
|
const FollowUpTaskList = lazy(() => import('./pages/health/FollowUpTaskList'));
|
|
const FollowUpRecordList = lazy(() => import('./pages/health/FollowUpRecordList'));
|
|
const ConsultationList = lazy(() => import('./pages/health/ConsultationList'));
|
|
const ConsultationDetail = lazy(() => import('./pages/health/ConsultationDetail'));
|
|
const PointsRuleList = lazy(() => import('./pages/health/PointsRuleList'));
|
|
const PointsProductList = lazy(() => import('./pages/health/PointsProductList'));
|
|
const PointsOrderList = lazy(() => import('./pages/health/PointsOrderList'));
|
|
const OfflineEventList = lazy(() => import('./pages/health/OfflineEventList'));
|
|
const StatisticsDashboard = lazy(() => import('./pages/health/StatisticsDashboard'));
|
|
const AiPromptList = lazy(() => import('./pages/health/AiPromptList'));
|
|
const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList'));
|
|
const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard'));
|
|
const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage'));
|
|
const AiKnowledgePage = lazy(() => import('./pages/health/AiKnowledgePage'));
|
|
const AiChatPage = lazy(() => import('./pages/ai/ChatPage'));
|
|
const AlertList = lazy(() => import('./pages/health/AlertList'));
|
|
const AlertDashboard = lazy(() => import('./pages/health/AlertDashboard'));
|
|
const AlertRuleList = lazy(() => import('./pages/health/AlertRuleList'));
|
|
const DeviceManage = lazy(() => import('./pages/health/DeviceManage'));
|
|
const RealtimeMonitor = lazy(() => import('./pages/health/RealtimeMonitor'));
|
|
const OAuthClientList = lazy(() => import('./pages/health/OAuthClientList'));
|
|
const DialysisManageList = lazy(() => import('./pages/health/DialysisManageList'));
|
|
const ActionInbox = lazy(() => import('./pages/health/ActionInbox'));
|
|
const FollowUpTemplateList = lazy(() => import('./pages/health/FollowUpTemplateList'));
|
|
const CarePlanList = lazy(() => import('./pages/health/CarePlanList'));
|
|
const CarePlanDetail = lazy(() => import('./pages/health/CarePlanDetail'));
|
|
const ShiftList = lazy(() => import('./pages/health/ShiftList'));
|
|
const ShiftDetail = lazy(() => import('./pages/health/ShiftDetail'));
|
|
const MedicationRecordList = lazy(() => import('./pages/health/MedicationRecordList'));
|
|
const BleGatewayList = lazy(() => import('./pages/health/BleGatewayList'));
|
|
const BleGatewayDetail = lazy(() => import('./pages/health/BleGatewayDetail'));
|
|
const CriticalValueThresholdList = lazy(() => import('./pages/health/CriticalValueThresholdList'));
|
|
const DiagnosisList = lazy(() => import('./pages/health/DiagnosisList'));
|
|
const FamilyProxyPage = lazy(() => import('./pages/health/FamilyProxyPage'));
|
|
const ConsentList = lazy(() => import('./pages/health/ConsentList'));
|
|
|
|
// 内容管理
|
|
const ArticleManageList = lazy(() => import('./pages/health/ArticleManageList'));
|
|
const ArticleEditor = lazy(() => import('./pages/health/articleEditor/ArticleEditor'));
|
|
const ArticleCategoryManage = lazy(() => import('./pages/health/ArticleCategoryManage'));
|
|
const ArticleTagManage = lazy(() => import('./pages/health/ArticleTagManage'));
|
|
const BannerManage = lazy(() => import('./pages/health/BannerManage'));
|
|
const MediaLibrary = lazy(() => import('./pages/health/MediaLibrary'));
|
|
|
|
function FrozenRoute() {
|
|
return <Result status="info" title="功能暂未开放" subTitle="该功能正在优化中,敬请期待" />;
|
|
}
|
|
|
|
function ForbiddenPage() {
|
|
const navigate = useNavigate();
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
|
|
<Result
|
|
status="403"
|
|
title="权限不足"
|
|
subTitle="您没有访问此页面的权限,请联系管理员"
|
|
extra={<button onClick={() => navigate('/')} style={{ cursor: 'pointer', color: 'var(--ant-color-primary)', background: 'none', border: 'none', fontSize: 14 }}>返回首页</button>}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
const permissions = useAuthStore((s) => s.permissions);
|
|
const location = useLocation();
|
|
|
|
if (!isAuthenticated) return <Navigate to="/login" replace />;
|
|
|
|
const path = location.pathname;
|
|
|
|
// 冻结路由检查
|
|
if (FROZEN_ROUTES.some((frozen) => path.startsWith(frozen))) {
|
|
return <FrozenRoute />;
|
|
}
|
|
|
|
// 首页/工作台始终放行
|
|
if (path === '/' || path === '') return <>{children}</>;
|
|
|
|
const matchedPrefix = Object.keys(ROUTE_PERMISSIONS).find(
|
|
(prefix) => path === prefix || path.startsWith(prefix + '/'),
|
|
);
|
|
if (matchedPrefix) {
|
|
const required = ROUTE_PERMISSIONS[matchedPrefix];
|
|
const hasAccess = required.some((r) => permissions.includes(r));
|
|
if (!hasAccess) return <ForbiddenPage />;
|
|
} else {
|
|
return <ForbiddenPage />;
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|
|
|
|
const baseToken = {
|
|
borderRadius: 10,
|
|
borderRadiusLG: 12,
|
|
borderRadiusSM: 6,
|
|
fontFamily: "'Noto Sans SC', -apple-system, system-ui, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', Helvetica, Arial, sans-serif",
|
|
fontSize: 14,
|
|
fontSizeHeading4: 20,
|
|
controlHeight: 40,
|
|
controlHeightLG: 44,
|
|
controlHeightSM: 32,
|
|
boxShadow: 'none',
|
|
boxShadowSecondary: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
|
|
};
|
|
|
|
const baseComponents = {
|
|
Button: { primaryShadow: 'none', fontWeight: 500 },
|
|
Card: { paddingLG: 20 },
|
|
Menu: { itemBorderRadius: 10, itemMarginInline: 8, itemHeight: 40 },
|
|
Modal: { borderRadiusLG: 16 },
|
|
Tag: { borderRadiusSM: 6 },
|
|
};
|
|
|
|
const themeConfigs: Record<ThemeName, { token: Record<string, unknown>; components: Record<string, Record<string, unknown>> }> = {
|
|
blue: {
|
|
token: {
|
|
...baseToken,
|
|
colorPrimary: '#2563eb',
|
|
colorSuccess: '#059669',
|
|
colorWarning: '#d97706',
|
|
colorError: '#dc2626',
|
|
colorInfo: '#0284c7',
|
|
colorBgLayout: '#f8fafc',
|
|
colorBgContainer: '#ffffff',
|
|
colorBgElevated: '#ffffff',
|
|
colorBorder: '#e2e8f0',
|
|
colorBorderSecondary: '#f1f5f9',
|
|
},
|
|
components: {
|
|
...baseComponents,
|
|
Table: { headerBg: '#f1f5f9', headerColor: '#475569', rowHoverBg: '#f1f5f9', fontSize: 14 },
|
|
},
|
|
},
|
|
warm: {
|
|
token: {
|
|
...baseToken,
|
|
borderRadius: 12,
|
|
borderRadiusLG: 14,
|
|
borderRadiusSM: 8,
|
|
colorPrimary: '#C4623A',
|
|
colorSuccess: '#5B7A5E',
|
|
colorWarning: '#C4873A',
|
|
colorError: '#B54A4A',
|
|
colorInfo: '#8B7A5E',
|
|
colorBgLayout: '#F5F0EB',
|
|
colorBgContainer: '#ffffff',
|
|
colorBgElevated: '#ffffff',
|
|
colorBorder: '#E8E2DC',
|
|
colorBorderSecondary: '#F0EBE5',
|
|
},
|
|
components: {
|
|
...baseComponents,
|
|
Table: { headerBg: '#EDE8E2', headerColor: '#7A756E', rowHoverBg: '#F5F0EB', fontSize: 14 },
|
|
},
|
|
},
|
|
dark: {
|
|
token: {
|
|
...baseToken,
|
|
colorPrimary: '#60A5FA',
|
|
colorSuccess: '#34D399',
|
|
colorWarning: '#FBBF24',
|
|
colorError: '#F87171',
|
|
colorInfo: '#38BDF8',
|
|
colorBgLayout: '#0F172A',
|
|
colorBgContainer: '#1E293B',
|
|
colorBgElevated: '#334155',
|
|
colorBorder: '#334155',
|
|
colorBorderSecondary: 'rgba(255,255,255,0.06)',
|
|
boxShadow: 'none',
|
|
boxShadowSecondary: '0 2px 8px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2)',
|
|
},
|
|
components: {
|
|
...baseComponents,
|
|
Table: { headerBg: '#1E293B', headerColor: '#94A3B8', rowHoverBg: '#1E293B', fontSize: 14 },
|
|
},
|
|
},
|
|
emerald: {
|
|
token: {
|
|
...baseToken,
|
|
borderRadius: 10,
|
|
borderRadiusLG: 14,
|
|
borderRadiusSM: 8,
|
|
colorPrimary: '#5B7A5E',
|
|
colorSuccess: '#3D7A42',
|
|
colorWarning: '#B8863A',
|
|
colorError: '#A54A4A',
|
|
colorInfo: '#4A7A8B',
|
|
colorBgLayout: '#F4F7F4',
|
|
colorBgContainer: '#ffffff',
|
|
colorBgElevated: '#ffffff',
|
|
colorBorder: '#D5DED5',
|
|
colorBorderSecondary: '#E5ECE5',
|
|
},
|
|
components: {
|
|
...baseComponents,
|
|
Table: { headerBg: '#EDF2ED', headerColor: '#5A6E5A', rowHoverBg: '#F4F7F4', fontSize: 14 },
|
|
},
|
|
},
|
|
};
|
|
|
|
export default function App() {
|
|
const loadFromStorage = useAuthStore((s) => s.loadFromStorage);
|
|
const themeName = useAppStore((s) => s.theme);
|
|
|
|
useEffect(() => {
|
|
loadFromStorage();
|
|
}, [loadFromStorage]);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-theme', themeName);
|
|
}, [themeName]);
|
|
|
|
// DEV mode: validate all routes have permission declarations
|
|
useEffect(() => {
|
|
validateRouteCoverage([
|
|
"/users", "/roles", "/organizations", "/workflow", "/messages", "/settings",
|
|
"/plugins/admin", "/plugins/market",
|
|
"/health/statistics", "/health/patients", "/health/tags", "/health/doctors",
|
|
"/health/appointments", "/health/schedules", "/health/follow-up-tasks",
|
|
"/health/follow-up-records", "/health/consultations",
|
|
"/health/points-rules", "/health/points-products", "/health/points-orders",
|
|
"/health/offline-events", "/health/ai-prompts", "/health/ai-analysis",
|
|
"/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/alerts", "/health/alert-dashboard",
|
|
"/ai/chat",
|
|
"/health/alert-rules", "/health/devices", "/health/realtime-monitor",
|
|
"/health/oauth-clients", "/health/dialysis", "/health/action-inbox",
|
|
"/health/follow-up-templates", "/health/care-plans", "/health/shifts",
|
|
"/health/medications", "/health/ble-gateways",
|
|
"/health/critical-value-thresholds", "/health/diagnoses",
|
|
"/health/family-proxy", "/health/consents",
|
|
"/health/articles", "/health/article-categories", "/health/article-tags",
|
|
"/health/banners", "/health/media-library",
|
|
"/health/medication-records",
|
|
]);
|
|
}, []);
|
|
|
|
const isDark = themeName === 'dark';
|
|
const antTheme = useMemo(() => themeConfigs[themeName] ?? themeConfigs.blue, [themeName]);
|
|
|
|
return (
|
|
<>
|
|
<a href="#root" className="erp-skip-link">跳转到主要内容</a>
|
|
<ConfigProvider
|
|
locale={zhCN}
|
|
theme={{
|
|
...antTheme,
|
|
algorithm: isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
|
}}
|
|
>
|
|
<HashRouter>
|
|
<Routes>
|
|
<Route path="/login" element={<Login />} />
|
|
<Route
|
|
path="/*"
|
|
element={
|
|
<PrivateRoute>
|
|
<MainLayout>
|
|
<ErrorBoundary>
|
|
<Suspense fallback={<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}><Spin size="large" /></div>}>
|
|
<Routes>
|
|
<Route path="/" element={<Home />} />
|
|
<Route path="/users" element={<Users />} />
|
|
<Route path="/roles" element={<Roles />} />
|
|
<Route path="/organizations" element={<Organizations />} />
|
|
<Route path="/workflow" element={<Workflow />} />
|
|
<Route path="/messages" element={<Messages />} />
|
|
<Route path="/settings" element={<Settings />} />
|
|
<Route path="/plugins/admin" element={<PluginAdmin />} />
|
|
<Route path="/plugins/market" element={<PluginMarket />} />
|
|
<Route path="/plugins/:pluginId/tabs/:pageLabel" element={<PluginTabsPage />} />
|
|
<Route path="/plugins/:pluginId/tree/:entityName" element={<PluginTreePage />} />
|
|
<Route path="/plugins/:pluginId/graph/:entityName" element={<PluginGraphPage />} />
|
|
<Route path="/plugins/:pluginId/dashboard" element={<PluginDashboardPage />} />
|
|
<Route path="/plugins/:pluginId/kanban/:entityName" element={<PluginKanbanPage />} />
|
|
<Route path="/plugins/:pluginId/:entityName" element={<PluginCRUDPage />} />
|
|
{/* 健康管理 */}
|
|
<Route path="/health/statistics" element={<StatisticsDashboard />} />
|
|
<Route path="/health/patients" element={<PatientList />} />
|
|
<Route path="/health/patients/:id" element={<PatientDetail />} />
|
|
<Route path="/health/tags" element={<PatientTagManage />} />
|
|
<Route path="/health/doctors" element={<DoctorList />} />
|
|
<Route path="/health/appointments" element={<AppointmentList />} />
|
|
<Route path="/health/schedules" element={<DoctorSchedule />} />
|
|
<Route path="/health/follow-up-tasks" element={<FollowUpTaskList />} />
|
|
<Route path="/health/follow-up-records" element={<FollowUpRecordList />} />
|
|
<Route path="/health/consultations" element={<ConsultationList />} />
|
|
<Route path="/health/consultations/:id" element={<ConsultationDetail />} />
|
|
<Route path="/health/points-rules" element={<PointsRuleList />} />
|
|
<Route path="/health/points-products" element={<PointsProductList />} />
|
|
<Route path="/health/points-orders" element={<PointsOrderList />} />
|
|
<Route path="/health/offline-events" element={<OfflineEventList />} />
|
|
<Route path="/health/ai-prompts" element={<AiPromptList />} />
|
|
<Route path="/health/ai-analysis" element={<AiAnalysisList />} />
|
|
<Route path="/health/ai-usage" element={<AiUsageDashboard />} />
|
|
<Route path="/health/ai-config" element={<AiConfigPage />} />
|
|
<Route path="/health/ai-knowledge" element={<AiKnowledgePage />} />
|
|
<Route path="/ai/chat" element={<AiChatPage />} />
|
|
<Route path="/health/alerts" element={<AlertList />} />
|
|
<Route path="/health/alert-dashboard" element={<AlertDashboard />} />
|
|
<Route path="/health/alert-rules" element={<AlertRuleList />} />
|
|
<Route path="/health/devices" element={<DeviceManage />} />
|
|
<Route path="/health/realtime-monitor" element={<RealtimeMonitor />} />
|
|
<Route path="/health/oauth-clients" element={<OAuthClientList />} />
|
|
<Route path="/health/dialysis" element={<DialysisManageList />} />
|
|
<Route path="/health/action-inbox" element={<ActionInbox />} />
|
|
<Route path="/health/follow-up-templates" element={<FollowUpTemplateList />} />
|
|
<Route path="/health/care-plans" element={<CarePlanList />} />
|
|
<Route path="/health/care-plans/:id" element={<CarePlanDetail />} />
|
|
<Route path="/health/shifts" element={<ShiftList />} />
|
|
<Route path="/health/shifts/:id" element={<ShiftDetail />} />
|
|
<Route path="/health/medications" element={<MedicationRecordList />} />
|
|
<Route path="/health/ble-gateways" element={<BleGatewayList />} />
|
|
<Route path="/health/ble-gateways/:id" element={<BleGatewayDetail />} />
|
|
<Route path="/health/critical-value-thresholds" element={<CriticalValueThresholdList />} />
|
|
<Route path="/health/diagnoses" element={<DiagnosisList />} />
|
|
<Route path="/health/family-proxy" element={<FamilyProxyPage />} />
|
|
<Route path="/health/consents" element={<ConsentList />} />
|
|
{/* 内容管理 */}
|
|
<Route path="/health/articles" element={<ArticleManageList />} />
|
|
<Route path="/health/articles/new" element={<ArticleEditor />} />
|
|
<Route path="/health/articles/:id/edit" element={<ArticleEditor />} />
|
|
<Route path="/health/article-categories" element={<ArticleCategoryManage />} />
|
|
<Route path="/health/article-tags" element={<ArticleTagManage />} />
|
|
<Route path="/health/banners" element={<BannerManage />} />
|
|
<Route path="/health/media-library" element={<MediaLibrary />} />
|
|
</Routes>
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
</MainLayout>
|
|
</PrivateRoute>
|
|
}
|
|
/>
|
|
</Routes>
|
|
</HashRouter>
|
|
</ConfigProvider>
|
|
</>
|
|
);
|
|
}
|