diff --git a/src/api/cases.ts b/src/api/cases.ts index 5977e418..42c36da3 100644 --- a/src/api/cases.ts +++ b/src/api/cases.ts @@ -56,6 +56,7 @@ export interface CreateCaseDraftPayload { title: string case_type: DraftCaseType institution_id?: number + department_id?: number department_name?: string traditional?: Record teaching?: Record @@ -77,6 +78,30 @@ export interface CreateCaseDraftParams { payload: CreateCaseDraftPayload } +export interface SaveCaseDraftPayload { + title?: string + institution_id?: number + department_id?: number + department_name?: string + traditional?: Record + teaching?: Record + exam_items?: CaseExamItemPayload[] + scoring_rules?: CaseScoringRulePayload[] + difficulty?: string + chief_complaint?: string + description?: string + patient_age?: number + patient_gender?: string + tags?: string | string[] + icd_codes?: string[] + estimated_minutes?: number + osce_enabled?: boolean +} + +export interface SaveCaseDraftParams extends CaseActionParams { + payload: SaveCaseDraftPayload +} + export interface AiGenerateCasePayload { prompt: string case_type: DraftCaseType @@ -503,6 +528,20 @@ export async function createCaseDraft(params: CreateCaseDraftParams): Promise { + const response = await fetch(`/server/api/cms/cases/${params.id}/save-draft/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token), + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params.payload) + }) + + return parseMutationResponse(response, '保存病例草稿失败') +} + export async function generateCaseWithAi(params: AiGenerateCaseParams): Promise { const response = await fetch('/server/api/cms/cases/ai-generate/', { method: 'POST', diff --git a/src/api/users.ts b/src/api/users.ts index 0e455157..f751afe8 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -33,6 +33,17 @@ export interface UserListResult { total: number } +export interface InstitutionOption { + id: number + name: string + code: string +} + +export interface DepartmentOption { + id: number + name: string +} + export interface CreateUserPayload { phone: string real_name: string @@ -87,6 +98,8 @@ export interface ImportUsersParams { export interface ExportUsersParams extends UserListParams {} const listKeys = ['results', 'list', 'rows', 'items', 'records', 'users'] +const institutionListKeys = ['results', 'list', 'rows', 'items', 'records', 'institutions', 'institution_list', 'institutionList'] +const departmentListKeys = ['results', 'list', 'rows', 'items', 'records', 'departments', 'department_list', 'departmentList'] const totalKeys = ['count', 'total', 'total_count', 'totalCount'] function parseResponseText(text: string): unknown { @@ -209,6 +222,54 @@ function findUserPayload(data: unknown): { items: unknown[]; total: number } { return { items: [], total: getTotal(record, 0) } } +function findInstitutionItems(data: unknown): unknown[] { + if (Array.isArray(data)) { + return data + } + + if (!data || typeof data !== 'object') { + return [] + } + + const record = data as Record + for (const key of institutionListKeys) { + const value = record[key] + if (Array.isArray(value)) { + return value + } + } + + if (record.data && typeof record.data === 'object') { + return findInstitutionItems(record.data) + } + + return [] +} + +function findDepartmentItems(data: unknown): unknown[] { + if (Array.isArray(data)) { + return data + } + + if (!data || typeof data !== 'object') { + return [] + } + + const record = data as Record + for (const key of departmentListKeys) { + const value = record[key] + if (Array.isArray(value)) { + return value + } + } + + if (record.data && typeof record.data === 'object') { + return findDepartmentItems(record.data) + } + + return [] +} + function normalizeUser(item: unknown, index: number): UserListItem { const record = item && typeof item === 'object' ? (item as Record) : {} const name = getString(record, ['name', 'real_name', 'realName', 'nickname', 'username'], `用户${index + 1}`) @@ -235,6 +296,51 @@ function normalizeUser(item: unknown, index: number): UserListItem { } } +function normalizeInstitutionOption(item: unknown): InstitutionOption | null { + if (typeof item === 'number' || (typeof item === 'string' && Number.isFinite(Number(item)))) { + const id = Number(item) + return { id, name: `机构 ${id}`, code: '' } + } + + if (!item || typeof item !== 'object') { + return null + } + + const record = item as Record + const id = Number(getString(record, ['id', 'institution_id', 'institutionId', 'value', 'pk'], '')) + if (!Number.isFinite(id) || id <= 0) { + return null + } + + return { + id, + name: getString(record, ['name', 'institution_name', 'institutionName', 'hospital_name', 'hospitalName', 'label', 'title'], `机构 ${id}`), + code: getString(record, ['code', 'institution_code', 'institutionCode'], '') + } +} + +function normalizeDepartmentOption(item: unknown): DepartmentOption | null { + if (typeof item === 'number' || (typeof item === 'string' && Number.isFinite(Number(item)))) { + const id = Number(item) + return { id, name: `科室 ${id}` } + } + + if (!item || typeof item !== 'object') { + return null + } + + const record = item as Record + const id = Number(getString(record, ['id', 'department_id', 'departmentId', 'value', 'pk'], '')) + if (!Number.isFinite(id) || id <= 0) { + return null + } + + return { + id, + name: getString(record, ['name', 'department_name', 'departmentName', 'label', 'title'], `科室 ${id}`) + } +} + function createAuthorization(token: string) { return /^Bearer\s+/i.test(token) ? token : `Bearer ${token}` } @@ -338,6 +444,66 @@ export async function fetchUsers(params: UserListParams): Promise { + const response = await fetch('/server/api/user/institution_list/', { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(token) + } + }) + const text = await response.text() + const data = parseResponseText(text) + + if (!response.ok) { + const message = getMessageFromResponse(data) || '获取机构列表失败' + throw new Error(message) + } + + const seen = new Set() + return findInstitutionItems(data) + .map(normalizeInstitutionOption) + .filter((item): item is InstitutionOption => { + if (!item || seen.has(item.id)) { + return false + } + seen.add(item.id) + return true + }) +} + +export async function fetchMyDepartments(token: string, institutionId?: number): Promise { + const query = new URLSearchParams() + if (institutionId) { + query.set('institution_id', String(institutionId)) + } + const response = await fetch(`/server/api/user/my_departments/${query.toString() ? `?${query.toString()}` : ''}`, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(token) + } + }) + const text = await response.text() + const data = parseResponseText(text) + + if (!response.ok) { + const message = getMessageFromResponse(data) || '获取科室列表失败' + throw new Error(message) + } + + const seen = new Set() + return findDepartmentItems(data) + .map(normalizeDepartmentOption) + .filter((item): item is DepartmentOption => { + if (!item || seen.has(item.id)) { + return false + } + seen.add(item.id) + return true + }) +} + export async function createUser(params: CreateUserParams): Promise { const response = await fetch('/server/api/cms/users/', { method: 'POST', diff --git a/src/views/AiCaseGenerateView.vue b/src/views/AiCaseGenerateView.vue index 45952b4a..2f33483f 100644 --- a/src/views/AiCaseGenerateView.vue +++ b/src/views/AiCaseGenerateView.vue @@ -205,20 +205,6 @@ -
-
-

评分规则

- 添加规则 -
-
- - - - - -
-
-

检查/检验项目

@@ -469,18 +455,6 @@ function resetDraftForm() { draftForm.exam_items = [] } -function addScoringRule() { - draftForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' }) -} - -function removeScoringRule(index: number) { - if (draftForm.scoring_rules.length <= 1) { - return - } - - draftForm.scoring_rules.splice(index, 1) -} - function addExamItem() { draftForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' }) } @@ -544,25 +518,19 @@ function buildCaseStructure(): Record { function normalizeScoringRules(): CaseScoringRulePayload[] { const rules = draftForm.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: true, + scoring_standard: rule.scoring_standard.trim() + } + }) + .filter(rule => rule.dimension || rule.scoring_standard) if (!rules.length) { - throw new Error('至少添加 1 条评分规则') - } - - 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') - } + return [createDefaultScoringRule()] } return rules.map(rule => ({ @@ -573,6 +541,15 @@ function normalizeScoringRules(): CaseScoringRulePayload[] { })) } +function createDefaultScoringRule(): CaseScoringRulePayload { + return { + dimension: draftForm.case_type === 'teaching' ? '教学目标达成' : '诊断与处置', + score_weight: 1, + ai_auto_score: true, + scoring_standard: '由AI根据病例内容生成评分标准' + } +} + function normalizeExamItems(): CaseExamItemPayload[] { const items = draftForm.exam_items .map(item => ({ diff --git a/src/views/CasesView.vue b/src/views/CasesView.vue index 81e28be5..8eef782e 100644 --- a/src/views/CasesView.vue +++ b/src/views/CasesView.vue @@ -136,13 +136,44 @@ - - + + + + - - + + + + @@ -243,20 +274,6 @@
-
-
-

评分规则

- 添加规则 -
-
- - - - - -
-
-

检查/检验项目

@@ -372,6 +389,7 @@ class="case-create-form case-detail-form" :model="detailForm" :rules="caseRules" + :disabled="!canEditDetailCase" label-position="top" >
@@ -387,20 +405,51 @@ - + - - + + + + - - + + + + @@ -501,39 +550,40 @@
-
+

评分规则

- 添加规则 -
-
- - - - -
+ + + + + + +

检查/检验项目

- 添加项目 + 添加项目
-
未添加项目,可直接提交。
+
未添加项目,可直接保存草稿。
- +
@@ -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(null) const importCaseType = ref('traditional') const importFileList = ref([]) const cases = ref([]) +const institutionOptions = ref([]) +const departmentOptions = ref([]) const relationsCase = ref(null) const detailCase = ref(null) const caseDetail = ref(null) @@ -656,6 +714,7 @@ const caseForm = reactive({ title: '', case_type: 'traditional', institution_id: undefined, + department_id: undefined, department_name: '', difficulty: '', chief_complaint: '', @@ -680,6 +739,7 @@ const detailForm = reactive({ 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 { if (form.case_type === 'teaching') { if (!form.teaching_learning_objectives.trim()) { @@ -973,27 +1137,49 @@ function buildCaseStructure(form: CaseDraftForm): Record { } } +function buildEditableCaseStructure(form: CaseDraftForm): Record { + 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) diff --git a/src/views/RoleUsersView.vue b/src/views/RoleUsersView.vue index ef49be77..c239fba0 100644 --- a/src/views/RoleUsersView.vue +++ b/src/views/RoleUsersView.vue @@ -98,7 +98,7 @@ - + @@ -228,6 +228,7 @@ const pagination = reactive({ page: 1, total: 0 }) +const roleUserPageSize = 10 const userForm = reactive({ phone: '', real_name: '', @@ -248,7 +249,7 @@ const roleOptions: Array<{ label: string; value: ManagedRoleType }> = [ { label: '内容管理员', value: 'content_admin' } ] const pageConfigs: Record = { - doctor: { title: '医生管理', description: '维护本院医生账号,支持查询、导入导出、启停和密码重置。' }, + doctor: { title: '医生管理', description: '维护本院医生和内容管理员账号,支持查询、导入导出、启停和密码重置。' }, student: { title: '医学生管理', description: '维护本院医学生账号,支持查询、导入导出、启停和密码重置。' }, content_admin: { title: '内容管理员', description: '维护本院内容管理员账号,支持查询、导入导出、启停和密码重置。' } } @@ -258,6 +259,14 @@ const currentRoleType = computed(() => { return roleType === 'student' || roleType === 'content_admin' ? roleType : 'doctor' }) const pageConfig = computed(() => pageConfigs[currentRoleType.value]) +const listRoleTypes = computed(() => + currentRoleType.value === 'doctor' ? ['doctor', 'content_admin'] : [currentRoleType.value] +) +const formRoleOptions = computed(() => + currentRoleType.value === 'doctor' + ? roleOptions.filter(item => item.value === 'doctor' || item.value === 'content_admin') + : roleOptions.filter(item => item.value === currentRoleType.value) +) const userDialogTitle = computed(() => (userMode.value === 'create' ? '新增用户' : '编辑用户')) const userRules: FormRules = { phone: [ @@ -276,16 +285,38 @@ async function loadUsers() { try { loading.value = true - const result = await fetchUsers({ - token: appStore.token, - roleType: currentRoleType.value, - search: filters.search, - status: filters.status, - gender: filters.gender, - page: pagination.page - }) - users.value = result.users - pagination.total = result.total + if (listRoleTypes.value.length === 1) { + const result = await fetchUsers({ + token: appStore.token, + roleType: listRoleTypes.value[0], + search: filters.search, + status: filters.status, + gender: filters.gender, + page: pagination.page, + size: roleUserPageSize + }) + users.value = result.users + pagination.total = result.total + } else { + const fetchSize = pagination.page * roleUserPageSize + const results = await Promise.all( + listRoleTypes.value.map(roleType => + fetchUsers({ + token: appStore.token, + roleType, + search: filters.search, + status: filters.status, + gender: filters.gender, + page: 1, + size: fetchSize + }) + ) + ) + const mergedUsers = uniqueUsers(results.flatMap(result => result.users)) + const offset = (pagination.page - 1) * roleUserPageSize + users.value = mergedUsers.slice(offset, offset + roleUserPageSize) + pagination.total = results.reduce((total, result) => total + result.total, 0) + } } catch (error) { ElMessage.error(error instanceof Error ? error.message : '获取人员列表失败') } finally { @@ -541,14 +572,19 @@ async function exportCurrentUsers() { try { exporting.value = true - await exportUsers({ - token: appStore.token, - roleType: currentRoleType.value, - search: filters.search, - status: filters.status, - gender: filters.gender, - page: pagination.page - }) + await Promise.all( + listRoleTypes.value.map(roleType => + exportUsers({ + token: appStore.token, + roleType, + search: filters.search, + status: filters.status, + gender: filters.gender, + page: pagination.page, + size: roleUserPageSize + }) + ) + ) ElMessage.success('用户导出已开始') } catch (error) { ElMessage.error(error instanceof Error ? error.message : '导出用户失败') @@ -593,6 +629,18 @@ function normalizeManagedRole(value: string): ManagedRoleType { return 'doctor' } +function uniqueUsers(items: UserListItem[]) { + const seen = new Set() + return items.filter(item => { + const key = item.id || item.phone + if (seen.has(key)) { + return false + } + seen.add(key) + return true + }) +} + watch(currentRoleType, () => { pagination.page = 1 resetUserForm() diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue index 1620fea3..106246b1 100644 --- a/src/views/UsersView.vue +++ b/src/views/UsersView.vue @@ -16,9 +16,23 @@
- + + + + - @@ -100,8 +114,22 @@ - - + + + + @@ -189,11 +217,13 @@ import { disableUser, downloadUserImportTemplate, exportUsers, + fetchInstitutionList, fetchUsers, importUsers, resetUserPassword, updateUser, type CreateUserPayload, + type InstitutionOption, type UpdateUserPayload, type UserListItem } from '@/api/users' @@ -206,6 +236,7 @@ const importing = ref(false) const exporting = ref(false) const downloadingTemplate = ref(false) const resettingPassword = ref(false) +const loadingInstitutions = ref(false) const userDialogVisible = ref(false) const importDialogVisible = ref(false) const resetPasswordDialogVisible = ref(false) @@ -217,7 +248,14 @@ const resetPasswordUser = ref(null) const importFile = ref(null) const importFileList = ref([]) const users = ref([]) -const filters = reactive({ +const institutionOptions = ref([]) +const filters = reactive<{ + search: string + roleType: string + institution: number | '' + status: string + gender: string +}>({ search: '', roleType: '', institution: '', @@ -251,6 +289,10 @@ const roleTypeOptions = [ { label: '医院管理员 hospital_admin', value: 'hospital_admin' }, { label: '内容管理员 content_admin', value: 'content_admin' } ] +const filterRoleTypeOptions = [ + { label: '全部', value: '' }, + ...roleTypeOptions +] const userRules: FormRules = { phone: [ @@ -259,7 +301,7 @@ const userRules: FormRules = { ], real_name: [{ required: true, message: '请输入姓名', trigger: 'blur' }], role_type: [{ required: true, message: '请选择或输入角色码', trigger: 'change' }], - institution: [{ required: true, message: '请输入机构ID', trigger: 'blur' }] + institution: [{ required: true, message: '请选择机构', trigger: 'change' }] } const userDialogTitle = computed(() => (userMode.value === 'create' ? '新增用户' : '编辑用户')) @@ -273,9 +315,9 @@ async function loadUsers() { loading.value = true const result = await fetchUsers({ token: appStore.token, - roleType: filters.roleType || appStore.roleType, + roleType: filters.roleType, search: filters.search, - institution: filters.institution, + institution: filters.institution === '' ? '' : String(filters.institution), status: filters.status, gender: filters.gender, page: pagination.page, @@ -314,6 +356,7 @@ function handleSizeChange(size: number) { function openCreateDialog() { userMode.value = 'create' userDialogVisible.value = true + loadInstitutionOptions() } function openEditDialog(row: UserListItem) { @@ -329,6 +372,7 @@ function openEditDialog(row: UserListItem) { userForm.training_stage = row.trainingStage userForm.status = row.status userDialogVisible.value = true + loadInstitutionOptions() } function openImportDialog() { @@ -372,6 +416,31 @@ function buildCreatePayload(): CreateUserPayload { return payload } +async function loadInstitutionOptions() { + if (!appStore.token || 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 + } +} + +function handleInstitutionVisibleChange(visible: boolean) { + if (visible) { + loadInstitutionOptions() + } +} + +function institutionOptionLabel(item: InstitutionOption) { + return item.code ? `${item.name} (${item.code})` : item.name +} + function buildUpdatePayload(): UpdateUserPayload { if (!editingUser.value) { return {} @@ -561,9 +630,9 @@ async function exportCurrentUsers() { exporting.value = true await exportUsers({ token: appStore.token, - roleType: filters.roleType || appStore.roleType, + roleType: filters.roleType, search: filters.search, - institution: filters.institution, + institution: filters.institution === '' ? '' : String(filters.institution), status: filters.status, gender: filters.gender, page: pagination.page