From 1679acad1279bd5a78c59dcecb010f91eb095b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=A4=A9=E9=AA=84?= <5307576@qq.com> Date: Wed, 17 Jun 2026 16:13:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E7=97=85=E5=8E=86=E5=AD=97=E6=AE=B5=E7=BC=BA=E5=A4=B1=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/CaseReviewView.vue | 541 +++++++++++++++++++++++++++++++++-- src/views/CasesView.vue | 109 +++++-- src/views/DashboardView.vue | 5 - 3 files changed, 608 insertions(+), 47 deletions(-) diff --git a/src/views/CaseReviewView.vue b/src/views/CaseReviewView.vue index 5fc486bd..a1e7ace4 100644 --- a/src/views/CaseReviewView.vue +++ b/src/views/CaseReviewView.vue @@ -80,19 +80,174 @@ - +
- - {{ detailDisplay.id }} - {{ caseTypeLabel(detailDisplay.caseType) }} - {{ publishStatusLabel(detailCase.publishStatus) }} - {{ detailDisplay.difficulty }} - {{ detailDisplay.department }} - {{ detailDisplay.patient }} - {{ detailDisplay.estimatedMinutes }} - {{ detailDisplay.chiefComplaint }} - {{ detailDisplay.description }} - + +
+
+

基础信息

+ ID {{ detailDisplay.id }} / {{ publishStatusLabel(detailCase.publishStatus) }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+

{{ caseTypeLabel(detailForm.case_type) }}内容

+ + +
+ +
+
+

评分规则

+
+ + + + + + + +
+ +
+
+

检查/检验项目

+
+
暂无检查/检验项目
+
+ + + + +
+
+
+ +
@@ -111,6 +266,46 @@ import { } 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_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 publishing = ref(false) @@ -119,6 +314,29 @@ const detailDrawerVisible = ref(false) const cases = ref([]) const detailCase = ref(null) const caseDetail = ref(null) +const detailForm = reactive({ + 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_learning_objectives: '', + teaching_key_points: '', + teaching_reference_answer: '', + scoring_rules: [], + exam_items: [] +}) const filters = reactive({ search: '', @@ -131,6 +349,9 @@ const pagination = reactive({ 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) { @@ -184,6 +405,7 @@ async function openDetailDrawer(row: CaseListItem) { token: appStore.token, id: row.id }) + fillDetailForm(row, caseDetail.value) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '获取病例详情失败') } finally { @@ -218,6 +440,68 @@ async function confirmPublishCase(row: CaseListItem) { } } +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_learning_objectives = '' + detailForm.teaching_key_points = '' + detailForm.teaching_reference_answer = '' + detailForm.scoring_rules = [] + detailForm.exam_items = [] +} + +function fillDetailForm(row: CaseListItem, fullData: unknown) { + resetReviewDetailForm() + + const record = getDetailRecord(fullData) + const traditional = getRecord(getFirst(record, ['traditional'])) + const teaching = getRecord(getFirst(record, ['teaching'])) + 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']) + detailForm.traditional_standard_treatment = getString(traditional, ['standard_treatment', 'standardTreatment']) + detailForm.traditional_guideline_reference = getString(traditional, ['guideline_reference', 'guidelineReference']) + detailForm.teaching_learning_objectives = getString(teaching, ['learning_objectives', 'learningObjectives']) + detailForm.teaching_key_points = getString(teaching, ['key_points', 'keyPoints']) + detailForm.teaching_reference_answer = getString(teaching, ['reference_answer', 'referenceAnswer']) + 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 || '-' @@ -244,16 +528,225 @@ function createDetailDisplay(row: CaseListItem | null, fullData: unknown) { } function getDetailRecord(value: unknown): Record { - if (!value || typeof value !== 'object' || Array.isArray(value)) { - return {} + return findDetailRecord(value) || {} +} + +function findDetailRecord(value: unknown, depth = 0): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value) || depth > 4) { + return null } const record = value as Record - if (record.data && typeof record.data === 'object' && !Array.isArray(record.data)) { - return record.data as Record + 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 + } } - return record + 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) { + 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 { + return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {} +} + +function getFirst(record: Record, keys: string[]) { + for (const key of keys) { + if (record[key] !== undefined && record[key] !== null) { + return record[key] + } + } + + return undefined +} + +function getString(record: Record, 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, 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, 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, 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 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 + 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, keys: string[]) { @@ -282,6 +775,14 @@ function patientGenderLabel(value: string) { 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 '待审核' @@ -306,3 +807,9 @@ function formatDateTime(value: string) { onMounted(loadReviewCases) + + diff --git a/src/views/CasesView.vue b/src/views/CasesView.vue index 2b4c9fcf..bf9f6046 100644 --- a/src/views/CasesView.vue +++ b/src/views/CasesView.vue @@ -550,7 +550,7 @@ -
+

评分规则

@@ -799,11 +799,8 @@ const searchPlaceholder = computed(() => (isContentAdmin.value ? '搜索病例' 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()) - : [] + detailForm.scoring_rules.filter(rule => rule.dimension.trim() || rule.scoring_standard.trim()) ) const relationsCurrentTitle = computed(() => { if (!relationsCase.value) { @@ -1503,8 +1500,8 @@ function fillCaseFormFromImportedPdf(result: ImportCasePdfResult) { 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) + const scoringRules = getImportScoringRules(result.raw, result.data, record) + const examItems = getImportExamItems(result.raw, result.data, record) caseForm.case_type = caseType caseForm.title = getImportString(record, ['title', 'name', 'case_title', 'caseTitle']) @@ -1539,8 +1536,8 @@ function fillDetailForm(row: CaseListItem, fullData: unknown) { const record = getDetailRecord(fullData) const traditional = getImportRecord(getImportFirst(record, ['traditional'])) const teaching = getImportRecord(getImportFirst(record, ['teaching'])) - const scoringRules = getImportScoringRules(record) - const examItems = getImportExamItems(record) + const scoringRules = getImportScoringRules(fullData, record) + const examItems = getImportExamItems(fullData, 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')) @@ -1646,8 +1643,8 @@ function getImportStringList(record: Record, keys: string[]): s return [] } -function getImportScoringRules(record: Record): ScoringRuleForm[] { - const raw = getImportFirst(record, ['scoring_rules', 'scoringRules']) +function getImportScoringRules(...sources: unknown[]): ScoringRuleForm[] { + const raw = findImportArray(['scoring_rules', 'scoringRules', 'score_rules', 'scoreRules', 'rules'], sources) if (!Array.isArray(raw)) { return [] } @@ -1661,14 +1658,14 @@ function getImportScoringRules(record: Record): ScoringRuleForm 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']) + scoring_standard: getImportString(itemRecord, ['scoring_standard', 'scoringStandard', 'standard', 'description', 'content']) } }) .filter(item => item.dimension || item.scoring_standard) } -function getImportExamItems(record: Record): ExamItemForm[] { - const raw = getImportFirst(record, ['exam_items', 'examItems']) +function getImportExamItems(...sources: unknown[]): ExamItemForm[] { + const raw = findImportArray(['exam_items', 'examItems', 'exams', 'exam_list', 'examList', 'items'], sources) if (!Array.isArray(raw)) { return [] } @@ -1680,12 +1677,70 @@ function getImportExamItems(record: Record): ExamItemForm[] { 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']) + result_text: getImportString(itemRecord, ['result_text', 'resultText', 'result', 'value', 'content']) } }) .filter(item => item.item_code || item.item_name || item.item_type || item.result_text) } +function findImportArray(keys: string[], sources: unknown[]) { + for (const source of sources) { + const found = findImportArrayInSource(source, keys) + if (found) { + return found + } + } + + return undefined +} + +function findImportArrayInSource(value: unknown, keys: string[], depth = 0): unknown[] | undefined { + if (!value || typeof value !== 'object' || depth > 5) { + return undefined + } + + if (Array.isArray(value)) { + return undefined + } + + const record = value as Record + 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 = findImportArrayInSource(record[key], keys, depth + 1) + if (found) { + return found + } + } + + for (const child of Object.values(record)) { + const found = findImportArrayInSource(child, keys, depth + 1) + if (found) { + return found + } + } + + return undefined +} + function normalizeImportCaseType(value: string, fallback: DraftCaseType): DraftCaseType { return value === 'teaching' ? 'teaching' : fallback } @@ -1740,12 +1795,7 @@ function findDetailRecord(value: unknown, depth = 0): Record | } const record = value as Record - if (hasCaseDetailFields(record)) { - return record - } - const preferredKeys = [ - 'data', 'case', 'case_detail', 'caseDetail', @@ -1753,9 +1803,10 @@ function findDetailRecord(value: unknown, depth = 0): Record | 'caseInfo', 'detail', 'full', - 'result', 'record', - 'item' + 'item', + 'data', + 'result' ] for (const key of preferredKeys) { const found = findDetailRecord(record[key], depth + 1) @@ -1764,6 +1815,10 @@ function findDetailRecord(value: unknown, depth = 0): Record | } } + if (hasCaseDetailFields(record)) { + return record + } + for (const child of Object.values(record)) { const found = findDetailRecord(child, depth + 1) if (found) { @@ -1783,12 +1838,16 @@ function hasCaseDetailFields(record: Record) { 'caseType', 'chief_complaint', 'chiefComplaint', + 'description', + 'summary', + 'content', + 'difficulty', + 'estimated_minutes', + 'estimatedMinutes', 'patient_age', 'patientAge', 'patient_gender', - 'patientGender', - 'publish_status', - 'publishStatus' + 'patientGender' ].some(key => record[key] !== undefined && record[key] !== null) } diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 234d370d..b87341ed 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -6,10 +6,6 @@

超级管理员数据驾驶舱

从机构、用户活跃、训练效果、病例资产和 AI 服务稳定性监控平台整体运营状况。

-
- 导出报表 - 刷新数据 -
@@ -76,7 +72,6 @@ import { computed, onMounted, ref } from 'vue' import type { EChartsOption } from 'echarts' import { ElMessage } from 'element-plus' -import { Download, Refresh } from '@element-plus/icons-vue' import ChartPanel from '@/components/ChartPanel.vue' import { fetchPlatformOverview, type PlatformOverview } from '@/api/stats' import { useAppStore } from '@/stores/app'