fix: 解决bug

This commit is contained in:
王天骄
2026-06-16 16:36:14 +08:00
parent fc7ddff6c6
commit bf77619551
6 changed files with 661 additions and 151 deletions
+289 -78
View File
@@ -136,13 +136,44 @@
</el-form-item>
</el-col>
<el-col v-if="canManageInstitution" :span="8">
<el-form-item label="机构ID">
<el-input-number v-model="caseForm.institution_id" :min="1" :precision="0" :controls="false" placeholder="缺省落创建者机构" />
<el-form-item label="机构">
<el-select
v-model="caseForm.institution_id"
:loading="loadingInstitutions"
clearable
filterable
placeholder="请选择机构"
@change="handleCaseInstitutionChange"
@visible-change="handleInstitutionVisibleChange"
>
<el-option
v-for="item in institutionOptions"
:key="item.id"
:label="institutionOptionLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="科室名称">
<el-input v-model="caseForm.department_name" placeholder="请输入科室名称" />
<el-form-item label="科室">
<el-select
v-model="caseForm.department_id"
:disabled="canManageInstitution && !caseForm.institution_id"
:loading="loadingDepartments"
clearable
filterable
placeholder="请选择科室"
@change="handleCaseDepartmentChange"
@visible-change="handleDepartmentVisibleChange"
>
<el-option
v-for="item in departmentOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
@@ -243,20 +274,6 @@
</template>
</div>
<div class="case-form-section">
<div class="case-section-title">
<h3>评分规则</h3>
<el-button :icon="Plus" @click="addScoringRule">添加规则</el-button>
</div>
<div v-for="(rule, index) in caseForm.scoring_rules" :key="index" class="scoring-rule-row">
<el-input v-model="rule.dimension" placeholder="维度" />
<el-input-number v-model="rule.score_weight" :min="0.01" :max="1" :step="0.05" :precision="2" :controls="false" placeholder="权重" />
<el-switch v-model="rule.ai_auto_score" active-text="AI评分" inactive-text="人工" />
<el-input v-model="rule.scoring_standard" placeholder="评分标准" />
<el-button :icon="Delete" :disabled="caseForm.scoring_rules.length === 1" circle @click="removeScoringRule(index)" />
</div>
</div>
<div class="case-form-section">
<div class="case-section-title">
<h3>检查/检验项目</h3>
@@ -372,6 +389,7 @@
class="case-create-form case-detail-form"
:model="detailForm"
:rules="caseRules"
:disabled="!canEditDetailCase"
label-position="top"
>
<div class="case-form-section">
@@ -387,20 +405,51 @@
</el-col>
<el-col :span="12">
<el-form-item label="病例类型" prop="case_type">
<el-select v-model="detailForm.case_type" placeholder="请选择病例类型">
<el-select v-model="detailForm.case_type" disabled placeholder="请选择病例类型">
<el-option label="传统病例" value="traditional" />
<el-option label="教学病例" value="teaching" />
</el-select>
</el-form-item>
</el-col>
<el-col v-if="canManageInstitution" :span="8">
<el-form-item label="机构ID">
<el-input-number v-model="detailForm.institution_id" :min="1" :precision="0" :controls="false" placeholder="缺省落创建者机构" />
<el-form-item label="机构">
<el-select
v-model="detailForm.institution_id"
:loading="loadingInstitutions"
clearable
filterable
placeholder="请选择机构"
@change="handleDetailInstitutionChange"
@visible-change="handleInstitutionVisibleChange"
>
<el-option
v-for="item in institutionOptions"
:key="item.id"
:label="institutionOptionLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="科室名称">
<el-input v-model="detailForm.department_name" placeholder="请输入科室名称" />
<el-form-item label="科室">
<el-select
v-model="detailForm.department_id"
:disabled="canManageInstitution && !detailForm.institution_id"
:loading="loadingDepartments"
clearable
filterable
placeholder="请选择科室"
@change="handleDetailDepartmentChange"
@visible-change="handleDepartmentVisibleChange"
>
<el-option
v-for="item in departmentOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
@@ -501,39 +550,40 @@
</template>
</div>
<div class="case-form-section">
<div v-if="showPublishedScoringRules" class="case-form-section">
<div class="case-section-title">
<h3>评分规则</h3>
<el-button :icon="Plus" @click="addDetailScoringRule">添加规则</el-button>
</div>
<div v-for="(rule, index) in detailForm.scoring_rules" :key="`detail-rule-${index}`" class="scoring-rule-row">
<el-input v-model="rule.dimension" placeholder="维度" />
<el-input-number v-model="rule.score_weight" :min="0.01" :max="1" :step="0.05" :precision="2" :controls="false" placeholder="权重" />
<el-switch v-model="rule.ai_auto_score" active-text="AI评分" inactive-text="人工" />
<el-input v-model="rule.scoring_standard" placeholder="评分标准" />
<el-button :icon="Delete" :disabled="detailForm.scoring_rules.length === 1" circle @click="removeDetailScoringRule(index)" />
</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>
<el-button :icon="Plus" @click="addDetailExamItem">添加项目</el-button>
<el-button :icon="Plus" :disabled="!canEditDetailCase" @click="addDetailExamItem">添加项目</el-button>
</div>
<div v-if="!detailForm.exam_items.length" class="case-empty-line">未添加项目可直接提交</div>
<div v-if="!detailForm.exam_items.length" class="case-empty-line">未添加项目可直接保存草稿</div>
<div v-for="(item, index) in detailForm.exam_items" :key="`detail-item-${index}`" class="exam-item-row">
<el-input v-model="item.item_code" placeholder="项目编码,必须唯一" />
<el-input v-model="item.item_name" placeholder="项目名称" />
<el-input v-model="item.item_type" placeholder="项目类型" />
<el-input v-model="item.result_text" placeholder="结果文本" />
<el-button :icon="Delete" circle @click="removeDetailExamItem(index)" />
<el-button :icon="Delete" :disabled="!canEditDetailCase" circle @click="removeDetailExamItem(index)" />
</div>
</div>
</el-form>
<div class="drawer-form-footer">
<el-button @click="detailDrawerVisible = false">取消</el-button>
<el-button :loading="submittingDetail" type="primary" @click="submitDetailForm">提交</el-button>
<el-button @click="detailDrawerVisible = false">{{ canEditDetailCase ? '取消' : '关闭' }}</el-button>
<el-button v-if="canEditDetailCase" :loading="submittingDetail" type="primary" @click="submitDetailForm">保存草稿</el-button>
</div>
</div>
</el-drawer>
@@ -551,6 +601,7 @@ import {
fetchCaseFull,
fetchCases,
importCasePdf,
saveCaseDraft,
submitCase,
updateCaseRelations,
type CaseExamItemPayload,
@@ -560,8 +611,10 @@ import {
type CaseScoringRulePayload,
type CreateCaseDraftPayload,
type DraftCaseType,
type SaveCaseDraftPayload,
type UpdateCaseRelationsPayload
} from '@/api/cases'
import { fetchInstitutionList, fetchMyDepartments, type DepartmentOption, type InstitutionOption } from '@/api/users'
import { useAppStore } from '@/stores/app'
type RelationInstitutionMode = 'keep' | 'set' | 'clear'
@@ -585,6 +638,7 @@ interface CaseDraftForm {
title: string
case_type: DraftCaseType
institution_id?: number
department_id?: number
department_name: string
difficulty: string
chief_complaint: string
@@ -612,6 +666,8 @@ const importing = ref(false)
const savingRelations = ref(false)
const detailLoading = ref(false)
const submittingDetail = ref(false)
const loadingInstitutions = ref(false)
const loadingDepartments = ref(false)
const caseDialogVisible = ref(false)
const importDialogVisible = ref(false)
const relationsDialogVisible = ref(false)
@@ -623,6 +679,8 @@ const importFile = ref<File | null>(null)
const importCaseType = ref<DraftCaseType>('traditional')
const importFileList = ref<UploadUserFile[]>([])
const cases = ref<CaseListItem[]>([])
const institutionOptions = ref<InstitutionOption[]>([])
const departmentOptions = ref<DepartmentOption[]>([])
const relationsCase = ref<CaseListItem | null>(null)
const detailCase = ref<CaseListItem | null>(null)
const caseDetail = ref<unknown>(null)
@@ -656,6 +714,7 @@ const caseForm = reactive<CaseDraftForm>({
title: '',
case_type: 'traditional',
institution_id: undefined,
department_id: undefined,
department_name: '',
difficulty: '',
chief_complaint: '',
@@ -680,6 +739,7 @@ const detailForm = reactive<CaseDraftForm>({
title: '',
case_type: 'traditional',
institution_id: undefined,
department_id: undefined,
department_name: '',
difficulty: '',
chief_complaint: '',
@@ -731,6 +791,13 @@ const pageDescription = computed(() =>
const searchPlaceholder = computed(() => (isContentAdmin.value ? '搜索病例' : '搜索标题/主诉/标签/ICD'))
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())
: []
)
const relationsCurrentTitle = computed(() => {
if (!relationsCase.value) {
return ''
@@ -802,6 +869,8 @@ function handleSizeChange() {
function openCreateDialog() {
caseDialogVisible.value = true
loadInstitutionOptions()
loadDepartmentOptions(caseForm.institution_id)
}
function resetCaseForm() {
@@ -820,6 +889,7 @@ function resetDraftForm(form: CaseDraftForm) {
form.title = ''
form.case_type = 'traditional'
form.institution_id = undefined
form.department_id = undefined
form.department_name = ''
form.difficulty = ''
form.chief_complaint = ''
@@ -840,18 +910,6 @@ function resetDraftForm(form: CaseDraftForm) {
form.exam_items = []
}
function addScoringRule() {
caseForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' })
}
function removeScoringRule(index: number) {
if (caseForm.scoring_rules.length <= 1) {
return
}
caseForm.scoring_rules.splice(index, 1)
}
function addExamItem() {
caseForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
}
@@ -860,26 +918,104 @@ function removeExamItem(index: number) {
caseForm.exam_items.splice(index, 1)
}
function addDetailScoringRule() {
detailForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' })
}
function removeDetailScoringRule(index: number) {
if (detailForm.scoring_rules.length <= 1) {
function addDetailExamItem() {
if (!canEditDetailCase.value) {
return
}
detailForm.scoring_rules.splice(index, 1)
}
function addDetailExamItem() {
detailForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
}
function removeDetailExamItem(index: number) {
if (!canEditDetailCase.value) {
return
}
detailForm.exam_items.splice(index, 1)
}
async function loadInstitutionOptions() {
if (!appStore.token || !canManageInstitution.value || loadingInstitutions.value || institutionOptions.value.length) {
return
}
try {
loadingInstitutions.value = true
institutionOptions.value = await fetchInstitutionList(appStore.token)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取机构列表失败')
} finally {
loadingInstitutions.value = false
}
}
async function loadDepartmentOptions(institutionId?: number) {
if (!appStore.token || loadingDepartments.value) {
return
}
if (canManageInstitution.value && !institutionId) {
departmentOptions.value = []
return
}
try {
loadingDepartments.value = true
departmentOptions.value = await fetchMyDepartments(appStore.token, institutionId)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取科室列表失败')
} finally {
loadingDepartments.value = false
}
}
function handleInstitutionVisibleChange(visible: boolean) {
if (visible) {
loadInstitutionOptions()
}
}
function handleDepartmentVisibleChange(visible: boolean) {
if (visible) {
loadDepartmentOptions(activeCaseForm().institution_id)
}
}
function handleCaseInstitutionChange() {
caseForm.department_id = undefined
caseForm.department_name = ''
loadDepartmentOptions(caseForm.institution_id)
}
function handleDetailInstitutionChange() {
detailForm.department_id = undefined
detailForm.department_name = ''
loadDepartmentOptions(detailForm.institution_id)
}
function handleCaseDepartmentChange(value?: number) {
caseForm.department_name = getDepartmentName(value)
}
function handleDetailDepartmentChange(value?: number) {
detailForm.department_name = getDepartmentName(value)
}
function activeCaseForm() {
return detailDrawerVisible.value ? detailForm : caseForm
}
function getDepartmentName(value?: number) {
if (!value) {
return ''
}
return departmentOptions.value.find(item => item.id === value)?.name || ''
}
function institutionOptionLabel(item: InstitutionOption) {
return item.code ? `${item.name} (${item.code})` : item.name
}
async function submitCaseForm() {
if (!appStore.token) {
ElMessage.warning('缺少登录信息,请重新登录')
@@ -932,6 +1068,7 @@ function buildDraftPayload(form: CaseDraftForm): CreateCaseDraftPayload {
payload[form.case_type] = structure
if (canManageInstitution.value && form.institution_id) payload.institution_id = form.institution_id
if (form.department_id) payload.department_id = form.department_id
if (form.department_name.trim()) payload.department_name = form.department_name.trim()
if (form.difficulty.trim()) payload.difficulty = form.difficulty.trim()
if (form.chief_complaint.trim()) payload.chief_complaint = form.chief_complaint.trim()
@@ -949,6 +1086,33 @@ function buildDraftPayload(form: CaseDraftForm): CreateCaseDraftPayload {
return payload
}
function buildSaveDraftPayload(form: CaseDraftForm): SaveCaseDraftPayload {
const structure = buildEditableCaseStructure(form)
const scoringRules = normalizeScoringRules(form)
const examItems = normalizeExamItems(form)
const payload: SaveCaseDraftPayload = {
title: form.title.trim(),
department_name: form.department_name.trim(),
difficulty: form.difficulty.trim(),
chief_complaint: form.chief_complaint.trim(),
description: form.description.trim(),
patient_gender: form.patient_gender,
tags: isContentAdmin.value ? form.tags.trim() : parseTextList(form.tags),
icd_codes: parseTextList(form.icd_codes),
osce_enabled: form.osce_enabled,
scoring_rules: scoringRules,
exam_items: examItems
}
payload[form.case_type] = structure
if (canManageInstitution.value && form.institution_id) payload.institution_id = form.institution_id
if (form.department_id) payload.department_id = form.department_id
if (form.patient_age !== undefined) payload.patient_age = form.patient_age
if (form.estimated_minutes !== undefined) payload.estimated_minutes = form.estimated_minutes
return payload
}
function buildCaseStructure(form: CaseDraftForm): Record<string, unknown> {
if (form.case_type === 'teaching') {
if (!form.teaching_learning_objectives.trim()) {
@@ -973,27 +1137,49 @@ function buildCaseStructure(form: CaseDraftForm): Record<string, unknown> {
}
}
function buildEditableCaseStructure(form: CaseDraftForm): Record<string, unknown> {
if (form.case_type === 'teaching') {
if (!form.teaching_learning_objectives.trim()) {
throw new Error('请输入教学目标')
}
return {
learning_objectives: form.teaching_learning_objectives.trim(),
key_points: form.teaching_key_points.trim(),
reference_answer: form.teaching_reference_answer.trim()
}
}
if (!form.traditional_standard_diagnosis.trim()) {
throw new Error('请输入标准诊断')
}
return {
standard_diagnosis: form.traditional_standard_diagnosis.trim(),
standard_treatment: form.traditional_standard_treatment.trim(),
guideline_reference: form.traditional_guideline_reference.trim()
}
}
function normalizeScoringRules(form: CaseDraftForm): CaseScoringRulePayload[] {
const rules = form.scoring_rules
.map(rule => ({
dimension: rule.dimension.trim(),
score_weight: Number(rule.score_weight),
ai_auto_score: rule.ai_auto_score,
scoring_standard: rule.scoring_standard.trim()
}))
.filter(rule => rule.dimension || rule.scoring_standard || Number.isFinite(rule.score_weight))
.map((rule, index) => {
const scoreWeight = Number(rule.score_weight)
return {
dimension: rule.dimension.trim() || (rule.scoring_standard.trim() ? `评分维度${index + 1}` : ''),
score_weight: Number.isFinite(scoreWeight) && scoreWeight > 0 && scoreWeight <= 1 ? scoreWeight : 1,
ai_auto_score: rule.ai_auto_score,
scoring_standard: rule.scoring_standard.trim()
}
})
.filter(rule => rule.dimension || rule.scoring_standard)
if (!rules.length) {
throw new Error('至少添加 1 条评分规则')
return [createDefaultScoringRule(form)]
}
for (const rule of rules) {
if (!rule.dimension) {
throw new Error('评分规则的维度必填')
}
if (!Number.isFinite(rule.score_weight) || rule.score_weight <= 0 || rule.score_weight > 1) {
throw new Error('评分规则权重必须大于 0 且不超过 1')
}
rule.ai_auto_score = true
}
return rules.map(rule => ({
@@ -1004,6 +1190,15 @@ function normalizeScoringRules(form: CaseDraftForm): CaseScoringRulePayload[] {
}))
}
function createDefaultScoringRule(form: CaseDraftForm): CaseScoringRulePayload {
return {
dimension: form.case_type === 'teaching' ? '教学目标达成' : '诊断与处置',
score_weight: 1,
ai_auto_score: true,
scoring_standard: '由AI根据病例内容生成评分标准'
}
}
function normalizeExamItems(form: CaseDraftForm): CaseExamItemPayload[] {
const items = form.exam_items
.map(item => ({
@@ -1228,6 +1423,7 @@ async function openDetailDrawer(row: CaseListItem) {
detailCase.value = row
detailDrawerVisible.value = true
caseDetail.value = null
loadInstitutionOptions()
try {
detailLoading.value = true
@@ -1236,6 +1432,7 @@ async function openDetailDrawer(row: CaseListItem) {
id: row.id
})
fillDetailForm(row, caseDetail.value)
loadDepartmentOptions(detailForm.institution_id)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '获取病例详情失败')
} finally {
@@ -1248,15 +1445,19 @@ async function submitDetailForm() {
ElMessage.warning('缺少登录信息,请重新登录')
return
}
if (!canEditDetailCase.value) {
ElMessage.warning('只有草稿病例可以保存草稿')
return
}
const isValid = await detailFormRef.value?.validate().catch(() => false)
if (!isValid) {
return
}
let payload: CreateCaseDraftPayload
let payload: SaveCaseDraftPayload
try {
payload = buildDraftPayload(detailForm)
payload = buildSaveDraftPayload(detailForm)
} catch (error) {
ElMessage.warning(error instanceof Error ? error.message : '请检查病例表单')
return
@@ -1264,16 +1465,16 @@ async function submitDetailForm() {
try {
submittingDetail.value = true
await submitCase({
await saveCaseDraft({
token: appStore.token,
id: detailCase.value.id,
payload
})
ElMessage.success('病例已提交')
ElMessage.success('病例草稿已保存')
detailDrawerVisible.value = false
await loadCases()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '提交病例失败')
ElMessage.error(error instanceof Error ? error.message : '保存病例草稿失败')
} finally {
submittingDetail.value = false
}
@@ -1299,6 +1500,7 @@ function fillCaseFormFromImportedPdf(result: ImportCasePdfResult) {
caseForm.case_type = caseType
caseForm.title = getImportString(record, ['title', 'name', 'case_title', 'caseTitle'])
caseForm.department_name = getImportString(record, ['department_name', 'departmentName'])
caseForm.department_id = getImportNumber(record, ['department_id', 'departmentId']) ?? undefined
caseForm.difficulty = getImportString(record, ['difficulty'])
caseForm.chief_complaint = getImportString(record, ['chief_complaint', 'chiefComplaint'])
caseForm.description = getImportString(record, ['description', 'summary', 'content'])
@@ -1334,6 +1536,7 @@ function fillDetailForm(row: CaseListItem, fullData: unknown) {
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'))
detailForm.institution_id = getImportNumber(record, ['institution_id', 'institutionId']) ?? undefined
detailForm.department_id = getImportNumber(record, ['department_id', 'departmentId']) ?? undefined
detailForm.department_name = getImportString(record, ['department_name', 'departmentName'], row.departmentName)
detailForm.difficulty = getImportString(record, ['difficulty'], row.difficulty)
detailForm.chief_complaint = getImportString(record, ['chief_complaint', 'chiefComplaint'], row.chiefComplaint)
@@ -1580,5 +1783,13 @@ function formatDateTime(value: string) {
return value.replace('T', ' ').slice(0, 19)
}
function formatScoreWeight(value: number) {
if (!Number.isFinite(value)) {
return '-'
}
return value <= 1 ? `${Math.round(value * 100)}%` : String(value)
}
onMounted(loadCases)
</script>