feat: 项目结构重构优化
This commit is contained in:
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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, " ");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user