diff --git a/index.html b/index.html index 46d9818c..80948d5f 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - MediAI - 医疗AI平台管理系统 + 新方正集团
diff --git a/src/api/cases.ts b/src/api/cases.ts index 32d89bb3..5977e418 100644 --- a/src/api/cases.ts +++ b/src/api/cases.ts @@ -49,6 +49,7 @@ export interface CaseExamItemPayload { item_code: string item_name?: string item_type?: string + result_text?: string } export interface CreateCaseDraftPayload { @@ -111,9 +112,17 @@ export interface AiGenerateCaseResult { raw: unknown } +export interface ImportCasePdfResult { + parseId: string + caseType: DraftCaseType + data: Record + raw: unknown +} + export interface ImportCasePdfParams { token: string file: File + case_type: DraftCaseType } export interface UpdateCaseRelationsPayload { @@ -133,6 +142,12 @@ export interface CaseActionParams { id: string | number } +export interface SubmitCasePayload extends CreateCaseDraftPayload {} + +export interface SubmitCaseParams extends CaseActionParams { + payload?: SubmitCasePayload +} + const listKeys = ['results', 'list', 'rows', 'items', 'records', 'cases'] const totalKeys = ['count', 'total', 'total_count', 'totalCount'] @@ -231,6 +246,10 @@ function getStringList(record: Record, keys: string[]): string[ return [] } +function normalizeDraftCaseType(value: string, fallback: DraftCaseType = 'traditional'): DraftCaseType { + return value === 'teaching' ? 'teaching' : fallback +} + function normalizePublishStatus(value: unknown): CasePublishStatus | null { if (value === 0 || value === 1 || value === 2) { return value @@ -384,6 +403,22 @@ function normalizeAiGenerateResult(data: unknown): AiGenerateCaseResult { } } +function normalizeImportCasePdfResult(data: unknown, fallbackCaseType: DraftCaseType): ImportCasePdfResult { + const root = getRecord(data) + const payload = root.parse_id || root.parseId || root.case_type || root.caseType || root.title + ? root + : getRecord(root.data) + const nestedData = getRecord(payload.data) + const generatedData = Object.keys(nestedData).length ? nestedData : payload + + return { + parseId: getString(payload, ['parse_id', 'parseId']), + caseType: normalizeDraftCaseType(getString(payload, ['case_type', 'caseType']), fallbackCaseType), + data: generatedData, + raw: data + } +} + function createCaseQuery(params: Partial) { const query = new URLSearchParams() if (params.search?.trim()) { @@ -489,9 +524,10 @@ export async function generateCaseWithAi(params: AiGenerateCaseParams): Promise< return normalizeAiGenerateResult(data) } -export async function importCasePdf(params: ImportCasePdfParams): Promise { +export async function importCasePdf(params: ImportCasePdfParams): Promise { const formData = new FormData() - formData.append('file', params.file) + formData.append('files', params.file) + formData.append('case_type', params.case_type) const response = await fetch('/server/api/cms/cases/import-pdf/', { method: 'POST', @@ -502,7 +538,8 @@ export async function importCasePdf(params: ImportCasePdfParams): Promise { @@ -519,13 +556,15 @@ export async function updateCaseRelations(params: UpdateCaseRelationsParams): Pr return parseMutationResponse(response, '编辑病例关联失败') } -export async function submitCase(params: CaseActionParams): Promise { +export async function submitCase(params: SubmitCaseParams): Promise { const response = await fetch(`/server/api/cms/cases/${params.id}/submit/`, { method: 'POST', headers: { Accept: 'application/json', - Authorization: createAuthorization(params.token) - } + Authorization: createAuthorization(params.token), + ...(params.payload ? { 'Content-Type': 'application/json' } : {}) + }, + ...(params.payload ? { body: JSON.stringify(params.payload) } : {}) }) return parseMutationResponse(response, '提交病例失败') diff --git a/src/api/users.ts b/src/api/users.ts index 5a1b8363..0e455157 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -394,7 +394,7 @@ export async function resetUserPassword(params: ResetUserPasswordParams): Promis export async function importUsers(params: ImportUsersParams): Promise { const formData = new FormData() - formData.append('file', params.file) + formData.append('files', params.file) const response = await fetch('/server/api/cms/users/import/', { method: 'POST', diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 00000000..bbad25ea Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/assets/styles/main.scss b/src/assets/styles/main.scss index d0d3f923..beec6d5d 100644 --- a/src/assets/styles/main.scss +++ b/src/assets/styles/main.scss @@ -90,13 +90,21 @@ p { display: inline-flex; align-items: center; justify-content: center; + gap: 10px; height: 42px; - padding: 0 16px; + padding: 0 16px 0 8px; border: 1px solid rgb(255 255 255 / 38%); border-radius: 8px; font-size: 18px; font-weight: 800; background: rgb(255 255 255 / 12%); + + img { + width: 30px; + height: 30px; + border-radius: 6px; + object-fit: cover; + } } .login-brand h1 { @@ -194,17 +202,46 @@ p { transition: grid-template-columns 0.22s ease; &.collapsed { - grid-template-columns: 82px 1fr; + grid-template-columns: 88px 1fr; .brand-copy, + .nav-section-title, .nav-item span, .user-meta { display: none; } - .sidebar-header, + .sidebar-header { + display: grid; + grid-template-rows: 38px 26px; + align-content: center; + justify-content: center; + gap: 8px; + padding: 0; + } + .nav-item { justify-content: center; + padding: 0; + } + + .nav-list { + padding: 16px 18px; + } + + .sidebar-toggle { + position: static; + margin: 0 auto; + width: 26px; + height: 26px; + min-height: 26px; + color: #c7d2fe; + background: rgb(255 255 255 / 12%); + } + + .user-card { + justify-content: center; + margin-bottom: 0; } .sidebar-footer { @@ -235,6 +272,7 @@ p { } .sidebar-header { + position: relative; display: flex; align-items: center; gap: 12px; @@ -250,13 +288,20 @@ p { justify-content: center; width: 38px; height: 38px; + overflow: hidden; border-radius: 8px; - color: #fff; - font-weight: 800; - background: linear-gradient(135deg, var(--primary), var(--teal)); + background: #fff; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } } .brand-copy { + min-width: 0; + strong, span { display: block; @@ -264,7 +309,8 @@ p { strong { color: #fff; - font-size: 18px; + font-size: 17px; + white-space: nowrap; } span { @@ -274,6 +320,20 @@ p { } } +.sidebar-toggle { + margin-left: auto; + color: #dbeafe; + border-color: rgb(255 255 255 / 22%); + background: rgb(255 255 255 / 8%); + + &:hover, + &:focus { + color: #fff; + border-color: rgb(255 255 255 / 38%); + background: rgb(37 99 235 / 34%); + } +} + .sidebar-scroll { flex: 1; } @@ -307,6 +367,7 @@ p { transition: background 0.16s ease, color 0.16s ease; .el-icon { + flex: 0 0 auto; font-size: 18px; } @@ -801,6 +862,7 @@ p { } .case-create-form, +.case-import-form, .case-relations-form { .el-select, .el-input-number { @@ -844,7 +906,7 @@ p { } .exam-item-row { - grid-template-columns: minmax(150px, 0.8fr) minmax(160px, 1fr) minmax(120px, 0.8fr) 36px; + grid-template-columns: minmax(130px, 0.8fr) minmax(140px, 1fr) minmax(110px, 0.7fr) minmax(150px, 1fr) 36px; } .case-empty-line { @@ -862,6 +924,18 @@ p { .case-detail-drawer { display: grid; gap: 16px; + min-height: 100%; +} + +.case-detail-form { + align-content: start; +} + +.drawer-form-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 8px; } .kanban-grid { @@ -990,6 +1064,16 @@ p { gap: 16px; } +.ai-result-actions { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.ai-draft-form { + min-width: 0; +} + .ai-result-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 07149468..91222a58 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -2,11 +2,16 @@
@@ -388,6 +554,7 @@ import { submitCase, updateCaseRelations, type CaseExamItemPayload, + type ImportCasePdfResult, type CaseListItem, type CasePublishStatus, type CaseScoringRulePayload, @@ -411,6 +578,7 @@ interface ExamItemForm { item_code: string item_name: string item_type: string + result_text: string } interface CaseDraftForm { @@ -443,13 +611,16 @@ const savingCase = ref(false) const importing = ref(false) const savingRelations = ref(false) const detailLoading = ref(false) +const submittingDetail = ref(false) const caseDialogVisible = ref(false) const importDialogVisible = ref(false) const relationsDialogVisible = ref(false) const detailDrawerVisible = ref(false) const caseFormRef = ref() +const detailFormRef = ref() const importUploadRef = ref() const importFile = ref(null) +const importCaseType = ref('traditional') const importFileList = ref([]) const cases = ref([]) const relationsCase = ref(null) @@ -505,6 +676,30 @@ const caseForm = reactive({ exam_items: [] }) +const detailForm = reactive({ + title: '', + case_type: 'traditional', + institution_id: undefined, + 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: [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }], + exam_items: [] +}) + const relationsForm = reactive<{ institutionMode: RelationInstitutionMode institution_id?: number @@ -610,30 +805,41 @@ function openCreateDialog() { } function resetCaseForm() { - caseForm.title = '' - caseForm.case_type = 'traditional' - caseForm.institution_id = undefined - caseForm.department_name = '' - caseForm.difficulty = '' - caseForm.chief_complaint = '' - caseForm.description = '' - caseForm.patient_age = undefined - caseForm.patient_gender = '' - caseForm.tags = '' - caseForm.icd_codes = '' - caseForm.estimated_minutes = undefined - caseForm.osce_enabled = false - caseForm.traditional_standard_diagnosis = '' - caseForm.traditional_standard_treatment = '' - caseForm.traditional_guideline_reference = '' - caseForm.teaching_learning_objectives = '' - caseForm.teaching_key_points = '' - caseForm.teaching_reference_answer = '' - caseForm.scoring_rules = [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }] - caseForm.exam_items = [] + resetDraftForm(caseForm) caseFormRef.value?.clearValidate() } +function resetDetailForm() { + detailCase.value = null + caseDetail.value = null + resetDraftForm(detailForm) + detailFormRef.value?.clearValidate() +} + +function resetDraftForm(form: CaseDraftForm) { + form.title = '' + form.case_type = 'traditional' + form.institution_id = undefined + form.department_name = '' + form.difficulty = '' + form.chief_complaint = '' + form.description = '' + form.patient_age = undefined + form.patient_gender = '' + form.tags = '' + form.icd_codes = '' + form.estimated_minutes = undefined + form.osce_enabled = false + form.traditional_standard_diagnosis = '' + form.traditional_standard_treatment = '' + form.traditional_guideline_reference = '' + form.teaching_learning_objectives = '' + form.teaching_key_points = '' + form.teaching_reference_answer = '' + form.scoring_rules = [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }] + form.exam_items = [] +} + function addScoringRule() { caseForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }) } @@ -647,13 +853,33 @@ function removeScoringRule(index: number) { } function addExamItem() { - caseForm.exam_items.push({ item_code: '', item_name: '', item_type: '' }) + caseForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' }) } 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) { + 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) { + detailForm.exam_items.splice(index, 1) +} + async function submitCaseForm() { if (!appStore.token) { ElMessage.warning('缺少登录信息,请重新登录') @@ -691,60 +917,64 @@ async function submitCaseForm() { } function buildCreatePayload(): CreateCaseDraftPayload { - const structure = buildCaseStructure() - const scoringRules = normalizeScoringRules() - const examItems = normalizeExamItems() + return buildDraftPayload(caseForm) +} + +function buildDraftPayload(form: CaseDraftForm): CreateCaseDraftPayload { + const structure = buildCaseStructure(form) + const scoringRules = normalizeScoringRules(form) + const examItems = normalizeExamItems(form) const payload: CreateCaseDraftPayload = { - title: caseForm.title.trim(), - case_type: caseForm.case_type, + title: form.title.trim(), + case_type: form.case_type, scoring_rules: scoringRules } - payload[caseForm.case_type] = structure - if (canManageInstitution.value && caseForm.institution_id) payload.institution_id = caseForm.institution_id - if (caseForm.department_name.trim()) payload.department_name = caseForm.department_name.trim() - if (caseForm.difficulty.trim()) payload.difficulty = caseForm.difficulty.trim() - if (caseForm.chief_complaint.trim()) payload.chief_complaint = caseForm.chief_complaint.trim() - if (caseForm.description.trim()) payload.description = caseForm.description.trim() - if (caseForm.patient_age !== undefined) payload.patient_age = caseForm.patient_age - if (caseForm.patient_gender) payload.patient_gender = caseForm.patient_gender - if (caseForm.tags.trim()) { - payload.tags = isContentAdmin.value ? caseForm.tags.trim() : parseTextList(caseForm.tags) + payload[form.case_type] = structure + if (canManageInstitution.value && form.institution_id) payload.institution_id = form.institution_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() + if (form.description.trim()) payload.description = form.description.trim() + if (form.patient_age !== undefined) payload.patient_age = form.patient_age + if (form.patient_gender) payload.patient_gender = form.patient_gender + if (form.tags.trim()) { + payload.tags = isContentAdmin.value ? form.tags.trim() : parseTextList(form.tags) } - if (parseTextList(caseForm.icd_codes).length) payload.icd_codes = parseTextList(caseForm.icd_codes) - if (caseForm.estimated_minutes !== undefined) payload.estimated_minutes = caseForm.estimated_minutes - if (caseForm.osce_enabled) payload.osce_enabled = caseForm.osce_enabled + if (parseTextList(form.icd_codes).length) payload.icd_codes = parseTextList(form.icd_codes) + if (form.estimated_minutes !== undefined) payload.estimated_minutes = form.estimated_minutes + if (form.osce_enabled) payload.osce_enabled = form.osce_enabled if (examItems.length) payload.exam_items = examItems return payload } -function buildCaseStructure(): Record { - if (caseForm.case_type === 'teaching') { - if (!caseForm.teaching_learning_objectives.trim()) { +function buildCaseStructure(form: CaseDraftForm): Record { + if (form.case_type === 'teaching') { + if (!form.teaching_learning_objectives.trim()) { throw new Error('请输入教学目标') } return { - learning_objectives: caseForm.teaching_learning_objectives.trim(), - ...(caseForm.teaching_key_points.trim() ? { key_points: caseForm.teaching_key_points.trim() } : {}), - ...(caseForm.teaching_reference_answer.trim() ? { reference_answer: caseForm.teaching_reference_answer.trim() } : {}) + learning_objectives: form.teaching_learning_objectives.trim(), + ...(form.teaching_key_points.trim() ? { key_points: form.teaching_key_points.trim() } : {}), + ...(form.teaching_reference_answer.trim() ? { reference_answer: form.teaching_reference_answer.trim() } : {}) } } - if (!caseForm.traditional_standard_diagnosis.trim()) { + if (!form.traditional_standard_diagnosis.trim()) { throw new Error('请输入标准诊断') } return { - standard_diagnosis: caseForm.traditional_standard_diagnosis.trim(), - ...(caseForm.traditional_standard_treatment.trim() ? { standard_treatment: caseForm.traditional_standard_treatment.trim() } : {}), - ...(caseForm.traditional_guideline_reference.trim() ? { guideline_reference: caseForm.traditional_guideline_reference.trim() } : {}) + standard_diagnosis: form.traditional_standard_diagnosis.trim(), + ...(form.traditional_standard_treatment.trim() ? { standard_treatment: form.traditional_standard_treatment.trim() } : {}), + ...(form.traditional_guideline_reference.trim() ? { guideline_reference: form.traditional_guideline_reference.trim() } : {}) } } -function normalizeScoringRules(): CaseScoringRulePayload[] { - const rules = caseForm.scoring_rules +function normalizeScoringRules(form: CaseDraftForm): CaseScoringRulePayload[] { + const rules = form.scoring_rules .map(rule => ({ dimension: rule.dimension.trim(), score_weight: Number(rule.score_weight), @@ -774,14 +1004,15 @@ function normalizeScoringRules(): CaseScoringRulePayload[] { })) } -function normalizeExamItems(): CaseExamItemPayload[] { - const items = caseForm.exam_items +function normalizeExamItems(form: CaseDraftForm): CaseExamItemPayload[] { + const items = form.exam_items .map(item => ({ item_code: item.item_code.trim(), item_name: item.item_name.trim(), - item_type: item.item_type.trim() + item_type: item.item_type.trim(), + result_text: item.result_text.trim() })) - .filter(item => item.item_code || item.item_name || item.item_type) + .filter(item => item.item_code || item.item_name || item.item_type || item.result_text) const codes = new Set() for (const item of items) { @@ -797,7 +1028,8 @@ function normalizeExamItems(): CaseExamItemPayload[] { return items.map(item => ({ item_code: item.item_code, ...(item.item_name ? { item_name: item.item_name } : {}), - ...(item.item_type ? { item_type: item.item_type } : {}) + ...(item.item_type ? { item_type: item.item_type } : {}), + ...(item.result_text ? { result_text: item.result_text } : {}) })) } @@ -807,6 +1039,7 @@ function openImportDialog() { function resetImportFile() { importFile.value = null + importCaseType.value = 'traditional' importFileList.value = [] importUploadRef.value?.clearFiles() } @@ -842,14 +1075,15 @@ async function submitImportPdf() { try { importing.value = true - await importCasePdf({ + const result = await importCasePdf({ token: appStore.token, - file: importFile.value + file: importFile.value, + case_type: importCaseType.value }) - ElMessage.success('PDF 病例导入完成') + fillCaseFormFromImportedPdf(result) importDialogVisible.value = false - pagination.page = 1 - await loadCases() + caseDialogVisible.value = true + ElMessage.success('PDF 病例导入完成,请确认后保存草稿') } catch (error) { ElMessage.error(error instanceof Error ? error.message : '导入 PDF 病例失败') } finally { @@ -1001,6 +1235,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 { @@ -1008,6 +1243,42 @@ async function openDetailDrawer(row: CaseListItem) { } } +async function submitDetailForm() { + if (!appStore.token || !detailCase.value) { + ElMessage.warning('缺少登录信息,请重新登录') + return + } + + const isValid = await detailFormRef.value?.validate().catch(() => false) + if (!isValid) { + return + } + + let payload: CreateCaseDraftPayload + try { + payload = buildDraftPayload(detailForm) + } catch (error) { + ElMessage.warning(error instanceof Error ? error.message : '请检查病例表单') + return + } + + try { + submittingDetail.value = true + await submitCase({ + token: appStore.token, + id: detailCase.value.id, + payload + }) + ElMessage.success('病例已提交') + detailDrawerVisible.value = false + await loadCases() + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : '提交病例失败') + } finally { + submittingDetail.value = false + } +} + function parseTextList(value: string): string[] { return value .split(/[,\n,;;]/) @@ -1015,6 +1286,205 @@ function parseTextList(value: string): string[] { .filter(Boolean) } +function fillCaseFormFromImportedPdf(result: ImportCasePdfResult) { + resetDraftForm(caseForm) + + const record = getImportRecord(result.data) + 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) + + caseForm.case_type = caseType + caseForm.title = getImportString(record, ['title', 'name', 'case_title', 'caseTitle']) + caseForm.department_name = getImportString(record, ['department_name', 'departmentName']) + caseForm.difficulty = getImportString(record, ['difficulty']) + caseForm.chief_complaint = getImportString(record, ['chief_complaint', 'chiefComplaint']) + caseForm.description = getImportString(record, ['description', 'summary', 'content']) + caseForm.patient_age = getImportNumber(record, ['patient_age', 'patientAge']) ?? undefined + caseForm.patient_gender = normalizeImportGender(getImportString(record, ['patient_gender', 'patientGender'])) + caseForm.tags = getImportStringList(record, ['tags', 'tag_list', 'tagList']).join(', ') + caseForm.icd_codes = getImportStringList(record, ['icd_codes', 'icdCodes', 'icd_list', 'icdList']).join(', ') + caseForm.estimated_minutes = getImportNumber(record, ['estimated_minutes', 'estimatedMinutes']) ?? undefined + caseForm.osce_enabled = getImportBoolean(record, ['osce_enabled', 'osceEnabled']) + + caseForm.traditional_standard_diagnosis = getImportString(traditional, ['standard_diagnosis', 'standardDiagnosis']) + caseForm.traditional_standard_treatment = getImportString(traditional, ['standard_treatment', 'standardTreatment']) + caseForm.traditional_guideline_reference = getImportString(traditional, ['guideline_reference', 'guidelineReference']) + caseForm.teaching_learning_objectives = getImportString(teaching, ['learning_objectives', 'learningObjectives']) + caseForm.teaching_key_points = getImportString(teaching, ['key_points', 'keyPoints']) + caseForm.teaching_reference_answer = getImportString(teaching, ['reference_answer', 'referenceAnswer']) + caseForm.scoring_rules = scoringRules.length + ? scoringRules + : [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }] + caseForm.exam_items = examItems + caseFormRef.value?.clearValidate() +} + +function fillDetailForm(row: CaseListItem, fullData: unknown) { + resetDraftForm(detailForm) + + const record = getDetailRecord(fullData) + const traditional = getImportRecord(getImportFirst(record, ['traditional'])) + const teaching = getImportRecord(getImportFirst(record, ['teaching'])) + const scoringRules = getImportScoringRules(record) + const examItems = getImportExamItems(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')) + detailForm.institution_id = getImportNumber(record, ['institution_id', 'institutionId']) ?? 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) + detailForm.description = getImportString(record, ['description', 'summary', 'content']) + detailForm.patient_age = getImportNumber(record, ['patient_age', 'patientAge']) ?? undefined + detailForm.patient_gender = normalizeImportGender(getImportString(record, ['patient_gender', 'patientGender'])) + detailForm.tags = getImportStringList(record, ['tags', 'tag_list', 'tagList']).join(', ') + detailForm.icd_codes = getImportStringList(record, ['icd_codes', 'icdCodes', 'icd_list', 'icdList']).join(', ') + detailForm.estimated_minutes = getImportNumber(record, ['estimated_minutes', 'estimatedMinutes']) ?? row.estimatedMinutes ?? undefined + detailForm.osce_enabled = getImportBoolean(record, ['osce_enabled', 'osceEnabled']) + detailForm.traditional_standard_diagnosis = getImportString(traditional, ['standard_diagnosis', 'standardDiagnosis']) + detailForm.traditional_standard_treatment = getImportString(traditional, ['standard_treatment', 'standardTreatment']) + detailForm.traditional_guideline_reference = getImportString(traditional, ['guideline_reference', 'guidelineReference']) + detailForm.teaching_learning_objectives = getImportString(teaching, ['learning_objectives', 'learningObjectives']) + detailForm.teaching_key_points = getImportString(teaching, ['key_points', 'keyPoints']) + detailForm.teaching_reference_answer = getImportString(teaching, ['reference_answer', 'referenceAnswer']) + detailForm.scoring_rules = scoringRules.length + ? scoringRules + : [{ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }] + detailForm.exam_items = examItems + detailFormRef.value?.clearValidate() +} + +function getImportRecord(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {} +} + +function getImportFirst(record: Record, keys: string[]): unknown { + for (const key of keys) { + if (record[key] !== undefined && record[key] !== null) { + return record[key] + } + } + + return undefined +} + +function getImportString(record: Record, keys: string[], fallback = ''): string { + 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 getImportNumber(record: Record, keys: string[]): number | null { + const value = getImportFirst(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 getImportBoolean(record: Record, keys: string[]) { + const value = getImportFirst(record, keys) + if (typeof value === 'boolean') { + return value + } + if (typeof value === 'number') { + return value === 1 + } + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase() + return ['true', '1', 'yes', 'y', 'on', '开启', '启用', '是'].includes(normalized) + } + + return false +} + +function getImportStringList(record: Record, keys: string[]): string[] { + const value = getImportFirst(record, keys) + if (Array.isArray(value)) { + return value + .map(item => { + if (typeof item === 'string' || typeof item === 'number') { + return String(item).trim() + } + const itemRecord = getImportRecord(item) + return getImportString(itemRecord, ['name', 'title', 'code', 'value']) + }) + .filter(Boolean) + } + + if (typeof value === 'string') { + return parseTextList(value) + } + + return [] +} + +function getImportScoringRules(record: Record): ScoringRuleForm[] { + const raw = getImportFirst(record, ['scoring_rules', 'scoringRules']) + if (!Array.isArray(raw)) { + return [] + } + + return raw + .map(item => { + const itemRecord = getImportRecord(item) + const scoreWeight = getImportNumber(itemRecord, ['score_weight', 'scoreWeight', 'weight']) + const rawAutoScore = getImportFirst(itemRecord, ['ai_auto_score', 'aiAutoScore']) + return { + 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']) + } + }) + .filter(item => item.dimension || item.scoring_standard) +} + +function getImportExamItems(record: Record): ExamItemForm[] { + const raw = getImportFirst(record, ['exam_items', 'examItems']) + if (!Array.isArray(raw)) { + return [] + } + + return raw + .map(item => { + const itemRecord = getImportRecord(item) + return { + 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']) + } + }) + .filter(item => item.item_code || item.item_name || item.item_type || item.result_text) +} + +function normalizeImportCaseType(value: string, fallback: DraftCaseType): DraftCaseType { + return value === 'teaching' ? 'teaching' : fallback +} + +function normalizeImportGender(value: string) { + if (value === '男') return 'male' + if (value === '女') return 'female' + if (value === '未知') return 'unknown' + return ['male', 'female', 'unknown'].includes(value) ? value : '' +} + function createDetailDisplay(row: CaseListItem | null, fullData: unknown) { const record = getDetailRecord(fullData) const title = getDetailString(record, ['title', 'name']) || row?.title || '-' diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 655a2bf3..5f0a401e 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -3,8 +3,11 @@