发布v2.0.0分支

This commit is contained in:
zhangtao
2025-07-13 19:31:23 +08:00
parent 4150a39713
commit 6a8ca3becc
342 changed files with 39441 additions and 14737 deletions
+86
View File
@@ -0,0 +1,86 @@
import { Storage } from "./storage";
import { AUTH_KEYS } from "@/constants";
/**
* 身份验证工具类
* 集中管理所有与认证相关的功能,包括:
* - 登录状态判断
* - Token 的存取
* - 记住我功能的状态管理
*/
export class Auth {
/**
* 判断用户是否已登录
* @returns 是否已登录
*/
static isLoggedIn(): boolean {
return !!Auth.getAccessToken();
}
/**
* 获取当前有效的访问令牌
* 会根据"记住我"状态从适当的存储位置获取
* @returns 当前有效的访问令牌
*/
static getAccessToken(): string {
const isRememberMe = Storage.get<boolean>(AUTH_KEYS.REMEMBER_ME, false);
// 根据"记住我"状态决定从哪个存储位置获取token
return isRememberMe
? Storage.get(AUTH_KEYS.ACCESS_TOKEN, "")
: Storage.sessionGet(AUTH_KEYS.ACCESS_TOKEN, "");
}
/**
* 获取刷新令牌
* @returns 当前有效的刷新令牌
*/
static getRefreshToken(): string {
const isRememberMe = Storage.get<boolean>(AUTH_KEYS.REMEMBER_ME, false);
return isRememberMe
? Storage.get(AUTH_KEYS.REFRESH_TOKEN, "")
: Storage.sessionGet(AUTH_KEYS.REFRESH_TOKEN, "");
}
/**
* 设置访问令牌和刷新令牌
* @param accessToken 访问令牌
* @param refreshToken 刷新令牌
* @param rememberMe 是否记住我
*/
static setTokens(accessToken: string, refreshToken: string, rememberMe: boolean): void {
// 保存"记住我"状态
Storage.set(AUTH_KEYS.REMEMBER_ME, rememberMe);
if (rememberMe) {
// 使用localStorage长期保存
Storage.set(AUTH_KEYS.ACCESS_TOKEN, accessToken);
Storage.set(AUTH_KEYS.REFRESH_TOKEN, refreshToken);
} else {
// 使用sessionStorage临时保存
Storage.sessionSet(AUTH_KEYS.ACCESS_TOKEN, accessToken);
Storage.sessionSet(AUTH_KEYS.REFRESH_TOKEN, refreshToken);
// 清除localStorage中可能存在的token
Storage.remove(AUTH_KEYS.ACCESS_TOKEN);
Storage.remove(AUTH_KEYS.REFRESH_TOKEN);
}
}
/**
* 清除所有身份验证相关的数据
*/
static 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);
// 不清除记住我设置,保留用户偏好
}
/**
* 获取"记住我"状态
* @returns 是否记住我
*/
static getRememberMe(): boolean {
return Storage.get<boolean>(AUTH_KEYS.REMEMBER_ME, false);
}
}
@@ -1,29 +1,35 @@
import storage from "store";
export function timeFix() {
const time = new Date();
const hour = time.getHours();
return hour < 9
? "早上好"
: hour <= 11
? "上午好"
: hour <= 13
? "中午好"
: hour < 20
? "下午好"
: "晚上好";
}
export function getRangeDate(startDate, endDate) {
let targetArr = [];
let start = new Date(startDate);
let end = new Date(endDate);
let startDateInfo = {
// 问候语:根据当前小时返回不同问候语
export function greetings () {
// 当前时间(用于计算问候语)
const currentDate = new Date();
const hours = currentDate.getHours();
if (hours >= 6 && hours < 8) {
return "晨起披衣出草堂,轩窗已自喜微凉🌅!";
} else if (hours >= 8 && hours < 12) {
return `上午好!`;
} else if (hours >= 12 && hours < 14) {
return `中午好!`;
} else if (hours >= 14 && hours < 18) {
return `下午好!`;
} else if (hours >= 18 && hours < 24) {
return `晚上好!`;
} else {
return "偷偷向银河要了一把碎星,只等你闭上眼睛撒入你的梦中,晚安🌛!";
}
};
export function getRangeDate(startDate: string | number | Date, endDate: string | number | Date) {
const targetArr = [];
const start = new Date(startDate);
const end = new Date(endDate);
const startDateInfo = {
year: start.getFullYear(),
month: start.getMonth() + 1,
day: start.getDate(),
};
let endDateInfo = {
const endDateInfo = {
year: end.getFullYear(),
month: end.getMonth() + 1,
day: end.getDate(),
@@ -33,12 +39,12 @@ export function getRangeDate(startDate, endDate) {
if (startDateInfo.month !== endDateInfo.month) {
//同年,不同月份
//获取开始时间所在月的月底日期
let startMax = new Date(
const startMax = new Date(
startDateInfo.year,
startDateInfo.month,
0
).getDate();
let endNum = startMax - startDateInfo.day + endDateInfo.day;
const endNum = startMax - startDateInfo.day + endDateInfo.day;
for (let i = startDateInfo.day; i <= startDateInfo.day + endNum; i++) {
if (i > startMax) {
targetArr.push(
@@ -72,12 +78,12 @@ export function getRangeDate(startDate, endDate) {
}
} else {
//不同年 【既然不同年那肯定也不同月】
let startMax = new Date(
const startMax = new Date(
startDateInfo.year,
startDateInfo.month,
0
).getDate();
let endNum = startMax - startDateInfo.day + endDateInfo.day;
const endNum = startMax - startDateInfo.day + endDateInfo.day;
for (let i = startDateInfo.day; i <= startDateInfo.day + endNum; i++) {
if (i > startMax) {
targetArr.push(
@@ -100,10 +106,6 @@ export function getRangeDate(startDate, endDate) {
return targetArr;
}
export function save_token(access_token, refresh_token, expires_in) {
storage.set("Access-Token", access_token, new Date().getTime() + expires_in);
storage.set("Refresh-Token", refresh_token);
}
// export function listToTree(list) {
// let resultList = list.filter((item) => {
@@ -117,13 +119,15 @@ export function save_token(access_token, refresh_token, expires_in) {
// });
// return cloneDeep(resultList);
// }
export function listToTree(list) {
const map = {};
export function listToTree(list: any[]) {
const map: { [key: string | number]: any } = {};
// 创建映射表,保留每个节点的 parent_id 等原始字段
list.forEach(item => map[item.id] = { ...item });
list.forEach((item) => {
map[item.id] = { ...item };
});
const tree = [];
list.forEach(item => {
const tree: any[] = [];
list.forEach((item) => {
const parentId = item.parent_id;
if (parentId && map[parentId]) {
// 将当前节点加入其父节点的 children 数组中
@@ -139,14 +143,16 @@ export function listToTree(list) {
return tree;
}
export function cloneDeep(obj) {
export function cloneDeep(obj: any) {
return JSON.parse(JSON.stringify(obj));
}
export function isEmpty(obj) {
export function isEmpty(obj: string | null | undefined) {
if (obj === undefined || obj === null || obj === "") {
return true;
} else {
return false;
}
}
+73
View File
@@ -0,0 +1,73 @@
import { reactive, toRefs } from 'vue'
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
import dayjs from 'dayjs'
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const DATE_FORMAT = 'YYYY-MM-DD'
export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
return dayjs(date).format(format)
}
export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
return dayjs(date).format(format)
}
export const useNow = (immediate = true) => {
let timer: ReturnType<typeof setInterval>
const state = reactive({
year: 0,
month: 0,
week: '',
day: 0,
hour: '',
minute: '',
second: 0,
meridiem: ''
})
const update = () => {
const now = dayjs()
const h = now.format('HH')
const m = now.format('mm')
const s = now.get('s')
state.year = now.get('y')
state.month = now.get('M') + 1
state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
state.day = now.get('date')
state.hour = h
state.minute = m
state.second = s
state.meridiem = now.format('A')
}
function start() {
update()
clearInterval(timer)
timer = setInterval(() => update(), 1000)
}
function stop() {
clearInterval(timer)
}
tryOnMounted(() => {
if (immediate) {
start()
}
})
tryOnUnmounted(() => {
stop()
})
return {
...toRefs(state),
start,
stop
}
}
+12
View 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
View 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
View 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;
-96
View File
@@ -1,96 +0,0 @@
import axios from "axios";
import storage from "store";
import router from "@/router";
import { getNewToken } from "@/api/system/auth";
import { save_token } from "@/utils/util";
import { message } from 'ant-design-vue';
// 创建 axios 实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: import.meta.env.VITE_TIMEOUT, // 请求超时时间
});
// 异常拦截处理器
const errorHandler = async (error) => {
// console.error(error);
if (!error.response) {
return Promise.reject(error);
}
const data = error.response.data;
// localstorage 获取 access_token
const access_token = storage.get("Access-Token");
// || data.msg === "用户未登录或登录已过期"
if (data.status_code === 403 || data.msg.includes("请重新登录")) {
storage.remove("Access-Token");
storage.remove("Refresh-Token");
message.error(data.msg);
router.push("/login");
return Promise.reject(error);
}
if (data.status_code === 401) {
if (!access_token) {
message.error(data.msg);
router.push("/login");
return Promise.reject(error);
}
const refresh_token = storage.get("Refresh-Token");
storage.remove("Access-Token");
storage.remove("Refresh-Token");
return getNewToken({ refresh_token: refresh_token })
.then((response) => {
const result = response.data;
if (result.status_code === 200) {
save_token(
result.data.access_token,
result.data.refresh_token,
result.data.expires_in
);
return request(error.response.config).then((response) => response);
}
message.error(result.msg);
router.push("/login");
return Promise.reject(error);
})
.catch((error) => {
message.error(data.msg);
return Promise.reject(error);
});
} else {
message.error(data.msg);
return Promise.reject(error);
}
};
// 请求拦截器
request.interceptors.request.use((config) => {
const token = storage.get("Access-Token");
// 如果 token 存在
// 让每个请求携带自定义 token 请根据实际情况自行修改
if (token) {
config.headers["Authorization"] = "Bearer " + token;
}
return config;
}, errorHandler);
// 响应拦截器
request.interceptors.response.use((response) => {
// 如果是文件下载类型的响应直接返回
if (response.config.responseType === 'blob') {
return response;
}
if (response.data.status_code !== 200) {
message.error(response.data.msg);
return Promise.reject(response);
}
// message.success(response.data.msg);
return response;
}, errorHandler);
export default request;
+116
View File
@@ -0,0 +1,116 @@
import axios, { type InternalAxiosRequestConfig, type AxiosResponse, type AxiosInstance, type AxiosError} from "axios";
import qs from "qs";
import { useUserStoreHook } from "@/store/modules/user.store";
import { ResultEnum } from "@/enums/api/result.enum";
import { Auth } from "@/utils/auth";
import router from "@/router";
/**
* 创建 HTTP 请求实例
*/
const httpRequest: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: import.meta.env.VITE_TIMEOUT,
headers: { "Content-Type": "application/json;charset=utf-8" },
paramsSerializer: (params) => qs.stringify(params),
});
/**
* 请求拦截器 - 添加 Authorization 头
*/
httpRequest.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = Auth.getAccessToken();
// 如果 Authorization 设置为 no-auth,则不携带 Token
if (!config.headers.Authorization && accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
} else {
delete config.headers.Authorization;
}
return config;
},
(error) => {
console.error("请求异常:", error);
ElMessage.error(error);
return Promise.reject(error);
}
);
/**
* 响应拦截器 - 统一处理响应和错误
*/
httpRequest.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
// 如果响应是二进制流,则直接返回(用于文件下载、Excel 导出等)
if (response?.config?.responseType === "blob") {
return response;
}
const data = response.data;
// 请求成功
if (data.code === ResultEnum.SUCCESS) {
ElMessage.success(data.msg);
return response;
} else if (data.code === ResultEnum.ERROR) {
ElMessage.error(data.msg || "请求错误");
return Promise.reject(new Error(data.msg || "请求错误"));
} else if (data.code === ResultEnum.EXCEPTION) {
ElMessage.error(data.msg || "服务异常");
return Promise.reject(new Error(data.msg || "服务异常"));
} else {
ElMessage.error(data.msg || "请求异常");
return Promise.reject(new Error(data.msg || "请求异常"));
}
},
async (error: AxiosError) => {
console.error("响应异常 " + error);
const { response } = error;
// 网络错误或服务器无响应
if (!response) {
ElMessage.error("网络连接失败,请检查网络设置");
return Promise.reject(error);
}
const { status_code, msg } = response.data as ApiResponse;
switch (status_code) {
case ResultEnum.ACCESS_TOKEN_INVALID:
// Access Token 过期
await redirectToLogin("登录已过期,请重新登录");
return Promise.reject(new Error(msg || "Refresh Token Invalid"));
default:
ElMessage.error(msg || "响应错误");
return Promise.reject(new Error(msg || "响应错误"));
}
}
);
/**
* 重定向到登录页面
*/
async function redirectToLogin(message: string = "请重新登录"): Promise<void> {
try {
ElNotification({
title: "提示",
message,
type: "warning",
duration: 3000,
});
await useUserStoreHook().resetAllState();
// 跳转到登录页,保留当前路由用于登录后跳转
const currentPath = router.currentRoute.value.fullPath;
await router.push(`/login?redirect=${encodeURIComponent(currentPath)}`);
} catch (error) {
console.error("Redirect to login error:", error);
}
}
export default httpRequest;
+64
View File
@@ -0,0 +1,64 @@
/**
* 存储工具类
* 提供localStorage和sessionStorage操作方法
*/
export class Storage {
/**
* localStorage 存储
*/
static set(key: string, value: any): void {
localStorage.setItem(key, JSON.stringify(value));
}
static get<T>(key: string, defaultValue?: T): T {
const value = localStorage.getItem(key);
if (!value) return defaultValue as T;
try {
return JSON.parse(value);
} catch {
// 如果解析失败,返回原始字符串
return value as unknown as T;
}
}
static remove(key: string): void {
localStorage.removeItem(key);
}
/**
* localStorage 清空
*/
static clear(): void {
localStorage.clear();
}
/**
* sessionStorage 存储
*/
static sessionSet(key: string, value: any): void {
sessionStorage.setItem(key, JSON.stringify(value));
}
static sessionGet<T>(key: string, defaultValue?: T): T {
const value = sessionStorage.getItem(key);
if (!value) return defaultValue as T;
try {
return JSON.parse(value);
} catch {
// 如果解析失败,返回原始字符串
return value as unknown as T;
}
}
static sessionRemove(key: string): void {
sessionStorage.removeItem(key);
}
/**
* sessionStorage 清空
*/
static sessionClear(): void {
sessionStorage.clear();
}
}
+112
View File
@@ -0,0 +1,112 @@
import { ThemeMode } from "@/enums";
// 辅助函数:将十六进制颜色转换为 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, theme: string): string {
const rgb = hexToRgb(hex);
// 是否是暗黑模式
const isDarkMode = theme === "dark" ? 0 : 255;
const newRgb = rgb.map((val) =>
Math.max(0, Math.min(255, Math.round(val + (isDarkMode - val) * factor)))
) as [number, number, number];
return rgbToHex(...newRgb);
} */
/**
* 加深颜色值
* @param {String} color 颜色值字符串
* @param {Number} level 加深的程度,限0-1之间
* @returns {String} 返回处理后的颜色值
*/
export function getDarkColor(color: string, level: number): string {
const rgb = hexToRgb(color);
for (let i = 0; i < 3; i++) rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level));
return rgbToHex(rgb[0], rgb[1], rgb[2]);
}
/**
* 变浅颜色值
* @param {String} color 颜色值字符串
* @param {Number} level 加深的程度,限0-1之间
* @returns {String} 返回处理后的颜色值
*/
export const getLightColor = (color: string, level: number): string => {
const rgb = hexToRgb(color);
for (let i = 0; i < 3; i++) rgb[i] = Math.round(255 * level + rgb[i] * (1 - level));
return rgbToHex(rgb[0], rgb[1], rgb[2]);
};
/**
* 生成主题色
* @param primary 主题色
* @param theme 主题类型
*/
export function generateThemeColors(primary: string, theme: ThemeMode) {
const colors: Record<string, string> = {
primary,
};
// 生成浅色变体
for (let i = 1; i <= 9; i++) {
colors[`primary-light-${i}`] =
theme === ThemeMode.LIGHT
? `${getLightColor(primary, i / 10)}`
: `${getDarkColor(primary, i / 10)}`;
}
// 生成深色变体
colors["primary-dark-2"] =
theme === ThemeMode.LIGHT ? `${getLightColor(primary, 0.2)}` : `${getDarkColor(primary, 0.3)}`;
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);
});
// 确保主题色立即生效,强制重新渲染
requestAnimationFrame(() => {
// 触发样式重新计算
el.style.setProperty("--theme-update-trigger", Date.now().toString());
});
}
/**
* 切换暗黑模式
*
* @param isDark 是否启用暗黑模式
*/
export function toggleDarkMode(isDark: boolean) {
if (isDark) {
document.documentElement.classList.add(ThemeMode.DARK);
} else {
document.documentElement.classList.remove(ThemeMode.DARK);
}
}
/**
* 切换浅色主题下的侧边栏颜色方案
*
* @param isBlue 布尔值,表示是否开启深蓝色侧边栏颜色方案
*/
export function toggleSidebarColor(isBuleSidebar: boolean) {
if (isBuleSidebar) {
document.documentElement.classList.add("sidebar-color-blue");
} else {
document.documentElement.classList.remove("sidebar-color-blue");
}
}