Bläddra i källkod

Merge pull request #314 from xiaoxian521/perf/login

perf: login
RealityBoy 2 år sedan
förälder
incheckning
76f6a9df89

+ 3 - 29
src/layout/components/navbar.vue

@@ -1,52 +1,26 @@
 <script setup lang="ts">
-import { useI18n } from "vue-i18n";
 import { useNav } from "../hooks/nav";
-import { useRoute } from "vue-router";
 import Search from "./search/index.vue";
 import Notice from "./notice/index.vue";
 import mixNav from "./sidebar/mixNav.vue";
 import avatars from "/@/assets/avatars.jpg";
-import { watch, getCurrentInstance } from "vue";
 import Breadcrumb from "./sidebar/breadCrumb.vue";
 import { deviceDetection } from "@pureadmin/utils";
 import screenfull from "../components/screenfull/index.vue";
+import { useTranslationLang } from "../hooks/useTranslationLang";
 import globalization from "/@/assets/svg/globalization.svg?component";
 
-const route = useRoute();
-const { locale, t } = useI18n();
-const instance =
-  getCurrentInstance().appContext.config.globalProperties.$storage;
 const {
   logout,
   onPanel,
-  changeTitle,
   pureApp,
   username,
   avatarsStyle,
   getDropdownItemStyle,
-  getDropdownItemClass,
-  changeWangeditorLanguage
+  getDropdownItemClass
 } = useNav();
 
-watch(
-  () => locale.value,
-  () => {
-    changeTitle(route.meta);
-    locale.value === "en"
-      ? changeWangeditorLanguage(locale.value)
-      : changeWangeditorLanguage("zh-CN");
-  }
-);
-
-function translationCh() {
-  instance.locale = { locale: "zh" };
-  locale.value = "zh";
-}
-
-function translationEn() {
-  instance.locale = { locale: "en" };
-  locale.value = "en";
-}
+const { t, locale, translationCh, translationEn } = useTranslationLang();
 </script>
 
 <template>

+ 1 - 1
src/layout/components/search/index.vue

@@ -1,6 +1,6 @@
 <script lang="ts" setup>
 import { SearchModal } from "./components";
-import useBoolean from "../../hooks/useBoolean";
+import { useBoolean } from "../../hooks/useBoolean";
 const { bool: show, toggle } = useBoolean();
 function handleSearch() {
   toggle();

+ 14 - 115
src/layout/components/setting/index.vue

@@ -6,33 +6,24 @@ import {
   reactive,
   computed,
   nextTick,
-  useCssModule,
-  getCurrentInstance
+  useCssModule
 } from "vue";
-import { find } from "lodash-unified";
 import { getConfig } from "/@/config";
 import { useRouter } from "vue-router";
 import panel from "../panel/index.vue";
 import { emitter } from "/@/utils/mitt";
 import { templateRef } from "@vueuse/core";
-import { TinyColor } from "@ctrl/tinycolor";
-import { themeColorsType } from "../../types";
 import { routerArrays } from "/@/layout/types";
-import type { StorageConfigs } from "/#/index";
 import { useAppStoreHook } from "/@/store/modules/app";
-import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
 import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
+import { useDataThemeChange } from "/@/layout/hooks/useDataThemeChange";
 import {
   useDark,
   debounce,
   storageLocal,
   storageSession
 } from "@pureadmin/utils";
-import {
-  darken,
-  lighten,
-  toggleTheme
-} from "@pureadmin/theme/dist/browser-utils";
+import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
 
 import dayIcon from "/@/assets/svg/day.svg?component";
 import darkIcon from "/@/assets/svg/dark.svg?component";
@@ -40,44 +31,21 @@ import darkIcon from "/@/assets/svg/dark.svg?component";
 const router = useRouter();
 const { isDark } = useDark();
 const { isSelect } = useCssModule();
-const body = document.documentElement as HTMLElement;
-const instance =
-  getCurrentInstance().appContext.app.config.globalProperties.$storage;
-
-const instanceConfig =
-  getCurrentInstance().appContext.app.config.globalProperties.$config;
-
-let themeColors = ref<Array<themeColorsType>>([
-  /* 道奇蓝(默认) */
-  { color: "#1b2a47", themeColor: "default" },
-  /* 亮白色 */
-  { color: "#ffffff", themeColor: "light" },
-  /* 猩红色 */
-  { color: "#f5222d", themeColor: "dusk" },
-  /* 橙红色 */
-  { color: "#fa541c", themeColor: "volcano" },
-  /* 金色 */
-  { color: "#fadb14", themeColor: "yellow" },
-  /* 绿宝石 */
-  { color: "#13c2c2", themeColor: "mingQing" },
-  /* 酸橙绿 */
-  { color: "#52c41a", themeColor: "auroraGreen" },
-  /* 深粉色 */
-  { color: "#eb2f96", themeColor: "pink" },
-  /* 深紫罗兰色 */
-  { color: "#722ed1", themeColor: "saucePurple" }
-]);
 
 const mixRef = templateRef<HTMLElement | null>("mixRef", null);
 const verticalRef = templateRef<HTMLElement | null>("verticalRef", null);
 const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null);
 
-let layoutTheme =
-  ref(storageLocal.getItem<StorageConfigs>("responsive-layout")) ||
-  ref({
-    layout: instanceConfig?.Layout ?? "vertical",
-    theme: instanceConfig?.Theme ?? "default"
-  });
+const {
+  body,
+  instance,
+  dataTheme,
+  layoutTheme,
+  themeColors,
+  dataThemeChange,
+  setEpThemeColor,
+  setLayoutThemeColor
+} = useDataThemeChange();
 
 /* body添加layout属性,作用于src/style/sidebar.scss */
 if (unref(layoutTheme)) {
@@ -162,7 +130,7 @@ function onReset() {
   router.push("/login");
   const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
   useAppStoreHook().setLayout(Layout);
-  useEpThemeStoreHook().setEpThemeColor(EpThemeColor);
+  setEpThemeColor(EpThemeColor);
   useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
   toggleClass(Grey, "html-grey", document.querySelector("html"));
   toggleClass(Weak, "html-weakness", document.querySelector("html"));
@@ -245,75 +213,6 @@ function setLayoutModel(layout: string) {
   useAppStoreHook().setLayout(layout);
 }
 
-/** 设置导航主题色 */
-function setLayoutThemeColor(theme: string) {
-  layoutTheme.value.theme = theme;
-  toggleTheme({
-    scopeName: `layout-theme-${theme}`
-  });
-  instance.layout = {
-    layout: useAppStoreHook().layout,
-    theme,
-    darkMode: dataTheme.value,
-    sidebarStatus: instance.layout.sidebarStatus,
-    epThemeColor: instance.layout.epThemeColor
-  };
-
-  if (theme === "default" || theme === "light") {
-    setEpThemeColor(getConfig().EpThemeColor);
-  } else {
-    const colors = find(themeColors.value, { themeColor: theme });
-    setEpThemeColor(colors.color);
-  }
-}
-
-/**
- * @description 自动计算hover和active颜色
- * @see {@link https://element-plus.org/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2}
- */
-const shadeBgColor = (color: string): string => {
-  return new TinyColor(color).shade(10).toString();
-};
-
-/** 设置ep主题色 */
-const setEpThemeColor = (color: string) => {
-  useEpThemeStoreHook().setEpThemeColor(color);
-  body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
-  document.documentElement.style.setProperty("--el-color-primary", color);
-  for (let i = 1; i <= 9; i++) {
-    document.documentElement.style.setProperty(
-      `--el-color-primary-light-${i}`,
-      lighten(color, i / 10)
-    );
-  }
-  for (let i = 1; i <= 2; i++) {
-    document.documentElement.style.setProperty(
-      `--el-color-primary-dark-${i}`,
-      darken(color, i / 10)
-    );
-  }
-};
-
-let dataTheme = ref<boolean>(instance.layout.darkMode);
-
-/** 日间、夜间主题切换 */
-function dataThemeChange() {
-  /* 如果当前是light夜间主题,默认切换到default主题 */
-  if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
-    setLayoutThemeColor("default");
-  } else {
-    setLayoutThemeColor(useEpThemeStoreHook().epTheme);
-  }
-
-  if (dataTheme.value) {
-    instance.layout.darkMode = true;
-    document.documentElement.classList.add("dark");
-  } else {
-    instance.layout.darkMode = false;
-    document.documentElement.classList.remove("dark");
-  }
-}
-
 /* 初始化项目配置 */
 nextTick(() => {
   settings.greyVal &&

+ 0 - 2
src/layout/components/tag/index.scss

@@ -173,8 +173,6 @@
 }
 
 .el-dropdown-menu {
-  padding: 0;
-
   li {
     width: 100%;
     margin: 0;

+ 1 - 1
src/layout/hooks/useBoolean.ts

@@ -1,6 +1,6 @@
 import { ref } from "vue";
 
-export default function useBoolean(initValue = false) {
+export function useBoolean(initValue = false) {
   const bool = ref(initValue);
 
   function setBool(value: boolean) {

+ 120 - 0
src/layout/hooks/useDataThemeChange.ts

@@ -0,0 +1,120 @@
+import { getConfig } from "/@/config";
+import { find } from "lodash-unified";
+import { useLayout } from "./useLayout";
+import { themeColorsType } from "../types";
+import { TinyColor } from "@ctrl/tinycolor";
+import { ref, getCurrentInstance } from "vue";
+import { useAppStoreHook } from "/@/store/modules/app";
+import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
+import {
+  darken,
+  lighten,
+  toggleTheme
+} from "@pureadmin/theme/dist/browser-utils";
+
+export function useDataThemeChange() {
+  const { layoutTheme } = useLayout();
+  const themeColors = ref<Array<themeColorsType>>([
+    /* 道奇蓝(默认) */
+    { color: "#1b2a47", themeColor: "default" },
+    /* 亮白色 */
+    { color: "#ffffff", themeColor: "light" },
+    /* 猩红色 */
+    { color: "#f5222d", themeColor: "dusk" },
+    /* 橙红色 */
+    { color: "#fa541c", themeColor: "volcano" },
+    /* 金色 */
+    { color: "#fadb14", themeColor: "yellow" },
+    /* 绿宝石 */
+    { color: "#13c2c2", themeColor: "mingQing" },
+    /* 酸橙绿 */
+    { color: "#52c41a", themeColor: "auroraGreen" },
+    /* 深粉色 */
+    { color: "#eb2f96", themeColor: "pink" },
+    /* 深紫罗兰色 */
+    { color: "#722ed1", themeColor: "saucePurple" }
+  ]);
+
+  const body = document.documentElement as HTMLElement;
+  const instance =
+    getCurrentInstance().appContext.app.config.globalProperties.$storage;
+
+  /** 设置导航主题色 */
+  function setLayoutThemeColor(theme = "default") {
+    layoutTheme.value.theme = theme;
+    toggleTheme({
+      scopeName: `layout-theme-${theme}`
+    });
+    instance.layout = {
+      layout: useAppStoreHook().layout,
+      theme,
+      darkMode: dataTheme.value,
+      sidebarStatus: instance.layout.sidebarStatus,
+      epThemeColor: instance.layout.epThemeColor
+    };
+
+    if (theme === "default" || theme === "light") {
+      setEpThemeColor(getConfig().EpThemeColor);
+    } else {
+      const colors = find(themeColors.value, { themeColor: theme });
+      setEpThemeColor(colors.color);
+    }
+  }
+
+  /**
+   * @description 自动计算hover和active颜色
+   * @see {@link https://element-plus.org/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2}
+   */
+  const shadeBgColor = (color: string): string => {
+    return new TinyColor(color).shade(10).toString();
+  };
+
+  /** 设置ep主题色 */
+  const setEpThemeColor = (color: string) => {
+    useEpThemeStoreHook().setEpThemeColor(color);
+    body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
+    document.documentElement.style.setProperty("--el-color-primary", color);
+    for (let i = 1; i <= 9; i++) {
+      document.documentElement.style.setProperty(
+        `--el-color-primary-light-${i}`,
+        lighten(color, i / 10)
+      );
+    }
+    for (let i = 1; i <= 2; i++) {
+      document.documentElement.style.setProperty(
+        `--el-color-primary-dark-${i}`,
+        darken(color, i / 10)
+      );
+    }
+  };
+  const dataTheme = ref<boolean>(instance?.layout?.darkMode);
+
+  /** 日间、夜间主题切换 */
+  function dataThemeChange() {
+    /* 如果当前是light夜间主题,默认切换到default主题 */
+    if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
+      setLayoutThemeColor("default");
+    } else {
+      setLayoutThemeColor(useEpThemeStoreHook().epTheme);
+    }
+
+    if (dataTheme.value) {
+      instance.layout.darkMode = true;
+      document.documentElement.classList.add("dark");
+    } else {
+      instance.layout.darkMode = false;
+      document.documentElement.classList.remove("dark");
+    }
+  }
+
+  return {
+    body,
+    instance,
+    dataTheme,
+    layoutTheme,
+    themeColors,
+    dataThemeChange,
+    setEpThemeColor,
+    setLayoutThemeColor
+  };
+}

+ 61 - 0
src/layout/hooks/useLayout.ts

@@ -0,0 +1,61 @@
+import { useI18n } from "vue-i18n";
+import { routerArrays } from "../types";
+import { computed, getCurrentInstance } from "vue";
+import { useMultiTagsStore } from "/@/store/modules/multiTags";
+
+export function useLayout() {
+  const instance = getCurrentInstance().appContext.app.config.globalProperties;
+
+  const initStorage = () => {
+    // 路由
+    if (
+      useMultiTagsStore().multiTagsCache &&
+      (!instance.$storage.tags || instance.$storage.tags.length === 0)
+    ) {
+      // eslint-disable-next-line vue/no-side-effects-in-computed-properties
+      instance.$storage.tags = routerArrays;
+    }
+    // 国际化
+    if (!instance.$storage.locale) {
+      // eslint-disable-next-line
+      instance.$storage.locale = { locale: instance.$config?.Locale ?? "zh" };
+      useI18n().locale.value = instance.$config?.Locale ?? "zh";
+    }
+    // 导航
+    if (!instance.$storage.layout) {
+      // eslint-disable-next-line vue/no-side-effects-in-computed-properties
+      instance.$storage.layout = {
+        layout: instance.$config?.Layout ?? "vertical",
+        theme: instance.$config?.Theme ?? "default",
+        darkMode: instance.$config?.DarkMode ?? false,
+        sidebarStatus: instance.$config?.SidebarStatus ?? true,
+        epThemeColor: instance.$config?.EpThemeColor ?? "#409EFF"
+      };
+    }
+    // 灰色模式、色弱模式、隐藏标签页
+    if (!instance.$storage.configure) {
+      // eslint-disable-next-line
+      instance.$storage.configure = {
+        grey: instance.$config?.Grey ?? false,
+        weak: instance.$config?.Weak ?? false,
+        hideTabs: instance.$config?.HideTabs ?? false,
+        showLogo: instance.$config?.ShowLogo ?? true,
+        showModel: instance.$config?.ShowModel ?? "smart",
+        multiTagsCache: instance.$config?.MultiTagsCache ?? false
+      };
+    }
+  };
+  // 清空缓存后从serverConfig.json读取默认配置并赋值到storage中
+  const layout = computed(() => {
+    return instance.$storage?.layout.layout;
+  });
+  const layoutTheme = computed(() => {
+    return instance.$storage.layout;
+  });
+  return {
+    layout,
+    instance,
+    layoutTheme,
+    initStorage
+  };
+}

+ 38 - 0
src/layout/hooks/useTranslationLang.ts

@@ -0,0 +1,38 @@
+import { useNav } from "./nav";
+import { useI18n } from "vue-i18n";
+import { useRoute } from "vue-router";
+import { watch, getCurrentInstance } from "vue";
+
+export function useTranslationLang() {
+  const { changeTitle, changeWangeditorLanguage } = useNav();
+  const { locale, t } = useI18n();
+  const route = useRoute();
+  const instance =
+    getCurrentInstance().appContext.config.globalProperties.$storage;
+
+  function translationCh() {
+    instance.locale = { locale: "zh" };
+    locale.value = "zh";
+  }
+
+  function translationEn() {
+    instance.locale = { locale: "en" };
+    locale.value = "en";
+  }
+
+  watch(
+    () => locale.value,
+    () => {
+      changeTitle(route.meta);
+      locale.value === "en"
+        ? changeWangeditorLanguage(locale.value)
+        : changeWangeditorLanguage("zh-CN");
+    }
+  );
+  return {
+    t,
+    locale,
+    translationCh,
+    translationEn
+  };
+}

+ 3 - 53
src/layout/index.vue

@@ -1,20 +1,11 @@
 <script setup lang="ts">
-import {
-  h,
-  reactive,
-  computed,
-  onMounted,
-  defineComponent,
-  getCurrentInstance
-} from "vue";
 import { setType } from "./types";
-import { useI18n } from "vue-i18n";
-import { routerArrays } from "./types";
 import { emitter } from "/@/utils/mitt";
+import { useLayout } from "./hooks/useLayout";
 import { useAppStoreHook } from "/@/store/modules/app";
 import { deviceDetection, useDark } from "@pureadmin/utils";
-import { useMultiTagsStore } from "/@/store/modules/multiTags";
 import { useSettingStoreHook } from "/@/store/modules/settings";
+import { h, reactive, computed, onMounted, defineComponent } from "vue";
 
 import backTop from "/@/assets/svg/back_top.svg?component";
 import fullScreen from "/@/assets/svg/full_screen.svg?component";
@@ -30,49 +21,8 @@ import Horizontal from "./components/sidebar/horizontal.vue";
 const { isDark } = useDark();
 const isMobile = deviceDetection();
 const pureSetting = useSettingStoreHook();
-const instance = getCurrentInstance().appContext.app.config.globalProperties;
 
-// 清空缓存后从serverConfig.json读取默认配置并赋值到storage中
-const layout = computed(() => {
-  // 路由
-  if (
-    useMultiTagsStore().multiTagsCache &&
-    (!instance.$storage.tags || instance.$storage.tags.length === 0)
-  ) {
-    // eslint-disable-next-line vue/no-side-effects-in-computed-properties
-    instance.$storage.tags = routerArrays;
-  }
-  // 国际化
-  if (!instance.$storage.locale) {
-    // eslint-disable-next-line
-    instance.$storage.locale = { locale: instance.$config?.Locale ?? "zh" };
-    useI18n().locale.value = instance.$config?.Locale ?? "zh";
-  }
-  // 导航
-  if (!instance.$storage.layout) {
-    // eslint-disable-next-line vue/no-side-effects-in-computed-properties
-    instance.$storage.layout = {
-      layout: instance.$config?.Layout ?? "vertical",
-      theme: instance.$config?.Theme ?? "default",
-      darkMode: instance.$config?.DarkMode ?? false,
-      sidebarStatus: instance.$config?.SidebarStatus ?? true,
-      epThemeColor: instance.$config?.EpThemeColor ?? "#409EFF"
-    };
-  }
-  // 灰色模式、色弱模式、隐藏标签页
-  if (!instance.$storage.configure) {
-    // eslint-disable-next-line
-    instance.$storage.configure = {
-      grey: instance.$config?.Grey ?? false,
-      weak: instance.$config?.Weak ?? false,
-      hideTabs: instance.$config?.HideTabs ?? false,
-      showLogo: instance.$config?.ShowLogo ?? true,
-      showModel: instance.$config?.ShowModel ?? "smart",
-      multiTagsCache: instance.$config?.MultiTagsCache ?? false
-    };
-  }
-  return instance.$storage?.layout.layout;
-});
+const { instance, layout } = useLayout();
 
 const set: setType = reactive({
   sidebar: computed(() => {

+ 1 - 1
src/style/element-plus.scss

@@ -25,7 +25,7 @@
 }
 
 .el-dropdown-menu {
-  padding: 2px 0 !important;
+  padding: 0 !important;
 }
 
 .el-range-separator {

+ 191 - 114
src/views/login/index.vue

@@ -7,15 +7,23 @@ import qrCode from "./components/qrCode.vue";
 import regist from "./components/regist.vue";
 import update from "./components/update.vue";
 import { initRouter } from "/@/router/utils";
+import { useNav } from "/@/layout/hooks/nav";
 import { message } from "@pureadmin/components";
 import type { FormInstance } from "element-plus";
 import { storageSession } from "@pureadmin/utils";
-import { ref, reactive, watch, computed, getCurrentInstance } from "vue";
 import { operates, thirdParty } from "./utils/enums";
+import { useLayout } from "/@/layout/hooks/useLayout";
 import { useUserStoreHook } from "/@/store/modules/user";
 import { bg, avatar, currentWeek } from "./utils/static";
 import { ReImageVerify } from "/@/components/ReImageVerify";
 import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
+import { useTranslationLang } from "/@/layout/hooks/useTranslationLang";
+import { useDataThemeChange } from "/@/layout/hooks/useDataThemeChange";
+import { ref, reactive, watch, computed, getCurrentInstance } from "vue";
+
+import dayIcon from "/@/assets/svg/day.svg?component";
+import darkIcon from "/@/assets/svg/dark.svg?component";
+import globalization from "/@/assets/svg/globalization.svg?component";
 
 defineOptions({
   name: "Login"
@@ -31,6 +39,13 @@ const currentPage = computed(() => {
   return useUserStoreHook().currentPage;
 });
 
+const { initStorage } = useLayout();
+initStorage();
+
+const { dataTheme, dataThemeChange } = useDataThemeChange();
+const { getDropdownItemStyle, getDropdownItemClass } = useNav();
+const { locale, translationCh, translationEn } = useTranslationLang();
+
 const ruleForm = reactive({
   username: "admin",
   password: "admin123",
@@ -67,138 +82,184 @@ function onHandle(value) {
 watch(imgCode, value => {
   useUserStoreHook().SET_VERIFYCODE(value);
 });
+
+dataThemeChange();
 </script>
 
 <template>
-  <img :src="bg" class="wave" />
-  <div class="login-container">
-    <div class="img">
-      <component :is="currentWeek" />
-    </div>
-    <div class="login-box">
-      <div class="login-form">
-        <avatar class="avatar" />
-        <Motion>
-          <h2>{{ title }}</h2>
-        </Motion>
-
-        <el-form
-          v-if="currentPage === 0"
-          ref="ruleFormRef"
-          :model="ruleForm"
-          :rules="loginRules"
-          size="large"
-          @keyup.enter="onLogin(ruleFormRef)"
-        >
-          <Motion :delay="100">
-            <el-form-item prop="username">
-              <el-input
-                clearable
-                v-model="ruleForm.username"
-                placeholder="账号"
-                :prefix-icon="useRenderIcon('user')"
+  <div class="wh-full select-none">
+    <img :src="bg" class="wave" />
+    <div class="flex-c absolute right-5 top-3">
+      <!-- 主题 -->
+      <el-switch
+        v-model="dataTheme"
+        inline-prompt
+        :active-icon="dayIcon"
+        :inactive-icon="darkIcon"
+        @change="dataThemeChange"
+      />
+      <!-- 国际化 -->
+      <el-dropdown trigger="click">
+        <globalization
+          class="hover:color-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
+              :style="getDropdownItemStyle(locale, 'zh')"
+              :class="['!dark:color-white', getDropdownItemClass(locale, 'zh')]"
+              @click="translationCh"
+            >
+              <IconifyIconOffline
+                class="check-zh"
+                v-show="locale === 'zh'"
+                icon="check"
               />
-            </el-form-item>
+              简体中文
+            </el-dropdown-item>
+            <el-dropdown-item
+              :style="getDropdownItemStyle(locale, 'en')"
+              :class="['!dark:color-white', getDropdownItemClass(locale, 'en')]"
+              @click="translationEn"
+            >
+              <span class="check-en" v-show="locale === '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="currentWeek" />
+      </div>
+      <div class="login-box">
+        <div class="login-form">
+          <avatar class="avatar" />
+          <Motion>
+            <h2 class="outline-none">{{ title }}</h2>
           </Motion>
 
-          <Motion :delay="150">
-            <el-form-item prop="password">
-              <el-input
-                clearable
-                show-password
-                v-model="ruleForm.password"
-                placeholder="密码"
-                :prefix-icon="useRenderIcon('lock')"
-              />
-            </el-form-item>
-          </Motion>
+          <el-form
+            v-if="currentPage === 0"
+            ref="ruleFormRef"
+            :model="ruleForm"
+            :rules="loginRules"
+            size="large"
+            @keyup.enter="onLogin(ruleFormRef)"
+          >
+            <Motion :delay="100">
+              <el-form-item prop="username">
+                <el-input
+                  clearable
+                  v-model="ruleForm.username"
+                  placeholder="账号"
+                  :prefix-icon="useRenderIcon('user')"
+                />
+              </el-form-item>
+            </Motion>
 
-          <Motion :delay="200">
-            <el-form-item prop="verifyCode">
-              <el-input
-                clearable
-                v-model="ruleForm.verifyCode"
-                placeholder="验证码"
-                :prefix-icon="
-                  useRenderIcon('ri:shield-keyhole-line', { online: true })
-                "
-              >
-                <template v-slot:append>
-                  <ReImageVerify v-model:code="imgCode" />
-                </template>
-              </el-input>
-            </el-form-item>
-          </Motion>
+            <Motion :delay="150">
+              <el-form-item prop="password">
+                <el-input
+                  clearable
+                  show-password
+                  v-model="ruleForm.password"
+                  placeholder="密码"
+                  :prefix-icon="useRenderIcon('lock')"
+                />
+              </el-form-item>
+            </Motion>
 
-          <Motion :delay="250">
-            <el-form-item>
-              <div class="w-full h-20px flex justify-between items-center">
-                <el-checkbox v-model="checked">记住密码</el-checkbox>
+            <Motion :delay="200">
+              <el-form-item prop="verifyCode">
+                <el-input
+                  clearable
+                  v-model="ruleForm.verifyCode"
+                  placeholder="验证码"
+                  :prefix-icon="
+                    useRenderIcon('ri:shield-keyhole-line', { online: true })
+                  "
+                >
+                  <template v-slot:append>
+                    <ReImageVerify v-model:code="imgCode" />
+                  </template>
+                </el-input>
+              </el-form-item>
+            </Motion>
+
+            <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-button
+                    link
+                    type="primary"
+                    @click="useUserStoreHook().SET_CURRENTPAGE(4)"
+                  >
+                    忘记密码?
+                  </el-button>
+                </div>
                 <el-button
-                  link
+                  class="w-full mt-4"
+                  size="default"
                   type="primary"
-                  @click="useUserStoreHook().SET_CURRENTPAGE(4)"
+                  :loading="loading"
+                  @click="onLogin(ruleFormRef)"
                 >
-                  忘记密码?
+                  登录
                 </el-button>
-              </div>
-              <el-button
-                class="w-full mt-4"
-                size="default"
-                type="primary"
-                :loading="loading"
-                @click="onLogin(ruleFormRef)"
-              >
-                登录
-              </el-button>
-            </el-form-item>
-          </Motion>
+              </el-form-item>
+            </Motion>
 
-          <Motion :delay="300">
+            <Motion :delay="300">
+              <el-form-item>
+                <div class="w-full h-20px flex justify-between items-center">
+                  <el-button
+                    v-for="(item, index) in operates"
+                    :key="index"
+                    class="w-full mt-4"
+                    size="default"
+                    @click="onHandle(index + 1)"
+                  >
+                    {{ item.title }}
+                  </el-button>
+                </div>
+              </el-form-item>
+            </Motion>
+          </el-form>
+
+          <Motion v-if="currentPage === 0" :delay="350">
             <el-form-item>
-              <div class="w-full h-20px flex justify-between items-center">
-                <el-button
-                  v-for="(item, index) in operates"
+              <el-divider>
+                <p class="text-gray-500 text-xs">第三方登录</p>
+              </el-divider>
+              <div class="w-full flex justify-evenly">
+                <span
+                  v-for="(item, index) in thirdParty"
                   :key="index"
-                  class="w-full mt-4"
-                  size="default"
-                  @click="onHandle(index + 1)"
+                  :title="`${item.title}登录`"
                 >
-                  {{ item.title }}
-                </el-button>
+                  <IconifyIconOnline
+                    :icon="`ri:${item.icon}-fill`"
+                    width="20"
+                    class="cursor-pointer text-gray-500 hover:text-blue-400"
+                  />
+                </span>
               </div>
             </el-form-item>
           </Motion>
-        </el-form>
-
-        <Motion v-if="currentPage === 0" :delay="350">
-          <el-form-item>
-            <el-divider>
-              <p class="text-gray-500 text-xs">第三方登录</p>
-            </el-divider>
-            <div class="w-full flex justify-evenly">
-              <span
-                v-for="(item, index) in thirdParty"
-                :key="index"
-                :title="`${item.title}登录`"
-              >
-                <IconifyIconOnline
-                  :icon="`ri:${item.icon}-fill`"
-                  width="20"
-                  class="cursor-pointer text-gray-500 hover:text-blue-400"
-                />
-              </span>
-            </div>
-          </el-form-item>
-        </Motion>
-        <!-- 手机号登录 -->
-        <phone v-if="currentPage === 1" />
-        <!-- 二维码登录 -->
-        <qrCode v-if="currentPage === 2" />
-        <!-- 注册 -->
-        <regist v-if="currentPage === 3" />
-        <!-- 忘记密码 -->
-        <update v-if="currentPage === 4" />
+          <!-- 手机号登录 -->
+          <phone v-if="currentPage === 1" />
+          <!-- 二维码登录 -->
+          <qrCode v-if="currentPage === 2" />
+          <!-- 注册 -->
+          <regist v-if="currentPage === 3" />
+          <!-- 忘记密码 -->
+          <update v-if="currentPage === 4" />
+        </div>
       </div>
     </div>
   </div>
@@ -212,4 +273,20 @@ watch(imgCode, value => {
 :deep(.el-input-group__append, .el-input-group__prepend) {
   padding: 0;
 }
+
+.translation {
+  ::v-deep(.el-dropdown-menu__item) {
+    padding: 5px 40px;
+  }
+
+  .check-zh {
+    position: absolute;
+    left: 20px;
+  }
+
+  .check-en {
+    position: absolute;
+    left: 20px;
+  }
+}
 </style>