chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
// Layui 轻量级请求模块(基于 Fetch API + layui layer)
|
||||
// 支持文件上传
|
||||
//
|
||||
// 使用示例:
|
||||
// layui.use(['http'], function(){ var req = layui.request; req.post('/api/upload', formData).then(...).catch(...) });
|
||||
|
||||
layui.define(["layer", "setter"], function (exports) {
|
||||
"use strict";
|
||||
|
||||
var setter = layui.setter,
|
||||
layer = layui.layer;
|
||||
|
||||
let isRefreshing = false;
|
||||
let refreshQueue = [];
|
||||
let loadIndex;
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig = {
|
||||
baseURL: '',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
withCredentials: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新 Tokens
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function refreshToken() {
|
||||
const { refresh_token } = setter.getTokens();
|
||||
if (!refresh_token) {
|
||||
throw new Error("No refresh token");
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), defaultConfig.timeout);
|
||||
|
||||
try {
|
||||
var Accept = defaultConfig.headers['Accept'] || 'application/json';
|
||||
const response = await fetch(defaultConfig.baseURL + "/api/refresh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': Accept,
|
||||
...defaultConfig.headers,
|
||||
},
|
||||
credentials: defaultConfig.withCredentials ? 'include' : 'same-origin',
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({ refresh_token }),
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const res = await response.json();
|
||||
|
||||
if (res.code === 200) {
|
||||
setter.saveTokens({
|
||||
access_token: res.access || res.access_token,
|
||||
refresh_token: res.refresh || res.refresh_token || refresh_token,
|
||||
});
|
||||
return res.access || res.access_token;
|
||||
} else {
|
||||
throw new Error(res.msg || "Refresh failed");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timeout');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试请求
|
||||
* @param {Object} options - fetch 配置选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function retryRequest(options) {
|
||||
const { access_token } = setter.getTokens();
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), options.timeout || defaultConfig.timeout);
|
||||
|
||||
try {
|
||||
// 构建请求头
|
||||
const headers = { ...defaultConfig.headers, ...options.headers };
|
||||
const { access_token: newToken } = setter.getTokens();
|
||||
if (newToken) {
|
||||
headers['Authorization'] = `Bearer ${newToken}`;
|
||||
}
|
||||
|
||||
// 如果是 FormData,删除 Content-Type 让浏览器自动设置
|
||||
if (options.data instanceof FormData) {
|
||||
delete headers['Content-Type'];
|
||||
}
|
||||
|
||||
const fetchOptions = {
|
||||
method: options.method || 'GET',
|
||||
headers,
|
||||
credentials: defaultConfig.withCredentials ? 'include' : 'same-origin',
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// 添加 body(如果不是 GET/HEAD 请求)
|
||||
if (options.method !== 'GET' && options.method !== 'HEAD') {
|
||||
fetchOptions.body = options.data instanceof FormData
|
||||
? options.data
|
||||
: JSON.stringify(options.data);
|
||||
}
|
||||
|
||||
const response = await fetch(options.url, fetchOptions);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 处理响应头中的 token
|
||||
const authHeader = response.headers.get('Authorization');
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const newToken = authHeader.substring(7);
|
||||
setter.saveTokens({ access_token: newToken });
|
||||
}
|
||||
|
||||
let data;
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else if (contentType && contentType.includes('text/')) {
|
||||
data = await response.text();
|
||||
} else {
|
||||
data = await response.blob();
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(response.statusText || 'Request failed');
|
||||
error.status = response.status;
|
||||
error.data = data;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
error.message = 'Request timeout';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch 请求核心方法
|
||||
* @param {Object} options - fetch 配置选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function _Request(options) {
|
||||
const { access_token } = setter.getTokens();
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), options.timeout || defaultConfig.timeout);
|
||||
|
||||
try {
|
||||
// 构建请求头
|
||||
const headers = { ...defaultConfig.headers, ...options.headers };
|
||||
if (access_token) {
|
||||
headers['Authorization'] = `Bearer ${access_token}`;
|
||||
}
|
||||
|
||||
// 如果是 FormData,删除 Content-Type 让浏览器自动设置
|
||||
if (options.data instanceof FormData) {
|
||||
delete headers['Content-Type'];
|
||||
}
|
||||
|
||||
const fetchOptions = {
|
||||
method: options.method || 'GET',
|
||||
headers,
|
||||
credentials: defaultConfig.withCredentials ? 'include' : 'same-origin',
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// 添加 body(如果不是 GET/HEAD 请求)
|
||||
if (options.method !== 'GET' && options.method !== 'HEAD') {
|
||||
fetchOptions.body = options.data instanceof FormData
|
||||
? options.data
|
||||
: JSON.stringify(options.data);
|
||||
}
|
||||
|
||||
const response = await fetch(options.url, fetchOptions);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 处理响应头中的 token
|
||||
const authHeader = response.headers.get('Authorization');
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const newToken = authHeader.substring(7);
|
||||
setter.saveTokens({ access_token: newToken });
|
||||
}
|
||||
|
||||
let data;
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else if (contentType && contentType.includes('text/')) {
|
||||
data = await response.text();
|
||||
} else {
|
||||
data = await response.blob();
|
||||
}
|
||||
|
||||
// 检查响应数据中的 token
|
||||
if (data && (data.access_token || data.access)) {
|
||||
setter.saveTokens({
|
||||
access_token: data.access_token || data.access,
|
||||
refresh_token: data.refresh_token || data.refresh,
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 && !options._retry) {
|
||||
// 401 错误,尝试刷新 token
|
||||
return new Promise((resolve, reject) => {
|
||||
refreshQueue.push({ resolve, reject, options });
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
|
||||
refreshToken()
|
||||
.then(() => {
|
||||
const queue = [...refreshQueue];
|
||||
refreshQueue = [];
|
||||
|
||||
queue.forEach(({ resolve, reject, options }) => {
|
||||
options._retry = true;
|
||||
retryRequest(options)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
isRefreshing = false;
|
||||
})
|
||||
.catch((refreshError) => {
|
||||
refreshQueue.forEach(({ reject }) => reject(refreshError));
|
||||
refreshQueue = [];
|
||||
isRefreshing = false;
|
||||
|
||||
// 清除本地存储并跳转登录
|
||||
localStorage.clear();
|
||||
layer.msg("登录已过期,请重新登录", { icon: 2, time: 2000 });
|
||||
setTimeout(() => {
|
||||
window.top.location.href = "./login.html";
|
||||
// if (window === window.parent) {
|
||||
// window.location.href = "./login.html";
|
||||
// } else {
|
||||
// window.parent.location.href = "./login.html";
|
||||
// }
|
||||
}, 2000);
|
||||
|
||||
reject(refreshError);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const error = new Error(response.statusText || 'Request failed');
|
||||
error.status = response.status;
|
||||
error.data = data;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
error.message = 'Request timeout';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
async function setAccept(accept = "json") {
|
||||
switch (accept) {
|
||||
case 'xml':
|
||||
defaultConfig.headers['Accept'] = `application/xml`;
|
||||
break;
|
||||
case 'html':
|
||||
defaultConfig.headers['Accept'] = `text/html`;
|
||||
break;
|
||||
default:
|
||||
defaultConfig.headers['Accept'] = `application/json`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 暴露接口
|
||||
var http = {
|
||||
/**
|
||||
* 配置请求默认参数
|
||||
* @param {Object} cfg - 配置对象
|
||||
* @returns {Object} 当前配置
|
||||
*/
|
||||
config: function (cfg) {
|
||||
Object.assign(defaultConfig, cfg || {});
|
||||
return defaultConfig;
|
||||
},
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
* @param {Object} options - fetch 配置选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
request: _Request,
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
* @param {string} url - 请求地址
|
||||
* @param {Object} params - URL 参数
|
||||
* @param {Object} options - fetch 配置选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get: function (url, params, options = {}) {
|
||||
// 处理 URL 参数
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
url += (url.includes('?') ? '&' : '?') + queryString;
|
||||
}
|
||||
|
||||
options.method = "GET";
|
||||
options.url = defaultConfig.baseURL + url;
|
||||
return _Request(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
* @param {string} url - 请求地址
|
||||
* @param {Object|FormData} data - 请求数据或 FormData 对象
|
||||
* @param {Object} options - fetch 配置选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
post: function (url, data, options = {}) {
|
||||
options.method = "POST";
|
||||
options.url = defaultConfig.baseURL + url;
|
||||
options.data = data;
|
||||
return _Request(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
* @param {string} url - 请求地址
|
||||
* @param {Object|FormData} data - 请求数据或 FormData 对象
|
||||
* @param {Object} options - fetch 配置选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
put: function (url, data, options = {}) {
|
||||
options.method = "PUT";
|
||||
options.url = defaultConfig.baseURL + url;
|
||||
options.data = data;
|
||||
return _Request(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
* @param {string} url - 请求地址
|
||||
* @param {Object} params - URL 参数
|
||||
* @param {Object} options - fetch 配置选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
delete: function (url, params, options = {}) {
|
||||
// 处理 URL 参数
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
url += (url.includes('?') ? '&' : '?') + queryString;
|
||||
}
|
||||
|
||||
options.method = "DELETE";
|
||||
options.url = defaultConfig.baseURL + url;
|
||||
return _Request(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* 上传文件(简化版)
|
||||
* @param {string} url - 上传地址
|
||||
* @param {File|File[]} files - 文件对象或文件数组
|
||||
* @param {Object} params - 其他参数
|
||||
* @param {Object} options - fetch 配置选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
upload: function (url, files, params = {}, options = {}) {
|
||||
const formData = new FormData();
|
||||
|
||||
// 添加文件
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach((file, index) => {
|
||||
formData.append(`file${index}`, file);
|
||||
});
|
||||
} else if (files) {
|
||||
formData.append('file', files);
|
||||
}
|
||||
|
||||
// 添加其他参数
|
||||
Object.keys(params).forEach(key => {
|
||||
formData.append(key, params[key]);
|
||||
});
|
||||
|
||||
options.method = "POST";
|
||||
options.url = defaultConfig.baseURL + url;
|
||||
options.data = formData;
|
||||
return _Request(options);
|
||||
},
|
||||
/**
|
||||
* 刷新 token
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
refreshToken: refreshToken,
|
||||
|
||||
accept: setAccept,
|
||||
|
||||
};
|
||||
|
||||
exports("http", http);
|
||||
});
|
||||
Reference in New Issue
Block a user