309 lines
11 KiB
Vue
309 lines
11 KiB
Vue
|
|
<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="50%" destroy-on-close>
|
|||
|
|
<div v-loading="detailLoading" class="case-detail-drawer">
|
|||
|
|
<el-descriptions v-if="detailCase" :column="2" border>
|
|||
|
|
<el-descriptions-item label="ID">{{ detailDisplay.id }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="类型">{{ caseTypeLabel(detailDisplay.caseType) }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="状态">{{ publishStatusLabel(detailCase.publishStatus) }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="难度">{{ detailDisplay.difficulty }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="科室">{{ detailDisplay.department }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="患者">{{ detailDisplay.patient }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="预计时长">{{ detailDisplay.estimatedMinutes }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="主诉" :span="2">{{ detailDisplay.chiefComplaint }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="描述" :span="2">{{ detailDisplay.description }}</el-descriptions-item>
|
|||
|
|
</el-descriptions>
|
|||
|
|
</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'
|
|||
|
|
|
|||
|
|
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 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))
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
})
|
|||
|
|
} 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 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> {
|
|||
|
|
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 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>
|