Files
cms/src/views/CasesView.vue
T
2026-06-17 10:25:23 +08:00

1805 lines
67 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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.
<template>
<div class="page-stack">
<section class="page-toolbar">
<div>
<h1>病例库</h1>
<p>{{ pageDescription }}</p>
</div>
<div class="toolbar-actions">
<el-button :icon="Upload" @click="openImportDialog">PDF 导入</el-button>
<el-button :icon="Plus" type="primary" @click="openCreateDialog">新增草稿</el-button>
</div>
</section>
<section class="filter-bar case-filter" :class="{ 'content-case-filter': isContentAdmin }">
<el-input v-model="filters.search" :prefix-icon="Search" clearable :placeholder="searchPlaceholder" @keyup.enter="loadCases" />
<el-select v-model="filters.case_type" clearable placeholder="病例类型">
<el-option label="传统病例" value="traditional" />
<el-option label="教学病例" value="teaching" />
</el-select>
<el-select v-model="filters.publish_status" clearable placeholder="发布状态">
<el-option label="草稿" :value="0" />
<el-option label="正常" :value="1" />
<el-option label="已发布" :value="2" />
</el-select>
<el-input v-if="canManageInstitution" v-model="filters.institution" clearable placeholder="机构ID" @keyup.enter="loadCases" />
<template v-else>
<el-input v-model="filters.department" clearable placeholder="科室ID" @keyup.enter="loadCases" />
<el-input v-model="filters.status" clearable placeholder="状态" @keyup.enter="loadCases" />
<el-select v-model="filters.osce_enabled" clearable placeholder="OSCE">
<el-option label="开启" value="true" />
<el-option label="关闭" value="false" />
</el-select>
<el-select v-model="filters.ordering" clearable placeholder="排序">
<el-option label="最近更新" value="-updated_at" />
<el-option label="最早更新" value="updated_at" />
<el-option label="最近创建" value="-created_at" />
<el-option label="最早创建" value="created_at" />
</el-select>
</template>
<el-button :icon="Search" type="primary" @click="handleSearch">查询</el-button>
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
</section>
<section class="data-section">
<el-table v-loading="loading" :data="cases" empty-text="暂无病例数据" row-key="id">
<el-table-column prop="id" label="ID" width="90" />
<el-table-column prop="title" label="病例标题" min-width="220">
<template #default="{ row }">
<strong>{{ row.title }}</strong>
<span v-if="row.chiefComplaint" class="table-subtext">{{ row.chiefComplaint }}</span>
</template>
</el-table-column>
<el-table-column label="类型" width="110">
<template #default="{ row }">
<el-tag>{{ caseTypeLabel(row.caseType) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="canManageInstitution ? '机构/科室' : '科室'" min-width="180">
<template #default="{ row }">
<template v-if="canManageInstitution">
<span>{{ row.institutionName || row.institutionId || '-' }}</span>
<span class="table-subtext">{{ row.departmentName || row.departmentId || '未关联科室' }}</span>
</template>
<span v-else>{{ row.departmentName || row.departmentId || '未关联科室' }}</span>
</template>
</el-table-column>
<el-table-column label="难度" width="90">
<template #default="{ row }">
<el-tag :type="difficultyTagType(row.difficulty)">{{ row.difficulty || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="标签/ICD" min-width="180">
<template #default="{ row }">
<div class="case-tag-list">
<el-tag v-for="tag in row.tags.slice(0, 2)" :key="tag" size="small" type="info">{{ tag }}</el-tag>
<el-tag v-for="code in row.icdCodes.slice(0, 2)" :key="code" size="small">{{ code }}</el-tag>
<span v-if="row.tags.length + row.icdCodes.length > 4" class="table-subtext">+{{ row.tags.length + row.icdCodes.length - 4 }}</span>
<span v-if="!row.tags.length && !row.icdCodes.length">-</span>
</div>
</template>
</el-table-column>
<el-table-column label="时长" width="90">
<template #default="{ row }">
{{ row.estimatedMinutes ? `${row.estimatedMinutes} 分钟` : '-' }}
</template>
</el-table-column>
<el-table-column label="更新时间" width="170">
<template #default="{ row }">
{{ formatDateTime(row.updatedAt || row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="publishStatusTagType(row.publishStatus)">{{ publishStatusLabel(row.publishStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetailDrawer(row)">查看</el-button>
<el-button link type="primary" @click="openRelationsDialog(row)">关联</el-button>
<el-button link type="primary" :disabled="row.publishStatus !== 0" @click="confirmSubmitCase(row)">提交</el-button>
<el-button link type="danger" @click="confirmDisableCase(row)">停用/重录</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
background
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadCases"
@size-change="handleSizeChange"
/>
</div>
</section>
<el-dialog v-model="caseDialogVisible" title="新增病例草稿" width="960px" @closed="resetCaseForm">
<el-form ref="caseFormRef" class="case-create-form" :model="caseForm" :rules="caseRules" label-position="top">
<div class="case-form-section">
<h3>基础信息</h3>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="病例标题" prop="title">
<el-input v-model="caseForm.title" placeholder="请输入病例标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="病例类型" prop="case_type">
<el-select v-model="caseForm.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="机构">
<el-select
v-model="caseForm.institution_id"
:loading="loadingInstitutions"
clearable
filterable
placeholder="请选择机构"
@change="handleCaseInstitutionChange"
@visible-change="handleInstitutionVisibleChange"
>
<el-option
v-for="item in institutionOptions"
:key="item.id"
:label="institutionOptionLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="科室">
<el-select
v-model="caseForm.department_id"
:disabled="canManageInstitution && !caseForm.institution_id"
:loading="loadingDepartments"
clearable
filterable
placeholder="请选择科室"
@change="handleCaseDepartmentChange"
@visible-change="handleDepartmentVisibleChange"
>
<el-option
v-for="item in departmentOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="难度">
<el-select v-model="caseForm.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="caseForm.chief_complaint" placeholder="请输入主诉" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="患者年龄">
<el-input-number v-model="caseForm.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="caseForm.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="caseForm.tags" placeholder="多个用逗号分隔" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="ICD 编码">
<el-input v-model="caseForm.icd_codes" placeholder="多个用逗号分隔" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="预计分钟">
<el-input-number v-model="caseForm.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="caseForm.osce_enabled" active-text="开启" inactive-text="关闭" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="caseForm.description" type="textarea" :rows="3" placeholder="请输入病例背景或教学说明" />
</el-form-item>
</el-col>
</el-row>
</div>
<div class="case-form-section">
<h3>{{ caseTypeLabel(caseForm.case_type) }}内容</h3>
<template v-if="caseForm.case_type === 'traditional'">
<el-row :gutter="14">
<el-col :span="8">
<el-form-item label="标准诊断" prop="traditional_standard_diagnosis">
<el-input v-model="caseForm.traditional_standard_diagnosis" placeholder="如:上呼吸道感染" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="标准治疗">
<el-input v-model="caseForm.traditional_standard_treatment" placeholder="如:对症治疗,退热处理" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="指南依据">
<el-input v-model="caseForm.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="caseForm.teaching_learning_objectives" placeholder="请输入教学目标" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="教学重点">
<el-input v-model="caseForm.teaching_key_points" placeholder="请输入教学重点" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="参考答案">
<el-input v-model="caseForm.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="addExamItem">添加项目</el-button>
</div>
<div v-if="!caseForm.exam_items.length" class="case-empty-line">未添加项目可直接保存草稿</div>
<div v-for="(item, index) in caseForm.exam_items" :key="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="removeExamItem(index)" />
</div>
</div>
</el-form>
<template #footer>
<el-button @click="caseDialogVisible = false">取消</el-button>
<el-button :loading="savingCase" type="primary" @click="submitCaseForm">保存草稿</el-button>
</template>
</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"
drag
:auto-upload="false"
:limit="1"
:on-change="handleImportFileChange"
:on-exceed="handleImportExceed"
:on-remove="handleImportFileRemove"
accept=".pdf,application/pdf"
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">拖拽 PDF 到此处或点击选择</div>
<template #tip>
<div class="el-upload__tip">选择一个 PDF 文件后即可上传导入</div>
</template>
</el-upload>
<template #footer>
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button :disabled="!importFile" :loading="importing" type="primary" @click="submitImportPdf">上传</el-button>
</template>
</el-dialog>
<el-dialog v-model="relationsDialogVisible" title="编辑病例关联" width="620px" @closed="resetRelationsForm">
<el-form class="case-relations-form" label-position="top">
<el-alert
v-if="relationsCase"
:title="relationsCurrentTitle"
type="info"
:closable="false"
/>
<el-form-item v-if="canManageInstitution" label="机构关联">
<el-radio-group v-model="relationsForm.institutionMode">
<el-radio-button label="keep">不修改</el-radio-button>
<el-radio-button label="set">设置ID</el-radio-button>
<el-radio-button label="clear">清空</el-radio-button>
</el-radio-group>
<el-input-number
v-if="relationsForm.institutionMode === 'set'"
v-model="relationsForm.institution_id"
class="relation-input"
:min="1"
:precision="0"
:controls="false"
placeholder="请输入机构ID"
/>
</el-form-item>
<el-form-item label="科室关联">
<el-radio-group v-model="relationsForm.departmentMode">
<el-radio-button label="keep">不修改</el-radio-button>
<el-radio-button label="id">设置ID</el-radio-button>
<el-radio-button label="name">按名称</el-radio-button>
<el-radio-button label="clear">清空</el-radio-button>
</el-radio-group>
<el-input-number
v-if="relationsForm.departmentMode === 'id'"
v-model="relationsForm.department_id"
class="relation-input"
:min="1"
:precision="0"
:controls="false"
placeholder="请输入科室ID"
/>
<el-input
v-if="relationsForm.departmentMode === 'name'"
v-model="relationsForm.department_name"
class="relation-input"
placeholder="请输入科室名称"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="relationsDialogVisible = false">取消</el-button>
<el-button :loading="savingRelations" type="primary" @click="submitRelationsForm">保存关联</el-button>
</template>
</el-dialog>
<el-drawer v-model="detailDrawerVisible" :title="detailDrawerTitle" size="72%" destroy-on-close @closed="resetDetailForm">
<div v-loading="detailLoading" class="case-detail-drawer">
<el-form
v-if="detailCase"
ref="detailFormRef"
class="case-create-form case-detail-form"
:model="detailForm"
:rules="caseRules"
:disabled="!canEditDetailCase"
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" disabled 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="机构">
<el-select
v-model="detailForm.institution_id"
:loading="loadingInstitutions"
clearable
filterable
placeholder="请选择机构"
@change="handleDetailInstitutionChange"
@visible-change="handleInstitutionVisibleChange"
>
<el-option
v-for="item in institutionOptions"
:key="item.id"
:label="institutionOptionLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="科室">
<el-select
v-model="detailForm.department_id"
:disabled="canManageInstitution && !detailForm.institution_id"
:loading="loadingDepartments"
clearable
filterable
placeholder="请选择科室"
@change="handleDetailDepartmentChange"
@visible-change="handleDepartmentVisibleChange"
>
<el-option
v-for="item in departmentOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</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 v-if="showPublishedScoringRules" class="case-form-section">
<div class="case-section-title">
<h3>评分规则</h3>
</div>
<el-table :data="visibleScoringRules" border size="small" empty-text="暂无评分规则">
<el-table-column prop="dimension" label="维度" min-width="160" />
<el-table-column label="权重" width="100">
<template #default="{ row }">
{{ formatScoreWeight(row.score_weight) }}
</template>
</el-table-column>
<el-table-column prop="scoring_standard" label="评分标准" min-width="260" show-overflow-tooltip />
</el-table>
</div>
<div class="case-form-section">
<div class="case-section-title">
<h3>检查/检验项目</h3>
<el-button :icon="Plus" :disabled="!canEditDetailCase" @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" :disabled="!canEditDetailCase" circle @click="removeDetailExamItem(index)" />
</div>
</div>
</el-form>
<div class="drawer-form-footer">
<el-button @click="detailDrawerVisible = false">{{ canEditDetailCase ? '取消' : '关闭' }}</el-button>
<el-button v-if="canEditDetailCase" :loading="submittingDetail" type="primary" @click="submitDetailForm">保存草稿</el-button>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
import { Delete, Plus, Refresh, Search, Upload, UploadFilled } from '@element-plus/icons-vue'
import {
createCaseDraft,
disableCase,
fetchCaseFull,
fetchCases,
importCasePdf,
saveCaseDraft,
submitCase,
updateCaseRelations,
type CaseExamItemPayload,
type ImportCasePdfResult,
type CaseListItem,
type CasePublishStatus,
type CaseScoringRulePayload,
type CreateCaseDraftPayload,
type DraftCaseType,
type SaveCaseDraftPayload,
type UpdateCaseRelationsPayload
} from '@/api/cases'
import { fetchInstitutionList, fetchMyDepartments, type DepartmentOption, type InstitutionOption } from '@/api/users'
import { useAppStore } from '@/stores/app'
type RelationInstitutionMode = 'keep' | 'set' | 'clear'
type RelationDepartmentMode = 'keep' | 'id' | 'name' | 'clear'
interface ScoringRuleForm {
dimension: string
score_weight: number
ai_auto_score: boolean
scoring_standard: string
}
interface ExamItemForm {
item_code: string
item_name: string
item_type: string
result_text: string
}
const examItemFields: Array<{ key: keyof ExamItemForm; label: string }> = [
{ key: 'item_code', label: '项目编码' },
{ key: 'item_name', label: '项目名称' },
{ key: 'item_type', label: '项目类型' },
{ key: 'result_text', label: '结果文本' }
]
interface CaseDraftForm {
title: string
case_type: DraftCaseType
institution_id?: number
department_id?: number
department_name: string
difficulty: string
chief_complaint: string
description: string
patient_age?: number
patient_gender: string
tags: string
icd_codes: string
estimated_minutes?: number
osce_enabled: boolean
traditional_standard_diagnosis: string
traditional_standard_treatment: string
traditional_guideline_reference: string
teaching_learning_objectives: string
teaching_key_points: string
teaching_reference_answer: string
scoring_rules: ScoringRuleForm[]
exam_items: ExamItemForm[]
}
const appStore = useAppStore()
const loading = ref(false)
const savingCase = ref(false)
const importing = ref(false)
const savingRelations = ref(false)
const detailLoading = ref(false)
const submittingDetail = ref(false)
const loadingInstitutions = ref(false)
const loadingDepartments = 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 institutionOptions = ref<InstitutionOption[]>([])
const departmentOptions = ref<DepartmentOption[]>([])
const relationsCase = ref<CaseListItem | null>(null)
const detailCase = ref<CaseListItem | null>(null)
const caseDetail = ref<unknown>(null)
const filters = reactive<{
search: string
case_type: string
publish_status: CasePublishStatus | ''
institution: string
department: string
status: string
osce_enabled: string
ordering: string
}>({
search: '',
case_type: '',
publish_status: '',
institution: '',
department: '',
status: '',
osce_enabled: '',
ordering: ''
})
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
const caseForm = reactive<CaseDraftForm>({
title: '',
case_type: 'traditional',
institution_id: undefined,
department_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 detailForm = reactive<CaseDraftForm>({
title: '',
case_type: 'traditional',
institution_id: undefined,
department_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
departmentMode: RelationDepartmentMode
department_id?: number
department_name: string
}>({
institutionMode: 'keep',
institution_id: undefined,
departmentMode: 'keep',
department_id: undefined,
department_name: ''
})
const caseRules: FormRules = {
title: [{ required: true, message: '请输入病例标题', trigger: 'blur' }],
case_type: [{ required: true, message: '请选择病例类型', trigger: 'change' }],
traditional_standard_diagnosis: [{ required: true, message: '请输入标准诊断', trigger: 'blur' }],
teaching_learning_objectives: [{ required: true, message: '请输入教学目标', trigger: 'blur' }]
}
const isContentAdmin = computed(() => appStore.user.role === 'content-admin')
const canManageInstitution = computed(() => !isContentAdmin.value)
const pageDescription = computed(() =>
isContentAdmin.value
? '维护内容管理员病例资产,支持科室筛选、PDF 导入、草稿创建、关联编辑和状态流转。'
: '维护超级管理员病例资产,支持检索、PDF 导入、草稿创建、关联编辑和状态流转。'
)
const searchPlaceholder = computed(() => (isContentAdmin.value ? '搜索病例' : '搜索标题/主诉/标签/ICD'))
const detailDrawerTitle = computed(() => (detailCase.value ? `病例详情:${detailCase.value.title}` : '病例详情'))
const detailDisplay = computed(() => createDetailDisplay(detailCase.value, caseDetail.value))
const canEditDetailCase = computed(() => detailCase.value?.publishStatus === 0)
const showPublishedScoringRules = computed(() => detailCase.value?.publishStatus === 2)
const visibleScoringRules = computed(() =>
showPublishedScoringRules.value
? detailForm.scoring_rules.filter(rule => rule.dimension.trim() || rule.scoring_standard.trim())
: []
)
const relationsCurrentTitle = computed(() => {
if (!relationsCase.value) {
return ''
}
const department = relationsCase.value.departmentName || relationsCase.value.departmentId || '未关联'
if (canManageInstitution.value) {
const institution = relationsCase.value.institutionName || relationsCase.value.institutionId || '未关联'
return `当前:机构 ${institution} / 科室 ${department}`
}
return `当前科室:${department}`
})
async function loadCases() {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
try {
loading.value = true
const result = await fetchCases({
token: appStore.token,
search: filters.search,
case_type: filters.case_type,
publish_status: filters.publish_status,
...(canManageInstitution.value
? { institution: filters.institution }
: {
department: filters.department,
status: filters.status,
osce_enabled: filters.osce_enabled,
ordering: filters.ordering
}),
page: pagination.page
})
cases.value = result.cases
pagination.total = result.total
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取病例列表失败')
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadCases()
}
function resetFilters() {
filters.search = ''
filters.case_type = ''
filters.publish_status = ''
filters.institution = ''
filters.department = ''
filters.status = ''
filters.osce_enabled = ''
filters.ordering = ''
pagination.page = 1
loadCases()
}
function handleSizeChange() {
pagination.page = 1
loadCases()
}
function openCreateDialog() {
caseDialogVisible.value = true
loadInstitutionOptions()
loadDepartmentOptions(caseForm.institution_id)
}
function resetCaseForm() {
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_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 addExamItem() {
caseForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
}
function removeExamItem(index: number) {
caseForm.exam_items.splice(index, 1)
}
function addDetailExamItem() {
if (!canEditDetailCase.value) {
return
}
detailForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
}
function removeDetailExamItem(index: number) {
if (!canEditDetailCase.value) {
return
}
detailForm.exam_items.splice(index, 1)
}
async function loadInstitutionOptions() {
if (!appStore.token || !canManageInstitution.value || loadingInstitutions.value || institutionOptions.value.length) {
return
}
try {
loadingInstitutions.value = true
institutionOptions.value = await fetchInstitutionList(appStore.token)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取机构列表失败')
} finally {
loadingInstitutions.value = false
}
}
async function loadDepartmentOptions(institutionId?: number) {
if (!appStore.token || loadingDepartments.value) {
return
}
if (canManageInstitution.value && !institutionId) {
departmentOptions.value = []
return
}
try {
loadingDepartments.value = true
departmentOptions.value = await fetchMyDepartments(appStore.token, institutionId)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取科室列表失败')
} finally {
loadingDepartments.value = false
}
}
function handleInstitutionVisibleChange(visible: boolean) {
if (visible) {
loadInstitutionOptions()
}
}
function handleDepartmentVisibleChange(visible: boolean) {
if (visible) {
loadDepartmentOptions(activeCaseForm().institution_id)
}
}
function handleCaseInstitutionChange() {
caseForm.department_id = undefined
caseForm.department_name = ''
loadDepartmentOptions(caseForm.institution_id)
}
function handleDetailInstitutionChange() {
detailForm.department_id = undefined
detailForm.department_name = ''
loadDepartmentOptions(detailForm.institution_id)
}
function handleCaseDepartmentChange(value?: number) {
caseForm.department_name = getDepartmentName(value)
}
function handleDetailDepartmentChange(value?: number) {
detailForm.department_name = getDepartmentName(value)
}
function activeCaseForm() {
return detailDrawerVisible.value ? detailForm : caseForm
}
function getDepartmentName(value?: number) {
if (!value) {
return ''
}
return departmentOptions.value.find(item => item.id === value)?.name || ''
}
function institutionOptionLabel(item: InstitutionOption) {
return item.code ? `${item.name} (${item.code})` : item.name
}
async function submitCaseForm() {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
const isValid = await caseFormRef.value?.validate().catch(() => false)
if (!isValid) {
return
}
let payload: CreateCaseDraftPayload
try {
payload = buildCreatePayload()
} catch (error) {
ElMessage.warning(error instanceof Error ? error.message : '请检查病例表单')
return
}
try {
savingCase.value = true
await createCaseDraft({
token: appStore.token,
payload
})
ElMessage.success('病例草稿已新增')
caseDialogVisible.value = false
pagination.page = 1
await loadCases()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '新增病例草稿失败')
} finally {
savingCase.value = false
}
}
function buildCreatePayload(): CreateCaseDraftPayload {
return buildDraftPayload(caseForm)
}
function buildDraftPayload(form: CaseDraftForm): CreateCaseDraftPayload {
const structure = buildCaseStructure(form)
const scoringRules = normalizeScoringRules(form)
const examItems = normalizeExamItems(form)
const payload: CreateCaseDraftPayload = {
title: form.title.trim(),
case_type: form.case_type,
scoring_rules: scoringRules
}
payload[form.case_type] = structure
if (canManageInstitution.value && form.institution_id) payload.institution_id = form.institution_id
if (form.department_id) payload.department_id = form.department_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(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 buildSaveDraftPayload(form: CaseDraftForm): SaveCaseDraftPayload {
const structure = buildEditableCaseStructure(form)
const scoringRules = normalizeScoringRules(form)
const examItems = normalizeExamItems(form)
const payload: SaveCaseDraftPayload = {
title: form.title.trim(),
department_name: form.department_name.trim(),
difficulty: form.difficulty.trim(),
chief_complaint: form.chief_complaint.trim(),
description: form.description.trim(),
patient_gender: form.patient_gender,
tags: isContentAdmin.value ? form.tags.trim() : parseTextList(form.tags),
icd_codes: parseTextList(form.icd_codes),
osce_enabled: form.osce_enabled,
scoring_rules: scoringRules,
exam_items: examItems
}
payload[form.case_type] = structure
if (canManageInstitution.value && form.institution_id) payload.institution_id = form.institution_id
if (form.department_id) payload.department_id = form.department_id
if (form.patient_age !== undefined) payload.patient_age = form.patient_age
if (form.estimated_minutes !== undefined) payload.estimated_minutes = form.estimated_minutes
return payload
}
function buildCaseStructure(form: CaseDraftForm): Record<string, unknown> {
if (form.case_type === 'teaching') {
if (!form.teaching_learning_objectives.trim()) {
throw new Error('请输入教学目标')
}
return {
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 (!form.traditional_standard_diagnosis.trim()) {
throw new Error('请输入标准诊断')
}
return {
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 buildEditableCaseStructure(form: CaseDraftForm): Record<string, unknown> {
if (form.case_type === 'teaching') {
if (!form.teaching_learning_objectives.trim()) {
throw new Error('请输入教学目标')
}
return {
learning_objectives: form.teaching_learning_objectives.trim(),
key_points: form.teaching_key_points.trim(),
reference_answer: form.teaching_reference_answer.trim()
}
}
if (!form.traditional_standard_diagnosis.trim()) {
throw new Error('请输入标准诊断')
}
return {
standard_diagnosis: form.traditional_standard_diagnosis.trim(),
standard_treatment: form.traditional_standard_treatment.trim(),
guideline_reference: form.traditional_guideline_reference.trim()
}
}
function normalizeScoringRules(form: CaseDraftForm): CaseScoringRulePayload[] {
const rules = form.scoring_rules
.map((rule, index) => {
const scoreWeight = Number(rule.score_weight)
return {
dimension: rule.dimension.trim() || (rule.scoring_standard.trim() ? `评分维度${index + 1}` : ''),
score_weight: Number.isFinite(scoreWeight) && scoreWeight > 0 && scoreWeight <= 1 ? scoreWeight : 1,
ai_auto_score: rule.ai_auto_score,
scoring_standard: rule.scoring_standard.trim()
}
})
.filter(rule => rule.dimension || rule.scoring_standard)
if (!rules.length) {
return [createDefaultScoringRule(form)]
}
for (const rule of rules) {
rule.ai_auto_score = true
}
return rules.map(rule => ({
dimension: rule.dimension,
score_weight: rule.score_weight,
ai_auto_score: rule.ai_auto_score,
...(rule.scoring_standard ? { scoring_standard: rule.scoring_standard } : {})
}))
}
function createDefaultScoringRule(form: CaseDraftForm): CaseScoringRulePayload {
return {
dimension: form.case_type === 'teaching' ? '教学目标达成' : '诊断与处置',
score_weight: 1,
ai_auto_score: true,
scoring_standard: '由AI根据病例内容生成评分标准'
}
}
function normalizeExamItems(form: CaseDraftForm): CaseExamItemPayload[] {
const items = form.exam_items
.map((item, index) => ({
index,
item_code: item.item_code.trim(),
item_name: item.item_name.trim(),
item_type: item.item_type.trim(),
result_text: item.result_text.trim()
}))
.filter(item => examItemFields.some(({ key }) => item[key]))
const codes = new Set<string>()
for (const item of items) {
const missingLabels = examItemFields.filter(({ key }) => !item[key]).map(({ label }) => label)
if (missingLabels.length) {
throw new Error(`${item.index + 1}个检查/检验项目需完整填写,缺少:${missingLabels.join('、')}`)
}
if (codes.has(item.item_code)) {
throw new Error(`检查/检验项目编码重复:${item.item_code}`)
}
codes.add(item.item_code)
}
return items.map(item => ({
item_code: item.item_code,
item_name: item.item_name,
item_type: item.item_type,
result_text: item.result_text
}))
}
function openImportDialog() {
importDialogVisible.value = true
}
function resetImportFile() {
importFile.value = null
importCaseType.value = 'traditional'
importFileList.value = []
importUploadRef.value?.clearFiles()
}
const handleImportFileChange: UploadProps['onChange'] = uploadFile => {
importFile.value = uploadFile.raw || null
}
const handleImportFileRemove: UploadProps['onRemove'] = () => {
importFile.value = null
}
const handleImportExceed: UploadProps['onExceed'] = files => {
const file = files[0] as UploadRawFile | undefined
if (!file) {
return
}
importUploadRef.value?.clearFiles()
importUploadRef.value?.handleStart(file)
importFile.value = file
}
async function submitImportPdf() {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
if (!importFile.value) {
ElMessage.warning('请先选择 PDF 文件')
return
}
try {
importing.value = true
const result = await importCasePdf({
token: appStore.token,
file: importFile.value,
case_type: importCaseType.value
})
fillCaseFormFromImportedPdf(result)
importDialogVisible.value = false
caseDialogVisible.value = true
ElMessage.success('PDF 病例导入完成,请确认后保存草稿')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '导入 PDF 病例失败')
} finally {
importing.value = false
}
}
function openRelationsDialog(row: CaseListItem) {
relationsCase.value = row
relationsDialogVisible.value = true
}
function resetRelationsForm() {
relationsCase.value = null
relationsForm.institutionMode = 'keep'
relationsForm.institution_id = undefined
relationsForm.departmentMode = 'keep'
relationsForm.department_id = undefined
relationsForm.department_name = ''
}
async function submitRelationsForm() {
if (!appStore.token || !relationsCase.value) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
let payload: UpdateCaseRelationsPayload
try {
payload = buildRelationsPayload()
} catch (error) {
ElMessage.warning(error instanceof Error ? error.message : '请检查关联信息')
return
}
try {
savingRelations.value = true
await updateCaseRelations({
token: appStore.token,
id: relationsCase.value.id,
payload
})
ElMessage.success('病例关联已更新')
relationsDialogVisible.value = false
await loadCases()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '编辑病例关联失败')
} finally {
savingRelations.value = false
}
}
function buildRelationsPayload(): UpdateCaseRelationsPayload {
const payload: UpdateCaseRelationsPayload = {}
if (canManageInstitution.value && relationsForm.institutionMode === 'set') {
if (!relationsForm.institution_id) {
throw new Error('请输入机构ID')
}
payload.institution_id = relationsForm.institution_id
} else if (canManageInstitution.value && relationsForm.institutionMode === 'clear') {
payload.institution_id = null
}
if (relationsForm.departmentMode === 'id') {
if (!relationsForm.department_id) {
throw new Error('请输入科室ID')
}
payload.department_id = relationsForm.department_id
} else if (relationsForm.departmentMode === 'name') {
if (!relationsForm.department_name.trim()) {
throw new Error('请输入科室名称')
}
payload.department_name = relationsForm.department_name.trim()
} else if (relationsForm.departmentMode === 'clear') {
payload.department_id = null
}
if (!Object.keys(payload).length) {
throw new Error(canManageInstitution.value ? '请至少修改机构或科室中的一项' : '请至少修改科室关联')
}
return payload
}
async function confirmSubmitCase(row: CaseListItem) {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
try {
await ElMessageBox.confirm(`确认提交「${row.title}」为正常状态吗?`, '提交病例', {
confirmButtonText: '提交',
cancelButtonText: '取消',
type: 'warning'
})
await submitCase({
token: appStore.token,
id: row.id
})
ElMessage.success('病例已提交')
await loadCases()
} catch (error) {
if (error !== 'cancel' && error !== 'close') {
ElMessage.error(error instanceof Error ? error.message : '提交病例失败')
}
}
}
async function confirmDisableCase(row: CaseListItem) {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
try {
await ElMessageBox.confirm(`确认停用/重录「${row.title}」吗?`, '停用/重录病例', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
await disableCase({
token: appStore.token,
id: row.id
})
ElMessage.success('病例已停用/重录')
await loadCases()
} catch (error) {
if (error !== 'cancel' && error !== 'close') {
ElMessage.error(error instanceof Error ? error.message : '停用/重录病例失败')
}
}
}
async function openDetailDrawer(row: CaseListItem) {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
detailCase.value = row
detailDrawerVisible.value = true
caseDetail.value = null
loadInstitutionOptions()
try {
detailLoading.value = true
caseDetail.value = await fetchCaseFull({
token: appStore.token,
id: row.id
})
fillDetailForm(row, caseDetail.value)
loadDepartmentOptions(detailForm.institution_id)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取病例详情失败')
} finally {
detailLoading.value = false
}
}
async function submitDetailForm() {
if (!appStore.token || !detailCase.value) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
if (!canEditDetailCase.value) {
ElMessage.warning('只有草稿病例可以保存草稿')
return
}
const isValid = await detailFormRef.value?.validate().catch(() => false)
if (!isValid) {
return
}
let payload: SaveCaseDraftPayload
try {
payload = buildSaveDraftPayload(detailForm)
} catch (error) {
ElMessage.warning(error instanceof Error ? error.message : '请检查病例表单')
return
}
try {
submittingDetail.value = true
await saveCaseDraft({
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,;;]/)
.map(item => item.trim())
.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.department_id = getImportNumber(record, ['department_id', 'departmentId']) ?? undefined
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_id = getImportNumber(record, ['department_id', 'departmentId']) ?? 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 || '-'
const caseType = getDetailString(record, ['case_type', 'caseType']) || row?.caseType || ''
const difficulty = getDetailString(record, ['difficulty']) || row?.difficulty || '-'
const institution = getDetailString(record, ['institution_name', 'institutionName']) || row?.institutionName || row?.institutionId || '-'
const department = getDetailString(record, ['department_name', 'departmentName']) || row?.departmentName || row?.departmentId || '-'
const chiefComplaint = getDetailString(record, ['chief_complaint', 'chiefComplaint']) || row?.chiefComplaint || '-'
const description = getDetailString(record, ['description', 'summary', 'content']) || '-'
const patientAge = getDetailString(record, ['patient_age', 'patientAge'])
const patientGender = patientGenderLabel(getDetailString(record, ['patient_gender', 'patientGender']))
const estimatedMinutes = getDetailString(record, ['estimated_minutes', 'estimatedMinutes']) || (row?.estimatedMinutes ? String(row.estimatedMinutes) : '')
return {
id: getDetailString(record, ['id']) || row?.id || '-',
title,
caseType,
difficulty,
institution,
department,
chiefComplaint,
description,
patient: [patientAge ? `${patientAge}` : '', patientGender].filter(Boolean).join(' / ') || '-',
estimatedMinutes: estimatedMinutes ? `${estimatedMinutes} 分钟` : '-'
}
}
function getDetailRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {}
}
const record = value as Record<string, unknown>
if (record.data && typeof record.data === 'object' && !Array.isArray(record.data)) {
return record.data as Record<string, unknown>
}
return record
}
function getDetailString(record: Record<string, unknown>, keys: string[]) {
for (const key of keys) {
const value = record[key]
if (typeof value === 'string' && value.trim()) return value
if (typeof value === 'number') return String(value)
}
return ''
}
function caseTypeLabel(type: string) {
const labels: Record<string, string> = {
traditional: '传统病例',
teaching: '教学病例'
}
return labels[type] || type || '-'
}
function patientGenderLabel(value: string) {
if (value === 'male' || value === '男') return '男'
if (value === 'female' || value === '女') return '女'
if (value === 'unknown') return '未知'
return value || ''
}
function publishStatusLabel(status: CasePublishStatus | null) {
if (status === 0) return '草稿'
if (status === 1) return '正常'
if (status === 2) return '已发布'
return '-'
}
function publishStatusTagType(status: CasePublishStatus | null) {
if (status === 0) return 'info'
if (status === 1) return 'warning'
if (status === 2) return 'success'
return 'info'
}
function difficultyTagType(value: string) {
if (['高', 'high', 'hard'].includes(value.toLowerCase())) return 'danger'
if (['中', 'medium', 'middle'].includes(value.toLowerCase())) return 'warning'
if (['低', 'low', 'easy'].includes(value.toLowerCase())) return 'success'
return 'info'
}
function formatDateTime(value: string) {
if (!value) {
return '-'
}
return value.replace('T', ' ').slice(0, 19)
}
function formatScoreWeight(value: number) {
if (!Number.isFinite(value)) {
return '-'
}
return value <= 1 ? `${Math.round(value * 100)}%` : String(value)
}
onMounted(loadCases)
</script>