feat: 联调
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user