feat: 项目结构重构优化

This commit is contained in:
Ray.Hao
2025-12-26 12:35:37 +08:00
parent 65ad4fe59f
commit aa374dd2ba
164 changed files with 11305 additions and 3103 deletions

View File

@@ -1,47 +1,46 @@
import { Storage } from "./storage";
import { AUTH_KEYS } from "@/constants";
import { ROLE_ROOT } from "@/constants";
import { useUserStoreHook } from "@/store/modules/user-store";
import { STORAGE_KEYS, ROLE_ROOT } from "@/constants";
import { useUserStoreHook } from "@/store/modules/user";
import router from "@/router";
// 负责本地凭证与偏好的读写
export const AuthStorage = {
getAccessToken(): string {
const isRememberMe = Storage.get<boolean>(AUTH_KEYS.REMEMBER_ME, false);
const isRememberMe = Storage.get<boolean>(STORAGE_KEYS.REMEMBER_ME, false);
return isRememberMe
? Storage.get(AUTH_KEYS.ACCESS_TOKEN, "")
: Storage.sessionGet(AUTH_KEYS.ACCESS_TOKEN, "");
? Storage.get(STORAGE_KEYS.ACCESS_TOKEN, "")
: Storage.sessionGet(STORAGE_KEYS.ACCESS_TOKEN, "");
},
getRefreshToken(): string {
const isRememberMe = Storage.get<boolean>(AUTH_KEYS.REMEMBER_ME, false);
const isRememberMe = Storage.get<boolean>(STORAGE_KEYS.REMEMBER_ME, false);
return isRememberMe
? Storage.get(AUTH_KEYS.REFRESH_TOKEN, "")
: Storage.sessionGet(AUTH_KEYS.REFRESH_TOKEN, "");
? Storage.get(STORAGE_KEYS.REFRESH_TOKEN, "")
: Storage.sessionGet(STORAGE_KEYS.REFRESH_TOKEN, "");
},
setTokens(accessToken: string, refreshToken: string, rememberMe: boolean): void {
Storage.set(AUTH_KEYS.REMEMBER_ME, rememberMe);
Storage.set(STORAGE_KEYS.REMEMBER_ME, rememberMe);
if (rememberMe) {
Storage.set(AUTH_KEYS.ACCESS_TOKEN, accessToken);
Storage.set(AUTH_KEYS.REFRESH_TOKEN, refreshToken);
Storage.set(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
Storage.set(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
} else {
Storage.sessionSet(AUTH_KEYS.ACCESS_TOKEN, accessToken);
Storage.sessionSet(AUTH_KEYS.REFRESH_TOKEN, refreshToken);
Storage.remove(AUTH_KEYS.ACCESS_TOKEN);
Storage.remove(AUTH_KEYS.REFRESH_TOKEN);
Storage.sessionSet(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
Storage.sessionSet(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
Storage.remove(STORAGE_KEYS.ACCESS_TOKEN);
Storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
}
},
clearAuth(): void {
Storage.remove(AUTH_KEYS.ACCESS_TOKEN);
Storage.remove(AUTH_KEYS.REFRESH_TOKEN);
Storage.sessionRemove(AUTH_KEYS.ACCESS_TOKEN);
Storage.sessionRemove(AUTH_KEYS.REFRESH_TOKEN);
Storage.remove(STORAGE_KEYS.ACCESS_TOKEN);
Storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
Storage.sessionRemove(STORAGE_KEYS.ACCESS_TOKEN);
Storage.sessionRemove(STORAGE_KEYS.REFRESH_TOKEN);
},
getRememberMe(): boolean {
return Storage.get<boolean>(AUTH_KEYS.REMEMBER_ME, false);
return Storage.get<boolean>(STORAGE_KEYS.REMEMBER_ME, false);
},
};

View File

@@ -1,51 +0,0 @@
/**
* DOM 操作相关工具函数
*/
/**
* 检查元素是否包含指定 class
* @param ele HTML 元素
* @param cls class 名称
* @returns 是否包含
*
* @example
* ```ts
* const hasActiveClass = hasClass(element, 'active');
* ```
*/
export function hasClass(ele: HTMLElement, cls: string): boolean {
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
}
/**
* 为元素添加 class
* @param ele HTML 元素
* @param cls class 名称
*
* @example
* ```ts
* addClass(element, 'active');
* ```
*/
export function addClass(ele: HTMLElement, cls: string): void {
if (!hasClass(ele, cls)) {
ele.className += " " + cls;
}
}
/**
* 从元素移除 class
* @param ele HTML 元素
* @param cls class 名称
*
* @example
* ```ts
* removeClass(element, 'active');
* ```
*/
export function removeClass(ele: HTMLElement, cls: string): void {
if (hasClass(ele, cls)) {
const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");
ele.className = ele.className.replace(reg, " ");
}
}

View File

@@ -1,12 +0,0 @@
// 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;
}

View File

@@ -1,23 +1,9 @@
/**
* 工具函数统一导出
*
* 本文件作为 barrel export统一管理所有工具函数的导出
* 各类工具函数按功能分类存放在不同文件中:
* - dom.ts: DOM 操作相关
* - validate.ts: 数据验证相关
* - format.ts: 数据格式化相关
* - download.ts: 文件下载相关
* - auth.ts: 权限认证相关
* - storage.ts: 本地存储相关
* - request.ts: 网络请求相关
* - theme.ts: 主题相关
*/
// DOM 操作
export { hasClass, addClass, removeClass } from "./dom";
// 数据验证
export { isExternal, isValidURL, isEmail, isMobile } from "./validate";
export { isExternal, isValidURL, isEmail, isMobile, VALIDATORS } from "./validate";
// 数据格式化
export { formatGrowthRate, formatFileSize, formatNumber, formatCurrency } from "./format";

View File

@@ -1,18 +0,0 @@
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;

View File

@@ -1,18 +0,0 @@
/**
* 全局组件注册工具
*
* @description
* 批量注册 Element Plus 图标等全局组件
*/
import type { App } from "vue";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
/**
* 注册 Element Plus 所有图标为全局组件
*/
export function registerElementIcons(app: App) {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
}

View File

@@ -1,117 +1,124 @@
import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axios";
import qs from "qs";
import { ApiCodeEnum } from "@/enums/api";
import { useUserStoreHook } from "@/store/modules/user";
import { AuthStorage, redirectToLogin } from "@/utils/auth";
import { STORAGE_KEYS } from "@/constants";
import { useTokenRefresh } from "@/composables/auth/useTokenRefresh";
import { authConfig } from "@/settings";
// 初始化token刷新组合式函数
const { refreshTokenAndRetry } = useTokenRefresh();
// ============================================
// HTTP 请求实例
// ============================================
/**
* 创建 HTTP 请求实例
*/
const httpRequest = axios.create({
const http = 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),
});
/**
* 请求拦截器 - 添加 Authorization 头
*/
httpRequest.interceptors.request.use(
// ============================================
// 请求拦截器
// ============================================
http.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = AuthStorage.getAccessToken();
const token = AuthStorage.getAccessToken();
// 如果 Authorization 设置为 no-auth则不携带 Token
if (config.headers.Authorization !== "no-auth" && accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
const tenantId = localStorage.getItem(STORAGE_KEYS.TENANT_ID);
if (tenantId) {
config.headers["tenant-id"] = tenantId;
}
} else {
if (config.headers.Authorization === "no-auth") {
delete config.headers.Authorization;
delete config.headers["tenant-id"];
} else if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
console.error("Request interceptor error:", error);
return Promise.reject(error);
}
(error) => Promise.reject(error)
);
/**
* 响应拦截器 - 统一处理响应和错误
*/
httpRequest.interceptors.response.use(
// ============================================
// 响应拦截器
// ============================================
http.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
// 如果响应是二进制数据,则直接返回response对象用于文件下载、Excel导出、图片显示等
if (response.config.responseType === "blob" || response.config.responseType === "arraybuffer") {
// 二进制数据直接返回
const { responseType } = response.config;
if (responseType === "blob" || responseType === "arraybuffer") {
return response;
}
const { code, data, msg } = response.data;
// 请求成功
if (code === ApiCodeEnum.SUCCESS) {
return data;
}
// 特殊处理:需要选择租户(不显示错误提示,返回特殊对象供业务层处理)
// 需要选择租户(特殊业务码,传递给调用方处理)
if (code === ApiCodeEnum.CHOOSE_TENANT) {
return Promise.reject({
code: ApiCodeEnum.CHOOSE_TENANT,
data,
msg,
});
return Promise.reject({ code, data, msg });
}
// 业务错误
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Business Error"));
return Promise.reject(new Error(msg || "Error"));
},
async (error) => {
console.error("Response interceptor error:", error);
async (error) => {
const { config, response } = error;
// 网络错误或服务器无响应
if (!response) {
ElMessage.error("网络连接失败,请检查网络设置");
ElMessage.error("网络连接失败");
return Promise.reject(error);
}
const { code, msg } = response.data as ApiResponse;
switch (code) {
case ApiCodeEnum.ACCESS_TOKEN_INVALID:
// Access Token 过期
if (authConfig.enableTokenRefresh) {
// 启用了token刷新尝试刷新
return refreshTokenAndRetry(config, httpRequest);
} else {
// 未启用token刷新直接跳转登录页
await redirectToLogin("登录已过期,请重新登录");
return Promise.reject(new Error(msg || "Access Token Invalid"));
}
case ApiCodeEnum.REFRESH_TOKEN_INVALID:
// Refresh Token 过期,跳转登录页
await redirectToLogin("登录已过期,请重新登录");
return Promise.reject(new Error(msg || "Refresh Token Invalid"));
default:
ElMessage.error(msg || "请求失败");
return Promise.reject(new Error(msg || "Request Error"));
// Token 过期处理
if (code === ApiCodeEnum.ACCESS_TOKEN_INVALID) {
return retryWithRefresh(config);
}
if (code === ApiCodeEnum.REFRESH_TOKEN_INVALID) {
await redirectToLogin("登录已过期,请重新登录");
return Promise.reject(new Error(msg || "Token Invalid"));
}
ElMessage.error(msg || "请求失败");
return Promise.reject(new Error(msg || "Error"));
}
);
export default httpRequest;
export default http;
// ============================================
// Token 刷新重试
// ============================================
type Pending = { resolve: (v: unknown) => void; reject: (e: Error) => void };
let refreshing = false;
const queue: Pending[] = [];
async function retryWithRefresh(config: InternalAxiosRequestConfig): Promise<unknown> {
return new Promise((resolve, reject) => {
queue.push({ resolve, reject });
if (refreshing) return;
refreshing = true;
useUserStoreHook()
.refreshToken()
.then(() => {
const token = AuthStorage.getAccessToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
queue.forEach(({ resolve }) => http(config).then(resolve).catch(reject));
})
.catch(async () => {
queue.forEach(({ reject }) => reject(new Error("Token refresh failed")));
await redirectToLogin("登录已过期,请重新登录");
})
.finally(() => {
queue.length = 0;
refreshing = false;
});
});
}

View File

@@ -2,16 +2,26 @@ import { STORAGE_KEYS, APP_PREFIX } from "@/constants";
/**
* 存储工具类
* 提供localStorage和sessionStorage操作方法
*
* @description
* 提供 localStorage 和 sessionStorage 的统一操作接口
* 支持自动 JSON 序列化/反序列化
*
* @author 有来技术团队
*/
export class Storage {
// ==================== localStorage 操作 ====================
/**
* localStorage 存储
* 存储数据到 localStorage
*/
static set(key: string, value: any): void {
localStorage.setItem(key, JSON.stringify(value));
}
/**
* 从 localStorage 获取数据
*/
static get<T>(key: string, defaultValue?: T): T {
const value = localStorage.getItem(key);
if (!value) return defaultValue as T;
@@ -24,17 +34,25 @@ export class Storage {
}
}
/**
* 从 localStorage 删除数据
*/
static remove(key: string): void {
localStorage.removeItem(key);
}
// ==================== sessionStorage 操作 ====================
/**
* sessionStorage 存储
* 存储数据到 sessionStorage
*/
static sessionSet(key: string, value: any): void {
sessionStorage.setItem(key, JSON.stringify(value));
}
/**
* 从 sessionStorage 获取数据
*/
static sessionGet<T>(key: string, defaultValue?: T): T {
const value = sessionStorage.getItem(key);
if (!value) return defaultValue as T;
@@ -47,20 +65,26 @@ export class Storage {
}
}
/**
* 从 sessionStorage 删除数据
*/
static sessionRemove(key: string): void {
sessionStorage.removeItem(key);
}
// ==================== 批量清理操作 ====================
/**
* 存储清理工具方法
* 清理指定键的存储localStorage + sessionStorage
*/
// 清理指定键的存储localStorage + sessionStorage
static clear(key: string): void {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
}
// 批量清理存储
/**
* 批量清理存储
*/
static clearMultiple(keys: string[]): void {
keys.forEach((key) => {
localStorage.removeItem(key);
@@ -68,7 +92,15 @@ export class Storage {
});
}
// 清理指定前缀的存储
/**
* 清理指定前缀的存储
*
* @example
* ```ts
* // 清理所有认证相关的存储
* Storage.clearByPrefix('vea:auth:');
* ```
*/
static clearByPrefix(prefix: string): void {
// localStorage 清理
const localKeys = Object.keys(localStorage).filter((key) => key.startsWith(prefix));
@@ -80,21 +112,18 @@ export class Storage {
}
/**
* 项目特定的清理便利方法
* 清理所有项目相关的存储
*
* @description
* 清理所有以 APP_PREFIX 开头的存储项
*/
// 清理所有项目相关的存储
static clearAllProject(): void {
const keys = Object.values(STORAGE_KEYS);
this.clearMultiple(keys);
this.clearByPrefix(`${APP_PREFIX}:`);
}
// 清理特定分类的存储
static clearByCategory(category: "auth" | "system" | "ui" | "app"): void {
const prefix = `${APP_PREFIX}:${category}:`;
this.clearByPrefix(prefix);
}
// 获取所有项目相关的存储键
/**
* 获取所有项目相关的存储键
*/
static getAllProjectKeys(): string[] {
return Object.values(STORAGE_KEYS);
}

View File

@@ -2,6 +2,8 @@
* 数据验证相关工具函数
*/
import type { FormItemRule } from "element-plus";
/**
* 判断是否是外部链接
* @param path 路径字符串
@@ -57,3 +59,48 @@ export function isMobile(mobile: string): boolean {
const pattern = /^1[3|4|5|6|7|8|9][0-9]\d{8}$/;
return pattern.test(mobile);
}
/**
* 表单验证规则生成器
*/
export const VALIDATORS = {
/** 必填项验证 */
required(message: string): FormItemRule {
return { required: true, message, trigger: "blur" };
},
/** 邮箱验证 */
email: {
type: "email",
message: "请输入正确的邮箱地址",
trigger: "blur",
} as FormItemRule,
/** 手机号验证 */
mobile: {
pattern: /^1[3-9]\d{9}$/,
message: "请输入正确的手机号码",
trigger: "blur",
} as FormItemRule,
/** URL 验证 */
url: {
type: "url",
message: "请输入正确的URL地址",
trigger: "blur",
} as FormItemRule,
/** 数字验证 */
number: {
type: "number",
message: "请输入数字",
trigger: "blur",
} as FormItemRule,
/** 整数验证 */
integer: {
type: "integer",
message: "请输入整数",
trigger: "blur",
} as FormItemRule,
};

View File

@@ -1,69 +0,0 @@
/**
* 表单验证规则工具
*
* @description
* 提供常用的表单验证规则
*/
import type { FormItemRule } from "element-plus";
/**
* 验证规则生成器
*/
export const VALIDATORS = {
/**
* 必填项验证
*/
required(message: string): FormItemRule {
return {
required: true,
message,
trigger: "blur",
};
},
/**
* 邮箱验证
*/
email: {
type: "email",
message: "请输入正确的邮箱地址",
trigger: "blur",
} as FormItemRule,
/**
* 手机号验证
*/
mobile: {
pattern: /^1[3-9]\d{9}$/,
message: "请输入正确的手机号码",
trigger: "blur",
} as FormItemRule,
/**
* URL 验证
*/
url: {
type: "url",
message: "请输入正确的URL地址",
trigger: "blur",
} as FormItemRule,
/**
* 数字验证
*/
number: {
type: "number",
message: "请输入数字",
trigger: "blur",
} as FormItemRule,
/**
* 整数验证
*/
integer: {
type: "integer",
message: "请输入整数",
trigger: "blur",
} as FormItemRule,
};

View File

@@ -1,114 +0,0 @@
/**
* WebSocket 服务管理
*
* @description
* 统一管理应用中的所有 WebSocket 连接
* - 字典同步 WebSocket
* - 在线用户计数 WebSocket
* - 其他业务 WebSocket
*
* @author 有来技术团队
*/
import { useDictSync } from "@/composables";
import { AuthStorage } from "@/utils/auth";
/**
* WebSocket 服务实例约定接口
*/
type WebSocketService = {
disconnect?: () => void;
closeWebSocket?: () => void;
cleanup?: () => void;
[key: string]: any;
};
/**
* 全局 WebSocket 实例管理
*/
const websocketInstances = new Map<string, WebSocketService>();
/**
* 防止重复初始化的状态标记
*/
let isInitialized = false;
let dictWebSocketInstance: ReturnType<typeof useDictSync> | null = null;
/**
* 注册 WebSocket 实例
*/
export function registerWebSocketInstance(key: string, instance: WebSocketService) {
websocketInstances.set(key, instance);
}
/**
* 获取 WebSocket 实例
*/
export function getWebSocketInstance(key: string) {
return websocketInstances.get(key);
}
/**
* 初始化 WebSocket 服务
*/
export function setupWebSocket() {
if (isInitialized) {
console.warn("[WebSocket] 已初始化,跳过重复初始化");
return;
}
if (!AuthStorage.getAccessToken()) {
console.warn("[WebSocket] 未登录,跳过 WebSocket 初始化");
return;
}
try {
dictWebSocketInstance = useDictSync();
registerWebSocketInstance("dict-sync", dictWebSocketInstance);
isInitialized = true;
console.log("[WebSocket] 初始化成功");
} catch (error) {
console.error("[WebSocket] 初始化失败:", error);
}
}
/**
* 清理所有 WebSocket 连接
*/
export function cleanupWebSocket() {
console.log("[WebSocket] 开始清理连接...");
websocketInstances.forEach((instance, key) => {
try {
if (instance.disconnect) {
instance.disconnect();
} else if (instance.closeWebSocket) {
instance.closeWebSocket();
} else if (instance.cleanup) {
instance.cleanup();
}
console.log(`[WebSocket] ${key} 已断开`);
} catch (error) {
console.error(`[WebSocket] ${key} 清理失败:`, error);
}
});
websocketInstances.clear();
dictWebSocketInstance = null;
isInitialized = false;
console.log("[WebSocket] 清理完成");
}
/**
* 重新初始化 WebSocket
*/
export function reinitializeWebSocket() {
cleanupWebSocket();
setupWebSocket();
}
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", () => {
cleanupWebSocket();
});
}