feat: Architecture initialization
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 通用工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 生成唯一 ID
|
||||
*/
|
||||
export function uniqueId(prefix = ''): string {
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
||||
return prefix ? `${prefix}_${id}` : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简易深拷贝(基于 JSON,适用于纯数据对象)
|
||||
*/
|
||||
export function deepClone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => any>(fn: T, wait = 300) {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
const debounced = (...args: Parameters<T>) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), wait);
|
||||
};
|
||||
debounced.cancel = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = null;
|
||||
};
|
||||
return debounced;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流
|
||||
*/
|
||||
export function throttle<T extends (...args: any[]) => any>(fn: T, wait = 300) {
|
||||
let lastTime = 0;
|
||||
return (...args: Parameters<T>) => {
|
||||
const now = Date.now();
|
||||
if (now - lastTime >= wait) {
|
||||
lastTime = now;
|
||||
fn(...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
export function formatDate(date: Date | string | number, format = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||
const d = new Date(date);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const map: Record<string, string> = {
|
||||
YYYY: String(d.getFullYear()),
|
||||
MM: pad(d.getMonth() + 1),
|
||||
DD: pad(d.getDate()),
|
||||
HH: pad(d.getHours()),
|
||||
mm: pad(d.getMinutes()),
|
||||
ss: pad(d.getSeconds()),
|
||||
};
|
||||
|
||||
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => map[match]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 树形数据扁平化
|
||||
*/
|
||||
export interface TreeNode {
|
||||
id: string | number;
|
||||
children?: TreeNode[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function flattenTree<T extends TreeNode>(tree: T[]): T[] {
|
||||
const result: T[] = [];
|
||||
const stack = [...tree].reverse();
|
||||
while (stack.length) {
|
||||
const node = stack.pop() as T;
|
||||
result.push(node);
|
||||
if (node.children?.length) {
|
||||
stack.push(...(node.children as T[]).reverse());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { auth } from '@/hooks/';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const baseUrl = import.meta.env?.VITE_API_BASE_URL || '';
|
||||
const DEFAULT_TIMEOUT = 10000;
|
||||
|
||||
const activeControllers = new Set<AbortController>();
|
||||
|
||||
/** 统一处理 401 未授权逻辑 */
|
||||
const handleUnauthorized = (msg?: string) => {
|
||||
auth.logout();
|
||||
message.error(msg || '登录状态已过期,请重新登录');
|
||||
window.location.href = '/login';
|
||||
};
|
||||
|
||||
const request = (
|
||||
url: string,
|
||||
options: RequestInit & {
|
||||
query?: Record<string, any>;
|
||||
responseType?: 'json' | 'blob';
|
||||
} = {},
|
||||
) => {
|
||||
let finalUrl = baseUrl + url;
|
||||
|
||||
if (options.query) {
|
||||
finalUrl += '?' + new URLSearchParams(options.query).toString();
|
||||
}
|
||||
|
||||
const token = auth.token.value;
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (!(options.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 优先使用外部传入的 signal(来自 useRequest 取消机制);
|
||||
// 无外部 signal 时自建 controller 用于超时取消
|
||||
const externalSignal = (options as RequestInit & { signal?: AbortSignal }).signal;
|
||||
const controller = new AbortController();
|
||||
|
||||
activeControllers.add(controller);
|
||||
|
||||
// 合并外部 signal 与内部 controller:任一 abort 都会取消请求
|
||||
const onExternalAbort = () => controller.abort();
|
||||
if (externalSignal) {
|
||||
if (externalSignal.aborted) {
|
||||
controller.abort();
|
||||
} else {
|
||||
externalSignal.addEventListener('abort', onExternalAbort, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
const init: RequestInit = {
|
||||
...options,
|
||||
headers: { ...headers, ...(options.headers as Record<string, string>) },
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort(); // 超时触发 abort
|
||||
}, DEFAULT_TIMEOUT);
|
||||
|
||||
const cleanupSignal = () => {
|
||||
clearTimeout(timeoutId);
|
||||
if (externalSignal) externalSignal.removeEventListener('abort', onExternalAbort);
|
||||
};
|
||||
|
||||
return fetch(finalUrl, init)
|
||||
.then(async (res) => {
|
||||
cleanupSignal();
|
||||
activeControllers.delete(controller);
|
||||
|
||||
if (res.status === 401) {
|
||||
handleUnauthorized();
|
||||
return Promise.reject(new Error('Unauthorized'));
|
||||
}
|
||||
|
||||
if (res.status === 500) {
|
||||
message.error('服务器开小差了,请稍后再试');
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
return Promise.reject(new Error(errorData.message || '服务器内部错误'));
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
message.error(errorData.message || '请求失败');
|
||||
return Promise.reject(new Error(errorData.message || '请求失败'));
|
||||
}
|
||||
|
||||
if (options.responseType === 'blob') {
|
||||
return res.blob();
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.code === 401) {
|
||||
handleUnauthorized(data.msg);
|
||||
return Promise.reject(new Error(data.msg || 'Unauthorized'));
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch((err) => {
|
||||
cleanupSignal();
|
||||
|
||||
activeControllers.delete(controller);
|
||||
|
||||
if (err.name === 'AbortError' || err.message === 'The user aborted a request.') {
|
||||
// 使用特殊标记对象区分“请求被取消”和“请求成功但返回 null”
|
||||
return Promise.resolve({ __aborted: true });
|
||||
}
|
||||
|
||||
if (err.message === 'Failed to fetch') {
|
||||
message.error('网络连接失败,请检查网络');
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
export const cancelAllRequests = () => {
|
||||
activeControllers.forEach((controller) => {
|
||||
controller.abort();
|
||||
});
|
||||
activeControllers.clear();
|
||||
};
|
||||
|
||||
export { request };
|
||||
|
||||
export const get = (url: string, query?: Record<string, any>, options?: { signal?: AbortSignal }) =>
|
||||
request(url, { method: 'GET', query, signal: options?.signal });
|
||||
|
||||
export const post = (
|
||||
url: string,
|
||||
body?: any,
|
||||
query?: Record<string, any>,
|
||||
options?: { signal?: AbortSignal },
|
||||
) =>
|
||||
request(url, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
query,
|
||||
signal: options?.signal,
|
||||
});
|
||||
|
||||
export const put = (url: string, body?: any, query?: Record<string, any>) =>
|
||||
request(url, {
|
||||
method: 'PUT',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
query,
|
||||
});
|
||||
|
||||
export const del = (url: string, query?: Record<string, any>) =>
|
||||
request(url, { method: 'DELETE', query });
|
||||
|
||||
export default {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
delete: del,
|
||||
};
|
||||
Reference in New Issue
Block a user