mirror of
				https://github.com/pure-admin/pure-admin-thin.git
				synced 2025-11-04 17:44:48 +08:00 
			
		
		
		
	feat: 完成密码加密登录功能
This commit is contained in:
		
							parent
							
								
									8d59c5f9a1
								
							
						
					
					
						commit
						03d9b2b6f8
					
				@ -36,10 +36,12 @@
 | 
			
		||||
    "@vueuse/motion": "^2.0.0",
 | 
			
		||||
    "animate.css": "^4.1.1",
 | 
			
		||||
    "axios": "^1.4.0",
 | 
			
		||||
    "crypto-js": "^4.1.1",
 | 
			
		||||
    "dayjs": "^1.11.8",
 | 
			
		||||
    "echarts": "^5.4.2",
 | 
			
		||||
    "element-plus": "^2.3.6",
 | 
			
		||||
    "js-cookie": "^3.0.5",
 | 
			
		||||
    "jsencrypt": "^3.3.2",
 | 
			
		||||
    "mitt": "^3.0.0",
 | 
			
		||||
    "mockjs": "^1.1.0",
 | 
			
		||||
    "nprogress": "^0.2.0",
 | 
			
		||||
@ -47,9 +49,9 @@
 | 
			
		||||
    "pinia": "^2.1.4",
 | 
			
		||||
    "qrcode": "^1.5.3",
 | 
			
		||||
    "qs": "^6.11.2",
 | 
			
		||||
    "typeit": "^8.7.1",
 | 
			
		||||
    "responsive-storage": "^2.2.0",
 | 
			
		||||
    "sortablejs": "^1.15.0",
 | 
			
		||||
    "typeit": "^8.7.1",
 | 
			
		||||
    "vue": "^3.3.4",
 | 
			
		||||
    "vue-router": "^4.2.2",
 | 
			
		||||
    "vue-types": "^5.0.4"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@ -28,6 +28,7 @@ specifiers:
 | 
			
		||||
  autoprefixer: ^10.4.14
 | 
			
		||||
  axios: ^1.4.0
 | 
			
		||||
  cloc: ^2.11.0
 | 
			
		||||
  crypto-js: ^4.1.1
 | 
			
		||||
  cssnano: ^6.0.1
 | 
			
		||||
  dayjs: ^1.11.8
 | 
			
		||||
  echarts: ^5.4.2
 | 
			
		||||
@ -37,6 +38,7 @@ specifiers:
 | 
			
		||||
  eslint-plugin-vue: ^9.15.0
 | 
			
		||||
  husky: ^8.0.3
 | 
			
		||||
  js-cookie: ^3.0.5
 | 
			
		||||
  jsencrypt: ^3.3.2
 | 
			
		||||
  lint-staged: ^13.2.2
 | 
			
		||||
  mitt: ^3.0.0
 | 
			
		||||
  mockjs: ^1.1.0
 | 
			
		||||
@ -94,10 +96,12 @@ dependencies:
 | 
			
		||||
  "@vueuse/motion": 2.0.0_vue@3.3.4
 | 
			
		||||
  animate.css: 4.1.1
 | 
			
		||||
  axios: 1.4.0
 | 
			
		||||
  crypto-js: 4.1.1
 | 
			
		||||
  dayjs: 1.11.8
 | 
			
		||||
  echarts: 5.4.2
 | 
			
		||||
  element-plus: 2.3.6_vue@3.3.4
 | 
			
		||||
  js-cookie: 3.0.5
 | 
			
		||||
  jsencrypt: 3.3.2
 | 
			
		||||
  mitt: 3.0.0
 | 
			
		||||
  mockjs: 1.1.0
 | 
			
		||||
  nprogress: 0.2.0
 | 
			
		||||
@ -3116,6 +3120,13 @@ packages:
 | 
			
		||||
      which: 2.0.2
 | 
			
		||||
    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:
 | 
			
		||||
    resolution:
 | 
			
		||||
      {
 | 
			
		||||
@ -5127,6 +5138,13 @@ packages:
 | 
			
		||||
      argparse: 2.0.1
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /jsencrypt/3.3.2:
 | 
			
		||||
    resolution:
 | 
			
		||||
      {
 | 
			
		||||
        integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==
 | 
			
		||||
      }
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /jsesc/2.5.2:
 | 
			
		||||
    resolution:
 | 
			
		||||
      {
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,23 @@
 | 
			
		||||
import { http } from "@/utils/http";
 | 
			
		||||
 | 
			
		||||
/** 可以做成泛型 */
 | 
			
		||||
export type CaptchaResult = {
 | 
			
		||||
  success: boolean;
 | 
			
		||||
  data: CaptchaDTO;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CaptchaDTO = {
 | 
			
		||||
  /** 验证码开关 */
 | 
			
		||||
  isCaptchaOn: boolean;
 | 
			
		||||
  /**  */
 | 
			
		||||
  uuid: string;
 | 
			
		||||
  /** `token` */
 | 
			
		||||
  img: string;
 | 
			
		||||
  /**  验证码的base64图片 */
 | 
			
		||||
  captchaCodeImg: string;
 | 
			
		||||
  /** 验证码对应的缓存key */
 | 
			
		||||
  captchaCodeKey: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type LoginByPasswordDTO = {
 | 
			
		||||
  /** 用户名 */
 | 
			
		||||
  username: string;
 | 
			
		||||
  /**  密码 */
 | 
			
		||||
  password: string;
 | 
			
		||||
  /** 验证码 */
 | 
			
		||||
  captchaCode: string;
 | 
			
		||||
  /** 验证码对应的缓存key */
 | 
			
		||||
  captchaCodeKey: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type RefreshTokenResult = {
 | 
			
		||||
@ -29,7 +34,12 @@ export type RefreshTokenResult = {
 | 
			
		||||
 | 
			
		||||
/** 验证码接口 */
 | 
			
		||||
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 */
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import Cookies from "js-cookie";
 | 
			
		||||
import { storageSession } from "@pureadmin/utils";
 | 
			
		||||
import { useUserStoreHook } from "@/store/modules/user";
 | 
			
		||||
import { aesEncrypt, aesDecrypt } from "@/utils/crypt";
 | 
			
		||||
 | 
			
		||||
export interface DataInfo<T> {
 | 
			
		||||
  /** token */
 | 
			
		||||
@ -16,13 +17,15 @@ export interface DataInfo<T> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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` */
 | 
			
		||||
export function getToken(): DataInfo<number> {
 | 
			
		||||
  // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
 | 
			
		||||
  return Cookies.get(TokenKey)
 | 
			
		||||
    ? JSON.parse(Cookies.get(TokenKey))
 | 
			
		||||
  return Cookies.get(tokenKey)
 | 
			
		||||
    ? JSON.parse(Cookies.get(tokenKey))
 | 
			
		||||
    : storageSession().getItem(sessionKey);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -39,10 +42,10 @@ export function setToken(data: DataInfo<Date>) {
 | 
			
		||||
  const cookieString = JSON.stringify({ accessToken, expires });
 | 
			
		||||
 | 
			
		||||
  expires > 0
 | 
			
		||||
    ? Cookies.set(TokenKey, cookieString, {
 | 
			
		||||
    ? Cookies.set(tokenKey, cookieString, {
 | 
			
		||||
        expires: (expires - Date.now()) / 86400000
 | 
			
		||||
      })
 | 
			
		||||
    : Cookies.set(TokenKey, cookieString);
 | 
			
		||||
    : Cookies.set(tokenKey, cookieString);
 | 
			
		||||
 | 
			
		||||
  function setSessionKey(username: string, roles: Array<string>) {
 | 
			
		||||
    useUserStoreHook().SET_USERNAME(username);
 | 
			
		||||
@ -69,10 +72,43 @@ export function setToken(data: DataInfo<Date>) {
 | 
			
		||||
 | 
			
		||||
/** 删除`token`以及key值为`user-info`的session信息 */
 | 
			
		||||
export function removeToken() {
 | 
			
		||||
  Cookies.remove(TokenKey);
 | 
			
		||||
  Cookies.remove(tokenKey);
 | 
			
		||||
  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格式) */
 | 
			
		||||
export const formatToken = (token: string): string => {
 | 
			
		||||
  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,
 | 
			
		||||
  onMounted,
 | 
			
		||||
  onBeforeUnmount,
 | 
			
		||||
  onBeforeMount
 | 
			
		||||
  onBeforeMount,
 | 
			
		||||
  watch
 | 
			
		||||
} from "vue";
 | 
			
		||||
import Motion from "./utils/motion";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
@ -20,11 +21,18 @@ import { useNav } from "@/layout/hooks/useNav";
 | 
			
		||||
import type { FormInstance } from "element-plus";
 | 
			
		||||
import { operates, thirdParty } from "./utils/enums";
 | 
			
		||||
import { useLayout } from "@/layout/hooks/useLayout";
 | 
			
		||||
import { useUserStoreHook } from "@/store/modules/user";
 | 
			
		||||
import { rsaEncrypt } from "@/utils/crypt";
 | 
			
		||||
import { initRouter, getTopMenu } from "@/router/utils";
 | 
			
		||||
import { bg, avatar, illustration } from "./utils/static";
 | 
			
		||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 | 
			
		||||
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
 | 
			
		||||
import {
 | 
			
		||||
  saveIsRememberMe,
 | 
			
		||||
  getIsRememberMe,
 | 
			
		||||
  savePassword,
 | 
			
		||||
  getPassword,
 | 
			
		||||
  removePassword
 | 
			
		||||
} from "@/utils/auth";
 | 
			
		||||
 | 
			
		||||
import dayIcon from "@/assets/svg/day.svg?component";
 | 
			
		||||
import darkIcon from "@/assets/svg/dark.svg?component";
 | 
			
		||||
@ -42,7 +50,7 @@ const isCaptchaOn = ref(false);
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
const checked = ref(false);
 | 
			
		||||
const isRememberMe = ref(false);
 | 
			
		||||
const ruleFormRef = ref<FormInstance>();
 | 
			
		||||
// 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码)
 | 
			
		||||
const currentPage = ref(0);
 | 
			
		||||
@ -56,8 +64,9 @@ const { title } = useNav();
 | 
			
		||||
 | 
			
		||||
const ruleForm = reactive({
 | 
			
		||||
  username: "admin",
 | 
			
		||||
  password: "admin123",
 | 
			
		||||
  verifyCode: ""
 | 
			
		||||
  password: getPassword(),
 | 
			
		||||
  captchaCode: "",
 | 
			
		||||
  captchaCodeKey: ""
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const onLogin = async (formEl: FormInstance | undefined) => {
 | 
			
		||||
@ -65,17 +74,23 @@ const onLogin = async (formEl: FormInstance | undefined) => {
 | 
			
		||||
  if (!formEl) return;
 | 
			
		||||
  await formEl.validate((valid, fields) => {
 | 
			
		||||
    if (valid) {
 | 
			
		||||
      useUserStoreHook()
 | 
			
		||||
        .loginByUsername({ username: ruleForm.username, password: "admin123" })
 | 
			
		||||
        .then(res => {
 | 
			
		||||
          if (res.success) {
 | 
			
		||||
            // 获取后端路由
 | 
			
		||||
            initRouter().then(() => {
 | 
			
		||||
              router.push(getTopMenu(true).path);
 | 
			
		||||
              message("登录成功", { type: "success" });
 | 
			
		||||
            });
 | 
			
		||||
      CommonAPI.loginByPassword({
 | 
			
		||||
        username: ruleForm.username,
 | 
			
		||||
        password: rsaEncrypt(ruleForm.password),
 | 
			
		||||
        captchaCode: ruleForm.captchaCode,
 | 
			
		||||
        captchaCodeKey: ruleForm.captchaCodeKey
 | 
			
		||||
      }).then(res => {
 | 
			
		||||
        if (res.code === 0) {
 | 
			
		||||
          // 获取后端路由
 | 
			
		||||
          initRouter().then(() => {
 | 
			
		||||
            router.push(getTopMenu(true).path);
 | 
			
		||||
            message("登录成功", { type: "success" });
 | 
			
		||||
          });
 | 
			
		||||
          if (isRememberMe.value) {
 | 
			
		||||
            savePassword(ruleForm.password);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
      return fields;
 | 
			
		||||
@ -93,12 +108,25 @@ function onkeypress({ code }: KeyboardEvent) {
 | 
			
		||||
async function getCaptchaCode() {
 | 
			
		||||
  await CommonAPI.getCaptchaCode().then(res => {
 | 
			
		||||
    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(() => {
 | 
			
		||||
  getCaptchaCode();
 | 
			
		||||
  isRememberMe.value = getIsRememberMe();
 | 
			
		||||
  if (isRememberMe.value) {
 | 
			
		||||
    ruleForm.password = getPassword();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
@ -179,10 +207,10 @@ onBeforeUnmount(() => {
 | 
			
		||||
            </Motion>
 | 
			
		||||
 | 
			
		||||
            <Motion :delay="200">
 | 
			
		||||
              <el-form-item v-if="isCaptchaOn" prop="verifyCode">
 | 
			
		||||
              <el-form-item v-if="isCaptchaOn" prop="captchaCode">
 | 
			
		||||
                <el-input
 | 
			
		||||
                  clearable
 | 
			
		||||
                  v-model="ruleForm.verifyCode"
 | 
			
		||||
                  v-model="ruleForm.captchaCode"
 | 
			
		||||
                  placeholder="验证码"
 | 
			
		||||
                  :prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
 | 
			
		||||
                >
 | 
			
		||||
@ -202,7 +230,7 @@ onBeforeUnmount(() => {
 | 
			
		||||
            <Motion :delay="250">
 | 
			
		||||
              <el-form-item>
 | 
			
		||||
                <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>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 ResponseData<T> = {
 | 
			
		||||
  code: number;
 | 
			
		||||
  msg: string;
 | 
			
		||||
  data: T;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Effect = "light" | "dark";
 | 
			
		||||
 | 
			
		||||
interface ChangeEvent extends Event {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user