feat: 🚀 登录完成

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

View File

@ -1,19 +1,10 @@
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 = {
@ -27,14 +18,14 @@ const defaultConfig: AxiosRequestConfig = {
// 跨域允许携带凭证 // 跨域允许携带凭证
// 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 {
@ -56,24 +47,19 @@ class PureHttp {
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,
url: string,
param?: AxiosRequestConfig,
axiosConfig?: PureHttpRequestConfig
): Promise<T> {
const config = { const config = {
method, method,
url, url,
...param, ...param,
...axiosConfig ...axiosConfig,
} as PureHttpRequestConfig; } as PureHttpRequestConfig;
// 单独处理自定义请求/响应回调 // 单独处理自定义请求/响应回调
@ -90,21 +76,13 @@ class PureHttp {
} }
/** 单独抽离的`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);
} }
/** 请求拦截 */ /** 请求拦截 */
@ -114,7 +92,7 @@ class PureHttp {
// 开启进度条动画 // 开启进度条动画
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;
} }
@ -123,7 +101,7 @@ class PureHttp {
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 => {
@ -139,7 +117,7 @@ class PureHttp {
.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 = [];
}) })
@ -149,9 +127,7 @@ class PureHttp {
} }
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 { } else {
@ -159,9 +135,7 @@ class PureHttp {
} }
}); });
}, },
error => { error => error,
return Promise.reject(error);
}
); );
} }
@ -174,7 +148,7 @@ class PureHttp {
// 关闭进度条动画 // 关闭进度条动画
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;
} }
@ -189,10 +163,10 @@ class PureHttp {
$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;
} },
); );
} }
} }

View File

@ -1,5 +1,5 @@
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 {
/** 头像 */ /** 头像 */
@ -31,7 +31,7 @@ export interface RefreshTokenResult {
/** 登录 */ /** 登录 */
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 }
);
}; };

View File

@ -1,80 +1,39 @@
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: {
/** 存储头像 */
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) { async loginByUsername(data: any) {
const result = await fetchLogin(data); 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;
}, },
@ -85,10 +44,10 @@ export const useUserStore = defineStore({
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;
}, },
@ -97,13 +56,13 @@ export const useUserStore = defineStore({
* *
*/ */
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');
}, },
/** /**
@ -122,8 +81,8 @@ export const useUserStore = defineStore({
reject(error); reject(error);
}); });
}); });
} },
} },
}); });
export function useUserStoreHook() { export function useUserStoreHook() {

View File

@ -1,10 +1,10 @@
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 */
@ -21,22 +21,20 @@ export interface DataInfo<T> {
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,73 +43,44 @@ 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) {
const userStore = useUserStore();
let expires = 0; let expires = 0;
const { accessToken, refreshToken } = data; const { token, refreshToken } = data;
const { isRemembered, loginDay } = useUserStoreHook(); const { isRemembered, loginDay } = useUserStoreHook();
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳将此处代码改为expires = data.expires然后把上面的DataInfo<Date>改成DataInfo<number>即可 expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳将此处代码改为expires = data.expires然后把上面的DataInfo<Date>改成DataInfo<number>即可
const cookieString = JSON.stringify({ accessToken, expires, refreshToken }); 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, storageLocal().setItem(userKey, { refreshToken, expires, avatar, username, nickname, roles, permissions });
expires,
avatar,
username,
nickname,
roles,
permissions
});
} }
if (data.username && data.roles) { if (data.username && data.roles) {
const { username, roles } = data; const { username, roles } = data;
setUserKey({ setUserKey({
avatar: data?.avatar ?? "", avatar: data?.avatar ?? '',
username, username,
nickname: data?.nickname ?? "", nickname: data?.nickname ?? '',
roles, roles,
permissions: data?.permissions ?? [] permissions: data?.permissions ?? [],
}); });
} else { } else {
const avatar = const avatar = storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? '';
storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? ""; const username = storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? '';
const username = const nickname = storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? '';
storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? ""; const roles = storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
const nickname = const permissions = storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? ""; setUserKey({ avatar, username, nickname, roles, permissions });
const roles =
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
const permissions =
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
setUserKey({
avatar,
username,
nickname,
roles,
permissions
});
} }
} }
@ -124,18 +93,15 @@ export function removeToken() {
/** 格式化tokenjwt格式 */ /** 格式化tokenjwt格式 */
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;
}; };

View File

@ -1,128 +1,30 @@
<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>
@ -130,37 +32,18 @@ onBeforeUnmount(() => {
<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"> <el-dropdown trigger="click">
<globalization <globalization class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300" />
class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300"
/>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="translation"> <el-dropdown-menu class="translation">
<el-dropdown-item <el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]" :style="getDropdownItemStyle(locale, 'zh')" @click="translationCh">
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]" <IconifyIconOffline v-show="locale === 'zh'" :icon="Check" class="check-zh" />
:style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh"
>
<IconifyIconOffline
v-show="locale === 'zh'"
:icon="Check"
class="check-zh"
/>
简体中文 简体中文
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'en')]" :style="getDropdownItemStyle(locale, 'en')" @click="translationEn">
:class="['dark:!text-white', getDropdownItemClass(locale, 'en')]"
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
>
<span v-show="locale === 'en'" class="check-en"> <span v-show="locale === 'en'" class="check-en">
<IconifyIconOffline :icon="Check" /> <IconifyIconOffline :icon="Check" />
</span> </span>
@ -181,89 +64,8 @@ onBeforeUnmount(() => {
<h2 class="outline-none">{{ title }}</h2> <h2 class="outline-none">{{ title }}</h2>
</Motion> </Motion>
<el-form <!-- 登录表单 -->
ref="ruleFormRef" <login-form />
: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>
<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>
</div> </div>
@ -271,7 +73,7 @@ onBeforeUnmount(() => {
</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>

View File

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

View File

@ -1,40 +1,17 @@
import { h, defineComponent, withDirectives, resolveDirective } from "vue"; import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
/** 封装@vueuse/motion动画库中的自定义指令v-motion */ /** 封装@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
}
}
}
]
]
);
}
}); });

View File

@ -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>{
username: [{ required: true, message: $t('login.usernameRegex'), trigger: 'blur' }],
password: [ password: [
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (value === "") { if (value === '') {
callback(new Error($t("login.purePassWordReg"))); callback(new Error($t('login.purePassWordReg')));
} else if (!REGEXP_PWD.test(value)) { } else if (!REGEXP_PWD.test(value)) {
callback(new Error($t("login.purePassWordRuleReg"))); callback(new Error($t('login.purePassWordRuleReg')));
} else { } else {
callback(); callback();
} }
}, },
trigger: "blur" trigger: 'blur',
} },
], ],
emailCode: [{ required: true, trigger: "blur", type: "string" }] emailCode: [{ required: true, trigger: 'blur', type: 'string' }],
}); });
export { loginRules }; export { loginRules };

View File

@ -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 };