mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 21:06:32 +00:00
发布v2.0.0分支
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 + "%";
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user