feat: 联调
This commit is contained in:
@@ -31,6 +31,31 @@ export type EvaluationResult = {
|
||||
overall_comment: string
|
||||
}
|
||||
|
||||
export type EvaluationDetail = {
|
||||
evaluation_id: number
|
||||
session_id: number
|
||||
case_id: number
|
||||
case_title: string
|
||||
score_type: ScoreType
|
||||
total_score: number
|
||||
dimension_scores: DimensionScore[]
|
||||
score_details: ScoreDetail[]
|
||||
overall_comment: string
|
||||
pdf_file_path?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export type EvaluationPdfExport = {
|
||||
export_id: number
|
||||
file_path: string
|
||||
}
|
||||
|
||||
export type EvaluationPdfDownload = {
|
||||
blob?: Blob
|
||||
filePath?: string
|
||||
fileName: string
|
||||
}
|
||||
|
||||
export async function generateEvaluation(sessionId: number, scoreType: ScoreType = 'percentage') {
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/sessions/${sessionId}/evaluation`, {
|
||||
method: 'POST',
|
||||
@@ -51,3 +76,69 @@ export async function generateEvaluation(sessionId: number, scoreType: ScoreType
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
export async function fetchEvaluationDetail(evaluationId: number) {
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/evaluations/${evaluationId}`, {
|
||||
method: 'GET',
|
||||
headers: authHeaders()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readError(response))
|
||||
}
|
||||
|
||||
const result = (await response.json()) as ApiEnvelope<EvaluationDetail>
|
||||
if (result.code !== 'OK' || !result.data) {
|
||||
throw new Error(result.message || '评价详情加载失败')
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
export async function downloadEvaluationPdf(evaluationId: number): Promise<EvaluationPdfDownload> {
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/evaluations/${evaluationId}/download-pdf`, {
|
||||
method: 'GET',
|
||||
headers: authHeaders('application/pdf')
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readError(response))
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('Content-Type') || ''
|
||||
const disposition = response.headers.get('Content-Disposition') || ''
|
||||
const fileName = readDownloadFileName(disposition, evaluationId)
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
const result = (await response.json()) as ApiEnvelope<EvaluationPdfExport>
|
||||
if (result.code !== 'OK' || !result.data?.file_path) {
|
||||
throw new Error(result.message || 'PDF 下载失败')
|
||||
}
|
||||
|
||||
return {
|
||||
filePath: result.data.file_path,
|
||||
fileName: readFileNameFromPath(result.data.file_path, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
blob: await response.blob(),
|
||||
fileName
|
||||
}
|
||||
}
|
||||
|
||||
function readDownloadFileName(disposition: string, evaluationId: number) {
|
||||
const utf8Name = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1]
|
||||
if (utf8Name) return decodeURIComponent(utf8Name)
|
||||
|
||||
const quotedName = disposition.match(/filename="?([^"]+)"?/i)?.[1]
|
||||
if (quotedName) return quotedName
|
||||
|
||||
return `training_record_${evaluationId}.pdf`
|
||||
}
|
||||
|
||||
function readFileNameFromPath(filePath: string, fallback: string) {
|
||||
const cleanPath = filePath.split('?')[0].split('#')[0]
|
||||
const name = cleanPath.split('/').filter(Boolean).pop()
|
||||
return name || fallback
|
||||
}
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import { FASTAPI_BASE_URL, authHeaders, readError, type ApiEnvelope, type ScoreType } from './session'
|
||||
import type { EvaluationResult } from './assessment'
|
||||
|
||||
export type TeachingAnswer = {
|
||||
question_id: number | string
|
||||
selected_answer: string
|
||||
}
|
||||
|
||||
export type TeachingCaseOption = {
|
||||
key: string
|
||||
value: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export type TeachingCaseQuestion = {
|
||||
id: number | string
|
||||
question: string
|
||||
options: TeachingCaseOption[]
|
||||
correctAnswer?: string
|
||||
analysis?: string
|
||||
note?: string
|
||||
videoTitle?: string
|
||||
videoDesc?: string
|
||||
videoUrl?: string
|
||||
}
|
||||
|
||||
type TeachingEvaluationPayload = {
|
||||
case_id: number
|
||||
answers: TeachingAnswer[]
|
||||
score_type?: ScoreType
|
||||
}
|
||||
|
||||
type TeachingCaseItemsResponse = {
|
||||
items?: unknown[]
|
||||
questions?: unknown[]
|
||||
results?: unknown[]
|
||||
list?: unknown[]
|
||||
}
|
||||
|
||||
export async function fetchTeachingCaseItems(caseId: number) {
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/teaching/cases/${caseId}/items`, {
|
||||
method: 'GET',
|
||||
headers: authHeaders()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readError(response))
|
||||
}
|
||||
|
||||
const result = (await response.json()) as ApiEnvelope<TeachingCaseItemsResponse | unknown[]>
|
||||
if (result.code !== 'OK' || !result.data) {
|
||||
throw new Error(result.message || '题目列表加载失败')
|
||||
}
|
||||
|
||||
const rawItems = Array.isArray(result.data)
|
||||
? result.data
|
||||
: result.data.items || result.data.questions || result.data.results || result.data.list || []
|
||||
|
||||
return rawItems.map(normalizeTeachingQuestion).filter((item): item is TeachingCaseQuestion => Boolean(item))
|
||||
}
|
||||
|
||||
export async function generateTeachingEvaluation(payload: TeachingEvaluationPayload) {
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/teaching/evaluation`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readError(response))
|
||||
}
|
||||
|
||||
const result = (await response.json()) as ApiEnvelope<EvaluationResult>
|
||||
if (result.code !== 'OK' || !result.data?.evaluation_id) {
|
||||
throw new Error(result.message || '教学评价生成失败')
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
function normalizeTeachingQuestion(item: unknown) {
|
||||
if (!item || typeof item !== 'object') return null
|
||||
|
||||
const source = item as Record<string, unknown>
|
||||
const video = readObject(source.video)
|
||||
const id = readId(source)
|
||||
const question = readString(source, ['question', 'title', 'stem', 'content', 'text'])
|
||||
const options = normalizeOptions(source.options || source.choices || source.answers)
|
||||
|
||||
if (!id || !question || options.length === 0) return null
|
||||
|
||||
return {
|
||||
id,
|
||||
question,
|
||||
options,
|
||||
correctAnswer: readString(source, ['correct_answer', 'correctAnswer', 'answer', 'right_answer']),
|
||||
analysis: readString(source, ['analysis', 'explanation', '解析']),
|
||||
note: readString(source, ['note', 'hint', 'comment']),
|
||||
videoTitle: readString(video, ['title', 'name']) || readString(source, ['video_title', 'videoTitle']),
|
||||
videoDesc: readString(video, ['description', 'desc']) || readString(source, ['video_desc', 'videoDesc', 'video_description']),
|
||||
videoUrl: readString(video, ['url']) || readString(source, ['video_url', 'videoUrl'])
|
||||
}
|
||||
}
|
||||
|
||||
function readObject(value: unknown) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>
|
||||
return {}
|
||||
}
|
||||
|
||||
function readId(source: Record<string, unknown>) {
|
||||
const value = source.question_id || source.id || source.item_id
|
||||
if (typeof value === 'number' || typeof value === 'string') return value
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeOptions(value: unknown): TeachingCaseOption[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item, index) => normalizeOption(item, index)).filter((item): item is TeachingCaseOption => Boolean(item))
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.entries(value as Record<string, unknown>).map(([key, text]) => ({
|
||||
key,
|
||||
value: key,
|
||||
text: typeof text === 'string' ? text : String(text || '')
|
||||
})).filter(item => item.text)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function normalizeOption(item: unknown, index: number) {
|
||||
const fallbackKey = String.fromCharCode(65 + index)
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
key: fallbackKey,
|
||||
value: fallbackKey,
|
||||
text: item
|
||||
}
|
||||
}
|
||||
|
||||
if (!item || typeof item !== 'object') return null
|
||||
|
||||
const source = item as Record<string, unknown>
|
||||
const value = readString(source, ['value', 'key', 'option', 'option_key', 'id']) || fallbackKey
|
||||
const key = readString(source, ['key', 'option', 'option_key']) || value
|
||||
const text = readString(source, ['label', 'text', 'content', 'option_text', 'name']) || value
|
||||
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
function readString(source: Record<string, unknown>, keys: string[]) {
|
||||
for (const key of keys) {
|
||||
const value = source[key]
|
||||
if (typeof value === 'string' && value.trim()) return value
|
||||
if (typeof value === 'number') return String(value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
Reference in New Issue
Block a user