import type { ApiPayload } from '@/types/api'; import { clearAuth, getAuth } from './auth'; const apiBase = import.meta.env.VITE_API_BASE || '/dev-api'; type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; export interface RequestOptions { url: string; method?: HttpMethod; data?: unknown; header?: Record; timeout?: number; auth?: boolean; clearAuthOnUnauthorized?: boolean; } const normalizeUrl = (url: string) => `${apiBase}${url.startsWith('/') ? url : `/${url}`}`; export const apiUrl = normalizeUrl; export const authHeaders = (json = true, includeAuth = true) => { const auth = getAuth(); return { ...(json ? { 'Content-Type': 'application/json' } : {}), ...(includeAuth && auth.token ? { Authorization: `Bearer ${auth.token}` } : {}), ...(includeAuth && auth.clientId ? { clientid: auth.clientId } : {}) }; }; export const readPayload = (statusCode: number, data: unknown, clearAuthOnUnauthorized = true): T => { const payload = data as ApiPayload; const code = typeof payload?.code === 'number' ? payload.code : undefined; if (statusCode < 200 || statusCode >= 300 || (code !== undefined && code !== 200)) { if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403 || code === 401 || code === 403)) { clearAuth(); } throw new Error(payload?.msg || `HTTP ${statusCode}`); } return payload?.data as T; }; export const parsePayloadText = (statusCode: number, text: string, clearAuthOnUnauthorized = true) => { try { return JSON.parse(text || '{}'); } catch (error) { if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403)) clearAuth(); throw error instanceof SyntaxError ? new Error(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败') : error; } }; export const readTextPayload = (statusCode: number, text: string): T => readPayload(statusCode, parsePayloadText(statusCode, text)); const requestWithXhr = async (options: RequestOptions): Promise => { const method = options.method || 'GET'; const hasBody = method !== 'GET' && options.data !== undefined; const response = await new Promise<{ status: number; data: unknown }>((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(method, normalizeUrl(options.url)); xhr.timeout = options.timeout || 30000; Object.entries({ ...authHeaders(hasBody, options.auth !== false), ...options.header }).forEach(([key, value]) => xhr.setRequestHeader(key, value)); xhr.onload = () => { try { resolve({ status: xhr.status, data: parsePayloadText(xhr.status, xhr.responseText || 'null', options.clearAuthOnUnauthorized !== false) }); } catch { reject(new Error(xhr.status >= 400 ? `HTTP ${xhr.status}` : '响应解析失败')); } }; xhr.onerror = () => reject(new Error('网络请求失败')); xhr.ontimeout = () => reject(new Error('网络请求超时')); xhr.send(hasBody ? JSON.stringify(options.data) : undefined); }); return readPayload(response.status, response.data, options.clearAuthOnUnauthorized !== false); }; export const apiRequest = async (options: RequestOptions): Promise => { if (typeof XMLHttpRequest !== 'undefined') { return requestWithXhr(options); } const response = await new Promise((resolve, reject) => { uni.request({ url: normalizeUrl(options.url), method: options.method || 'GET', data: options.data as string | ArrayBuffer | Record | undefined, header: { ...authHeaders(options.method !== 'GET', options.auth !== false), ...options.header }, timeout: options.timeout || 30000, success: resolve, fail: reject }); }); return readPayload(response.statusCode, response.data, options.clearAuthOnUnauthorized !== false); }; export const authenticatedFileUrl = async (url: string): Promise => { const fullUrl = normalizeUrl(url); if (typeof XMLHttpRequest !== 'undefined' && typeof URL !== 'undefined') { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', fullUrl); xhr.responseType = 'blob'; Object.entries(authHeaders(false)).forEach(([key, value]) => xhr.setRequestHeader(key, value)); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(URL.createObjectURL(xhr.response)); return; } if (xhr.status === 401 || xhr.status === 403) clearAuth(); reject(new Error(`HTTP ${xhr.status}`)); }; xhr.onerror = () => reject(new Error('网络请求失败')); xhr.send(); }); } return new Promise((resolve, reject) => { uni.downloadFile({ url: fullUrl, header: authHeaders(false), success: (response) => { if (response.statusCode >= 200 && response.statusCode < 300) { resolve(response.tempFilePath); return; } if (response.statusCode === 401 || response.statusCode === 403) clearAuth(); reject(new Error(`HTTP ${response.statusCode}`)); }, fail: () => reject(new Error('网络请求失败')) }); }); }; export const releaseAuthenticatedFileUrl = (url: string) => { if (url.startsWith('blob:') && typeof URL !== 'undefined') { URL.revokeObjectURL(url); } };