export interface UserListParams { token: string roleType: string page: number size?: number search?: string institution?: string status?: string gender?: string } export interface UserListItem { id: string name: string account: string role: string roleType: string org: string lastLogin: string enabled: boolean phone: string realName: string gender: 0 | 1 | 2 titleName: string major: string trainingStage: string status: 0 | 1 institutionId: string } export interface UserListResult { users: UserListItem[] total: number } export interface InstitutionOption { id: number name: string code: string } export interface DepartmentOption { id: number name: string } export interface CreateUserPayload { phone: string real_name: string role_type: string institution?: number gender?: 0 | 1 | 2 title_name?: string major?: string training_stage?: string status?: 0 | 1 } export interface UpdateUserPayload { real_name?: string phone?: string role_type?: string institution?: number gender?: 0 | 1 | 2 title_name?: string major?: string training_stage?: string status?: 0 | 1 } export interface CreateUserParams { token: string payload: CreateUserPayload } export interface UpdateUserParams { token: string id: string | number payload: UpdateUserPayload } export interface DisableUserParams { token: string id: string | number } export interface ResetUserPasswordParams { token: string id: string | number password?: string } export interface ImportUsersParams { token: string file: File } export interface ExportUsersParams extends UserListParams {} const listKeys = ['results', 'list', 'rows', 'items', 'records', 'users'] const institutionListKeys = ['results', 'list', 'rows', 'items', 'records', 'institutions', 'institution_list', 'institutionList'] const departmentListKeys = ['results', 'list', 'rows', 'items', 'records', 'departments', 'department_list', 'departmentList'] const totalKeys = ['count', 'total', 'total_count', 'totalCount'] 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, 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 getBoolean(record: Record, keys: string[], fallback = true): boolean { for (const key of keys) { const value = record[key] if (typeof value === 'boolean') { return value } if (typeof value === 'number') { return value === 1 } if (typeof value === 'string') { return !['0', 'false', 'disabled', 'inactive', '禁用', '停用', '冻结'].includes(value.toLowerCase()) } } return fallback } function getGender(record: Record): 0 | 1 | 2 { const value = record.gender if (value === 1 || value === '1') return 1 if (value === 2 || value === '2') return 2 return 0 } function getStatus(record: Record): 0 | 1 { const value = record.status if (value === 0 || value === '0' || value === false || value === '禁用') { return 0 } return 1 } function getTotal(record: Record, 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 findUserPayload(data: unknown): { items: unknown[]; total: number } { if (Array.isArray(data)) { return { items: data, total: data.length } } if (!data || typeof data !== 'object') { return { items: [], total: 0 } } const record = data as Record 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 = findUserPayload(record.data) return { items: nested.items, total: nested.total || getTotal(record, nested.items.length) } } return { items: [], total: getTotal(record, 0) } } function findInstitutionItems(data: unknown): unknown[] { if (Array.isArray(data)) { return data } if (!data || typeof data !== 'object') { return [] } const record = data as Record for (const key of institutionListKeys) { const value = record[key] if (Array.isArray(value)) { return value } } if (record.data && typeof record.data === 'object') { return findInstitutionItems(record.data) } return [] } function findDepartmentItems(data: unknown): unknown[] { if (Array.isArray(data)) { return data } if (!data || typeof data !== 'object') { return [] } const record = data as Record for (const key of departmentListKeys) { const value = record[key] if (Array.isArray(value)) { return value } } if (record.data && typeof record.data === 'object') { return findDepartmentItems(record.data) } return [] } function normalizeUser(item: unknown, index: number): UserListItem { const record = item && typeof item === 'object' ? (item as Record) : {} const name = getString(record, ['name', 'real_name', 'realName', 'nickname', 'username'], `用户${index + 1}`) const roleType = getString(record, ['role_type', 'roleType', 'role'], '') const status = getStatus(record) const institutionId = getString(record, ['institution', 'institution_id', 'institutionId'], '') return { id: getString(record, ['id', 'uuid', 'user_id', 'userId'], `${index}`), name, account: getString(record, ['account', 'username', 'phone', 'mobile', 'email'], name), role: getString(record, ['role_name', 'roleName', 'role', 'role_type', 'roleType']), roleType, org: getString(record, ['org', 'organization', 'organization_name', 'organizationName', 'institution_name', 'institutionName', 'hospital', 'hospital_name', 'hospitalName'], institutionId || '-'), lastLogin: getString(record, ['last_login', 'lastLogin', 'login_time', 'loginTime']), enabled: getBoolean(record, ['enabled', 'is_active', 'isActive', 'status']), phone: getString(record, ['phone', 'mobile', 'account', 'username'], ''), realName: getString(record, ['real_name', 'realName', 'name', 'nickname'], name), gender: getGender(record), titleName: getString(record, ['title_name', 'titleName']), major: getString(record, ['major']), trainingStage: getString(record, ['training_stage', 'trainingStage']), status, institutionId } } function normalizeInstitutionOption(item: unknown): InstitutionOption | null { if (typeof item === 'number' || (typeof item === 'string' && Number.isFinite(Number(item)))) { const id = Number(item) return { id, name: `机构 ${id}`, code: '' } } if (!item || typeof item !== 'object') { return null } const record = item as Record const id = Number(getString(record, ['id', 'institution_id', 'institutionId', 'value', 'pk'], '')) if (!Number.isFinite(id) || id <= 0) { return null } return { id, name: getString(record, ['name', 'institution_name', 'institutionName', 'hospital_name', 'hospitalName', 'label', 'title'], `机构 ${id}`), code: getString(record, ['code', 'institution_code', 'institutionCode'], '') } } function normalizeDepartmentOption(item: unknown): DepartmentOption | null { if (typeof item === 'number' || (typeof item === 'string' && Number.isFinite(Number(item)))) { const id = Number(item) return { id, name: `科室 ${id}` } } if (!item || typeof item !== 'object') { return null } const record = item as Record const id = Number(getString(record, ['id', 'department_id', 'departmentId', 'value', 'pk'], '')) if (!Number.isFinite(id) || id <= 0) { return null } return { id, name: getString(record, ['name', 'department_name', 'departmentName', 'label', 'title'], `科室 ${id}`) } } function createAuthorization(token: string) { return /^Bearer\s+/i.test(token) ? token : `Bearer ${token}` } function createUserQuery(params: Partial, includePage = true) { const query = new URLSearchParams() if (params.roleType) { query.set('role_type', params.roleType) } if (params.search?.trim()) { query.set('search', params.search.trim()) } if (params.institution?.trim()) { query.set('institution', params.institution.trim()) } if (params.status) { query.set('status', params.status) } if (params.gender) { query.set('gender', params.gender) } if (includePage && params.page) { query.set('page', String(params.page)) } if (includePage && params.size) { query.set('size', String(params.size)) } 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 fetchUsers(params: UserListParams): Promise { const query = createUserQuery(params) const authorization = createAuthorization(params.token) const response = await fetch(`/server/api/cms/users/?${query.toString()}`, { method: 'GET', headers: { Accept: 'application/json', Authorization: authorization } }) const text = await response.text() const data = parseResponseText(text) if (!response.ok) { const message = getMessageFromResponse(data) || '获取用户列表失败' throw new Error(message) } const payload = findUserPayload(data) return { users: payload.items.map(normalizeUser), total: payload.total } } export async function fetchInstitutionList(token: string): Promise { const response = await fetch('/server/api/user/institution_list/', { method: 'GET', headers: { Accept: 'application/json', Authorization: createAuthorization(token) } }) const text = await response.text() const data = parseResponseText(text) if (!response.ok) { const message = getMessageFromResponse(data) || '获取机构列表失败' throw new Error(message) } const seen = new Set() return findInstitutionItems(data) .map(normalizeInstitutionOption) .filter((item): item is InstitutionOption => { if (!item || seen.has(item.id)) { return false } seen.add(item.id) return true }) } export async function fetchMyDepartments(token: string, institutionId?: number): Promise { const query = new URLSearchParams() if (institutionId) { query.set('institution_id', String(institutionId)) } const response = await fetch(`/server/api/user/my_departments/${query.toString() ? `?${query.toString()}` : ''}`, { method: 'GET', headers: { Accept: 'application/json', Authorization: createAuthorization(token) } }) const text = await response.text() const data = parseResponseText(text) if (!response.ok) { const message = getMessageFromResponse(data) || '获取科室列表失败' throw new Error(message) } const seen = new Set() return findDepartmentItems(data) .map(normalizeDepartmentOption) .filter((item): item is DepartmentOption => { if (!item || seen.has(item.id)) { return false } seen.add(item.id) return true }) } export async function createUser(params: CreateUserParams): Promise { const response = await fetch('/server/api/cms/users/', { 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 updateUser(params: UpdateUserParams): Promise { const response = await fetch(`/server/api/cms/users/${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 disableUser(params: DisableUserParams): Promise { const response = await fetch(`/server/api/cms/users/${params.id}/disable/`, { method: 'POST', headers: { Accept: 'application/json', Authorization: createAuthorization(params.token) } }) return parseMutationResponse(response, '停用用户失败') } export async function resetUserPassword(params: ResetUserPasswordParams): Promise { const payload = params.password ? { password: params.password } : {} const response = await fetch(`/server/api/cms/users/${params.id}/reset-password/`, { method: 'POST', headers: { Accept: 'application/json', Authorization: createAuthorization(params.token), 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) return parseMutationResponse(response, '重置密码失败') } export async function importUsers(params: ImportUsersParams): Promise { const formData = new FormData() formData.append('file', params.file) const response = await fetch('/server/api/cms/users/import/', { method: 'POST', headers: { Accept: 'application/json', Authorization: createAuthorization(params.token) }, body: formData }) return parseMutationResponse(response, '导入用户失败') } export async function exportUsers(params: ExportUsersParams): Promise { const query = createUserQuery(params, false) const url = `/server/api/cms/users/export/${query.toString() ? `?${query.toString()}` : ''}` const response = await fetch(url, { method: 'GET', headers: { Authorization: createAuthorization(params.token) } }) await downloadResponse(response, 'users.xlsx', '导出用户失败') } export async function downloadUserImportTemplate(token: string): Promise { const response = await fetch('/server/api/cms/users/import-template/', { method: 'GET', headers: { Authorization: createAuthorization(token) } }) await downloadResponse(response, 'users-import-template.xlsx', '下载导入模板失败') }