feat(dc/tool-c): 完成前端基础框架(Day 4 MVP)

核心功能:
- 新增Tool C主入口(index.tsx, 258行):状态管理+布局
- 新增Header组件(91行):顶栏+返回按钮+导出
- 新增Toolbar组件(104行):7个快捷按钮+搜索框
- 新增DataGrid组件(111行):AG Grid Community集成
- 新增Sidebar组件(149行):右侧栏骨架版
- 新增API封装(toolC.ts, 218行):8个API方法
- 新增类型定义(types/index.ts, 62行)

AG Grid集成:
- 安装ag-grid-community + ag-grid-react
- Excel风格表格渲染
- 列排序、过滤、调整宽度
- 缺失值高亮显示(红色斜体)
- 数值右对齐
- 自定义Emerald绿色主题(ag-grid-custom.css, 113行)
- 虚拟滚动支持大数据

路由配置:
- 更新dc/index.tsx:新增ToolCModule懒加载
- 更新Portal.tsx:Tool C状态改为ready
- 路径:/data-cleaning/tool-c

API封装(8个方法):
- uploadFile(上传CSV/Excel)
- getSession(获取Session元数据)
- getPreviewData(获取预览数据)
- updateHeartbeat(延长10分钟)
- generateCode(生成代码,不执行)
- executeCode(执行代码)
- processMessage(生成+执行,一步到位)核心API
- getChatHistory(对话历史)

文档更新:
- 新增Day 4前端基础完成总结(213行)
- 更新工具C当前状态文档
- 更新TODO清单(Day 1-4标记完成)
- 更新系统总体设计文档

测试数据准备:
- cqol-demo.csv(21列x313行真实医疗数据)
- G鼓膜穿孔数据.xlsx(备用)

Day 5待完成:
- MessageItem组件(消息渲染)
- CodeBlock组件(Prism.js代码高亮)
- InputArea组件(输入框交互)
- InsightsPanel组件(数据洞察)
- 完善Sidebar(完整Chat交互)
- 端到端测试

影响范围:
- frontend-v2/src/modules/dc/pages/tool-c/*(新增11个文件)
- frontend-v2/src/modules/dc/api/toolC.ts(新增)
- frontend-v2/src/modules/dc/index.tsx(更新路由)
- frontend-v2/src/modules/dc/pages/Portal.tsx(启用Tool C)
- docs/03-业务模块/DC-数据清洗整理/*(文档更新)
- package.json(新增依赖)

Breaking Changes: 无

总代码行数:+1106行(前端基础框架)

Refs: #Tool-C-Day4
This commit is contained in:
2025-12-07 17:40:07 +08:00
parent f01981bf78
commit 2c7ed94161
20 changed files with 3173 additions and 105 deletions

View File

@@ -0,0 +1,257 @@
/**
* Tool C - 科研数据编辑器
*
* AI驱动的Excel风格数据清洗工具
* 核心功能AG Grid表格 + AI Copilot
*/
import { useState, useEffect } from 'react';
import Header from './components/Header';
import Toolbar from './components/Toolbar';
import DataGrid from './components/DataGrid';
import Sidebar from './components/Sidebar';
import * as api from '../../api/toolC';
// ==================== 类型定义 ====================
interface ToolCState {
// Session信息
sessionId: string | null;
fileName: string;
// 表格数据
data: Record<string, any>[];
columns: Array<{ id: string; name: string; type?: string }>;
// AI对话
messages: Message[];
// UI状态
isLoading: boolean;
isSidebarOpen: boolean;
}
interface Message {
id: number;
role: 'user' | 'assistant' | 'system';
content: string;
code?: {
content: string;
status: 'pending' | 'running' | 'success' | 'error';
};
onExecute?: () => void;
}
// ==================== 主组件 ====================
const ToolC = () => {
const [state, setState] = useState<ToolCState>({
sessionId: null,
fileName: '',
data: [],
columns: [],
messages: [],
isLoading: false,
isSidebarOpen: true,
});
// 更新状态辅助函数
const updateState = (updates: Partial<ToolCState>) => {
setState((prev) => ({ ...prev, ...updates }));
};
// ==================== 文件上传 ====================
const handleFileUpload = async (file: File) => {
try {
updateState({ isLoading: true });
// 调用上传API
const result = await api.uploadFile(file);
if (result.success) {
updateState({
sessionId: result.data.sessionId,
fileName: file.name,
});
// 获取预览数据
const preview = await api.getPreviewData(result.data.sessionId);
if (preview.success) {
updateState({
data: preview.data.rows,
columns: preview.data.columns.map((col) => ({
id: col,
name: col,
type: 'text',
})),
messages: [
{
id: Date.now(),
role: 'system',
content: `✅ 文件上传成功!共 ${preview.data.totalRows}× ${preview.data.totalCols} 列数据。`,
},
],
});
}
}
} catch (error: any) {
console.error('上传失败:', error);
updateState({
messages: [
{
id: Date.now(),
role: 'system',
content: `❌ 上传失败:${error.response?.data?.error || error.message}`,
},
],
});
} finally {
updateState({ isLoading: false });
}
};
// ==================== AI消息发送 ====================
const handleSendMessage = async (message: string) => {
if (!state.sessionId) {
alert('请先上传文件');
return;
}
// 添加用户消息
const userMessage: Message = {
id: Date.now(),
role: 'user',
content: message,
};
updateState({
messages: [...state.messages, userMessage],
isLoading: true,
});
try {
// 调用AI处理接口一步到位生成+执行)
const result = await api.processMessage(state.sessionId, message);
if (result.success) {
const { code, explanation, executeResult, retryCount } = result.data;
// 添加AI消息
const aiMessage: Message = {
id: Date.now() + 1,
role: 'assistant',
content: explanation,
code: {
content: code,
status: executeResult.success ? 'success' : 'error',
},
};
updateState({
messages: [...state.messages, userMessage, aiMessage],
});
// 如果执行成功,更新表格数据
if (executeResult.success && executeResult.newDataPreview) {
updateState({
data: executeResult.newDataPreview,
messages: [
...state.messages,
userMessage,
aiMessage,
{
id: Date.now() + 2,
role: 'system',
content: `✅ 代码执行成功!表格已更新。${retryCount > 0 ? `(重试 ${retryCount} 次后成功)` : ''}`,
},
],
});
} else if (!executeResult.success) {
updateState({
messages: [
...state.messages,
userMessage,
aiMessage,
{
id: Date.now() + 2,
role: 'system',
content: `❌ 执行失败:${executeResult.error}`,
},
],
});
}
}
} catch (error: any) {
console.error('AI处理失败:', error);
updateState({
messages: [
...state.messages,
userMessage,
{
id: Date.now() + 1,
role: 'system',
content: `❌ 处理失败:${error.response?.data?.error || error.message}`,
},
],
});
} finally {
updateState({ isLoading: false });
}
};
// ==================== 心跳机制 ====================
useEffect(() => {
if (!state.sessionId) return;
// 每5分钟更新一次心跳
const timer = setInterval(async () => {
try {
await api.updateHeartbeat(state.sessionId!);
console.log('心跳更新成功');
} catch (error) {
console.error('心跳更新失败:', error);
}
}, 5 * 60 * 1000); // 5分钟
return () => clearInterval(timer);
}, [state.sessionId]);
// ==================== 渲染 ====================
return (
<div className="h-screen w-screen flex flex-col bg-slate-50 overflow-hidden">
{/* 顶部栏 */}
<Header
fileName={state.fileName || '未上传文件'}
onExport={() => alert('导出功能开发中...')}
/>
{/* 主工作区 */}
<div className="flex-1 flex overflow-hidden">
{/* 左侧:表格区域 */}
<div className="flex-1 flex flex-col min-w-0">
<Toolbar />
<div className="flex-1 overflow-auto p-6">
<DataGrid data={state.data} columns={state.columns} />
</div>
</div>
{/* 右侧AI Copilot */}
{state.isSidebarOpen && (
<Sidebar
isOpen={state.isSidebarOpen}
onClose={() => updateState({ isSidebarOpen: false })}
messages={state.messages}
onSendMessage={handleSendMessage}
onFileUpload={handleFileUpload}
isLoading={state.isLoading}
hasSession={!!state.sessionId}
/>
)}
</div>
</div>
);
};
export default ToolC;