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:
2025-11-16 15:43:17 +08:00
parent 5579ffa78e
commit 11325f88a7
39 changed files with 8051 additions and 0 deletions

24
frontend-v2/.gitignore vendored Normal file
View 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
View 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

View 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
View 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

File diff suppressed because it is too large Load Diff

39
frontend-v2/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View 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
View 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
View 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

View 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

View 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

View 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

View 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. 生产环境优化:
* - 隐藏详细错误堆栈(避免泄露代码信息)
* - 提供错误报告功能(用户可以反馈问题)
* - 记录完整错误上下文(用户操作、路由、设备信息)
*/

View 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. 后续优化:
* - [ ] 添加"反馈问题"按钮
* - [ ] 集成错误报告系统
* - [ ] 记录用户操作路径
* - [ ] 自动重试机制(网络错误)
*/

View 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
})
}

View 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
}

View 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+ - 完善】
* - [ ] 动态权限配置
* - [ ] 权限缓存优化
* - [ ] 权限变更实时通知
*/

View 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'

View 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]
}

View 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'

View 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智能文献筛选4LLM模型</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+】完整的权限和付费体系
*/

View 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
*/

View 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
View 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
View 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>,
)

View 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

View 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

View File

@@ -0,0 +1,18 @@
import Placeholder from '@/shared/components/Placeholder'
const DCModule = () => {
return (
<Placeholder
title="数据清洗模块"
description="功能规划中,将提供智能数据清洗和整理工具"
moduleName="DC - Data Cleaning"
/>
)
}
export default DCModule

View 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

View 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

View 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

View 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

View 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

View 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,
},
}

View 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
View 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"]
}

View 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"]
}

View 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,
},
},
},
})