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
506 lines
26 KiB
TypeScript
506 lines
26 KiB
TypeScript
import React, { useState } from 'react';
|
||
import {
|
||
Table2,
|
||
Calculator,
|
||
CalendarClock,
|
||
Split,
|
||
Filter,
|
||
Download,
|
||
ArrowRight,
|
||
Trash2,
|
||
AlertCircle,
|
||
BarChart3,
|
||
Hash,
|
||
Type,
|
||
Calendar,
|
||
ChevronDown,
|
||
Undo2,
|
||
Redo2,
|
||
Save,
|
||
Wand2,
|
||
Settings,
|
||
FileUp,
|
||
Search,
|
||
ArrowLeftRight // 新增图标
|
||
} from 'lucide-react';
|
||
|
||
// --- 模拟数据 ---
|
||
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<string | null>(null);
|
||
const [showSidebar, setShowSidebar] = useState(false);
|
||
const [showModal, setShowModal] = useState<'calc' | 'recode' | 'pivot' | null>(null); // 新增 pivot 状态
|
||
|
||
const selectedCol = COLUMNS.find(c => c.id === selectedColId);
|
||
|
||
const handleColClick = (colId: string) => {
|
||
if (selectedColId === colId) {
|
||
setShowSidebar(!showSidebar);
|
||
} else {
|
||
setSelectedColId(colId);
|
||
setShowSidebar(true);
|
||
}
|
||
};
|
||
|
||
// --- 组件:顶部工具按钮 ---
|
||
const ToolbarButton = ({
|
||
icon: Icon,
|
||
label,
|
||
desc,
|
||
colorClass = "text-slate-600 bg-slate-50 hover:bg-slate-100",
|
||
onClick
|
||
}: { icon: any, label: string, desc: string, colorClass?: string, onClick?: () => void }) => (
|
||
<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}`}>
|
||
<Icon className="w-5 h-5" />
|
||
</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>
|
||
);
|
||
|
||
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">
|
||
<Table2 className="w-5 h-5" />
|
||
</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="撤销"><Undo2 className="w-4 h-4" /></button>
|
||
<button className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-full" title="重做"><Redo2 className="w-4 h-4" /></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">
|
||
<Save className="w-3 h-3" /> 保存快照
|
||
</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">
|
||
<Download className="w-3 h-3" /> 导出分析
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
{/* 2. Top Toolbar (扁平化核心功能) */}
|
||
<div className="bg-white border-b border-slate-200 px-6 py-2 flex items-center gap-2 overflow-x-auto">
|
||
|
||
<ToolbarButton
|
||
icon={Calculator}
|
||
label="生成新变量"
|
||
desc="使用公式计算生成新列 (如 BMI = 体重/身高²)"
|
||
colorClass="bg-emerald-50 text-emerald-600 group-hover:bg-emerald-100"
|
||
onClick={() => setShowModal('calc')}
|
||
/>
|
||
|
||
<ToolbarButton
|
||
icon={CalendarClock}
|
||
label="计算时间差"
|
||
desc="计算两个日期之间的天数/年数 (如 年龄、住院天数)"
|
||
colorClass="bg-blue-50 text-blue-600 group-hover:bg-blue-100"
|
||
/>
|
||
|
||
{/* 新增:长宽转换按钮 */}
|
||
<ToolbarButton
|
||
icon={ArrowLeftRight}
|
||
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
|
||
icon={FileUp}
|
||
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
|
||
icon={AlertCircle}
|
||
label="跨列规则检查"
|
||
desc="自定义逻辑规则 (如: 男性不能有孕产史) 并标记错误行"
|
||
colorClass="bg-orange-50 text-orange-600 group-hover:bg-orange-100"
|
||
/>
|
||
|
||
<ToolbarButton
|
||
icon={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="flex-1"></div> {/* Spacer */}
|
||
|
||
<div className="relative">
|
||
<Search className="w-4 h-4 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' && <Hash className="w-3 h-3 text-slate-400" />}
|
||
{col.type === 'text' && <Type className="w-3 h-3 text-slate-400" />}
|
||
{col.type === 'category' && <Split className="w-3 h-3 text-slate-400" />}
|
||
{col.type === 'date' && <Calendar className="w-3 h-3 text-slate-400" />}
|
||
{col.name}
|
||
</div>
|
||
<ChevronDown className="w-3 h-3 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 as keyof typeof row];
|
||
const isNull = val === null || val === '';
|
||
const isOutlier = col.id === 'age' && (val as number) > 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>
|
||
|
||
{/* 4. Smart Sidebar (基于列类型的操作聚合) */}
|
||
{showSidebar && selectedCol && (
|
||
<div className="w-80 bg-white border-l border-slate-200 flex flex-col shadow-xl animate-in slide-in-from-right duration-300 absolute right-0 top-0 bottom-0 z-20">
|
||
|
||
{/* 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">
|
||
<ArrowRight className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||
|
||
{/* A. 数值列操作 */}
|
||
{selectedCol.type === 'number' && (
|
||
<>
|
||
<div className="space-y-3">
|
||
<h4 className="text-sm font-bold text-slate-800 flex items-center gap-2">
|
||
<BarChart3 className="w-4 h-4 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">
|
||
<AlertCircle className="w-4 h-4 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">
|
||
<Split className="w-4 h-4 text-emerald-600" />
|
||
<div>
|
||
<div className="font-medium">生成分类变量 (Binning)</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">
|
||
<Wand2 className="w-4 h-4 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>
|
||
</>
|
||
)}
|
||
|
||
{/* B. 分类/文本列操作 */}
|
||
{(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">
|
||
<BarChart3 className="w-4 h-4 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')}
|
||
>
|
||
<Settings className="w-4 h-4 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">
|
||
<Wand2 className="w-4 h-4 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>
|
||
)}
|
||
|
||
</div>
|
||
|
||
{/* Modals (Mock) */}
|
||
{showModal === 'calc' && (
|
||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center animate-in fade-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)}><Trash2 className="w-4 h-4 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>
|
||
)}
|
||
|
||
{/* Recode Modal */}
|
||
{showModal === 'recode' && (
|
||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center animate-in fade-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)}><Trash2 className="w-4 h-4 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>
|
||
<ArrowRight className="w-4 h-4 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>
|
||
<ArrowRight className="w-4 h-4 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>
|
||
<ArrowRight className="w-4 h-4 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 fade-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">
|
||
<ArrowLeftRight className="w-5 h-5 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)}><Trash2 className="w-4 h-4 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>
|
||
<ArrowRight className="w-4 h-4 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>
|
||
);
|
||
};
|
||
|
||
export default ToolC_EditorV2; |