feat: 联调
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user