feat(ssa): Complete T-test end-to-end testing with 9 bug fixes - Phase 1 core 85% complete. R service: missing value auto-filter. Backend: error handling, variable matching, dynamic filename. Frontend: module activation, session isolation, error propagation. Full flow verified.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# SSA-Pro 前端开发指南
|
||||
|
||||
> **文档版本:** v1.3
|
||||
> **文档版本:** v1.5
|
||||
> **创建日期:** 2026-02-18
|
||||
> **最后更新:** 2026-02-18(纳入 V3.0 终极审查建议)
|
||||
> **最后更新:** 2026-02-18(纳入专家配置体系 + 护栏 Action 展示)
|
||||
> **目标读者:** 前端工程师
|
||||
> **原型参考:** `03-UI设计/智能统计分析V2.html`
|
||||
|
||||
@@ -19,7 +19,8 @@ frontend-v2/src/modules/ssa/
|
||||
│ ├── layout/
|
||||
│ │ ├── SSASidebar.tsx # 左侧边栏
|
||||
│ │ ├── SSAHeader.tsx # 顶部标题栏
|
||||
│ │ └── SSAInputArea.tsx # 底部输入区
|
||||
│ │ ├── SSAInputArea.tsx # 底部输入区
|
||||
│ │ └── ModeSwitch.tsx # 🆕 模式切换 Tab
|
||||
│ ├── chat/
|
||||
│ │ ├── MessageList.tsx # 消息流容器
|
||||
│ │ ├── SystemMessage.tsx # 系统消息气泡
|
||||
@@ -31,44 +32,59 @@ frontend-v2/src/modules/ssa/
|
||||
│ │ ├── PlanCard.tsx # 分析计划确认卡片 ⭐
|
||||
│ │ ├── ExecutionTrace.tsx # 执行路径树 ⭐
|
||||
│ │ ├── ExecutionProgress.tsx# 📌 执行进度动画 ⭐
|
||||
│ │ └── ResultCard.tsx # 结果报告卡片 ⭐
|
||||
│ │ ├── ResultCard.tsx # 结果报告卡片 ⭐
|
||||
│ │ └── SAPPreview.tsx # 🆕 SAP 文档预览/下载
|
||||
│ ├── consult/ # 🆕 咨询模式组件
|
||||
│ │ ├── ConsultChat.tsx # 无数据对话界面
|
||||
│ │ └── SAPDownloadButton.tsx# SAP 下载按钮
|
||||
│ └── common/
|
||||
│ ├── APATable.tsx # 三线表组件
|
||||
│ └── PlotViewer.tsx # 图表查看器
|
||||
├── hooks/
|
||||
│ ├── useSSASession.ts # 会话管理 Hook
|
||||
│ └── useSSAExecution.ts # 执行控制 Hook
|
||||
│ ├── useSSAExecution.ts # 执行控制 Hook
|
||||
│ └── useSSAConsult.ts # 🆕 咨询模式 Hook
|
||||
├── store/
|
||||
│ └── ssaStore.ts # Zustand Store
|
||||
│ └── ssaStore.ts # Zustand Store(含 mode 状态)
|
||||
├── api/
|
||||
│ └── ssaApi.ts # API 封装
|
||||
│ └── ssaApi.ts # API 封装(含咨询 API)
|
||||
├── types/
|
||||
│ └── index.ts # 类型定义
|
||||
└── styles/
|
||||
└── ssa.css # 模块样式
|
||||
```
|
||||
|
||||
### 1.1 🆕 双模式设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **模式切换** | 顶部 Tab 切换"智能分析"/"统计咨询" |
|
||||
| **无数据友好** | 咨询模式不要求上传数据 |
|
||||
| **SAP 导出** | 咨询完成后可下载 Word/Markdown |
|
||||
|
||||
---
|
||||
|
||||
## 2. 原型图核心元素解析
|
||||
|
||||
根据 `智能统计分析V2.html` 原型,需实现以下核心 UI:
|
||||
|
||||
### 2.1 整体布局
|
||||
### 2.1 整体布局(含模式切换)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ┌───────────┐ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ │ │ Header (会话标题) │ │
|
||||
│ │ Sidebar │ ├─────────────────────────────────────────────┤ │
|
||||
│ │ │ │ │ │
|
||||
│ │ - 导入数据│ │ Chat Flow (消息流) │ │
|
||||
│ │ - 新会话 │ │ │ │
|
||||
│ │ - 历史 │ │ - SystemMessage (欢迎/上传引导) │ │
|
||||
│ │ │ │ 🆕 [智能分析] [统计咨询] ← 模式切换 Tab │ │
|
||||
│ │ Sidebar │ │ Header (会话标题) │ │
|
||||
│ │ │ ├─────────────────────────────────────────────┤ │
|
||||
│ │ - 导入数据│ │ │ │
|
||||
│ │ - 新会话 │ │ Chat Flow (消息流) │ │
|
||||
│ │ - 历史 │ │ │ │
|
||||
│ │ │ │ - SystemMessage (欢迎/上传引导) │ │
|
||||
│ │ │ │ - UserMessage (用户输入) │ │
|
||||
│ │ │ │ - PlanCard (计划确认) │ │
|
||||
│ │ ─────── │ │ - ExecutionTrace (执行路径) │ │
|
||||
│ │ 数据状态 │ │ - ResultCard (结果报告) │ │
|
||||
│ │ (分析模式) │ │ - 🆕 SAPPreview (咨询模式) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ ├─────────────────────────────────────────────┤ │
|
||||
│ │ │ │ InputArea (输入框 + 发送按钮) │ │
|
||||
@@ -214,9 +230,11 @@ import { CheckCircleFilled, ExclamationCircleFilled,
|
||||
interface TraceStep {
|
||||
id: string;
|
||||
label: string;
|
||||
status: 'success' | 'warning' | 'error' | 'running' | 'pending';
|
||||
status: 'success' | 'warning' | 'error' | 'running' | 'pending' | 'switched'; // 🆕 switched
|
||||
detail?: string;
|
||||
subLabel?: string;
|
||||
actionType?: 'Block' | 'Warn' | 'Switch'; // 🆕 护栏 Action 类型
|
||||
switchTarget?: string; // 🆕 Switch 目标工具
|
||||
}
|
||||
|
||||
interface ExecutionTraceProps {
|
||||
@@ -232,12 +250,32 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = ({ steps }) => {
|
||||
return <ExclamationCircleFilled className="text-amber-500" />;
|
||||
case 'error':
|
||||
return <ExclamationCircleFilled className="text-red-500" />;
|
||||
case 'switched': // 🆕 方法切换
|
||||
return <SwapOutlined className="text-blue-500" />;
|
||||
case 'running':
|
||||
return <LoadingOutlined className="text-blue-500" spin />;
|
||||
default:
|
||||
return <div className="w-4 h-4 rounded-full bg-slate-200" />;
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 获取 Action 类型标签
|
||||
const getActionTag = (step: TraceStep) => {
|
||||
if (!step.actionType) return null;
|
||||
|
||||
const tagStyles = {
|
||||
'Block': 'bg-red-100 text-red-700 border-red-200',
|
||||
'Warn': 'bg-amber-100 text-amber-700 border-amber-200',
|
||||
'Switch': 'bg-blue-100 text-blue-700 border-blue-200'
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`ml-2 px-1.5 py-0.5 text-xs rounded border ${tagStyles[step.actionType]}`}>
|
||||
{step.actionType}
|
||||
{step.switchTarget && <span className="ml-1">→ {step.switchTarget}</span>}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm">
|
||||
@@ -641,26 +679,39 @@ export const APATable: React.FC<APATableProps> = ({ columns, data }) => {
|
||||
|
||||
---
|
||||
|
||||
## 4. Zustand Store
|
||||
## 4. Zustand Store(含模式切换)
|
||||
|
||||
```typescript
|
||||
// store/ssaStore.ts
|
||||
import { create } from 'zustand';
|
||||
|
||||
// 🆕 模式类型
|
||||
type SSAMode = 'analysis' | 'consult';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
contentType: 'text' | 'plan' | 'result' | 'trace';
|
||||
contentType: 'text' | 'plan' | 'result' | 'trace' | 'sap'; // 🆕 增加 sap 类型
|
||||
content: any;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// 🆕 SAP 文档类型
|
||||
interface SAPDocument {
|
||||
title: string;
|
||||
sections: Array<{ heading: string; content: string }>;
|
||||
recommendedTools: string[];
|
||||
}
|
||||
|
||||
interface SSAState {
|
||||
// 🆕 模式
|
||||
mode: SSAMode;
|
||||
|
||||
// 会话
|
||||
sessionId: string | null;
|
||||
sessionTitle: string;
|
||||
|
||||
// 数据
|
||||
// 数据(分析模式)
|
||||
dataLoaded: boolean;
|
||||
dataSchema: object | null;
|
||||
dataFileName: string;
|
||||
@@ -674,17 +725,25 @@ interface SSAState {
|
||||
isExecuting: boolean;
|
||||
currentPlan: object | null;
|
||||
|
||||
// 🆕 咨询模式状态
|
||||
currentSAP: SAPDocument | null;
|
||||
isGeneratingSAP: boolean;
|
||||
|
||||
// Actions
|
||||
setMode: (mode: SSAMode) => void; // 🆕
|
||||
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;
|
||||
setCurrentSAP: (sap: SAPDocument | null) => void; // 🆕
|
||||
setGeneratingSAP: (generating: boolean) => void; // 🆕
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useSSAStore = create<SSAState>((set, get) => ({
|
||||
mode: 'analysis', // 🆕 默认分析模式
|
||||
sessionId: null,
|
||||
sessionTitle: '新会话',
|
||||
dataLoaded: false,
|
||||
@@ -695,6 +754,19 @@ export const useSSAStore = create<SSAState>((set, get) => ({
|
||||
isPlanning: false,
|
||||
isExecuting: false,
|
||||
currentPlan: null,
|
||||
currentSAP: null, // 🆕
|
||||
isGeneratingSAP: false, // 🆕
|
||||
|
||||
// 🆕 切换模式
|
||||
setMode: (mode) => set({
|
||||
mode,
|
||||
// 切换模式时重置会话
|
||||
sessionId: null,
|
||||
messages: [],
|
||||
dataLoaded: false,
|
||||
currentPlan: null,
|
||||
currentSAP: null
|
||||
}),
|
||||
|
||||
setSession: (id, title = '新会话') => set({ sessionId: id, sessionTitle: title }),
|
||||
|
||||
@@ -719,8 +791,11 @@ export const useSSAStore = create<SSAState>((set, get) => ({
|
||||
setPlanning: (planning) => set({ isPlanning: planning }),
|
||||
setExecuting: (executing) => set({ isExecuting: executing }),
|
||||
setCurrentPlan: (plan) => set({ currentPlan: plan }),
|
||||
setCurrentSAP: (sap) => set({ currentSAP: sap }), // 🆕
|
||||
setGeneratingSAP: (generating) => set({ isGeneratingSAP: generating }), // 🆕
|
||||
|
||||
reset: () => set({
|
||||
mode: 'analysis',
|
||||
sessionId: null,
|
||||
sessionTitle: '新会话',
|
||||
dataLoaded: false,
|
||||
@@ -730,14 +805,16 @@ export const useSSAStore = create<SSAState>((set, get) => ({
|
||||
messages: [],
|
||||
isPlanning: false,
|
||||
isExecuting: false,
|
||||
currentPlan: null
|
||||
currentPlan: null,
|
||||
currentSAP: null,
|
||||
isGeneratingSAP: false
|
||||
})
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 封装
|
||||
## 5. API 封装(含咨询模式)
|
||||
|
||||
```typescript
|
||||
// api/ssaApi.ts
|
||||
@@ -746,6 +823,8 @@ import { apiClient } from '@/common/api/client';
|
||||
const BASE = '/api/v1/ssa';
|
||||
|
||||
export const ssaApi = {
|
||||
// ==================== 智能分析模式 ====================
|
||||
|
||||
// 会话
|
||||
createSession: () =>
|
||||
apiClient.post<{ id: string }>(`${BASE}/sessions`),
|
||||
@@ -780,6 +859,50 @@ export const ssaApi = {
|
||||
apiClient.get(`${BASE}/sessions/${sessionId}/download-code/${messageId}`, {
|
||||
responseType: 'blob'
|
||||
}),
|
||||
|
||||
// ==================== 🆕 咨询模式 ====================
|
||||
|
||||
// 创建咨询会话(无数据)
|
||||
createConsultSession: () =>
|
||||
apiClient.post<{ id: string }>(`${BASE}/consult`),
|
||||
|
||||
// 咨询对话
|
||||
consultChat: (sessionId: string, message: string) =>
|
||||
apiClient.post<{ response: string }>(`${BASE}/consult/${sessionId}/chat`, { message }),
|
||||
|
||||
// 生成 SAP 文档
|
||||
generateSAP: (sessionId: string) =>
|
||||
apiClient.post<{
|
||||
title: string;
|
||||
sections: Array<{ heading: string; content: string }>;
|
||||
recommendedTools: string[];
|
||||
}>(`${BASE}/consult/${sessionId}/generate-sap`),
|
||||
|
||||
// 下载 SAP(Word/Markdown)
|
||||
downloadSAP: (sessionId: string, format: 'word' | 'markdown' = 'word') =>
|
||||
apiClient.get(`${BASE}/consult/${sessionId}/download-sap`, {
|
||||
params: { format },
|
||||
responseType: 'blob'
|
||||
}),
|
||||
|
||||
// ==================== 🆕 配置中台 ====================
|
||||
|
||||
// 导入配置
|
||||
importConfig: (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return apiClient.post(`${BASE}/config/import`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
},
|
||||
|
||||
// 获取工具列表
|
||||
getConfigTools: () =>
|
||||
apiClient.get(`${BASE}/config/tools`),
|
||||
|
||||
// 热加载配置(Admin)
|
||||
reloadConfig: () =>
|
||||
apiClient.post(`${BASE}/config/reload`),
|
||||
};
|
||||
```
|
||||
|
||||
@@ -864,15 +987,301 @@ export const ssaRoutes = [
|
||||
| 组件 | 功能 | 状态 |
|
||||
|------|------|------|
|
||||
| SSASidebar | 导入数据、新建会话、历史列表、数据状态 | ⬜ |
|
||||
| 🆕 **ModeSwitch** | **模式切换 Tab(智能分析/统计咨询)** | ⬜ |
|
||||
| DataUploader | 拖拽/点击上传,进度显示 | ⬜ |
|
||||
| MessageList | 消息流滚动,自动滚底 | ⬜ |
|
||||
| PlanCard | 参数展示、护栏提示、确认/修改按钮 | ⬜ |
|
||||
| 🆕 PlanCard | **增加"仅下载方案"按钮(咨询模式)** | ⬜ |
|
||||
| ExecutionTrace | 步骤树、状态图标、连接线 | ⬜ |
|
||||
| **ExecutionProgress** | **📌 执行中进度动画,缓解等待焦虑** | ⬜ |
|
||||
| ResultCard | 三线表、图表、解读、下载按钮 | ⬜ |
|
||||
| APATable | APA 格式表格样式 | ⬜ |
|
||||
| Zustand Store | 状态管理 | ⬜ |
|
||||
| API 对接 | 所有接口联调,**超时 120s** | ⬜ |
|
||||
| 🆕 **ConsultChat** | **无数据咨询对话界面** | ⬜ |
|
||||
| 🆕 **SAPPreview** | **SAP 文档预览/下载** | ⬜ |
|
||||
| 🆕 **SAPDownloadButton** | **Word/Markdown 下载选择** | ⬜ |
|
||||
| Zustand Store | 状态管理,**含 mode 切换** | ⬜ |
|
||||
| API 对接 | 所有接口联调,**含咨询 API** | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 9. 🆕 新增组件实现
|
||||
|
||||
### 9.1 ModeSwitch(模式切换 Tab)
|
||||
|
||||
```tsx
|
||||
// components/layout/ModeSwitch.tsx
|
||||
import React from 'react';
|
||||
import { Segmented } from 'antd';
|
||||
import { BarChartOutlined, MessageOutlined } from '@ant-design/icons';
|
||||
import { useSSAStore } from '../../store/ssaStore';
|
||||
|
||||
export const ModeSwitch: React.FC = () => {
|
||||
const { mode, setMode } = useSSAStore();
|
||||
|
||||
return (
|
||||
<Segmented
|
||||
value={mode}
|
||||
onChange={(value) => setMode(value as 'analysis' | 'consult')}
|
||||
options={[
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<BarChartOutlined />
|
||||
<span>智能分析</span>
|
||||
</div>
|
||||
),
|
||||
value: 'analysis',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<MessageOutlined />
|
||||
<span>统计咨询</span>
|
||||
</div>
|
||||
),
|
||||
value: 'consult',
|
||||
},
|
||||
]}
|
||||
className="bg-slate-100"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 9.2 ConsultChat(无数据咨询界面)
|
||||
|
||||
```tsx
|
||||
// components/consult/ConsultChat.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { Input, Button, Alert } from 'antd';
|
||||
import { SendOutlined, FileWordOutlined } from '@ant-design/icons';
|
||||
import { useSSAStore } from '../../store/ssaStore';
|
||||
import { ssaApi } from '../../api/ssaApi';
|
||||
|
||||
export const ConsultChat: React.FC = () => {
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {
|
||||
sessionId,
|
||||
messages,
|
||||
addMessage,
|
||||
setSession,
|
||||
setCurrentSAP,
|
||||
setGeneratingSAP,
|
||||
isGeneratingSAP
|
||||
} = useSSAStore();
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// 如果没有会话,先创建
|
||||
let currentSessionId = sessionId;
|
||||
if (!currentSessionId) {
|
||||
const { data } = await ssaApi.createConsultSession();
|
||||
currentSessionId = data.id;
|
||||
setSession(data.id, '统计咨询');
|
||||
}
|
||||
|
||||
// 添加用户消息
|
||||
addMessage({ role: 'user', contentType: 'text', content: { text: input } });
|
||||
setInput('');
|
||||
|
||||
// 发送咨询
|
||||
const { data } = await ssaApi.consultChat(currentSessionId!, input);
|
||||
|
||||
// 添加 AI 回复
|
||||
addMessage({ role: 'assistant', contentType: 'text', content: { text: data.response } });
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleGenerateSAP = async () => {
|
||||
if (!sessionId) return;
|
||||
|
||||
setGeneratingSAP(true);
|
||||
const { data } = await ssaApi.generateSAP(sessionId);
|
||||
setCurrentSAP(data);
|
||||
addMessage({ role: 'assistant', contentType: 'sap', content: data });
|
||||
setGeneratingSAP(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 引导提示 */}
|
||||
<Alert
|
||||
message="统计咨询模式"
|
||||
description="描述您的研究设计和分析需求,无需上传数据。完成咨询后可生成统计分析计划(SAP)文档。"
|
||||
type="info"
|
||||
showIcon
|
||||
className="mx-4 mt-4"
|
||||
/>
|
||||
|
||||
{/* 消息流 */}
|
||||
<div className="flex-1 overflow-auto p-4 space-y-4">
|
||||
{messages.map(msg => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div className={`
|
||||
max-w-[80%] p-3 rounded-lg
|
||||
${msg.role === 'user'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-slate-100 text-slate-800'}
|
||||
`}>
|
||||
{msg.contentType === 'sap'
|
||||
? <SAPPreview sap={msg.content} />
|
||||
: msg.content.text
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 输入区 */}
|
||||
<div className="p-4 border-t border-slate-200">
|
||||
<div className="flex gap-2">
|
||||
<Input.TextArea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="描述您的研究设计和统计分析需求..."
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSend}
|
||||
loading={loading}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
<Button
|
||||
icon={<FileWordOutlined />}
|
||||
onClick={handleGenerateSAP}
|
||||
loading={isGeneratingSAP}
|
||||
disabled={messages.length < 2}
|
||||
>
|
||||
生成 SAP
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 9.3 SAPPreview(SAP 文档预览)
|
||||
|
||||
```tsx
|
||||
// components/cards/SAPPreview.tsx
|
||||
import React from 'react';
|
||||
import { Card, Button, Space, Typography, Divider, Tag } from 'antd';
|
||||
import { DownloadOutlined, FileWordOutlined, FileMarkdownOutlined } from '@ant-design/icons';
|
||||
import { ssaApi } from '../../api/ssaApi';
|
||||
import { useSSAStore } from '../../store/ssaStore';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface SAPDocument {
|
||||
title: string;
|
||||
sections: Array<{ heading: string; content: string }>;
|
||||
recommendedTools: string[];
|
||||
}
|
||||
|
||||
interface SAPPreviewProps {
|
||||
sap: SAPDocument;
|
||||
}
|
||||
|
||||
export const SAPPreview: React.FC<SAPPreviewProps> = ({ sap }) => {
|
||||
const { sessionId } = useSSAStore();
|
||||
|
||||
const handleDownload = async (format: 'word' | 'markdown') => {
|
||||
if (!sessionId) return;
|
||||
|
||||
const response = await ssaApi.downloadSAP(sessionId, format);
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = format === 'word' ? 'SAP.docx' : 'SAP.md';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="sap-preview"
|
||||
title={
|
||||
<Space>
|
||||
<FileWordOutlined className="text-blue-500" />
|
||||
<span>统计分析计划 (SAP)</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<FileWordOutlined />}
|
||||
onClick={() => handleDownload('word')}
|
||||
>
|
||||
Word
|
||||
</Button>
|
||||
<Button
|
||||
icon={<FileMarkdownOutlined />}
|
||||
onClick={() => handleDownload('markdown')}
|
||||
>
|
||||
Markdown
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Title level={4}>{sap.title}</Title>
|
||||
|
||||
{sap.sections.map((section, idx) => (
|
||||
<div key={idx} className="mb-4">
|
||||
<Title level={5} className="text-slate-700">{section.heading}</Title>
|
||||
<Paragraph className="text-slate-600">{section.content}</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<Text strong>推荐统计方法:</Text>
|
||||
<div className="mt-2">
|
||||
{sap.recommendedTools.map((tool, idx) => (
|
||||
<Tag key={idx} color="blue">{tool}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 9.4 PlanCard 增强(支持仅下载方案)
|
||||
|
||||
```tsx
|
||||
// components/cards/PlanCard.tsx 增加的按钮
|
||||
// 在 "确认并执行" 按钮旁边添加:
|
||||
|
||||
{/* 🆕 仅下载方案(咨询模式下或用户选择不执行) */}
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onDownloadPlanOnly}
|
||||
>
|
||||
仅下载方案
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user