Files
cms/src/views/CaseReviewView.vue
T
2026-06-18 10:35:48 +08:00

882 lines
30 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>审核内容管理员提交的病例确认无误后发布到医院病例库</p>
</div>
<div class="toolbar-actions">
<el-button :icon="Refresh" @click="resetFilters">刷新</el-button>
</div>
</section>
<section class="filter-bar case-review-filter">
<el-input v-model="filters.search" :prefix-icon="Search" clearable placeholder="搜索病例" @keyup.enter="handleSearch" />
<el-input v-model="filters.department" clearable placeholder="科室ID" @keyup.enter="handleSearch" />
<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="240">
<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="科室" min-width="150">
<template #default="{ row }">
{{ row.departmentName || row.departmentId || '未关联科室' }}
</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="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="warning">{{ publishStatusLabel(row.publishStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetailDrawer(row)">查看</el-button>
<el-button link type="success" @click="confirmPublishCase(row)">发布</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="pagination.page"
:total="pagination.total"
background
layout="total, prev, pager, next, jumper"
@current-change="loadReviewCases"
/>
</div>
</section>
<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"
class="case-create-form case-detail-form"
:model="detailForm"
disabled
label-position="top"
>
<div class="case-form-section">
<div class="case-section-title">
<h3>基础信息</h3>
<el-tag type="warning">ID {{ detailDisplay.id }} / {{ publishStatusLabel(detailCase.publishStatus) }}</el-tag>
</div>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="病例标题">
<el-input v-model="detailForm.title" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="病例类型">
<el-select v-model="detailForm.case_type">
<el-option label="传统病例" value="traditional" />
<el-option label="教学病例" value="teaching" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="机构">
<el-input v-model="detailForm.institution_name" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="科室">
<el-input v-model="detailForm.department_name" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="难度">
<el-select v-model="detailForm.difficulty">
<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" />
</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" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="患者性别">
<el-select v-model="detailForm.patient_gender" clearable>
<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" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="ICD 编码">
<el-input v-model="detailForm.icd_codes" />
</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" />
</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="标准诊断">
<el-input v-model="detailForm.traditional_standard_diagnosis" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="标准治疗">
<el-input v-model="detailForm.traditional_standard_treatment" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="指南依据">
<el-input v-model="detailForm.traditional_guideline_reference" />
</el-form-item>
</el-col>
</el-row>
</template>
<template v-else>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="教学目标">
<el-input v-model="detailForm.teaching_goal" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="讨论问题">
<el-input v-model="detailForm.discussion_questions" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教师指南">
<el-input v-model="detailForm.teacher_guide" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="评分重点">
<el-input v-model="detailForm.scoring_focus" />
</el-form-item>
</el-col>
</el-row>
</template>
</div>
<div v-if="visibleScoringRules.length" 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>
</div>
<div v-if="!detailForm.exam_items.length" class="case-empty-line">暂无检查/检验项目</div>
<div v-for="(item, index) in detailForm.exam_items" :key="`review-detail-item-${index}`" class="exam-item-row review-exam-item-row">
<el-input v-model="item.item_code" />
<el-input v-model="item.item_name" />
<el-input v-model="item.item_type" />
<el-input v-model="item.result_text" />
</div>
</div>
</el-form>
<div class="drawer-form-footer">
<el-button @click="detailDrawerVisible = false">关闭</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 { Refresh, Search } from '@element-plus/icons-vue'
import {
fetchCaseFull,
fetchCases,
publishCase,
type CaseListItem,
type CasePublishStatus
} from '@/api/cases'
import { useAppStore } from '@/stores/app'
type ReviewCaseType = 'traditional' | 'teaching'
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
}
interface ReviewCaseDetailForm {
title: string
case_type: ReviewCaseType
institution_name: string
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_goal: string
discussion_questions: string
teacher_guide: string
scoring_focus: string
scoring_rules: ScoringRuleForm[]
exam_items: ExamItemForm[]
}
const appStore = useAppStore()
const loading = ref(false)
const publishing = ref(false)
const detailLoading = ref(false)
const detailDrawerVisible = ref(false)
const cases = ref<CaseListItem[]>([])
const detailCase = ref<CaseListItem | null>(null)
const caseDetail = ref<unknown>(null)
const detailForm = reactive<ReviewCaseDetailForm>({
title: '',
case_type: 'traditional',
institution_name: '',
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_goal: '',
discussion_questions: '',
teacher_guide: '',
scoring_focus: '',
scoring_rules: [],
exam_items: []
})
const filters = reactive({
search: '',
department: ''
})
const pagination = reactive({
page: 1,
total: 0
})
const detailDrawerTitle = computed(() => (detailCase.value ? `病例详情:${detailCase.value.title}` : '病例详情'))
const detailDisplay = computed(() => createDetailDisplay(detailCase.value, caseDetail.value))
const visibleScoringRules = computed(() =>
detailForm.scoring_rules.filter(rule => rule.dimension.trim() || rule.scoring_standard.trim())
)
async function loadReviewCases() {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
try {
loading.value = true
const result = await fetchCases({
token: appStore.token,
publish_status: 1,
search: filters.search,
department: filters.department,
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
loadReviewCases()
}
function resetFilters() {
filters.search = ''
filters.department = ''
pagination.page = 1
loadReviewCases()
}
async function openDetailDrawer(row: CaseListItem) {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
detailCase.value = row
detailDrawerVisible.value = true
caseDetail.value = null
try {
detailLoading.value = true
caseDetail.value = await fetchCaseFull({
token: appStore.token,
id: row.id
})
fillDetailForm(row, caseDetail.value)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取病例详情失败')
} finally {
detailLoading.value = false
}
}
async function confirmPublishCase(row: CaseListItem) {
if (!appStore.token || publishing.value) {
return
}
try {
await ElMessageBox.confirm(`确认发布「${row.title}」吗?`, '发布病例', {
confirmButtonText: '发布',
cancelButtonText: '取消',
type: 'warning'
})
publishing.value = true
await publishCase({
token: appStore.token,
id: row.id
})
ElMessage.success('病例已发布')
await loadReviewCases()
} catch (error) {
if (error !== 'cancel' && error !== 'close') {
ElMessage.error(error instanceof Error ? error.message : '发布病例失败')
}
} finally {
publishing.value = false
}
}
function resetDetailForm() {
detailCase.value = null
caseDetail.value = null
resetReviewDetailForm()
}
function resetReviewDetailForm() {
detailForm.title = ''
detailForm.case_type = 'traditional'
detailForm.institution_name = ''
detailForm.department_name = ''
detailForm.difficulty = ''
detailForm.chief_complaint = ''
detailForm.description = ''
detailForm.patient_age = undefined
detailForm.patient_gender = ''
detailForm.tags = ''
detailForm.icd_codes = ''
detailForm.estimated_minutes = undefined
detailForm.osce_enabled = false
detailForm.traditional_standard_diagnosis = ''
detailForm.traditional_standard_treatment = ''
detailForm.traditional_guideline_reference = ''
detailForm.teaching_goal = ''
detailForm.discussion_questions = ''
detailForm.teacher_guide = ''
detailForm.scoring_focus = ''
detailForm.scoring_rules = []
detailForm.exam_items = []
}
function fillDetailForm(row: CaseListItem, fullData: unknown) {
resetReviewDetailForm()
const record = getDetailRecord(fullData)
const contentSources = [record, fullData]
const traditional = findRecord(['traditional', 'traditional_case', 'traditionalCase'], contentSources) ?? {}
const teaching = findRecord(['teaching', 'teaching_case', 'teachingCase'], contentSources) ?? {}
const scoringRules = getScoringRules(fullData, record)
const examItems = getExamItems(fullData, record)
detailForm.title = getString(record, ['title', 'name', 'case_title', 'caseTitle'], row.title)
detailForm.case_type = normalizeCaseType(getString(record, ['case_type', 'caseType'], row.caseType), normalizeCaseType(row.caseType, 'traditional'))
detailForm.institution_name = getString(record, ['institution_name', 'institutionName'], row.institutionName || row.institutionId)
detailForm.department_name = getString(record, ['department_name', 'departmentName'], row.departmentName || row.departmentId)
detailForm.difficulty = getString(record, ['difficulty'], row.difficulty)
detailForm.chief_complaint = getString(record, ['chief_complaint', 'chiefComplaint'], row.chiefComplaint)
detailForm.description = getString(record, ['description', 'summary', 'content'])
detailForm.patient_age = getNumber(record, ['patient_age', 'patientAge']) ?? undefined
detailForm.patient_gender = normalizeGender(getFirst(record, ['patient_gender', 'patientGender']))
detailForm.tags = getStringList(record, ['tags', 'tag_list', 'tagList']).join(', ')
detailForm.icd_codes = getStringList(record, ['icd_codes', 'icdCodes', 'icd_list', 'icdList']).join(', ')
detailForm.estimated_minutes = getNumber(record, ['estimated_minutes', 'estimatedMinutes']) ?? row.estimatedMinutes ?? undefined
detailForm.osce_enabled = getBoolean(record, ['osce_enabled', 'osceEnabled'])
detailForm.traditional_standard_diagnosis =
getString(traditional, ['standard_diagnosis', 'standardDiagnosis']) ||
getString(record, ['standard_diagnosis', 'standardDiagnosis'])
detailForm.traditional_standard_treatment =
getString(traditional, ['standard_treatment', 'standardTreatment']) ||
getString(record, ['standard_treatment', 'standardTreatment'])
detailForm.traditional_guideline_reference =
getString(traditional, ['guideline_reference', 'guidelineReference']) ||
getString(record, ['guideline_reference', 'guidelineReference'])
detailForm.teaching_goal =
getString(teaching, ['teaching_goal', 'teachingGoal']) ||
getString(record, ['teaching_goal', 'teachingGoal'])
detailForm.discussion_questions =
getString(teaching, ['discussion_questions', 'discussionQuestions']) ||
getString(record, ['discussion_questions', 'discussionQuestions'])
detailForm.teacher_guide =
getString(teaching, ['teacher_guide', 'teacherGuide']) ||
getString(record, ['teacher_guide', 'teacherGuide'])
detailForm.scoring_focus =
getString(teaching, ['scoring_focus', 'scoringFocus']) ||
getString(record, ['scoring_focus', 'scoringFocus'])
detailForm.scoring_rules = scoringRules
detailForm.exam_items = examItems
}
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 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,
department,
chiefComplaint,
description,
patient: [patientAge ? `${patientAge}` : '', patientGender].filter(Boolean).join(' / ') || '-',
estimatedMinutes: estimatedMinutes ? `${estimatedMinutes} 分钟` : '-'
}
}
function getDetailRecord(value: unknown): Record<string, unknown> {
return findDetailRecord(value) || {}
}
function findDetailRecord(value: unknown, depth = 0): Record<string, unknown> | null {
if (!value || typeof value !== 'object' || Array.isArray(value) || depth > 4) {
return null
}
const record = value as Record<string, unknown>
const preferredKeys = ['case', 'case_detail', 'caseDetail', 'case_info', 'caseInfo', 'detail', 'full', 'record', 'item', 'data', 'result']
for (const key of preferredKeys) {
const found = findDetailRecord(record[key], depth + 1)
if (found) {
return found
}
}
if (hasCaseDetailFields(record)) {
return record
}
for (const child of Object.values(record)) {
const found = findDetailRecord(child, depth + 1)
if (found) {
return found
}
}
return null
}
function hasCaseDetailFields(record: Record<string, unknown>) {
return [
'title',
'case_title',
'caseTitle',
'case_type',
'caseType',
'chief_complaint',
'chiefComplaint',
'description',
'summary',
'content',
'difficulty',
'estimated_minutes',
'estimatedMinutes',
'patient_age',
'patientAge',
'patient_gender',
'patientGender'
].some(key => record[key] !== undefined && record[key] !== null)
}
function getRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {}
}
function getFirst(record: Record<string, unknown>, keys: string[]) {
for (const key of keys) {
if (record[key] !== undefined && record[key] !== null) {
return record[key]
}
}
return undefined
}
function getString(record: Record<string, unknown>, keys: string[], fallback = '') {
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 getNumber(record: Record<string, unknown>, keys: string[]) {
const value = getFirst(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 getBoolean(record: Record<string, unknown>, keys: string[]) {
const value = getFirst(record, keys)
if (typeof value === 'boolean') return value
if (typeof value === 'number') return value === 1
if (typeof value === 'string') {
return ['true', '1', 'yes', 'y', 'on', '开启', '启用', '是'].includes(value.trim().toLowerCase())
}
return false
}
function getStringList(record: Record<string, unknown>, keys: string[]) {
const value = getFirst(record, keys)
if (Array.isArray(value)) {
return value
.map(item => {
if (typeof item === 'string' || typeof item === 'number') {
return String(item).trim()
}
return getString(getRecord(item), ['name', 'title', 'code', 'value'])
})
.filter(Boolean)
}
if (typeof value === 'string') {
return value
.split(/[,\n,;;]/)
.map(item => item.trim())
.filter(Boolean)
}
return []
}
function getScoringRules(...sources: unknown[]): ScoringRuleForm[] {
const raw = findArray(['scoring_rules', 'scoringRules', 'score_rules', 'scoreRules', 'rules'], sources)
if (!Array.isArray(raw)) {
return []
}
return raw
.map(item => {
const itemRecord = getRecord(item)
const scoreWeight = getNumber(itemRecord, ['score_weight', 'scoreWeight', 'weight'])
const rawAutoScore = getFirst(itemRecord, ['ai_auto_score', 'aiAutoScore'])
return {
dimension: getString(itemRecord, ['dimension', 'name']),
score_weight: scoreWeight ?? 1,
ai_auto_score: rawAutoScore === undefined ? true : getBoolean(itemRecord, ['ai_auto_score', 'aiAutoScore']),
scoring_standard: getString(itemRecord, ['scoring_standard', 'scoringStandard', 'standard', 'description', 'content'])
}
})
.filter(item => item.dimension || item.scoring_standard)
}
function getExamItems(...sources: unknown[]): ExamItemForm[] {
const raw = findArray(['exam_items', 'examItems', 'exams', 'exam_list', 'examList', 'items'], sources)
if (!Array.isArray(raw)) {
return []
}
return raw
.map(item => {
const itemRecord = getRecord(item)
return {
item_code: getString(itemRecord, ['item_code', 'itemCode', 'code']),
item_name: getString(itemRecord, ['item_name', 'itemName', 'name']),
item_type: getString(itemRecord, ['item_type', 'itemType', 'type']),
result_text: getString(itemRecord, ['result_text', 'resultText', 'result', 'value', 'content'])
}
})
.filter(item => item.item_code || item.item_name || item.item_type || item.result_text)
}
function findArray(keys: string[], sources: unknown[]) {
for (const source of sources) {
const found = findArrayInSource(source, keys)
if (found) {
return found
}
}
return undefined
}
function findRecord(keys: string[], sources: unknown[]) {
for (const source of sources) {
const found = findRecordInSource(source, keys)
if (found) {
return found
}
}
return undefined
}
function findRecordInSource(value: unknown, keys: string[], depth = 0): Record<string, unknown> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value) || depth > 5) {
return undefined
}
const record = value as Record<string, unknown>
for (const key of keys) {
const nested = record[key]
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
return nested as Record<string, unknown>
}
}
const preferredKeys = ['data', 'case', 'case_detail', 'caseDetail', 'case_info', 'caseInfo', 'detail', 'full', 'result', 'record', 'item']
for (const key of preferredKeys) {
const found = findRecordInSource(record[key], keys, depth + 1)
if (found) {
return found
}
}
for (const child of Object.values(record)) {
const found = findRecordInSource(child, keys, depth + 1)
if (found) {
return found
}
}
return undefined
}
function findArrayInSource(value: unknown, keys: string[], depth = 0): unknown[] | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value) || depth > 5) {
return undefined
}
const record = value as Record<string, unknown>
for (const key of keys) {
const nested = record[key]
if (Array.isArray(nested)) {
return nested
}
}
const preferredKeys = ['data', 'case', 'case_detail', 'caseDetail', 'case_info', 'caseInfo', 'detail', 'full', 'result', 'record', 'item']
for (const key of preferredKeys) {
const found = findArrayInSource(record[key], keys, depth + 1)
if (found) {
return found
}
}
for (const child of Object.values(record)) {
const found = findArrayInSource(child, keys, depth + 1)
if (found) {
return found
}
}
return undefined
}
function normalizeCaseType(value: string, fallback: ReviewCaseType): ReviewCaseType {
return value === 'teaching' ? 'teaching' : fallback
}
function normalizeGender(value: unknown) {
if (value === undefined || value === null) {
return ''
}
const normalized = String(value).trim().toLowerCase()
if (['male', 'm', 'man', '1', '男'].includes(normalized)) return 'male'
if (['female', 'f', 'woman', '2', '女'].includes(normalized)) return 'female'
if (['unknown', 'unknow', '0', '未知', '不详'].includes(normalized)) return 'unknown'
return ''
}
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 formatScoreWeight(value: number) {
if (!Number.isFinite(value)) {
return '-'
}
return value <= 1 ? `${Math.round(value * 100)}%` : String(value)
}
function publishStatusLabel(status: CasePublishStatus | null) {
if (status === 0) return '草稿'
if (status === 1) return '待审核'
if (status === 2) return '已发布'
return '-'
}
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)
}
onMounted(loadReviewCases)
</script>
<style scoped lang="scss">
.review-exam-item-row {
grid-template-columns: minmax(130px, 0.8fr) minmax(140px, 1fr) minmax(110px, 0.7fr) minmax(150px, 1fr);
}
</style>