2025-05-22 20:42:10 +08:00

136 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}