1805 lines
67 KiB
Vue
1805 lines
67 KiB
Vue
<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>
|