600 lines
16 KiB
TypeScript
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', '下载导入模板失败')
|
|
}
|