mirror of
https://github.com/pure-admin/pure-admin-thin.git
synced 2025-04-25 16:07:19 +08:00
feat: 完成密码加密登录功能
This commit is contained in:
parent
8d59c5f9a1
commit
03d9b2b6f8
@ -36,10 +36,12 @@
|
|||||||
"@vueuse/motion": "^2.0.0",
|
"@vueuse/motion": "^2.0.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
"dayjs": "^1.11.8",
|
"dayjs": "^1.11.8",
|
||||||
"echarts": "^5.4.2",
|
"echarts": "^5.4.2",
|
||||||
"element-plus": "^2.3.6",
|
"element-plus": "^2.3.6",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"jsencrypt": "^3.3.2",
|
||||||
"mitt": "^3.0.0",
|
"mitt": "^3.0.0",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
@ -47,9 +49,9 @@
|
|||||||
"pinia": "^2.1.4",
|
"pinia": "^2.1.4",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"typeit": "^8.7.1",
|
|
||||||
"responsive-storage": "^2.2.0",
|
"responsive-storage": "^2.2.0",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
|
"typeit": "^8.7.1",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.2",
|
"vue-router": "^4.2.2",
|
||||||
"vue-types": "^5.0.4"
|
"vue-types": "^5.0.4"
|
||||||
|
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@ -28,6 +28,7 @@ specifiers:
|
|||||||
autoprefixer: ^10.4.14
|
autoprefixer: ^10.4.14
|
||||||
axios: ^1.4.0
|
axios: ^1.4.0
|
||||||
cloc: ^2.11.0
|
cloc: ^2.11.0
|
||||||
|
crypto-js: ^4.1.1
|
||||||
cssnano: ^6.0.1
|
cssnano: ^6.0.1
|
||||||
dayjs: ^1.11.8
|
dayjs: ^1.11.8
|
||||||
echarts: ^5.4.2
|
echarts: ^5.4.2
|
||||||
@ -37,6 +38,7 @@ specifiers:
|
|||||||
eslint-plugin-vue: ^9.15.0
|
eslint-plugin-vue: ^9.15.0
|
||||||
husky: ^8.0.3
|
husky: ^8.0.3
|
||||||
js-cookie: ^3.0.5
|
js-cookie: ^3.0.5
|
||||||
|
jsencrypt: ^3.3.2
|
||||||
lint-staged: ^13.2.2
|
lint-staged: ^13.2.2
|
||||||
mitt: ^3.0.0
|
mitt: ^3.0.0
|
||||||
mockjs: ^1.1.0
|
mockjs: ^1.1.0
|
||||||
@ -94,10 +96,12 @@ dependencies:
|
|||||||
"@vueuse/motion": 2.0.0_vue@3.3.4
|
"@vueuse/motion": 2.0.0_vue@3.3.4
|
||||||
animate.css: 4.1.1
|
animate.css: 4.1.1
|
||||||
axios: 1.4.0
|
axios: 1.4.0
|
||||||
|
crypto-js: 4.1.1
|
||||||
dayjs: 1.11.8
|
dayjs: 1.11.8
|
||||||
echarts: 5.4.2
|
echarts: 5.4.2
|
||||||
element-plus: 2.3.6_vue@3.3.4
|
element-plus: 2.3.6_vue@3.3.4
|
||||||
js-cookie: 3.0.5
|
js-cookie: 3.0.5
|
||||||
|
jsencrypt: 3.3.2
|
||||||
mitt: 3.0.0
|
mitt: 3.0.0
|
||||||
mockjs: 1.1.0
|
mockjs: 1.1.0
|
||||||
nprogress: 0.2.0
|
nprogress: 0.2.0
|
||||||
@ -3116,6 +3120,13 @@ packages:
|
|||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/crypto-js/4.1.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
|
||||||
|
}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/css-declaration-sorter/6.4.0_postcss@8.4.24:
|
/css-declaration-sorter/6.4.0_postcss@8.4.24:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -5127,6 +5138,13 @@ packages:
|
|||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jsencrypt/3.3.2:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==
|
||||||
|
}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/jsesc/2.5.2:
|
/jsesc/2.5.2:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
import { http } from "@/utils/http";
|
import { http } from "@/utils/http";
|
||||||
|
|
||||||
/** 可以做成泛型 */
|
|
||||||
export type CaptchaResult = {
|
|
||||||
success: boolean;
|
|
||||||
data: CaptchaDTO;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CaptchaDTO = {
|
export type CaptchaDTO = {
|
||||||
/** 验证码开关 */
|
/** 验证码开关 */
|
||||||
isCaptchaOn: boolean;
|
isCaptchaOn: boolean;
|
||||||
/** */
|
/** 验证码的base64图片 */
|
||||||
uuid: string;
|
captchaCodeImg: string;
|
||||||
/** `token` */
|
/** 验证码对应的缓存key */
|
||||||
img: string;
|
captchaCodeKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginByPasswordDTO = {
|
||||||
|
/** 用户名 */
|
||||||
|
username: string;
|
||||||
|
/** 密码 */
|
||||||
|
password: string;
|
||||||
|
/** 验证码 */
|
||||||
|
captchaCode: string;
|
||||||
|
/** 验证码对应的缓存key */
|
||||||
|
captchaCodeKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RefreshTokenResult = {
|
export type RefreshTokenResult = {
|
||||||
@ -29,7 +34,12 @@ export type RefreshTokenResult = {
|
|||||||
|
|
||||||
/** 验证码接口 */
|
/** 验证码接口 */
|
||||||
export const getCaptchaCode = () => {
|
export const getCaptchaCode = () => {
|
||||||
return http.request<CaptchaResult>("get", "/captchaImage");
|
return http.request<ResponseData<CaptchaDTO>>("get", "/captchaImage");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 登录接口 */
|
||||||
|
export const loginByPassword = (data: LoginByPasswordDTO) => {
|
||||||
|
return http.request<ResponseData<any>>("post", "/login", { data });
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 刷新token */
|
/** 刷新token */
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { storageSession } from "@pureadmin/utils";
|
import { storageSession } from "@pureadmin/utils";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
|
import { aesEncrypt, aesDecrypt } from "@/utils/crypt";
|
||||||
|
|
||||||
export interface DataInfo<T> {
|
export interface DataInfo<T> {
|
||||||
/** token */
|
/** token */
|
||||||
@ -16,13 +17,15 @@ export interface DataInfo<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const sessionKey = "user-info";
|
export const sessionKey = "user-info";
|
||||||
export const TokenKey = "authorized-token";
|
export const tokenKey = "authorized-token";
|
||||||
|
export const isRememberMeKey = "ag-is-remember-me";
|
||||||
|
export const passwordKey = "ag-password";
|
||||||
|
|
||||||
/** 获取`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))
|
? JSON.parse(Cookies.get(tokenKey))
|
||||||
: storageSession().getItem(sessionKey);
|
: storageSession().getItem(sessionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,10 +42,10 @@ export function setToken(data: DataInfo<Date>) {
|
|||||||
const cookieString = JSON.stringify({ accessToken, expires });
|
const cookieString = JSON.stringify({ accessToken, expires });
|
||||||
|
|
||||||
expires > 0
|
expires > 0
|
||||||
? Cookies.set(TokenKey, cookieString, {
|
? Cookies.set(tokenKey, cookieString, {
|
||||||
expires: (expires - Date.now()) / 86400000
|
expires: (expires - Date.now()) / 86400000
|
||||||
})
|
})
|
||||||
: Cookies.set(TokenKey, cookieString);
|
: Cookies.set(tokenKey, cookieString);
|
||||||
|
|
||||||
function setSessionKey(username: string, roles: Array<string>) {
|
function setSessionKey(username: string, roles: Array<string>) {
|
||||||
useUserStoreHook().SET_USERNAME(username);
|
useUserStoreHook().SET_USERNAME(username);
|
||||||
@ -69,10 +72,43 @@ export function setToken(data: DataInfo<Date>) {
|
|||||||
|
|
||||||
/** 删除`token`以及key值为`user-info`的session信息 */
|
/** 删除`token`以及key值为`user-info`的session信息 */
|
||||||
export function removeToken() {
|
export function removeToken() {
|
||||||
Cookies.remove(TokenKey);
|
Cookies.remove(tokenKey);
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将密码加密后 存入cookies中 */
|
||||||
|
export function savePassword(password: string) {
|
||||||
|
const encryptPassword = aesEncrypt(password);
|
||||||
|
Cookies.set(passwordKey, encryptPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将密码中cookies中删除 */
|
||||||
|
export function removePassword() {
|
||||||
|
Cookies.remove(passwordKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取密码 并解密 */
|
||||||
|
export function getPassword(): string {
|
||||||
|
const encryptPassword = Cookies.get(passwordKey);
|
||||||
|
if (
|
||||||
|
encryptPassword !== null &&
|
||||||
|
encryptPassword !== undefined &&
|
||||||
|
encryptPassword.trim() !== ""
|
||||||
|
) {
|
||||||
|
return aesDecrypt(encryptPassword);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveIsRememberMe(isRememberMe: boolean) {
|
||||||
|
Cookies.set(isRememberMeKey, isRememberMe.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIsRememberMe() {
|
||||||
|
const value = Cookies.get(isRememberMeKey);
|
||||||
|
return value === "true";
|
||||||
|
}
|
||||||
|
|
||||||
/** 格式化token(jwt格式) */
|
/** 格式化token(jwt格式) */
|
||||||
export const formatToken = (token: string): string => {
|
export const formatToken = (token: string): string => {
|
||||||
return "Bearer " + token;
|
return "Bearer " + token;
|
||||||
|
44
src/utils/crypt.ts
Normal file
44
src/utils/crypt.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { JSEncrypt } from "jsencrypt";
|
||||||
|
import * as CryptoJS from "crypto-js";
|
||||||
|
import { isEmpty } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
// 密钥对生成 http://web.chacuo.net/netrsakeypair
|
||||||
|
// RSA 公钥 对应的私钥放在后端项目的application-basic.yml文件下
|
||||||
|
const publicKey =
|
||||||
|
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCh6HkK+rCM37FAzCHVythTc6pxvr551K07CRhdX/NjCddHAuQMOd/57R5fiIwgVNEfCsD1cIyS6A8IWj4DtJLR2t29JehPpqiFSJ4hNtDcLNxNJiYRcCQvyMQeyQIPE5Ljc35c72YwDtQAsIJChsauyLrc+E6HC3gn1JDm18HNXwIDAQAB";
|
||||||
|
|
||||||
|
// 加密
|
||||||
|
export function rsaEncrypt(txt) {
|
||||||
|
const encryptor = new JSEncrypt();
|
||||||
|
encryptor.setPublicKey(publicKey); // 设置公钥
|
||||||
|
return encryptor.encrypt(txt); // 对数据进行加密
|
||||||
|
}
|
||||||
|
|
||||||
|
const aesKey = "agileboot1234567";
|
||||||
|
|
||||||
|
export function aesEncrypt(txt): string {
|
||||||
|
if (isEmpty(txt)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const message = CryptoJS.enc.Utf8.parse(txt);
|
||||||
|
const secretPassphrase = CryptoJS.enc.Utf8.parse(aesKey);
|
||||||
|
const iv = CryptoJS.enc.Utf8.parse(aesKey);
|
||||||
|
|
||||||
|
const encrypted = CryptoJS.AES.encrypt(message, secretPassphrase, {
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
padding: CryptoJS.pad.Pkcs7,
|
||||||
|
iv
|
||||||
|
}).toString();
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aesDecrypt(txtEncrypt): string {
|
||||||
|
const secretPassphrase = CryptoJS.enc.Utf8.parse(aesKey);
|
||||||
|
const iv = CryptoJS.enc.Utf8.parse(aesKey);
|
||||||
|
const decrypted = CryptoJS.AES.decrypt(txtEncrypt, secretPassphrase, {
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
padding: CryptoJS.pad.Pkcs7,
|
||||||
|
iv
|
||||||
|
}).toString(CryptoJS.enc.Utf8);
|
||||||
|
return decrypted;
|
||||||
|
}
|
@ -5,7 +5,8 @@ import {
|
|||||||
reactive,
|
reactive,
|
||||||
onMounted,
|
onMounted,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
onBeforeMount
|
onBeforeMount,
|
||||||
|
watch
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import Motion from "./utils/motion";
|
import Motion from "./utils/motion";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
@ -20,11 +21,18 @@ import { useNav } from "@/layout/hooks/useNav";
|
|||||||
import type { FormInstance } from "element-plus";
|
import type { FormInstance } from "element-plus";
|
||||||
import { operates, thirdParty } from "./utils/enums";
|
import { operates, thirdParty } from "./utils/enums";
|
||||||
import { useLayout } from "@/layout/hooks/useLayout";
|
import { useLayout } from "@/layout/hooks/useLayout";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { rsaEncrypt } from "@/utils/crypt";
|
||||||
import { initRouter, getTopMenu } from "@/router/utils";
|
import { initRouter, getTopMenu } from "@/router/utils";
|
||||||
import { bg, avatar, illustration } from "./utils/static";
|
import { bg, avatar, illustration } from "./utils/static";
|
||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
|
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
|
||||||
|
import {
|
||||||
|
saveIsRememberMe,
|
||||||
|
getIsRememberMe,
|
||||||
|
savePassword,
|
||||||
|
getPassword,
|
||||||
|
removePassword
|
||||||
|
} from "@/utils/auth";
|
||||||
|
|
||||||
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";
|
||||||
@ -42,7 +50,7 @@ const isCaptchaOn = ref(false);
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const checked = ref(false);
|
const isRememberMe = ref(false);
|
||||||
const ruleFormRef = ref<FormInstance>();
|
const ruleFormRef = ref<FormInstance>();
|
||||||
// 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码)
|
// 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码)
|
||||||
const currentPage = ref(0);
|
const currentPage = ref(0);
|
||||||
@ -56,8 +64,9 @@ const { title } = useNav();
|
|||||||
|
|
||||||
const ruleForm = reactive({
|
const ruleForm = reactive({
|
||||||
username: "admin",
|
username: "admin",
|
||||||
password: "admin123",
|
password: getPassword(),
|
||||||
verifyCode: ""
|
captchaCode: "",
|
||||||
|
captchaCodeKey: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
const onLogin = async (formEl: FormInstance | undefined) => {
|
const onLogin = async (formEl: FormInstance | undefined) => {
|
||||||
@ -65,17 +74,23 @@ const onLogin = async (formEl: FormInstance | undefined) => {
|
|||||||
if (!formEl) return;
|
if (!formEl) return;
|
||||||
await formEl.validate((valid, fields) => {
|
await formEl.validate((valid, fields) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
useUserStoreHook()
|
CommonAPI.loginByPassword({
|
||||||
.loginByUsername({ username: ruleForm.username, password: "admin123" })
|
username: ruleForm.username,
|
||||||
.then(res => {
|
password: rsaEncrypt(ruleForm.password),
|
||||||
if (res.success) {
|
captchaCode: ruleForm.captchaCode,
|
||||||
// 获取后端路由
|
captchaCodeKey: ruleForm.captchaCodeKey
|
||||||
initRouter().then(() => {
|
}).then(res => {
|
||||||
router.push(getTopMenu(true).path);
|
if (res.code === 0) {
|
||||||
message("登录成功", { type: "success" });
|
// 获取后端路由
|
||||||
});
|
initRouter().then(() => {
|
||||||
|
router.push(getTopMenu(true).path);
|
||||||
|
message("登录成功", { type: "success" });
|
||||||
|
});
|
||||||
|
if (isRememberMe.value) {
|
||||||
|
savePassword(ruleForm.password);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
return fields;
|
return fields;
|
||||||
@ -93,12 +108,25 @@ function onkeypress({ code }: KeyboardEvent) {
|
|||||||
async function getCaptchaCode() {
|
async function getCaptchaCode() {
|
||||||
await CommonAPI.getCaptchaCode().then(res => {
|
await CommonAPI.getCaptchaCode().then(res => {
|
||||||
isCaptchaOn.value = res.data.isCaptchaOn;
|
isCaptchaOn.value = res.data.isCaptchaOn;
|
||||||
captchaCodeBase64.value = `data:image/gif;base64,${res.data.img}`;
|
captchaCodeBase64.value = `data:image/gif;base64,${res.data.captchaCodeImg}`;
|
||||||
|
ruleForm.captchaCodeKey = res.data.captchaCodeKey;
|
||||||
|
console.log(ruleForm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(isRememberMe, newVal => {
|
||||||
|
saveIsRememberMe(newVal);
|
||||||
|
if (newVal === false) {
|
||||||
|
removePassword();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
getCaptchaCode();
|
getCaptchaCode();
|
||||||
|
isRememberMe.value = getIsRememberMe();
|
||||||
|
if (isRememberMe.value) {
|
||||||
|
ruleForm.password = getPassword();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -179,10 +207,10 @@ onBeforeUnmount(() => {
|
|||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="200">
|
<Motion :delay="200">
|
||||||
<el-form-item v-if="isCaptchaOn" prop="verifyCode">
|
<el-form-item v-if="isCaptchaOn" prop="captchaCode">
|
||||||
<el-input
|
<el-input
|
||||||
clearable
|
clearable
|
||||||
v-model="ruleForm.verifyCode"
|
v-model="ruleForm.captchaCode"
|
||||||
placeholder="验证码"
|
placeholder="验证码"
|
||||||
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
||||||
>
|
>
|
||||||
@ -202,7 +230,7 @@ onBeforeUnmount(() => {
|
|||||||
<Motion :delay="250">
|
<Motion :delay="250">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<div class="w-full h-[20px] flex justify-between items-center">
|
<div class="w-full h-[20px] flex justify-between items-center">
|
||||||
<el-checkbox v-model="checked"> 记住密码 </el-checkbox>
|
<el-checkbox v-model="isRememberMe"> 记住密码 </el-checkbox>
|
||||||
<el-button link type="primary" @click="currentPage = 4">
|
<el-button link type="primary" @click="currentPage = 4">
|
||||||
忘记密码
|
忘记密码
|
||||||
</el-button>
|
</el-button>
|
||||||
|
6
types/index.d.ts
vendored
6
types/index.d.ts
vendored
@ -49,6 +49,12 @@ type TimeoutHandle = ReturnType<typeof setTimeout>;
|
|||||||
|
|
||||||
type IntervalHandle = ReturnType<typeof setInterval>;
|
type IntervalHandle = ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
type ResponseData<T> = {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
|
||||||
type Effect = "light" | "dark";
|
type Effect = "light" | "dark";
|
||||||
|
|
||||||
interface ChangeEvent extends Event {
|
interface ChangeEvent extends Event {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user