Files
vueapp/api/cases.ts
T
2026-06-13 06:05:37 +08:00

256 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<CaseListSource, string> = {
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<ClinicalCase[]> {
const page = await fetchCaseListPage(source, query)
return page.results
}
export async function fetchCaseListPage(source: CaseListSource = 'recommended', query: CaseListQuery = {}): Promise<CaseListPage> {
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<ServerCaseListPage> {
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<string, unknown> | 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<string, string> = {
'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<string, QueryValue>) {
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<string, unknown>
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 '男'
}