feat: 🚀 登录完成
This commit is contained in:
parent
33fc048aab
commit
86ec14cae5
|
@ -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();
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/** 格式化token(jwt格式) */
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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 } } }]]);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in New Issue