feat: 🚀 登录完成
This commit is contained in:
parent
33fc048aab
commit
86ec14cae5
|
@ -1,200 +1,174 @@
|
||||||
import Axios, {
|
import Axios, { type AxiosInstance, type AxiosRequestConfig, type CustomParamsSerializer } from 'axios';
|
||||||
type AxiosInstance,
|
import type { PureHttpError, PureHttpRequestConfig, PureHttpResponse, RequestMethods } from './types';
|
||||||
type AxiosRequestConfig,
|
import { stringify } from 'qs';
|
||||||
type CustomParamsSerializer
|
import NProgress from '@/utils/progress';
|
||||||
} from "axios";
|
import { formatToken, getToken } from '@/utils/auth';
|
||||||
import type {
|
import { useUserStoreHook } from '@/store/modules/user';
|
||||||
PureHttpError,
|
import { message } from '@/utils/message';
|
||||||
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
|
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
|
||||||
const defaultConfig: AxiosRequestConfig = {
|
const defaultConfig: AxiosRequestConfig = {
|
||||||
// 默认请求地址
|
// 默认请求地址
|
||||||
baseURL: import.meta.env.VITE_BASE_API,
|
baseURL: import.meta.env.VITE_BASE_API,
|
||||||
// 设置超时时间
|
// 设置超时时间
|
||||||
timeout: import.meta.env.VITE_BASE_API_TIMEOUT,
|
timeout: import.meta.env.VITE_BASE_API_TIMEOUT,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
retry: import.meta.env.VITE_BASE_API_RETRY, //设置全局重试请求次数(最多重试几次请求)
|
retry: import.meta.env.VITE_BASE_API_RETRY, //设置全局重试请求次数(最多重试几次请求)
|
||||||
retryDelay: import.meta.env.VITE_BASE_API_RETRY_DELAY, //设置全局请求间隔
|
retryDelay: import.meta.env.VITE_BASE_API_RETRY_DELAY, //设置全局请求间隔
|
||||||
// 跨域允许携带凭证
|
// 跨域允许携带凭证
|
||||||
// withCredentials: true,
|
// withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json, text/plain, */*",
|
Accept: 'application/json, text/plain, */*',
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"X-Requested-With": "XMLHttpRequest"
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
},
|
},
|
||||||
// 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
|
// 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
|
||||||
paramsSerializer: {
|
paramsSerializer: {
|
||||||
serialize: stringify as unknown as CustomParamsSerializer
|
serialize: stringify as unknown as CustomParamsSerializer,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class PureHttp {
|
class PureHttp {
|
||||||
/** `token`过期后,暂存待执行的请求 */
|
/** `token`过期后,暂存待执行的请求 */
|
||||||
private static requests = [];
|
private static requests = [];
|
||||||
/** 防止重复刷新`token` */
|
/** 防止重复刷新`token` */
|
||||||
private static isRefreshing = false;
|
private static isRefreshing = false;
|
||||||
/** 初始化配置对象 */
|
/** 初始化配置对象 */
|
||||||
private static initConfig: PureHttpRequestConfig = {};
|
private static initConfig: PureHttpRequestConfig = {};
|
||||||
/** 保存当前`Axios`实例对象 */
|
/** 保存当前`Axios`实例对象 */
|
||||||
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
|
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.httpInterceptorsRequest();
|
this.httpInterceptorsRequest();
|
||||||
this.httpInterceptorsResponse();
|
this.httpInterceptorsResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重连原始请求 */
|
/** 重连原始请求 */
|
||||||
private static retryOriginalRequest(config: PureHttpRequestConfig) {
|
private static retryOriginalRequest(config: PureHttpRequestConfig) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
PureHttp.requests.push((token: string) => {
|
PureHttp.requests.push((token: string) => {
|
||||||
config.headers["token"] = formatToken(token);
|
config.headers['token'] = formatToken(token);
|
||||||
resolve(config);
|
resolve(config);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用请求工具函数 */
|
/** 通用请求工具函数 */
|
||||||
public request<T>(
|
public request<T>(method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: PureHttpRequestConfig): Promise<T> {
|
||||||
method: RequestMethods,
|
const config = {
|
||||||
url: string,
|
method,
|
||||||
param?: AxiosRequestConfig,
|
url,
|
||||||
axiosConfig?: PureHttpRequestConfig
|
...param,
|
||||||
): Promise<T> {
|
...axiosConfig,
|
||||||
const config = {
|
} as PureHttpRequestConfig;
|
||||||
method,
|
|
||||||
url,
|
|
||||||
...param,
|
|
||||||
...axiosConfig
|
|
||||||
} as PureHttpRequestConfig;
|
|
||||||
|
|
||||||
// 单独处理自定义请求/响应回调
|
// 单独处理自定义请求/响应回调
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
PureHttp.axiosInstance
|
PureHttp.axiosInstance
|
||||||
.request(config)
|
.request(config)
|
||||||
.then((response: undefined) => {
|
.then((response: undefined) => {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单独抽离的`post`工具函数 */
|
/** 单独抽离的`post`工具函数 */
|
||||||
public post<T, P>(
|
public post<T, P>(url: string, params?: AxiosRequestConfig<P>, config?: PureHttpRequestConfig): Promise<T> {
|
||||||
url: string,
|
return this.request<T>('post', url, params, config);
|
||||||
params?: AxiosRequestConfig<P>,
|
}
|
||||||
config?: PureHttpRequestConfig
|
|
||||||
): Promise<T> {
|
|
||||||
return this.request<T>("post", url, params, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 单独抽离的`get`工具函数 */
|
/** 单独抽离的`get`工具函数 */
|
||||||
public get<T, P>(
|
public get<T, P>(url: string, params?: AxiosRequestConfig<P>, config?: PureHttpRequestConfig): Promise<T> {
|
||||||
url: string,
|
return this.request<T>('get', url, params, config);
|
||||||
params?: AxiosRequestConfig<P>,
|
}
|
||||||
config?: PureHttpRequestConfig
|
|
||||||
): Promise<T> {
|
|
||||||
return this.request<T>("get", url, params, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 请求拦截 */
|
/** 请求拦截 */
|
||||||
private httpInterceptorsRequest(): void {
|
private httpInterceptorsRequest(): void {
|
||||||
PureHttp.axiosInstance.interceptors.request.use(
|
PureHttp.axiosInstance.interceptors.request.use(
|
||||||
async (config: PureHttpRequestConfig): Promise<any> => {
|
async (config: PureHttpRequestConfig): Promise<any> => {
|
||||||
// 开启进度条动画
|
// 开启进度条动画
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||||
if (typeof config.beforeRequestCallback === "function") {
|
if (typeof config.beforeRequestCallback === 'function') {
|
||||||
config.beforeRequestCallback(config);
|
config.beforeRequestCallback(config);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
if (PureHttp.initConfig.beforeRequestCallback) {
|
if (PureHttp.initConfig.beforeRequestCallback) {
|
||||||
PureHttp.initConfig.beforeRequestCallback(config);
|
PureHttp.initConfig.beforeRequestCallback(config);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
|
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
|
||||||
const whiteList = ["/refresh-token", "/login"];
|
const whiteList = ['/refresh-token', '/login'];
|
||||||
return whiteList.some(url => config.url.endsWith(url))
|
return whiteList.some(url => config.url.endsWith(url))
|
||||||
? config
|
? config
|
||||||
: new Promise(resolve => {
|
: new Promise(resolve => {
|
||||||
const data = getToken();
|
const data = getToken();
|
||||||
if (data) {
|
if (data) {
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
const expired = parseInt(data.expires) - now <= 0;
|
const expired = parseInt(data.expires) - now <= 0;
|
||||||
if (expired) {
|
if (expired) {
|
||||||
if (!PureHttp.isRefreshing) {
|
if (!PureHttp.isRefreshing) {
|
||||||
PureHttp.isRefreshing = true;
|
PureHttp.isRefreshing = true;
|
||||||
// token过期刷新
|
// token过期刷新
|
||||||
useUserStoreHook()
|
useUserStoreHook()
|
||||||
.handRefreshToken({ refreshToken: data.refreshToken })
|
.handRefreshToken({ refreshToken: data.refreshToken })
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
const token = res.data.accessToken;
|
const token = res.data.accessToken;
|
||||||
config.headers["Authorization"] = formatToken(token);
|
config.headers['token'] = formatToken(token);
|
||||||
PureHttp.requests.forEach(cb => cb(token));
|
PureHttp.requests.forEach(cb => cb(token));
|
||||||
PureHttp.requests = [];
|
PureHttp.requests = [];
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
PureHttp.isRefreshing = false;
|
PureHttp.isRefreshing = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resolve(PureHttp.retryOriginalRequest(config));
|
resolve(PureHttp.retryOriginalRequest(config));
|
||||||
} else {
|
} else {
|
||||||
config.headers["Authorization"] = formatToken(
|
config.headers['token'] = formatToken(data.accessToken);
|
||||||
data.accessToken
|
resolve(config);
|
||||||
);
|
}
|
||||||
resolve(config);
|
} else {
|
||||||
}
|
resolve(config);
|
||||||
} else {
|
}
|
||||||
resolve(config);
|
});
|
||||||
}
|
},
|
||||||
});
|
error => error,
|
||||||
},
|
);
|
||||||
error => {
|
}
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 响应拦截 */
|
/** 响应拦截 */
|
||||||
private httpInterceptorsResponse(): void {
|
private httpInterceptorsResponse(): void {
|
||||||
const instance = PureHttp.axiosInstance;
|
const instance = PureHttp.axiosInstance;
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(response: PureHttpResponse) => {
|
(response: PureHttpResponse) => {
|
||||||
const $config = response.config;
|
const $config = response.config;
|
||||||
// 关闭进度条动画
|
// 关闭进度条动画
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||||
if (typeof $config.beforeResponseCallback === "function") {
|
if (typeof $config.beforeResponseCallback === 'function') {
|
||||||
$config.beforeResponseCallback(response);
|
$config.beforeResponseCallback(response);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
if (PureHttp.initConfig.beforeResponseCallback) {
|
if (PureHttp.initConfig.beforeResponseCallback) {
|
||||||
PureHttp.initConfig.beforeResponseCallback(response);
|
PureHttp.initConfig.beforeResponseCallback(response);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
(error: PureHttpError) => {
|
(error: PureHttpError) => {
|
||||||
const $error = error;
|
const $error = error;
|
||||||
$error.isCancelRequest = Axios.isCancel($error);
|
$error.isCancelRequest = Axios.isCancel($error);
|
||||||
// 关闭进度条动画
|
// 关闭进度条动画
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
message(error.message, { type: "error" });
|
message(error.message, { type: 'error' });
|
||||||
// 所有的响应异常 区分来源为取消请求/非取消请求
|
// 所有的响应异常 区分来源为取消请求/非取消请求
|
||||||
return $error;
|
return $error;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const http = new PureHttp();
|
export const http = new PureHttp();
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
import { http } from "@/api/service";
|
import { http } from '@/api/service';
|
||||||
import type { BaseResult } from "@/types/common/BaseResult";
|
import type { BaseResult } from '@/types/common/BaseResult';
|
||||||
|
|
||||||
export interface UserResult {
|
export interface UserResult {
|
||||||
/** 头像 */
|
/** 头像 */
|
||||||
avatar: string;
|
avatar: string;
|
||||||
/** 用户名 */
|
/** 用户名 */
|
||||||
username: string;
|
username: string;
|
||||||
/** 昵称 */
|
/** 昵称 */
|
||||||
nickname: string;
|
nickname: string;
|
||||||
/** 当前登录用户的角色 */
|
/** 当前登录用户的角色 */
|
||||||
roles: Array<string>;
|
roles: Array<string>;
|
||||||
/** 按钮级别权限 */
|
/** 按钮级别权限 */
|
||||||
permissions: Array<string>;
|
permissions: Array<string>;
|
||||||
/** `token` */
|
/** `token` */
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
/** 用于调用刷新`accessToken`的接口时所需的`token` */
|
/** 用于调用刷新`accessToken`的接口时所需的`token` */
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
|
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
|
||||||
expires: Date;
|
expires: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshTokenResult {
|
export interface RefreshTokenResult {
|
||||||
/** `token` */
|
/** `token` */
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
/** 用于调用刷新`accessToken`的接口时所需的`token` */
|
/** 用于调用刷新`accessToken`的接口时所需的`token` */
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
|
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
|
||||||
expires: Date;
|
expires: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录 */
|
/** 登录 */
|
||||||
export const fetchLogin = (data?: object) => {
|
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
|
* @param data
|
||||||
*/
|
*/
|
||||||
export const fetchPostEmailCode = (data: any) => {
|
export const fetchPostEmailCode = (data: any) => {
|
||||||
return http.request<BaseResult<any>>(
|
return http.request<BaseResult<any>>('post', '/user/noAuth/sendLoginEmail', { data }, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||||
"post",
|
|
||||||
"/user/noAuth/sendLoginEmail",
|
|
||||||
{ data },
|
|
||||||
{ headers: { "Content-Type": "multipart/form-data" } }
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 刷新`token` */
|
/** 刷新`token` */
|
||||||
export const refreshTokenApi = (data?: object) => {
|
export const refreshTokenApi = (data?: object) => {
|
||||||
return http.request<BaseResult<RefreshTokenResult>>(
|
return http.request<BaseResult<RefreshTokenResult>>('post', '/refresh-token', { data });
|
||||||
"post",
|
|
||||||
"/refresh-token",
|
|
||||||
{ data }
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,131 +1,90 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from 'pinia';
|
||||||
import {
|
import { resetRouter, router, routerArrays, storageLocal, store, type userType } from '../utils';
|
||||||
resetRouter,
|
import { fetchLogin, fetchPostEmailCode, refreshTokenApi, type RefreshTokenResult } from '@/api/v1/user';
|
||||||
router,
|
import { useMultiTagsStoreHook } from './multiTags';
|
||||||
routerArrays,
|
import { type DataInfo, removeToken, setToken, userKey } from '@/utils/auth';
|
||||||
storageLocal,
|
import { message } from '@/utils/message';
|
||||||
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({
|
export const useUserStore = defineStore({
|
||||||
id: "system-user",
|
id: 'system-user',
|
||||||
state: (): userType => ({
|
state: (): userType => ({
|
||||||
// 头像
|
// 头像
|
||||||
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
|
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? '',
|
||||||
// 用户名
|
// 用户名
|
||||||
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "",
|
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? '',
|
||||||
// 昵称
|
// 昵称
|
||||||
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "",
|
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? '',
|
||||||
// 页面级别权限
|
// 页面级别权限
|
||||||
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
|
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
|
||||||
// 按钮级别权限
|
// 按钮级别权限
|
||||||
permissions:
|
permissions: storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [],
|
||||||
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [],
|
// 是否勾选了登录页的免登录
|
||||||
// 是否勾选了登录页的免登录
|
isRemembered: false,
|
||||||
isRemembered: false,
|
// 登录页的免登录存储几天,默认7天
|
||||||
// 登录页的免登录存储几天,默认7天
|
loginDay: 7,
|
||||||
loginDay: 7
|
}),
|
||||||
}),
|
actions: {
|
||||||
actions: {
|
/** 登入 */
|
||||||
/** 存储头像 */
|
async loginByUsername(data: any) {
|
||||||
SET_AVATAR(avatar: string) {
|
const result = await fetchLogin(data);
|
||||||
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);
|
|
||||||
|
|
||||||
if (result.code === 200) {
|
if (result.code === 200) {
|
||||||
setToken(data.data);
|
setToken(result.data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
message(result.message, { type: "error" });
|
message(result.message, { type: 'error' });
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* * 发送邮箱验证码
|
* * 发送邮箱验证码
|
||||||
* @param email
|
* @param email
|
||||||
*/
|
*/
|
||||||
async postEmailCode(email: string) {
|
async postEmailCode(email: string) {
|
||||||
const response = await fetchPostEmailCode({ email });
|
const response = await fetchPostEmailCode({ email });
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
message(response.message, { type: "success" });
|
message(response.message, { type: 'success' });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
message(response.message, { type: "error" });
|
message(response.message, { type: 'error' });
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 前端登出(不调用接口)
|
* 前端登出(不调用接口)
|
||||||
*/
|
*/
|
||||||
async logOut() {
|
async logOut() {
|
||||||
this.username = "";
|
this.username = '';
|
||||||
this.roles = [];
|
this.roles = [];
|
||||||
this.permissions = [];
|
this.permissions = [];
|
||||||
removeToken();
|
removeToken();
|
||||||
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
|
useMultiTagsStoreHook().handleTags('equal', [...routerArrays]);
|
||||||
resetRouter();
|
resetRouter();
|
||||||
await router.push("/login");
|
await router.push('/login');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新`token`
|
* 刷新`token`
|
||||||
*/
|
*/
|
||||||
async handRefreshToken(data) {
|
async handRefreshToken(data) {
|
||||||
return new Promise<RefreshTokenResult>((resolve, reject) => {
|
return new Promise<RefreshTokenResult>((resolve, reject) => {
|
||||||
refreshTokenApi(data)
|
refreshTokenApi(data)
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setToken(data.data);
|
setToken(data.data);
|
||||||
resolve(data);
|
resolve(data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useUserStoreHook() {
|
export function useUserStoreHook() {
|
||||||
return useUserStore(store);
|
return useUserStore(store);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,40 @@
|
||||||
import Cookies from "js-cookie";
|
import Cookies from 'js-cookie';
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStore, useUserStoreHook } from '@/store/modules/user';
|
||||||
import { isIncludeAllChildren, isString, storageLocal } from "@pureadmin/utils";
|
import { isIncludeAllChildren, isString, storageLocal } from '@pureadmin/utils';
|
||||||
|
|
||||||
export interface DataInfo<T> {
|
export interface DataInfo<T> {
|
||||||
/** token */
|
/** token */
|
||||||
accessToken: string;
|
token: string;
|
||||||
/** `accessToken`的过期时间(时间戳) */
|
/** `accessToken`的过期时间(时间戳) */
|
||||||
expires: T;
|
expires: T;
|
||||||
/** 用于调用刷新accessToken的接口时所需的token */
|
/** 用于调用刷新accessToken的接口时所需的token */
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
/** 头像 */
|
/** 头像 */
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
/** 用户名 */
|
/** 用户名 */
|
||||||
username?: string;
|
username?: string;
|
||||||
/** 昵称 */
|
/** 昵称 */
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
/** 当前登录用户的角色 */
|
/** 当前登录用户的角色 */
|
||||||
roles?: Array<string>;
|
roles?: Array<string>;
|
||||||
/** 当前登录用户的按钮级别权限 */
|
/** 当前登录用户的按钮级别权限 */
|
||||||
permissions?: Array<string>;
|
permissions?: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userKey = "user-info";
|
export const userKey = 'user-info';
|
||||||
export const TokenKey = "authorized-token";
|
export const TokenKey = 'authorized-token';
|
||||||
/**
|
/**
|
||||||
* 通过`multiple-tabs`是否在`cookie`中,判断用户是否已经登录系统,
|
* 通过`multiple-tabs`是否在`cookie`中,判断用户是否已经登录系统,
|
||||||
* 从而支持多标签页打开已经登录的系统后无需再登录。
|
* 从而支持多标签页打开已经登录的系统后无需再登录。
|
||||||
* 浏览器完全关闭后`multiple-tabs`将自动从`cookie`中销毁,
|
* 浏览器完全关闭后`multiple-tabs`将自动从`cookie`中销毁,
|
||||||
* 再次打开浏览器需要重新登录系统
|
* 再次打开浏览器需要重新登录系统
|
||||||
* */
|
* */
|
||||||
export const multipleTabsKey = "multiple-tabs";
|
export const multipleTabsKey = 'multiple-tabs';
|
||||||
|
|
||||||
/** 获取`token` */
|
/** 获取`token` */
|
||||||
export function getToken(): DataInfo<number> {
|
export function getToken(): DataInfo<number> {
|
||||||
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
|
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
|
||||||
return Cookies.get(TokenKey)
|
return Cookies.get(TokenKey) ? JSON.parse(Cookies.get(TokenKey)) : storageLocal().getItem(userKey);
|
||||||
? JSON.parse(Cookies.get(TokenKey))
|
|
||||||
: storageLocal().getItem(userKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,97 +43,65 @@ export function getToken(): DataInfo<number> {
|
||||||
* 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁)
|
* 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁)
|
||||||
* 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这七条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
|
* 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这七条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
|
||||||
*/
|
*/
|
||||||
export function setToken(data: DataInfo<Date>) {
|
export function setToken(data: any) {
|
||||||
let expires = 0;
|
const userStore = useUserStore();
|
||||||
const { accessToken, refreshToken } = data;
|
let expires = 0;
|
||||||
const { isRemembered, loginDay } = useUserStoreHook();
|
const { token, refreshToken } = data;
|
||||||
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
|
const { isRemembered, loginDay } = useUserStoreHook();
|
||||||
const cookieString = JSON.stringify({ accessToken, expires, refreshToken });
|
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
|
||||||
|
const cookieString = JSON.stringify({ token, expires, refreshToken });
|
||||||
|
|
||||||
expires > 0
|
expires > 0 ? Cookies.set(TokenKey, cookieString, { expires: (expires - Date.now()) / 86400000 }) : Cookies.set(TokenKey, cookieString);
|
||||||
? Cookies.set(TokenKey, cookieString, {
|
|
||||||
expires: (expires - Date.now()) / 86400000
|
|
||||||
})
|
|
||||||
: Cookies.set(TokenKey, cookieString);
|
|
||||||
|
|
||||||
Cookies.set(
|
Cookies.set(multipleTabsKey, 'true', isRemembered ? { expires: loginDay } : {});
|
||||||
multipleTabsKey,
|
|
||||||
"true",
|
|
||||||
isRemembered
|
|
||||||
? {
|
|
||||||
expires: loginDay
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
);
|
|
||||||
|
|
||||||
function setUserKey({ avatar, username, nickname, roles, permissions }) {
|
function setUserKey({ avatar, username, nickname, roles, permissions }) {
|
||||||
useUserStoreHook().SET_AVATAR(avatar);
|
userStore.avatar = avatar;
|
||||||
useUserStoreHook().SET_USERNAME(username);
|
userStore.username = username;
|
||||||
useUserStoreHook().SET_NICKNAME(nickname);
|
userStore.nickname = nickname;
|
||||||
useUserStoreHook().SET_ROLES(roles);
|
userStore.roles = roles;
|
||||||
useUserStoreHook().SET_PERMS(permissions);
|
userStore.permissions = permissions;
|
||||||
storageLocal().setItem(userKey, {
|
|
||||||
refreshToken,
|
|
||||||
expires,
|
|
||||||
avatar,
|
|
||||||
username,
|
|
||||||
nickname,
|
|
||||||
roles,
|
|
||||||
permissions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.username && data.roles) {
|
storageLocal().setItem(userKey, { refreshToken, expires, avatar, username, nickname, roles, permissions });
|
||||||
const { username, roles } = data;
|
}
|
||||||
setUserKey({
|
|
||||||
avatar: data?.avatar ?? "",
|
if (data.username && data.roles) {
|
||||||
username,
|
const { username, roles } = data;
|
||||||
nickname: data?.nickname ?? "",
|
setUserKey({
|
||||||
roles,
|
avatar: data?.avatar ?? '',
|
||||||
permissions: data?.permissions ?? []
|
username,
|
||||||
});
|
nickname: data?.nickname ?? '',
|
||||||
} else {
|
roles,
|
||||||
const avatar =
|
permissions: data?.permissions ?? [],
|
||||||
storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "";
|
});
|
||||||
const username =
|
} else {
|
||||||
storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "";
|
const avatar = storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? '';
|
||||||
const nickname =
|
const username = storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? '';
|
||||||
storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "";
|
const nickname = storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? '';
|
||||||
const roles =
|
const roles = storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
|
||||||
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
|
const permissions = storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
|
||||||
const permissions =
|
setUserKey({ avatar, username, nickname, roles, permissions });
|
||||||
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
|
}
|
||||||
setUserKey({
|
|
||||||
avatar,
|
|
||||||
username,
|
|
||||||
nickname,
|
|
||||||
roles,
|
|
||||||
permissions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除`token`以及key值为`user-info`的localStorage信息 */
|
/** 删除`token`以及key值为`user-info`的localStorage信息 */
|
||||||
export function removeToken() {
|
export function removeToken() {
|
||||||
Cookies.remove(TokenKey);
|
Cookies.remove(TokenKey);
|
||||||
Cookies.remove(multipleTabsKey);
|
Cookies.remove(multipleTabsKey);
|
||||||
storageLocal().removeItem(userKey);
|
storageLocal().removeItem(userKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 格式化token(jwt格式) */
|
/** 格式化token(jwt格式) */
|
||||||
export const formatToken = (token: string): string => {
|
export const formatToken = (token: string): string => {
|
||||||
return "Bearer " + token;
|
return 'Bearer ' + token;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/
|
/** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/
|
||||||
export const hasPerms = (value: string | Array<string>): boolean => {
|
export const hasPerms = (value: string | Array<string>): boolean => {
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
const allPerms = "*:*:*";
|
const allPerms = '*:*:*';
|
||||||
const { permissions } = useUserStoreHook();
|
const { permissions } = useUserStoreHook();
|
||||||
if (!permissions) return false;
|
if (!permissions) return false;
|
||||||
if (permissions.length === 1 && permissions[0] === allPerms) return true;
|
if (permissions.length === 1 && permissions[0] === allPerms) return true;
|
||||||
const isAuths = isString(value)
|
return isString(value) ? permissions.includes(value) : isIncludeAllChildren(value, permissions);
|
||||||
? permissions.includes(value)
|
|
||||||
: isIncludeAllChildren(value, permissions);
|
|
||||||
return isAuths ? true : false;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,297 +1,99 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useI18n } from "vue-i18n";
|
import Motion from './utils/motion';
|
||||||
import Motion from "./utils/motion";
|
import { useNav } from '@/layout/hooks/useNav';
|
||||||
import { useRouter } from "vue-router";
|
import { useLayout } from '@/layout/hooks/useLayout';
|
||||||
import { message } from "@/utils/message";
|
import { avatar, bg, illustration } from './utils/static';
|
||||||
import { loginRules } from "./utils/rule";
|
import { toRaw } from 'vue';
|
||||||
import { useNav } from "@/layout/hooks/useNav";
|
import { useTranslationLang } from '@/layout/hooks/useTranslationLang';
|
||||||
import type { FormInstance } from "element-plus";
|
import { useDataThemeChange } from '@/layout/hooks/useDataThemeChange';
|
||||||
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 dayIcon from "@/assets/svg/day.svg?component";
|
import dayIcon from '@/assets/svg/day.svg?component';
|
||||||
import darkIcon from "@/assets/svg/dark.svg?component";
|
import darkIcon from '@/assets/svg/dark.svg?component';
|
||||||
import globalization from "@/assets/svg/globalization.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 Check from "@iconify-icons/ep/check";
|
|
||||||
import User from "@iconify-icons/ri/user-3-fill";
|
import LoginForm from '@/views/login/login-form.vue';
|
||||||
import { getTopMenu, initRouter } from "@/router/utils";
|
|
||||||
|
|
||||||
defineOptions({
|
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();
|
const { initStorage } = useLayout();
|
||||||
initStorage();
|
initStorage();
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
|
const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
|
||||||
dataThemeChange(overallStyle.value);
|
dataThemeChange(overallStyle.value);
|
||||||
const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
|
const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
|
||||||
const { locale, translationCh, translationEn } = useTranslationLang();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="select-none">
|
<div class="select-none">
|
||||||
<img :src="bg" alt="" class="wave" />
|
<img :src="bg" alt="" class="wave" />
|
||||||
<div class="flex-c absolute right-5 top-3">
|
<div class="flex-c absolute right-5 top-3">
|
||||||
<!-- 主题 -->
|
<!-- 主题 -->
|
||||||
<el-switch
|
<el-switch v-model="dataTheme" :active-icon="dayIcon" :inactive-icon="darkIcon" inline-prompt @change="dataThemeChange" />
|
||||||
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>
|
|
||||||
|
|
||||||
<el-form
|
<!-- 国际化 -->
|
||||||
ref="ruleFormRef"
|
<el-dropdown trigger="click">
|
||||||
:model="ruleForm"
|
<globalization class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300" />
|
||||||
:rules="loginRules"
|
<template #dropdown>
|
||||||
size="large"
|
<el-dropdown-menu class="translation">
|
||||||
>
|
<el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]" :style="getDropdownItemStyle(locale, 'zh')" @click="translationCh">
|
||||||
<Motion :delay="100">
|
<IconifyIconOffline v-show="locale === 'zh'" :icon="Check" class="check-zh" />
|
||||||
<el-form-item
|
简体中文
|
||||||
:rules="[
|
</el-dropdown-item>
|
||||||
{
|
<el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'en')]" :style="getDropdownItemStyle(locale, 'en')" @click="translationEn">
|
||||||
required: true,
|
<span v-show="locale === 'en'" class="check-en">
|
||||||
message: $t('login.usernameRegex'),
|
<IconifyIconOffline :icon="Check" />
|
||||||
trigger: 'blur'
|
</span>
|
||||||
}
|
English
|
||||||
]"
|
</el-dropdown-item>
|
||||||
prop="username"
|
</el-dropdown-menu>
|
||||||
>
|
</template>
|
||||||
<el-input
|
</el-dropdown>
|
||||||
v-model="ruleForm.username"
|
</div>
|
||||||
:placeholder="t('login.username')"
|
<div class="login-container">
|
||||||
:prefix-icon="useRenderIcon(User)"
|
<div class="img">
|
||||||
clearable
|
<component :is="toRaw(illustration)" />
|
||||||
/>
|
</div>
|
||||||
</el-form-item>
|
<div class="login-box">
|
||||||
</Motion>
|
<div class="login-form">
|
||||||
|
<avatar class="avatar" />
|
||||||
|
<Motion>
|
||||||
|
<h2 class="outline-none">{{ title }}</h2>
|
||||||
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="150">
|
<!-- 登录表单 -->
|
||||||
<el-form-item prop="password">
|
<login-form />
|
||||||
<el-input
|
</div>
|
||||||
v-model="ruleForm.password"
|
</div>
|
||||||
:placeholder="t('login.password')"
|
</div>
|
||||||
:prefix-icon="useRenderIcon(Lock)"
|
</div>
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@import url("@/style/login.css");
|
@import url('@/style/login.css');
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.el-input-group__append, .el-input-group__prepend) {
|
:deep(.el-input-group__append, .el-input-group__prepend) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.translation {
|
.translation {
|
||||||
::v-deep(.el-dropdown-menu__item) {
|
::v-deep(.el-dropdown-menu__item) {
|
||||||
padding: 5px 40px;
|
padding: 5px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-zh {
|
.check-zh {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-en {
|
.check-en {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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 */
|
/** 封装@vueuse/motion动画库中的自定义指令v-motion */
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Motion",
|
name: 'Motion',
|
||||||
props: {
|
props: {
|
||||||
delay: {
|
delay: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 50
|
default: 50,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
const { delay } = this;
|
const { delay } = this;
|
||||||
const motion = resolveDirective("motion");
|
const motion = resolveDirective('motion');
|
||||||
return withDirectives(
|
return withDirectives(h('div', {}, { default: () => [this.$slots.default()] }), [[motion, { initial: { opacity: 0, y: 100 }, enter: { opacity: 1, y: 0, transition: { delay } } }]]);
|
||||||
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 { reactive } from 'vue';
|
||||||
import type { FormRules } from "element-plus";
|
import type { FormRules } from 'element-plus';
|
||||||
import { $t } from "@/plugins/i18n";
|
import { $t } from '@/plugins/i18n';
|
||||||
|
|
||||||
/** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */
|
/** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */
|
||||||
export const REGEXP_PWD =
|
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}$/;
|
||||||
/^(?![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>{
|
const loginRules = reactive(<FormRules>{
|
||||||
password: [
|
username: [{ required: true, message: $t('login.usernameRegex'), trigger: 'blur' }],
|
||||||
{
|
password: [
|
||||||
validator: (rule, value, callback) => {
|
{
|
||||||
if (value === "") {
|
validator: (rule, value, callback) => {
|
||||||
callback(new Error($t("login.purePassWordReg")));
|
if (value === '') {
|
||||||
} else if (!REGEXP_PWD.test(value)) {
|
callback(new Error($t('login.purePassWordReg')));
|
||||||
callback(new Error($t("login.purePassWordRuleReg")));
|
} else if (!REGEXP_PWD.test(value)) {
|
||||||
} else {
|
callback(new Error($t('login.purePassWordRuleReg')));
|
||||||
callback();
|
} else {
|
||||||
}
|
callback();
|
||||||
},
|
}
|
||||||
trigger: "blur"
|
},
|
||||||
}
|
trigger: 'blur',
|
||||||
],
|
},
|
||||||
emailCode: [{ required: true, trigger: "blur", type: "string" }]
|
],
|
||||||
|
emailCode: [{ required: true, trigger: 'blur', type: 'string' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
export { loginRules };
|
export { loginRules };
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import bg from "@/assets/login/bg.png";
|
import bg from '@/assets/login/bg.png';
|
||||||
import avatar from "@/assets/login/avatar.svg?component";
|
import avatar from '@/assets/login/avatar.svg?component';
|
||||||
import illustration from "@/assets/login/illustration.svg?component";
|
import illustration from '@/assets/login/illustration.svg?component';
|
||||||
|
|
||||||
export { bg, avatar, illustration };
|
export { bg, avatar, illustration };
|
||||||
|
|
Loading…
Reference in New Issue