Files
AIclinicalresearch/frontend-v2/src/modules/dc/pages/tool-c/components/ComputeDialog.tsx
HaHafeng f729699510 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
2025-12-08 17:38:08 +08:00

316 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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> logsqrt等函数对负数/NaN</div>
<div> 使</div>
</div>
</div>
}
type="warning"
showIcon={false}
className="bg-yellow-50 border-yellow-200"
/>
</div>
</Modal>
);
};
export default ComputeDialog;