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,148 @@
/**
* Tool C Sidebar组件
*
* 右侧栏AI CopilotChat + Insights
* Day 4: 骨架版本
* Day 5: 完整实现
*/
import { MessageSquare, X, Upload } from 'lucide-react';
interface SidebarProps {
isOpen: boolean;
onClose: () => void;
messages: any[];
onSendMessage: (message: string) => void;
onFileUpload: (file: File) => void;
isLoading?: boolean;
hasSession: boolean;
}
const Sidebar: React.FC<SidebarProps> = ({
isOpen,
onClose,
messages,
onSendMessage,
onFileUpload,
isLoading,
hasSession,
}) => {
if (!isOpen) return null;
return (
<div className="w-[420px] bg-white border-l border-slate-200 flex flex-col">
{/* Header */}
<div className="h-14 border-b border-slate-200 flex items-center justify-between px-4">
<div className="flex items-center gap-2 text-emerald-700 font-medium">
<MessageSquare size={18} />
<span>AI助手</span>
</div>
<button
onClick={onClose}
className="p-1 rounded-md hover:bg-slate-100 text-slate-400 hover:text-slate-600"
>
<X size={18} />
</button>
</div>
{/* Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* 如果没有Session显示上传区域 */}
{!hasSession ? (
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center space-y-4">
<div className="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto">
<Upload className="w-8 h-8 text-emerald-600" />
</div>
<div>
<h3 className="font-semibold text-slate-900 mb-2"></h3>
<p className="text-sm text-slate-500 mb-4">
CSVExcel <br /> 10MB
</p>
</div>
<input
type="file"
accept=".csv,.xlsx,.xls"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) onFileUpload(file);
}}
className="hidden"
id="file-upload-sidebar"
/>
<label
htmlFor="file-upload-sidebar"
className="inline-flex items-center gap-2 px-6 py-3 bg-emerald-600 text-white rounded-lg cursor-pointer hover:bg-emerald-700 transition-all text-sm font-medium"
>
<Upload size={16} />
</label>
</div>
</div>
) : (
// 如果有Session显示简化的消息列表
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.length === 0 && (
<div className="text-center text-slate-400 text-sm py-12">
<p className="mb-2">👋 AI数据分析师</p>
<p>"把age列的缺失值填补为中位数"</p>
</div>
)}
{messages.map((msg) => (
<div
key={msg.id}
className={`p-3 rounded-lg text-sm ${
msg.role === 'user'
? 'bg-emerald-600 text-white ml-auto max-w-[80%]'
: msg.role === 'system'
? 'bg-slate-100 text-slate-600 text-center text-xs'
: 'bg-slate-50 text-slate-800'
}`}
>
{msg.content}
</div>
))}
{isLoading && (
<div className="flex items-center gap-2 text-slate-400 text-sm">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-emerald-500 border-t-transparent"></div>
<span>AI正在思考...</span>
</div>
)}
</div>
)}
{/* 输入区域 */}
{hasSession && (
<div className="border-t border-slate-200 p-4">
<div className="flex gap-2">
<input
type="text"
placeholder="告诉AI你想做什么..."
className="flex-1 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
onSendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
disabled={isLoading}
/>
<button
className="bg-emerald-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-emerald-700 disabled:bg-slate-200 disabled:text-slate-400 disabled:cursor-not-allowed transition-colors"
disabled={isLoading}
>
</button>
</div>
<div className="text-xs text-slate-400 mt-2">
Enter
</div>
</div>
)}
</div>
</div>
);
};
export default Sidebar;