import { API_BASE_URL, ApiRequestError } from './auth' export type CaseMode = 'training' | 'teaching' export type CaseListSource = 'recommended' | 'specialty' | 'weak' | 'teaching' | 'teacher-task' export type CaseTone = 'blue' | 'teal' | 'pink' | 'orange' | 'purple' | 'green' export type ClinicalCase = { id: string title: string patientName: string gender: '男' | '女' age: number department: string departmentId?: number scene: string caseNo: string tone: CaseTone mode: CaseMode caseType?: string caseTypeDisplay?: string difficulty?: string difficultyScore?: number chiefComplaint?: string description?: string tags?: string competencyTags?: string[] estimatedMinutes?: number osceEnabled?: boolean myBestScore?: number | null myTrainCount?: number | null createdAt?: string source?: CaseListSource } export type CaseListQuery = { search?: string case_type?: string difficulty?: string department?: string | number page?: number page_size?: number } export type CaseListPage = { count: number next: string | null previous: string | null results: ClinicalCase[] } type QueryValue = string | number | boolean | null | undefined type ServerCaseListPage = { count?: number next?: string | null previous?: string | null results?: ServerCaseRecord[] } type ServerCaseRecord = { id?: number | string title?: string case_type?: string case_type_display?: string difficulty?: string difficulty_score?: number department?: number department_name?: string chief_complaint?: string description?: string patient_age?: number patient_gender?: string tags?: string competency_tags?: string[] estimated_minutes?: number osce_enabled?: boolean created_at?: string my_best_score?: number | null my_train_count?: number | null } const CASE_LIST_PATHS: Record = { recommended: '/case/mobile/recommended/', specialty: '/case/mobile/specialty/', weak: '/case/mobile/weak/', teaching: '/case/mobile/teaching/', 'teacher-task': '/case/mobile/teacher-task/' } const TONES: CaseTone[] = ['blue', 'teal', 'pink', 'orange', 'purple', 'green'] export async function fetchCaseList(source: CaseListSource = 'recommended', query: CaseListQuery = {}): Promise { const page = await fetchCaseListPage(source, query) return page.results } export async function fetchCaseListPage(source: CaseListSource = 'recommended', query: CaseListQuery = {}): Promise { const path = CASE_LIST_PATHS[source] || CASE_LIST_PATHS.recommended const payload = await requestCaseList(path, query) const page = normalizeCaseListPage(payload, source) return page } export function readStoredClinicalCase() { const value = uni.getStorageSync('clinical-thinking-selected-case') if (value && typeof value === 'object') return value as ClinicalCase return null } export function resolveClinicalCaseId(caseItem?: ClinicalCase | null) { if (!caseItem) return 0 const explicitId = Number(caseItem.id) if (Number.isInteger(explicitId) && explicitId > 0) return explicitId const caseNo = Number(caseItem.caseNo) if (Number.isInteger(caseNo) && caseNo > 0) return caseNo const matched = String(caseItem.id).match(/\d+/) const fallbackId = matched ? Number(matched[0]) : 0 return Number.isInteger(fallbackId) && fallbackId > 0 ? fallbackId : 0 } function requestCaseList(path: string, query: CaseListQuery): Promise { return new Promise((resolve, reject) => { uni.request({ url: `${API_BASE_URL}${withQuery(path, query)}`, method: 'GET', timeout: 10000, header: createAuthHeaders(), success: response => { if (response.statusCode >= 200 && response.statusCode < 300) { resolve(response.data as ServerCaseListPage) return } const payload = response.data as Record | undefined const code = typeof payload?.code === 'string' ? payload.code : undefined reject(new ApiRequestError(readErrorMessage(response.data, `病例列表加载失败(${response.statusCode})`), code, response.statusCode)) }, fail: error => { reject(new ApiRequestError(error.errMsg || '无法连接服务')) } }) }) } function normalizeCaseListPage(payload: ServerCaseListPage, source: CaseListSource): CaseListPage { const results = Array.isArray(payload.results) ? payload.results : [] return { count: readNumber(payload.count, results.length), next: typeof payload.next === 'string' ? payload.next : null, previous: typeof payload.previous === 'string' ? payload.previous : null, results: results.map((item, index) => normalizeCaseRecord(item, source, index)) } } function normalizeCaseRecord(item: ServerCaseRecord, source: CaseListSource, index: number): ClinicalCase { const id = readString(item.id, `case-${index + 1}`) const title = readString(item.title, '未命名病例') const gender = normalizeGender(item.patient_gender) const departmentName = readString(item.department_name, '未配置科室') const caseType = readString(item.case_type) const caseTypeDisplay = readString(item.case_type_display, caseType || '练习病例') const chiefComplaint = readString(item.chief_complaint, title) const mode = source === 'teaching' || caseType === 'teaching' ? 'teaching' : 'training' return { id, title, patientName: gender === '女' ? '患者女士' : '患者先生', gender, age: readNumber(item.patient_age, 0), department: departmentName, departmentId: readOptionalNumber(item.department), scene: caseTypeDisplay, caseNo: id, tone: TONES[index % TONES.length], mode, caseType, caseTypeDisplay, difficulty: readString(item.difficulty), difficultyScore: readOptionalNumber(item.difficulty_score), chiefComplaint, description: readString(item.description), tags: readString(item.tags), competencyTags: Array.isArray(item.competency_tags) ? item.competency_tags.map(String) : [], estimatedMinutes: readOptionalNumber(item.estimated_minutes), osceEnabled: Boolean(item.osce_enabled), myBestScore: typeof item.my_best_score === 'number' ? item.my_best_score : null, myTrainCount: typeof item.my_train_count === 'number' ? item.my_train_count : null, createdAt: readString(item.created_at), source } } function createAuthHeaders() { const headers: Record = { 'Content-Type': 'application/json', Accept: 'application/json' } const token = readAccessToken() if (token) headers.Authorization = `Bearer ${token}` return headers } function readAccessToken() { try { const token = uni.getStorageSync('clinical-thinking-access-token') return typeof token === 'string' ? token.trim() : '' } catch { return '' } } function withQuery(path: string, query: Record) { const params = Object.entries(query) .filter(([, value]) => value !== undefined && value !== null && value !== '') .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) .join('&') return params ? `${path}?${params}` : path } function readErrorMessage(data: unknown, fallback: string) { if (data && typeof data === 'object') { const payload = data as Record const message = payload.message || payload.detail || payload.error if (typeof message === 'string' && message.trim()) return message } return fallback } function readString(value: unknown, fallback = '') { if (typeof value === 'string') return value.trim() || fallback if (typeof value === 'number') return String(value) return fallback } function readNumber(value: unknown, fallback = 0) { const numberValue = Number(value) return Number.isFinite(numberValue) ? numberValue : fallback } function readOptionalNumber(value: unknown) { const numberValue = Number(value) return Number.isFinite(numberValue) ? numberValue : undefined } function normalizeGender(value: unknown): '男' | '女' { if (value === 'female' || value === '女') return '女' return '男' }