Files
cms/src/api/users.ts
T
2026-06-18 17:36:17 +08:00

600 lines
16 KiB
TypeScript

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<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>, 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<string, unknown>, 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<string, unknown>): 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<string, unknown>): 0 | 1 {
const value = record.status
if (value === 0 || value === '0' || value === false || value === '禁用') {
return 0
}
return 1
}
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 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<string, unknown>
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<string, unknown>
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<string, unknown>
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<string, unknown>) : {}
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<string, unknown>
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<string, unknown>
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<UserListParams>, 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<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 fetchUsers(params: UserListParams): Promise<UserListResult> {
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<InstitutionOption[]> {
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<number>()
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<DepartmentOption[]> {
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<number>()
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<unknown> {
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<unknown> {
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<unknown> {
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<unknown> {
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<unknown> {
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<void> {
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<void> {
const response = await fetch('/server/api/cms/users/import-template/', {
method: 'GET',
headers: {
Authorization: createAuthorization(token)
}
})
await downloadResponse(response, 'users-import-template.xlsx', '下载导入模板失败')
}