feat(dc): Complete Tool C quick action buttons Phase 1-2 - 7 functions

Summary:
- Implement 7 quick action functions (filter, recode, binning, conditional, dropna, compute, pivot)
- Refactor to pre-written Python functions architecture (stable and secure)
- Add 7 Python operations modules with full type hints
- Add 7 frontend Dialog components with user-friendly UI
- Fix NaN serialization issues and auto type conversion
- Update all related documentation

Technical Details:
- Python: operations/ module (filter.py, recode.py, binning.py, conditional.py, dropna.py, compute.py, pivot.py)
- Backend: QuickActionService.ts with 7 execute methods
- Frontend: 7 Dialog components with complete validation
- Toolbar: Enable 7 quick action buttons

Status: Phase 1-2 completed, basic testing passed, ready for further testing
This commit is contained in:
2025-12-08 17:38:08 +08:00
parent af325348b8
commit f729699510
158 changed files with 13814 additions and 273 deletions

View File

@@ -0,0 +1,282 @@
/**
* 数值映射(重编码)对话框
*
* 功能:
* - 自动提取列的唯一值
* - 用户配置映射关系
* - 支持创建新列或覆盖原列
*/
import React, { useState, useEffect } from 'react';
import { Modal, Select, Input, Button, Checkbox, Table, Spin, App } from 'antd';
interface RecodeDialogProps {
visible: boolean;
columns: Array<{ id: string; name: string }>;
data: any[];
sessionId: string | null;
onClose: () => void;
onApply: (newData: any[]) => void;
}
interface MappingRow {
originalValue: any;
newValue: string;
}
const RecodeDialog: React.FC<RecodeDialogProps> = ({
visible,
columns,
data,
sessionId,
onClose,
onApply,
}) => {
const { message } = App.useApp();
const [selectedColumn, setSelectedColumn] = useState<string>('');
const [uniqueValues, setUniqueValues] = useState<any[]>([]);
const [mappingTable, setMappingTable] = useState<MappingRow[]>([]);
const [createNewColumn, setCreateNewColumn] = useState(true);
const [newColumnName, setNewColumnName] = useState('');
const [loading, setLoading] = useState(false);
const [extracting, setExtracting] = useState(false);
// 当选择列时,提取唯一值
useEffect(() => {
if (!selectedColumn || !data || data.length === 0) {
setUniqueValues([]);
setMappingTable([]);
return;
}
setExtracting(true);
// 提取唯一值
const values = data.map((row) => row[selectedColumn]);
const unique = Array.from(new Set(values)).filter(v => v !== null && v !== undefined && v !== '');
setUniqueValues(unique);
// 初始化映射表
const initialMapping = unique.map((val) => ({
originalValue: val,
newValue: '',
}));
setMappingTable(initialMapping);
// 生成默认新列名
setNewColumnName(`${selectedColumn}_编码`);
setExtracting(false);
}, [selectedColumn, data]);
// 更新映射值
const updateMapping = (originalValue: any, newValue: string) => {
setMappingTable(
mappingTable.map((row) =>
row.originalValue === originalValue ? { ...row, newValue } : row
)
);
};
// 表格列定义
const tableColumns = [
{
title: '原值',
dataIndex: 'originalValue',
key: 'originalValue',
width: '45%',
render: (value: any) => (
<span className="font-mono text-sm">{String(value)}</span>
),
},
{
title: '→',
key: 'arrow',
width: '10%',
align: 'center' as const,
render: () => <span className="text-slate-400"></span>,
},
{
title: '新值',
dataIndex: 'newValue',
key: 'newValue',
width: '45%',
render: (_: any, record: MappingRow) => (
<Input
placeholder="输入新值"
value={record.newValue}
onChange={(e) => updateMapping(record.originalValue, e.target.value)}
size="small"
/>
),
},
];
// 执行重编码
const handleApply = async () => {
if (!sessionId || !selectedColumn) {
message.error('请先选择列');
return;
}
// 验证映射表
const emptyMapping = mappingTable.find((row) => !row.newValue);
if (emptyMapping) {
message.warning('请为所有原值指定新值');
return;
}
if (createNewColumn && !newColumnName) {
message.warning('请输入新列名');
return;
}
// 构建映射对象
const mapping: Record<string, any> = {};
mappingTable.forEach((row) => {
mapping[String(row.originalValue)] = row.newValue;
});
setLoading(true);
try {
const response = await fetch('/api/v1/dc/tool-c/quick-action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId,
action: 'recode',
params: {
column: selectedColumn,
mapping,
createNewColumn,
newColumnName: createNewColumn ? newColumnName : undefined,
},
}),
});
const result = await response.json();
if (result.success) {
message.success('重编码成功!');
onApply(result.data.newDataPreview);
onClose();
} else {
// 友好的错误提示
message.error({
content: result.error || '重编码失败',
duration: 5,
});
}
} catch (error: any) {
console.error('[RecodeDialog] 执行失败:', error);
message.error({
content: '网络错误,请检查服务是否正常运行',
duration: 5,
});
} finally {
setLoading(false);
}
};
return (
<Modal
title="🔄 数值映射(重编码)"
open={visible}
onCancel={onClose}
width={800}
footer={null}
>
<div className="space-y-4">
{/* 选择列 */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
</label>
<Select
placeholder="选择列"
value={selectedColumn || undefined}
onChange={setSelectedColumn}
showSearch
style={{ width: '100%' }}
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={columns.map((col) => ({ value: col.id, label: col.name }))}
/>
</div>
{/* 映射表 */}
{selectedColumn && (
<>
{extracting ? (
<div className="text-center py-8">
<Spin tip="正在提取唯一值..." />
</div>
) : (
<>
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-slate-700">
{uniqueValues.length}
</label>
<span className="text-xs text-slate-500">
💡 1,2,3...
</span>
</div>
<Table
dataSource={mappingTable}
columns={tableColumns}
pagination={false}
size="small"
rowKey="originalValue"
scroll={{ y: 300 }}
/>
</div>
{/* 新列选项 */}
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
<Checkbox
checked={createNewColumn}
onChange={(e) => setCreateNewColumn(e.target.checked)}
>
<span className="text-sm font-medium"></span>
</Checkbox>
{createNewColumn && (
<div className="mt-2">
<Input
placeholder="输入新列名"
value={newColumnName}
onChange={(e) => setNewColumnName(e.target.value)}
size="small"
prefix={<span className="text-xs text-slate-500"></span>}
/>
</div>
)}
</div>
</>
)}
</>
)}
{/* 操作按钮 */}
<div className="flex items-center justify-end gap-2 pt-4 border-t border-slate-200">
<Button onClick={onClose}></Button>
<Button
type="primary"
onClick={handleApply}
loading={loading}
disabled={!selectedColumn || mappingTable.length === 0}
>
</Button>
</div>
</div>
</Modal>
);
};
export default RecodeDialog;