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:
257
frontend-v2/src/modules/dc/pages/tool-c/index.tsx
Normal file
257
frontend-v2/src/modules/dc/pages/tool-c/index.tsx
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user