mirror of
https://github.com/pure-admin/pure-admin-thin.git
synced 2025-11-16 23:53:37 +08:00
release: update 5.1.0
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { storageSession } from "@pureadmin/utils";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
|
||||
export interface DataInfo<T> {
|
||||
@@ -15,26 +15,34 @@ export interface DataInfo<T> {
|
||||
roles?: Array<string>;
|
||||
}
|
||||
|
||||
export const sessionKey = "user-info";
|
||||
export const userKey = "user-info";
|
||||
export const TokenKey = "authorized-token";
|
||||
/**
|
||||
* 通过`multiple-tabs`是否在`cookie`中,判断用户是否已经登录系统,
|
||||
* 从而支持多标签页打开已经登录的系统后无需再登录。
|
||||
* 浏览器完全关闭后`multiple-tabs`将自动从`cookie`中销毁,
|
||||
* 再次打开浏览器需要重新登录系统
|
||||
* */
|
||||
export const multipleTabsKey = "multiple-tabs";
|
||||
|
||||
/** 获取`token` */
|
||||
export function getToken(): DataInfo<number> {
|
||||
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
|
||||
return Cookies.get(TokenKey)
|
||||
? JSON.parse(Cookies.get(TokenKey))
|
||||
: storageSession().getItem(sessionKey);
|
||||
: storageLocal().getItem(userKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 设置`token`以及一些必要信息并采用无感刷新`token`方案
|
||||
* 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间)
|
||||
* 将`accessToken`、`expires`这两条信息放在key值为authorized-token的cookie里(过期自动销毁)
|
||||
* 将`username`、`roles`、`refreshToken`、`expires`这四条信息放在key值为`user-info`的sessionStorage里(浏览器关闭自动销毁)
|
||||
* 将`username`、`roles`、`refreshToken`、`expires`这四条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
|
||||
*/
|
||||
export function setToken(data: DataInfo<Date>) {
|
||||
let expires = 0;
|
||||
const { accessToken, refreshToken } = data;
|
||||
const { isRemembered, loginDay } = useUserStoreHook();
|
||||
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
|
||||
const cookieString = JSON.stringify({ accessToken, expires });
|
||||
|
||||
@@ -44,10 +52,20 @@ export function setToken(data: DataInfo<Date>) {
|
||||
})
|
||||
: Cookies.set(TokenKey, cookieString);
|
||||
|
||||
function setSessionKey(username: string, roles: Array<string>) {
|
||||
Cookies.set(
|
||||
multipleTabsKey,
|
||||
"true",
|
||||
isRemembered
|
||||
? {
|
||||
expires: loginDay
|
||||
}
|
||||
: {}
|
||||
);
|
||||
|
||||
function setUserKey(username: string, roles: Array<string>) {
|
||||
useUserStoreHook().SET_USERNAME(username);
|
||||
useUserStoreHook().SET_ROLES(roles);
|
||||
storageSession().setItem(sessionKey, {
|
||||
storageLocal().setItem(userKey, {
|
||||
refreshToken,
|
||||
expires,
|
||||
username,
|
||||
@@ -57,20 +75,21 @@ export function setToken(data: DataInfo<Date>) {
|
||||
|
||||
if (data.username && data.roles) {
|
||||
const { username, roles } = data;
|
||||
setSessionKey(username, roles);
|
||||
setUserKey(username, roles);
|
||||
} else {
|
||||
const username =
|
||||
storageSession().getItem<DataInfo<number>>(sessionKey)?.username ?? "";
|
||||
storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "";
|
||||
const roles =
|
||||
storageSession().getItem<DataInfo<number>>(sessionKey)?.roles ?? [];
|
||||
setSessionKey(username, roles);
|
||||
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
|
||||
setUserKey(username, roles);
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除`token`以及key值为`user-info`的session信息 */
|
||||
/** 删除`token`以及key值为`user-info`的localStorage信息 */
|
||||
export function removeToken() {
|
||||
Cookies.remove(TokenKey);
|
||||
sessionStorage.clear();
|
||||
Cookies.remove(multipleTabsKey);
|
||||
storageLocal().removeItem(userKey);
|
||||
}
|
||||
|
||||
/** 格式化token(jwt格式) */
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Axios, {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
CustomParamsSerializer
|
||||
type AxiosInstance,
|
||||
type AxiosRequestConfig,
|
||||
type CustomParamsSerializer
|
||||
} from "axios";
|
||||
import {
|
||||
import type {
|
||||
PureHttpError,
|
||||
RequestMethods,
|
||||
PureHttpResponse,
|
||||
@@ -73,8 +73,8 @@ class PureHttp {
|
||||
return config;
|
||||
}
|
||||
/** 请求白名单,放置一些不需要token的接口(通过设置请求白名单,防止token过期后再请求造成的死循环问题) */
|
||||
const whiteList = ["/refreshToken", "/login"];
|
||||
return whiteList.some(v => config.url.indexOf(v) > -1)
|
||||
const whiteList = ["/refresh-token", "/login"];
|
||||
return whiteList.find(url => url === config.url)
|
||||
? config
|
||||
: new Promise(resolve => {
|
||||
const data = getToken();
|
||||
|
||||
2
src/utils/http/types.d.ts
vendored
2
src/utils/http/types.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import Axios, {
|
||||
import type {
|
||||
Method,
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
|
||||
109
src/utils/localforage/index.ts
Normal file
109
src/utils/localforage/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import forage from "localforage";
|
||||
import type { LocalForage, ProxyStorage, ExpiresData } from "./types.d";
|
||||
|
||||
class StorageProxy implements ProxyStorage {
|
||||
protected storage: LocalForage;
|
||||
constructor(storageModel) {
|
||||
this.storage = storageModel;
|
||||
this.storage.config({
|
||||
// 首选IndexedDB作为第一驱动,不支持IndexedDB会自动降级到localStorage(WebSQL被弃用,详情看https://developer.chrome.com/blog/deprecating-web-sql)
|
||||
driver: [this.storage.INDEXEDDB, this.storage.LOCALSTORAGE],
|
||||
name: "pure-admin"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 将对应键名的数据保存到离线仓库
|
||||
* @param k 键名
|
||||
* @param v 键值
|
||||
* @param m 缓存时间(单位`分`,默认`0`分钟,永久缓存)
|
||||
*/
|
||||
public async setItem<T>(k: string, v: T, m = 0): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.storage
|
||||
.setItem(k, {
|
||||
data: v,
|
||||
expires: m ? new Date().getTime() + m * 60 * 1000 : 0
|
||||
})
|
||||
.then(value => {
|
||||
resolve(value.data);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从离线仓库中获取对应键名的值
|
||||
* @param k 键名
|
||||
*/
|
||||
public async getItem<T>(k: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.storage
|
||||
.getItem(k)
|
||||
.then((value: ExpiresData<T>) => {
|
||||
value && (value.expires > new Date().getTime() || value.expires === 0)
|
||||
? resolve(value.data)
|
||||
: resolve(null);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从离线仓库中删除对应键名的值
|
||||
* @param k 键名
|
||||
*/
|
||||
public async removeItem(k: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.storage
|
||||
.removeItem(k)
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从离线仓库中删除所有的键名,重置数据库
|
||||
*/
|
||||
public async clear() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.storage
|
||||
.clear()
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取数据仓库中所有的key
|
||||
*/
|
||||
public async keys() {
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
this.storage
|
||||
.keys()
|
||||
.then(keys => {
|
||||
resolve(keys);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 二次封装 [localforage](https://localforage.docschina.org/) 支持设置过期时间,提供完整的类型提示
|
||||
*/
|
||||
export const localForage = () => new StorageProxy(forage);
|
||||
166
src/utils/localforage/types.d.ts
vendored
Normal file
166
src/utils/localforage/types.d.ts
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
// https://github.com/localForage/localForage/blob/master/typings/localforage.d.ts
|
||||
|
||||
interface LocalForageDbInstanceOptions {
|
||||
name?: string;
|
||||
|
||||
storeName?: string;
|
||||
}
|
||||
|
||||
interface LocalForageOptions extends LocalForageDbInstanceOptions {
|
||||
driver?: string | string[];
|
||||
|
||||
size?: number;
|
||||
|
||||
version?: number;
|
||||
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface LocalForageDbMethodsCore {
|
||||
getItem<T>(
|
||||
key: string,
|
||||
callback?: (err: any, value: T | null) => void
|
||||
): Promise<T | null>;
|
||||
|
||||
setItem<T>(
|
||||
key: string,
|
||||
value: T,
|
||||
callback?: (err: any, value: T) => void
|
||||
): Promise<T>;
|
||||
|
||||
removeItem(key: string, callback?: (err: any) => void): Promise<void>;
|
||||
|
||||
clear(callback?: (err: any) => void): Promise<void>;
|
||||
|
||||
length(callback?: (err: any, numberOfKeys: number) => void): Promise<number>;
|
||||
|
||||
key(
|
||||
keyIndex: number,
|
||||
callback?: (err: any, key: string) => void
|
||||
): Promise<string>;
|
||||
|
||||
keys(callback?: (err: any, keys: string[]) => void): Promise<string[]>;
|
||||
|
||||
iterate<T, U>(
|
||||
iteratee: (value: T, key: string, iterationNumber: number) => U,
|
||||
callback?: (err: any, result: U) => void
|
||||
): Promise<U>;
|
||||
}
|
||||
|
||||
interface LocalForageDropInstanceFn {
|
||||
(
|
||||
dbInstanceOptions?: LocalForageDbInstanceOptions,
|
||||
callback?: (err: any) => void
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
interface LocalForageDriverMethodsOptional {
|
||||
dropInstance?: LocalForageDropInstanceFn;
|
||||
}
|
||||
|
||||
// duplicating LocalForageDriverMethodsOptional to preserve TS v2.0 support,
|
||||
// since Partial<> isn't supported there
|
||||
interface LocalForageDbMethodsOptional {
|
||||
dropInstance: LocalForageDropInstanceFn;
|
||||
}
|
||||
|
||||
interface LocalForageDriverDbMethods
|
||||
extends LocalForageDbMethodsCore,
|
||||
LocalForageDriverMethodsOptional {}
|
||||
|
||||
interface LocalForageDriverSupportFunc {
|
||||
(): Promise<boolean>;
|
||||
}
|
||||
|
||||
interface LocalForageDriver extends LocalForageDriverDbMethods {
|
||||
_driver: string;
|
||||
|
||||
_initStorage(options: LocalForageOptions): void;
|
||||
|
||||
_support?: boolean | LocalForageDriverSupportFunc;
|
||||
}
|
||||
|
||||
interface LocalForageSerializer {
|
||||
serialize<T>(
|
||||
value: T | ArrayBuffer | Blob,
|
||||
callback: (value: string, error: any) => void
|
||||
): void;
|
||||
|
||||
deserialize<T>(value: string): T | ArrayBuffer | Blob;
|
||||
|
||||
stringToBuffer(serializedString: string): ArrayBuffer;
|
||||
|
||||
bufferToString(buffer: ArrayBuffer): string;
|
||||
}
|
||||
|
||||
interface LocalForageDbMethods
|
||||
extends LocalForageDbMethodsCore,
|
||||
LocalForageDbMethodsOptional {}
|
||||
|
||||
export interface LocalForage extends LocalForageDbMethods {
|
||||
LOCALSTORAGE: string;
|
||||
WEBSQL: string;
|
||||
INDEXEDDB: string;
|
||||
|
||||
/**
|
||||
* Set and persist localForage options. This must be called before any other calls to localForage are made, but can be called after localForage is loaded.
|
||||
* If you set any config values with this method they will persist after driver changes, so you can call config() then setDriver()
|
||||
* @param {LocalForageOptions} options?
|
||||
*/
|
||||
config(options: LocalForageOptions): boolean;
|
||||
config(options: string): any;
|
||||
config(): LocalForageOptions;
|
||||
|
||||
/**
|
||||
* Create a new instance of localForage to point to a different store.
|
||||
* All the configuration options used by config are supported.
|
||||
* @param {LocalForageOptions} options
|
||||
*/
|
||||
createInstance(options: LocalForageOptions): LocalForage;
|
||||
|
||||
driver(): string;
|
||||
|
||||
/**
|
||||
* Force usage of a particular driver or drivers, if available.
|
||||
* @param {string} driver
|
||||
*/
|
||||
setDriver(
|
||||
driver: string | string[],
|
||||
callback?: () => void,
|
||||
errorCallback?: (error: any) => void
|
||||
): Promise<void>;
|
||||
|
||||
defineDriver(
|
||||
driver: LocalForageDriver,
|
||||
callback?: () => void,
|
||||
errorCallback?: (error: any) => void
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Return a particular driver
|
||||
* @param {string} driver
|
||||
*/
|
||||
getDriver(driver: string): Promise<LocalForageDriver>;
|
||||
|
||||
getSerializer(
|
||||
callback?: (serializer: LocalForageSerializer) => void
|
||||
): Promise<LocalForageSerializer>;
|
||||
|
||||
supports(driverName: string): boolean;
|
||||
|
||||
ready(callback?: (error: any) => void): Promise<void>;
|
||||
}
|
||||
|
||||
// Customize
|
||||
|
||||
export interface ProxyStorage {
|
||||
setItem<T>(k: string, v: T, m: number): Promise<T>;
|
||||
getItem<T>(k: string): Promise<T>;
|
||||
removeItem(k: string): Promise<void>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ExpiresData<T> {
|
||||
data: T;
|
||||
expires: number;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type VNode } from "vue";
|
||||
import type { VNode } from "vue";
|
||||
import { isFunction } from "@pureadmin/utils";
|
||||
import { type MessageHandler, ElMessage } from "element-plus";
|
||||
|
||||
|
||||
28
src/utils/preventDefault.ts
Normal file
28
src/utils/preventDefault.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
|
||||
/** 是否为`img`标签 */
|
||||
function isImgElement(element) {
|
||||
return typeof HTMLImageElement !== "undefined"
|
||||
? element instanceof HTMLImageElement
|
||||
: element.tagName.toLowerCase() === "img";
|
||||
}
|
||||
|
||||
// 在 src/main.ts 引入并调用即可 import { addPreventDefault } from "@/utils/preventDefault"; addPreventDefault();
|
||||
export const addPreventDefault = () => {
|
||||
// 阻止通过键盘F12快捷键打开浏览器开发者工具面板
|
||||
useEventListener(
|
||||
window.document,
|
||||
"keydown",
|
||||
ev => ev.key === "F12" && ev.preventDefault()
|
||||
);
|
||||
// 阻止浏览器默认的右键菜单弹出(不会影响自定义右键事件)
|
||||
useEventListener(window.document, "contextmenu", ev => ev.preventDefault());
|
||||
// 阻止页面元素选中
|
||||
useEventListener(window.document, "selectstart", ev => ev.preventDefault());
|
||||
// 浏览器中图片通常默认是可拖动的,并且可以在新标签页或窗口中打开,或者将其拖动到其他应用程序中,此处将其禁用,使其默认不可拖动
|
||||
useEventListener(
|
||||
window.document,
|
||||
"dragstart",
|
||||
ev => isImgElement(ev?.target) && ev.preventDefault()
|
||||
);
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import type { CSSProperties, VNodeChild } from "vue";
|
||||
import {
|
||||
createTypes,
|
||||
toValidableType,
|
||||
VueTypesInterface,
|
||||
VueTypeValidableDef
|
||||
type VueTypesInterface,
|
||||
type VueTypeValidableDef
|
||||
} from "vue-types";
|
||||
|
||||
export type VueNode = VNodeChild | JSX.Element;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// 响应式storage
|
||||
import { App } from "vue";
|
||||
import type { App } from "vue";
|
||||
import Storage from "responsive-storage";
|
||||
import { routerArrays } from "@/layout/types";
|
||||
import { responsiveStorageNameSpace } from "@/config";
|
||||
|
||||
export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
|
||||
export const injectResponsiveStorage = (app: App, config: PlatformConfigs) => {
|
||||
const nameSpace = responsiveStorageNameSpace();
|
||||
const configObj = Object.assign(
|
||||
{
|
||||
@@ -15,15 +15,19 @@ export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
|
||||
// layout模式以及主题
|
||||
layout: Storage.getData("layout", nameSpace) ?? {
|
||||
layout: config.Layout ?? "vertical",
|
||||
theme: config.Theme ?? "default",
|
||||
theme: config.Theme ?? "light",
|
||||
darkMode: config.DarkMode ?? false,
|
||||
sidebarStatus: config.SidebarStatus ?? true,
|
||||
epThemeColor: config.EpThemeColor ?? "#409EFF"
|
||||
epThemeColor: config.EpThemeColor ?? "#409EFF",
|
||||
themeColor: config.Theme ?? "light", // 主题色(对应项目配置中的主题色,与theme不同的是它不会受到浅色、深色整体风格切换的影响,只会在手动点击主题色时改变)
|
||||
overallStyle: config.OverallStyle ?? "light" // 整体风格(浅色:light、深色:dark、自动:system)
|
||||
},
|
||||
// 项目配置-界面显示
|
||||
configure: Storage.getData("configure", nameSpace) ?? {
|
||||
grey: config.Grey ?? false,
|
||||
weak: config.Weak ?? false,
|
||||
hideTabs: config.HideTabs ?? false,
|
||||
hideFooter: config.HideFooter ?? true,
|
||||
showLogo: config.ShowLogo ?? true,
|
||||
showModel: config.ShowModel ?? "smart",
|
||||
multiTagsCache: config.MultiTagsCache ?? false
|
||||
|
||||
@@ -2,7 +2,7 @@ import { removeToken, setToken, type DataInfo } from "./auth";
|
||||
import { subBefore, getQueryMap } from "@pureadmin/utils";
|
||||
|
||||
/**
|
||||
* 简版前端单点登录,根据实际业务自行编写
|
||||
* 简版前端单点登录,根据实际业务自行编写,平台启动后本地可以跳后面这个链接进行测试 http://localhost:8848/#/permission/page/index?username=sso&roles=admin&accessToken=eyJhbGciOiJIUzUxMiJ9.admin
|
||||
* 划重点:
|
||||
* 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理
|
||||
* 1.清空本地旧信息;
|
||||
@@ -40,8 +40,8 @@ import { subBefore, getQueryMap } from "@pureadmin/utils";
|
||||
setToken(params);
|
||||
|
||||
// 删除不需要显示在 url 的参数
|
||||
delete params["roles"];
|
||||
delete params["accessToken"];
|
||||
delete params.roles;
|
||||
delete params.accessToken;
|
||||
|
||||
const newUrl = `${location.origin}${location.pathname}${subBefore(
|
||||
location.hash,
|
||||
|
||||
Reference in New Issue
Block a user