Files
AIclinicalresearch/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx
HaHafeng dac3cecf78 feat(iit): Complete IIT Manager Agent Day 1 - Environment initialization and WeChat integration
Summary:
- Complete IIT Manager Agent MVP Day 1 (12.5% progress)
- Database: Create iit_schema with 5 tables (IitProject, IitPendingAction, IitTaskRun, IitUserMapping, IitAuditLog)
- Backend: Add module structure (577 lines) and types (223 lines)
- WeChat: Configure Enterprise WeChat app (CorpID, AgentID, Secret)
- WeChat: Obtain web authorization and JS-SDK authorization
- WeChat: Configure trusted domain (iit.xunzhengyixue.com)
- Frontend: Deploy v1.2 with WeChat domain verification file
- Frontend: Fix CRLF issue in docker-entrypoint.sh (CRLF -> LF)
- Testing: 11/11 database CRUD tests passed
- Testing: Access Token retrieval test passed
- Docs: Create module status and development guide
- Docs: Update MVP task list with Day 1 completion
- Docs: Rename deployment doc to SAE real-time status record
- Deployment: Update frontend internal IP to 172.17.173.80

Technical Details:
- Prisma: Multi-schema support (iit_schema)
- pg-boss: Job queue integration prepared
- Taro 4.x: Framework selected for WeChat Mini Program
- Shadow State: Architecture foundation laid
- Docker: Fix entrypoint script line endings for Linux container

Status: Day 1/14 complete, ready for Day 2 REDCap integration
2026-01-01 14:32:58 +08:00

302 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* Pivot面板长表→宽表
* 从PivotDialog.tsx提取作为TransformDialog的子组件
*/
import React, { useState } from 'react';
import { Select, Button, Alert, Checkbox, Radio, App } from 'antd';
import { ArrowLeftRight, Info } from 'lucide-react';
interface Props {
columns: Array<{ id: string; name: string }>;
sessionId: string | null;
onApply: (newData: any[]) => void;
onClose: () => void;
}
const PivotPanel: React.FC<Props> = ({
columns,
sessionId,
onApply,
onClose,
}) => {
const { message } = App.useApp();
const [indexColumn, setIndexColumn] = useState<string>('');
const [pivotColumn, setPivotColumn] = useState<string>('');
const [valueColumns, setValueColumns] = useState<string[]>([]);
const [aggfunc, setAggfunc] = useState<'first' | 'last' | 'mean' | 'sum'>('first');
const [loading, setLoading] = useState(false);
const [keepUnusedColumns, setKeepUnusedColumns] = useState(false);
const [unusedAggMethod, setUnusedAggMethod] = useState<'first' | 'mode' | 'mean'>('first');
// 执行
const handleApply = async () => {
if (!sessionId) {
message.error('Session ID不存在');
return;
}
// 验证
if (!indexColumn) {
message.warning('请选择索引列(唯一标识列)');
return;
}
if (!pivotColumn) {
message.warning('请选择透视列(要变成列名的列)');
return;
}
if (valueColumns.length === 0) {
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: 'pivot',
params: {
indexColumn,
pivotColumn,
valueColumns,
aggfunc,
keepUnusedColumns,
unusedAggMethod,
},
}),
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Pivot转换失败');
}
message.success('Pivot转换成功');
// 更新父组件数据
if (result.data?.newDataPreview) {
onApply(result.data.newDataPreview);
}
// 成功后关闭
onClose();
} catch (error: any) {
message.error(error.message || '执行失败');
} finally {
setLoading(false);
}
};
return (
<div className="space-y-4 pb-4">
{/* 说明 */}
<Alert
description={
<div className="text-xs space-y-1">
<div> "一人多行""一人一行"</div>
<div> 访线访2访1</div>
<div> Event_Name列的"基线""随访2周" FMA_基线FMA_随访2周</div>
</div>
}
type="info"
showIcon
icon={<Info size={16} />}
className="mb-4"
/>
{/* 索引列 */}
<div>
<label className="text-sm font-medium text-slate-700 mb-1 block">
ID<span className="text-red-500">*</span>
</label>
<Select
placeholder="选择唯一标识列"
value={indexColumn || undefined}
onChange={setIndexColumn}
className="w-full"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={columns.map(col => ({ label: col.name, value: col.id }))}
/>
<div className="text-xs text-slate-500 mt-1">
</div>
</div>
{/* 透视列 */}
<div>
<label className="text-sm font-medium text-slate-700 mb-1 block">
访<span className="text-red-500">*</span>
</label>
<Select
placeholder="选择透视列"
value={pivotColumn || undefined}
onChange={setPivotColumn}
className="w-full"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={columns
.filter(col => col.id !== indexColumn)
.map(col => ({ label: col.name, value: col.id }))}
/>
<div className="text-xs text-slate-500 mt-1">
</div>
</div>
{/* 值列 */}
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">
<span className="text-red-500">*</span>
</label>
<div className="border rounded-lg p-3 max-h-48 overflow-y-auto bg-slate-50">
<Checkbox.Group
value={valueColumns}
onChange={setValueColumns}
className="flex flex-col gap-2"
>
{columns
.filter(col => col.id !== indexColumn && col.id !== pivotColumn)
.map((col) => (
<Checkbox key={col.id} value={col.id}>
{col.name}
</Checkbox>
))}
</Checkbox.Group>
</div>
<div className="text-xs text-slate-500 mt-1">
FMA_基线FMA_随访2周
</div>
</div>
{/* 聚合方式 */}
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">
</label>
<Radio.Group value={aggfunc} onChange={(e) => setAggfunc(e.target.value)}>
<div className="space-y-2">
<Radio value="first">
<div className="ml-2">
<span className="font-medium"></span>
<span className="text-xs text-slate-500 ml-2"></span>
</div>
</Radio>
<Radio value="last">
<span className="ml-2 font-medium"></span>
</Radio>
<Radio value="mean">
<span className="ml-2 font-medium"></span>
</Radio>
<Radio value="sum">
<span className="ml-2 font-medium"></span>
</Radio>
</div>
</Radio.Group>
</div>
{/* 高级选项 */}
<div className="border-t pt-4">
<div className="flex items-center gap-2 mb-3">
<span className="text-sm font-medium text-slate-700"> </span>
</div>
<Checkbox
checked={keepUnusedColumns}
onChange={(e) => setKeepUnusedColumns(e.target.checked)}
>
<span className="text-sm font-medium"></span>
</Checkbox>
{keepUnusedColumns && (
<div className="ml-6 mt-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
<label className="text-sm font-medium text-slate-700 mb-2 block">
</label>
<Radio.Group
value={unusedAggMethod}
onChange={(e) => setUnusedAggMethod(e.target.value)}
>
<div className="space-y-2">
<Radio value="first">
<div className="ml-2">
<span className="font-medium text-sm"></span>
<span className="text-xs text-slate-500 ml-2"></span>
</div>
</Radio>
<Radio value="mode">
<span className="ml-2 font-medium text-sm"></span>
</Radio>
<Radio value="mean">
<div className="ml-2">
<span className="font-medium text-sm"></span>
<span className="text-xs text-slate-500 ml-2"></span>
</div>
</Radio>
</div>
</Radio.Group>
</div>
)}
</div>
{/* 警告 */}
<Alert
description={
<div className="text-xs space-y-1">
<div> Pivot操作会显著改变数据结构</div>
<div> </div>
</div>
}
type="warning"
showIcon={false}
className="bg-orange-50 border-orange-200"
/>
{/* 底部按钮 */}
<div className="flex justify-end gap-2 pt-4 border-t">
<Button onClick={onClose}>
</Button>
<Button
type="primary"
onClick={handleApply}
loading={loading}
icon={<ArrowLeftRight size={16} />}
>
</Button>
</div>
</div>
);
};
export default PivotPanel;