export interface InstitutionListParams { token: string search?: string type?: string province?: string city?: string page?: number } export interface InstitutionListItem { id: number code: string name: string type: string level: string province: string city: string bannerUrl: string createdAt: string updatedAt: string } export interface InstitutionListResult { institutions: InstitutionListItem[] total: number } export interface InstitutionPayload { code?: string name?: string type?: string level?: string province?: string city?: string banner_url?: string } export interface CreateInstitutionPayload extends InstitutionPayload { code: string name: string } export interface InstitutionMutationParams { token: string payload: InstitutionPayload } export interface UpdateInstitutionParams extends InstitutionMutationParams { id: number } export interface DisableInstitutionParams { token: string id: number } export interface UploadInstitutionBannerParams { token: string id: number file: File } export interface UploadInstitutionBannerResult { message: string bannerUrl: string raw: unknown } export interface ImportInstitutionsParams { token: string file: File } export interface ExportInstitutionsParams extends InstitutionListParams {} 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 const message = record.message || record.msg || record.detail if (typeof message === 'string') { return message } return getMessageFromResponse(record.data) } function getString(record: Record, key: string): string { const value = record[key] if (typeof value === 'string') { return value } if (typeof value === 'number') { return String(value) } return '' } function getBannerUrlFromResponse(data: unknown): string { if (!data || typeof data !== 'object') { return '' } const record = data as Record const bannerUrl = record.banner_url || record.bannerUrl if (typeof bannerUrl === 'string') { return bannerUrl } const nested = record.data || record.result || record.payload if (nested && typeof nested === 'object') { return getBannerUrlFromResponse(nested) } return '' } function getInstitutionIdFromResponse(data: unknown): number { if (!data || typeof data !== 'object') { return 0 } const record = data as Record const id = record.id || record.institution_id || record.institutionId if (typeof id === 'number' && Number.isFinite(id)) { return id } if (typeof id === 'string' && Number.isFinite(Number(id))) { return Number(id) } const nested = record.data || record.result || record.payload || record.institution if (nested && typeof nested === 'object') { return getInstitutionIdFromResponse(nested) } return 0 } function normalizeInstitution(item: unknown): InstitutionListItem { const record = item && typeof item === 'object' ? (item as Record) : {} const id = record.id return { id: typeof id === 'number' ? id : Number(id) || 0, code: getString(record, 'code'), name: getString(record, 'name'), type: getString(record, 'type'), level: getString(record, 'level'), province: getString(record, 'province'), city: getString(record, 'city'), bannerUrl: getString(record, 'banner_url'), createdAt: getString(record, 'created_at'), updatedAt: getString(record, 'updated_at') } } function getTotal(data: unknown, fallback: number): number { if (!data || typeof data !== 'object') { return fallback } const record = data as Record const total = record.count || record.total if (typeof total === 'number') { return total } if (typeof total === 'string' && Number.isFinite(Number(total))) { return Number(total) } return fallback } function getResults(data: unknown): unknown[] { if (Array.isArray(data)) { return data } if (!data || typeof data !== 'object') { return [] } const record = data as Record if (Array.isArray(record.results)) { return record.results } if (Array.isArray(record.data)) { return record.data } if (record.data && typeof record.data === 'object') { return getResults(record.data) } return [] } function createInstitutionQuery(params: Partial) { const query = new URLSearchParams() if (params.search?.trim()) { query.set('search', params.search.trim()) } if (params.type) { query.set('type', params.type) } if (params.province?.trim()) { query.set('province', params.province.trim()) } if (params.city?.trim()) { query.set('city', params.city.trim()) } if (params.page) { query.set('page', String(params.page)) } return query } async function parseMutationResponse(response: Response, fallbackMessage: string): Promise { const text = await response.text() const data = parseResponseText(text) if (!response.ok) { const message = getMessageFromResponse(data) || fallbackMessage throw new Error(message) } return data } function getFilenameFromDisposition(disposition: string | null, fallback: string) { if (!disposition) { return fallback } const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i) if (utf8Match?.[1]) { return decodeURIComponent(utf8Match[1]) } const filenameMatch = disposition.match(/filename="?([^"]+)"?/i) return filenameMatch?.[1] || fallback } async function downloadResponse(response: Response, fallbackFilename: string, fallbackMessage: string) { if (!response.ok) { const text = await response.text() const data = parseResponseText(text) const message = getMessageFromResponse(data) || fallbackMessage throw new Error(message) } const blob = await response.blob() const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = getFilenameFromDisposition(response.headers.get('content-disposition'), fallbackFilename) document.body.appendChild(link) link.click() link.remove() URL.revokeObjectURL(url) } export async function fetchInstitutions(params: InstitutionListParams): Promise { const query = createInstitutionQuery(params) const url = `/server/api/cms/institutions/${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 results = getResults(data) return { institutions: results.map(normalizeInstitution), total: getTotal(data, results.length) } } export async function createInstitution(params: InstitutionMutationParams): Promise { const response = await fetch('/server/api/cms/institutions/', { method: 'POST', headers: { Accept: 'application/json', Authorization: createAuthorization(params.token), 'Content-Type': 'application/json' }, body: JSON.stringify(params.payload) }) const data = await parseMutationResponse(response, '新增机构失败') const normalized = normalizeInstitution(data) const id = normalized.id || getInstitutionIdFromResponse(data) return { ...normalized, id } } export async function updateInstitution(params: UpdateInstitutionParams): Promise { const response = await fetch(`/server/api/cms/institutions/${params.id}/update/`, { 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 disableInstitution(params: DisableInstitutionParams): Promise { const response = await fetch(`/server/api/cms/institutions/${params.id}/disable/`, { method: 'POST', headers: { Accept: 'application/json', Authorization: createAuthorization(params.token) } }) return parseMutationResponse(response, '停用机构失败') } export async function uploadInstitutionBanner(params: UploadInstitutionBannerParams): Promise { const formData = new FormData() formData.append('file', params.file) const response = await fetch(`/server/api/cms/institutions/${params.id}/banner/`, { method: 'POST', headers: { Accept: 'application/json', Authorization: createAuthorization(params.token) }, body: formData }) const data = await parseMutationResponse(response, '上传机构背景图失败') return { message: getMessageFromResponse(data), bannerUrl: getBannerUrlFromResponse(data), raw: data } } export async function importInstitutions(params: ImportInstitutionsParams): Promise { const formData = new FormData() formData.append('file', params.file) const response = await fetch('/server/api/cms/institutions/import/', { method: 'POST', headers: { Accept: 'application/json', Authorization: createAuthorization(params.token) }, body: formData }) return parseMutationResponse(response, '导入机构失败') } export async function exportInstitutions(params: ExportInstitutionsParams): Promise { const query = createInstitutionQuery(params) const url = `/server/api/cms/institutions/export/${query.toString() ? `?${query.toString()}` : ''}` const response = await fetch(url, { method: 'GET', headers: { Authorization: createAuthorization(params.token) } }) await downloadResponse(response, 'institutions.xlsx', '导出机构失败') } export async function downloadInstitutionImportTemplate(token: string): Promise { const response = await fetch('/server/api/cms/institutions/import-template/', { method: 'GET', headers: { Authorization: createAuthorization(token) } }) await downloadResponse(response, 'institutions-import-template.xlsx', '下载导入模板失败') }