feat(frontend): Day 6 - frontend basic architecture completed

This commit is contained in:
AI Clinical Dev Team
2025-10-10 17:22:37 +08:00
parent 0db54b2d31
commit f7a500bc79
20 changed files with 6718 additions and 0 deletions

29
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# 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?
# Environment variables
.env.local
.env.production

146
frontend/README.md Normal file
View File

@@ -0,0 +1,146 @@
# AI临床研究平台 - 前端
基于 Vite + React + TypeScript + Ant Design 构建
## 🚀 快速开始
### 1. 安装依赖
```bash
npm install
```
### 2. 启动开发服务器
```bash
npm run dev
```
访问http://localhost:3000
### 3. 构建生产版本
```bash
npm run build
```
## 📦 技术栈
- **框架**: React 18 + TypeScript
- **构建工具**: Vite 6
- **UI组件库**: Ant Design 5
- **CSS框架**: Tailwind CSS 3
- **路由**: React Router 6
- **HTTP客户端**: Axios
- **状态管理**: React HooksDay 7+可能引入Zustand
## 📁 项目结构
```
frontend/
├── src/
│ ├── api/ # API请求
│ │ ├── request.ts # Axios配置
│ │ └── index.ts # API接口
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ ├── layouts/ # 布局组件
│ │ └── MainLayout.tsx
│ ├── pages/ # 页面组件
│ │ ├── HomePage.tsx # 首页
│ │ └── AgentPage.tsx # 智能体页面
│ ├── types/ # TypeScript类型定义
│ ├── utils/ # 工具函数
│ ├── App.tsx # 根组件
│ ├── main.tsx # 入口文件
│ └── index.css # 全局样式
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── tailwind.config.js
```
## 🎨 功能特性
### Day 6 已完成
- ✅ 项目初始化Vite + React + TypeScript
- ✅ Ant Design UI组件库集成
- ✅ Tailwind CSS配置
- ✅ React Router路由配置
- ✅ 主布局组件(侧边栏 + 头部 + 内容区)
- ✅ 首页展示12个智能体卡片
- ✅ 智能体页面(占位符)
- ✅ Axios请求配置
- ✅ TypeScript类型定义
### Day 7-8 待开发
- ⏳ 项目管理功能
- ⏳ 知识库管理功能
- ⏳ 个人中心页面
- ⏳ 设置页面
### Day 9-10 待开发
- ⏳ 智能对话组件
- ⏳ 与后端API集成
- ⏳ 模型切换功能
- ⏳ 知识库@引用功能
## 🔧 开发规范
### 代码规范
- 使用TypeScript类型检查
- 使用ESLint代码检查
- 组件使用函数式组件 + Hooks
- 样式优先使用Tailwind CSS
- 复杂样式使用Ant Design内联style
### 命名规范
- 组件文件PascalCaseHomePage.tsx
- 普通文件camelCaserequest.ts
- 常量UPPER_SNAKE_CASEAPI_BASE_URL
### Git提交规范
- feat: 新功能
- fix: 修复bug
- docs: 文档更新
- style: 代码格式调整
- refactor: 代码重构
## 📡 API配置
后端API地址http://localhost:3001/api/v1
代理配置已在`vite.config.ts`中设置:
```typescript
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
}
```
## 🎯 智能体列表
1. 📊 选题评价
2. 🔍 PICOS构建
3. 📚 文献检索
4. 🎯 文献筛选
5. 📋 数据提取
6. ⚖️ 偏倚评价
7. 📈 Meta分析
8. 🌲 森林图绘制
9. 💡 结果解读
10. 📝 方案撰写
11. ✍️ 文章撰写
12. 📬 投稿辅助
## 📞 技术支持
详见项目根目录的完整文档:
- `docs/01-设计文档/API设计规范.md`
- `docs/02-开发规范/代码规范.md`
- `docs/04-开发计划/开发里程碑.md`
---
**Day 6 完成!前端基础架构搭建完毕!** 🎉

14
frontend/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<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>AI临床研究平台</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5828
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
frontend/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "ai-clinical-frontend",
"version": "1.0.0",
"type": "module",
"description": "AI Clinical Research Platform - Frontend",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"antd": "^5.22.5",
"axios": "^1.7.9",
"@ant-design/icons": "^5.5.2"
},
"devDependencies": {
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.18.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"vite": "^6.0.7"
}
}

View File

@@ -0,0 +1,7 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

19
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { Routes, Route, Navigate } from 'react-router-dom'
import MainLayout from './layouts/MainLayout'
import HomePage from './pages/HomePage'
import AgentPage from './pages/AgentPage'
function App() {
return (
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<HomePage />} />
<Route path="agent/:agentId" element={<AgentPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
)
}
export default App

23
frontend/src/api/index.ts Normal file
View File

@@ -0,0 +1,23 @@
import request from './request'
// 健康检查
export const healthCheck = () => {
return request.get('/health')
}
// API信息
export const getApiInfo = () => {
return request.get('/')
}
// TODO: Day 9+ 添加更多API
// - 项目管理API
// - 对话API
// - 知识库API
// - 用户API
export default {
healthCheck,
getApiInfo,
}

View File

@@ -0,0 +1,59 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios'
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: '/api/v1',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 添加token
const token = localStorage.getItem('token')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
return response.data
},
(error) => {
if (error.response) {
const { status } = error.response
switch (status) {
case 401:
// 未授权清除token并跳转登录
localStorage.removeItem('token')
window.location.href = '/login'
break
case 403:
console.error('没有权限访问该资源')
break
case 404:
console.error('请求的资源不存在')
break
case 500:
console.error('服务器错误')
break
default:
console.error(`请求错误: ${status}`)
}
}
return Promise.reject(error)
}
)
export default request

23
frontend/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', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
width: 100vw;
height: 100vh;
overflow: hidden;
}

View File

@@ -0,0 +1,168 @@
import { useState } from 'react'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
import { Layout, Menu, Avatar, Dropdown, Button } from 'antd'
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
HomeOutlined,
ExperimentOutlined,
UserOutlined,
SettingOutlined,
LogoutOutlined,
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
const { Header, Sider, Content } = Layout
// 12个智能体配置
const AGENTS = [
{ id: 'topic-evaluation', name: '选题评价', icon: '📊' },
{ id: 'picos-construction', name: 'PICOS构建', icon: '🔍' },
{ id: 'literature-search', name: '文献检索', icon: '📚' },
{ id: 'literature-screening', name: '文献筛选', icon: '🎯' },
{ id: 'data-extraction', name: '数据提取', icon: '📋' },
{ id: 'bias-assessment', name: '偏倚评价', icon: '⚖️' },
{ id: 'meta-analysis', name: 'Meta分析', icon: '📈' },
{ id: 'forest-plot', name: '森林图绘制', icon: '🌲' },
{ id: 'results-interpretation', name: '结果解读', icon: '💡' },
{ id: 'protocol-writing', name: '方案撰写', icon: '📝' },
{ id: 'article-writing', name: '文章撰写', icon: '✍️' },
{ id: 'submission-assistance', name: '投稿辅助', icon: '📬' },
]
const MainLayout = () => {
const [collapsed, setCollapsed] = useState(false)
const navigate = useNavigate()
const location = useLocation()
// 菜单项配置
const menuItems: MenuProps['items'] = [
{
key: '/',
icon: <HomeOutlined />,
label: '首页',
},
{
key: 'agents',
icon: <ExperimentOutlined />,
label: '智能体',
children: AGENTS.map((agent) => ({
key: `/agent/${agent.id}`,
label: `${agent.icon} ${agent.name}`,
})),
},
]
// 用户下拉菜单
const userMenuItems: MenuProps['items'] = [
{
key: 'profile',
icon: <UserOutlined />,
label: '个人中心',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '设置',
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
danger: true,
},
]
const handleMenuClick = ({ key }: { key: string }) => {
navigate(key)
}
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') {
console.log('退出登录')
// TODO: 实现登出逻辑
}
}
return (
<Layout style={{ height: '100vh' }}>
{/* 侧边栏 */}
<Sider trigger={null} collapsible collapsed={collapsed} theme="light">
<div
style={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: collapsed ? 18 : 20,
fontWeight: 'bold',
color: '#1890ff',
borderBottom: '1px solid #f0f0f0',
}}
>
{collapsed ? '🏥' : '🏥 AI临床研究'}
</div>
<Menu
mode="inline"
selectedKeys={[location.pathname]}
defaultOpenKeys={['agents']}
items={menuItems}
onClick={handleMenuClick}
style={{ borderRight: 0 }}
/>
</Sider>
{/* 主内容区 */}
<Layout>
{/* 头部 */}
<Header
style={{
padding: '0 24px',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: '1px solid #f0f0f0',
}}
>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
fontSize: '16px',
width: 64,
height: 64,
}}
/>
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="bottomRight">
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
<Avatar icon={<UserOutlined />} />
<span></span>
</div>
</Dropdown>
</Header>
{/* 内容区 */}
<Content
style={{
margin: '24px',
padding: 24,
background: '#fff',
borderRadius: 8,
overflow: 'auto',
}}
>
<Outlet />
</Content>
</Layout>
</Layout>
)
}
export default MainLayout

18
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { ConfigProvider } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ConfigProvider locale={zhCN}>
<BrowserRouter>
<App />
</BrowserRouter>
</ConfigProvider>
</React.StrictMode>,
)

View File

@@ -0,0 +1,92 @@
import { useParams } from 'react-router-dom'
import { Card, Typography, Tag, Button, Space } from 'antd'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
const { Title, Paragraph } = Typography
const AGENTS_MAP: Record<string, { name: string; icon: string; desc: string }> = {
'topic-evaluation': { name: '选题评价', icon: '📊', desc: '评估研究选题的价值和可行性' },
'picos-construction': { name: 'PICOS构建', icon: '🔍', desc: '构建研究问题的PICOS框架' },
'literature-search': { name: '文献检索', icon: '📚', desc: '系统化检索相关医学文献' },
'literature-screening': { name: '文献筛选', icon: '🎯', desc: '根据纳入排除标准筛选文献' },
'data-extraction': { name: '数据提取', icon: '📋', desc: '从文献中提取关键数据' },
'bias-assessment': { name: '偏倚评价', icon: '⚖️', desc: '评估研究偏倚风险' },
'meta-analysis': { name: 'Meta分析', icon: '📈', desc: '进行统计学meta分析' },
'forest-plot': { name: '森林图绘制', icon: '🌲', desc: '绘制meta分析森林图' },
'results-interpretation': { name: '结果解读', icon: '💡', desc: '解读分析结果的临床意义' },
'protocol-writing': { name: '方案撰写', icon: '📝', desc: '撰写研究方案' },
'article-writing': { name: '文章撰写', icon: '✍️', desc: '撰写学术论文' },
'submission-assistance': { name: '投稿辅助', icon: '📬', desc: '辅助期刊投稿' },
}
const AgentPage = () => {
const { agentId } = useParams<{ agentId: string }>()
const navigate = useNavigate()
const agent = agentId ? AGENTS_MAP[agentId] : null
if (!agent) {
return (
<Card>
<Paragraph></Paragraph>
<Button onClick={() => navigate('/')}></Button>
</Card>
)
}
return (
<div>
{/* 头部 */}
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/')}>
</Button>
<Card>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<span style={{ fontSize: 48, marginRight: 16 }}>{agent.icon}</span>
<Title level={2} style={{ display: 'inline', margin: 0 }}>
{agent.name}
</Title>
<Tag color="blue" style={{ marginLeft: 16 }}>
AI智能体
</Tag>
</div>
<Paragraph type="secondary" style={{ fontSize: 16 }}>
{agent.desc}
</Paragraph>
</Space>
</Card>
{/* 功能区域 - 占位符 */}
<Card title="智能对话区域" extra={<Tag color="orange"></Tag>}>
<div
style={{
height: 400,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f5f5f5',
borderRadius: 8,
}}
>
<Space direction="vertical" align="center">
<div style={{ fontSize: 64 }}>💬</div>
<Title level={4} type="secondary">
...
</Title>
<Paragraph type="secondary">
Day 9-10
</Paragraph>
</Space>
</div>
</Card>
</Space>
</div>
)
}
export default AgentPage

View File

@@ -0,0 +1,72 @@
import { Card, Row, Col, Typography, Button } from 'antd'
import { useNavigate } from 'react-router-dom'
import { ExperimentOutlined, RocketOutlined } from '@ant-design/icons'
const { Title, Paragraph } = Typography
const AGENTS = [
{ id: 'topic-evaluation', name: '选题评价', icon: '📊', desc: '评估研究选题的价值和可行性' },
{ id: 'picos-construction', name: 'PICOS构建', icon: '🔍', desc: '构建研究问题的PICOS框架' },
{ id: 'literature-search', name: '文献检索', icon: '📚', desc: '系统化检索相关医学文献' },
{ id: 'literature-screening', name: '文献筛选', icon: '🎯', desc: '根据纳入排除标准筛选文献' },
{ id: 'data-extraction', name: '数据提取', icon: '📋', desc: '从文献中提取关键数据' },
{ id: 'bias-assessment', name: '偏倚评价', icon: '⚖️', desc: '评估研究偏倚风险' },
{ id: 'meta-analysis', name: 'Meta分析', icon: '📈', desc: '进行统计学meta分析' },
{ id: 'forest-plot', name: '森林图绘制', icon: '🌲', desc: '绘制meta分析森林图' },
{ id: 'results-interpretation', name: '结果解读', icon: '💡', desc: '解读分析结果的临床意义' },
{ id: 'protocol-writing', name: '方案撰写', icon: '📝', desc: '撰写研究方案' },
{ id: 'article-writing', name: '文章撰写', icon: '✍️', desc: '撰写学术论文' },
{ id: 'submission-assistance', name: '投稿辅助', icon: '📬', desc: '辅助期刊投稿' },
]
const HomePage = () => {
const navigate = useNavigate()
return (
<div>
{/* 欢迎区域 */}
<Card style={{ marginBottom: 24, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
<div style={{ color: 'white', padding: '20px 0' }}>
<Title level={2} style={{ color: 'white', marginBottom: 16 }}>
<RocketOutlined /> 使 AI临床研究平台
</Title>
<Paragraph style={{ color: 'white', fontSize: 16, marginBottom: 0 }}>
12
</Paragraph>
</div>
</Card>
{/* 智能体卡片 */}
<Title level={3} style={{ marginBottom: 16 }}>
<ExperimentOutlined />
</Title>
<Row gutter={[16, 16]}>
{AGENTS.map((agent) => (
<Col xs={24} sm={12} md={8} lg={6} key={agent.id}>
<Card
hoverable
style={{ height: '100%' }}
onClick={() => navigate(`/agent/${agent.id}`)}
>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 48, marginBottom: 12 }}>{agent.icon}</div>
<Title level={4} style={{ marginBottom: 8 }}>
{agent.name}
</Title>
<Paragraph type="secondary" style={{ marginBottom: 16, minHeight: 44 }}>
{agent.desc}
</Paragraph>
<Button type="primary" block>
使
</Button>
</div>
</Card>
</Col>
))}
</Row>
</div>
)
}
export default HomePage

View File

@@ -0,0 +1,89 @@
// 用户类型
export interface User {
id: string
email: string
name?: string
avatarUrl?: string
role: 'user' | 'admin'
}
// 项目类型
export interface Project {
id: string
userId: string
name: string
description: string
conversationCount: number
createdAt: string
updatedAt: string
}
// 会话类型
export interface Conversation {
id: string
userId: string
projectId?: string
agentId: string
title: string
modelName: string
messageCount: number
totalTokens: number
createdAt: string
updatedAt: string
}
// 消息类型
export interface Message {
id: string
conversationId: string
role: 'user' | 'assistant'
content: string
metadata?: {
kbReferences?: string[]
citations?: any[]
modelParams?: any
}
tokens?: number
isPinned: boolean
createdAt: string
}
// 知识库类型
export interface KnowledgeBase {
id: string
userId: string
name: string
description?: string
difyDatasetId: string
fileCount: number
totalSizeBytes: number
createdAt: string
updatedAt: string
}
// 文档类型
export interface Document {
id: string
kbId: string
userId: string
filename: string
fileType: string
fileSizeBytes: number
fileUrl: string
difyDocumentId: string
status: 'uploading' | 'processing' | 'completed' | 'failed'
progress: number
errorMessage?: string
segmentsCount?: number
tokensCount?: number
uploadedAt: string
processedAt?: string
}
// API响应类型
export interface ApiResponse<T = any> {
success: boolean
data: T
message?: string
}

View File

@@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
corePlugins: {
preflight: false, // 禁用 Tailwind 的基础样式重置,避免与 Ant Design 冲突
},
}

32
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path alias */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

23
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,23 @@
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,
},
},
},
})

13
frontend/启动前端.bat Normal file
View File

@@ -0,0 +1,13 @@
@echo off
chcp 65001 >nul
echo ====================================
echo AI临床研究平台 - 前端开发服务器
echo ====================================
echo.
cd /d %~dp0
echo 正在启动前端开发服务器...
echo 访问地址: http://localhost:3000
echo.
call npm run dev
pause