feat: 联调

This commit is contained in:
王天骄
2026-06-12 18:10:15 +08:00
parent 9fddb42ebe
commit 1d093c9589
12 changed files with 2309 additions and 56 deletions
+308
View File
@@ -0,0 +1,308 @@
<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>