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

295 lines
8.5 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'
import type { DimensionScore, EvaluationDetail, ScoreDetail } from './assessment'
import type { ScoreType } from './session'
type QueryValue = string | number | boolean | null | undefined
type ServerEnvelope<T> = {
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<string, unknown>
weak_dimensions: string[]
strong_dimensions: string[]
ai_preference: Record<string, unknown>
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<EvaluationDetail> & Record<string, unknown>
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<UserProfile>('/user/profile/')
}
export function fetchUserTrainingRecords(query: TrainingRecordQuery = {}) {
return serverRequest<TrainingRecordsResponse>('/user/training-records/', {
search: query.search,
page: query.page
})
}
export function fetchUserAnalysis() {
return serverRequest<UserAnalysis>('/user/analysis/')
}
export function fetchCompetencyMetrics() {
return serverRequest<CompetencyMetrics>('/user/competency-metrics/')
}
export async function fetchEvaluationList(query: EvaluationListQuery = {}) {
const payload = await serverRequest<unknown>('/v1/evaluations', {
page: query.page,
page_size: query.page_size
})
return unwrapServerData<EvaluationListResponse>(payload)
}
export async function fetchServerEvaluationDetail(evaluationId: number) {
const payload = await serverRequest<unknown>(`/v1/evaluations/${encodeURIComponent(String(evaluationId))}`)
const data = unwrapServerData<Record<string, unknown>>(payload)
return normalizeEvaluationDetail(data, evaluationId)
}
function serverRequest<T>(path: string, query?: Record<string, QueryValue>): Promise<T> {
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<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 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>) {
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<string, unknown>
const message = payload.message || payload.detail || payload.error
if (typeof message === 'string' && message.trim()) return message
}
return fallback
}
function unwrapServerData<T>(payload: unknown): T {
if (payload && typeof payload === 'object' && 'data' in payload) {
const envelope = payload as ServerEnvelope<T>
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<string, unknown>, 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<string, unknown> {
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
}