Files
AIclinicalresearch/docs/03-业务模块/DC-数据清洗整理/03-UI设计/工具C_科研数据编辑器_原型设计_V2.html
HaHafeng d4d33528c7 feat(dc): Complete Phase 1 - Portal workbench page development
Summary:
- Implement DC module Portal page with 3 tool cards
- Create ToolCard component with decorative background and hover animations
- Implement TaskList component with table layout and progress bars
- Implement AssetLibrary component with tab switching and file cards
- Complete database verification (4 tables confirmed)
- Complete backend API verification (6 endpoints ready)
- Optimize UI to match prototype design (V2.html)

Frontend Components (~715 lines):
- components/ToolCard.tsx - Tool cards with animations
- components/TaskList.tsx - Recent tasks table view
- components/AssetLibrary.tsx - Data asset library with tabs
- hooks/useRecentTasks.ts - Task state management
- hooks/useAssets.ts - Asset state management
- pages/Portal.tsx - Main portal page
- types/portal.ts - TypeScript type definitions

Backend Verification:
- Backend API: 1495 lines code verified
- Database: dc_schema with 4 tables verified
- API endpoints: 6 endpoints tested (templates API works)

Documentation:
- Database verification report
- Backend API test report
- Phase 1 completion summary
- UI optimization report
- Development task checklist
- Development plan for Tool B

Status: Phase 1 completed (100%), ready for browser testing
Next: Phase 2 - Tool B Step 1 and 2 development
2025-12-02 21:53:24 +08:00

534 lines
38 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工具C - 科研数据编辑器 V2 (扁平化交互版)</title>
<!-- 1. 引入 Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 2. 引入 React 和 ReactDOM -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- 3. 引入 Babel -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- 4. 引入 Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
.animate-in { animation: fadeIn 0.3s ease-out forwards; }
.slide-in-right { animation: slideInRight 0.3s ease-out forwards; }
/* 滚动条美化 */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
</style>
</head>
<body class="bg-slate-50 text-slate-800 font-sans antialiased overflow-hidden">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// --- 图标组件适配 (Safe Shim) ---
// 使用 ref 容器隔离 React 和 DOM 操作,避免 removeChild 错误
const LucideIcon = ({ name, className, size = 16, ...props }) => {
const containerRef = useRef(null);
useEffect(() => {
if (containerRef.current) {
containerRef.current.innerHTML = '';
const i = document.createElement('i');
i.setAttribute('data-lucide', name);
containerRef.current.appendChild(i);
lucide.createIcons({
root: containerRef.current,
nameAttr: 'data-lucide',
attrs: { class: className, width: size, height: size, ...props }
});
}
}, [name, className, size]);
return <span ref={containerRef} style={{ display: 'inline-flex', verticalAlign: 'middle' }}></span>;
};
// --- 模拟数据 ---
const MOCK_DATA = [
{ id: 'P001', age: 45, gender: 'Male', bmi: 24.5, admission_date: '2023-01-12', lab_val: '4.5' },
{ id: 'P002', age: 62, gender: 'Female', bmi: 28.1, admission_date: '2023-01-15', lab_val: '5.1' },
{ id: 'P003', age: 205, gender: 'Male', bmi: null, admission_date: '2023-02-01', lab_val: '<0.1' },
{ id: 'P004', age: 58, gender: 'F', bmi: 22.4, admission_date: '2023-02-10', lab_val: '4.8' },
{ id: 'P005', age: 34, gender: 'Male', bmi: 21.0, admission_date: '2023-03-05', lab_val: '5.2' },
{ id: 'P006', age: 71, gender: 'Female', bmi: 30.5, admission_date: '2023-03-12', lab_val: '6.0' },
{ id: 'P007', age: null, gender: 'Male', bmi: 25.3, admission_date: '2023-04-01', lab_val: '4.9' },
{ id: 'P008', age: 49, gender: 'Male', bmi: 26.8, admission_date: '2023-04-05', lab_val: '5.5' },
{ id: 'P009', age: 55, gender: 'Female', bmi: 23.9, admission_date: '2023-04-10', lab_val: '4.2' },
{ id: 'P010', age: 66, gender: 'Male', bmi: 29.1, admission_date: '2023-04-12', lab_val: '5.8' },
];
const COLUMNS = [
{ id: 'id', name: '病人ID', type: 'text', locked: true },
{ id: 'age', name: '年龄', type: 'number' },
{ id: 'gender', name: '性别', type: 'category' },
{ id: 'bmi', name: 'BMI指数', type: 'number' },
{ id: 'admission_date', name: '入院日期', type: 'date' },
{ id: 'lab_val', name: '肌酐', type: 'text' },
];
const ToolC_EditorV2 = () => {
const [selectedColId, setSelectedColId] = useState(null);
const [showSidebar, setShowSidebar] = useState(false);
const [showModal, setShowModal] = useState(null);
const selectedCol = COLUMNS.find(c => c.id === selectedColId);
const handleColClick = (colId) => {
if (selectedColId === colId) {
setShowSidebar(!showSidebar);
} else {
setSelectedColId(colId);
setShowSidebar(true);
}
};
// --- 顶部工具栏按钮组件 ---
const ToolbarButton = ({ iconName, label, desc, colorClass = "text-slate-600 bg-slate-50 hover:bg-slate-100", onClick }) => (
<button
onClick={onClick}
className="relative group flex flex-col items-center justify-center w-24 h-16 rounded-lg transition-all hover:shadow-sm"
>
<div className={`p-2 rounded-lg mb-1 ${colorClass}`}>
<LucideIcon name={iconName} size={20} />
</div>
<span className="text-[11px] font-medium text-slate-600">{label}</span>
{/* Custom Tooltip */}
<div className="absolute top-full mt-2 px-3 py-2 bg-slate-800 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-50">
{desc}
<div className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-slate-800 rotate-45"></div>
</div>
</button>
);
// --- 侧边栏渲染 ---
const renderSidebar = () => {
if (!showSidebar || !selectedCol) return null;
return (
<div className="w-80 bg-white border-l border-slate-200 flex flex-col shadow-xl slide-in-right absolute right-0 top-0 bottom-0 z-20">
{/* Sidebar Header */}
<div className="p-4 border-b border-slate-100 bg-slate-50 flex justify-between items-start">
<div>
<div className="text-xs text-slate-500 uppercase tracking-wider mb-1">当前选中列</div>
<h3 className="text-lg font-bold text-slate-900 flex items-center gap-2">
{selectedCol.name}
<span className="px-2 py-0.5 bg-slate-200 text-slate-600 text-[10px] rounded-full font-mono">{selectedCol.id}</span>
</h3>
</div>
<button onClick={() => setShowSidebar(false)} className="text-slate-400 hover:text-slate-600">
<LucideIcon name="arrow-right" size={20} />
</button>
</div>
{/* Type Definition */}
<div className="p-4 border-b border-slate-100">
<label className="block text-xs font-semibold text-slate-500 mb-2">变量类型定义</label>
<div className="flex gap-2">
<button className={`flex-1 py-1.5 text-xs rounded border flex items-center justify-center gap-1 ${selectedCol.type === 'number' ? 'bg-emerald-50 border-emerald-200 text-emerald-700' : 'border-slate-200 text-slate-600 hover:bg-slate-50'}`}><LucideIcon name="hash" size={12} /> 数值</button>
<button className={`flex-1 py-1.5 text-xs rounded border flex items-center justify-center gap-1 ${selectedCol.type === 'text' ? 'bg-emerald-50 border-emerald-200 text-emerald-700' : 'border-slate-200 text-slate-600 hover:bg-slate-50'}`}><LucideIcon name="type" size={12} /> 文本</button>
<button className={`flex-1 py-1.5 text-xs rounded border flex items-center justify-center gap-1 ${selectedCol.type === 'category' ? 'bg-emerald-50 border-emerald-200 text-emerald-700' : 'border-slate-200 text-slate-600 hover:bg-slate-50'}`}><LucideIcon name="split" size={12} /> 分类</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-6">
{/* 数值型列操作 */}
{selectedCol.type === 'number' && (
<>
<div className="space-y-3">
<h4 className="text-sm font-bold text-slate-800 flex items-center gap-2">
<LucideIcon name="bar-chart-3" size={16} className="text-slate-500" /> 分布直方图
</h4>
<div className="h-24 flex items-end gap-1 px-2 border-b border-slate-200 pb-1">
{[10, 25, 45, 80, 50, 30, 15, 5, 2, 1].map((h, i) => (
<div key={i} className="flex-1 bg-emerald-200 hover:bg-emerald-400 transition-colors rounded-t-sm" style={{ height: `${h}%` }}></div>
))}
</div>
<div className="flex justify-between text-xs text-slate-400 px-1">
<span>Min: 34</span>
<span>Max: 205</span>
</div>
<div className="bg-orange-50 border border-orange-100 p-3 rounded-lg flex gap-3 items-start">
<LucideIcon name="alert-circle" size={16} className="text-orange-600 mt-0.5" />
<div>
<div className="text-xs font-bold text-orange-700">发现异常值</div>
<div className="text-[10px] text-orange-600 mt-1"> <strong>205</strong> </div>
<button className="mt-2 text-xs bg-white border border-orange-200 text-orange-700 px-2 py-1 rounded hover:bg-orange-100">
处理异常值
</button>
</div>
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-bold text-slate-800">针对此列的操作</h4>
<button className="w-full text-left px-3 py-3 rounded-lg bg-slate-50 hover:bg-slate-100 text-sm text-slate-700 flex items-center gap-3 border border-slate-200 hover:border-emerald-300 transition-all group">
<LucideIcon name="split" size={16} className="text-emerald-600" />
<div>
<div className="font-medium">生成分类变量</div>
<div className="text-[10px] text-slate-400 group-hover:text-slate-500">例如: 将年龄分为 老年/中年/青年</div>
</div>
</button>
<button className="w-full text-left px-3 py-3 rounded-lg bg-slate-50 hover:bg-slate-100 text-sm text-slate-700 flex items-center gap-3 border border-slate-200 hover:border-emerald-300 transition-all group">
<LucideIcon name="wand-2" size={16} className="text-blue-600" />
<div>
<div className="font-medium">填补缺失值</div>
<div className="text-[10px] text-slate-400 group-hover:text-slate-500">使用均值中位数或特定值填补</div>
</div>
</button>
</div>
</>
)}
{/* 分类/文本型列操作 */}
{(selectedCol.type === 'category' || selectedCol.type === 'text') && (
<>
<div className="space-y-3">
<h4 className="text-sm font-bold text-slate-800 flex items-center gap-2">
<LucideIcon name="bar-chart-3" size={16} className="text-slate-500" /> 频次统计
</h4>
<div className="space-y-2">
{['Male (50%)', 'Female (40%)', 'F (10%)'].map((label, i) => (
<div key={i} className="relative h-6 bg-slate-50 rounded overflow-hidden">
<div className="absolute top-0 left-0 h-full bg-emerald-100" style={{ width: label.includes('50') ? '50%' : label.includes('40') ? '40%' : '10%' }}></div>
<div className="absolute inset-0 flex items-center px-2 text-xs text-slate-700 justify-between">
<span>{label.split(' ')[0]}</span>
<span className="text-slate-400">{label.split(' ')[1]}</span>
</div>
</div>
))}
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-bold text-slate-800">针对此列的操作</h4>
<button
className="w-full text-left px-3 py-3 rounded-lg bg-slate-50 hover:bg-slate-100 text-sm text-slate-700 flex items-center gap-3 border border-slate-200 hover:border-emerald-300 transition-all group"
onClick={() => setShowModal('recode')}
>
<LucideIcon name="settings" size={16} className="text-blue-600" />
<div>
<div className="font-medium">数值映射 (Recode)</div>
<div className="text-[10px] text-slate-400 group-hover:text-slate-500">例如: Male->1, Female->0</div>
</div>
</button>
<button className="w-full text-left px-3 py-3 rounded-lg bg-slate-50 hover:bg-slate-100 text-sm text-slate-700 flex items-center gap-3 border border-slate-200 hover:border-emerald-300 transition-all group">
<LucideIcon name="shield-check" size={16} className="text-purple-600" />
<div>
<div className="font-medium">设为敏感字段 (脱敏)</div>
<div className="text-[10px] text-slate-400 group-hover:text-slate-500">隐藏或加密该列数据</div>
</div>
</button>
</div>
</>
)}
</div>
</div>
);
};
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-800 flex flex-col relative overflow-hidden">
{/* 1. Header */}
<header className="bg-white border-b border-slate-200 h-14 flex items-center justify-between px-4 z-30">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-emerald-100 rounded-lg flex items-center justify-center text-emerald-600">
<LucideIcon name="table-2" size={20} />
</div>
<span className="font-bold text-slate-900">科研数据编辑器</span>
<div className="h-4 w-[1px] bg-slate-300 mx-2"></div>
<span className="text-xs text-slate-500">肺癌数据集_2023.csv (未保存)</span>
</div>
<div className="flex items-center gap-2">
<button className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-full" title="撤销"><LucideIcon name="undo-2" size={16} /></button>
<button className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-full" title="重做"><LucideIcon name="redo-2" size={16} /></button>
<div className="h-4 w-[1px] bg-slate-300 mx-2"></div>
<button className="flex items-center gap-2 px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-medium hover:bg-slate-200">
<LucideIcon name="save" size={12} /> 保存快照
</button>
<button className="flex items-center gap-2 px-4 py-1.5 bg-emerald-600 text-white rounded-lg text-xs font-medium hover:bg-emerald-700 shadow-sm">
<LucideIcon name="download" size={12} /> 导出分析
</button>
</div>
</header>
{/* 2. Top Toolbar (V2 扁平化 + 长宽转换) */}
<div className="bg-white border-b border-slate-200 px-6 py-2 flex items-center gap-2 overflow-x-auto">
<ToolbarButton
iconName="calculator"
label="生成新变量"
desc="使用公式计算生成新列 (如 BMI = 体重/身高²)"
colorClass="bg-emerald-50 text-emerald-600 group-hover:bg-emerald-100"
onClick={() => setShowModal('calc')}
/>
<ToolbarButton
iconName="calendar-clock"
label="计算时间差"
desc="计算两个日期之间的天数/年数 (如 年龄、住院天数)"
colorClass="bg-blue-50 text-blue-600 group-hover:bg-blue-100"
/>
<ToolbarButton
iconName="arrow-left-right"
label="长宽转换"
desc="将'一人多行'转换为'一人一行' (透视/Pivot)"
colorClass="bg-blue-50 text-blue-600 group-hover:bg-blue-100"
onClick={() => setShowModal('pivot')}
/>
<div className="w-[1px] h-10 bg-slate-200 mx-2"></div>
<ToolbarButton
iconName="file-up"
label="拆分数据集"
desc="按某一列的值将数据拆分为多个文件或 Sheet"
colorClass="bg-purple-50 text-purple-600 group-hover:bg-purple-100"
/>
<div className="w-[1px] h-10 bg-slate-200 mx-2"></div>
<ToolbarButton
iconName="alert-circle"
label="跨列规则检查"
desc="自定义逻辑规则 (如: 男性不能有孕产史) 并标记错误行"
colorClass="bg-orange-50 text-orange-600 group-hover:bg-orange-100"
/>
<ToolbarButton
iconName="filter"
label="构建入排标准"
desc="组合多重筛选条件,生成最终分析集 (Cohort Selection)"
colorClass="bg-indigo-50 text-indigo-600 group-hover:bg-indigo-100"
/>
<div className="w-[1px] h-10 bg-slate-200 mx-2"></div>
<div className="relative ml-auto">
<LucideIcon name="search" size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input type="text" placeholder="搜索值..." className="pl-9 pr-4 py-1.5 text-sm border border-slate-200 rounded-full focus:border-emerald-500 outline-none" />
</div>
</div>
{/* 3. Main Grid Area */}
<div className="flex-1 relative overflow-hidden bg-slate-100 flex">
{/* Grid */}
<div className="flex-1 overflow-auto p-6 transition-all duration-300" style={{ marginRight: showSidebar ? '320px' : '0' }}>
<div className="bg-white border border-slate-200 shadow-sm rounded-lg overflow-hidden min-w-[800px]">
<div className="flex bg-slate-50 border-b border-slate-200 sticky top-0 z-10">
{COLUMNS.map((col) => (
<div
key={col.id}
className={`h-10 px-4 flex items-center justify-between border-r border-slate-200 text-xs font-bold text-slate-600 cursor-pointer hover:bg-slate-100 transition-colors ${selectedColId === col.id ? 'bg-emerald-50 text-emerald-700' : ''}`}
style={{ width: col.id === 'id' ? '100px' : '150px' }}
onClick={() => handleColClick(col.id)}
>
<div className="flex items-center gap-1.5">
{col.type === 'number' && <LucideIcon name="hash" size={12} className="text-slate-400" />}
{col.type === 'text' && <LucideIcon name="type" size={12} className="text-slate-400" />}
{col.type === 'category' && <LucideIcon name="split" size={12} className="text-slate-400" />}
{col.type === 'date' && <LucideIcon name="calendar" size={12} className="text-slate-400" />}
{col.name}
</div>
<LucideIcon name="chevron-down" size={12} className="text-slate-300" />
</div>
))}
</div>
<div className="divide-y divide-slate-100 text-sm text-slate-700">
{MOCK_DATA.map((row, idx) => (
<div key={idx} className="flex hover:bg-slate-50 transition-colors">
{COLUMNS.map((col) => {
const val = row[col.id];
const isNull = val === null || val === '';
const isOutlier = col.id === 'age' && val > 120;
const isDirty = col.id === 'lab_val' && typeof val === 'string' && val.includes('<');
return (
<div
key={col.id}
className={`h-10 px-4 flex items-center border-r border-slate-100
${isNull ? 'bg-red-50' : ''}
${isOutlier ? 'bg-orange-100 text-orange-700 font-bold' : ''}
${isDirty ? 'text-purple-600 font-medium' : ''}
${selectedColId === col.id ? 'bg-emerald-50/30' : ''}
`}
style={{ width: col.id === 'id' ? '100px' : '150px' }}
>
{isNull ? <span className="text-[10px] text-red-400 italic">NULL</span> : val}
</div>
);
})}
</div>
))}
</div>
</div>
</div>
{renderSidebar()}
</div>
{/* Modals */}
{showModal === 'calc' && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center animate-in">
<div className="bg-white rounded-xl shadow-2xl w-[500px] overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-bold text-slate-900">生成新变量 (Formula)</h3>
<button onClick={() => setShowModal(null)}><LucideIcon name="trash-2" size={16} className="text-slate-400 rotate-45" /></button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-xs font-bold text-slate-500 mb-1">新变量名称</label>
<input type="text" className="w-full border p-2 rounded text-sm" placeholder="例如: BMI_Calc" defaultValue="New_Var" />
</div>
<div>
<label className="block text-xs font-bold text-slate-500 mb-1">计算公式</label>
<div className="w-full h-24 border p-2 rounded bg-slate-50 font-mono text-sm text-slate-700">
[体重] / ([身高] / 100) ^ 2
</div>
<div className="flex gap-2 mt-2">
<button className="text-xs px-2 py-1 bg-slate-100 rounded hover:bg-slate-200">+ </button>
<button className="text-xs px-2 py-1 bg-slate-100 rounded hover:bg-slate-200">- </button>
<button className="text-xs px-2 py-1 bg-slate-100 rounded hover:bg-slate-200">* </button>
<button className="text-xs px-2 py-1 bg-slate-100 rounded hover:bg-slate-200">/ </button>
<button className="text-xs px-2 py-1 bg-purple-100 text-purple-700 rounded hover:bg-purple-200">ln()</button>
</div>
</div>
</div>
<div className="px-6 py-4 bg-slate-50 flex justify-end gap-3">
<button onClick={() => setShowModal(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">取消</button>
<button onClick={() => setShowModal(null)} className="px-4 py-2 text-sm bg-emerald-600 text-white rounded-lg hover:bg-emerald-700">计算并生成</button>
</div>
</div>
</div>
)}
{showModal === 'recode' && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center animate-in">
<div className="bg-white rounded-xl shadow-2xl w-[400px] overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-bold text-slate-900">数值映射 (Recode)</h3>
<button onClick={() => setShowModal(null)}><LucideIcon name="trash-2" size={16} className="text-slate-400 rotate-45" /></button>
</div>
<div className="p-6">
<div className="text-xs text-slate-500 mb-4">将源值映射为新的统计数值</div>
<div className="space-y-2">
<div className="flex items-center gap-4"><span className="w-20 text-sm font-medium text-right">Male</span><LucideIcon name="arrow-right" size={16} className="text-slate-300" /><input type="text" className="w-20 border p-1 rounded text-center" defaultValue="1" /></div>
<div className="flex items-center gap-4"><span className="w-20 text-sm font-medium text-right">Female</span><LucideIcon name="arrow-right" size={16} className="text-slate-300" /><input type="text" className="w-20 border p-1 rounded text-center" defaultValue="0" /></div>
<div className="flex items-center gap-4"><span className="w-20 text-sm font-medium text-right">F</span><LucideIcon name="arrow-right" size={16} className="text-slate-300" /><input type="text" className="w-20 border p-1 rounded text-center" defaultValue="0" /></div>
</div>
</div>
<div className="px-6 py-4 bg-slate-50 flex justify-end gap-3">
<button onClick={() => setShowModal(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">取消</button>
<button onClick={() => setShowModal(null)} className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">应用映射</button>
</div>
</div>
</div>
)}
{/* Pivot Modal (新增) */}
{showModal === 'pivot' && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center animate-in">
<div className="bg-white rounded-xl shadow-2xl w-[600px] overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-blue-50/50">
<div className="flex items-center gap-2">
<LucideIcon name="arrow-left-right" size={20} className="text-blue-600" />
<div>
<h3 className="font-bold text-slate-900">长宽表转换 (Pivot)</h3>
<p className="text-xs text-slate-500">"一人多行"转换为"一人一行"适用于重复测量分析</p>
</div>
</div>
<button onClick={() => setShowModal(null)}><LucideIcon name="trash-2" size={16} className="text-slate-400 rotate-45" /></button>
</div>
<div className="p-6 grid grid-cols-3 gap-6">
{/* 列 1: 主键 */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 block">1. 谁是唯一ID? ()</label>
<select className="w-full p-2 border border-slate-300 rounded text-sm bg-white">
<option>病人ID</option>
<option>姓名</option>
</select>
<p className="text-[10px] text-slate-400">转换后每个ID只保留一行</p>
</div>
{/* 列 2: 区分列 */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 block">2. 谁是区分次序? (列后缀)</label>
<select className="w-full p-2 border border-slate-300 rounded text-sm bg-white">
<option>入院日期</option>
<option>就诊次序</option>
</select>
<p className="text-[10px] text-slate-400">生成列名如: 白细胞_20230101</p>
</div>
{/* 列 3: 值列 */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 block">3. 哪些值要铺平? (内容)</label>
<div className="border border-slate-300 rounded p-2 h-32 overflow-y-auto bg-slate-50 text-sm space-y-1">
<label className="flex items-center gap-2"><input type="checkbox" checked className="rounded text-blue-600" /> <span>白细胞</span></label>
<label className="flex items-center gap-2"><input type="checkbox" checked className="rounded text-blue-600" /> <span>病理报告</span></label>
<label className="flex items-center gap-2"><input type="checkbox" checked className="rounded text-blue-600" /> <span>B超结果</span></label>
<label className="flex items-center gap-2"><input type="checkbox" className="rounded text-blue-600" /> <span>年龄 (无需铺平)</span></label>
</div>
</div>
</div>
<div className="px-6 pb-6">
<div className="bg-slate-50 border border-slate-200 rounded p-3">
<div className="text-xs font-bold text-slate-500 mb-2">转换预览:</div>
<div className="flex items-center gap-4 text-xs">
<div className="border p-1 bg-white text-slate-400 rounded">
ID | 时间 | <br/>
P1 | T1 | A<br/>
P1 | T2 | B
</div>
<LucideIcon name="arrow-right" size={16} className="text-blue-500" />
<div className="border p-1 bg-white text-blue-600 font-medium rounded shadow-sm">
ID | _T1 | _T2<br/>
P1 | A | B
</div>
</div>
</div>
</div>
<div className="px-6 py-4 bg-slate-50 flex justify-end gap-3">
<button onClick={() => setShowModal(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">取消</button>
<button onClick={() => setShowModal(null)} className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 shadow-sm">开始转换</button>
</div>
</div>
</div>
)}
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<ToolC_EditorV2 />);
</script>
</body>
</html>