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
316 lines
9.3 KiB
TypeScript
316 lines
9.3 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { Modal, Input, Button, Alert, Collapse, Tag, App } from 'antd';
|
||
import { Calculator, Lightbulb, BookOpen } from 'lucide-react';
|
||
|
||
interface Props {
|
||
visible: boolean;
|
||
onClose: () => void;
|
||
onApply: (newData: any[]) => void;
|
||
columns: Array<{ id: string; name: string }>;
|
||
sessionId: string | null;
|
||
}
|
||
|
||
const ComputeDialog: React.FC<Props> = ({
|
||
visible,
|
||
onClose,
|
||
onApply,
|
||
columns,
|
||
sessionId,
|
||
}) => {
|
||
const { message } = App.useApp();
|
||
const [newColumnName, setNewColumnName] = useState('新列');
|
||
const [formula, setFormula] = useState('');
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
// 公式示例
|
||
const examples = [
|
||
{
|
||
name: 'BMI计算',
|
||
formula: '体重 / (身高/100)**2',
|
||
description: '需要身高(cm)和体重(kg)列',
|
||
},
|
||
{
|
||
name: '年龄分组',
|
||
formula: 'round(年龄 / 10) * 10',
|
||
description: '按10岁为一组',
|
||
},
|
||
{
|
||
name: '综合得分',
|
||
formula: '(FMA得分 * 0.6 + ADL得分 * 0.4)',
|
||
description: '加权平均分',
|
||
},
|
||
{
|
||
name: '变化率(%)',
|
||
formula: '(随访值 - 基线值) / 基线值 * 100',
|
||
description: '计算变化百分比',
|
||
},
|
||
{
|
||
name: '对数转换',
|
||
formula: 'log(值 + 1)',
|
||
description: '对数变换(处理偏态分布)',
|
||
},
|
||
];
|
||
|
||
// 支持的函数
|
||
const functions = [
|
||
{ name: 'abs(x)', desc: '绝对值' },
|
||
{ name: 'round(x)', desc: '四舍五入' },
|
||
{ name: 'sqrt(x)', desc: '平方根' },
|
||
{ name: 'log(x)', desc: '自然对数' },
|
||
{ name: 'log10(x)', desc: '常用对数' },
|
||
{ name: 'exp(x)', desc: '指数函数' },
|
||
{ name: 'floor(x)', desc: '向下取整' },
|
||
{ name: 'ceil(x)', desc: '向上取整' },
|
||
{ name: 'min(a,b)', desc: '最小值' },
|
||
{ name: 'max(a,b)', desc: '最大值' },
|
||
];
|
||
|
||
// 使用示例公式
|
||
const useExample = (exampleFormula: string) => {
|
||
setFormula(exampleFormula);
|
||
};
|
||
|
||
// 执行
|
||
const handleApply = async () => {
|
||
if (!sessionId) {
|
||
message.error('Session ID不存在');
|
||
return;
|
||
}
|
||
|
||
if (!newColumnName.trim()) {
|
||
message.warning('请输入新列名');
|
||
return;
|
||
}
|
||
|
||
if (!formula.trim()) {
|
||
message.warning('请输入计算公式');
|
||
return;
|
||
}
|
||
|
||
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: 'compute',
|
||
params: {
|
||
newColumnName: newColumnName.trim(),
|
||
formula: formula.trim(),
|
||
},
|
||
}),
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (!result.success) {
|
||
throw new Error(result.error || '计算列失败');
|
||
}
|
||
|
||
message.success('计算列成功!');
|
||
|
||
// 更新父组件数据
|
||
if (result.data?.newDataPreview) {
|
||
onApply(result.data.newDataPreview);
|
||
}
|
||
|
||
// 成功后关闭
|
||
onClose();
|
||
} catch (error: any) {
|
||
message.error(error.message || '执行失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
title={
|
||
<div className="flex items-center gap-2">
|
||
<Calculator size={20} className="text-blue-500" />
|
||
<span>计算列</span>
|
||
</div>
|
||
}
|
||
open={visible}
|
||
onCancel={onClose}
|
||
width={800}
|
||
footer={[
|
||
<Button key="cancel" onClick={onClose}>
|
||
取消
|
||
</Button>,
|
||
<Button
|
||
key="apply"
|
||
type="primary"
|
||
onClick={handleApply}
|
||
loading={loading}
|
||
icon={<Calculator size={16} />}
|
||
>
|
||
执行计算
|
||
</Button>,
|
||
]}
|
||
>
|
||
<div className="space-y-4">
|
||
{/* 说明 */}
|
||
<Alert
|
||
title="如何使用"
|
||
description={
|
||
<div className="text-xs space-y-1">
|
||
<div>• 基于现有列计算新列</div>
|
||
<div>• 支持四则运算(+, -, *, /)和幂运算(**)</div>
|
||
<div>• 支持常用数学函数(abs, round, sqrt, log等)</div>
|
||
<div>• 列名直接引用,如:体重, 身高, 年龄</div>
|
||
</div>
|
||
}
|
||
type="info"
|
||
showIcon
|
||
className="mb-4"
|
||
/>
|
||
|
||
{/* 新列名 */}
|
||
<div>
|
||
<label className="text-sm font-medium text-slate-700 mb-1 block">
|
||
新列名:
|
||
</label>
|
||
<Input
|
||
placeholder="如:BMI, 综合得分"
|
||
value={newColumnName}
|
||
onChange={(e) => setNewColumnName(e.target.value)}
|
||
size="large"
|
||
/>
|
||
</div>
|
||
|
||
{/* 计算公式 */}
|
||
<div>
|
||
<label className="text-sm font-medium text-slate-700 mb-1 block">
|
||
计算公式:
|
||
</label>
|
||
<Input.TextArea
|
||
placeholder="如:体重 / (身高/100)**2"
|
||
value={formula}
|
||
onChange={(e) => setFormula(e.target.value)}
|
||
rows={3}
|
||
size="large"
|
||
className="font-mono"
|
||
/>
|
||
<div className="text-xs text-slate-500 mt-1">
|
||
直接使用列名,支持 +、-、*、/、**(幂)、()(括号)
|
||
</div>
|
||
</div>
|
||
|
||
{/* 可用列名 */}
|
||
<div>
|
||
<div className="text-sm font-medium text-slate-700 mb-2 flex items-center gap-2">
|
||
<BookOpen size={16} />
|
||
可用列名(点击复制):
|
||
</div>
|
||
<div className="flex flex-wrap gap-2 p-3 bg-slate-50 rounded-lg max-h-32 overflow-y-auto">
|
||
{columns.map((col) => (
|
||
<Tag
|
||
key={col.id}
|
||
color="blue"
|
||
className="cursor-pointer hover:bg-blue-100"
|
||
onClick={() => {
|
||
setFormula(formula + (formula ? ' ' : '') + col.name);
|
||
}}
|
||
>
|
||
{col.name}
|
||
</Tag>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 公式示例 */}
|
||
<Collapse
|
||
items={[
|
||
{
|
||
key: 'examples',
|
||
label: (
|
||
<div className="flex items-center gap-2">
|
||
<Lightbulb size={16} className="text-yellow-500" />
|
||
<span className="font-medium">公式示例(点击使用)</span>
|
||
</div>
|
||
),
|
||
children: (
|
||
<div className="space-y-2">
|
||
{examples.map((example, index) => (
|
||
<div
|
||
key={index}
|
||
className="border rounded-lg p-3 hover:bg-blue-50 cursor-pointer transition-colors"
|
||
onClick={() => useExample(example.formula)}
|
||
>
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<div className="font-medium text-sm text-blue-600">
|
||
{example.name}
|
||
</div>
|
||
<div className="text-xs text-slate-600 mt-1">
|
||
{example.description}
|
||
</div>
|
||
<code className="text-xs bg-slate-100 px-2 py-1 rounded mt-2 block font-mono">
|
||
{example.formula}
|
||
</code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: 'functions',
|
||
label: (
|
||
<div className="flex items-center gap-2">
|
||
<Calculator size={16} className="text-green-500" />
|
||
<span className="font-medium">支持的函数</span>
|
||
</div>
|
||
),
|
||
children: (
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{functions.map((func, index) => (
|
||
<div
|
||
key={index}
|
||
className="flex items-center justify-between p-2 bg-slate-50 rounded"
|
||
>
|
||
<code className="text-xs font-mono text-blue-600">
|
||
{func.name}
|
||
</code>
|
||
<span className="text-xs text-slate-600">
|
||
{func.desc}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
),
|
||
},
|
||
]}
|
||
defaultActiveKey={['examples']}
|
||
className="bg-white"
|
||
/>
|
||
|
||
{/* 温馨提示 */}
|
||
<Alert
|
||
message={
|
||
<div className="text-xs">
|
||
<strong>提示</strong>:
|
||
<div className="mt-1 space-y-1">
|
||
<div>• 列名区分大小写,需与数据表中完全一致</div>
|
||
<div>• 除零会产生无穷大或NaN值</div>
|
||
<div>• log、sqrt等函数对负数/零会产生NaN</div>
|
||
<div>• 建议先用简单公式测试,再使用复杂公式</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
type="warning"
|
||
showIcon={false}
|
||
className="bg-yellow-50 border-yellow-200"
|
||
/>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
export default ComputeDialog;
|
||
|