feat: 联调

This commit is contained in:
王天骄
2026-06-12 18:10:15 +08:00
parent 9fddb42ebe
commit 1d093c9589
12 changed files with 2309 additions and 56 deletions
+572
View File
@@ -0,0 +1,572 @@
export type DraftCaseType = 'traditional' | 'teaching'
export type CasePublishStatus = 0 | 1 | 2
export interface CaseListParams {
token: string
search?: string
case_type?: string
publish_status?: CasePublishStatus | ''
institution?: string
department?: string
status?: string
osce_enabled?: boolean | string | ''
ordering?: string
page?: number
}
export interface CaseListItem {
id: string
title: string
caseType: string
publishStatus: CasePublishStatus | null
institutionId: string
institutionName: string
departmentId: string
departmentName: string
difficulty: string
chiefComplaint: string
tags: string[]
icdCodes: string[]
estimatedMinutes: number | null
updatedAt: string
createdAt: string
}
export interface CaseListResult {
cases: CaseListItem[]
total: number
}
export interface CaseScoringRulePayload {
dimension: string
score_weight: number
description?: string
ai_auto_score?: boolean
scoring_standard?: string
}
export interface CaseExamItemPayload {
item_code: string
item_name?: string
item_type?: string
}
export interface CreateCaseDraftPayload {
title: string
case_type: DraftCaseType
institution_id?: number
department_name?: string
traditional?: Record<string, unknown>
teaching?: Record<string, unknown>
scoring_rules: CaseScoringRulePayload[]
exam_items?: CaseExamItemPayload[]
difficulty?: string
chief_complaint?: string
description?: string
patient_age?: number
patient_gender?: string
tags?: string | string[]
icd_codes?: string[]
estimated_minutes?: number
osce_enabled?: boolean
}
export interface CreateCaseDraftParams {
token: string
payload: CreateCaseDraftPayload
}
export interface AiGenerateCasePayload {
prompt: string
case_type: DraftCaseType
}
export interface AiGenerateCaseParams {
token: string
payload: AiGenerateCasePayload
}
export interface AiCaseUsage {
promptTokens: number | null
completionTokens: number | null
}
export interface AiGeneratedCaseData extends Record<string, unknown> {
title?: string
case_type?: string
chief_complaint?: string
department_name?: string
patient_age?: number
patient_gender?: string
}
export interface AiGenerateCaseResult {
parseId: string
caseType: string
aiUsage: AiCaseUsage
promptVersion: string
parsingSeconds: number | null
generatingSeconds: number | null
data: AiGeneratedCaseData
raw: unknown
}
export interface ImportCasePdfParams {
token: string
file: File
}
export interface UpdateCaseRelationsPayload {
institution_id?: number | null
department_id?: number | null
department_name?: string
}
export interface UpdateCaseRelationsParams {
token: string
id: string | number
payload: UpdateCaseRelationsPayload
}
export interface CaseActionParams {
token: string
id: string | number
}
const listKeys = ['results', 'list', 'rows', 'items', 'records', 'cases']
const totalKeys = ['count', 'total', 'total_count', 'totalCount']
function createAuthorization(token: string) {
return /^Bearer\s+/i.test(token) ? token : `Bearer ${token}`
}
function parseResponseText(text: string): unknown {
if (!text) {
return null
}
try {
return JSON.parse(text)
} catch {
return null
}
}
function getMessageFromResponse(data: unknown): string {
if (!data || typeof data !== 'object') {
return ''
}
const record = data as Record<string, unknown>
const message = record.message || record.msg || record.detail
if (typeof message === 'string') {
return message
}
return getMessageFromResponse(record.data)
}
function getRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {}
}
function getFirst(record: Record<string, unknown>, keys: string[]): unknown {
for (const key of keys) {
if (record[key] !== undefined && record[key] !== null) {
return record[key]
}
}
return undefined
}
function getString(record: Record<string, unknown>, keys: string[], fallback = ''): string {
for (const key of keys) {
const value = record[key]
if (typeof value === 'string' && value.trim()) {
return value
}
if (typeof value === 'number') {
return String(value)
}
}
return fallback
}
function getNumber(record: Record<string, unknown>, keys: string[]): number | null {
const value = getFirst(record, keys)
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
return Number(value)
}
return null
}
function getStringList(record: Record<string, unknown>, keys: string[]): string[] {
const value = getFirst(record, keys)
if (Array.isArray(value)) {
return value
.map(item => {
if (typeof item === 'string' || typeof item === 'number') {
return String(item)
}
const itemRecord = getRecord(item)
return getString(itemRecord, ['name', 'title', 'code', 'value'])
})
.map(item => item.trim())
.filter(Boolean)
}
if (typeof value === 'string') {
return value
.split(/[,\n,;;]/)
.map(item => item.trim())
.filter(Boolean)
}
return []
}
function normalizePublishStatus(value: unknown): CasePublishStatus | null {
if (value === 0 || value === 1 || value === 2) {
return value
}
if (typeof value === 'string') {
const normalized = value.trim()
const numeric = Number(normalized)
if (numeric === 0 || numeric === 1 || numeric === 2) {
return numeric
}
if (normalized.includes('草稿') || normalized.toLowerCase() === 'draft') {
return 0
}
if (normalized.includes('发布') || normalized.toLowerCase() === 'published') {
return 2
}
if (normalized.includes('正常') || normalized.includes('启用') || normalized.toLowerCase() === 'active') {
return 1
}
}
return null
}
function getTotal(record: Record<string, unknown>, fallback: number): number {
for (const key of totalKeys) {
const value = record[key]
if (typeof value === 'number') {
return value
}
if (typeof value === 'string' && Number.isFinite(Number(value))) {
return Number(value)
}
}
return fallback
}
function findCasePayload(data: unknown): { items: unknown[]; total: number } {
if (Array.isArray(data)) {
return { items: data, total: data.length }
}
const record = getRecord(data)
if (!Object.keys(record).length) {
return { items: [], total: 0 }
}
for (const key of listKeys) {
const value = record[key]
if (Array.isArray(value)) {
return { items: value, total: getTotal(record, value.length) }
}
}
if (Array.isArray(record.data)) {
return { items: record.data, total: getTotal(record, record.data.length) }
}
if (record.data && typeof record.data === 'object') {
const nested = findCasePayload(record.data)
return {
items: nested.items,
total: nested.total || getTotal(record, nested.items.length)
}
}
return { items: [], total: getTotal(record, 0) }
}
function getRelatedId(record: Record<string, unknown>, directKeys: string[], objectKeys: string[]): string {
const direct = getString(record, directKeys)
if (direct) {
return direct
}
for (const key of objectKeys) {
const nested = getRecord(record[key])
const value = getString(nested, ['id', 'pk', 'value'])
if (value) {
return value
}
}
return ''
}
function getRelatedName(record: Record<string, unknown>, directKeys: string[], objectKeys: string[]): string {
const direct = getString(record, directKeys)
if (direct) {
return direct
}
for (const key of objectKeys) {
const nested = getRecord(record[key])
const value = getString(nested, ['name', 'title', 'label'])
if (value) {
return value
}
}
return ''
}
function normalizeCase(item: unknown, index: number): CaseListItem {
const record = getRecord(item)
const publishStatus = normalizePublishStatus(getFirst(record, ['publish_status', 'publishStatus', 'status']))
const title = getString(record, ['title', 'name', 'case_title', 'caseTitle'], `病例${index + 1}`)
return {
id: getString(record, ['id', 'uuid', 'case_id', 'caseId'], `${index}`),
title,
caseType: getString(record, ['case_type', 'caseType', 'type']),
publishStatus,
institutionId: getRelatedId(record, ['institution_id', 'institutionId'], ['institution', 'institution_info', 'institutionInfo']),
institutionName: getRelatedName(record, ['institution_name', 'institutionName', 'organization_name', 'organizationName'], ['institution', 'institution_info', 'institutionInfo']),
departmentId: getRelatedId(record, ['department_id', 'departmentId'], ['department', 'department_info', 'departmentInfo']),
departmentName: getRelatedName(record, ['department_name', 'departmentName'], ['department', 'department_info', 'departmentInfo']),
difficulty: getString(record, ['difficulty']),
chiefComplaint: getString(record, ['chief_complaint', 'chiefComplaint']),
tags: getStringList(record, ['tags', 'tag_list', 'tagList']),
icdCodes: getStringList(record, ['icd_codes', 'icdCodes', 'icd_list', 'icdList']),
estimatedMinutes: getNumber(record, ['estimated_minutes', 'estimatedMinutes']),
updatedAt: getString(record, ['updated_at', 'updatedAt', 'modified_at', 'modifiedAt']),
createdAt: getString(record, ['created_at', 'createdAt'])
}
}
function normalizeAiGenerateResult(data: unknown): AiGenerateCaseResult {
const root = getRecord(data)
const payload = root.parse_id || root.parseId ? root : getRecord(root.data)
const usage = getRecord(getFirst(payload, ['ai_usage', 'aiUsage']))
const generatedData = getRecord(payload.data)
return {
parseId: getString(payload, ['parse_id', 'parseId']),
caseType: getString(payload, ['case_type', 'caseType']),
aiUsage: {
promptTokens: getNumber(usage, ['prompt_tokens', 'promptTokens']),
completionTokens: getNumber(usage, ['completion_tokens', 'completionTokens'])
},
promptVersion: getString(payload, ['prompt_version', 'promptVersion']),
parsingSeconds: getNumber(payload, ['parsing_seconds', 'parsingSeconds']),
generatingSeconds: getNumber(payload, ['generating_seconds', 'generatingSeconds']),
data: generatedData as AiGeneratedCaseData,
raw: data
}
}
function createCaseQuery(params: Partial<CaseListParams>) {
const query = new URLSearchParams()
if (params.search?.trim()) {
query.set('search', params.search.trim())
}
if (params.case_type) {
query.set('case_type', params.case_type)
}
if (params.publish_status !== undefined && params.publish_status !== '') {
query.set('publish_status', String(params.publish_status))
}
if (params.institution?.trim()) {
query.set('institution', params.institution.trim())
}
if (params.department?.trim()) {
query.set('department', params.department.trim())
}
if (params.status?.trim()) {
query.set('status', params.status.trim())
}
if (params.osce_enabled !== undefined && params.osce_enabled !== '') {
query.set('osce_enabled', String(params.osce_enabled))
}
if (params.ordering?.trim()) {
query.set('ordering', params.ordering.trim())
}
if (params.page) {
query.set('page', String(params.page))
}
return query
}
async function parseMutationResponse(response: Response, fallbackMessage: string): Promise<unknown> {
const text = await response.text()
const data = parseResponseText(text)
if (!response.ok) {
const message = getMessageFromResponse(data) || fallbackMessage
throw new Error(message)
}
return data
}
export async function fetchCases(params: CaseListParams): Promise<CaseListResult> {
const query = createCaseQuery(params)
const url = `/server/api/cms/cases/${query.toString() ? `?${query.toString()}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token)
}
})
const text = await response.text()
const data = parseResponseText(text)
if (!response.ok) {
const message = getMessageFromResponse(data) || '获取病例列表失败'
throw new Error(message)
}
const payload = findCasePayload(data)
return {
cases: payload.items.map(normalizeCase),
total: payload.total
}
}
export async function createCaseDraft(params: CreateCaseDraftParams): Promise<unknown> {
const response = await fetch('/server/api/cms/cases/', {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token),
'Content-Type': 'application/json'
},
body: JSON.stringify(params.payload)
})
return parseMutationResponse(response, '新增病例草稿失败')
}
export async function generateCaseWithAi(params: AiGenerateCaseParams): Promise<AiGenerateCaseResult> {
const response = await fetch('/server/api/cms/cases/ai-generate/', {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token),
'Content-Type': 'application/json'
},
body: JSON.stringify(params.payload)
})
const text = await response.text()
const data = parseResponseText(text)
if (!response.ok) {
const message = getMessageFromResponse(data) || 'AI 生成病例失败'
throw new Error(message)
}
return normalizeAiGenerateResult(data)
}
export async function importCasePdf(params: ImportCasePdfParams): Promise<unknown> {
const formData = new FormData()
formData.append('file', params.file)
const response = await fetch('/server/api/cms/cases/import-pdf/', {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token)
},
body: formData
})
return parseMutationResponse(response, '导入 PDF 病例失败')
}
export async function updateCaseRelations(params: UpdateCaseRelationsParams): Promise<unknown> {
const response = await fetch(`/server/api/cms/cases/${params.id}/relations/`, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token),
'Content-Type': 'application/json'
},
body: JSON.stringify(params.payload)
})
return parseMutationResponse(response, '编辑病例关联失败')
}
export async function submitCase(params: CaseActionParams): Promise<unknown> {
const response = await fetch(`/server/api/cms/cases/${params.id}/submit/`, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token)
}
})
return parseMutationResponse(response, '提交病例失败')
}
export async function disableCase(params: CaseActionParams): Promise<unknown> {
const response = await fetch(`/server/api/cms/cases/${params.id}/disable/`, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token)
}
})
return parseMutationResponse(response, '停用病例失败')
}
export async function publishCase(params: CaseActionParams): Promise<unknown> {
const response = await fetch(`/server/api/cms/cases/${params.id}/publish/`, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token)
}
})
return parseMutationResponse(response, '发布病例失败')
}
export async function fetchCaseFull(params: CaseActionParams): Promise<unknown> {
const response = await fetch(`/server/api/cms/cases/${params.id}/full/`, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: createAuthorization(params.token)
}
})
const text = await response.text()
const data = parseResponseText(text)
if (!response.ok) {
const message = getMessageFromResponse(data) || '获取病例详情失败'
throw new Error(message)
}
return data
}