Files
AIclinicalresearch/docs/03-业务模块/SSA-智能统计分析/04-开发计划/04-前端开发指南.md
HaHafeng 8137e3cde2 feat(ssa): Complete SSA-Pro MVP development plan v1.3
Summary:

- Add PRD and architecture design V4 (Brain-Hand model)

- Complete 5 development guide documents

- Pass 3 rounds of team review (v1.0 -> v1.3)

- Add module status guide document

- Update system status document

Key Features:

- Brain-Hand architecture: Node.js + R Docker

- Statistical guardrails with auto degradation

- HITL workflow: PlanCard -> ExecutionTrace -> ResultCard

- Mixed data protocol: inline vs OSS

- Reproducible R code delivery

MVP Scope: 10 statistical tools

Status: Design 100%, ready for development
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 21:58:37 +08:00

898 lines
26 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SSA-Pro 前端开发指南
> **文档版本:** v1.3
> **创建日期:** 2026-02-18
> **最后更新:** 2026-02-18纳入 V3.0 终极审查建议)
> **目标读者:** 前端工程师
> **原型参考:** `03-UI设计/智能统计分析V2.html`
---
## 1. 模块目录结构
```
frontend-v2/src/modules/ssa/
├── index.ts # 模块入口,导出路由
├── pages/
│ └── SSAWorkspace.tsx # 主页面(工作区)
├── components/
│ ├── layout/
│ │ ├── SSASidebar.tsx # 左侧边栏
│ │ ├── SSAHeader.tsx # 顶部标题栏
│ │ └── SSAInputArea.tsx # 底部输入区
│ ├── chat/
│ │ ├── MessageList.tsx # 消息流容器
│ │ ├── SystemMessage.tsx # 系统消息气泡
│ │ ├── UserMessage.tsx # 用户消息气泡
│ │ └── AssistantMessage.tsx # AI 消息(含卡片)
│ ├── cards/
│ │ ├── DataUploader.tsx # 数据上传区
│ │ ├── DataStatus.tsx # 数据集状态卡片
│ │ ├── PlanCard.tsx # 分析计划确认卡片 ⭐
│ │ ├── ExecutionTrace.tsx # 执行路径树 ⭐
│ │ ├── ExecutionProgress.tsx# 📌 执行进度动画 ⭐
│ │ └── ResultCard.tsx # 结果报告卡片 ⭐
│ └── common/
│ ├── APATable.tsx # 三线表组件
│ └── PlotViewer.tsx # 图表查看器
├── hooks/
│ ├── useSSASession.ts # 会话管理 Hook
│ └── useSSAExecution.ts # 执行控制 Hook
├── store/
│ └── ssaStore.ts # Zustand Store
├── api/
│ └── ssaApi.ts # API 封装
├── types/
│ └── index.ts # 类型定义
└── styles/
└── ssa.css # 模块样式
```
---
## 2. 原型图核心元素解析
根据 `智能统计分析V2.html` 原型,需实现以下核心 UI
### 2.1 整体布局
```
┌─────────────────────────────────────────────────────────────────┐
│ ┌───────────┐ ┌─────────────────────────────────────────────┐ │
│ │ │ │ Header (会话标题) │ │
│ │ Sidebar │ ├─────────────────────────────────────────────┤ │
│ │ │ │ │ │
│ │ - 导入数据│ │ Chat Flow (消息流) │ │
│ │ - 新会话 │ │ │ │
│ │ - 历史 │ │ - SystemMessage (欢迎/上传引导) │ │
│ │ │ │ - UserMessage (用户输入) │ │
│ │ │ │ - PlanCard (计划确认) │ │
│ │ ─────── │ │ - ExecutionTrace (执行路径) │ │
│ │ 数据状态 │ │ - ResultCard (结果报告) │ │
│ │ │ │ │ │
│ │ │ ├─────────────────────────────────────────────┤ │
│ │ │ │ InputArea (输入框 + 发送按钮) │ │
│ └───────────┘ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 核心组件设计规范
| 组件 | 原型特征 | 实现要点 |
|------|---------|---------|
| **SSASidebar** | 宽 256px白色背景阴影分隔 | Logo + 按钮 + 历史列表 + 数据状态 |
| **PlanCard** | 圆角卡片,分组/检验变量展示,护栏警告 | 支持参数编辑、确认/修改按钮 |
| **ExecutionTrace** | 竖向树状结构,带状态图标和连接线 | 动画展开,步骤状态(成功/警告/进行中) |
| **ResultCard** | 多区块:三线表 + 图表 + 解读 + 下载 | APA 格式表格Base64 图片渲染 |
| **APATable** | 顶线(2px) + 表头下线(1px) + 底线(2px) | 数字右对齐,等宽字体 |
---
## 3. 核心组件实现
### 3.1 PlanCard计划确认卡片
```tsx
// components/cards/PlanCard.tsx
import React from 'react';
import { Card, Button, Tag, Alert, Space, Descriptions } from 'antd';
import { PlayCircleOutlined, EditOutlined, SafetyOutlined } from '@ant-design/icons';
interface PlanCardProps {
plan: {
tool_code: string;
tool_name: string;
reasoning: string;
params: Record<string, any>;
guardrails: {
check_normality?: boolean;
auto_fix?: boolean;
};
};
dataSchema: {
columns: Array<{ name: string; type: string; uniqueValues?: string[] }>;
};
onConfirm: () => void;
onEdit: () => void;
loading?: boolean;
}
export const PlanCard: React.FC<PlanCardProps> = ({
plan,
dataSchema,
onConfirm,
onEdit,
loading = false
}) => {
// 查找变量类型信息
const getColumnInfo = (colName: string) => {
const col = dataSchema.columns.find(c => c.name === colName);
if (!col) return '';
if (col.type === 'categorical' && col.uniqueValues) {
return `(分类: ${col.uniqueValues.slice(0, 3).join('/')})`;
}
return `(${col.type === 'numeric' ? '数值型' : '分类型'})`;
};
return (
<Card
className="plan-card"
title={
<Space>
<span></span>
<Tag color="blue">{plan.tool_name}</Tag>
</Space>
}
styles={{ header: { background: '#f8fafc' } }}
>
{/* 变量映射 */}
<div className="grid grid-cols-2 gap-4 mb-4">
{Object.entries(plan.params).map(([key, value]) => (
<div key={key} className="bg-slate-50 p-3 rounded border border-slate-100">
<div className="text-xs text-slate-400 uppercase font-bold mb-1">
{key.replace(/_/g, ' ')}
</div>
<div className="text-sm font-medium text-slate-800">
{String(value)}
<span className="text-xs text-slate-400 font-normal ml-1">
{getColumnInfo(String(value))}
</span>
</div>
</div>
))}
</div>
{/* 护栏提示 */}
{plan.guardrails.check_normality && (
<Alert
type="warning"
icon={<SafetyOutlined />}
showIcon
message="统计护栏 (自动执行)"
description={
<ul className="list-disc list-inside text-xs mt-1 space-y-1">
<li>Shapiro-Wilk </li>
<li>Levene </li>
{plan.guardrails.auto_fix && (
<li className="font-medium">
Wilcoxon
</li>
)}
</ul>
}
className="mb-4"
/>
)}
{/* 操作按钮 */}
<div className="flex justify-end gap-3 pt-3 border-t border-slate-100">
<Button icon={<EditOutlined />} onClick={onEdit}>
</Button>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={onConfirm}
loading={loading}
>
</Button>
</div>
</Card>
);
};
```
### 3.2 ExecutionTrace执行路径树
```tsx
// components/cards/ExecutionTrace.tsx
import React from 'react';
import { CheckCircleFilled, ExclamationCircleFilled,
SwapOutlined, CalculatorOutlined, LoadingOutlined } from '@ant-design/icons';
interface TraceStep {
id: string;
label: string;
status: 'success' | 'warning' | 'error' | 'running' | 'pending';
detail?: string;
subLabel?: string;
}
interface ExecutionTraceProps {
steps: TraceStep[];
}
export const ExecutionTrace: React.FC<ExecutionTraceProps> = ({ steps }) => {
const getIcon = (status: TraceStep['status']) => {
switch (status) {
case 'success':
return <CheckCircleFilled className="text-green-500" />;
case 'warning':
return <ExclamationCircleFilled className="text-amber-500" />;
case 'error':
return <ExclamationCircleFilled className="text-red-500" />;
case 'running':
return <LoadingOutlined className="text-blue-500" spin />;
default:
return <div className="w-4 h-4 rounded-full bg-slate-200" />;
}
};
return (
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="text-xs font-bold text-slate-400 uppercase mb-3 tracking-wider flex justify-between">
<span></span>
{steps.every(s => s.status === 'success') && (
<span className="text-green-600">
<CheckCircleFilled />
</span>
)}
</div>
<div className="space-y-3 font-mono text-xs relative pl-2">
{/* 连接线 */}
<div className="absolute left-[19px] top-2 bottom-4 w-px bg-slate-200" />
{steps.map((step, idx) => (
<div key={step.id} className="flex items-start gap-3 relative z-10">
<div className="w-4 h-4 flex items-center justify-center mt-0.5">
{getIcon(step.status)}
</div>
<div>
<span className={`
${step.status === 'warning' ? 'text-amber-700 font-medium' : ''}
${step.status === 'success' && step.detail ? 'text-slate-800 font-medium' : 'text-slate-600'}
`}>
{step.label}
</span>
{step.detail && (
<div className="mt-1">
<span className={`
px-1.5 py-0.5 rounded border font-bold
${step.status === 'error'
? 'bg-red-50 text-red-600 border-red-100'
: 'bg-green-50 text-green-600 border-green-100'}
`}>
{step.detail}
</span>
{step.subLabel && (
<span className="ml-2 text-slate-400">{step.subLabel}</span>
)}
</div>
)}
</div>
</div>
))}
</div>
</div>
);
};
// 使用示例
const mockSteps: TraceStep[] = [
{ id: '1', label: '加载数据 (n=150)', status: 'success' },
{
id: '2',
label: '正态性检验 (Shapiro-Wilk)',
status: 'error',
detail: 'P = 0.002 (< 0.05) ❌',
subLabel: '-> 拒绝正态假设'
},
{ id: '3', label: '策略切换: T-Test -> Wilcoxon Test', status: 'warning' },
{ id: '4', label: '计算完成', status: 'success' },
];
```
### 3.3 ExecutionProgress📌 执行进度动画)
```tsx
// components/cards/ExecutionProgress.tsx
import React, { useState, useEffect } from 'react';
import { LoadingOutlined, CheckCircleFilled } from '@ant-design/icons';
import { Progress, Typography } from 'antd';
const { Text } = Typography;
interface ExecutionProgressProps {
isExecuting: boolean;
onComplete?: () => void;
}
// 📌 模拟进度文案(缓解用户等待焦虑)
const PROGRESS_MESSAGES = [
'正在加载数据...',
'执行统计护栏检验...',
'进行核心计算...',
'生成可视化图表...',
'格式化结果...',
'即将完成...'
];
export const ExecutionProgress: React.FC<ExecutionProgressProps> = ({
isExecuting,
onComplete
}) => {
const [progress, setProgress] = useState(0);
const [messageIndex, setMessageIndex] = useState(0);
useEffect(() => {
if (!isExecuting) {
setProgress(0);
setMessageIndex(0);
return;
}
// 📌 模拟进度(实际进度由后端控制)
const progressInterval = setInterval(() => {
setProgress(prev => {
if (prev >= 90) return prev; // 卡在 90%,等待真正完成
return prev + Math.random() * 10;
});
}, 500);
const messageInterval = setInterval(() => {
setMessageIndex(prev =>
prev < PROGRESS_MESSAGES.length - 1 ? prev + 1 : prev
);
}, 2000);
return () => {
clearInterval(progressInterval);
clearInterval(messageInterval);
};
}, [isExecuting]);
if (!isExecuting) return null;
return (
<div className="bg-white border border-blue-200 rounded-xl p-6 shadow-sm animate-pulse">
<div className="flex items-center gap-3 mb-4">
<LoadingOutlined className="text-blue-500 text-xl" spin />
<Text strong className="text-blue-700"></Text>
</div>
<Progress
percent={Math.round(progress)}
status="active"
strokeColor={{
'0%': '#3b82f6',
'100%': '#10b981'
}}
/>
<Text type="secondary" className="text-sm mt-2 block">
{PROGRESS_MESSAGES[messageIndex]}
</Text>
<Text type="secondary" className="text-xs mt-4 block opacity-60">
10-30 ...
</Text>
</div>
);
};
```
### 3.4 ResultCard结果报告卡片
```tsx
// components/cards/ResultCard.tsx
import React from 'react';
import { Button, Divider, Space, Typography } from 'antd';
import { DownloadOutlined, FileWordOutlined } from '@ant-design/icons';
import { APATable } from '../common/APATable';
import { PlotViewer } from '../common/PlotViewer';
const { Title, Paragraph, Text } = Typography;
interface ResultCardProps {
result: {
method: string;
statistic: number;
p_value: number;
p_value_fmt: string; // 🆕 R 服务返回的格式化 p 值
group_stats: Array<{
group: string;
n: number;
mean?: number;
median?: number;
sd?: number;
iqr?: [number, number];
}>;
};
plots: string[]; // Base64 图片
interpretation?: string; // Critic 解读
reproducibleCode: string;
onDownloadCode: () => void;
}
export const ResultCard: React.FC<ResultCardProps> = ({
result,
plots,
interpretation,
reproducibleCode,
onDownloadCode
}) => {
// 构造表格数据
const tableData = result.group_stats.map(g => ({
group: g.group,
n: g.n,
value: g.median
? `${g.median.toFixed(2)} [${g.iqr?.[0].toFixed(2)} - ${g.iqr?.[1].toFixed(2)}]`
: `${g.mean?.toFixed(2)} ± ${g.sd?.toFixed(2)}`
}));
const columns = [
{ key: 'group', title: 'Group', width: 120 },
{ key: 'n', title: 'N', width: 60, align: 'right' as const },
{ key: 'value', title: result.method.includes('Wilcoxon') ? 'Median [IQR]' : 'Mean ± SD' },
{
key: 'statistic',
title: 'Statistic',
render: () => result.statistic.toFixed(2),
rowSpan: tableData.length
},
{
key: 'pValue',
title: 'P-Value',
render: () => (
<Text strong>
{/* 🆕 直接使用 R 服务返回的格式化值 */}
{result.p_value_fmt}
{result.p_value < 0.01 ? ' **' : result.p_value < 0.05 ? ' *' : ''}
</Text>
),
rowSpan: tableData.length
},
];
return (
<div className="bg-white border border-slate-200 rounded-xl shadow-md overflow-hidden">
{/* Header */}
<div className="bg-slate-50 px-6 py-4 border-b border-slate-200">
<Title level={5} className="mb-0"></Title>
<Text type="secondary" className="text-xs">
{result.method}
</Text>
</div>
{/* 1. 统计表格 */}
<div className="p-6 border-b border-slate-100">
<div className="flex items-center gap-2 mb-4">
<div className="w-1 h-4 bg-blue-600 rounded-full" />
<Text strong> 1. (线)</Text>
</div>
<APATable columns={columns} data={tableData} />
<Text type="secondary" className="text-xs mt-2 block italic">
Note: {result.method.includes('Wilcoxon') ? 'IQR = Interquartile Range' : 'SD = Standard Deviation'};
* P &lt; 0.05, ** P &lt; 0.01.
</Text>
</div>
{/* 2. 图表 */}
{plots.length > 0 && (
<div className="p-6 border-b border-slate-100">
<div className="flex items-center gap-2 mb-3">
<div className="w-1 h-4 bg-blue-600 rounded-full" />
<Text strong> 1. </Text>
</div>
<PlotViewer src={plots[0]} alt="Statistical Plot" />
</div>
)}
{/* 3. 方法与解读 */}
{interpretation && (
<div className="p-6 bg-slate-50/50 border-b border-slate-100">
<div className="flex items-center gap-2 mb-3">
<div className="w-1 h-4 bg-indigo-600 rounded-full" />
<Text strong></Text>
</div>
<div
className="prose prose-sm max-w-none text-slate-600"
dangerouslySetInnerHTML={{ __html: interpretation }}
/>
</div>
)}
{/* 4. 资产交付 */}
<div className="bg-slate-100 p-4 flex items-center justify-between">
<Text type="secondary" className="text-xs font-semibold uppercase tracking-wide">
</Text>
<Space>
<Button
icon={<DownloadOutlined />}
onClick={onDownloadCode}
>
R
</Button>
<Button
type="primary"
icon={<FileWordOutlined />}
disabled // MVP 阶段禁用
>
(Word)
</Button>
</Space>
</div>
</div>
);
};
```
### 3.5 APATable三线表
```tsx
// components/common/APATable.tsx
import React from 'react';
import './APATable.css';
interface Column {
key: string;
title: string;
width?: number;
align?: 'left' | 'center' | 'right';
render?: (value: any, record: any, index: number) => React.ReactNode;
rowSpan?: number;
}
interface APATableProps {
columns: Column[];
data: Record<string, any>[];
}
export const APATable: React.FC<APATableProps> = ({ columns, data }) => {
return (
<div className="overflow-x-auto">
<table className="apa-table">
<thead>
<tr>
{columns.map(col => (
<th
key={col.key}
style={{ width: col.width, textAlign: col.align || 'left' }}
>
{col.title}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIdx) => (
<tr key={rowIdx}>
{columns.map((col, colIdx) => {
// 处理 rowSpan
if (col.rowSpan && rowIdx > 0) return null;
const value = col.render
? col.render(row[col.key], row, rowIdx)
: row[col.key];
return (
<td
key={col.key}
rowSpan={col.rowSpan}
style={{ textAlign: col.align || 'left' }}
className={col.rowSpan ? 'align-middle border-l border-slate-100' : ''}
>
{value}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
};
```
```css
/* components/common/APATable.css */
.apa-table {
width: 100%;
border-collapse: collapse;
font-variant-numeric: tabular-nums;
font-size: 14px;
}
.apa-table thead th {
border-top: 2px solid #1e293b;
border-bottom: 1px solid #1e293b;
padding: 8px 12px;
text-align: left;
font-weight: 600;
color: #334155;
}
.apa-table tbody td {
padding: 8px 12px;
border-bottom: 1px solid #e2e8f0;
color: #475569;
}
.apa-table tbody tr:last-child td {
border-bottom: 2px solid #1e293b;
}
```
---
## 4. Zustand Store
```typescript
// store/ssaStore.ts
import { create } from 'zustand';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
contentType: 'text' | 'plan' | 'result' | 'trace';
content: any;
createdAt: string;
}
interface SSAState {
// 会话
sessionId: string | null;
sessionTitle: string;
// 数据
dataLoaded: boolean;
dataSchema: object | null;
dataFileName: string;
dataRowCount: number;
// 消息
messages: Message[];
// 执行状态
isPlanning: boolean;
isExecuting: boolean;
currentPlan: object | null;
// Actions
setSession: (id: string, title?: string) => void;
setDataLoaded: (schema: object, fileName: string, rowCount: number) => void;
addMessage: (message: Omit<Message, 'id' | 'createdAt'>) => void;
setPlanning: (planning: boolean) => void;
setExecuting: (executing: boolean) => void;
setCurrentPlan: (plan: object | null) => void;
reset: () => void;
}
export const useSSAStore = create<SSAState>((set, get) => ({
sessionId: null,
sessionTitle: '新会话',
dataLoaded: false,
dataSchema: null,
dataFileName: '',
dataRowCount: 0,
messages: [],
isPlanning: false,
isExecuting: false,
currentPlan: null,
setSession: (id, title = '新会话') => set({ sessionId: id, sessionTitle: title }),
setDataLoaded: (schema, fileName, rowCount) => set({
dataLoaded: true,
dataSchema: schema,
dataFileName: fileName,
dataRowCount: rowCount
}),
addMessage: (message) => set(state => ({
messages: [
...state.messages,
{
...message,
id: crypto.randomUUID(),
createdAt: new Date().toISOString()
}
]
})),
setPlanning: (planning) => set({ isPlanning: planning }),
setExecuting: (executing) => set({ isExecuting: executing }),
setCurrentPlan: (plan) => set({ currentPlan: plan }),
reset: () => set({
sessionId: null,
sessionTitle: '新会话',
dataLoaded: false,
dataSchema: null,
dataFileName: '',
dataRowCount: 0,
messages: [],
isPlanning: false,
isExecuting: false,
currentPlan: null
})
}));
```
---
## 5. API 封装
```typescript
// api/ssaApi.ts
import { apiClient } from '@/common/api/client';
const BASE = '/api/v1/ssa';
export const ssaApi = {
// 会话
createSession: () =>
apiClient.post<{ id: string }>(`${BASE}/sessions`),
getSession: (id: string) =>
apiClient.get(`${BASE}/sessions/${id}`),
listSessions: () =>
apiClient.get(`${BASE}/sessions`),
// 数据上传
uploadData: (sessionId: string, file: File) => {
const formData = new FormData();
formData.append('file', file);
return apiClient.post(`${BASE}/sessions/${sessionId}/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
},
// 生成计划
generatePlan: (sessionId: string, query: string) =>
apiClient.post(`${BASE}/sessions/${sessionId}/plan`, { query }),
// 执行分析(📌 超时 120s
executeAnalysis: (sessionId: string, plan: object) =>
apiClient.post(`${BASE}/sessions/${sessionId}/execute`, { plan }, {
timeout: 120000 // 📌 120s 超时,应对复杂计算
}),
// 下载代码
downloadCode: (sessionId: string, messageId: string) =>
apiClient.get(`${BASE}/sessions/${sessionId}/download-code/${messageId}`, {
responseType: 'blob'
}),
};
```
---
## 6. 模块注册
```typescript
// index.ts
import { lazy } from 'react';
const SSAWorkspace = lazy(() => import('./pages/SSAWorkspace'));
export const ssaRoutes = [
{
path: '/ssa',
element: <SSAWorkspace />,
meta: {
title: '智能统计分析',
icon: 'BarChartOutlined',
requireAuth: true
}
}
];
// 在 moduleRegistry.ts 中注册
// import { ssaRoutes } from './modules/ssa';
// registerModule('ssa', ssaRoutes);
```
---
## 7. 样式规范
### 7.1 颜色系统(与原型对齐)
```css
/* styles/ssa.css */
:root {
--ssa-primary: #3b82f6; /* blue-500 */
--ssa-primary-hover: #2563eb; /* blue-600 */
--ssa-bg: #f8fafc; /* slate-50 */
--ssa-card-bg: #ffffff;
--ssa-border: #e2e8f0; /* slate-200 */
--ssa-text: #334155; /* slate-700 */
--ssa-text-muted: #94a3b8; /* slate-400 */
--ssa-success: #22c55e; /* green-500 */
--ssa-warning: #f59e0b; /* amber-500 */
--ssa-error: #ef4444; /* red-500 */
}
```
### 7.2 动画
```css
/* 渐入动画 */
.ssa-fade-in {
animation: ssaFadeIn 0.4s ease-out forwards;
opacity: 0;
}
/* 上滑动画 */
.ssa-slide-up {
animation: ssaSlideUp 0.4s ease-out forwards;
opacity: 0;
transform: translateY(10px);
}
@keyframes ssaFadeIn {
to { opacity: 1; }
}
@keyframes ssaSlideUp {
to { transform: translateY(0); opacity: 1; }
}
```
---
## 8. 开发检查清单
| 组件 | 功能 | 状态 |
|------|------|------|
| SSASidebar | 导入数据、新建会话、历史列表、数据状态 | ⬜ |
| DataUploader | 拖拽/点击上传,进度显示 | ⬜ |
| MessageList | 消息流滚动,自动滚底 | ⬜ |
| PlanCard | 参数展示、护栏提示、确认/修改按钮 | ⬜ |
| ExecutionTrace | 步骤树、状态图标、连接线 | ⬜ |
| **ExecutionProgress** | **📌 执行中进度动画,缓解等待焦虑** | ⬜ |
| ResultCard | 三线表、图表、解读、下载按钮 | ⬜ |
| APATable | APA 格式表格样式 | ⬜ |
| Zustand Store | 状态管理 | ⬜ |
| API 对接 | 所有接口联调,**超时 120s** | ⬜ |
---
## 9. 关键配置
### 9.1 Axios 全局超时配置
```typescript
// api/client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: '/api',
timeout: 60000, // 默认 60s
headers: {
'Content-Type': 'application/json'
}
});
// 📌 对于 SSA 执行接口,单独设置 120s 超时
// 见 ssaApi.executeAnalysis
```