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

@@ -23,6 +23,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.555.0",
"mathjs": "^15.1.0",
"prismjs": "^1.30.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.5",
@@ -6169,6 +6170,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz",

View File

@@ -25,6 +25,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.555.0",
"mathjs": "^15.1.0",
"prismjs": "^1.30.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.5",

View File

@@ -0,0 +1,217 @@
/**
* Tool C API封装
*
* 提供6个核心API方法
* - Session管理上传、获取、预览、心跳
* - AI功能生成代码、执行代码、一步到位处理、获取历史
*/
import axios from 'axios';
const BASE_URL = '/api/v1/dc/tool-c';
// ==================== 类型定义 ====================
export interface UploadResponse {
success: boolean;
message: string;
data: {
sessionId: string;
fileName: string;
fileSize: number;
totalRows: number;
totalCols: number;
columns: string[];
};
}
export interface SessionData {
success: boolean;
data: {
id: string;
fileName: string;
totalRows: number;
totalCols: number;
columns: string[];
createdAt: string;
expiresAt: string;
};
}
export interface PreviewData {
success: boolean;
data: {
sessionId: string;
fileName: string;
totalRows: number;
totalCols: number;
columns: string[];
rows: Record<string, any>[];
};
}
export interface AIProcessResponse {
success: boolean;
message: string;
data: {
code: string;
explanation: string;
executeResult: {
success: boolean;
result?: any;
newDataPreview?: Record<string, any>[];
error?: string;
};
retryCount: number;
messageId: string;
};
}
export interface ChatHistoryResponse {
success: boolean;
data: {
sessionId: string;
history: Array<{
role: 'user' | 'assistant' | 'system';
content: string;
}>;
count: number;
};
}
// ==================== Session管理 ====================
/**
* 上传CSV/Excel文件
*/
export const uploadFile = async (file: File): Promise<UploadResponse> => {
const formData = new FormData();
formData.append('file', file);
const response = await axios.post(`${BASE_URL}/sessions/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 30000, // 30秒超时
});
return response.data;
};
/**
* 获取Session元数据
*/
export const getSession = async (sessionId: string): Promise<SessionData> => {
const response = await axios.get(`${BASE_URL}/sessions/${sessionId}`);
return response.data;
};
/**
* 获取预览数据前100行
*/
export const getPreviewData = async (sessionId: string): Promise<PreviewData> => {
const response = await axios.get(`${BASE_URL}/sessions/${sessionId}/preview`);
return response.data;
};
/**
* 获取完整数据
*/
export const getFullData = async (sessionId: string): Promise<PreviewData> => {
const response = await axios.get(`${BASE_URL}/sessions/${sessionId}/full`);
return response.data;
};
/**
* 更新心跳延长10分钟
*/
export const updateHeartbeat = async (sessionId: string): Promise<{ success: boolean }> => {
const response = await axios.post(`${BASE_URL}/sessions/${sessionId}/heartbeat`);
return response.data;
};
/**
* 删除Session
*/
export const deleteSession = async (sessionId: string): Promise<{ success: boolean }> => {
const response = await axios.delete(`${BASE_URL}/sessions/${sessionId}`);
return response.data;
};
// ==================== AI功能 ====================
/**
* 生成代码(不执行)
*/
export const generateCode = async (
sessionId: string,
message: string
): Promise<{
success: boolean;
data: {
code: string;
explanation: string;
messageId: string;
};
}> => {
const response = await axios.post(`${BASE_URL}/ai/generate`, {
sessionId,
message,
});
return response.data;
};
/**
* 执行代码
*/
export const executeCode = async (
sessionId: string,
code: string,
messageId: string
): Promise<{
success: boolean;
data: {
result: any;
newDataPreview: Record<string, any>[];
} | null;
error?: string;
}> => {
const response = await axios.post(`${BASE_URL}/ai/execute`, {
sessionId,
code,
messageId,
});
return response.data;
};
/**
* 一步到位:生成并执行(带重试)⭐ 核心API
*/
export const processMessage = async (
sessionId: string,
message: string,
maxRetries: number = 3
): Promise<AIProcessResponse> => {
const response = await axios.post(
`${BASE_URL}/ai/process`,
{
sessionId,
message,
maxRetries,
},
{
timeout: 60000, // 60秒超时包含重试时间
}
);
return response.data;
};
/**
* 获取对话历史
*/
export const getChatHistory = async (
sessionId: string,
limit: number = 10
): Promise<ChatHistoryResponse> => {
const response = await axios.get(`${BASE_URL}/ai/history/${sessionId}?limit=${limit}`);
return response.data;
};

View File

@@ -5,8 +5,8 @@
* 路由结构:
* - / → Portal工作台主页
* - /tool-a → Tool A - 超级合并器(暂未开发)
* - /tool-b → Tool B - 病历结构化机器人(开发中
* - /tool-c → Tool C - 科研数据编辑器(暂未开发)
* - /tool-b → Tool B - 病历结构化机器人(✅ 已完成
* - /tool-c → Tool C - 科研数据编辑器(🚀 Day 4-5开发
*/
import { Suspense, lazy } from 'react';
@@ -17,6 +17,7 @@ import Placeholder from '@/shared/components/Placeholder';
// 懒加载组件
const Portal = lazy(() => import('./pages/Portal'));
const ToolBModule = lazy(() => import('./pages/tool-b/index'));
const ToolCModule = lazy(() => import('./pages/tool-c/index'));
const DCModule = () => {
return (
@@ -46,17 +47,8 @@ const DCModule = () => {
{/* Tool B - 病历结构化机器人(开发中) */}
<Route path="tool-b/*" element={<ToolBModule />} />
{/* Tool C - 科研数据编辑器(暂未开发) */}
<Route
path="tool-c/*"
element={
<Placeholder
title="Tool C - 科研数据编辑器"
description="该工具正在开发中,敬请期待"
moduleName="Excel风格的在线数据清洗工具"
/>
}
/>
{/* Tool C - 科研数据编辑器(Day 4-5开发 */}
<Route path="tool-c/*" element={<ToolCModule />} />
</Routes>
</Suspense>
);

View File

@@ -39,7 +39,7 @@ const Portal = () => {
description: 'Excel 风格的在线清洗工具。支持缺失值填补、值替换与分析集导出。',
icon: 'Table2',
color: 'emerald',
status: 'disabled',
status: 'ready', // ⭐ Day 4-5开发
route: '/data-cleaning/tool-c'
}
];

View File

@@ -0,0 +1,111 @@
/**
* Tool C DataGrid组件
*
* 基于AG Grid Community的Excel风格表格
* 核心功能:列排序、列过滤、列宽调整、缺失值高亮
*/
import { useMemo } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { ColDef } from 'ag-grid-community';
// 引入AG Grid样式
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import './ag-grid-custom.css';
interface DataGridProps {
data: Record<string, any>[];
columns: Array<{ id: string; name: string; type?: string }>;
onCellValueChanged?: (params: any) => void;
}
const DataGrid: React.FC<DataGridProps> = ({ 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 '';
},
// 数值类型右对齐
cellStyle: (params) => {
if (typeof params.value === 'number') {
return { textAlign: 'right' as const };
}
return undefined;
},
}));
}, [columns]);
// 默认列配置
const defaultColDef: ColDef = useMemo(
() => ({
flex: 0,
sortable: true,
filter: true,
resizable: true,
}),
[]
);
// 空状态
if (data.length === 0) {
return (
<div className="bg-white border border-slate-200 shadow-sm rounded-xl p-12 text-center">
<div className="text-slate-400 text-sm space-y-2">
<p className="text-lg">📊 </p>
<p>AI助手中上传CSV或Excel文件</p>
<div className="mt-4 text-xs text-slate-300">
.csv, .xlsx, .xls10MB
</div>
</div>
</div>
);
}
return (
<div className="bg-white border border-slate-200 shadow-sm rounded-xl overflow-hidden h-full">
<div className="ag-theme-alpine h-full" style={{ width: '100%', height: '100%' }}>
<AgGridReact
rowData={data}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
animateRows={true}
rowSelection="multiple"
onCellValueChanged={onCellValueChanged}
domLayout="normal"
suppressCellFocus={false}
enableCellTextSelection={true}
// 性能优化
rowBuffer={10}
debounceVerticalScrollbar={true}
// 主题样式
getRowStyle={(params) => {
if (params.rowIndex % 2 === 0) {
return { background: '#fafafa' };
}
}}
/>
</div>
</div>
);
};
export default DataGrid;

View File

@@ -0,0 +1,90 @@
/**
* Tool C Header组件
*
* 顶部栏:工具名称、文件名、操作按钮
*/
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<HeaderProps> = ({ fileName, onUndo, onRedo, onExport }) => {
const navigate = useNavigate();
return (
<header className="bg-white border-b border-slate-200 h-14 flex-none flex items-center justify-between px-4 z-20 shadow-sm">
{/* 左侧:导航 + 工具名称 */}
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/data-cleaning')}
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-slate-100 text-slate-600 hover:text-slate-900 transition-all"
title="返回数据清洗工作台"
>
<ArrowLeft className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</button>
<div className="h-4 w-px bg-slate-300"></div>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-emerald-600 rounded-lg flex items-center justify-center text-white shadow-md">
<Table2 size={20} />
</div>
<div>
<h1 className="text-lg font-bold text-slate-900">
<span className="text-emerald-600 text-xs px-1.5 py-0.5 bg-emerald-50 rounded-full ml-1">
Pro
</span>
</h1>
</div>
</div>
<div className="h-4 w-[1px] bg-slate-300 mx-2"></div>
<span className="text-xs text-slate-500 font-mono">{fileName}</span>
</div>
{/* 右侧:操作按钮 */}
<div className="flex items-center gap-3">
{/* 撤销/重做 */}
<div className="flex items-center bg-slate-100 rounded-lg p-1">
<button
onClick={onUndo}
className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all disabled:cursor-not-allowed"
disabled={true}
title="撤销(开发中)"
>
<Undo2 size={16} />
</button>
<button
onClick={onRedo}
className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all disabled:cursor-not-allowed"
disabled={true}
title="重做(开发中)"
>
<Redo2 size={16} />
</button>
</div>
{/* 导出按钮 */}
<button
onClick={onExport}
className="flex items-center gap-2 px-4 py-1.5 bg-slate-900 text-white rounded-lg text-xs font-medium hover:bg-slate-800 transition-all shadow-md"
title="导出Excel"
>
<Download size={12} />
</button>
</div>
</header>
);
};
export default Header;

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;

View File

@@ -0,0 +1,103 @@
/**
* Tool C Toolbar组件
*
* 扁平化工具栏7个快捷操作按钮 + 搜索框
*/
import {
Calculator,
CalendarClock,
ArrowLeftRight,
FileSearch,
Wand2,
Filter,
Search,
} from 'lucide-react';
interface ToolbarButtonProps {
icon: React.ElementType;
label: string;
colorClass: string;
onClick?: () => void;
disabled?: boolean;
}
const ToolbarButton: React.FC<ToolbarButtonProps> = ({
icon: Icon,
label,
colorClass,
onClick,
disabled = true, // MVP阶段暂不可用
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`flex flex-col items-center justify-center w-20 h-14 rounded-lg transition-all hover:shadow-sm disabled:opacity-50 disabled:cursor-not-allowed ${colorClass}`}
title={disabled ? '开发中...' : label}
>
<Icon className="w-5 h-5 mb-1" />
<span className="text-[10px] font-medium">{label}</span>
</button>
);
};
const Toolbar = () => {
return (
<div className="bg-white border-b border-slate-200 px-4 py-2 flex items-center gap-1 overflow-x-auto flex-none shadow-sm z-10">
{/* 7个快捷按钮 */}
<ToolbarButton
icon={Calculator}
label="生成新变量"
colorClass="text-emerald-600 bg-emerald-50 hover:bg-emerald-100"
/>
<ToolbarButton
icon={CalendarClock}
label="时间差"
colorClass="text-blue-600 bg-blue-50 hover:bg-blue-100"
/>
<ToolbarButton
icon={ArrowLeftRight}
label="横纵转换"
colorClass="text-cyan-600 bg-cyan-50 hover:bg-cyan-100"
/>
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
<ToolbarButton
icon={FileSearch}
label="查重"
colorClass="text-orange-600 bg-orange-50 hover:bg-orange-100"
/>
<ToolbarButton
icon={Wand2}
label="多重插补"
colorClass="text-rose-600 bg-rose-50 hover:bg-rose-100"
/>
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
<ToolbarButton
icon={Filter}
label="筛选分析集"
colorClass="text-indigo-600 bg-indigo-50 hover:bg-indigo-100"
/>
<div className="flex-1"></div>
{/* 搜索框 */}
<div className="relative">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
className="pl-9 pr-4 py-1.5 text-sm bg-slate-100 border-none rounded-full w-48 focus:w-64 transition-all outline-none focus:ring-2 focus:ring-emerald-500/20"
placeholder="搜索值..."
disabled
title="开发中..."
/>
</div>
</div>
);
};
export default Toolbar;

View File

@@ -0,0 +1,112 @@
/**
* AG Grid 自定义样式
*
* 目标让AG Grid看起来更像原型图V6
* - Emerald绿色主题
* - 更圆润的边角
* - 柔和的颜色
*/
.ag-theme-alpine {
/* 背景色 */
--ag-background-color: #ffffff;
--ag-header-background-color: #f8fafc;
--ag-odd-row-background-color: #fafafa;
/* 前景色 */
--ag-header-foreground-color: #475569;
--ag-foreground-color: #1e293b;
/* 边框 */
--ag-border-color: #e2e8f0;
--ag-row-border-color: #f1f5f9;
/* 悬停和选择 */
--ag-row-hover-color: #f0fdf4;
--ag-selected-row-background-color: #d1fae5;
/* 字体 */
--ag-font-family: inherit;
--ag-font-size: 13px;
/* 聚焦 */
--ag-range-selection-border-color: #10b981;
--ag-input-focus-border-color: #10b981;
}
/* 表头样式 */
.ag-theme-alpine .ag-header-cell {
font-weight: 600;
padding-left: 12px;
padding-right: 12px;
}
.ag-theme-alpine .ag-header-cell:hover {
background-color: #f1f5f9;
}
/* 单元格样式 */
.ag-theme-alpine .ag-cell {
display: flex;
align-items: center;
padding-left: 12px;
padding-right: 12px;
line-height: 1.5;
}
/* 缺失值高亮样式 */
.ag-theme-alpine .ag-cell.bg-red-50 {
background-color: #fef2f2 !important;
color: #f87171 !important;
font-style: italic;
}
.ag-theme-alpine .ag-cell.bg-red-50::before {
content: '—';
margin-right: 4px;
}
/* 选中行样式 */
.ag-theme-alpine .ag-row-selected {
background-color: #d1fae5 !important;
}
.ag-theme-alpine .ag-row-selected:hover {
background-color: #a7f3d0 !important;
}
/* 排序指示器 */
.ag-theme-alpine .ag-header-cell-sorted-asc,
.ag-theme-alpine .ag-header-cell-sorted-desc {
background-color: #ecfdf5;
color: #059669;
}
/* 滚动条美化 */
.ag-theme-alpine .ag-body-horizontal-scroll::-webkit-scrollbar,
.ag-theme-alpine .ag-body-vertical-scroll::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.ag-theme-alpine .ag-body-horizontal-scroll::-webkit-scrollbar-track,
.ag-theme-alpine .ag-body-vertical-scroll::-webkit-scrollbar-track {
background: #f8fafc;
}
.ag-theme-alpine .ag-body-horizontal-scroll::-webkit-scrollbar-thumb,
.ag-theme-alpine .ag-body-vertical-scroll::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.ag-theme-alpine .ag-body-horizontal-scroll::-webkit-scrollbar-thumb:hover,
.ag-theme-alpine .ag-body-vertical-scroll::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* 列调整大小指示器 */
.ag-theme-alpine .ag-header-cell-resize {
background-color: #10b981;
}

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;

View File

@@ -0,0 +1,61 @@
/**
* Tool C 类型定义
*/
// ==================== 消息类型 ====================
export interface Message {
id: number;
role: 'user' | 'assistant' | 'system';
content: string;
code?: {
content: string;
status: 'pending' | 'running' | 'success' | 'error';
};
onExecute?: () => void;
}
// ==================== 状态类型 ====================
export interface ToolCState {
// Session信息
sessionId: string | null;
fileName: string;
// 表格数据
data: Record<string, any>[];
columns: ColumnConfig[];
// AI对话
messages: Message[];
// UI状态
isLoading: boolean;
isSidebarOpen: boolean;
}
// ==================== 列配置 ====================
export interface ColumnConfig {
id: string;
name: string;
type?: 'text' | 'number' | 'date' | 'category';
width?: number;
}
// ==================== 数据洞察 ====================
export interface DataStats {
totalRows: number;
totalCols: number;
missingCount: number;
missingRate: number;
columnStats?: {
[columnName: string]: {
type: string;
missing: number;
unique: number;
};
};
}