/** * 指标-时间表转换面板 * * 将多个时间点列转换为"指标行+时间点列"格式 * 典型场景:制作临床研究Table 1,横向对比同一指标的时间变化 */ import React, { useState, useEffect } from 'react'; import { Button, Alert, Checkbox, App, Input, Spin, Tag } from 'antd'; import { ArrowLeftRight, Info, Sparkles } from 'lucide-react'; interface Props { columns: Array<{ id: string; name: string }>; sessionId: string | null; onApply: (newData: any[]) => void; onClose: () => void; } interface DetectedPattern { common_prefix: string; separator: string; timepoints: string[]; confidence: number; message: string; } const MetricTimePanel: React.FC = ({ columns, sessionId, onApply, onClose, }) => { const { message } = App.useApp(); const [idVars, setIdVars] = useState([]); const [valueVars, setValueVars] = useState([]); const [loading, setLoading] = useState(false); const [detecting, setDetecting] = useState(false); // 检测结果 const [pattern, setPattern] = useState(null); const [metricName, setMetricName] = useState(''); const [separator, setSeparator] = useState(''); const [timepointColName, setTimepointColName] = useState('时间点'); // 当值列变化时,自动检测模式 useEffect(() => { if (valueVars.length >= 2) { detectPattern(); } else { setPattern(null); setMetricName(''); setSeparator(''); } }, [valueVars]); // 自动检测模式 const detectPattern = async () => { setDetecting(true); try { const response = await fetch('/api/v1/dc/tool-c/metric-time/detect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId, valueVars, }), }); const result = await response.json(); if (result.success && result.pattern) { setPattern(result.pattern); setMetricName(result.pattern.common_prefix || ''); setSeparator(result.pattern.separator || ''); if (result.pattern.confidence >= 0.8) { message.success(`自动检测成功!置信度: ${(result.pattern.confidence * 100).toFixed(0)}%`); } else if (result.pattern.confidence >= 0.5) { message.warning(`检测成功但置信度较低 (${(result.pattern.confidence * 100).toFixed(0)}%),建议检查结果`); } else { message.warning('检测置信度较低,建议手动调整参数'); } } else { message.error(result.error || '模式检测失败'); } } catch (error: any) { message.error('检测失败:' + error.message); } finally { setDetecting(false); } }; // 执行转换 const handleApply = async () => { if (!sessionId) { message.error('Session ID不存在'); return; } // 验证 if (idVars.length === 0) { message.warning('请至少选择1个ID列'); return; } if (valueVars.length < 2) { message.warning('请至少选择2个值列'); return; } // 验证:ID列和值列不能重复 const overlap = idVars.filter(id => valueVars.includes(id)); if (overlap.length > 0) { message.error(`ID列和值列不能重复:${overlap.join(', ')}`); return; } if (!metricName.trim()) { message.warning('请输入指标名称'); return; } if (!timepointColName.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: 'metric_time', params: { idVars, valueVars, metricName, separator: separator || undefined, timepointColName, }, }), }); 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); } }; // 获取时间点预览(从pattern中) const timepoints = pattern?.timepoints || []; const timepointsSample = timepoints.slice(0, 5); const hasMoreTimepoints = timepoints.length > 5; return (
{/* 说明 */}
• 将多个时间点列转换为"指标行+时间点列"格式
• 典型场景:制作临床研究Table 1,横向对比同一指标的时间变化
• 示例:FMA___基线、FMA___2周 → 时间点列(FMA)+ 基线列 + 2周列
} type="info" showIcon icon={} className="mb-4" /> {/* 第1步:ID列 */}
{columns.map((col) => ( {col.name} ))}
这些列的值在转换后保持不变(如:Record_ID、性别、年龄)
{/* 第2步:值列 */}
{columns.map((col) => ( {col.name} ))}
至少选择2列。这些列应该属于同一个指标的不同时间点(已选择 {valueVars.length} 列)
{/* 第3步:自动检测结果 */} {valueVars.length >= 2 && (
第3步:自动检测分析 {detecting && }
{pattern ? (
{/* 检测结果 */}
✓ 检测成功
= 0.8 ? 'green' : pattern.confidence >= 0.5 ? 'orange' : 'red'}> 置信度: {(pattern.confidence * 100).toFixed(0)}%
• 检测到 {timepoints.length} 个时间点
• {pattern.message}
{/* 指标名称(可编辑) */}
setMetricName(e.target.value)} maxLength={100} suffix={ 🖊️ 可编辑 } />
自动检测到的指标名,可手动修改
{/* 分隔符(可编辑) */}
setSeparator(e.target.value)} maxLength={10} suffix={ {separator ? `"${separator}"` : '无'} } />
自动检测到的分隔符({separator ? `"${separator}"` : '未检测到'}),可手动修改
{/* 时间点列名 */}
setTimepointColName(e.target.value)} maxLength={30} />
新增列的列名,用于存储指标名称
{/* 时间点预览 */}
识别到的时间点({timepoints.length}个):
{timepointsSample.map((tp, idx) => ( {tp} ))} {hasMoreTimepoints && ( ... 还有 {timepoints.length - 5} 个 )}
) : (
正在自动检测列名模式...
)}
)} {/* 转换结果预览 */} {pattern && valueVars.length >= 2 && (
转换结果预览:
• 新增"{timepointColName}"列,值为:{metricName}
• 原来的 {valueVars.length} 列 → 转换为 {timepoints.length} 个时间点列
• 时间点列名:{timepointsSample.join(', ')}{hasMoreTimepoints ? '...' : ''}
} type="success" showIcon={false} className="bg-green-50 border-green-200" /> )} {/* 警告 */}
• 请确保选中的列属于同一个指标的不同时间点
• 如果检测结果不准确,可以手动修改指标名称和分隔符
} type="warning" showIcon={false} className="bg-orange-50 border-orange-200" /> {/* 底部按钮 */}
); }; export default MetricTimePanel;