feat: ai生成病例编辑

This commit is contained in:
王天骄
2026-06-15 17:28:35 +08:00
parent 3c2eb0a517
commit 9deadf2083
11 changed files with 1353 additions and 135 deletions
+545 -75
View File
@@ -267,6 +267,7 @@
<el-input v-model="item.item_code" placeholder="项目编码,必须唯一" />
<el-input v-model="item.item_name" placeholder="项目名称" />
<el-input v-model="item.item_type" placeholder="项目类型" />
<el-input v-model="item.result_text" placeholder="结果文本" />
<el-button :icon="Delete" circle @click="removeExamItem(index)" />
</div>
</div>
@@ -278,6 +279,14 @@
</el-dialog>
<el-dialog v-model="importDialogVisible" title="PDF 导入病例" width="560px" @closed="resetImportFile">
<el-form class="case-import-form" label-position="top">
<el-form-item label="病例类型">
<el-select v-model="importCaseType" placeholder="请选择病例类型">
<el-option label="传统病例" value="traditional" />
<el-option label="教学互动病例" value="teaching" />
</el-select>
</el-form-item>
</el-form>
<el-upload
ref="importUploadRef"
v-model:file-list="importFileList"
@@ -355,20 +364,177 @@
</template>
</el-dialog>
<el-drawer v-model="detailDrawerVisible" :title="detailDrawerTitle" size="50%" destroy-on-close>
<el-drawer v-model="detailDrawerVisible" :title="detailDrawerTitle" size="72%" destroy-on-close @closed="resetDetailForm">
<div v-loading="detailLoading" class="case-detail-drawer">
<el-descriptions v-if="detailCase" :column="2" border>
<el-descriptions-item label="ID">{{ detailDisplay.id }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ caseTypeLabel(detailDisplay.caseType) }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ publishStatusLabel(detailCase.publishStatus) }}</el-descriptions-item>
<el-descriptions-item label="难度">{{ detailDisplay.difficulty }}</el-descriptions-item>
<el-descriptions-item v-if="canManageInstitution" label="机构">{{ detailDisplay.institution }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ detailDisplay.department }}</el-descriptions-item>
<el-descriptions-item label="患者">{{ detailDisplay.patient }}</el-descriptions-item>
<el-descriptions-item label="预计时长">{{ detailDisplay.estimatedMinutes }}</el-descriptions-item>
<el-descriptions-item label="主诉" :span="2">{{ detailDisplay.chiefComplaint }}</el-descriptions-item>
<el-descriptions-item label="描述" :span="2">{{ detailDisplay.description }}</el-descriptions-item>
</el-descriptions>
<el-form
v-if="detailCase"
ref="detailFormRef"
class="case-create-form case-detail-form"
:model="detailForm"
:rules="caseRules"
label-position="top"
>
<div class="case-form-section">
<div class="case-section-title">
<h3>基础信息</h3>
<el-tag :type="publishStatusTagType(detailCase.publishStatus)">ID {{ detailCase.id }} / {{ publishStatusLabel(detailCase.publishStatus) }}</el-tag>
</div>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="病例标题" prop="title">
<el-input v-model="detailForm.title" placeholder="请输入病例标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="病例类型" prop="case_type">
<el-select v-model="detailForm.case_type" placeholder="请选择病例类型">
<el-option label="传统病例" value="traditional" />
<el-option label="教学病例" value="teaching" />
</el-select>
</el-form-item>
</el-col>
<el-col v-if="canManageInstitution" :span="8">
<el-form-item label="机构ID">
<el-input-number v-model="detailForm.institution_id" :min="1" :precision="0" :controls="false" placeholder="缺省落创建者机构" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="科室名称">
<el-input v-model="detailForm.department_name" placeholder="请输入科室名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="难度">
<el-select v-model="detailForm.difficulty" allow-create clearable filterable default-first-option placeholder="请选择或输入难度">
<el-option label="低" value="低" />
<el-option label="中" value="中" />
<el-option label="高" value="高" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主诉">
<el-input v-model="detailForm.chief_complaint" placeholder="请输入主诉" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="患者年龄">
<el-input-number v-model="detailForm.patient_age" :min="0" :max="130" :precision="0" :controls="false" placeholder="年龄" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="患者性别">
<el-select v-model="detailForm.patient_gender" clearable placeholder="请选择">
<el-option label="男" value="male" />
<el-option label="女" value="female" />
<el-option label="未知" value="unknown" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="标签">
<el-input v-model="detailForm.tags" placeholder="多个用逗号分隔" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="ICD 编码">
<el-input v-model="detailForm.icd_codes" placeholder="多个用逗号分隔" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="预计分钟">
<el-input-number v-model="detailForm.estimated_minutes" :min="1" :precision="0" :controls="false" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="OSCE">
<el-switch v-model="detailForm.osce_enabled" active-text="开启" inactive-text="关闭" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="detailForm.description" type="textarea" :rows="3" placeholder="请输入病例背景或教学说明" />
</el-form-item>
</el-col>
</el-row>
</div>
<div class="case-form-section">
<h3>{{ caseTypeLabel(detailForm.case_type) }}内容</h3>
<template v-if="detailForm.case_type === 'traditional'">
<el-row :gutter="14">
<el-col :span="8">
<el-form-item label="标准诊断" prop="traditional_standard_diagnosis">
<el-input v-model="detailForm.traditional_standard_diagnosis" placeholder="如:上呼吸道感染" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="标准治疗">
<el-input v-model="detailForm.traditional_standard_treatment" placeholder="如:对症治疗,退热处理" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="指南依据">
<el-input v-model="detailForm.traditional_guideline_reference" placeholder="如:《儿科学》第 9 版" />
</el-form-item>
</el-col>
</el-row>
</template>
<template v-else>
<el-row :gutter="14">
<el-col :span="8">
<el-form-item label="教学目标" prop="teaching_learning_objectives">
<el-input v-model="detailForm.teaching_learning_objectives" placeholder="请输入教学目标" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="教学重点">
<el-input v-model="detailForm.teaching_key_points" placeholder="请输入教学重点" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="参考答案">
<el-input v-model="detailForm.teaching_reference_answer" placeholder="请输入参考答案" />
</el-form-item>
</el-col>
</el-row>
</template>
</div>
<div class="case-form-section">
<div class="case-section-title">
<h3>评分规则</h3>
<el-button :icon="Plus" @click="addDetailScoringRule">添加规则</el-button>
</div>
<div v-for="(rule, index) in detailForm.scoring_rules" :key="`detail-rule-${index}`" class="scoring-rule-row">
<el-input v-model="rule.dimension" placeholder="维度" />
<el-input-number v-model="rule.score_weight" :min="0.01" :max="1" :step="0.05" :precision="2" :controls="false" placeholder="权重" />
<el-switch v-model="rule.ai_auto_score" active-text="AI评分" inactive-text="人工" />
<el-input v-model="rule.scoring_standard" placeholder="评分标准" />
<el-button :icon="Delete" :disabled="detailForm.scoring_rules.length === 1" circle @click="removeDetailScoringRule(index)" />
</div>
</div>
<div class="case-form-section">
<div class="case-section-title">
<h3>检查/检验项目</h3>
<el-button :icon="Plus" @click="addDetailExamItem">添加项目</el-button>
</div>
<div v-if="!detailForm.exam_items.length" class="case-empty-line">未添加项目可直接提交</div>
<div v-for="(item, index) in detailForm.exam_items" :key="`detail-item-${index}`" class="exam-item-row">
<el-input v-model="item.item_code" placeholder="项目编码,必须唯一" />
<el-input v-model="item.item_name" placeholder="项目名称" />
<el-input v-model="item.item_type" placeholder="项目类型" />
<el-input v-model="item.result_text" placeholder="结果文本" />
<el-button :icon="Delete" circle @click="removeDetailExamItem(index)" />
</div>
</div>
</el-form>
<div class="drawer-form-footer">
<el-button @click="detailDrawerVisible = false">取消</el-button>
<el-button :loading="submittingDetail" type="primary" @click="submitDetailForm">提交</el-button>
</div>
</div>
</el-drawer>
</div>
@@ -388,6 +554,7 @@ import {
submitCase,
updateCaseRelations,
type CaseExamItemPayload,
type ImportCasePdfResult,
type CaseListItem,
type CasePublishStatus,
type CaseScoringRulePayload,
@@ -411,6 +578,7 @@ interface ExamItemForm {
item_code: string
item_name: string
item_type: string
result_text: string
}
interface CaseDraftForm {
@@ -443,13 +611,16 @@ const savingCase = ref(false)
const importing = ref(false)
const savingRelations = ref(false)
const detailLoading = ref(false)
const submittingDetail = ref(false)
const caseDialogVisible = ref(false)
const importDialogVisible = ref(false)
const relationsDialogVisible = ref(false)
const detailDrawerVisible = ref(false)
const caseFormRef = ref<FormInstance>()
const detailFormRef = ref<FormInstance>()
const importUploadRef = ref<UploadInstance>()
const importFile = ref<File | null>(null)
const importCaseType = ref<DraftCaseType>('traditional')
const importFileList = ref<UploadUserFile[]>([])
const cases = ref<CaseListItem[]>([])
const relationsCase = ref<CaseListItem | null>(null)
@@ -505,6 +676,30 @@ const caseForm = reactive<CaseDraftForm>({
exam_items: []
})
const detailForm = reactive<CaseDraftForm>({
title: '',
case_type: 'traditional',
institution_id: undefined,
department_name: '',
difficulty: '',
chief_complaint: '',
description: '',
patient_age: undefined,
patient_gender: '',
tags: '',
icd_codes: '',
estimated_minutes: undefined,
osce_enabled: false,
traditional_standard_diagnosis: '',
traditional_standard_treatment: '',
traditional_guideline_reference: '',
teaching_learning_objectives: '',
teaching_key_points: '',
teaching_reference_answer: '',
scoring_rules: [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }],
exam_items: []
})
const relationsForm = reactive<{
institutionMode: RelationInstitutionMode
institution_id?: number
@@ -610,30 +805,41 @@ function openCreateDialog() {
}
function resetCaseForm() {
caseForm.title = ''
caseForm.case_type = 'traditional'
caseForm.institution_id = undefined
caseForm.department_name = ''
caseForm.difficulty = ''
caseForm.chief_complaint = ''
caseForm.description = ''
caseForm.patient_age = undefined
caseForm.patient_gender = ''
caseForm.tags = ''
caseForm.icd_codes = ''
caseForm.estimated_minutes = undefined
caseForm.osce_enabled = false
caseForm.traditional_standard_diagnosis = ''
caseForm.traditional_standard_treatment = ''
caseForm.traditional_guideline_reference = ''
caseForm.teaching_learning_objectives = ''
caseForm.teaching_key_points = ''
caseForm.teaching_reference_answer = ''
caseForm.scoring_rules = [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }]
caseForm.exam_items = []
resetDraftForm(caseForm)
caseFormRef.value?.clearValidate()
}
function resetDetailForm() {
detailCase.value = null
caseDetail.value = null
resetDraftForm(detailForm)
detailFormRef.value?.clearValidate()
}
function resetDraftForm(form: CaseDraftForm) {
form.title = ''
form.case_type = 'traditional'
form.institution_id = undefined
form.department_name = ''
form.difficulty = ''
form.chief_complaint = ''
form.description = ''
form.patient_age = undefined
form.patient_gender = ''
form.tags = ''
form.icd_codes = ''
form.estimated_minutes = undefined
form.osce_enabled = false
form.traditional_standard_diagnosis = ''
form.traditional_standard_treatment = ''
form.traditional_guideline_reference = ''
form.teaching_learning_objectives = ''
form.teaching_key_points = ''
form.teaching_reference_answer = ''
form.scoring_rules = [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }]
form.exam_items = []
}
function addScoringRule() {
caseForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' })
}
@@ -647,13 +853,33 @@ function removeScoringRule(index: number) {
}
function addExamItem() {
caseForm.exam_items.push({ item_code: '', item_name: '', item_type: '' })
caseForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
}
function removeExamItem(index: number) {
caseForm.exam_items.splice(index, 1)
}
function addDetailScoringRule() {
detailForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' })
}
function removeDetailScoringRule(index: number) {
if (detailForm.scoring_rules.length <= 1) {
return
}
detailForm.scoring_rules.splice(index, 1)
}
function addDetailExamItem() {
detailForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
}
function removeDetailExamItem(index: number) {
detailForm.exam_items.splice(index, 1)
}
async function submitCaseForm() {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
@@ -691,60 +917,64 @@ async function submitCaseForm() {
}
function buildCreatePayload(): CreateCaseDraftPayload {
const structure = buildCaseStructure()
const scoringRules = normalizeScoringRules()
const examItems = normalizeExamItems()
return buildDraftPayload(caseForm)
}
function buildDraftPayload(form: CaseDraftForm): CreateCaseDraftPayload {
const structure = buildCaseStructure(form)
const scoringRules = normalizeScoringRules(form)
const examItems = normalizeExamItems(form)
const payload: CreateCaseDraftPayload = {
title: caseForm.title.trim(),
case_type: caseForm.case_type,
title: form.title.trim(),
case_type: form.case_type,
scoring_rules: scoringRules
}
payload[caseForm.case_type] = structure
if (canManageInstitution.value && caseForm.institution_id) payload.institution_id = caseForm.institution_id
if (caseForm.department_name.trim()) payload.department_name = caseForm.department_name.trim()
if (caseForm.difficulty.trim()) payload.difficulty = caseForm.difficulty.trim()
if (caseForm.chief_complaint.trim()) payload.chief_complaint = caseForm.chief_complaint.trim()
if (caseForm.description.trim()) payload.description = caseForm.description.trim()
if (caseForm.patient_age !== undefined) payload.patient_age = caseForm.patient_age
if (caseForm.patient_gender) payload.patient_gender = caseForm.patient_gender
if (caseForm.tags.trim()) {
payload.tags = isContentAdmin.value ? caseForm.tags.trim() : parseTextList(caseForm.tags)
payload[form.case_type] = structure
if (canManageInstitution.value && form.institution_id) payload.institution_id = form.institution_id
if (form.department_name.trim()) payload.department_name = form.department_name.trim()
if (form.difficulty.trim()) payload.difficulty = form.difficulty.trim()
if (form.chief_complaint.trim()) payload.chief_complaint = form.chief_complaint.trim()
if (form.description.trim()) payload.description = form.description.trim()
if (form.patient_age !== undefined) payload.patient_age = form.patient_age
if (form.patient_gender) payload.patient_gender = form.patient_gender
if (form.tags.trim()) {
payload.tags = isContentAdmin.value ? form.tags.trim() : parseTextList(form.tags)
}
if (parseTextList(caseForm.icd_codes).length) payload.icd_codes = parseTextList(caseForm.icd_codes)
if (caseForm.estimated_minutes !== undefined) payload.estimated_minutes = caseForm.estimated_minutes
if (caseForm.osce_enabled) payload.osce_enabled = caseForm.osce_enabled
if (parseTextList(form.icd_codes).length) payload.icd_codes = parseTextList(form.icd_codes)
if (form.estimated_minutes !== undefined) payload.estimated_minutes = form.estimated_minutes
if (form.osce_enabled) payload.osce_enabled = form.osce_enabled
if (examItems.length) payload.exam_items = examItems
return payload
}
function buildCaseStructure(): Record<string, unknown> {
if (caseForm.case_type === 'teaching') {
if (!caseForm.teaching_learning_objectives.trim()) {
function buildCaseStructure(form: CaseDraftForm): Record<string, unknown> {
if (form.case_type === 'teaching') {
if (!form.teaching_learning_objectives.trim()) {
throw new Error('请输入教学目标')
}
return {
learning_objectives: caseForm.teaching_learning_objectives.trim(),
...(caseForm.teaching_key_points.trim() ? { key_points: caseForm.teaching_key_points.trim() } : {}),
...(caseForm.teaching_reference_answer.trim() ? { reference_answer: caseForm.teaching_reference_answer.trim() } : {})
learning_objectives: form.teaching_learning_objectives.trim(),
...(form.teaching_key_points.trim() ? { key_points: form.teaching_key_points.trim() } : {}),
...(form.teaching_reference_answer.trim() ? { reference_answer: form.teaching_reference_answer.trim() } : {})
}
}
if (!caseForm.traditional_standard_diagnosis.trim()) {
if (!form.traditional_standard_diagnosis.trim()) {
throw new Error('请输入标准诊断')
}
return {
standard_diagnosis: caseForm.traditional_standard_diagnosis.trim(),
...(caseForm.traditional_standard_treatment.trim() ? { standard_treatment: caseForm.traditional_standard_treatment.trim() } : {}),
...(caseForm.traditional_guideline_reference.trim() ? { guideline_reference: caseForm.traditional_guideline_reference.trim() } : {})
standard_diagnosis: form.traditional_standard_diagnosis.trim(),
...(form.traditional_standard_treatment.trim() ? { standard_treatment: form.traditional_standard_treatment.trim() } : {}),
...(form.traditional_guideline_reference.trim() ? { guideline_reference: form.traditional_guideline_reference.trim() } : {})
}
}
function normalizeScoringRules(): CaseScoringRulePayload[] {
const rules = caseForm.scoring_rules
function normalizeScoringRules(form: CaseDraftForm): CaseScoringRulePayload[] {
const rules = form.scoring_rules
.map(rule => ({
dimension: rule.dimension.trim(),
score_weight: Number(rule.score_weight),
@@ -774,14 +1004,15 @@ function normalizeScoringRules(): CaseScoringRulePayload[] {
}))
}
function normalizeExamItems(): CaseExamItemPayload[] {
const items = caseForm.exam_items
function normalizeExamItems(form: CaseDraftForm): CaseExamItemPayload[] {
const items = form.exam_items
.map(item => ({
item_code: item.item_code.trim(),
item_name: item.item_name.trim(),
item_type: item.item_type.trim()
item_type: item.item_type.trim(),
result_text: item.result_text.trim()
}))
.filter(item => item.item_code || item.item_name || item.item_type)
.filter(item => item.item_code || item.item_name || item.item_type || item.result_text)
const codes = new Set<string>()
for (const item of items) {
@@ -797,7 +1028,8 @@ function normalizeExamItems(): CaseExamItemPayload[] {
return items.map(item => ({
item_code: item.item_code,
...(item.item_name ? { item_name: item.item_name } : {}),
...(item.item_type ? { item_type: item.item_type } : {})
...(item.item_type ? { item_type: item.item_type } : {}),
...(item.result_text ? { result_text: item.result_text } : {})
}))
}
@@ -807,6 +1039,7 @@ function openImportDialog() {
function resetImportFile() {
importFile.value = null
importCaseType.value = 'traditional'
importFileList.value = []
importUploadRef.value?.clearFiles()
}
@@ -842,14 +1075,15 @@ async function submitImportPdf() {
try {
importing.value = true
await importCasePdf({
const result = await importCasePdf({
token: appStore.token,
file: importFile.value
file: importFile.value,
case_type: importCaseType.value
})
ElMessage.success('PDF 病例导入完成')
fillCaseFormFromImportedPdf(result)
importDialogVisible.value = false
pagination.page = 1
await loadCases()
caseDialogVisible.value = true
ElMessage.success('PDF 病例导入完成,请确认后保存草稿')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '导入 PDF 病例失败')
} finally {
@@ -1001,6 +1235,7 @@ async function openDetailDrawer(row: CaseListItem) {
token: appStore.token,
id: row.id
})
fillDetailForm(row, caseDetail.value)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取病例详情失败')
} finally {
@@ -1008,6 +1243,42 @@ async function openDetailDrawer(row: CaseListItem) {
}
}
async function submitDetailForm() {
if (!appStore.token || !detailCase.value) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
const isValid = await detailFormRef.value?.validate().catch(() => false)
if (!isValid) {
return
}
let payload: CreateCaseDraftPayload
try {
payload = buildDraftPayload(detailForm)
} catch (error) {
ElMessage.warning(error instanceof Error ? error.message : '请检查病例表单')
return
}
try {
submittingDetail.value = true
await submitCase({
token: appStore.token,
id: detailCase.value.id,
payload
})
ElMessage.success('病例已提交')
detailDrawerVisible.value = false
await loadCases()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '提交病例失败')
} finally {
submittingDetail.value = false
}
}
function parseTextList(value: string): string[] {
return value
.split(/[,\n,;;]/)
@@ -1015,6 +1286,205 @@ function parseTextList(value: string): string[] {
.filter(Boolean)
}
function fillCaseFormFromImportedPdf(result: ImportCasePdfResult) {
resetDraftForm(caseForm)
const record = getImportRecord(result.data)
const caseType = normalizeImportCaseType(getImportString(record, ['case_type', 'caseType']), result.caseType)
const traditional = getImportRecord(getImportFirst(record, ['traditional']))
const teaching = getImportRecord(getImportFirst(record, ['teaching']))
const scoringRules = getImportScoringRules(record)
const examItems = getImportExamItems(record)
caseForm.case_type = caseType
caseForm.title = getImportString(record, ['title', 'name', 'case_title', 'caseTitle'])
caseForm.department_name = getImportString(record, ['department_name', 'departmentName'])
caseForm.difficulty = getImportString(record, ['difficulty'])
caseForm.chief_complaint = getImportString(record, ['chief_complaint', 'chiefComplaint'])
caseForm.description = getImportString(record, ['description', 'summary', 'content'])
caseForm.patient_age = getImportNumber(record, ['patient_age', 'patientAge']) ?? undefined
caseForm.patient_gender = normalizeImportGender(getImportString(record, ['patient_gender', 'patientGender']))
caseForm.tags = getImportStringList(record, ['tags', 'tag_list', 'tagList']).join(', ')
caseForm.icd_codes = getImportStringList(record, ['icd_codes', 'icdCodes', 'icd_list', 'icdList']).join(', ')
caseForm.estimated_minutes = getImportNumber(record, ['estimated_minutes', 'estimatedMinutes']) ?? undefined
caseForm.osce_enabled = getImportBoolean(record, ['osce_enabled', 'osceEnabled'])
caseForm.traditional_standard_diagnosis = getImportString(traditional, ['standard_diagnosis', 'standardDiagnosis'])
caseForm.traditional_standard_treatment = getImportString(traditional, ['standard_treatment', 'standardTreatment'])
caseForm.traditional_guideline_reference = getImportString(traditional, ['guideline_reference', 'guidelineReference'])
caseForm.teaching_learning_objectives = getImportString(teaching, ['learning_objectives', 'learningObjectives'])
caseForm.teaching_key_points = getImportString(teaching, ['key_points', 'keyPoints'])
caseForm.teaching_reference_answer = getImportString(teaching, ['reference_answer', 'referenceAnswer'])
caseForm.scoring_rules = scoringRules.length
? scoringRules
: [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }]
caseForm.exam_items = examItems
caseFormRef.value?.clearValidate()
}
function fillDetailForm(row: CaseListItem, fullData: unknown) {
resetDraftForm(detailForm)
const record = getDetailRecord(fullData)
const traditional = getImportRecord(getImportFirst(record, ['traditional']))
const teaching = getImportRecord(getImportFirst(record, ['teaching']))
const scoringRules = getImportScoringRules(record)
const examItems = getImportExamItems(record)
detailForm.title = getImportString(record, ['title', 'name', 'case_title', 'caseTitle'], row.title)
detailForm.case_type = normalizeImportCaseType(getImportString(record, ['case_type', 'caseType'], row.caseType), normalizeImportCaseType(row.caseType, 'traditional'))
detailForm.institution_id = getImportNumber(record, ['institution_id', 'institutionId']) ?? undefined
detailForm.department_name = getImportString(record, ['department_name', 'departmentName'], row.departmentName)
detailForm.difficulty = getImportString(record, ['difficulty'], row.difficulty)
detailForm.chief_complaint = getImportString(record, ['chief_complaint', 'chiefComplaint'], row.chiefComplaint)
detailForm.description = getImportString(record, ['description', 'summary', 'content'])
detailForm.patient_age = getImportNumber(record, ['patient_age', 'patientAge']) ?? undefined
detailForm.patient_gender = normalizeImportGender(getImportString(record, ['patient_gender', 'patientGender']))
detailForm.tags = getImportStringList(record, ['tags', 'tag_list', 'tagList']).join(', ')
detailForm.icd_codes = getImportStringList(record, ['icd_codes', 'icdCodes', 'icd_list', 'icdList']).join(', ')
detailForm.estimated_minutes = getImportNumber(record, ['estimated_minutes', 'estimatedMinutes']) ?? row.estimatedMinutes ?? undefined
detailForm.osce_enabled = getImportBoolean(record, ['osce_enabled', 'osceEnabled'])
detailForm.traditional_standard_diagnosis = getImportString(traditional, ['standard_diagnosis', 'standardDiagnosis'])
detailForm.traditional_standard_treatment = getImportString(traditional, ['standard_treatment', 'standardTreatment'])
detailForm.traditional_guideline_reference = getImportString(traditional, ['guideline_reference', 'guidelineReference'])
detailForm.teaching_learning_objectives = getImportString(teaching, ['learning_objectives', 'learningObjectives'])
detailForm.teaching_key_points = getImportString(teaching, ['key_points', 'keyPoints'])
detailForm.teaching_reference_answer = getImportString(teaching, ['reference_answer', 'referenceAnswer'])
detailForm.scoring_rules = scoringRules.length
? scoringRules
: [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }]
detailForm.exam_items = examItems
detailFormRef.value?.clearValidate()
}
function getImportRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {}
}
function getImportFirst(record: Record<string, unknown>, keys: string[]): unknown {
for (const key of keys) {
if (record[key] !== undefined && record[key] !== null) {
return record[key]
}
}
return undefined
}
function getImportString(record: Record<string, unknown>, keys: string[], fallback = ''): string {
for (const key of keys) {
const value = record[key]
if (typeof value === 'string' && value.trim()) {
return value.trim()
}
if (typeof value === 'number') {
return String(value)
}
}
return fallback
}
function getImportNumber(record: Record<string, unknown>, keys: string[]): number | null {
const value = getImportFirst(record, keys)
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
return Number(value)
}
return null
}
function getImportBoolean(record: Record<string, unknown>, keys: string[]) {
const value = getImportFirst(record, keys)
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'number') {
return value === 1
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase()
return ['true', '1', 'yes', 'y', 'on', '开启', '启用', '是'].includes(normalized)
}
return false
}
function getImportStringList(record: Record<string, unknown>, keys: string[]): string[] {
const value = getImportFirst(record, keys)
if (Array.isArray(value)) {
return value
.map(item => {
if (typeof item === 'string' || typeof item === 'number') {
return String(item).trim()
}
const itemRecord = getImportRecord(item)
return getImportString(itemRecord, ['name', 'title', 'code', 'value'])
})
.filter(Boolean)
}
if (typeof value === 'string') {
return parseTextList(value)
}
return []
}
function getImportScoringRules(record: Record<string, unknown>): ScoringRuleForm[] {
const raw = getImportFirst(record, ['scoring_rules', 'scoringRules'])
if (!Array.isArray(raw)) {
return []
}
return raw
.map(item => {
const itemRecord = getImportRecord(item)
const scoreWeight = getImportNumber(itemRecord, ['score_weight', 'scoreWeight', 'weight'])
const rawAutoScore = getImportFirst(itemRecord, ['ai_auto_score', 'aiAutoScore'])
return {
dimension: getImportString(itemRecord, ['dimension', 'name']),
score_weight: scoreWeight ?? 1,
ai_auto_score: rawAutoScore === undefined ? true : getImportBoolean(itemRecord, ['ai_auto_score', 'aiAutoScore']),
scoring_standard: getImportString(itemRecord, ['scoring_standard', 'scoringStandard', 'description'])
}
})
.filter(item => item.dimension || item.scoring_standard)
}
function getImportExamItems(record: Record<string, unknown>): ExamItemForm[] {
const raw = getImportFirst(record, ['exam_items', 'examItems'])
if (!Array.isArray(raw)) {
return []
}
return raw
.map(item => {
const itemRecord = getImportRecord(item)
return {
item_code: getImportString(itemRecord, ['item_code', 'itemCode', 'code']),
item_name: getImportString(itemRecord, ['item_name', 'itemName', 'name']),
item_type: getImportString(itemRecord, ['item_type', 'itemType', 'type']),
result_text: getImportString(itemRecord, ['result_text', 'resultText'])
}
})
.filter(item => item.item_code || item.item_name || item.item_type || item.result_text)
}
function normalizeImportCaseType(value: string, fallback: DraftCaseType): DraftCaseType {
return value === 'teaching' ? 'teaching' : fallback
}
function normalizeImportGender(value: string) {
if (value === '男') return 'male'
if (value === '女') return 'female'
if (value === '未知') return 'unknown'
return ['male', 'female', 'unknown'].includes(value) ? value : ''
}
function createDetailDisplay(row: CaseListItem | null, fullData: unknown) {
const record = getDetailRecord(fullData)
const title = getDetailString(record, ['title', 'name']) || row?.title || '-'