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('file', file)
|
|||
|
|
|
|||
|
|
return request('/api/ocr', {
|
|||
|
|
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
|