# 工具C Day 4-5 前端开发计划(最终版) > **制定日期**: 2025-12-07 > **开发目标**: Tool C前端MVP - AI驱动的科研数据编辑器 > **预计工时**: 12-16小时(Day 4: 6-8h, Day 5: 6-8h) > **核心目标**: 端到端AI数据清洗流程可用 --- ## 🎯 核心决策 | 决策项 | 最终方案 | 理由 | |--------|---------|------| | **表格组件** | **AG Grid Community** | 用户强烈要求,Excel级体验 | | **开发优先级** | **AI核心功能优先** | 先验证后端,UI细节后续优化 | | **UI风格** | **严格还原原型V6** | Emerald绿色主题,符合Tool C定位 | | **测试数据** | **真实医疗数据** | cqol-demo.csv (21列x300+行) | | **Day 4-5目标** | **AI核心功能可用** | 上传→AI对话→执行→更新表格 | | **代码风格** | **同Tool B标准** | TypeScript + Tailwind + Lucide | --- ## 📦 技术栈 ### 核心依赖 ```json { "ag-grid-community": "^31.0.0", // 表格核心 "ag-grid-react": "^31.0.0", // React集成 "react": "^18.2.0", "react-router-dom": "^6.x", "lucide-react": "^0.x", // 图标库 "axios": "^1.x" // HTTP客户端 } ``` ### 技术选型 - **React 18**: Hooks + TypeScript - **Tailwind CSS**: 原子化CSS - **AG Grid Community**: 开源版,功能足够MVP - **Prism.js**: 代码语法高亮 - **React Markdown**: Markdown渲染(AI回复) --- ## 📐 页面布局设计 ``` ┌────────────────────────────────────────────────────────────┐ │ Header (h-14) │ │ [🟢 科研数据编辑器] lung_cancer.csv [撤销/重做] [导出] │ └────────────────────────────────────────────────────────────┘ ┌──────────────────────────┬─────────────────────────────────┐ │ Left Panel (flex-1) │ Right Sidebar (w-[420px]) │ │ │ │ │ ┌──────────────────────┐ │ ┌───────────────────────────┐ │ │ │ Toolbar (h-16) │ │ │ Tab: [Chat] [Insights] │ │ │ │ 7个快捷按钮 + 搜索 │ │ └───────────────────────────┘ │ │ └──────────────────────┘ │ │ │ │ ┌───────────────────────────┐ │ │ ┌──────────────────────┐ │ │ Chat Messages Area │ │ │ │ │ │ │ - 用户消息 │ │ │ │ AG Grid Table │ │ │ - AI消息(含代码块) │ │ │ │ (Excel风格) │ │ │ - 系统消息 │ │ │ │ │ │ │ │ │ │ │ 21列 x 300+行 │ │ └───────────────────────────┘ │ │ │ │ │ │ │ │ 支持: │ │ ┌───────────────────────────┐ │ │ │ - 列排序 │ │ │ Input + Send Button │ │ │ │ - 列宽调整 │ │ └───────────────────────────┘ │ │ │ - 单元格编辑(后期) │ │ │ │ │ - 选择高亮 │ │ │ │ └──────────────────────┘ │ │ └──────────────────────────┴─────────────────────────────────┘ ``` --- ## 🗂️ 文件结构规划 ``` frontend-v2/src/modules/dc/pages/tool-c/ ├── index.tsx # 主入口(状态管理+布局) ├── components/ │ ├── Header.tsx # 顶部栏 │ ├── Toolbar.tsx # 工具栏(7个按钮) │ ├── DataGrid.tsx # AG Grid表格(核心) │ ├── Sidebar.tsx # 右侧栏容器 │ ├── ChatPanel.tsx # Chat面板 │ ├── InsightsPanel.tsx # Insights面板 │ ├── MessageList.tsx # 消息列表 │ ├── MessageItem.tsx # 单条消息 │ ├── CodeBlock.tsx # 代码块渲染 │ └── InputArea.tsx # 输入框 ├── hooks/ │ ├── useToolC.ts # 核心状态管理Hook │ ├── useSession.ts # Session管理 │ ├── useChat.ts # AI对话逻辑 │ └── useDataGrid.ts # 表格数据管理 └── types/ └── index.ts # TypeScript类型定义 frontend-v2/src/modules/dc/api/ └── toolC.ts # API封装(6个方法) ``` --- ## ⏱️ Day 4 开发计划(6-8小时) ### 阶段1:项目初始化(1小时) #### 1.1 安装依赖 ```bash cd frontend-v2 npm install ag-grid-community ag-grid-react npm install prismjs @types/prismjs npm install react-markdown ``` #### 1.2 创建文件结构 ```bash mkdir -p src/modules/dc/pages/tool-c/{components,hooks,types} touch src/modules/dc/pages/tool-c/index.tsx # ... 创建其他文件 ``` #### 1.3 更新路由配置 ```typescript // src/App.tsx 或路由配置文件 } /> ``` #### 1.4 更新Portal页面 ```typescript // src/modules/dc/pages/Portal.tsx // 将Tool C的status从'disabled'改为'ready' { id: 'tool-c', title: '科研数据编辑器', status: 'ready', // ⭐ 修改这里 route: '/data-cleaning/tool-c' } ``` **交付物**: - ✅ 依赖安装完成 - ✅ 文件结构创建 - ✅ Portal可点击进入Tool C --- ### 阶段2:页面框架搭建(2小时) #### 2.1 创建主入口 (index.tsx) ```typescript import { useState } from 'react'; import Header from './components/Header'; import Toolbar from './components/Toolbar'; import DataGrid from './components/DataGrid'; import Sidebar from './components/Sidebar'; interface ToolCState { sessionId: string | null; fileName: string; data: any[]; columns: any[]; messages: any[]; isLoading: boolean; } const ToolC = () => { const [state, setState] = useState({ sessionId: null, fileName: 'cqol-demo.csv', data: [], columns: [], messages: [], isLoading: false, }); return (
); }; export default ToolC; ``` #### 2.2 创建Header组件 ```typescript // components/Header.tsx import { Table2, Undo2, Redo2, Download, ArrowLeft } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; interface HeaderProps { fileName: string; onUndo?: () => void; onRedo?: () => void; onExport?: () => void; } const Header: React.FC = ({ fileName, onUndo, onRedo, onExport }) => { const navigate = useNavigate(); return (
科研数据编辑器 Pro
{fileName}
); }; export default Header; ``` #### 2.3 创建Toolbar组件 ```typescript // components/Toolbar.tsx import { Calculator, CalendarClock, ArrowLeftRight, FileSearch, Wand2, Filter, Search } from 'lucide-react'; const ToolbarButton = ({ icon: Icon, label, colorClass }: any) => ( ); const Toolbar = () => { return (
); }; export default Toolbar; ``` **交付物**: - ✅ Header完成(带返回按钮) - ✅ Toolbar完成(7个按钮) - ✅ 基础布局可见 --- ### 阶段3:AG Grid集成(3-4小时) #### 3.1 创建DataGrid组件 ```typescript // components/DataGrid.tsx import { AgGridReact } from 'ag-grid-react'; import { ColDef } from 'ag-grid-community'; import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import { useMemo } from 'react'; interface DataGridProps { data: any[]; columns: Array<{ id: string; name: string; type?: string }>; onCellValueChanged?: (params: any) => void; } const DataGrid: React.FC = ({ data, columns, onCellValueChanged }) => { // 将列定义转换为AG Grid格式 const columnDefs: ColDef[] = useMemo(() => { return columns.map(col => ({ field: col.id, headerName: col.name, sortable: true, filter: true, resizable: true, editable: false, // MVP阶段暂不支持编辑 width: 120, minWidth: 80, // 根据类型设置样式 cellClass: (params) => { if (params.value === null || params.value === undefined || params.value === '') { return 'bg-red-50 text-red-400 italic'; } return ''; }, })); }, [columns]); // 默认列配置 const defaultColDef: ColDef = { flex: 0, sortable: true, filter: true, resizable: true, }; // 如果没有数据,显示空状态 if (data.length === 0) { return (

暂无数据

请在右侧AI助手中上传文件

); } return (
); }; export default DataGrid; ``` #### 3.2 自定义AG Grid样式 ```css /* 在全局CSS或组件内添加 */ .ag-theme-alpine { --ag-header-background-color: #f8fafc; --ag-header-foreground-color: #475569; --ag-border-color: #e2e8f0; --ag-row-hover-color: #f0fdf4; --ag-selected-row-background-color: #d1fae5; --ag-font-family: inherit; --ag-font-size: 13px; } .ag-theme-alpine .ag-header-cell { font-weight: 600; } .ag-theme-alpine .ag-cell { display: flex; align-items: center; } ``` #### 3.3 测试数据加载 ```typescript // 临时测试:在index.tsx中硬编码测试数据 const mockData = [ { id: 'P001', age: 27, sex: '女', bmi: 23.1, smoke: '否' }, { id: 'P002', age: 24, sex: '男', bmi: 16.7, smoke: '是' }, { id: 'P003', age: null, sex: '男', bmi: null, smoke: '是' }, ]; const mockColumns = [ { id: 'id', name: '病人ID', type: 'text' }, { id: 'age', name: '年龄', type: 'number' }, { id: 'sex', name: '性别', type: 'category' }, { id: 'bmi', name: 'BMI', type: 'number' }, { id: 'smoke', name: '吸烟史', type: 'category' }, ]; ``` **交付物**: - ✅ AG Grid成功渲染 - ✅ 缺失值高亮显示 - ✅ 列排序/过滤可用 - ✅ 列宽可调整 --- ## ⏱️ Day 5 开发计划(6-8小时) ### 阶段4:AI Chat面板(3小时) #### 4.1 创建Sidebar组件 ```typescript // components/Sidebar.tsx import { useState } from 'react'; import { MessageSquare, Lightbulb, X } from 'lucide-react'; import ChatPanel from './ChatPanel'; import InsightsPanel from './InsightsPanel'; interface SidebarProps { isOpen: boolean; onClose: () => void; messages: any[]; onSendMessage: (message: string) => void; dataStats?: any; } const Sidebar: React.FC = ({ isOpen, onClose, messages, onSendMessage, dataStats }) => { const [activeTab, setActiveTab] = useState<'chat' | 'insights'>('chat'); if (!isOpen) return null; return (
{/* Tab Header */}
{/* Tab Content */}
{activeTab === 'chat' ? ( ) : ( )}
); }; export default Sidebar; ``` #### 4.2 创建ChatPanel组件 ```typescript // components/ChatPanel.tsx import { useRef, useEffect } from 'react'; import MessageItem from './MessageItem'; import InputArea from './InputArea'; interface ChatPanelProps { messages: any[]; onSendMessage: (message: string) => void; isLoading?: boolean; } const ChatPanel: React.FC = ({ messages, onSendMessage, isLoading }) => { const messagesEndRef = useRef(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); return (
{/* Messages Area */}
{messages.length === 0 && (

👋 您好!我是您的AI数据分析师

试试说:"把年龄大于60的标记为老年组"

)} {messages.map((msg) => ( ))} {isLoading && (
AI正在思考...
)}
{/* Input Area */}
); }; export default ChatPanel; ``` #### 4.3 创建MessageItem组件 ```typescript // components/MessageItem.tsx import { User, Bot, CheckCircle2, XCircle, Play } from 'lucide-react'; import CodeBlock from './CodeBlock'; interface MessageItemProps { message: { id: string; role: 'user' | 'assistant' | 'system'; content: string; code?: { content: string; status: 'pending' | 'running' | 'success' | 'error'; }; onExecute?: () => void; }; } const MessageItem: React.FC = ({ message }) => { const { role, content, code } = message; // 系统消息 if (role === 'system') { return (
{content}
); } // 用户消息 if (role === 'user') { return (
{content}
); } // AI消息 return (
{content}
{code && (
)}
); }; export default MessageItem; ``` #### 4.4 创建CodeBlock组件 ```typescript // components/CodeBlock.tsx import { Copy, Check } from 'lucide-react'; import { useState } from 'react'; import Prism from 'prismjs'; import 'prismjs/themes/prism-tomorrow.css'; import 'prismjs/components/prism-python'; interface CodeBlockProps { code: string; language?: string; } const CodeBlock: React.FC = ({ code, language = 'python' }) => { const [copied, setCopied] = useState(false); const handleCopy = () => { navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const highlightedCode = Prism.highlight( code, Prism.languages[language] || Prism.languages.python, language ); return (
{language}
        
      
); }; export default CodeBlock; ``` #### 4.5 创建InputArea组件 ```typescript // components/InputArea.tsx import { Send } from 'lucide-react'; import { useState, KeyboardEvent } from 'react'; interface InputAreaProps { onSend: (message: string) => void; disabled?: boolean; } const InputArea: React.FC = ({ onSend, disabled }) => { const [input, setInput] = useState(''); const handleSend = () => { if (!input.trim() || disabled) return; onSend(input); setInput(''); }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return (