feat: 🚀 登录完成

This commit is contained in:
Bunny 2024-09-26 22:05:24 +08:00
parent 33fc048aab
commit 86ec14cae5
9 changed files with 574 additions and 765 deletions

View File

@ -1,200 +1,174 @@
import Axios, {
type AxiosInstance,
type AxiosRequestConfig,
type CustomParamsSerializer
} from "axios";
import type {
PureHttpError,
PureHttpRequestConfig,
PureHttpResponse,
RequestMethods
} from "./types";
import { stringify } from "qs";
import NProgress from "@/utils/progress";
import { formatToken, getToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
import { message } from "@/utils/message";
import Axios, { type AxiosInstance, type AxiosRequestConfig, type CustomParamsSerializer } from 'axios';
import type { PureHttpError, PureHttpRequestConfig, PureHttpResponse, RequestMethods } from './types';
import { stringify } from 'qs';
import NProgress from '@/utils/progress';
import { formatToken, getToken } from '@/utils/auth';
import { useUserStoreHook } from '@/store/modules/user';
import { message } from '@/utils/message';
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
// 默认请求地址
baseURL: import.meta.env.VITE_BASE_API,
// 设置超时时间
timeout: import.meta.env.VITE_BASE_API_TIMEOUT,
// @ts-expect-error
retry: import.meta.env.VITE_BASE_API_RETRY, //设置全局重试请求次数(最多重试几次请求)
retryDelay: import.meta.env.VITE_BASE_API_RETRY_DELAY, //设置全局请求间隔
// 跨域允许携带凭证
// withCredentials: true,
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
// 数组格式参数序列化https://github.com/axios/axios/issues/5142
paramsSerializer: {
serialize: stringify as unknown as CustomParamsSerializer
}
// 默认请求地址
baseURL: import.meta.env.VITE_BASE_API,
// 设置超时时间
timeout: import.meta.env.VITE_BASE_API_TIMEOUT,
// @ts-expect-error
retry: import.meta.env.VITE_BASE_API_RETRY, //设置全局重试请求次数(最多重试几次请求)
retryDelay: import.meta.env.VITE_BASE_API_RETRY_DELAY, //设置全局请求间隔
// 跨域允许携带凭证
// withCredentials: true,
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
// 数组格式参数序列化https://github.com/axios/axios/issues/5142
paramsSerializer: {
serialize: stringify as unknown as CustomParamsSerializer,
},
};
class PureHttp {
/** `token`过期后,暂存待执行的请求 */
private static requests = [];
/** 防止重复刷新`token` */
private static isRefreshing = false;
/** 初始化配置对象 */
private static initConfig: PureHttpRequestConfig = {};
/** 保存当前`Axios`实例对象 */
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
/** `token`过期后,暂存待执行的请求 */
private static requests = [];
/** 防止重复刷新`token` */
private static isRefreshing = false;
/** 初始化配置对象 */
private static initConfig: PureHttpRequestConfig = {};
/** 保存当前`Axios`实例对象 */
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
constructor() {
this.httpInterceptorsRequest();
this.httpInterceptorsResponse();
}
constructor() {
this.httpInterceptorsRequest();
this.httpInterceptorsResponse();
}
/** 重连原始请求 */
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
config.headers["token"] = formatToken(token);
resolve(config);
});
});
}
/** 重连原始请求 */
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
config.headers['token'] = formatToken(token);
resolve(config);
});
});
}
/** 通用请求工具函数 */
public request<T>(
method: RequestMethods,
url: string,
param?: AxiosRequestConfig,
axiosConfig?: PureHttpRequestConfig
): Promise<T> {
const config = {
method,
url,
...param,
...axiosConfig
} as PureHttpRequestConfig;
/** 通用请求工具函数 */
public request<T>(method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: PureHttpRequestConfig): Promise<T> {
const config = {
method,
url,
...param,
...axiosConfig,
} as PureHttpRequestConfig;
// 单独处理自定义请求/响应回调
return new Promise((resolve, reject) => {
PureHttp.axiosInstance
.request(config)
.then((response: undefined) => {
resolve(response);
})
.catch(error => {
reject(error);
});
});
}
// 单独处理自定义请求/响应回调
return new Promise((resolve, reject) => {
PureHttp.axiosInstance
.request(config)
.then((response: undefined) => {
resolve(response);
})
.catch(error => {
reject(error);
});
});
}
/** 单独抽离的`post`工具函数 */
public post<T, P>(
url: string,
params?: AxiosRequestConfig<P>,
config?: PureHttpRequestConfig
): Promise<T> {
return this.request<T>("post", url, params, config);
}
/** 单独抽离的`post`工具函数 */
public post<T, P>(url: string, params?: AxiosRequestConfig<P>, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>('post', url, params, config);
}
/** 单独抽离的`get`工具函数 */
public get<T, P>(
url: string,
params?: AxiosRequestConfig<P>,
config?: PureHttpRequestConfig
): Promise<T> {
return this.request<T>("get", url, params, config);
}
/** 单独抽离的`get`工具函数 */
public get<T, P>(url: string, params?: AxiosRequestConfig<P>, config?: PureHttpRequestConfig): Promise<T> {
return this.request<T>('get', url, params, config);
}
/** 请求拦截 */
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback(config);
return config;
}
if (PureHttp.initConfig.beforeRequestCallback) {
PureHttp.initConfig.beforeRequestCallback(config);
return config;
}
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
const whiteList = ["/refresh-token", "/login"];
return whiteList.some(url => config.url.endsWith(url))
? config
: new Promise(resolve => {
const data = getToken();
if (data) {
const now = new Date().getTime();
const expired = parseInt(data.expires) - now <= 0;
if (expired) {
if (!PureHttp.isRefreshing) {
PureHttp.isRefreshing = true;
// token过期刷新
useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken })
.then((res: any) => {
const token = res.data.accessToken;
config.headers["Authorization"] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token));
PureHttp.requests = [];
})
.finally(() => {
PureHttp.isRefreshing = false;
});
}
resolve(PureHttp.retryOriginalRequest(config));
} else {
config.headers["Authorization"] = formatToken(
data.accessToken
);
resolve(config);
}
} else {
resolve(config);
}
});
},
error => {
return Promise.reject(error);
}
);
}
/** 请求拦截 */
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof config.beforeRequestCallback === 'function') {
config.beforeRequestCallback(config);
return config;
}
if (PureHttp.initConfig.beforeRequestCallback) {
PureHttp.initConfig.beforeRequestCallback(config);
return config;
}
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
const whiteList = ['/refresh-token', '/login'];
return whiteList.some(url => config.url.endsWith(url))
? config
: new Promise(resolve => {
const data = getToken();
if (data) {
const now = new Date().getTime();
const expired = parseInt(data.expires) - now <= 0;
if (expired) {
if (!PureHttp.isRefreshing) {
PureHttp.isRefreshing = true;
// token过期刷新
useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken })
.then((res: any) => {
const token = res.data.accessToken;
config.headers['token'] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token));
PureHttp.requests = [];
})
.finally(() => {
PureHttp.isRefreshing = false;
});
}
resolve(PureHttp.retryOriginalRequest(config));
} else {
config.headers['token'] = formatToken(data.accessToken);
resolve(config);
}
} else {
resolve(config);
}
});
},
error => error,
);
}
/** 响应拦截 */
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response);
return response.data;
}
if (PureHttp.initConfig.beforeResponseCallback) {
PureHttp.initConfig.beforeResponseCallback(response);
return response.data;
}
return response.data;
},
(error: PureHttpError) => {
const $error = error;
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
NProgress.done();
message(error.message, { type: "error" });
// 所有的响应异常 区分来源为取消请求/非取消请求
return $error;
}
);
}
/** 响应拦截 */
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof $config.beforeResponseCallback === 'function') {
$config.beforeResponseCallback(response);
return response.data;
}
if (PureHttp.initConfig.beforeResponseCallback) {
PureHttp.initConfig.beforeResponseCallback(response);
return response.data;
}
return response.data;
},
(error: PureHttpError) => {
const $error = error;
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
NProgress.done();
message(error.message, { type: 'error' });
// 所有的响应异常 区分来源为取消请求/非取消请求
return $error;
},
);
}
}
export const http = new PureHttp();

View File

@ -1,37 +1,37 @@
import { http } from "@/api/service";
import type { BaseResult } from "@/types/common/BaseResult";
import { http } from '@/api/service';
import type { BaseResult } from '@/types/common/BaseResult';
export interface UserResult {
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
}
export interface RefreshTokenResult {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
}
/** 登录 */
export const fetchLogin = (data?: object) => {
return http.request<BaseResult<UserResult>>("post", "/login", { data });
return http.request<BaseResult<UserResult>>('post', '/login', { data });
};
/**
@ -39,19 +39,10 @@ export const fetchLogin = (data?: object) => {
* @param data
*/
export const fetchPostEmailCode = (data: any) => {
return http.request<BaseResult<any>>(
"post",
"/user/noAuth/sendLoginEmail",
{ data },
{ headers: { "Content-Type": "multipart/form-data" } }
);
return http.request<BaseResult<any>>('post', '/user/noAuth/sendLoginEmail', { data }, { headers: { 'Content-Type': 'multipart/form-data' } });
};
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<BaseResult<RefreshTokenResult>>(
"post",
"/refresh-token",
{ data }
);
return http.request<BaseResult<RefreshTokenResult>>('post', '/refresh-token', { data });
};

View File

@ -1,131 +1,90 @@
import { defineStore } from "pinia";
import {
resetRouter,
router,
routerArrays,
storageLocal,
store,
type userType
} from "../utils";
import {
fetchLogin,
fetchPostEmailCode,
refreshTokenApi,
type RefreshTokenResult
} from "@/api/v1/user";
import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, removeToken, setToken, userKey } from "@/utils/auth";
import { message } from "@/utils/message";
import { defineStore } from 'pinia';
import { resetRouter, router, routerArrays, storageLocal, store, type userType } from '../utils';
import { fetchLogin, fetchPostEmailCode, refreshTokenApi, type RefreshTokenResult } from '@/api/v1/user';
import { useMultiTagsStoreHook } from './multiTags';
import { type DataInfo, removeToken, setToken, userKey } from '@/utils/auth';
import { message } from '@/utils/message';
export const useUserStore = defineStore({
id: "system-user",
state: (): userType => ({
// 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
// 用户名
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "",
// 昵称
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "",
// 页面级别权限
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
// 按钮级别权限
permissions:
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [],
// 是否勾选了登录页的免登录
isRemembered: false,
// 登录页的免登录存储几天默认7天
loginDay: 7
}),
actions: {
/** 存储头像 */
SET_AVATAR(avatar: string) {
this.avatar = avatar;
},
/** 存储用户名 */
SET_USERNAME(username: string) {
this.username = username;
},
/** 存储昵称 */
SET_NICKNAME(nickname: string) {
this.nickname = nickname;
},
/** 存储角色 */
SET_ROLES(roles: Array<string>) {
this.roles = roles;
},
/** 存储按钮级别权限 */
SET_PERMS(permissions: Array<string>) {
this.permissions = permissions;
},
/** 存储是否勾选了登录页的免登录 */
SET_ISREMEMBERED(bool: boolean) {
this.isRemembered = bool;
},
/** 设置登录页的免登录存储几天 */
SET_LOGINDAY(value: number) {
this.loginDay = Number(value);
},
/** 登入 */
async loginByUsername(data: any) {
const result = await fetchLogin(data);
id: 'system-user',
state: (): userType => ({
// 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? '',
// 用户名
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? '',
// 昵称
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? '',
// 页面级别权限
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
// 按钮级别权限
permissions: storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [],
// 是否勾选了登录页的免登录
isRemembered: false,
// 登录页的免登录存储几天默认7天
loginDay: 7,
}),
actions: {
/** 登入 */
async loginByUsername(data: any) {
const result = await fetchLogin(data);
if (result.code === 200) {
setToken(data.data);
return true;
}
if (result.code === 200) {
setToken(result.data);
return true;
}
message(result.message, { type: "error" });
return false;
},
message(result.message, { type: 'error' });
return false;
},
/**
* *
* @param email
*/
async postEmailCode(email: string) {
const response = await fetchPostEmailCode({ email });
if (response.code === 200) {
message(response.message, { type: "success" });
return true;
}
message(response.message, { type: "error" });
/**
* *
* @param email
*/
async postEmailCode(email: string) {
const response = await fetchPostEmailCode({ email });
if (response.code === 200) {
message(response.message, { type: 'success' });
return true;
}
message(response.message, { type: 'error' });
return false;
},
return false;
},
/**
*
*/
async logOut() {
this.username = "";
this.roles = [];
this.permissions = [];
removeToken();
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter();
await router.push("/login");
},
/**
*
*/
async logOut() {
this.username = '';
this.roles = [];
this.permissions = [];
removeToken();
useMultiTagsStoreHook().handleTags('equal', [...routerArrays]);
resetRouter();
await router.push('/login');
},
/**
* `token`
*/
async handRefreshToken(data) {
return new Promise<RefreshTokenResult>((resolve, reject) => {
refreshTokenApi(data)
.then((data: any) => {
if (data) {
setToken(data.data);
resolve(data);
}
})
.catch(error => {
reject(error);
});
});
}
}
/**
* `token`
*/
async handRefreshToken(data) {
return new Promise<RefreshTokenResult>((resolve, reject) => {
refreshTokenApi(data)
.then((data: any) => {
if (data) {
setToken(data.data);
resolve(data);
}
})
.catch(error => {
reject(error);
});
});
},
},
});
export function useUserStoreHook() {
return useUserStore(store);
return useUserStore(store);
}

View File

@ -1,42 +1,40 @@
import Cookies from "js-cookie";
import { useUserStoreHook } from "@/store/modules/user";
import { isIncludeAllChildren, isString, storageLocal } from "@pureadmin/utils";
import Cookies from 'js-cookie';
import { useUserStore, useUserStoreHook } from '@/store/modules/user';
import { isIncludeAllChildren, isString, storageLocal } from '@pureadmin/utils';
export interface DataInfo<T> {
/** token */
accessToken: string;
/** `accessToken`的过期时间(时间戳) */
expires: T;
/** 用于调用刷新accessToken的接口时所需的token */
refreshToken: string;
/** 头像 */
avatar?: string;
/** 用户名 */
username?: string;
/** 昵称 */
nickname?: string;
/** 当前登录用户的角色 */
roles?: Array<string>;
/** 当前登录用户的按钮级别权限 */
permissions?: Array<string>;
/** token */
token: string;
/** `accessToken`的过期时间(时间戳) */
expires: T;
/** 用于调用刷新accessToken的接口时所需的token */
refreshToken: string;
/** 头像 */
avatar?: string;
/** 用户名 */
username?: string;
/** 昵称 */
nickname?: string;
/** 当前登录用户的角色 */
roles?: Array<string>;
/** 当前登录用户的按钮级别权限 */
permissions?: Array<string>;
}
export const userKey = "user-info";
export const TokenKey = "authorized-token";
export const userKey = 'user-info';
export const TokenKey = 'authorized-token';
/**
* `multiple-tabs``cookie`
*
* `multiple-tabs``cookie`
*
* */
export const multipleTabsKey = "multiple-tabs";
export const multipleTabsKey = 'multiple-tabs';
/** 获取`token` */
export function getToken(): DataInfo<number> {
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
return Cookies.get(TokenKey)
? JSON.parse(Cookies.get(TokenKey))
: storageLocal().getItem(userKey);
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
return Cookies.get(TokenKey) ? JSON.parse(Cookies.get(TokenKey)) : storageLocal().getItem(userKey);
}
/**
@ -45,97 +43,65 @@ export function getToken(): DataInfo<number> {
* `accessToken``expires``refreshToken`key值为authorized-token的cookie里
* `avatar``username``nickname``roles``permissions``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, refreshToken });
export function setToken(data: any) {
const userStore = useUserStore();
let expires = 0;
const { token, refreshToken } = data;
const { isRemembered, loginDay } = useUserStoreHook();
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳将此处代码改为expires = data.expires然后把上面的DataInfo<Date>改成DataInfo<number>即可
const cookieString = JSON.stringify({ token, expires, refreshToken });
expires > 0
? Cookies.set(TokenKey, cookieString, {
expires: (expires - Date.now()) / 86400000
})
: Cookies.set(TokenKey, cookieString);
expires > 0 ? Cookies.set(TokenKey, cookieString, { expires: (expires - Date.now()) / 86400000 }) : Cookies.set(TokenKey, cookieString);
Cookies.set(
multipleTabsKey,
"true",
isRemembered
? {
expires: loginDay
}
: {}
);
Cookies.set(multipleTabsKey, 'true', isRemembered ? { expires: loginDay } : {});
function setUserKey({ avatar, username, nickname, roles, permissions }) {
useUserStoreHook().SET_AVATAR(avatar);
useUserStoreHook().SET_USERNAME(username);
useUserStoreHook().SET_NICKNAME(nickname);
useUserStoreHook().SET_ROLES(roles);
useUserStoreHook().SET_PERMS(permissions);
storageLocal().setItem(userKey, {
refreshToken,
expires,
avatar,
username,
nickname,
roles,
permissions
});
}
function setUserKey({ avatar, username, nickname, roles, permissions }) {
userStore.avatar = avatar;
userStore.username = username;
userStore.nickname = nickname;
userStore.roles = roles;
userStore.permissions = permissions;
if (data.username && data.roles) {
const { username, roles } = data;
setUserKey({
avatar: data?.avatar ?? "",
username,
nickname: data?.nickname ?? "",
roles,
permissions: data?.permissions ?? []
});
} else {
const avatar =
storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "";
const username =
storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "";
const nickname =
storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "";
const roles =
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
const permissions =
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
setUserKey({
avatar,
username,
nickname,
roles,
permissions
});
}
storageLocal().setItem(userKey, { refreshToken, expires, avatar, username, nickname, roles, permissions });
}
if (data.username && data.roles) {
const { username, roles } = data;
setUserKey({
avatar: data?.avatar ?? '',
username,
nickname: data?.nickname ?? '',
roles,
permissions: data?.permissions ?? [],
});
} else {
const avatar = storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? '';
const username = storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? '';
const nickname = storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? '';
const roles = storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
const permissions = storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
setUserKey({ avatar, username, nickname, roles, permissions });
}
}
/** 删除`token`以及key值为`user-info`的localStorage信息 */
export function removeToken() {
Cookies.remove(TokenKey);
Cookies.remove(multipleTabsKey);
storageLocal().removeItem(userKey);
Cookies.remove(TokenKey);
Cookies.remove(multipleTabsKey);
storageLocal().removeItem(userKey);
}
/** 格式化tokenjwt格式 */
export const formatToken = (token: string): string => {
return "Bearer " + token;
return 'Bearer ' + token;
};
/** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/
export const hasPerms = (value: string | Array<string>): boolean => {
if (!value) return false;
const allPerms = "*:*:*";
const { permissions } = useUserStoreHook();
if (!permissions) return false;
if (permissions.length === 1 && permissions[0] === allPerms) return true;
const isAuths = isString(value)
? permissions.includes(value)
: isIncludeAllChildren(value, permissions);
return isAuths ? true : false;
if (!value) return false;
const allPerms = '*:*:*';
const { permissions } = useUserStoreHook();
if (!permissions) return false;
if (permissions.length === 1 && permissions[0] === allPerms) return true;
return isString(value) ? permissions.includes(value) : isIncludeAllChildren(value, permissions);
};

View File

@ -1,297 +1,99 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import Motion from "./utils/motion";
import { useRouter } from "vue-router";
import { message } from "@/utils/message";
import { loginRules } from "./utils/rule";
import { useNav } from "@/layout/hooks/useNav";
import type { FormInstance } from "element-plus";
import { $t } from "@/plugins/i18n";
import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStore } from "@/store/modules/user";
import { avatar, bg, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/CommonIcon/src/hooks";
import { onBeforeUnmount, onMounted, reactive, ref, toRaw } from "vue";
import { useTranslationLang } from "@/layout/hooks/useTranslationLang";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import Motion from './utils/motion';
import { useNav } from '@/layout/hooks/useNav';
import { useLayout } from '@/layout/hooks/useLayout';
import { avatar, bg, illustration } from './utils/static';
import { toRaw } from 'vue';
import { useTranslationLang } from '@/layout/hooks/useTranslationLang';
import { useDataThemeChange } from '@/layout/hooks/useDataThemeChange';
import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component";
import globalization from "@/assets/svg/globalization.svg?component";
import Lock from "@iconify-icons/ri/lock-fill";
import Check from "@iconify-icons/ep/check";
import User from "@iconify-icons/ri/user-3-fill";
import { getTopMenu, initRouter } from "@/router/utils";
import dayIcon from '@/assets/svg/day.svg?component';
import darkIcon from '@/assets/svg/dark.svg?component';
import globalization from '@/assets/svg/globalization.svg?component';
import Check from '@iconify-icons/ep/check';
import LoginForm from '@/views/login/login-form.vue';
defineOptions({
name: "Login"
name: 'Login',
});
const router = useRouter();
const loading = ref(false);
const ruleFormRef = ref<FormInstance>();
const sendSecond = ref(60);
const timer = ref(null);
const { initStorage } = useLayout();
initStorage();
const { t } = useI18n();
const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
dataThemeChange(overallStyle.value);
const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
const { locale, translationCh, translationEn } = useTranslationLang();
const userStore = useUserStore();
const ruleForm = reactive({
username: "1319900154@qq.com",
password: "admin123",
emailCode: ""
});
/**
* * 发送邮箱验证码
*/
const onSendEmailCode = async () => {
//
if (ruleForm.username === "" || ruleForm.username === void 0) {
message("请先填写邮箱地址", { type: "warning" });
return false;
}
const result = await userStore.postEmailCode(ruleForm.username);
if (result) {
//
onSendEmailTimer();
}
};
/**
* * 发送邮箱倒计时点击
*/
const onSendEmailTimer = () => {
//
timer.value = setInterval(() => {
// 0
if (sendSecond.value <= 0) {
clearInterval(timer.value);
timer.value = null;
sendSecond.value = 60;
return;
}
//
sendSecond.value--;
}, 1000);
};
/**
* 登录
* @param formEl
*/
const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(async valid => {
if (valid) {
loading.value = true;
const result = await userStore.loginByUsername(ruleForm);
if (result) {
//
await initRouter();
router.push(getTopMenu(true).path).then(() => {
message(t("login.loginSuccess"), { type: "success" });
});
} else {
message(t("login.loginFail"), { type: "error" });
}
loading.value = false;
}
});
};
/** 使用公共函数,避免`removeEventListener`失效 */
function onkeypress({ code }: KeyboardEvent) {
if (["Enter", "NumpadEnter"].includes(code)) {
onLogin(ruleFormRef.value);
}
}
onMounted(() => {
window.document.addEventListener("keypress", onkeypress);
});
onBeforeUnmount(() => {
window.document.removeEventListener("keypress", onkeypress);
});
</script>
<template>
<div class="select-none">
<img :src="bg" alt="" class="wave" />
<div class="flex-c absolute right-5 top-3">
<!-- 主题 -->
<el-switch
v-model="dataTheme"
:active-icon="dayIcon"
:inactive-icon="darkIcon"
inline-prompt
@change="dataThemeChange"
/>
<!-- 国际化 -->
<el-dropdown trigger="click">
<globalization
class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
:style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh"
>
<IconifyIconOffline
v-show="locale === 'zh'"
:icon="Check"
class="check-zh"
/>
简体中文
</el-dropdown-item>
<el-dropdown-item
:class="['dark:!text-white', getDropdownItemClass(locale, 'en')]"
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
>
<span v-show="locale === 'en'" class="check-en">
<IconifyIconOffline :icon="Check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="login-container">
<div class="img">
<component :is="toRaw(illustration)" />
</div>
<div class="login-box">
<div class="login-form">
<avatar class="avatar" />
<Motion>
<h2 class="outline-none">{{ title }}</h2>
</Motion>
<div class="select-none">
<img :src="bg" alt="" class="wave" />
<div class="flex-c absolute right-5 top-3">
<!-- 主题 -->
<el-switch v-model="dataTheme" :active-icon="dayIcon" :inactive-icon="darkIcon" inline-prompt @change="dataThemeChange" />
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="loginRules"
size="large"
>
<Motion :delay="100">
<el-form-item
:rules="[
{
required: true,
message: $t('login.usernameRegex'),
trigger: 'blur'
}
]"
prop="username"
>
<el-input
v-model="ruleForm.username"
:placeholder="t('login.username')"
:prefix-icon="useRenderIcon(User)"
clearable
/>
</el-form-item>
</Motion>
<!-- 国际化 -->
<el-dropdown trigger="click">
<globalization class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300" />
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]" :style="getDropdownItemStyle(locale, 'zh')" @click="translationCh">
<IconifyIconOffline v-show="locale === 'zh'" :icon="Check" class="check-zh" />
简体中文
</el-dropdown-item>
<el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'en')]" :style="getDropdownItemStyle(locale, 'en')" @click="translationEn">
<span v-show="locale === 'en'" class="check-en">
<IconifyIconOffline :icon="Check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="login-container">
<div class="img">
<component :is="toRaw(illustration)" />
</div>
<div class="login-box">
<div class="login-form">
<avatar class="avatar" />
<Motion>
<h2 class="outline-none">{{ title }}</h2>
</Motion>
<Motion :delay="150">
<el-form-item prop="password">
<el-input
v-model="ruleForm.password"
:placeholder="t('login.password')"
:prefix-icon="useRenderIcon(Lock)"
clearable
show-password
/>
</el-form-item>
</Motion>
<Motion :delay="150">
<el-form-item prop="emailCode">
<el-input
v-model="ruleForm.emailCode"
:placeholder="t('login.emailCode')"
:prefix-icon="useRenderIcon('ic:outline-email')"
clearable
@keydown.enter="onLogin(ruleFormRef)"
>
<template v-slot:append>
<el-link
v-if="sendSecond === 60"
:underline="false"
class="px-2"
type="primary"
@click="onSendEmailCode"
>
{{ t("login.getEmailCode") }}
</el-link>
<el-link
v-else
:underline="false"
class="px-2"
type="primary"
>
{{ sendSecond }}
{{ t("login.getCodeInfo") }}
</el-link>
</template>
</el-input>
</el-form-item>
</Motion>
<Motion :delay="250">
<el-button
:loading="loading"
class="w-full mt-4"
size="default"
type="primary"
@click="onLogin(ruleFormRef)"
>
{{ t("login.login") }}
</el-button>
</Motion>
</el-form>
</div>
</div>
</div>
</div>
<!-- 登录表单 -->
<login-form />
</div>
</div>
</div>
</div>
</template>
<style scoped>
@import url("@/style/login.css");
@import url('@/style/login.css');
</style>
<style lang="scss" scoped>
:deep(.el-input-group__append, .el-input-group__prepend) {
padding: 0;
padding: 0;
}
.translation {
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;
}
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;
}
.check-zh {
position: absolute;
left: 20px;
}
.check-zh {
position: absolute;
left: 20px;
}
.check-en {
position: absolute;
left: 20px;
}
.check-en {
position: absolute;
left: 20px;
}
}
</style>

View File

@ -0,0 +1,140 @@
<script lang="ts" setup>
import { loginRules } from '@/views/login/utils/rule';
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
import User from '@iconify-icons/ri/user-3-fill';
import Lock from '@iconify-icons/ri/lock-fill';
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useUserStore } from '@/store/modules/user';
import { message } from '@/utils/message';
import { getTopMenu, initRouter } from '@/router/utils';
import Motion from './utils/motion';
import type { FormInstance } from 'element-plus';
const router = useRouter();
const userStore = useUserStore();
const ruleFormRef = ref<FormInstance>();
const loading = ref(false);
const sendSecond = ref(60);
const timer = ref(null);
const { t } = useI18n();
const ruleForm = reactive({
username: '1319900154@qq.com',
password: 'admin123',
emailCode: '',
});
/**
* * 发送邮箱验证码
*/
const onSendEmailCode = async () => {
//
if (ruleForm.username === '' || ruleForm.username === void 0) {
message('请先填写邮箱地址', { type: 'warning' });
return false;
}
const result = await userStore.postEmailCode(ruleForm.username);
if (result) {
//
onSendEmailTimer();
}
};
/**
* * 发送邮箱倒计时点击
*/
const onSendEmailTimer = () => {
//
timer.value = setInterval(() => {
// 0
if (sendSecond.value <= 0) {
clearInterval(timer.value);
timer.value = null;
sendSecond.value = 60;
return;
}
//
sendSecond.value--;
}, 1000);
};
/**
* 登录
* @param formEl
*/
const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(async valid => {
if (valid) {
loading.value = true;
const result = await userStore.loginByUsername(ruleForm);
if (result) {
//
await initRouter();
router.push(getTopMenu(true).path).then(() => {
message(t('login.loginSuccess'), { type: 'success' });
});
}
loading.value = false;
}
});
};
/** 使用公共函数,避免`removeEventListener`失效 */
function onkeypress({ code }: KeyboardEvent) {
if (['Enter', 'NumpadEnter'].includes(code)) {
onLogin(ruleFormRef.value);
}
}
onMounted(() => {
window.document.addEventListener('keypress', onkeypress);
});
onBeforeUnmount(() => {
window.document.removeEventListener('keypress', onkeypress);
});
</script>
<template>
<el-form ref="ruleFormRef" :model="ruleForm" :rules="loginRules" size="large">
<Motion :delay="100">
<el-form-item prop="username">
<el-input v-model="ruleForm.username" :placeholder="t('login.username')" :prefix-icon="useRenderIcon(User)" clearable />
</el-form-item>
</Motion>
<Motion :delay="150">
<el-form-item prop="password">
<el-input v-model="ruleForm.password" :placeholder="t('login.password')" :prefix-icon="useRenderIcon(Lock)" clearable show-password />
</el-form-item>
</Motion>
<Motion :delay="150">
<el-form-item prop="emailCode">
<el-input v-model="ruleForm.emailCode" :placeholder="t('login.emailCode')" :prefix-icon="useRenderIcon('ic:outline-email')" clearable @keydown.enter="onLogin(ruleFormRef)">
<template v-slot:append>
<el-link v-if="sendSecond === 60" :underline="false" class="px-2" type="primary" @click="onSendEmailCode">
{{ t('login.getEmailCode') }}
</el-link>
<el-link v-else :underline="false" class="px-2" type="primary">
{{ sendSecond }}
{{ t('login.getCodeInfo') }}
</el-link>
</template>
</el-input>
</el-form-item>
</Motion>
<Motion :delay="250">
<el-button :loading="loading" class="w-full mt-4" size="default" type="primary" @click="onLogin(ruleFormRef)">
{{ t('login.login') }}
</el-button>
</Motion>
</el-form>
</template>

View File

@ -1,40 +1,17 @@
import { h, defineComponent, withDirectives, resolveDirective } from "vue";
import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
/** 封装@vueuse/motion动画库中的自定义指令v-motion */
export default defineComponent({
name: "Motion",
props: {
delay: {
type: Number,
default: 50
}
},
render() {
const { delay } = this;
const motion = resolveDirective("motion");
return withDirectives(
h(
"div",
{},
{
default: () => [this.$slots.default()]
}
),
[
[
motion,
{
initial: { opacity: 0, y: 100 },
enter: {
opacity: 1,
y: 0,
transition: {
delay
}
}
}
]
]
);
}
name: 'Motion',
props: {
delay: {
type: Number,
default: 50,
},
},
render() {
const { delay } = this;
const motion = resolveDirective('motion');
return withDirectives(h('div', {}, { default: () => [this.$slots.default()] }), [[motion, { initial: { opacity: 0, y: 100 }, enter: { opacity: 1, y: 0, transition: { delay } } }]]);
},
});

View File

@ -1,28 +1,28 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
import { $t } from "@/plugins/i18n";
import { reactive } from 'vue';
import type { FormRules } from 'element-plus';
import { $t } from '@/plugins/i18n';
/** 密码正则密码格式应为8-18位数字、字母、符号的任意两种组合 */
export const REGEXP_PWD =
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
export const REGEXP_PWD = /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
/** 登录校验 */
const loginRules = reactive(<FormRules>{
password: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error($t("login.purePassWordReg")));
} else if (!REGEXP_PWD.test(value)) {
callback(new Error($t("login.purePassWordRuleReg")));
} else {
callback();
}
},
trigger: "blur"
}
],
emailCode: [{ required: true, trigger: "blur", type: "string" }]
username: [{ required: true, message: $t('login.usernameRegex'), trigger: 'blur' }],
password: [
{
validator: (rule, value, callback) => {
if (value === '') {
callback(new Error($t('login.purePassWordReg')));
} else if (!REGEXP_PWD.test(value)) {
callback(new Error($t('login.purePassWordRuleReg')));
} else {
callback();
}
},
trigger: 'blur',
},
],
emailCode: [{ required: true, trigger: 'blur', type: 'string' }],
});
export { loginRules };

View File

@ -1,5 +1,5 @@
import bg from "@/assets/login/bg.png";
import avatar from "@/assets/login/avatar.svg?component";
import illustration from "@/assets/login/illustration.svg?component";
import bg from '@/assets/login/bg.png';
import avatar from '@/assets/login/avatar.svg?component';
import illustration from '@/assets/login/illustration.svg?component';
export { bg, avatar, illustration };