初始化项目

This commit is contained in:
2026-02-08 22:38:13 +08:00
commit 334d2c6312
201 changed files with 32724 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
import axios from "axios";
import config from "@/config";
import { useUserStore } from "@/stores/modules/user";
import { message } from "ant-design-vue";
import router from "@/router";
const request = axios.create({
timeout: 30000,
baseURL: config.API_URL,
});
// 是否正在刷新 token
let isRefreshing = false;
// 存储待重试的请求
let requests = [];
// 请求拦截器
request.interceptors.request.use(
(config) => {
const userStore = useUserStore();
const token = userStore.token;
// 如果有 token,添加到请求头
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
// 根据后端返回的数据结构进行处理
// 后端返回格式为 { code, message, data }
const { code, data, message: msg } = response.data;
// 请求成功
if (code === 200 || code === 1) {
return { code, data, message: msg };
}
// 其他错误码处理
message.error(msg || "请求失败");
return Promise.reject(new Error(msg || "请求失败"));
},
async (error) => {
const userStore = useUserStore();
const { response } = error;
// 无响应(网络错误、超时等)
if (!response) {
message.error("网络错误,请检查网络连接");
return Promise.reject(error);
}
const { status, data } = response;
// 401 未授权 - token 过期或无效
if (status === 401) {
// 如果正在刷新 token,将请求加入队列
if (isRefreshing) {
return new Promise((resolve) => {
requests.push((token) => {
// 重新设置请求头
error.config.headers["Authorization"] =
`Bearer ${token}`;
resolve(http(error.config));
});
});
}
// 标记正在刷新
isRefreshing = true;
try {
// 尝试刷新 token
const newToken = await refreshToken();
// 刷新成功,更新 token
userStore.setToken(newToken);
// 执行队列中的所有请求
requests.forEach((callback) => callback(newToken));
requests = [];
// 重新执行当前请求
error.config.headers["Authorization"] = `Bearer ${newToken}`;
return request(error.config);
} catch (refreshError) {
// 刷新失败,清空队列并跳转登录页
requests = [];
userStore.logout();
router.push("/login");
message.error("登录已过期,请重新登录");
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
// 403 禁止访问
if (status === 403) {
message.error("没有权限访问该资源");
return Promise.reject(error);
}
// 404 资源不存在
if (status === 404) {
message.error("请求的资源不存在");
return Promise.reject(error);
}
// 500 服务器错误
if (status >= 500) {
message.error("服务器错误,请稍后重试");
return Promise.reject(error);
}
// 其他错误
const errorMessage = data?.message || error.message || "请求失败";
message.error(errorMessage);
return Promise.reject(error);
},
);
// 刷新 token 的方法
async function refreshToken() {
// 刷新接口需要携带当前token在请求头中
const response = await request.post('auth/refresh');
// 返回格式为 { code, data: { token } }
return response.data.token;
}
export default request;
+499
View File
@@ -0,0 +1,499 @@
/*
* @Descripttion: 工具集
* @version: 2.0
* @LastEditors: sakuya
* @LastEditTime: 2026年1月15日
*/
import CryptoJS from "crypto-js";
import sysConfig from "@/config";
const tool = {};
/**
* 检查是否为有效的值(非null、非undefined、非空字符串、非空数组、非空对象)
* @param {*} value - 要检查的值
* @returns {boolean}
*/
tool.isValid = function (value) {
if (value === null || value === undefined) {
return false;
}
if (typeof value === "string" && value.trim() === "") {
return false;
}
if (Array.isArray(value) && value.length === 0) {
return false;
}
if (typeof value === "object" && Object.keys(value).length === 0) {
return false;
}
return true;
};
/**
* 防抖函数
* @param {Function} func - 要执行的函数
* @param {number} wait - 等待时间(毫秒)
* @param {boolean} immediate - 是否立即执行
* @returns {Function}
*/
tool.debounce = function (func, wait = 300, immediate = false) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
if (immediate && !timeout) {
func.apply(context, args);
}
timeout = setTimeout(() => {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}, wait);
};
};
/**
* 节流函数
* @param {Function} func - 要执行的函数
* @param {number} wait - 等待时间(毫秒)
* @param {Object} options - 配置选项 { leading: boolean, trailing: boolean }
* @returns {Function}
*/
tool.throttle = function (func, wait = 300, options = {}) {
let timeout;
let previous = 0;
const { leading = true, trailing = true } = options;
return function (...args) {
const context = this;
const now = Date.now();
if (!previous && !leading) {
previous = now;
}
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout && trailing) {
timeout = setTimeout(() => {
previous = leading ? Date.now() : 0;
timeout = null;
func.apply(context, args);
}, remaining);
}
};
};
/**
* 深拷贝对象(支持循环引用)
* @param {*} obj - 要拷贝的对象
* @param {WeakMap} hash - 用于检测循环引用
* @returns {*}
*/
tool.deepClone = function (obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (hash.has(obj)) {
return hash.get(obj);
}
const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
clone[key] = tool.deepClone(obj[key], hash);
}
}
return clone;
};
/* localStorage */
tool.data = {
set(key, data, datetime = 0) {
//加密
if (sysConfig.LS_ENCRYPTION == "AES") {
data = tool.crypto.AES.encrypt(
JSON.stringify(data),
sysConfig.LS_ENCRYPTION_key,
);
}
let cacheValue = {
content: data,
datetime:
parseInt(datetime) === 0
? 0
: new Date().getTime() + parseInt(datetime) * 1000,
};
return localStorage.setItem(key, JSON.stringify(cacheValue));
},
get(key) {
try {
const value = JSON.parse(localStorage.getItem(key));
if (value) {
let nowTime = new Date().getTime();
if (nowTime > value.datetime && value.datetime != 0) {
localStorage.removeItem(key);
return null;
}
//解密
if (sysConfig.LS_ENCRYPTION == "AES") {
value.content = JSON.parse(
tool.crypto.AES.decrypt(
value.content,
sysConfig.LS_ENCRYPTION_key,
),
);
}
return value.content;
}
return null;
} catch {
return null;
}
},
remove(key) {
return localStorage.removeItem(key);
},
clear() {
return localStorage.clear();
},
};
/*sessionStorage*/
tool.session = {
set(table, settings) {
const _set = JSON.stringify(settings);
return sessionStorage.setItem(table, _set);
},
get(table) {
const data = sessionStorage.getItem(table);
try {
return JSON.parse(data);
} catch {
return null;
}
},
remove(table) {
return sessionStorage.removeItem(table);
},
clear() {
return sessionStorage.clear();
},
};
/*cookie*/
tool.cookie = {
/**
* 设置cookie
* @param {string} name - cookie名称
* @param {string} value - cookie值
* @param {Object} config - 配置选项
*/
set(name, value, config = {}) {
const cfg = {
expires: null,
path: null,
domain: null,
secure: false,
httpOnly: false,
sameSite: "Lax",
...config,
};
let cookieStr = `${name}=${encodeURIComponent(value)}`;
if (cfg.expires) {
const exp = new Date();
exp.setTime(exp.getTime() + parseInt(cfg.expires) * 1000);
cookieStr += `;expires=${exp.toUTCString()}`;
}
if (cfg.path) {
cookieStr += `;path=${cfg.path}`;
}
if (cfg.domain) {
cookieStr += `;domain=${cfg.domain}`;
}
if (cfg.secure) {
cookieStr += `;secure`;
}
if (cfg.sameSite) {
cookieStr += `;SameSite=${cfg.sameSite}`;
}
document.cookie = cookieStr;
},
/**
* 获取cookie
* @param {string} name - cookie名称
* @returns {string|null}
*/
get(name) {
const arr = document.cookie.match(
new RegExp("(^| )" + name + "=([^;]*)(;|$)"),
);
if (arr != null) {
return decodeURIComponent(arr[2]);
}
return null;
},
/**
* 删除cookie
* @param {string} name - cookie名称
*/
remove(name) {
const exp = new Date();
exp.setTime(exp.getTime() - 1);
document.cookie = `${name}=;expires=${exp.toUTCString()}`;
},
};
/* Fullscreen */
/**
* 切换全屏状态
* @param {HTMLElement} element - 要全屏的元素
*/
tool.screen = function (element) {
const isFull = !!(
document.webkitIsFullScreen ||
document.mozFullScreen ||
document.msFullscreenElement ||
document.fullscreenElement
);
if (isFull) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
} else {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
}
}
};
/* 复制对象(浅拷贝) */
/**
* 浅拷贝对象
* @param {*} obj - 要拷贝的对象
* @returns {*} - 拷贝后的对象
*/
tool.objCopy = function (obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
return JSON.parse(JSON.stringify(obj));
};
/* 日期格式化 */
/**
* 格式化日期
* @param {Date|string|number} date - 日期对象、时间戳或日期字符串
* @param {string} fmt - 格式化字符串,默认 "yyyy-MM-dd hh:mm:ss"
* @returns {string} - 格式化后的日期字符串
*/
tool.dateFormat = function (date, fmt = "yyyy-MM-dd hh:mm:ss") {
if (!date) return "";
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) return "";
const o = {
"M+": dateObj.getMonth() + 1, // 月份
"d+": dateObj.getDate(), // 日
"h+": dateObj.getHours(), // 小时
"m+": dateObj.getMinutes(), // 分
"s+": dateObj.getSeconds(), // 秒
"q+": Math.floor((dateObj.getMonth() + 3) / 3), // 季度
S: dateObj.getMilliseconds(), // 毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
(dateObj.getFullYear() + "").substr(4 - RegExp.$1.length),
);
}
for (const k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length == 1
? o[k]
: ("00" + o[k]).substr(("" + o[k]).length),
);
}
}
return fmt;
};
/* 千分符 */
/**
* 格式化数字,添加千分位分隔符
* @param {number|string} num - 要格式化的数字
* @param {number} decimals - 保留小数位数,默认为0
* @returns {string} - 格式化后的字符串
*/
tool.groupSeparator = function (num, decimals = 0) {
if (num === null || num === undefined || num === "") return "";
const numStr = Number(num).toFixed(decimals);
const parts = numStr.split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
};
/* 常用加解密 */
tool.crypto = {
//MD5加密
MD5(data) {
return CryptoJS.MD5(data).toString();
},
//BASE64加解密
BASE64: {
encrypt(data) {
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data));
},
decrypt(cipher) {
return CryptoJS.enc.Base64.parse(cipher).toString(
CryptoJS.enc.Utf8,
);
},
},
//AES加解密
AES: {
encrypt(data, secretKey, config = {}) {
if (secretKey.length % 8 != 0) {
console.warn(
"[SCUI error]: 秘钥长度需为8的倍数,否则解密将会失败。",
);
}
const result = CryptoJS.AES.encrypt(
data,
CryptoJS.enc.Utf8.parse(secretKey),
{
iv: CryptoJS.enc.Utf8.parse(config.iv || ""),
mode: CryptoJS.mode[config.mode || "ECB"],
padding: CryptoJS.pad[config.padding || "Pkcs7"],
},
);
return result.toString();
},
decrypt(cipher, secretKey, config = {}) {
const result = CryptoJS.AES.decrypt(
cipher,
CryptoJS.enc.Utf8.parse(secretKey),
{
iv: CryptoJS.enc.Utf8.parse(config.iv || ""),
mode: CryptoJS.mode[config.mode || "ECB"],
padding: CryptoJS.pad[config.padding || "Pkcs7"],
},
);
return CryptoJS.enc.Utf8.stringify(result);
},
},
};
/* 树形数据转扁平数组 */
/**
* 将树形结构转换为扁平数组
* @param {Array} tree - 树形数组
* @param {Object} config - 配置项 { children: "children" }
* @returns {Array} - 扁平化后的数组
*/
tool.treeToList = function (tree, config = { children: "children" }) {
const result = [];
tree.forEach((item) => {
const tmp = { ...item };
const childrenKey = config.children || "children";
if (tmp[childrenKey] && tmp[childrenKey].length > 0) {
result.push({ ...item });
const childrenRoutes = tool.treeToList(tmp[childrenKey], config);
result.push(...childrenRoutes);
} else {
result.push(tmp);
}
});
return result;
};
/* 获取父节点数据(保留原有函数名) */
/**
* 根据ID获取父节点数据
* @param {Array} list - 数据列表
* @param {number|string} targetId - 目标ID
* @param {Object} config - 配置项 { pid: "parent_id", idField: "id", field: [] }
* @returns {*} - 父节点数据或指定字段
*/
tool.get_parents = function (
list,
targetId = 0,
config = { pid: "parent_id", idField: "id", field: [] },
) {
let res = null;
list.forEach((item) => {
if (item[config.idField || "id"] === targetId) {
if (config.field && config.field.length > 1) {
res = {};
config.field.forEach((field) => {
res[field] = item[field];
});
} else if (config.field && config.field.length === 1) {
res = item[config.field[0]];
} else {
res = item;
}
}
});
return res;
};
/* 获取数据字段 */
/**
* 从数据对象中提取指定字段
* @param {Object} data - 数据对象
* @param {Array} fields - 字段名数组
* @returns {*} - 提取的字段数据
*/
tool.getDataField = function (data, fields = []) {
if (!data || typeof data !== "object") {
return data;
}
if (fields.length === 0) {
return data;
}
if (fields.length === 1) {
return data[fields[0]];
} else {
const result = {};
fields.forEach((field) => {
result[field] = data[field];
});
return result;
}
};
// 兼容旧函数名
tool.tree_to_list = tool.treeToList;
tool.get_data_field = tool.getDataField;
export default tool;
+257
View File
@@ -0,0 +1,257 @@
/**
* WebSocket Client Helper
*
* Provides a simple interface for WebSocket connections
*/
class WebSocketClient {
constructor(url, options = {}) {
this.url = url
this.ws = null
this.reconnectAttempts = 0
this.maxReconnectAttempts = options.maxReconnectAttempts || 5
this.reconnectInterval = options.reconnectInterval || 3000
this.reconnectDelay = options.reconnectDelay || 1000
this.heartbeatInterval = options.heartbeatInterval || 30000
this.heartbeatTimer = null
this.isManualClose = false
this.isConnecting = false
// Event handlers
this.onOpen = options.onOpen || null
this.onMessage = options.onMessage || null
this.onError = options.onError || null
this.onClose = options.onClose || null
// Message handlers
this.messageHandlers = new Map()
}
/**
* Connect to WebSocket server
*/
connect() {
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
return
}
this.isConnecting = true
this.isManualClose = false
try {
this.ws = new WebSocket(this.url)
this.ws.onopen = (event) => {
console.log('WebSocket connected', event)
this.isConnecting = false
this.reconnectAttempts = 0
// Start heartbeat
this.startHeartbeat()
// Call onOpen handler
if (this.onOpen) {
this.onOpen(event)
}
}
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data)
console.log('WebSocket message received', message)
// Handle different message types
this.handleMessage(message)
// Call onMessage handler
if (this.onMessage) {
this.onMessage(message, event)
}
} catch (error) {
console.error('Failed to parse WebSocket message', error)
}
}
this.ws.onerror = (error) => {
console.error('WebSocket error', error)
this.isConnecting = false
// Stop heartbeat
this.stopHeartbeat()
// Call onError handler
if (this.onError) {
this.onError(error)
}
}
this.ws.onclose = (event) => {
console.log('WebSocket closed', event)
this.isConnecting = false
// Stop heartbeat
this.stopHeartbeat()
// Call onClose handler
if (this.onClose) {
this.onClose(event)
}
// Attempt to reconnect if not manually closed
if (!this.isManualClose && this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnect()
}
}
} catch (error) {
console.error('Failed to create WebSocket connection', error)
this.isConnecting = false
// Call onError handler
if (this.onError) {
this.onError(error)
}
}
}
/**
* Reconnect to WebSocket server
*/
reconnect() {
if (this.isConnecting || this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('Max reconnection attempts reached')
return
}
this.reconnectAttempts++
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
console.log(`Reconnecting attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`)
setTimeout(() => {
this.connect()
}, delay)
}
/**
* Disconnect from WebSocket server
*/
disconnect() {
this.isManualClose = true
this.stopHeartbeat()
if (this.ws) {
this.ws.close()
this.ws = null
}
}
/**
* Send message to server
*/
send(type, data = {}) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message = JSON.stringify({
type,
data
})
this.ws.send(message)
console.log('WebSocket message sent', { type, data })
} else {
console.warn('WebSocket is not connected')
}
}
/**
* Handle incoming messages
*/
handleMessage(message) {
const { type, data } = message
// Get handler for this message type
const handler = this.messageHandlers.get(type)
if (handler) {
handler(data)
}
}
/**
* Register message handler
*/
on(messageType, handler) {
this.messageHandlers.set(messageType, handler)
}
/**
* Unregister message handler
*/
off(messageType) {
this.messageHandlers.delete(messageType)
}
/**
* Start heartbeat
*/
startHeartbeat() {
this.stopHeartbeat()
this.heartbeatTimer = setInterval(() => {
this.send('heartbeat', { timestamp: Date.now() })
}, this.heartbeatInterval)
}
/**
* Stop heartbeat
*/
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
/**
* Get connection state
*/
get readyState() {
if (!this.ws) return WebSocket.CLOSED
return this.ws.readyState
}
/**
* Check if connected
*/
get isConnected() {
return this.ws && this.ws.readyState === WebSocket.OPEN
}
}
/**
* Create WebSocket connection
*/
export function createWebSocket(userId, token, options = {}) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const host = window.location.host
const url = `${protocol}//${host}/ws?user_id=${userId}&token=${token}`
return new WebSocketClient(url, options)
}
/**
* WebSocket singleton instance
*/
let wsClient = null
export function getWebSocket(userId, token, options = {}) {
if (!wsClient || !wsClient.isConnected) {
wsClient = createWebSocket(userId, token, options)
}
return wsClient
}
export function closeWebSocket() {
if (wsClient) {
wsClient.disconnect()
wsClient = null
}
}
export default WebSocketClient