first commiit
This commit is contained in:
27
src/utils/auth.ts
Normal file
27
src/utils/auth.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// 访问 token 缓存的 key
|
||||
const ACCESS_TOKEN_KEY = "access_token";
|
||||
// 刷新 token 缓存的 key
|
||||
const REFRESH_TOKEN_KEY = "refresh_token";
|
||||
|
||||
function getToken(): string {
|
||||
return localStorage.getItem(ACCESS_TOKEN_KEY) || "";
|
||||
}
|
||||
|
||||
function setToken(token: string) {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
function getRefreshToken(): string {
|
||||
return localStorage.getItem(REFRESH_TOKEN_KEY) || "";
|
||||
}
|
||||
|
||||
function setRefreshToken(token: string) {
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
function clearToken() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export { getToken, setToken, clearToken, getRefreshToken, setRefreshToken };
|
||||
29
src/utils/globalCancelToken.js
Normal file
29
src/utils/globalCancelToken.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// 最新的一个请求
|
||||
let cancel = null
|
||||
|
||||
// 所有请求
|
||||
let cancelTokenList = []
|
||||
|
||||
function setToken(cancelToken) {
|
||||
cancel = cancelToken
|
||||
cancelTokenList.push(cancelToken)
|
||||
}
|
||||
|
||||
function cancelToken() {
|
||||
cancel && cancel()
|
||||
cancelTokenList.pop()
|
||||
}
|
||||
|
||||
function clearAllToken() {
|
||||
while (cancelTokenList.length > 0) {
|
||||
let cancel = cancelTokenList.pop()
|
||||
console.log(cancel, 'cancel')
|
||||
cancel && cancel()
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
setToken,
|
||||
cancelToken,
|
||||
clearAllToken,
|
||||
}
|
||||
12
src/utils/i18n.ts
Normal file
12
src/utils/i18n.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// translate router.meta.title, be used in breadcrumb sidebar tagsview
|
||||
import i18n from "@/lang/index";
|
||||
|
||||
export function translateRouteTitle(title: any) {
|
||||
// 判断是否存在国际化配置,如果没有原生返回
|
||||
const hasKey = i18n.global.te("route." + title);
|
||||
if (hasKey) {
|
||||
const translatedTitle = i18n.global.t("route." + title);
|
||||
return translatedTitle;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
58
src/utils/index.ts
Normal file
58
src/utils/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Check if an element has a class
|
||||
* @param {HTMLElement} ele
|
||||
* @param {string} cls
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasClass(ele: HTMLElement, cls: string) {
|
||||
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add class to element
|
||||
* @param {HTMLElement} ele
|
||||
* @param {string} cls
|
||||
*/
|
||||
export function addClass(ele: HTMLElement, cls: string) {
|
||||
if (!hasClass(ele, cls)) ele.className += " " + cls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove class from element
|
||||
* @param {HTMLElement} ele
|
||||
* @param {string} cls
|
||||
*/
|
||||
export function removeClass(ele: HTMLElement, cls: string) {
|
||||
if (hasClass(ele, cls)) {
|
||||
const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");
|
||||
ele.className = ele.className.replace(reg, " ");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是外部链接
|
||||
*
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isExternal(path: string) {
|
||||
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
|
||||
return isExternal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化增长率,保留两位小数 ,并且去掉末尾的0 取绝对值
|
||||
*
|
||||
* @param growthRate
|
||||
* @returns
|
||||
*/
|
||||
export function formatGrowthRate(growthRate: number) {
|
||||
if (growthRate === 0) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const formattedRate = Math.abs(growthRate * 100)
|
||||
.toFixed(2)
|
||||
.replace(/\.?0+$/, "");
|
||||
return formattedRate + "%";
|
||||
}
|
||||
18
src/utils/nprogress.ts
Normal file
18
src/utils/nprogress.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
|
||||
// 进度条
|
||||
NProgress.configure({
|
||||
// 动画方式
|
||||
easing: "ease",
|
||||
// 递增进度条的速度
|
||||
speed: 500,
|
||||
// 是否显示加载ico
|
||||
showSpinner: false,
|
||||
// 自动递增间隔
|
||||
trickleSpeed: 200,
|
||||
// 初始化时的最小百分比
|
||||
minimum: 0.3,
|
||||
});
|
||||
|
||||
export default NProgress;
|
||||
134
src/utils/request-php.js
Normal file
134
src/utils/request-php.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import axios from "axios";
|
||||
import router from "@/router";
|
||||
import Config from "@/settings";
|
||||
import Cookies from "js-cookie";
|
||||
import { setToken } from "@/utils/globalCancelToken.js";
|
||||
function getToken() {
|
||||
return localStorage.getItem("bausertoken");
|
||||
}
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
// baseURL: process.env.NODE_ENV === 'production' ? process.env.VUE_APP_BASE_API : '/',
|
||||
baseURL: "https://czgdoumei.sxczgkj.com/index.php/api/", // api 的 base_url
|
||||
timeout: Config.timeout, // 请求超时时间
|
||||
});
|
||||
|
||||
// request拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
if (getToken()) {
|
||||
config.headers["bausertoken"] = getToken();
|
||||
}
|
||||
config.headers["Content-Type"] = "application/json";
|
||||
// 添加可取消请求配置
|
||||
config.cancelToken = new axios.CancelToken((c) => setToken(c));
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// response 拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data;
|
||||
console.log(data);
|
||||
if (data.code == 0) {
|
||||
ElNotification.error({
|
||||
title: data.msg,
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data.code == 439) {
|
||||
ElNotification.error({
|
||||
title: "请登录",
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data.code == 4399) {
|
||||
ElNotification.error({
|
||||
title: data.msg,
|
||||
duration: 5000,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
if (data.code == 1 && !data.data) {
|
||||
// ElNotification.success({
|
||||
// title: data.msg,
|
||||
// duration: 5000
|
||||
// })
|
||||
return true;
|
||||
}
|
||||
return data.data;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
if (axios.isCancel(error)) {
|
||||
console.log("请求已取消");
|
||||
ElNotification.error({
|
||||
title: "请求已取消",
|
||||
duration: 5000,
|
||||
});
|
||||
return Promise.reject("请求已取消");
|
||||
}
|
||||
|
||||
// 兼容blob下载出错json提示
|
||||
if (
|
||||
error.response.data instanceof Blob &&
|
||||
error.response.data.type.toLowerCase().indexOf("json") !== -1
|
||||
) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(error.response.data, "utf-8");
|
||||
reader.onload = function (e) {
|
||||
const errorMsg = JSON.parse(reader.result).message;
|
||||
ElNotification.error({
|
||||
title: errorMsg,
|
||||
duration: 5000,
|
||||
});
|
||||
};
|
||||
} else {
|
||||
let code = 0;
|
||||
try {
|
||||
code = error.response.data.status;
|
||||
} catch (e) {
|
||||
if (error.toString().indexOf("Error: timeout") !== -1) {
|
||||
ElNotification.error({
|
||||
title: "网络请求超时",
|
||||
duration: 5000,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
console.log(code);
|
||||
if (code) {
|
||||
if (code === 401) {
|
||||
// store.dispatch("LogOut").then(() => {
|
||||
// // 用户登录界面提示
|
||||
// Cookies.set("point", 401);
|
||||
// location.reload();
|
||||
// });
|
||||
} else if (code === 403) {
|
||||
router.push({ path: "/401" });
|
||||
} else {
|
||||
const errorMsg = error.response.data.message;
|
||||
if (errorMsg !== undefined) {
|
||||
ElNotification.error({
|
||||
title: errorMsg,
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElNotification.error({
|
||||
title: "接口请求失败",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
export default service;
|
||||
112
src/utils/request.ts
Normal file
112
src/utils/request.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axios";
|
||||
import qs from "qs";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { ResultEnum } from "@/enums/ResultEnum";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import router from "@/router";
|
||||
|
||||
// 创建 axios 实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API,
|
||||
timeout: 50000,
|
||||
headers: { "Content-Type": "application/json;charset=utf-8" },
|
||||
paramsSerializer: (params) => qs.stringify(params),
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const accessToken = getToken();
|
||||
// 如果 Authorization 设置为 no-auth,则不携带 Token,用于登录、刷新 Token 等接口
|
||||
if (config.headers.Authorization !== "no-auth" && accessToken) {
|
||||
config.headers.Authorization = accessToken;
|
||||
} else {
|
||||
delete config.headers.Authorization;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 如果响应是二进制流,则直接返回,用于下载文件、Excel 导出等
|
||||
if (response.config.responseType === "blob") {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { code, data, msg } = response.data;
|
||||
if (code === ResultEnum.SUCCESS || code === undefined || code === null) {
|
||||
return data ? data : response.data;
|
||||
}
|
||||
|
||||
ElMessage.error(msg || "系统出错");
|
||||
return Promise.reject(new Error(msg || "Error"));
|
||||
},
|
||||
async (error: any) => {
|
||||
// 非 2xx 状态码处理 401、403、500 等
|
||||
const { config, response } = error;
|
||||
if (response) {
|
||||
const { code, msg } = response.data;
|
||||
if (code === ResultEnum.ACCESS_TOKEN_INVALID) {
|
||||
// Token 过期,刷新 Token
|
||||
return handleTokenRefresh(config);
|
||||
} else if (code === ResultEnum.REFRESH_TOKEN_INVALID) {
|
||||
return Promise.reject(new Error(msg || "Error"));
|
||||
} else {
|
||||
ElMessage.error(msg || "系统出错");
|
||||
}
|
||||
}
|
||||
return Promise.reject(error.message);
|
||||
}
|
||||
);
|
||||
|
||||
export default service;
|
||||
|
||||
// 刷新 Token 的锁
|
||||
let isRefreshing = false;
|
||||
// 因 Token 过期导致失败的请求队列
|
||||
let requestsQueue: Array<() => void> = [];
|
||||
|
||||
// 刷新 Token 处理
|
||||
async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
|
||||
return new Promise((resolve) => {
|
||||
const requestCallback = () => {
|
||||
config.headers.Authorization = getToken();
|
||||
resolve(service(config));
|
||||
};
|
||||
|
||||
requestsQueue.push(requestCallback);
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
|
||||
// 刷新 Token
|
||||
useUserStoreHook()
|
||||
.refreshToken()
|
||||
.then(() => {
|
||||
// Token 刷新成功,执行请求队列
|
||||
requestsQueue.forEach((callback) => callback());
|
||||
requestsQueue = [];
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("handleTokenRefresh error", error);
|
||||
// Token 刷新失败,清除用户数据并跳转到登录
|
||||
ElNotification({
|
||||
title: "提示",
|
||||
message: "您的会话已过期,请重新登录",
|
||||
type: "info",
|
||||
});
|
||||
useUserStoreHook()
|
||||
.clearUserData()
|
||||
.then(() => {
|
||||
router.push("/login");
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshing = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
14
src/utils/rsaEncrypt.js
Normal file
14
src/utils/rsaEncrypt.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
|
||||
|
||||
// 密钥对生成 http://web.chacuo.net/netrsakeypair
|
||||
|
||||
const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANL378k3RiZHWx5AfJqdH9xRNBmD9wGD\n' +
|
||||
'2iRe41HdTNF8RUhNnHit5NpMNtGL0NPTSSpPjjI1kJfVorRvaQerUgkCAwEAAQ=='
|
||||
|
||||
// 加密
|
||||
export function encrypt(txt) {
|
||||
const encryptor = new JSEncrypt()
|
||||
encryptor.setPublicKey(publicKey) // 设置公钥
|
||||
return encryptor.encrypt(txt) // 对需要加密的数据进行加密
|
||||
}
|
||||
|
||||
52
src/utils/theme.ts
Normal file
52
src/utils/theme.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// 辅助函数:将十六进制颜色转换为 RGB
|
||||
function hexToRgb(hex: string): [number, number, number] {
|
||||
const bigint = parseInt(hex.slice(1), 16);
|
||||
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
|
||||
}
|
||||
|
||||
// 辅助函数:将 RGB 转换为十六进制颜色
|
||||
function rgbToHex(r: number, g: number, b: number): string {
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
// 辅助函数:调整颜色亮度
|
||||
function adjustBrightness(hex: string, factor: number): string {
|
||||
const rgb = hexToRgb(hex);
|
||||
const newRgb = rgb.map((val) =>
|
||||
Math.max(0, Math.min(255, Math.round(val + (255 - val) * factor)))
|
||||
) as [number, number, number];
|
||||
return rgbToHex(...newRgb);
|
||||
}
|
||||
|
||||
export function generateThemeColors(primary: string) {
|
||||
const colors: Record<string, string> = {
|
||||
primary,
|
||||
};
|
||||
|
||||
// 生成浅色变体
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
const factor = i * 0.1;
|
||||
colors[`primary-light-${i}`] = adjustBrightness(primary, factor);
|
||||
}
|
||||
|
||||
// 生成深色变体
|
||||
colors["primary-dark-2"] = adjustBrightness(primary, -0.2);
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
export function applyTheme(colors: Record<string, string>) {
|
||||
const el = document.documentElement;
|
||||
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
el.style.setProperty(`--el-color-${key}`, value);
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleDarkMode(isDark: boolean) {
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
93
src/utils/websocket.ts
Normal file
93
src/utils/websocket.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Client } from "@stomp/stompjs";
|
||||
import { getToken } from "@/utils/auth";
|
||||
|
||||
class WebSocketManager {
|
||||
private client: Client | null = null;
|
||||
private messageHandlers: Map<string, ((message: string) => void)[]> = new Map();
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 3; // 自定义最大重试次数
|
||||
private reconnectDelay = 5000; // 重试延迟(单位:毫秒)
|
||||
|
||||
// 初始化 WebSocket 客户端
|
||||
setupWebSocket() {
|
||||
const endpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
|
||||
|
||||
// 如果没有配置 WebSocket 端点或显式关闭,直接返回
|
||||
if (!endpoint) {
|
||||
console.log("WebSocket 已被禁用,如需打开请在配置文件中配置 VITE_APP_WS_ENDPOINT");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.client && this.client.connected) {
|
||||
console.log("客户端已存在并且连接正常");
|
||||
return this.client;
|
||||
}
|
||||
|
||||
this.client = new Client({
|
||||
brokerURL: endpoint,
|
||||
connectHeaders: {
|
||||
Authorization: getToken(),
|
||||
},
|
||||
heartbeatIncoming: 30000,
|
||||
heartbeatOutgoing: 30000,
|
||||
reconnectDelay: 0, // 设置为 0 禁用重连
|
||||
onConnect: () => {
|
||||
console.log(`连接到 WebSocket 服务器: ${endpoint}`);
|
||||
this.reconnectAttempts = 0; // 重置重连计数
|
||||
this.messageHandlers.forEach((handlers, topic) => {
|
||||
handlers.forEach((handler) => {
|
||||
this.subscribeToTopic(topic, handler);
|
||||
});
|
||||
});
|
||||
},
|
||||
onStompError: (frame) => {
|
||||
console.error(`连接错误: ${frame.headers["message"]}`);
|
||||
console.error(`错误详情: ${frame.body}`);
|
||||
},
|
||||
onDisconnect: () => {
|
||||
console.log(`WebSocket 连接已断开: ${endpoint}`);
|
||||
this.reconnectAttempts++;
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
console.log(`正在尝试重连... 尝试次数: ${this.reconnectAttempts}`);
|
||||
} else {
|
||||
console.log("重连次数已达上限,停止重连");
|
||||
this.client?.deactivate();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.client.activate();
|
||||
}
|
||||
|
||||
// 订阅主题
|
||||
public subscribeToTopic(topic: string, onMessage: (message: string) => void) {
|
||||
console.log(`正在订阅主题: ${topic}`);
|
||||
if (!this.client || !this.client.connected) {
|
||||
this.setupWebSocket();
|
||||
}
|
||||
|
||||
if (this.messageHandlers.has(topic)) {
|
||||
this.messageHandlers.get(topic)?.push(onMessage);
|
||||
} else {
|
||||
this.messageHandlers.set(topic, [onMessage]);
|
||||
}
|
||||
|
||||
if (this.client?.connected) {
|
||||
this.client.subscribe(topic, (message) => {
|
||||
const handlers = this.messageHandlers.get(topic);
|
||||
handlers?.forEach((handler) => handler(message.body));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 断开 WebSocket 连接
|
||||
public disconnect() {
|
||||
if (this.client) {
|
||||
console.log("断开 WebSocket 连接");
|
||||
this.client.deactivate();
|
||||
this.client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebSocketManager();
|
||||
Reference in New Issue
Block a user