import { API_BASE_URL, ApiRequestError } from './auth' import type { DimensionScore, EvaluationDetail, ScoreDetail } from './assessment' import type { ScoreType } from './session' type QueryValue = string | number | boolean | null | undefined type ServerEnvelope = { code?: string message?: string data?: T } export type UserProfile = { id: number username: string real_name: string phone: string avatar: string gender: number role_type: string institution: number | string | null institution_name: string department: number | string | null department_name: string title_name: string practice_years: string major: string training_stage: string learning_target: string competency_profile: Record weak_dimensions: string[] strong_dimensions: string[] ai_preference: Record total_training_count: number total_case_count: number current_level: string status: number last_login_time: string | null created_at: string updated_at: string } export type TrainingRecord = { record_id: number case_id: number case_title: string department: string trained_at: string score: number score_type: ScoreType evaluation_level: string training_mode: string case_type: string } export type TrainingRecordSummary = { total_cases: number total_hours: number avg_accuracy: number } export type TrainingRecordsResponse = { count: number next: string | null previous: string | null results: TrainingRecord[] summary: TrainingRecordSummary } export type AnalysisTrendItem = { label: string score: number } export type AnalysisRadarItem = { dimension: string score: number } export type UserAnalysis = { current_score: number score_delta_pct: number | null recent_trend: AnalysisTrendItem[] radar: AnalysisRadarItem[] weak_dimensions: string[] comment: string } export type CompetencyMetrics = { completed_cases: number completed_cases_week: number total_hours: number avg_score: number diagnosis_accuracy: number } export type EvaluationListItem = Partial & Record export type EvaluationListResponse = { count: number next: string | null previous: string | null results: EvaluationListItem[] } export type TrainingRecordQuery = { search?: string page?: number } export type EvaluationListQuery = { page?: number page_size?: number } export function fetchUserProfile() { return serverRequest('/user/profile/') } export function fetchUserTrainingRecords(query: TrainingRecordQuery = {}) { return serverRequest('/user/training-records/', { search: query.search, page: query.page }) } export function fetchUserAnalysis() { return serverRequest('/user/analysis/') } export function fetchCompetencyMetrics() { return serverRequest('/user/competency-metrics/') } export async function fetchEvaluationList(query: EvaluationListQuery = {}) { const payload = await serverRequest('/v1/evaluations', { page: query.page, page_size: query.page_size }) return unwrapServerData(payload) } export async function fetchServerEvaluationDetail(evaluationId: number) { const payload = await serverRequest(`/v1/evaluations/${encodeURIComponent(String(evaluationId))}`) const data = unwrapServerData>(payload) return normalizeEvaluationDetail(data, evaluationId) } function serverRequest(path: string, query?: Record): 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 T) 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 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) { if (!query) return path 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 unwrapServerData(payload: unknown): T { if (payload && typeof payload === 'object' && 'data' in payload) { const envelope = payload as ServerEnvelope if (envelope.code && envelope.code !== 'OK' && envelope.code !== 'ok') { throw new ApiRequestError(envelope.message || '请求失败', envelope.code) } if (envelope.data !== undefined) return envelope.data } return payload as T } function normalizeEvaluationDetail(data: Record, fallbackId: number): EvaluationDetail { const caseInfo = isRecord(data.case) ? data.case : {} const totalScore = readNumber(data.total_score ?? data.score ?? data.current_score, 0) return { evaluation_id: readNumber(data.evaluation_id ?? data.id ?? data.record_id, fallbackId), session_id: readNumber(data.session_id, 0), case_id: readNumber(data.case_id ?? caseInfo.id, 0), case_title: readString(data.case_title ?? data.caseTitle ?? caseInfo.title, '未命名病例'), score_type: readScoreType(data.score_type), total_score: totalScore, dimension_scores: normalizeDimensionScores(data.dimension_scores ?? data.radar), score_details: normalizeScoreDetails(data.score_details), overall_comment: readString(data.overall_comment ?? data.comment, '本次评价暂无详细点评。'), pdf_file_path: readOptionalString(data.pdf_file_path ?? data.pdfFilePath), created_at: readOptionalString(data.created_at ?? data.trained_at) } } function normalizeDimensionScores(value: unknown): DimensionScore[] { if (!Array.isArray(value)) return [] return value.map(item => { const record = isRecord(item) ? item : {} return { dimension: readString(record.dimension ?? record.label, '未命名维度'), score: readNumber(record.score, 0), max_score: readNumber(record.max_score, 100), comment: readOptionalString(record.comment), evidence: Array.isArray(record.evidence) ? record.evidence.map(String) : undefined, deductions: Array.isArray(record.deductions) ? record.deductions.map(String) : undefined, improvement: readOptionalString(record.improvement) } }) } function normalizeScoreDetails(value: unknown): ScoreDetail[] { if (!Array.isArray(value)) return [] return value.map(item => { const record = isRecord(item) ? item : {} return { dimension: readString(record.dimension, '未命名维度'), score: readNumber(record.score, 0), deducted_reason: readOptionalString(record.deducted_reason), ai_confidence: record.ai_confidence === undefined ? undefined : readNumber(record.ai_confidence, 0), comment: readOptionalString(record.comment) } }) } function readScoreType(value: unknown): ScoreType { return value === 'five_point' ? 'five_point' : 'percentage' } function readNumber(value: unknown, fallback = 0) { const numberValue = Number(value) return Number.isFinite(numberValue) ? numberValue : fallback } function readString(value: unknown, fallback = '') { if (typeof value === 'string') return value.trim() || fallback if (typeof value === 'number') return String(value) return fallback } function readOptionalString(value: unknown) { const text = readString(value) return text || undefined } function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === 'object' && !Array.isArray(value)) }