231 lines
5.7 KiB
JavaScript
231 lines
5.7 KiB
JavaScript
// 统一的 API 客户端
|
||
|
||
const API_BASE = '' // 生产环境由 Nginx 代理
|
||
|
||
import { ErrorCodes, matchErrorCode } from './errorCodes.js'
|
||
|
||
// 错误处理
|
||
class ApiError extends Error {
|
||
constructor(message, status, data, code = null) {
|
||
super(message)
|
||
this.name = 'ApiError'
|
||
this.status = status
|
||
this.data = data
|
||
this.code = code || matchErrorCode(status, message).code
|
||
}
|
||
|
||
// 获取格式化的错误信息
|
||
get formattedMessage() {
|
||
return `${this.code}: ${this.message}`
|
||
}
|
||
}
|
||
|
||
// 获取 Token
|
||
const getToken = () => localStorage.getItem('token')
|
||
|
||
// 统一请求方法
|
||
async function request(endpoint, options = {}) {
|
||
const token = getToken()
|
||
|
||
const defaultHeaders = {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
|
||
if (token) {
|
||
defaultHeaders['Authorization'] = `Bearer ${token}`
|
||
}
|
||
|
||
const config = {
|
||
...options,
|
||
headers: {
|
||
...defaultHeaders,
|
||
...(options.headers || {})
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}${endpoint}`, config)
|
||
|
||
const data = await response.json()
|
||
|
||
if (!response.ok) {
|
||
// 处理 401 未授权
|
||
if (response.status === 401) {
|
||
localStorage.removeItem('token')
|
||
localStorage.removeItem('user')
|
||
window.location.hash = '#/login'
|
||
throw new ApiError(
|
||
data.detail || data.message || '登录已过期,请重新登录',
|
||
401,
|
||
data,
|
||
'E00010'
|
||
)
|
||
}
|
||
|
||
// 处理 422 验证错误
|
||
if (response.status === 422 && data.detail) {
|
||
const detail = Array.isArray(data.detail) ? data.detail[0] : data.detail
|
||
const field = detail.loc ? detail.loc.join('.') : ''
|
||
const msg = detail.msg || detail.message || data.message
|
||
|
||
throw new ApiError(
|
||
`${field}: ${msg}`,
|
||
422,
|
||
data
|
||
)
|
||
}
|
||
|
||
// 其他错误
|
||
throw new ApiError(
|
||
data.detail || data.message || data.error || '请求失败',
|
||
response.status,
|
||
data
|
||
)
|
||
}
|
||
|
||
return data
|
||
} catch (error) {
|
||
// 网络错误
|
||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||
throw new ApiError('网络连接失败,请检查网络', 0, null, 'E00001')
|
||
}
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// API 模块
|
||
export const api = {
|
||
// 认证
|
||
auth: {
|
||
login: async (username, password) => {
|
||
// 登录时不使用 token
|
||
const params = new URLSearchParams()
|
||
params.append('username', username)
|
||
params.append('password', password)
|
||
|
||
const response = await fetch(`${API_BASE}/api/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: params.toString()
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (!response.ok) {
|
||
// 优先使用 error.code 和 error.message(后端标准格式)
|
||
const errorCode = data.error?.code || data.code
|
||
const errorMsg = data.error?.message || data.detail || data.message || '登录失败'
|
||
|
||
const error = new ApiError(errorMsg, response.status, data)
|
||
if (errorCode) error.code = errorCode
|
||
throw error
|
||
}
|
||
|
||
return data
|
||
},
|
||
|
||
register: (data) => request('/api/auth/register', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data)
|
||
})
|
||
},
|
||
|
||
// 当前用户
|
||
user: {
|
||
me: async () => {
|
||
const token = getToken()
|
||
if (!token) throw new ApiError('未登录', 401, null, 'E00010')
|
||
|
||
const response = await fetch(`${API_BASE}/api/users/me`, {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (!response.ok) {
|
||
throw new ApiError(
|
||
data.detail || data.message || '获取用户信息失败',
|
||
response.status,
|
||
data
|
||
)
|
||
}
|
||
|
||
return data
|
||
},
|
||
update: (data) => request('/api/users/me', {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data)
|
||
})
|
||
},
|
||
|
||
// 藏品
|
||
collections: {
|
||
// 列表
|
||
list: (params = {}) => {
|
||
const query = new URLSearchParams(params).toString()
|
||
return request(`/api/collections${query ? '?' + query : ''}`)
|
||
},
|
||
|
||
// 详情
|
||
get: (id) => request(`/api/collections/${id}`),
|
||
|
||
// 创建
|
||
create: (data) => request('/api/collections', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data)
|
||
}),
|
||
|
||
// 更新
|
||
update: (id, data) => request(`/api/collections/${id}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data)
|
||
}),
|
||
|
||
// 删除
|
||
delete: (id) => request(`/api/collections/${id}`, {
|
||
method: 'DELETE'
|
||
}),
|
||
|
||
// 下一个编号
|
||
nextCode: () => request('/api/collections/next-code'),
|
||
|
||
// 统计
|
||
stats: (params = {}) => {
|
||
const query = new URLSearchParams(params).toString()
|
||
return request(`/api/collections/stats${query ? '?' + query : ''}`)
|
||
},
|
||
|
||
// 导出
|
||
export: (params) => request(`/api/collections/export?${new URLSearchParams(params)}`)
|
||
},
|
||
|
||
// OCR
|
||
ocr: {
|
||
recognize: (file) => {
|
||
const formData = new FormData()
|
||
formData.append('image', file)
|
||
|
||
return request('/api/ocr/recognize', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
}
|
||
},
|
||
|
||
// 用户管理(仅管理员)
|
||
admin: {
|
||
users: {
|
||
list: (page = 1, limit = 20) => request(`/api/admin/users?page=${page}&limit=${limit}`),
|
||
get: (userId) => request(`/api/admin/users/${userId}`),
|
||
collections: (userId) => request(`/api/admin/users/${userId}/collections`),
|
||
count: (userId) => request(`/api/admin/users/${userId}/count`)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出错误类
|
||
export { ApiError }
|
||
|
||
// 默认导出
|
||
export default api
|