136 lines
3.6 KiB
TypeScript
136 lines
3.6 KiB
TypeScript
import { eventBus, EVENT_TYPES, notifyApiError } from "./event-bus"
|
||
|
||
// API 基础路径
|
||
const API_BASE_URL = "/api"
|
||
|
||
// Token 过期时间 (毫秒)
|
||
const TOKEN_EXPIRY_TIME = 24 * 60 * 60 * 1000 // 24小时
|
||
|
||
/**
|
||
* 检查 token 是否过期
|
||
* @returns boolean
|
||
*/
|
||
export function isTokenExpired(): boolean {
|
||
const tokenTimestamp = localStorage.getItem("tokenTimestamp")
|
||
if (!tokenTimestamp) return true
|
||
|
||
const timestamp = Number.parseInt(tokenTimestamp, 10)
|
||
const now = Date.now()
|
||
|
||
return now - timestamp > TOKEN_EXPIRY_TIME
|
||
}
|
||
|
||
/**
|
||
* 保存 token 和时间戳
|
||
* @param token 授权令牌
|
||
*/
|
||
export function saveToken(token: string): void {
|
||
localStorage.setItem("token", token)
|
||
localStorage.setItem("tokenTimestamp", Date.now().toString())
|
||
}
|
||
|
||
/**
|
||
* 清除 token 和相关数据
|
||
*/
|
||
export function clearToken(): void {
|
||
localStorage.removeItem("token")
|
||
localStorage.removeItem("tokenTimestamp")
|
||
localStorage.removeItem("user")
|
||
}
|
||
|
||
/**
|
||
* 发送带有授权令牌的 API 请求
|
||
* @param url 请求 URL (不需要包含 API_BASE_URL)
|
||
* @param options 请求选项
|
||
* @returns Promise<Response>
|
||
*/
|
||
export async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
|
||
// 从本地存储获取 token
|
||
const token = localStorage.getItem("token")
|
||
|
||
// 准备请求头
|
||
const headers = new Headers(options.headers || {})
|
||
|
||
// 如果有 token,添加到请求头
|
||
if (token) {
|
||
headers.set("Authorization", "Bearer " + token)
|
||
}
|
||
|
||
// 确保 Content-Type 为 application/json
|
||
if (options.body && !headers.has("Content-Type")) {
|
||
headers.set("Content-Type", "application/json")
|
||
}
|
||
|
||
// 合并选项
|
||
const mergedOptions = {
|
||
...options,
|
||
headers,
|
||
}
|
||
|
||
// 构建完整 URL (确保不重复添加 /api)
|
||
const fullUrl = url.startsWith("/") ? `${API_BASE_URL}${url}` : `${API_BASE_URL}/${url}`
|
||
|
||
try {
|
||
// 发送请求
|
||
const response = await fetch(fullUrl, mergedOptions)
|
||
|
||
// 检查是否是 401 错误 (未授权),可能是 token 过期
|
||
if (response.status === 401) {
|
||
// 清除无效的 token
|
||
clearToken()
|
||
|
||
// 触发认证过期事件
|
||
eventBus.emit(EVENT_TYPES.AUTH_EXPIRED)
|
||
|
||
// 如果不是登录或注册请求,可以重定向到登录页面
|
||
if (!url.includes("login") && !url.includes("register")) {
|
||
// 使用 window.location 而不是 router,因为这是一个工具函数
|
||
window.location.href = "/login"
|
||
return new Response(JSON.stringify({ code: 401, msg: "登录已过期,请重新登录" }), {
|
||
headers: { "Content-Type": "application/json" },
|
||
})
|
||
}
|
||
}
|
||
|
||
// 克隆响应以便可以读取两次
|
||
const clonedResponse = response.clone()
|
||
|
||
// 尝试解析响应为JSON
|
||
try {
|
||
const data = await clonedResponse.json()
|
||
|
||
// 如果响应码不是200,触发API错误事件
|
||
if (data.code !== 0) {
|
||
eventBus.emit(EVENT_TYPES.API_ERROR, {
|
||
url,
|
||
status: response.status,
|
||
code: data.code,
|
||
message: data.msg || "请求失败",
|
||
})
|
||
|
||
// 使用事件总线发送通知
|
||
notifyApiError("请求失败", data.msg || "服务器返回错误")
|
||
}
|
||
} catch (e) {
|
||
// 如果响应不是JSON格式,忽略错误
|
||
}
|
||
|
||
return response
|
||
} catch (error) {
|
||
// 网络错误或其他异常
|
||
const errorMessage = error instanceof Error ? error.message : "网络请求失败"
|
||
|
||
// 触发API错误事件
|
||
eventBus.emit(EVENT_TYPES.API_ERROR, {
|
||
url,
|
||
error: errorMessage,
|
||
})
|
||
|
||
// 使用事件总线发送通知
|
||
notifyApiError("网络错误", errorMessage)
|
||
|
||
// 重新抛出错误,让调用者处理
|
||
throw error
|
||
}
|
||
}
|