feat: ai生成病例编辑
This commit is contained in:
+545
-75
@@ -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 || '-'
|
||||
|
||||
Reference in New Issue
Block a user