feat(frontend): add frontend-v2 modular architecture (Task 17)
- React 19 + TypeScript + Vite - Module registration mechanism with dynamic loading - Permission management system (basic/advanced/premium) - Route guards for access control - Error boundaries for module isolation - 6 business module placeholders (AIA/ASL/PKB/DC/SSA/ST) - Top navigation layout - Tailwind CSS 3 + Ant Design 5
This commit is contained in:
24
frontend-v2/.gitignore
vendored
Normal file
24
frontend-v2/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
162
frontend-v2/README.md
Normal file
162
frontend-v2/README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# AI临床研究平台 - Frontend V2
|
||||
|
||||
> **版本:** V2.0
|
||||
> **创建日期:** 2025-11-12
|
||||
> **技术栈:** React 18 + TypeScript + Vite + Ant Design + Tailwind CSS
|
||||
|
||||
---
|
||||
|
||||
## 📋 项目说明
|
||||
|
||||
这是AI临床研究平台的**全新前端架构**,采用模块化设计,支持:
|
||||
|
||||
- ✅ 顶部导航布局
|
||||
- ✅ 模块化架构(5个业务模块)
|
||||
- ✅ 动态模块加载
|
||||
- ✅ 懒加载和代码分割
|
||||
- ✅ 统一的开发规范
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
访问:http://localhost:3000
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 预览生产构建
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── framework/ # 框架层(平台级基础设施)
|
||||
│ ├── layout/ # 布局系统
|
||||
│ ├── modules/ # 模块注册系统
|
||||
│ ├── router/ # 路由系统
|
||||
│ ├── permission/ # 权限控制
|
||||
│ └── config/ # 全局配置
|
||||
│
|
||||
├── modules/ # 业务模块(完全独立)
|
||||
│ ├── asl/ # AI智能文献
|
||||
│ ├── aia/ # AI智能问答
|
||||
│ ├── pkb/ # 个人知识库
|
||||
│ ├── rvw/ # 审稿系统
|
||||
│ └── dc/ # 数据清洗
|
||||
│
|
||||
├── shared/ # 共享资源
|
||||
│ ├── components/ # 通用组件
|
||||
│ ├── hooks/ # 通用Hooks
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── api/ # API客户端
|
||||
│
|
||||
├── pages/ # 页面组件
|
||||
│ └── HomePage.tsx # 首页
|
||||
│
|
||||
├── App.tsx # 应用根组件
|
||||
└── main.tsx # 应用入口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 模块说明
|
||||
|
||||
### 已实现
|
||||
|
||||
- ✅ **框架层**:顶部导航、主布局、模块注册
|
||||
- ✅ **占位页面**:5个模块的占位展示
|
||||
- ✅ **首页**:模块入口和统计信息
|
||||
|
||||
### 开发中
|
||||
|
||||
- 🚧 **ASL模块**:Week 3 开发(AI智能文献)
|
||||
|
||||
### 待开发
|
||||
|
||||
- 📋 **AIA模块**:AI智能问答(后续重写)
|
||||
- 📋 **PKB模块**:个人知识库(后续重写)
|
||||
- 📋 **RVW模块**:审稿系统(后续重写)
|
||||
- 📋 **DC模块**:数据清洗(占位)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 开发规范
|
||||
|
||||
### 命名规范
|
||||
|
||||
- **组件文件**:PascalCase(`TopNavigation.tsx`)
|
||||
- **Hooks文件**:camelCase + use前缀(`useAuth.ts`)
|
||||
- **工具函数**:camelCase(`formatDate.ts`)
|
||||
- **类型定义**:PascalCase(`types.ts`)
|
||||
|
||||
### 路径别名
|
||||
|
||||
使用 `@/` 作为 `src/` 的别名:
|
||||
|
||||
```typescript
|
||||
import TopNavigation from '@/framework/layout/TopNavigation'
|
||||
import Placeholder from '@/shared/components/Placeholder'
|
||||
```
|
||||
|
||||
### 模块开发
|
||||
|
||||
每个新模块需要:
|
||||
|
||||
1. 在 `src/modules/[模块名]/` 创建目录
|
||||
2. 创建 `index.tsx` 作为模块入口
|
||||
3. 在 `moduleRegistry.ts` 中注册模块
|
||||
4. 实现模块的 `ModuleDefinition` 接口
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [前后端模块化架构设计-V2](../../docs/00-系统总体设计/前后端模块化架构设计-V2.md)
|
||||
- [下一阶段行动计划](../../docs/08-项目管理/下一阶段行动计划-V2.2-完整版.md)
|
||||
|
||||
---
|
||||
|
||||
## 🔗 API代理
|
||||
|
||||
开发环境下,所有 `/api/*` 请求会被代理到后端服务器:
|
||||
|
||||
```
|
||||
Frontend: http://localhost:3000
|
||||
Backend: http://localhost:3001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 TODO
|
||||
|
||||
- [ ] 实现权限控制系统
|
||||
- [ ] 添加用户认证流程
|
||||
- [ ] 实现面包屑导航
|
||||
- [ ] 添加全局状态管理
|
||||
- [ ] 完善错误处理
|
||||
- [ ] 添加单元测试
|
||||
|
||||
---
|
||||
|
||||
**维护者:** 开发团队
|
||||
**最后更新:** 2025-11-12
|
||||
23
frontend-v2/eslint.config.js
Normal file
23
frontend-v2/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
frontend-v2/index.html
Normal file
13
frontend-v2/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend-v2</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5936
frontend-v2/package-lock.json
generated
Normal file
5936
frontend-v2/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
frontend-v2/package.json
Normal file
39
frontend-v2/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "frontend-v2",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"antd": "^5.28.1",
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
7
frontend-v2/postcss.config.cjs
Normal file
7
frontend-v2/postcss.config.cjs
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
1
frontend-v2/public/vite.svg
Normal file
1
frontend-v2/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
42
frontend-v2/src/App.css
Normal file
42
frontend-v2/src/App.css
Normal file
@@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
59
frontend-v2/src/App.tsx
Normal file
59
frontend-v2/src/App.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import { PermissionProvider } from './framework/permission'
|
||||
import { RouteGuard } from './framework/router'
|
||||
import MainLayout from './framework/layout/MainLayout'
|
||||
import HomePage from './pages/HomePage'
|
||||
import { MODULES } from './framework/modules/moduleRegistry'
|
||||
|
||||
/**
|
||||
* 应用根组件
|
||||
*
|
||||
* @description
|
||||
* - ConfigProvider: Ant Design国际化配置
|
||||
* - PermissionProvider: 权限管理系统(Week 2 Day 7新增)
|
||||
* - RouteGuard: 路由守卫保护(Week 2 Day 7新增)⭐
|
||||
* - BrowserRouter: 前端路由
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17:完整版权限系统
|
||||
*/
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
{/* 权限提供者:提供全局权限状态 */}
|
||||
<PermissionProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
{/* 首页 */}
|
||||
<Route index element={<HomePage />} />
|
||||
|
||||
{/* 动态加载模块路由 - 应用路由守卫保护 ⭐ */}
|
||||
{MODULES.map(module => (
|
||||
<Route
|
||||
key={module.id}
|
||||
path={`${module.path}/*`}
|
||||
element={
|
||||
// 为每个模块添加路由守卫
|
||||
<RouteGuard
|
||||
requiredVersion={module.requiredVersion}
|
||||
moduleName={module.name}
|
||||
>
|
||||
<module.component />
|
||||
</RouteGuard>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 404重定向 */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</PermissionProvider>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
1
frontend-v2/src/assets/react.svg
Normal file
1
frontend-v2/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
44
frontend-v2/src/framework/layout/MainLayout.tsx
Normal file
44
frontend-v2/src/framework/layout/MainLayout.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Suspense } from 'react'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Spin } from 'antd'
|
||||
import TopNavigation from './TopNavigation'
|
||||
import ErrorBoundary from '../modules/ErrorBoundary'
|
||||
|
||||
/**
|
||||
* 主布局组件
|
||||
*
|
||||
* @description
|
||||
* - 顶部导航栏
|
||||
* - 错误边界保护 ⭐ Week 2 Day 7 新增
|
||||
* - 懒加载支持(Suspense)
|
||||
* - 主内容区(Outlet)
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17:集成错误边界
|
||||
*/
|
||||
const MainLayout = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-gray-50">
|
||||
{/* 顶部导航 */}
|
||||
<TopNavigation />
|
||||
|
||||
{/* 主内容区 - 添加错误边界保护 ⭐ */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<ErrorBoundary moduleName="主应用">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Spin size="large" tip="加载中..." />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainLayout
|
||||
|
||||
|
||||
140
frontend-v2/src/framework/layout/TopNavigation.tsx
Normal file
140
frontend-v2/src/framework/layout/TopNavigation.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Dropdown, Avatar, Tooltip } from 'antd'
|
||||
import { UserOutlined, LogoutOutlined, SettingOutlined, LockOutlined } from '@ant-design/icons'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { getAvailableModules } from '../modules/moduleRegistry'
|
||||
import { usePermission } from '../permission'
|
||||
|
||||
/**
|
||||
* 顶部导航栏组件
|
||||
*
|
||||
* @description
|
||||
* - 显示Logo和平台名称
|
||||
* - 显示模块导航(根据用户权限过滤)⭐ Week 2 Day 7 新增
|
||||
* - 显示用户菜单
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17:集成权限系统
|
||||
*/
|
||||
const TopNavigation = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { user, checkModulePermission, logout } = usePermission()
|
||||
|
||||
// 获取用户有权访问的模块列表(权限过滤)⭐ 新增
|
||||
const availableModules = getAvailableModules(user?.version || 'basic')
|
||||
|
||||
// 获取当前激活的模块
|
||||
const activeModule = availableModules.find(module =>
|
||||
location.pathname.startsWith(module.path)
|
||||
)
|
||||
|
||||
// 用户菜单
|
||||
const userMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'profile',
|
||||
icon: <UserOutlined />,
|
||||
label: '个人中心',
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
icon: <SettingOutlined />,
|
||||
label: '设置',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
danger: true,
|
||||
},
|
||||
]
|
||||
|
||||
// 处理用户菜单点击
|
||||
const handleUserMenuClick = ({ key }: { key: string }) => {
|
||||
if (key === 'logout') {
|
||||
logout()
|
||||
navigate('/')
|
||||
} else {
|
||||
navigate(`/user/${key}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理模块点击(检查权限)
|
||||
const handleModuleClick = (modulePath: string, requiredVersion?: string) => {
|
||||
if (!checkModulePermission(requiredVersion as any)) {
|
||||
// 理论上不会到这里,因为已经过滤了
|
||||
console.warn('权限不足,无法访问该模块')
|
||||
return
|
||||
}
|
||||
navigate(modulePath)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-16 bg-white border-b border-gray-200 px-6 flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<div
|
||||
className="flex items-center gap-3 cursor-pointer"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
<div className="text-2xl">🏥</div>
|
||||
<span className="text-xl font-bold text-blue-600">AI临床研究平台</span>
|
||||
</div>
|
||||
|
||||
{/* 导航菜单 - 根据用户权限动态显示 ⭐ Week 2 Day 7 更新 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{availableModules.map(module => {
|
||||
const hasPermission = checkModulePermission(module.requiredVersion as any)
|
||||
const isActive = activeModule?.id === module.id
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={module.id}
|
||||
title={!hasPermission ? `需要${module.requiredVersion}版本` : ''}
|
||||
>
|
||||
<div
|
||||
onClick={() => hasPermission && handleModuleClick(module.path, module.requiredVersion)}
|
||||
className={`
|
||||
px-4 py-2 rounded-md transition-all
|
||||
${!hasPermission
|
||||
? 'text-gray-400 cursor-not-allowed opacity-50'
|
||||
: isActive
|
||||
? 'bg-blue-50 text-blue-600 font-semibold cursor-pointer'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-blue-600 cursor-pointer'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{!hasPermission && <LockOutlined className="text-xs" />}
|
||||
{module.name}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 用户菜单 - 显示真实用户信息 ⭐ Week 2 Day 7 更新 */}
|
||||
<Dropdown
|
||||
menu={{ items: userMenuItems, onClick: handleUserMenuClick }}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<div className="flex items-center gap-2 cursor-pointer px-3 py-2 rounded-md hover:bg-gray-50">
|
||||
<Avatar
|
||||
src={user?.avatar}
|
||||
icon={<UserOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-700 text-sm">{user?.name || '访客'}</span>
|
||||
<span className="text-xs text-gray-400">{user?.version || 'basic'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopNavigation
|
||||
|
||||
152
frontend-v2/src/framework/modules/ErrorBoundary.tsx
Normal file
152
frontend-v2/src/framework/modules/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react'
|
||||
import ModuleErrorFallback from './ModuleErrorFallback'
|
||||
|
||||
/**
|
||||
* 错误边界组件
|
||||
*
|
||||
* @description
|
||||
* React错误边界,捕获子组件树中的JavaScript错误
|
||||
* 防止整个应用崩溃,提供友好的错误提示和恢复机制
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ErrorBoundary>
|
||||
* <YourComponent />
|
||||
* </ErrorBoundary>
|
||||
* ```
|
||||
*
|
||||
* 注意事项:
|
||||
* - 错误边界无法捕获以下错误:
|
||||
* 1. 事件处理器中的错误(使用try-catch)
|
||||
* 2. 异步代码中的错误(使用try-catch)
|
||||
* 3. 服务端渲染的错误
|
||||
* 4. 错误边界自身的错误
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
/** 错误回退UI(可选,默认使用ModuleErrorFallback) */
|
||||
fallback?: ReactNode
|
||||
/** 模块名称(用于错误日志) */
|
||||
moduleName?: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
errorInfo: ErrorInfo | null
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当子组件抛出错误时调用
|
||||
*/
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
errorInfo: null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误被捕获后调用,用于记录错误日志
|
||||
*/
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
const { moduleName } = this.props
|
||||
|
||||
// 记录错误日志(当前使用console,后续接入真实日志系统)
|
||||
console.error('🚨 ErrorBoundary caught an error:', {
|
||||
module: moduleName || 'Unknown',
|
||||
error: error.toString(),
|
||||
componentStack: errorInfo.componentStack,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// 更新状态以显示错误信息
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
})
|
||||
|
||||
// TODO: 接入真实日志系统
|
||||
// 例如:Sentry.captureException(error, { extra: errorInfo })
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置错误状态,尝试重新渲染
|
||||
*/
|
||||
handleReset = () => {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasError, error, errorInfo } = this.state
|
||||
const { children, fallback, moduleName } = this.props
|
||||
|
||||
if (hasError) {
|
||||
// 如果提供了自定义fallback,使用自定义UI
|
||||
if (fallback) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// 使用默认错误提示UI
|
||||
return (
|
||||
<ModuleErrorFallback
|
||||
error={error}
|
||||
errorInfo={errorInfo}
|
||||
onReset={this.handleReset}
|
||||
moduleName={moduleName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
||||
|
||||
/**
|
||||
* 🔧 开发说明:错误边界最佳实践
|
||||
*
|
||||
* 1. 错误边界的放置位置:
|
||||
* - 全局级别:App根组件
|
||||
* - 模块级别:每个业务模块入口
|
||||
* - 关键组件级别:复杂的第三方组件
|
||||
*
|
||||
* 2. 错误日志:
|
||||
* 【当前阶段】console.error(开发调试)
|
||||
* 【Week 5+】接入 Sentry/LogRocket 等日志系统
|
||||
*
|
||||
* 3. 错误恢复策略:
|
||||
* - 提供"重试"按钮,尝试重新渲染
|
||||
* - 提供"返回首页"按钮,避免用户卡住
|
||||
* - 显示友好的错误提示,而非技术错误信息
|
||||
*
|
||||
* 4. 无法捕获的错误类型:
|
||||
* - 事件处理器:使用 try-catch
|
||||
* - 异步代码:使用 try-catch 或 .catch()
|
||||
* - setTimeout/setInterval:使用 try-catch 包裹回调
|
||||
*
|
||||
* 5. 生产环境优化:
|
||||
* - 隐藏详细错误堆栈(避免泄露代码信息)
|
||||
* - 提供错误报告功能(用户可以反馈问题)
|
||||
* - 记录完整错误上下文(用户操作、路由、设备信息)
|
||||
*/
|
||||
|
||||
215
frontend-v2/src/framework/modules/ModuleErrorFallback.tsx
Normal file
215
frontend-v2/src/framework/modules/ModuleErrorFallback.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import { ErrorInfo } from 'react'
|
||||
import { Result, Button, Collapse, Typography } from 'antd'
|
||||
import { BugOutlined, ReloadOutlined, HomeOutlined } from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
/**
|
||||
* 模块错误提示组件
|
||||
*
|
||||
* @description
|
||||
* 当模块加载或运行出错时显示的友好错误页面
|
||||
* 提供重试和返回首页的操作
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*/
|
||||
|
||||
const { Paragraph, Text } = Typography
|
||||
|
||||
interface ModuleErrorFallbackProps {
|
||||
/** 错误对象 */
|
||||
error: Error | null
|
||||
/** 错误详细信息 */
|
||||
errorInfo?: ErrorInfo | null
|
||||
/** 重置错误状态的回调 */
|
||||
onReset?: () => void
|
||||
/** 模块名称 */
|
||||
moduleName?: string
|
||||
}
|
||||
|
||||
const ModuleErrorFallback = ({
|
||||
error,
|
||||
errorInfo,
|
||||
onReset,
|
||||
moduleName,
|
||||
}: ModuleErrorFallbackProps) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// 是否显示详细错误信息(开发环境显示,生产环境隐藏)
|
||||
const isDevelopment = import.meta.env?.DEV ?? false
|
||||
|
||||
/**
|
||||
* 处理重试
|
||||
*/
|
||||
const handleRetry = () => {
|
||||
if (onReset) {
|
||||
onReset()
|
||||
} else {
|
||||
// 如果没有提供onReset,刷新页面
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回首页
|
||||
*/
|
||||
const handleGoHome = () => {
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center bg-gray-50 p-8">
|
||||
<div className="max-w-2xl w-full">
|
||||
<Result
|
||||
status="error"
|
||||
icon={<BugOutlined style={{ fontSize: 72, color: '#ff4d4f' }} />}
|
||||
title={
|
||||
<span className="text-2xl">
|
||||
{moduleName ? `${moduleName}模块` : '页面'}加载失败
|
||||
</span>
|
||||
}
|
||||
subTitle={
|
||||
<div className="space-y-3 mt-4">
|
||||
<Paragraph className="text-gray-600">
|
||||
抱歉,模块加载时遇到了一些问题。这可能是由于网络问题或代码错误导致的。
|
||||
</Paragraph>
|
||||
<Paragraph className="text-gray-500 text-sm">
|
||||
您可以尝试重新加载,或者返回首页。如果问题持续存在,请联系技术支持。
|
||||
</Paragraph>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRetry}
|
||||
size="large"
|
||||
>
|
||||
重新加载
|
||||
</Button>
|
||||
<Button
|
||||
icon={<HomeOutlined />}
|
||||
onClick={handleGoHome}
|
||||
size="large"
|
||||
>
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 开发环境:显示详细错误信息 */}
|
||||
{isDevelopment && error && (
|
||||
<div className="mt-8">
|
||||
<Collapse
|
||||
ghost
|
||||
items={[
|
||||
{
|
||||
key: 'error-details',
|
||||
label: (
|
||||
<Text type="secondary" className="text-sm">
|
||||
🔧 开发模式:查看错误详情
|
||||
</Text>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-4">
|
||||
{/* 错误消息 */}
|
||||
<div>
|
||||
<Text strong className="text-red-600">错误消息:</Text>
|
||||
<Paragraph
|
||||
code
|
||||
copyable
|
||||
className="mt-2 bg-red-50 p-3 rounded"
|
||||
>
|
||||
{error.toString()}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 错误堆栈 */}
|
||||
{error.stack && (
|
||||
<div>
|
||||
<Text strong className="text-red-600">错误堆栈:</Text>
|
||||
<Paragraph
|
||||
code
|
||||
copyable
|
||||
className="mt-2 bg-gray-50 p-3 rounded text-xs overflow-x-auto"
|
||||
style={{ whiteSpace: 'pre-wrap' }}
|
||||
>
|
||||
{error.stack}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 组件堆栈 */}
|
||||
{errorInfo?.componentStack && (
|
||||
<div>
|
||||
<Text strong className="text-red-600">组件堆栈:</Text>
|
||||
<Paragraph
|
||||
code
|
||||
copyable
|
||||
className="mt-2 bg-gray-50 p-3 rounded text-xs overflow-x-auto"
|
||||
style={{ whiteSpace: 'pre-wrap' }}
|
||||
>
|
||||
{errorInfo.componentStack}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 时间戳 */}
|
||||
<div>
|
||||
<Text type="secondary" className="text-xs">
|
||||
错误时间:{new Date().toLocaleString('zh-CN')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 生产环境:错误ID提示 */}
|
||||
{!isDevelopment && (
|
||||
<div className="mt-6 text-center">
|
||||
<Text type="secondary" className="text-xs">
|
||||
错误ID: {Date.now().toString(36)}
|
||||
</Text>
|
||||
<br />
|
||||
<Text type="secondary" className="text-xs">
|
||||
如需技术支持,请提供此错误ID
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModuleErrorFallback
|
||||
|
||||
/**
|
||||
* 🎨 设计说明:
|
||||
*
|
||||
* 1. 用户体验:
|
||||
* - ✅ 友好的错误提示(不显示技术术语)
|
||||
* - ✅ 明确的操作指引(重试/返回首页)
|
||||
* - ✅ 视觉上与整体风格一致(Ant Design Result)
|
||||
*
|
||||
* 2. 开发体验:
|
||||
* - ✅ 开发环境显示详细错误(方便调试)
|
||||
* - ✅ 错误信息可复制(方便分享给团队)
|
||||
* - ✅ 显示完整堆栈(快速定位问题)
|
||||
*
|
||||
* 3. 生产环境:
|
||||
* - ✅ 隐藏技术细节(安全性)
|
||||
* - ✅ 提供错误ID(方便追踪)
|
||||
* - ✅ 引导用户联系支持
|
||||
*
|
||||
* 4. 后续优化:
|
||||
* - [ ] 添加"反馈问题"按钮
|
||||
* - [ ] 集成错误报告系统
|
||||
* - [ ] 记录用户操作路径
|
||||
* - [ ] 自动重试机制(网络错误)
|
||||
*/
|
||||
|
||||
125
frontend-v2/src/framework/modules/moduleRegistry.ts
Normal file
125
frontend-v2/src/framework/modules/moduleRegistry.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { lazy } from 'react'
|
||||
import { ModuleDefinition } from './types'
|
||||
import {
|
||||
MessageOutlined,
|
||||
FileSearchOutlined,
|
||||
FolderOpenOutlined,
|
||||
ClearOutlined,
|
||||
BarChartOutlined,
|
||||
LineChartOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
/**
|
||||
* 模块注册中心
|
||||
* 按照平台架构文档顺序注册所有业务模块
|
||||
* 参考:docs/01-平台基础层/06-前端架构/01-前端总体架构设计.md
|
||||
*/
|
||||
export const MODULES: ModuleDefinition[] = [
|
||||
{
|
||||
id: 'ai-qa',
|
||||
name: 'AI问答',
|
||||
path: '/ai-qa',
|
||||
icon: MessageOutlined,
|
||||
component: lazy(() => import('@/modules/aia')),
|
||||
placeholder: true, // 后续重写
|
||||
requiredVersion: 'basic',
|
||||
description: '基于LLM的智能问答系统',
|
||||
},
|
||||
{
|
||||
id: 'literature-platform',
|
||||
name: 'AI智能文献',
|
||||
path: '/literature',
|
||||
icon: FileSearchOutlined,
|
||||
component: lazy(() => import('@/modules/asl')),
|
||||
placeholder: false, // Week 3 开发
|
||||
requiredVersion: 'advanced',
|
||||
description: 'AI驱动的文献筛选和分析系统',
|
||||
standalone: true, // 支持独立运行
|
||||
},
|
||||
{
|
||||
id: 'knowledge-base',
|
||||
name: '知识库',
|
||||
path: '/knowledge-base',
|
||||
icon: FolderOpenOutlined,
|
||||
component: lazy(() => import('@/modules/pkb')),
|
||||
placeholder: true, // 后续重写
|
||||
requiredVersion: 'basic',
|
||||
description: '个人知识库管理系统',
|
||||
},
|
||||
{
|
||||
id: 'data-cleaning',
|
||||
name: '智能数据清洗',
|
||||
path: '/data-cleaning',
|
||||
icon: ClearOutlined,
|
||||
component: lazy(() => import('@/modules/dc')),
|
||||
placeholder: true, // 占位
|
||||
requiredVersion: 'advanced',
|
||||
description: '智能数据清洗整理工具',
|
||||
},
|
||||
{
|
||||
id: 'statistical-analysis',
|
||||
name: '智能统计分析',
|
||||
path: '/intelligent-analysis',
|
||||
icon: BarChartOutlined,
|
||||
component: lazy(() => import('@/modules/ssa')),
|
||||
placeholder: true, // Java团队开发,前端集成
|
||||
requiredVersion: 'premium',
|
||||
description: '智能统计分析系统(Java团队开发)',
|
||||
isExternal: true, // 外部模块
|
||||
},
|
||||
{
|
||||
id: 'statistical-tools',
|
||||
name: '统计分析工具',
|
||||
path: '/statistical-tools',
|
||||
icon: LineChartOutlined,
|
||||
component: lazy(() => import('@/modules/st')),
|
||||
placeholder: true, // Java团队开发,前端集成
|
||||
requiredVersion: 'premium',
|
||||
description: '统计分析工具集(Java团队开发)',
|
||||
isExternal: true, // 外部模块
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 根据ID获取模块
|
||||
*/
|
||||
export const getModuleById = (id: string): ModuleDefinition | undefined => {
|
||||
return MODULES.find(module => module.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径获取模块
|
||||
*/
|
||||
export const getModuleByPath = (path: string): ModuleDefinition | undefined => {
|
||||
return MODULES.find(module => path.startsWith(module.path))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用模块(根据权限过滤)
|
||||
*
|
||||
* @param userVersion 用户版本(权限等级)
|
||||
* @returns 用户有权访问的模块列表
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17:实现权限过滤逻辑
|
||||
*/
|
||||
export const getAvailableModules = (userVersion: string = 'premium'): ModuleDefinition[] => {
|
||||
// 权限等级映射
|
||||
const versionLevel: Record<string, number> = {
|
||||
basic: 1,
|
||||
advanced: 2,
|
||||
premium: 3,
|
||||
}
|
||||
|
||||
const currentLevel = versionLevel[userVersion] || 0
|
||||
|
||||
// 过滤出用户有权限访问的模块
|
||||
return MODULES.filter(module => {
|
||||
// 如果模块没有权限要求,所有人都可以访问
|
||||
if (!module.requiredVersion) return true
|
||||
|
||||
// 检查用户权限等级是否满足模块要求
|
||||
const requiredLevel = versionLevel[module.requiredVersion] || 0
|
||||
return currentLevel >= requiredLevel
|
||||
})
|
||||
}
|
||||
|
||||
64
frontend-v2/src/framework/modules/types.ts
Normal file
64
frontend-v2/src/framework/modules/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ReactNode, LazyExoticComponent, ComponentType } from 'react'
|
||||
import { RouteObject } from 'react-router-dom'
|
||||
|
||||
/**
|
||||
* 用户版本类型
|
||||
*/
|
||||
export type UserVersion = 'basic' | 'advanced' | 'premium'
|
||||
|
||||
/**
|
||||
* 左侧导航项配置
|
||||
*/
|
||||
export interface SideNavItem {
|
||||
id: string
|
||||
label: string
|
||||
path: string
|
||||
icon?: ReactNode
|
||||
children?: SideNavItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块定义接口
|
||||
* 每个业务模块必须实现这个接口
|
||||
*/
|
||||
export interface ModuleDefinition {
|
||||
/** 模块唯一标识 */
|
||||
id: string
|
||||
|
||||
/** 模块名称(显示在导航栏) */
|
||||
name: string
|
||||
|
||||
/** 模块路由前缀 */
|
||||
path: string
|
||||
|
||||
/** 模块图标组件(可选) */
|
||||
icon?: ComponentType
|
||||
|
||||
/** 模块入口组件(懒加载) */
|
||||
component: LazyExoticComponent<ComponentType<any>>
|
||||
|
||||
/** 模块路由配置 */
|
||||
routes?: RouteObject[]
|
||||
|
||||
/** 模块是否有左侧导航 */
|
||||
hasSideNav?: boolean
|
||||
|
||||
/** 左侧导航配置(如果有) */
|
||||
sideNavConfig?: SideNavItem[]
|
||||
|
||||
/** 权限要求(可选) */
|
||||
requiredVersion?: UserVersion
|
||||
|
||||
/** 是否为占位模块 */
|
||||
placeholder?: boolean
|
||||
|
||||
/** 是否支持独立部署 */
|
||||
standalone?: boolean
|
||||
|
||||
/** 是否为外部模块(如Java团队开发) */
|
||||
isExternal?: boolean
|
||||
|
||||
/** 模块描述 */
|
||||
description?: string
|
||||
}
|
||||
|
||||
140
frontend-v2/src/framework/permission/PermissionContext.tsx
Normal file
140
frontend-v2/src/framework/permission/PermissionContext.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { createContext, useState, useCallback, ReactNode } from 'react'
|
||||
import { UserInfo, PermissionContextType, checkVersionLevel, UserVersion } from './types'
|
||||
|
||||
/**
|
||||
* 权限上下文
|
||||
*
|
||||
* @description 提供全局权限状态管理
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*
|
||||
* 注意:当前阶段(Week 2)用户信息为硬编码,方便开发测试
|
||||
* 后续计划:Week 2 Day 8-9 对接后端JWT认证
|
||||
*/
|
||||
|
||||
/**
|
||||
* 模拟用户数据(开发阶段使用)
|
||||
*
|
||||
* 🔧 开发说明:
|
||||
* - 当前硬编码为 premium 权限,可以访问所有模块
|
||||
* - 方便开发和测试所有功能
|
||||
* - 后续将从后端 JWT token 中解析真实用户信息
|
||||
*/
|
||||
const MOCK_USER: UserInfo = {
|
||||
id: 'test-user-001',
|
||||
name: '测试研究员',
|
||||
email: 'test@example.com',
|
||||
version: 'premium', // 👈 硬编码为最高权限
|
||||
avatar: null,
|
||||
isTrial: false,
|
||||
trialEndsAt: null,
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建权限上下文
|
||||
*/
|
||||
export const PermissionContext = createContext<PermissionContextType | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* 权限提供者组件
|
||||
*/
|
||||
interface PermissionProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const PermissionProvider = ({ children }: PermissionProviderProps) => {
|
||||
// 当前用户状态(开发阶段使用模拟数据)
|
||||
const [user, setUser] = useState<UserInfo | null>(MOCK_USER)
|
||||
|
||||
/**
|
||||
* 检查模块权限
|
||||
* @param requiredVersion 所需权限等级
|
||||
* @returns 是否有权限访问
|
||||
*/
|
||||
const checkModulePermission = useCallback(
|
||||
(requiredVersion?: UserVersion): boolean => {
|
||||
// 未登录用户无权限
|
||||
if (!user) return false
|
||||
|
||||
// 没有权限要求,允许访问
|
||||
if (!requiredVersion) return true
|
||||
|
||||
// 检查权限等级
|
||||
return checkVersionLevel(user.version, requiredVersion)
|
||||
},
|
||||
[user]
|
||||
)
|
||||
|
||||
/**
|
||||
* 检查功能权限
|
||||
* @param feature 功能标识
|
||||
* @returns 是否有权限使用该功能
|
||||
*
|
||||
* TODO: 后续可以基于功能列表进行更细粒度的权限控制
|
||||
*/
|
||||
const checkFeaturePermission = useCallback(
|
||||
(feature: string): boolean => {
|
||||
if (!user) return false
|
||||
|
||||
// 当前简化实现:premium用户拥有所有功能
|
||||
if (user.version === 'premium') return true
|
||||
|
||||
// 后续可以扩展为基于功能配置表的权限检查
|
||||
console.log('Feature permission check:', feature)
|
||||
return true
|
||||
},
|
||||
[user]
|
||||
)
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
const logout = useCallback(() => {
|
||||
setUser(null)
|
||||
// TODO: 清除后端session/token
|
||||
console.log('User logged out')
|
||||
}, [])
|
||||
|
||||
const value: PermissionContextType = {
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
checkModulePermission,
|
||||
checkFeaturePermission,
|
||||
setUser,
|
||||
logout,
|
||||
}
|
||||
|
||||
return (
|
||||
<PermissionContext.Provider value={value}>
|
||||
{children}
|
||||
</PermissionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔧 开发说明:权限系统演进计划
|
||||
*
|
||||
* 【当前阶段 - Week 2】
|
||||
* - ✅ 用户信息:硬编码为 premium
|
||||
* - ✅ 权限检查:基于 UserVersion 等级
|
||||
* - ✅ 功能完整:支持模块级权限控制
|
||||
*
|
||||
* 【Week 2 Day 8-9 - 对接后端】
|
||||
* - [ ] 从后端获取真实用户信息
|
||||
* - [ ] 解析 JWT token 获取用户权限
|
||||
* - [ ] 实现登录/登出功能
|
||||
* - [ ] 集成用户管理API
|
||||
*
|
||||
* 【Week 3-4 - ASL开发】
|
||||
* - [ ] 在ASL模块中应用权限控制
|
||||
* - [ ] 实现功能级权限(如:LLM模型选择权限)
|
||||
* - [ ] 添加试用期限制逻辑
|
||||
*
|
||||
* 【Week 5+ - 完善】
|
||||
* - [ ] 动态权限配置
|
||||
* - [ ] 权限缓存优化
|
||||
* - [ ] 权限变更实时通知
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
15
frontend-v2/src/framework/permission/index.ts
Normal file
15
frontend-v2/src/framework/permission/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 权限系统模块导出
|
||||
*
|
||||
* @description 统一导出权限相关的组件、Hook和类型
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*/
|
||||
|
||||
export { PermissionProvider, PermissionContext } from './PermissionContext'
|
||||
export { usePermission } from './usePermission'
|
||||
export type { UserInfo, UserVersion, PermissionContextType } from './types'
|
||||
export { VERSION_LEVEL, checkVersionLevel } from './types'
|
||||
|
||||
|
||||
|
||||
|
||||
87
frontend-v2/src/framework/permission/types.ts
Normal file
87
frontend-v2/src/framework/permission/types.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 权限系统类型定义
|
||||
*
|
||||
* @description 定义用户权限、角色等类型
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户版本类型(权限等级)
|
||||
* - basic: 基础版(免费试用)
|
||||
* - advanced: 高级版(付费用户)
|
||||
* - premium: 旗舰版(完整功能)
|
||||
*/
|
||||
export type UserVersion = 'basic' | 'advanced' | 'premium'
|
||||
|
||||
/**
|
||||
* 用户信息接口
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
id: string
|
||||
|
||||
/** 用户名称 */
|
||||
name: string
|
||||
|
||||
/** 用户邮箱 */
|
||||
email: string
|
||||
|
||||
/** 用户版本(权限等级) */
|
||||
version: UserVersion
|
||||
|
||||
/** 头像URL */
|
||||
avatar?: string | null
|
||||
|
||||
/** 是否试用中 */
|
||||
isTrial?: boolean
|
||||
|
||||
/** 试用到期时间 */
|
||||
trialEndsAt?: Date | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限上下文接口
|
||||
*/
|
||||
export interface PermissionContextType {
|
||||
/** 当前用户信息 */
|
||||
user: UserInfo | null
|
||||
|
||||
/** 是否已登录 */
|
||||
isAuthenticated: boolean
|
||||
|
||||
/** 检查模块权限 */
|
||||
checkModulePermission: (requiredVersion?: UserVersion) => boolean
|
||||
|
||||
/** 检查功能权限 */
|
||||
checkFeaturePermission: (feature: string) => boolean
|
||||
|
||||
/** 设置用户信息(登录时) */
|
||||
setUser: (user: UserInfo | null) => void
|
||||
|
||||
/** 退出登录 */
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限等级映射(用于比较)
|
||||
*/
|
||||
export const VERSION_LEVEL: Record<UserVersion, number> = {
|
||||
basic: 1,
|
||||
advanced: 2,
|
||||
premium: 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限等级是否满足要求
|
||||
*/
|
||||
export const checkVersionLevel = (
|
||||
userVersion: UserVersion,
|
||||
requiredVersion?: UserVersion
|
||||
): boolean => {
|
||||
if (!requiredVersion) return true
|
||||
return VERSION_LEVEL[userVersion] >= VERSION_LEVEL[requiredVersion]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
44
frontend-v2/src/framework/permission/usePermission.ts
Normal file
44
frontend-v2/src/framework/permission/usePermission.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useContext } from 'react'
|
||||
import { PermissionContext } from './PermissionContext'
|
||||
import { PermissionContextType } from './types'
|
||||
|
||||
/**
|
||||
* 权限Hook
|
||||
*
|
||||
* @description 提供便捷的权限检查功能
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const MyComponent = () => {
|
||||
* const { user, checkModulePermission } = usePermission()
|
||||
*
|
||||
* if (!checkModulePermission('advanced')) {
|
||||
* return <UpgradePrompt />
|
||||
* }
|
||||
*
|
||||
* return <div>欢迎 {user?.name}</div>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const usePermission = (): PermissionContextType => {
|
||||
const context = useContext(PermissionContext)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'usePermission must be used within a PermissionProvider. ' +
|
||||
'Please wrap your app with <PermissionProvider>.'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出权限相关类型(方便使用)
|
||||
*/
|
||||
export type { UserInfo, UserVersion, PermissionContextType } from './types'
|
||||
|
||||
|
||||
|
||||
|
||||
154
frontend-v2/src/framework/router/PermissionDenied.tsx
Normal file
154
frontend-v2/src/framework/router/PermissionDenied.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Result, Button } from 'antd'
|
||||
import { LockOutlined, HomeOutlined, RocketOutlined } from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
/**
|
||||
* 无权限访问提示页面
|
||||
*
|
||||
* @description
|
||||
* 当用户尝试访问无权限的模块时显示
|
||||
* 引导用户升级版本或返回首页
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*/
|
||||
|
||||
interface PermissionDeniedProps {
|
||||
/** 模块名称 */
|
||||
moduleName?: string
|
||||
/** 所需权限等级 */
|
||||
requiredVersion?: string
|
||||
/** 用户当前权限等级 */
|
||||
currentVersion?: string
|
||||
}
|
||||
|
||||
const PermissionDenied = ({
|
||||
moduleName = '该功能',
|
||||
requiredVersion = 'advanced',
|
||||
currentVersion = 'basic',
|
||||
}: PermissionDeniedProps) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
/**
|
||||
* 返回首页
|
||||
*/
|
||||
const handleGoHome = () => {
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* 去升级(后续实现)
|
||||
*/
|
||||
const handleUpgrade = () => {
|
||||
// TODO: 跳转到升级页面或打开升级对话框
|
||||
console.log('用户点击升级按钮')
|
||||
// 暂时跳转到首页
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
// 权限等级名称映射
|
||||
const versionName: Record<string, string> = {
|
||||
basic: '基础版',
|
||||
advanced: '高级版',
|
||||
premium: '旗舰版',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center bg-gray-50">
|
||||
<Result
|
||||
icon={<LockOutlined style={{ fontSize: 72, color: '#faad14' }} />}
|
||||
title={<span className="text-2xl">访问受限</span>}
|
||||
subTitle={
|
||||
<div className="space-y-3 mt-4">
|
||||
<p className="text-gray-600">
|
||||
抱歉,您当前是<strong>{versionName[currentVersion]}</strong>用户,
|
||||
无法访问<strong>{moduleName}</strong>。
|
||||
</p>
|
||||
<p className="text-gray-500">
|
||||
该功能需要<strong className="text-blue-600">{versionName[requiredVersion]}</strong>及以上权限。
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<div className="flex gap-3 justify-center mt-6">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<RocketOutlined />}
|
||||
onClick={handleUpgrade}
|
||||
size="large"
|
||||
>
|
||||
升级到{versionName[requiredVersion]}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<HomeOutlined />}
|
||||
onClick={handleGoHome}
|
||||
size="large"
|
||||
>
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* 版本对比 */}
|
||||
<div className="mt-8 max-w-md mx-auto">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<RocketOutlined className="text-blue-600" />
|
||||
<span className="font-semibold text-blue-900">
|
||||
升级{versionName[requiredVersion]}可解锁:
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-2 text-sm text-gray-700 list-disc list-inside">
|
||||
{requiredVersion === 'advanced' && (
|
||||
<>
|
||||
<li>AI智能文献筛选(4个LLM模型)</li>
|
||||
<li>更多存储空间</li>
|
||||
<li>优先技术支持</li>
|
||||
</>
|
||||
)}
|
||||
{requiredVersion === 'premium' && (
|
||||
<>
|
||||
<li>智能统计分析(外部系统集成)</li>
|
||||
<li>统计分析工具集</li>
|
||||
<li>无限存储空间</li>
|
||||
<li>专属客户经理</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Result>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionDenied
|
||||
|
||||
/**
|
||||
* 🎨 设计说明:
|
||||
*
|
||||
* 1. 用户体验:
|
||||
* - ✅ 明确告知用户为什么无法访问
|
||||
* - ✅ 显示当前版本和所需版本
|
||||
* - ✅ 提供明确的升级路径
|
||||
* - ✅ 展示升级后的价值(功能列表)
|
||||
*
|
||||
* 2. 商业转化:
|
||||
* - ✅ 突出显示"升级"按钮(主按钮)
|
||||
* - ✅ 列举升级后可获得的功能
|
||||
* - ✅ 引导用户做出升级决策
|
||||
*
|
||||
* 3. 后续优化:
|
||||
* - [ ] 接入真实的升级流程(支付系统)
|
||||
* - [ ] 显示价格对比
|
||||
* - [ ] 添加"免费试用"选项
|
||||
* - [ ] 记录转化数据(用户点击升级的次数)
|
||||
*
|
||||
* 4. 权限策略:
|
||||
* 【当前阶段】所有用户都是premium,不会看到此页面
|
||||
* 【Week 3+】ASL模块需要advanced权限,可测试此页面
|
||||
* 【Week 5+】完整的权限和付费体系
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
143
frontend-v2/src/framework/router/RouteGuard.tsx
Normal file
143
frontend-v2/src/framework/router/RouteGuard.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { usePermission } from '../permission'
|
||||
import PermissionDenied from './PermissionDenied'
|
||||
import type { UserVersion } from '../permission'
|
||||
|
||||
/**
|
||||
* 路由守卫组件
|
||||
*
|
||||
* @description
|
||||
* 保护需要特定权限的路由,防止用户通过URL直接访问无权限页面
|
||||
* 这是权限控制的"第二道防线"(第一道是TopNavigation的过滤)
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Route
|
||||
* path="/literature/*"
|
||||
* element={
|
||||
* <RouteGuard requiredVersion="advanced" moduleName="AI智能文献">
|
||||
* <ASLModule />
|
||||
* </RouteGuard>
|
||||
* }
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* 工作原理:
|
||||
* 1. 用户访问 /literature 路由
|
||||
* 2. RouteGuard 检查用户权限
|
||||
* 3. 如果有权限 → 渲染子组件
|
||||
* 4. 如果无权限 → 显示 PermissionDenied 页面
|
||||
* 5. 如果未登录 → 重定向到登录页(后续实现)
|
||||
*/
|
||||
|
||||
interface RouteGuardProps {
|
||||
/** 子组件 */
|
||||
children: ReactNode
|
||||
/** 所需权限等级 */
|
||||
requiredVersion?: UserVersion
|
||||
/** 模块名称(用于显示友好提示) */
|
||||
moduleName?: string
|
||||
/** 是否重定向到首页(默认显示无权限页面) */
|
||||
redirectToHome?: boolean
|
||||
}
|
||||
|
||||
const RouteGuard = ({
|
||||
children,
|
||||
requiredVersion,
|
||||
moduleName,
|
||||
redirectToHome = false,
|
||||
}: RouteGuardProps) => {
|
||||
const { user, isAuthenticated, checkModulePermission } = usePermission()
|
||||
|
||||
// 1. 检查是否登录(后续实现真实认证)
|
||||
if (!isAuthenticated) {
|
||||
// TODO: 后续实现真实的登录流程
|
||||
// 当前阶段:用户默认已登录(MOCK_USER)
|
||||
console.warn('用户未登录,应该重定向到登录页')
|
||||
// return <Navigate to="/login" replace />
|
||||
}
|
||||
|
||||
// 2. 检查权限等级
|
||||
const hasPermission = checkModulePermission(requiredVersion)
|
||||
|
||||
if (!hasPermission) {
|
||||
// 记录无权限访问尝试(用于后续分析和转化优化)
|
||||
console.log('🔒 权限不足:', {
|
||||
module: moduleName,
|
||||
requiredVersion,
|
||||
currentVersion: user?.version,
|
||||
userId: user?.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// 如果配置了重定向,直接返回首页
|
||||
if (redirectToHome) {
|
||||
return <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
// 显示无权限页面(推荐,引导用户升级)
|
||||
return (
|
||||
<PermissionDenied
|
||||
moduleName={moduleName}
|
||||
requiredVersion={requiredVersion}
|
||||
currentVersion={user?.version}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// 3. 有权限,渲染子组件
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export default RouteGuard
|
||||
|
||||
/**
|
||||
* 🛡️ 路由守卫最佳实践:
|
||||
*
|
||||
* 1. 双重防护策略:
|
||||
* - 第一道防线:TopNavigation(用户看不到无权限模块)
|
||||
* - 第二道防线:RouteGuard(防止URL直接访问)
|
||||
* - 为什么需要两道?防止用户通过浏览器直接输入URL绕过导航
|
||||
*
|
||||
* 2. 权限检查时机:
|
||||
* ✅ 路由渲染前检查(RouteGuard)
|
||||
* ✅ API请求前检查(后端)
|
||||
* ✅ 组件渲染前检查(usePermission)
|
||||
*
|
||||
* 3. 无权限时的处理策略:
|
||||
* 【推荐】显示PermissionDenied页面
|
||||
* - 优点:引导用户升级,商业转化机会
|
||||
* - 缺点:需要额外页面
|
||||
* 【备选】重定向到首页
|
||||
* - 优点:简单直接
|
||||
* - 缺点:用户体验不好,不利于转化
|
||||
*
|
||||
* 4. 后续演进计划:
|
||||
* 【Week 2 Day 8-9】对接后端JWT认证
|
||||
* - 实现真实的登录流程
|
||||
* - 从token解析用户权限
|
||||
* - 处理token过期
|
||||
*
|
||||
* 【Week 3-4】ASL模块测试
|
||||
* - ASL需要advanced权限
|
||||
* - 测试权限控制是否生效
|
||||
* - 优化无权限页面的转化率
|
||||
*
|
||||
* 【Week 5+】完善权限系统
|
||||
* - 动态权限配置
|
||||
* - 功能级权限控制(不仅是模块级)
|
||||
* - 权限变更实时生效
|
||||
*
|
||||
* 5. 安全注意事项:
|
||||
* ⚠️ 前端权限检查不是安全保障,仅用于用户体验
|
||||
* ✅ 后端必须进行权限验证(真正的安全防线)
|
||||
* ✅ 敏感数据不应该发送到前端
|
||||
* ✅ API调用必须携带认证token
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
13
frontend-v2/src/framework/router/index.ts
Normal file
13
frontend-v2/src/framework/router/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 路由系统模块导出
|
||||
*
|
||||
* @description 统一导出路由守卫、权限拒绝页面等组件
|
||||
* @version Week 2 Day 7 - 任务17
|
||||
*/
|
||||
|
||||
export { default as RouteGuard } from './RouteGuard'
|
||||
export { default as PermissionDenied } from './PermissionDenied'
|
||||
|
||||
|
||||
|
||||
|
||||
23
frontend-v2/src/index.css
Normal file
23
frontend-v2/src/index.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
10
frontend-v2/src/main.tsx
Normal file
10
frontend-v2/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
18
frontend-v2/src/modules/aia/index.tsx
Normal file
18
frontend-v2/src/modules/aia/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Placeholder from '@/shared/components/Placeholder'
|
||||
|
||||
const AIAModule = () => {
|
||||
return (
|
||||
<Placeholder
|
||||
title="AI智能问答模块"
|
||||
description="后续基于新架构重写,提供更好的用户体验"
|
||||
moduleName="AIA - AI Intelligent Assistant"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIAModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
18
frontend-v2/src/modules/asl/index.tsx
Normal file
18
frontend-v2/src/modules/asl/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Placeholder from '@/shared/components/Placeholder'
|
||||
|
||||
const ASLModule = () => {
|
||||
return (
|
||||
<Placeholder
|
||||
title="AI智能文献模块"
|
||||
description="Week 3 开始开发,支持4个LLM的智能文献筛选和分析"
|
||||
moduleName="ASL - AI Smart Literature"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ASLModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
18
frontend-v2/src/modules/dc/index.tsx
Normal file
18
frontend-v2/src/modules/dc/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Placeholder from '@/shared/components/Placeholder'
|
||||
|
||||
const DCModule = () => {
|
||||
return (
|
||||
<Placeholder
|
||||
title="数据清洗模块"
|
||||
description="功能规划中,将提供智能数据清洗和整理工具"
|
||||
moduleName="DC - Data Cleaning"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default DCModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
18
frontend-v2/src/modules/pkb/index.tsx
Normal file
18
frontend-v2/src/modules/pkb/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Placeholder from '@/shared/components/Placeholder'
|
||||
|
||||
const PKBModule = () => {
|
||||
return (
|
||||
<Placeholder
|
||||
title="个人知识库模块"
|
||||
description="后续基于新架构重写,提供更好的文档管理和智能检索"
|
||||
moduleName="PKB - Personal Knowledge Base"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PKBModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
22
frontend-v2/src/modules/ssa/index.tsx
Normal file
22
frontend-v2/src/modules/ssa/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import Placeholder from '../../shared/components/Placeholder'
|
||||
|
||||
/**
|
||||
* 智能统计分析模块
|
||||
* Java团队开发,前端仅做导航集成
|
||||
*/
|
||||
const SSAModule: React.FC = () => {
|
||||
return (
|
||||
<Placeholder
|
||||
moduleName="智能统计分析"
|
||||
message="由Java团队开发中,前端集成规划中"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SSAModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
22
frontend-v2/src/modules/st/index.tsx
Normal file
22
frontend-v2/src/modules/st/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import Placeholder from '../../shared/components/Placeholder'
|
||||
|
||||
/**
|
||||
* 统计分析工具模块
|
||||
* Java团队开发,前端仅做导航集成
|
||||
*/
|
||||
const STModule: React.FC = () => {
|
||||
return (
|
||||
<Placeholder
|
||||
moduleName="统计分析工具"
|
||||
message="由Java团队开发中,前端集成规划中"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default STModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
89
frontend-v2/src/pages/HomePage.tsx
Normal file
89
frontend-v2/src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Card, Row, Col } from 'antd'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { MODULES } from '@/framework/modules/moduleRegistry'
|
||||
|
||||
const HomePage = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="flex-1 p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* 欢迎标题 */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
欢迎使用 AI临床研究平台
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
选择一个模块开始您的研究工作
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 模块卡片 */}
|
||||
<Row gutter={[24, 24]}>
|
||||
{MODULES.map(module => (
|
||||
<Col xs={24} sm={12} lg={8} key={module.id}>
|
||||
<Card
|
||||
hoverable={!module.placeholder}
|
||||
onClick={() => !module.placeholder && navigate(module.path)}
|
||||
className={`h-full ${module.placeholder ? 'opacity-60' : ''}`}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center p-4">
|
||||
<div className="text-5xl mb-4">
|
||||
{module.icon && <module.icon />}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
{module.name}
|
||||
</h3>
|
||||
<p className="text-gray-500 text-sm mb-3">
|
||||
{module.description}
|
||||
</p>
|
||||
{/* 状态标签 */}
|
||||
{!module.placeholder && module.id === 'literature-platform' && (
|
||||
<span className="text-xs text-green-600 bg-green-50 px-3 py-1 rounded-full">
|
||||
Week 3 开发
|
||||
</span>
|
||||
)}
|
||||
{module.placeholder && module.isExternal && (
|
||||
<span className="text-xs text-purple-600 bg-purple-50 px-3 py-1 rounded-full">
|
||||
外部集成
|
||||
</span>
|
||||
)}
|
||||
{module.placeholder && !module.isExternal && (
|
||||
<span className="text-xs text-orange-500 bg-orange-50 px-3 py-1 rounded-full">
|
||||
规划中
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-blue-600">6</div>
|
||||
<div className="text-gray-600 mt-2">业务模块</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-green-600">10</div>
|
||||
<div className="text-gray-600 mt-2">数据库Schema</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-purple-600">4</div>
|
||||
<div className="text-gray-600 mt-2">集成LLM</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
|
||||
48
frontend-v2/src/shared/components/Placeholder.tsx
Normal file
48
frontend-v2/src/shared/components/Placeholder.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Result, Button } from 'antd'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { RocketOutlined } from '@ant-design/icons'
|
||||
|
||||
interface PlaceholderProps {
|
||||
title?: string
|
||||
description?: string
|
||||
moduleName?: string
|
||||
}
|
||||
|
||||
const Placeholder = ({
|
||||
title = '功能开发中',
|
||||
description = '该模块正在规划和开发中,敬请期待',
|
||||
moduleName
|
||||
}: PlaceholderProps) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center bg-gray-50">
|
||||
<Result
|
||||
icon={<RocketOutlined style={{ fontSize: 72, color: '#1890ff' }} />}
|
||||
title={<span className="text-2xl">{title}</span>}
|
||||
subTitle={
|
||||
<div className="space-y-2">
|
||||
<p className="text-gray-600">{description}</p>
|
||||
{moduleName && (
|
||||
<p className="text-sm text-gray-400">
|
||||
模块名称:{moduleName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate('/')}>
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
16
frontend-v2/tailwind.config.cjs
Normal file
16
frontend-v2/tailwind.config.cjs
Normal file
@@ -0,0 +1,16 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
// 与Ant Design配合使用
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
}
|
||||
|
||||
28
frontend-v2/tsconfig.app.json
Normal file
28
frontend-v2/tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
30
frontend-v2/tsconfig.json
Normal file
30
frontend-v2/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
26
frontend-v2/tsconfig.node.json
Normal file
26
frontend-v2/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
22
frontend-v2/vite.config.ts
Normal file
22
frontend-v2/vite.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user