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
This commit is contained in:
2025-12-02 21:53:24 +08:00
parent f240aa9236
commit d4d33528c7
83 changed files with 21863 additions and 1601 deletions

View File

@@ -0,0 +1,246 @@
/**
* DC模块 - 模板服务
*
* 功能:
* - 管理预设提取模板(疾病类型 + 报告类型)
* - 提供模板列表查询
* - Seed初始数据3个预设模板
*
* 平台能力复用:
* - ✅ prisma: 数据库操作
* - ✅ logger: 日志记录
*/
import { prisma } from '../../../../config/database.js';
import { logger } from '../../../../common/logging/index.js';
export interface TemplateField {
name: string;
desc: string;
width?: string; // TailwindCSS class
}
export interface Template {
id: string;
diseaseType: string;
reportType: string;
displayName: string;
fields: TemplateField[];
promptTemplate: string;
}
export class TemplateService {
/**
* 获取所有模板
*/
async getAllTemplates(): Promise<Template[]> {
try {
logger.info('[Template] Fetching all templates');
const templates = await prisma.dCTemplate.findMany({
orderBy: [{ diseaseType: 'asc' }, { reportType: 'asc' }]
});
logger.info('[Template] Templates fetched', { count: templates.length });
return templates.map(t => ({
id: t.id,
diseaseType: t.diseaseType,
reportType: t.reportType,
displayName: t.displayName,
fields: t.fields as TemplateField[],
promptTemplate: t.promptTemplate
}));
} catch (error) {
logger.error('[Template] Failed to fetch templates', { error });
throw error;
}
}
/**
* 根据疾病和报告类型获取模板
*/
async getTemplate(diseaseType: string, reportType: string): Promise<Template | null> {
try {
logger.info('[Template] Fetching template', { diseaseType, reportType });
const template = await prisma.dCTemplate.findUnique({
where: {
diseaseType_reportType: { diseaseType, reportType }
}
});
if (!template) {
logger.warn('[Template] Template not found', { diseaseType, reportType });
return null;
}
return {
id: template.id,
diseaseType: template.diseaseType,
reportType: template.reportType,
displayName: template.displayName,
fields: template.fields as TemplateField[],
promptTemplate: template.promptTemplate
};
} catch (error) {
logger.error('[Template] Failed to fetch template', { error, diseaseType, reportType });
throw error;
}
}
/**
* 初始化预设模板Seed数据
*
* 包括3个预设模板
* 1. 肺癌病理报告
* 2. 糖尿病入院记录
* 3. 高血压门诊病历
*/
async seedTemplates(): Promise<void> {
try {
logger.info('[Template] Seeding templates');
const templates = [
// 1. 肺癌病理报告
{
diseaseType: 'lung_cancer',
reportType: 'pathology',
displayName: '肺癌病理报告',
fields: [
{ name: '病理类型', desc: '如:浸润性腺癌、鳞状细胞癌', width: 'w-40' },
{ name: '分化程度', desc: '高/中/低分化', width: 'w-32' },
{ name: '肿瘤大小', desc: '最大径单位cm', width: 'w-32' },
{ name: '淋巴结转移', desc: '有/无及具体组别', width: 'w-48' },
{ name: '免疫组化', desc: '关键指标', width: 'w-56' }
],
promptTemplate: `你是一名病理学专家。请从以下肺癌病理报告中提取关键信息。
提取字段(必须返回以下所有字段):
- 病理类型:病理诊断类型(如浸润性腺癌、鳞状细胞癌)
- 分化程度:分化等级(高分化、中分化、低分化、未提及)
- 肿瘤大小肿瘤最大径单位cm
- 淋巴结转移:淋巴结转移情况(有/无及具体组别)
- 免疫组化:关键免疫组化指标
**输出格式严格的JSON格式不要有任何额外文本**
\`\`\`json
{
"病理类型": "...",
"分化程度": "...",
"肿瘤大小": "...",
"淋巴结转移": "...",
"免疫组化": "..."
}
\`\`\`
如果某个信息未在报告中提及,请填写"未提及"。`
},
// 2. 糖尿病入院记录
{
diseaseType: 'diabetes',
reportType: 'admission',
displayName: '糖尿病入院记录',
fields: [
{ name: '主诉', desc: '患者入院的主要症状', width: 'w-48' },
{ name: '现病史', desc: '发病过程', width: 'w-64' },
{ name: '既往史', desc: '糖尿病病史年限', width: 'w-40' },
{ name: '空腹血糖', desc: '单位mmol/L', width: 'w-32' },
{ name: '糖化血红蛋白', desc: '单位%', width: 'w-32' }
],
promptTemplate: `你是一名内分泌科专家。请从以下糖尿病患者入院记录中提取关键信息。
提取字段(必须返回以下所有字段):
- 主诉:患者入院时的主要症状
- 现病史:本次发病的过程和表现
- 既往史:糖尿病病史年限
- 空腹血糖最近的空腹血糖值单位mmol/L
- 糖化血红蛋白最近的HbA1c值单位%
**输出格式严格的JSON格式**
\`\`\`json
{
"主诉": "...",
"现病史": "...",
"既往史": "...",
"空腹血糖": "...",
"糖化血红蛋白": "..."
}
\`\`\`
如果某个信息未在记录中提及,请填写"未提及"。`
},
// 3. 高血压门诊病历
{
diseaseType: 'hypertension',
reportType: 'outpatient',
displayName: '高血压门诊病历',
fields: [
{ name: '血压值', desc: '单位mmHg', width: 'w-32' },
{ name: '心率', desc: '单位次/分', width: 'w-24' },
{ name: '当前用药', desc: '高血压药物', width: 'w-56' },
{ name: '靶器官损害', desc: '心/脑/肾', width: 'w-40' },
{ name: '危险分层', desc: '低/中/高/极高危', width: 'w-32' }
],
promptTemplate: `你是一名心内科专家。请从以下高血压患者门诊病历中提取关键信息。
提取字段(必须返回以下所有字段):
- 血压值:收缩压/舒张压单位mmHg
- 心率:心率(单位次/分)
- 当前用药:患者当前服用的高血压药物
- 靶器官损害:心脏、脑、肾脏等靶器官损害情况
- 危险分层:心血管风险分层(低危、中危、高危、极高危)
**输出格式严格的JSON格式**
\`\`\`json
{
"血压值": "...",
"心率": "...",
"当前用药": "...",
"靶器官损害": "...",
"危险分层": "..."
}
\`\`\`
如果某个信息未在病历中提及,请填写"未提及"。`
}
];
// 使用upsert避免重复
for (const template of templates) {
await prisma.dCTemplate.upsert({
where: {
diseaseType_reportType: {
diseaseType: template.diseaseType,
reportType: template.reportType
}
},
update: {
displayName: template.displayName,
fields: template.fields,
promptTemplate: template.promptTemplate
},
create: template
});
}
logger.info('[Template] Templates seeded successfully', { count: templates.length });
} catch (error) {
logger.error('[Template] Failed to seed templates', { error });
throw error;
}
}
}
// 导出单例
export const templateService = new TemplateService();