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

26 KiB
Raw Blame History

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计划确认卡片

// 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执行路径树

// 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📌 执行进度动画)

// 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结果报告卡片

// 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三线表

// 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>
  );
};
/* 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

// 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 封装

// 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. 模块注册

// 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 颜色系统(与原型对齐)

/* 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 动画

/* 渐入动画 */
.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 全局超时配置

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