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>
This commit is contained in:
2026-02-18 21:58:37 +08:00
parent f9ed0c2528
commit 8137e3cde2
19 changed files with 5756 additions and 98 deletions

View File

@@ -0,0 +1,897 @@
# 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
```