ch-xymy-h5/src/api/http.js
chenhao 7083c8d27e 恢复提现入口、abort 请求不再弹 toast、其他小修
- 恢复 Cashout/CashoutRecord/CashoutAccount 路由及 My/Balance 提现入口,去掉 VIP 校验
- 请求被 abort 时保持 pending,不再触发 caller 的 catch 弹 toast
- App.vue 未登录不请求微信分享配置
- ResetPwd 手机号校验改用 cellphone 字段并统一 showFailToast
- Team 页 active=1 走 referrer 单条数据分支
- 忽略 AGENTS.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-21 16:18:49 +08:00

308 lines
9.5 KiB
JavaScript
Raw 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.

/**
* 通用网络请求封装 - 使用 fetch
*/
const BASE_URL = import.meta.env.VITE_API_URL || 'http://127.0.0.1:3001';
// 默认超时时间 15 秒
const DEFAULT_TIMEOUT = 30000;
const pendingControllers = new Set();
const persistentControllers = new Set(); // 不会被 abortAll 取消的控制器
let loadingCount = 0;
// 记录是否已经在当前页面显示过骨架屏
let hasShownSkeletonInPage = false;
// 记录上次显示骨架屏时的页面路径
let lastSkeletonPath = '';
/**
* 显示骨架屏 - 只在页面切换且首次请求时显示
*/
function showSkeleton() {
loadingCount++;
// 检查是否正在页面切换或首次进入页面
const isChanging = window.__isPageChanging ? window.__isPageChanging() : false;
const currentPath = window.__currentPath ? window.__currentPath() : '';
// 当页面路径变化时,重置标志
if (lastSkeletonPath !== currentPath) {
hasShownSkeletonInPage = false;
lastSkeletonPath = currentPath;
}
// 只有在页面切换期间或当前页面尚未显示过骨架屏时,才显示骨架屏
if (isChanging || !hasShownSkeletonInPage) {
window.dispatchEvent(new Event('showSkeleton'));
hasShownSkeletonInPage = true;
}
}
function closeSkeleton() {
loadingCount = Math.max(loadingCount - 1, 0);
if (loadingCount === 0) {
window.dispatchEvent(new Event('closeSkeleton'));
}
}
// 错误码定义
const ERR_CODE = {
SUCCESS: 200,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
SERVER_ERROR: 500,
};
function getHeaders(contentType = 'application/json') {
const headers = {
'Content-Type': contentType,
};
const token = localStorage.getItem('member_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
function handleResponse(response) {
return response.text().then(text => {
let data;
try {
data = JSON.parse(text);
} catch (e) {
// JSON 解析失败,返回原始文本
return Promise.reject({
status: response.status,
message: '数据解析失败',
data: null
});
}
// 检查 HTTP 状态码
if (!response.ok) {
return Promise.reject({
status: response.status,
message: data.message || '请求失败',
data: null
});
}
// 检查业务状态码
if (data.status !== ERR_CODE.SUCCESS) {
return Promise.reject(data);
}
return data;
});
}
/**
* 处理 token 失效
*/
function handleUnauthorized(requestToken) {
// 只有本次请求发出时携带的 token 失效,才清除登录状态
if (!requestToken) {
return;
}
// 如果本地 token 已经被新的登录流程更新,不处理旧请求的 401/403
if (localStorage.getItem('member_token') !== requestToken) {
return;
}
localStorage.removeItem('member_token');
localStorage.removeItem('member_username');
if (window.location.hash !== '#/Login') {
window.location.href = '#/Login';
}
}
/**
* 统一错误处理
*/
function handleError(err, requestToken) {
// token 失效
if (err.status === ERR_CODE.UNAUTHORIZED) {
handleUnauthorized(requestToken);
}
return Promise.reject(err);
}
/**
* 带超时的 fetch
* @param {string} url - 请求地址
* @param {object} options - 请求选项
* @param {number} timeout - 超时时间
* @param {boolean} persistent - 是否为持久请求(不被 abortAll 取消)
*/
function fetchWithTimeout(url, options, timeout = DEFAULT_TIMEOUT, persistent = false) {
const controller = new AbortController();
const externalSignal = options.signal;
if (externalSignal) {
if (externalSignal.aborted) {
controller.abort();
} else {
externalSignal.addEventListener('abort', () => controller.abort(), { once: true });
}
}
// 根据 persistent 标志决定放入哪个集合
if (persistent) {
persistentControllers.add(controller);
} else {
pendingControllers.add(controller);
}
// 持久请求不显示骨架屏
if (!persistent) {
showSkeleton();
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
controller.abort();
reject({ status: 0, message: '网络超时,请稍后重试', data: null });
}, timeout);
fetch(url, { ...options, signal: controller.signal })
.then(res => {
clearTimeout(timer);
resolve(res);
})
.catch(err => {
clearTimeout(timer);
if (err.name === 'AbortError') {
// ponytail: 请求取消后保持 pending让 catch 不触发(避免路由切换时集体弹 toast
return;
}
reject({ status: 0, message: '网络连接失败', data: null });
})
.finally(() => {
// 从对应的集合中删除控制器
pendingControllers.delete(controller);
persistentControllers.delete(controller);
// 只有非持久请求才关闭骨架屏
if (!persistent) {
closeSkeleton();
}
});
});
}
/**
* 通用请求
* @param {string} url - 请求路径
* @param {object} data - 请求数据
* @param {string} method - 请求方法,默认 POST
* @param {number} timeout - 超时时间 ms
* @param {AbortSignal} signal - 外部信号
* @param {boolean} persistent - 是否为持久请求
* @returns {Promise}
*/
export function request(url, data, method = 'POST', timeout = DEFAULT_TIMEOUT, signal, persistent = false) {
const requestToken = localStorage.getItem('member_token');
return fetchWithTimeout(BASE_URL + url, {
method,
headers: getHeaders(),
body: JSON.stringify(data),
signal,
}, timeout, persistent).then(handleResponse).catch(err => handleError(err, requestToken));
}
export const get = (url, params, timeout = DEFAULT_TIMEOUT, signal, persistent = false) => {
const requestToken = localStorage.getItem('member_token');
let query = '';
if (params) {
query = '?' + new URLSearchParams(params).toString();
}
return fetchWithTimeout(BASE_URL + url + query, {
method: 'GET',
headers: getHeaders(),
signal,
}, timeout, persistent).then(handleResponse).catch(err => handleError(err, requestToken));
};
export const post = (url, data, timeout = DEFAULT_TIMEOUT, signal, persistent = false) => request(url, data, 'POST', timeout, signal, persistent);
export const put = (url, data, timeout = DEFAULT_TIMEOUT, signal, persistent = false) => request(url, data, 'PUT', timeout, signal, persistent);
export const del = (url, data, timeout = DEFAULT_TIMEOUT, signal, persistent = false) => request(url, data, 'DELETE', timeout, signal, persistent);
/**
* POST FormData 请求
* @param {string} url - 请求路径
* @param {FormData} formData - FormData 对象
* @param {number} timeout - 超时时间 ms
* @param {AbortSignal} signal - 外部信号
* @param {boolean} persistent - 是否为持久请求
* @returns {Promise}
*/
export function postForm(url, formData, timeout = DEFAULT_TIMEOUT, signal, persistent = false) {
const headers = {};
const token = localStorage.getItem('member_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return fetchWithTimeout(BASE_URL + url, {
method: 'POST',
headers,
body: formData,
signal,
}, timeout, persistent).then(handleResponse).catch(err => handleError(err, token));
}
/**
* 上传文件
* @param {string} url - 请求路径
* @param {File[]} files - 文件数组
* @param {number} timeout - 超时时间 ms默认 30 秒上传较长)
* @param {AbortSignal} signal - 外部信号
* @param {boolean} persistent - 是否为持久请求
* @returns {Promise}
*/
export function upload(url, files, timeout = 30000, signal, persistent = false) {
const formData = new FormData();
files.forEach(file => {
formData.append('file', file);
});
const headers = {};
const token = localStorage.getItem('member_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return fetchWithTimeout(BASE_URL + url, {
method: 'POST',
headers,
body: formData,
signal,
}, timeout, persistent)
.then(res => res.text().then(text => {
try {
const data = JSON.parse(text);
if (data.status !== ERR_CODE.SUCCESS) {
return Promise.reject(data);
}
return data;
} catch (e) {
return Promise.reject({ status: 0, message: '数据解析失败', data: null });
}
}))
.catch(handleError);
}
/**
* 取消所有请求并关闭骨架屏
* 注意:持久请求不会被取消
*/
export function abortAll() {
pendingControllers.forEach(controller => controller.abort());
pendingControllers.clear();
loadingCount = 0;
// 重置页面骨架屏标志,为下一次页面切换做准备
hasShownSkeletonInPage = false;
window.dispatchEvent(new Event('closeSkeleton'));
}
export default { get, post, put, del, upload, postForm, request, abortAll };