2026-06-12 15:43:30 +08:00
|
|
|
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
|
2026-06-17 15:11:53 +08:00
|
|
|
banner_url?: string
|
2026-06-12 15:43:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 15:11:53 +08:00
|
|
|
export interface UploadInstitutionBannerParams {
|
|
|
|
|
token: string
|
|
|
|
|
id: number
|
|
|
|
|
file: File
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UploadInstitutionBannerResult {
|
|
|
|
|
message: string
|
|
|
|
|
bannerUrl: string
|
|
|
|
|
raw: unknown
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 15:43:30 +08:00
|
|
|
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<string, unknown>
|
|
|
|
|
const message = record.message || record.msg || record.detail
|
|
|
|
|
if (typeof message === 'string') {
|
|
|
|
|
return message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getMessageFromResponse(record.data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getString(record: Record<string, unknown>, key: string): string {
|
|
|
|
|
const value = record[key]
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
if (typeof value === 'number') {
|
|
|
|
|
return String(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 15:11:53 +08:00
|
|
|
function getBannerUrlFromResponse(data: unknown): string {
|
|
|
|
|
if (!data || typeof data !== 'object') {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const record = data as Record<string, unknown>
|
|
|
|
|
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 ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 14:53:49 +08:00
|
|
|
function getInstitutionIdFromResponse(data: unknown): number {
|
|
|
|
|
if (!data || typeof data !== 'object') {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const record = data as Record<string, unknown>
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 15:43:30 +08:00
|
|
|
function normalizeInstitution(item: unknown): InstitutionListItem {
|
|
|
|
|
const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : {}
|
|
|
|
|
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<string, unknown>
|
|
|
|
|
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<string, unknown>
|
|
|
|
|
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<InstitutionListParams>) {
|
|
|
|
|
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<unknown> {
|
|
|
|
|
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<InstitutionListResult> {
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 14:53:49 +08:00
|
|
|
export async function createInstitution(params: InstitutionMutationParams): Promise<InstitutionListItem> {
|
2026-06-12 15:43:30 +08:00
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-18 14:53:49 +08:00
|
|
|
const data = await parseMutationResponse(response, '新增机构失败')
|
|
|
|
|
const normalized = normalizeInstitution(data)
|
|
|
|
|
const id = normalized.id || getInstitutionIdFromResponse(data)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...normalized,
|
|
|
|
|
id
|
|
|
|
|
}
|
2026-06-12 15:43:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateInstitution(params: UpdateInstitutionParams): Promise<unknown> {
|
|
|
|
|
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<unknown> {
|
|
|
|
|
const response = await fetch(`/server/api/cms/institutions/${params.id}/disable/`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
Authorization: createAuthorization(params.token)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return parseMutationResponse(response, '停用机构失败')
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 15:11:53 +08:00
|
|
|
export async function uploadInstitutionBanner(params: UploadInstitutionBannerParams): Promise<UploadInstitutionBannerResult> {
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 15:43:30 +08:00
|
|
|
export async function importInstitutions(params: ImportInstitutionsParams): Promise<unknown> {
|
|
|
|
|
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<void> {
|
|
|
|
|
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<void> {
|
|
|
|
|
const response = await fetch('/server/api/cms/institutions/import-template/', {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: createAuthorization(token)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await downloadResponse(response, 'institutions-import-template.xlsx', '下载导入模板失败')
|
|
|
|
|
}
|