Bläddra i källkod

refactor: refactor login page

vben 4 år sedan
förälder
incheckning
ec9478f76f

+ 1 - 6
.vscode/extensions.json

@@ -1,19 +1,14 @@
 {
   "recommendations": [
-    "johnsoncodehk.volar",
     "octref.vetur",
     "dbaeumer.vscode-eslint",
     "stylelint.vscode-stylelint",
-    "DavidAnson.vscode-markdownlint",
     "esbenp.prettier-vscode",
     "mrmlnc.vscode-less",
     "antfu.i18n-ally",
-    "cpylua.language-postcss",
-    "Orta.vscode-jest",
     "antfu.iconify",
     "mikestead.dotenv",
     "bradlc.vscode-tailwindcss",
-    "heybourn.headwind",
-    "znck.vue-language-features"
+    "heybourn.headwind"
   ]
 }

+ 5 - 0
.vscode/i18n-ally-reviews.yml

@@ -0,0 +1,5 @@
+# Review comments generated by i18n-ally. Please commit this file.
+
+reviews:
+  sys.login.autoLogin:
+    description: '1'

+ 5 - 0
CHANGELOG.zh_CN.md

@@ -1,9 +1,14 @@
 ## Wip
 
+### ✨ Refactor
+
+- 登录页重构,新增注册页面/重置密码页面/手机登录/二维码登录
+
 ### ✨ Features
 
 - 新增 `settingButtonPosition`配置项,用于配置`设置`按钮位置
 - `modal`可以通过双击头部切换全屏
+- 新增`CountDownInput`组件
 
 ### ⚡ Performance Improvements
 

+ 1 - 1
build/config/themeConfig.ts

@@ -94,7 +94,7 @@ export function generateModifyVars() {
     'disabled-color': 'rgba(0, 0, 0, 0.25)', //  Failure color
     'heading-color': 'rgba(0, 0, 0, 0.85)', //  Title color
     'text-color': 'rgba(0, 0, 0, 0.85)', //  Main text color
-    'text-color-secondary ': 'rgba(0, 0, 0, 0.45)', // Subtext color
+    'text-color-secondary': 'rgba(0, 0, 0, 0.45)', // Subtext color
     'font-size-base': '14px', //  Main font size
     'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', //  Floating shadow
     'border-color-base': '#d9d9d9', //  Border color,

+ 5 - 5
package.json

@@ -43,7 +43,7 @@
     "vditor": "^3.8.1",
     "vue": "^3.0.5",
     "vue-i18n": "9.0.0-rc.2",
-    "vue-router": "^4.0.3",
+    "vue-router": "^4.0.4",
     "vue-types": "^3.0.2",
     "vuex": "^4.0.0",
     "vuex-module-decorators": "^1.0.1",
@@ -92,7 +92,7 @@
     "pretty-quick": "^3.1.0",
     "rimraf": "^3.0.2",
     "rollup-plugin-visualizer": "^4.2.0",
-    "stylelint": "^13.10.0",
+    "stylelint": "^13.11.0",
     "stylelint-config-prettier": "^8.0.2",
     "stylelint-config-standard": "^20.0.0",
     "stylelint-order": "^4.1.0",
@@ -104,10 +104,10 @@
     "vite-plugin-imagemin": "^0.2.6",
     "vite-plugin-mock": "^2.1.4",
     "vite-plugin-purge-icons": "^0.7.0",
-    "vite-plugin-pwa": "^0.5.1",
-    "vite-plugin-style-import": "^0.7.2",
+    "vite-plugin-pwa": "^0.5.2",
+    "vite-plugin-style-import": "^0.7.3",
     "vite-plugin-theme": "^0.4.3",
-    "vite-plugin-windicss": "0.3.12",
+    "vite-plugin-windicss": "0.4.3",
     "vue-eslint-parser": "^7.5.0",
     "yargs": "^16.2.0"
   },

BIN
src/assets/images/login/login-bg.png


BIN
src/assets/images/login/login-in.png


+ 17 - 0
src/assets/svg/login-bg.svg

@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="6395" height="1079" viewBox="0 0 6395 1079">
+  <defs>
+    <clipPath id="clip-path">
+      <rect id="Rectangle_73" data-name="Rectangle 73" width="6395" height="1079" transform="translate(-5391)" fill="#fff"/>
+    </clipPath>
+    <linearGradient id="linear-gradient" x1="0.747" y1="0.222" x2="0.973" y2="0.807" gradientUnits="objectBoundingBox">
+      <stop offset="0" stop-color="#2b51b4"/>
+      <stop offset="1" stop-color="#1c3faa"/>
+    </linearGradient>
+  </defs>
+  <g id="Mask_Group_1" data-name="Mask Group 1" transform="translate(5391)" clip-path="url(#clip-path)">
+    <g id="Group_118" data-name="Group 118" transform="translate(-419.333 -1.126)">
+      <path id="Path_142" data-name="Path 142" d="M6271.734-6.176s-222.478,187.809-55.349,583.254c44.957,106.375,81.514,205.964,84.521,277,8.164,192.764-156.046,268.564-156.046,268.564l-653.53-26.8L5475.065-21.625Z" transform="translate(-4876.383 0)" fill="#f1f5f8"/>
+      <path id="Union_6" data-name="Union 6" d="M-2631.1,1081.8v-1.6H-8230.9V.022H-2631.1V0H-1871.4s-187.845,197.448-91.626,488.844c49.167,148.9,96.309,256.289,104.683,362.118,7.979,100.852-57.98,201.711-168.644,254.286-65.858,31.29-144.552,42.382-223.028,42.383C-2441.2,1147.632-2631.1,1081.8-2631.1,1081.8Z" transform="translate(3259.524 0.803)" fill="url(#linear-gradient)"/>
+    </g>
+  </g>
+</svg>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
src/assets/svg/login-box-bg.svg


+ 8 - 2
src/components/Application/src/AppLogo.vue

@@ -10,8 +10,13 @@
   >
     <img src="../../../assets/images/logo.png" />
     <div
-      class="ml-2 truncate xs:opacity-0 md:opacity-100"
-      :class="`${prefixCls}__title`"
+      class="ml-2 truncate md:opacity-100"
+      :class="[
+        `${prefixCls}__title`,
+        {
+          'xs:opacity-0': !alwaysShowTitle,
+        },
+      ]"
       v-show="showTitle"
     >
       {{ title }}
@@ -38,6 +43,7 @@
       theme: propTypes.oneOf(['light', 'dark']),
       // Whether to show title
       showTitle: propTypes.bool.def(true),
+      alwaysShowTitle: propTypes.bool.def(false),
     },
     setup() {
       const { prefixCls } = useDesign('app-logo');

+ 4 - 0
src/components/CountDown/index.ts

@@ -0,0 +1,4 @@
+import CountButton from './src/CountButton.vue';
+import CountdownInput from './src/CountdownInput.vue';
+
+export { CountdownInput, CountButton };

+ 57 - 0
src/components/CountDown/src/CountButton.vue

@@ -0,0 +1,57 @@
+<template>
+  <Button v-bind="$attrs" :disabled="isStart" @click="handleStart" :loading="loading">
+    {{
+      !isStart
+        ? t('component.countdown.normalText')
+        : t('component.countdown.sendText', [currentCount])
+    }}
+  </Button>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, PropType } from 'vue';
+
+  import { Button } from 'ant-design-vue';
+
+  import { useCountdown } from './useCountdown';
+  import { isFunction } from '/@/utils/is';
+  import { useI18n } from '/@/hooks/web/useI18n';
+
+  export default defineComponent({
+    name: 'CountButton',
+    components: { Button },
+    props: {
+      count: {
+        type: Number,
+        default: 60,
+      },
+      beforeStartFunc: {
+        type: Function as PropType<() => boolean>,
+        default: null,
+      },
+    },
+    setup(props) {
+      const loading = ref(false);
+
+      const { currentCount, isStart, start } = useCountdown(props.count);
+      const { t } = useI18n();
+      /**
+       * @description: Judge whether there is an external function before execution, and decide whether to start after execution
+       */
+      async function handleStart() {
+        const { beforeStartFunc } = props;
+        if (beforeStartFunc && isFunction(beforeStartFunc)) {
+          loading.value = true;
+          try {
+            const canStart = await beforeStartFunc();
+            canStart && start();
+          } finally {
+            loading.value = false;
+          }
+        } else {
+          start();
+        }
+      }
+      return { handleStart, isStart, currentCount, loading, t };
+    },
+  });
+</script>

+ 55 - 0
src/components/CountDown/src/CountdownInput.vue

@@ -0,0 +1,55 @@
+<template>
+  <div :class="prefixCls">
+    <AInput v-bind="$attrs" :size="size" v-model:value="state">
+      <template #addonAfter>
+        <CountButton :size="size" :count="count" :beforeStartFunc="sendCodeApi" />
+      </template>
+    </AInput>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  import { Input } from 'ant-design-vue';
+  import CountButton from './CountButton.vue';
+
+  import { propTypes } from '/@/utils/propTypes';
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  import { useRuleFormItem } from '/@/hooks/component/useFormItem';
+
+  export default defineComponent({
+    name: 'CountDownInput',
+    components: { [Input.name]: Input, CountButton },
+    props: {
+      value: propTypes.string,
+      size: propTypes.oneOf(['default', 'large', 'small']),
+      count: propTypes.number.def(60),
+      sendCodeApi: {
+        type: Function as PropType<() => boolean>,
+        default: null,
+      },
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('countdown-input');
+
+      const [state] = useRuleFormItem(props);
+      return { prefixCls, state };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-countdown-input';
+
+  .@{prefix-cls} {
+    .ant-input-group-addon {
+      padding-right: 0;
+      background-color: transparent;
+      border: none;
+
+      button {
+        font-size: 14px;
+      }
+    }
+  }
+</style>

+ 51 - 0
src/components/CountDown/src/useCountdown.ts

@@ -0,0 +1,51 @@
+import { ref, unref } from 'vue';
+import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
+
+export function useCountdown(count: number) {
+  const currentCount = ref(count);
+
+  const isStart = ref(false);
+
+  let timerId: ReturnType<typeof setInterval> | null;
+
+  function clear() {
+    timerId && window.clearInterval(timerId);
+  }
+
+  function stop() {
+    isStart.value = false;
+    timerId = null;
+    clear();
+  }
+
+  function start() {
+    if (unref(isStart) || !!timerId) {
+      return;
+    }
+    isStart.value = true;
+    timerId = setInterval(() => {
+      if (unref(currentCount) === 1) {
+        stop();
+        currentCount.value = count;
+      } else {
+        currentCount.value -= 1;
+      }
+    }, 1000);
+  }
+
+  function reset() {
+    currentCount.value = count;
+    stop();
+  }
+
+  function restart() {
+    reset();
+    start();
+  }
+
+  tryOnUnmounted(() => {
+    reset();
+  });
+
+  return { start, reset, restart, clear, stop, currentCount, isStart };
+}

+ 7 - 9
src/components/StrengthMeter/src/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div :class="prefixCls">
+  <div :class="prefixCls" class="relative">
     <InputPassword
       v-if="showInput"
       v-bind="$attrs"
@@ -24,15 +24,14 @@
   import { Input } from 'ant-design-vue';
 
   import zxcvbn from '@zxcvbn-ts/core';
-  import { propTypes } from '/@/utils/propTypes';
   import { useDesign } from '/@/hooks/web/useDesign';
+  import { propTypes } from '/@/utils/propTypes';
 
   export default defineComponent({
     name: 'StrengthMeter',
     components: { InputPassword: Input.Password },
     props: {
       value: propTypes.string,
-
       showInput: propTypes.bool.def(true),
       disabled: propTypes.bool,
     },
@@ -43,9 +42,9 @@
 
       const getPasswordStrength = computed(() => {
         const { disabled } = props;
-        if (disabled) return null;
+        if (disabled) return -1;
         const innerValue = unref(innerValueRef);
-        const score = innerValue ? zxcvbn(unref(innerValueRef)).score : null;
+        const score = innerValue ? zxcvbn(unref(innerValueRef)).score : -1;
         emit('score-change', score);
         return score;
       });
@@ -57,6 +56,7 @@
       watchEffect(() => {
         innerValueRef.value = props.value || '';
       });
+
       watch(
         () => unref(innerValueRef),
         (val) => {
@@ -77,14 +77,12 @@
   @prefix-cls: ~'@{namespace}-strength-meter';
 
   .@{prefix-cls} {
-    position: relative;
-
     &-bar {
       position: relative;
-      height: 4px;
+      height: 6px;
       margin: 10px auto 6px;
       background: @disabled-color;
-      border-radius: 3px;
+      border-radius: 6px;
 
       &::before,
       &::after {

+ 2 - 4
src/design/ant/index.less

@@ -13,10 +13,8 @@
   }
 }
 
-body {
-  .anticon:not(.app-iconify) {
-    vertical-align: 0.1em;
-  }
+span.anticon:not(.app-iconify) {
+  vertical-align: 0.125em;
 }
 
 .ant-back-top {

+ 4 - 24
src/design/var/breakpoint.less

@@ -2,37 +2,17 @@
 // ==============屏幕断点============
 // =================================
 
-// Extra small screen / phone
-@screen-xs: 480px;
-@screen-xs-min: @screen-xs;
-
 // Small screen / tablet
-@screen-sm: 576px;
-@screen-sm-min: @screen-sm;
+@screen-sm: 640px;
 
 // Medium screen / desktop
 @screen-md: 768px;
-@screen-md-min: @screen-md;
 
 // Large screen / wide desktop
-@screen-lg: 992px;
-@screen-lg-min: @screen-lg;
+@screen-lg: 1024px;
 
 // Extra large screen / full hd
-@screen-xl: 1200px;
-@screen-xl-min: @screen-xl;
+@screen-xl: 1280px;
 
 // Extra extra large screen / large desktop
-@screen-xxl: 1600px;
-@screen-xxl-min: @screen-xxl;
-
-@screen-xxxl: 1900px;
-@screen-xxxl-min: @screen-xxxl;
-
-// provide a maximum
-@screen-xs-max: (@screen-sm-min - 1px);
-@screen-sm-max: (@screen-md-min - 1px);
-@screen-md-max: (@screen-lg-min - 1px);
-@screen-lg-max: (@screen-xl-min - 1px);
-@screen-xl-max: (@screen-xxl-min - 1px);
-@screen-xxl-max: (@screen-xxxl-min - 1px);
+@screen-2xl: 1536px;

+ 1 - 1
src/layouts/default/header/components/user-dropdown/index.vue

@@ -3,7 +3,7 @@
     <span :class="[prefixCls, `${prefixCls}--${theme}`]">
       <img :class="`${prefixCls}__header`" :src="headerImg" />
       <span :class="`${prefixCls}__info`">
-        <span :class="`${prefixCls}__name anticon`">{{ getUserInfo.realName }}</span>
+        <span :class="`${prefixCls}__name`" class="truncate">{{ getUserInfo.realName }}</span>
       </span>
     </span>
 

+ 4 - 0
src/locales/lang/en/component/countdown.ts

@@ -0,0 +1,4 @@
+export default {
+  normalText: 'Get SMS code',
+  sendText: 'Reacquire in {0}s',
+};

+ 30 - 5
src/locales/lang/en/sys/login.ts

@@ -1,13 +1,38 @@
 export default {
-  loginButton: 'Login',
-  autoLogin: 'AutoLogin',
-  forgetPassword: 'Forget Password',
+  backSignIn: 'Back sign in',
+  mobileSignInFormTitle: 'Mobile sign in',
+  qrSignInFormTitle: 'Qr code sign in',
+  signInFormTitle: 'Sign in',
+  signUpFormTitle: 'Sign up',
+  forgetFormTitle: 'Reset password',
+
+  signInTitle: 'Backstage management system',
+  signInDesc: 'Enter your personal details and get started!',
+  policy: 'I agree to the xxx Privacy Policy',
+  scanSign: `scanning the code to complete the login`,
+
+  loginButton: 'Sign in',
+  registerButton: 'Sign up',
+  rememberMe: 'Remember me',
+  forgetPassword: 'Forget Password?',
+  otherSignIn: 'Sign in with',
 
   // notify
   loginSuccessTitle: 'Login successful',
   loginSuccessDesc: 'Welcome back',
 
   // placeholder
-  accountPlaceholder: 'Please input Username',
-  passwordPlaceholder: 'Please input Password',
+  accountPlaceholder: 'Please input username',
+  passwordPlaceholder: 'Please input password',
+  smsPlaceholder: 'Please input sms code',
+  mobilePlaceholder: 'Please input mobile',
+  policyPlaceholder: 'Register after checking',
+  diffPwd: 'The two passwords are inconsistent',
+
+  userName: 'Username',
+  password: 'Password',
+  confirmPassword: 'Confirm Password',
+  email: 'Email',
+  smsCode: 'SMS code',
+  mobile: 'Mobile',
 };

+ 4 - 0
src/locales/lang/zh_CN/component/countdown.ts

@@ -0,0 +1,4 @@
+export default {
+  normalText: '获取验证码',
+  sendText: '{0}秒后重新获取',
+};

+ 27 - 2
src/locales/lang/zh_CN/sys/login.ts

@@ -1,7 +1,21 @@
 export default {
+  backSignIn: '返回',
+  signInFormTitle: '登录',
+  mobileSignInFormTitle: '手机登录',
+  qrSignInFormTitle: '二维码登录',
+  signUpFormTitle: '注册',
+  forgetFormTitle: '重置密码',
+
+  signInTitle: '开箱即用的中后台管理系统',
+  signInDesc: '输入您的个人详细信息开始使用!',
+  policy: '我同意xxx隐私政策',
+  scanSign: `扫码后点击"确认",即可完成登录`,
+
   loginButton: '登录',
-  autoLogin: '自动登录',
-  forgetPassword: '忘记密码',
+  registerButton: '注册',
+  rememberMe: '记住我',
+  forgetPassword: '忘记密码?',
+  otherSignIn: '其他登录方式',
 
   // notify
   loginSuccessTitle: '登录成功',
@@ -10,4 +24,15 @@ export default {
   // placeholder
   accountPlaceholder: '请输入账号',
   passwordPlaceholder: '请输入密码',
+  smsPlaceholder: '请输入验证码',
+  mobilePlaceholder: '请输入手机号码',
+  policyPlaceholder: '勾选后才能注册',
+  diffPwd: '两次输入密码不一致',
+
+  userName: '账号',
+  password: '密码',
+  confirmPassword: '确认密码',
+  email: '邮箱',
+  smsCode: '短信验证码',
+  mobile: '手机号码',
 };

+ 54 - 111
src/views/sys/lock/LockPage.vue

@@ -1,37 +1,51 @@
 <template>
-  <div :class="prefixCls">
-    <div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate">
+  <div
+    :class="prefixCls"
+    class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
+  >
+    <div
+      :class="`${prefixCls}__unlock`"
+      class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
+      @click="handleShowForm(false)"
+      v-show="showDate"
+    >
       <LockOutlined />
       <span>{{ t('sys.lock.unlock') }}</span>
     </div>
 
-    <div :class="`${prefixCls}__date`">
-      <div :class="`${prefixCls}__hour`">
-        {{ hour }}
-        <span class="meridiem" v-show="showDate">{{ meridiem }}</span>
+    <div class="flex w-screen h-screen justify-center items-center">
+      <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
+        <span>{{ hour }}</span>
+        <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
+          {{ meridiem }}
+        </span>
       </div>
-      <div :class="`${prefixCls}__minute`">
-        {{ minute }}
+      <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
+        <span> {{ minute }}</span>
       </div>
     </div>
     <transition name="fade-slide">
       <div :class="`${prefixCls}-entry`" v-show="!showDate">
         <div :class="`${prefixCls}-entry-content`">
-          <div :class="`${prefixCls}-entry__header`">
+          <div :class="`${prefixCls}-entry__header enter-x`">
             <img :src="headerImg" :class="`${prefixCls}-entry__header-img`" />
             <p :class="`${prefixCls}-entry__header-name`">
               {{ realName }}
             </p>
           </div>
-          <InputPassword :placeholder="t('sys.lock.placeholder')" v-model:value="password" />
-          <span :class="`${prefixCls}-entry__err-msg`" v-if="errMsgRef">
+          <InputPassword
+            :placeholder="t('sys.lock.placeholder')"
+            class="enter-x"
+            v-model:value="password"
+          />
+          <span :class="`${prefixCls}-entry__err-msg enter-x`" v-if="errMsgRef">
             {{ t('sys.lock.alert') }}
           </span>
-          <div :class="`${prefixCls}-entry__footer`">
+          <div :class="`${prefixCls}-entry__footer enter-x`">
             <a-button
               type="link"
               size="small"
-              class="mt-2 mr-2"
+              class="mt-2 mr-2 enter-x"
               :disabled="loadingRef"
               @click="handleShowForm(true)"
             >
@@ -40,7 +54,7 @@
             <a-button
               type="link"
               size="small"
-              class="mt-2 mr-2"
+              class="mt-2 mr-2 enter-x"
               :disabled="loadingRef"
               @click="goLogin"
             >
@@ -54,11 +68,11 @@
       </div>
     </transition>
 
-    <div :class="`${prefixCls}__footer-date`">
-      <div class="time" v-show="!showDate">
-        {{ hour }}:{{ minute }} <span class="meridiem">{{ meridiem }}</span>
+    <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
+      <div class="text-5xl mb-4 enter-x" v-show="!showDate">
+        {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
       </div>
-      <div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
+      <div class="text-2xl"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
     </div>
   </div>
 </template>
@@ -144,125 +158,54 @@
   @prefix-cls: ~'@{namespace}-lock-page';
 
   .@{prefix-cls} {
-    position: fixed;
-    top: 0;
-    right: 0;
-    bottom: 0;
-    left: 0;
     z-index: @lock-page-z-index;
-    display: flex;
-    width: 100vw;
-    height: 100vh;
-    // background: rgba(23, 27, 41);
-    background: #000;
-    align-items: center;
-    justify-content: center;
 
     &__unlock {
-      position: absolute;
-      top: 0;
-      left: 50%;
-      display: flex;
-      height: 50px;
-      padding-top: 20px;
-      font-size: 18px;
-      color: #fff;
-      cursor: pointer;
       transform: translate(-50%, 0);
-      flex-direction: column;
-      align-items: center;
-      justify-content: space-between;
-      transition: all 0.3s;
-    }
-
-    &__date {
-      display: flex;
-      width: 100vw;
-      height: 100vh;
-      align-items: center;
-      justify-content: center;
-    }
-
-    &__hour {
-      position: relative;
-      margin-right: 80px;
-
-      .meridiem {
-        position: absolute;
-        top: 20px;
-        left: 20px;
-        font-size: 26px;
-      }
-      @media (max-width: @screen-xs) {
-        margin-right: 20px;
-      }
     }
 
     &__hour,
     &__minute {
       display: flex;
-      width: 40%;
-      height: 74%;
       font-weight: 700;
       color: #bababa;
       background: #141313;
       border-radius: 30px;
       justify-content: center;
       align-items: center;
-      @media (min-width: @screen-xxxl-min) {
-        font-size: 46em;
-      }
-      @media (min-width: @screen-xl-max) and (max-width: @screen-xxl-max) {
-        font-size: 38em;
-      }
 
-      @media (min-width: @screen-lg-max) and (max-width: @screen-xl-max) {
-        font-size: 30em;
-      }
-      @media (min-width: @screen-md-max) and (max-width: @screen-lg-max) {
-        font-size: 23em;
+      @media screen and (max-width: @screen-md) {
+        span:not(.meridiem) {
+          font-size: 160px;
+        }
       }
-      @media (min-width: @screen-sm-max) and (max-width: @screen-md-max) {
-        height: 50%;
-        font-size: 12em;
-        border-radius: 10px;
 
-        .meridiem {
-          font-size: 20px;
+      @media screen and (min-width: @screen-md) {
+        span:not(.meridiem) {
+          font-size: 160px;
         }
       }
-      @media (min-width: @screen-xs-max) and (max-width: @screen-sm-max) {
-        font-size: 13em;
-      }
-      @media (max-width: @screen-xs) {
-        height: 30%;
-        font-size: 5em;
-        border-radius: 10px;
 
-        .meridiem {
-          font-size: 14px;
+      @media screen and (max-width: @screen-sm) {
+        span:not(.meridiem) {
+          font-size: 90px;
         }
       }
-    }
-
-    &__footer-date {
-      position: absolute;
-      bottom: 20px;
-      width: 100%;
-      font-family: helvetica;
-      color: #bababa;
-      text-align: center;
-
-      .time {
-        font-size: 50px;
-
-        .meridiem {
-          font-size: 32px;
+      @media screen and (min-width: @screen-lg) {
+        span:not(.meridiem) {
+          font-size: 220px;
         }
       }
 
-      .date {
-        font-size: 26px;
+      @media screen and (min-width: @screen-xl) {
+        span:not(.meridiem) {
+          font-size: 260px;
+        }
+      }
+      @media screen and (min-width: @screen-2xl) {
+        span:not(.meridiem) {
+          font-size: 320px;
+        }
       }
     }
 

+ 90 - 0
src/views/sys/login/ForgetPasswordForm.vue

@@ -0,0 +1,90 @@
+<template>
+  <Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
+    <FormItem name="account" class="enter-x">
+      <Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
+    </FormItem>
+
+    <FormItem name="mobile" class="enter-x">
+      <Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
+    </FormItem>
+    <FormItem name="sms" class="enter-x">
+      <CountdownInput
+        size="large"
+        v-model:value="formData.sms"
+        :placeholder="t('sys.login.smsCode')"
+      />
+    </FormItem>
+
+    <FormItem class="enter-x">
+      <Button
+        type="primary"
+        size="large"
+        block
+        @click="handleReset"
+        :loading="loading"
+        class="enter-x"
+      >
+        {{ t('common.resetText') }}
+      </Button>
+      <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
+        {{ t('sys.login.backSignIn') }}
+      </Button>
+    </FormItem>
+  </Form>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive, ref } from 'vue';
+
+  import { Form, Input, Button } from 'ant-design-vue';
+  import { CountdownInput } from '/@/components/CountDown';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
+
+  export default defineComponent({
+    name: 'ForgetPasswordForm',
+    components: {
+      Button,
+      Form,
+      FormItem: Form.Item,
+      Input,
+      CountdownInput,
+    },
+    setup() {
+      const { t } = useI18n();
+      const { setLoginState } = useLoginState();
+      const { getFormRules } = useFormRules();
+
+      const formRef = ref<any>(null);
+      const loading = ref(false);
+
+      const formData = reactive({
+        account: '',
+        mobile: '',
+        sms: '',
+      });
+
+      const { validForm } = useFormValid(formRef);
+
+      async function handleReset() {
+        const data = await validForm();
+        if (!data) return;
+        console.log(data);
+      }
+
+      function handleBackLogin() {
+        setLoginState(LoginStateEnum.LOGIN);
+      }
+
+      return {
+        t,
+        formRef,
+        formData,
+        getFormRules,
+        handleReset,
+        loading,
+        handleBackLogin,
+      };
+    },
+  });
+</script>

+ 134 - 183
src/views/sys/login/Login.vue

@@ -1,228 +1,179 @@
 <template>
-  <div class="login">
-    <div class="opacity-0 login-mask lg:opacity-100"></div>
-    <div class="justify-center login-form-wrap lg:justify-end">
-      <div class="mx-6 login-form">
-        <AppLocalePicker v-if="showLocale" class="login-form__locale" />
-        <div class="px-2 py-10 login-form__content">
-          <header>
-            <img :src="logo" class="mr-4" />
-            <h1>{{ title }}</h1>
-          </header>
-
-          <a-form class="login-form__main" :model="formData" :rules="formRules" ref="formRef">
-            <a-form-item name="account">
-              <a-input size="large" v-model:value="formData.account" placeholder="username: vben" />
-            </a-form-item>
-            <a-form-item name="password">
-              <a-input-password
-                size="large"
-                visibilityToggle
-                v-model:value="formData.password"
-                placeholder="password: 123456"
-              />
-            </a-form-item>
-
-            <a-row>
-              <a-col :span="12">
-                <a-form-item>
-                  <!-- No logic, you need to deal with it yourself -->
-                  <a-checkbox v-model:checked="autoLogin" size="small">{{
-                    t('sys.login.autoLogin')
-                  }}</a-checkbox>
-                </a-form-item>
-              </a-col>
-              <a-col :span="12">
-                <a-form-item :style="{ 'text-align': 'right' }">
-                  <!-- No logic, you need to deal with it yourself -->
-                  <a-button type="link" size="small">
-                    {{ t('sys.login.forgetPassword') }}
-                  </a-button>
-                </a-form-item>
-              </a-col>
-            </a-row>
-            <a-form-item>
-              <a-button
-                type="primary"
-                size="large"
-                class="rounded-sm"
-                :block="true"
-                @click="login"
-                :loading="formState.loading"
-              >
-                {{ t('sys.login.loginButton') }}
-              </a-button>
-            </a-form-item>
-          </a-form>
+  <div :class="prefixCls" class="relative w-full h-full px-4">
+    <AppLocalePicker
+      class="absolute top-4 right-4 enter-x text-white xl:text-gray-600"
+      :showText="false"
+    />
+
+    <span class="-enter-x xl:hidden">
+      <AppLogo :alwaysShowTitle="true" />
+    </span>
+
+    <div class="container relative h-full py-2 mx-auto sm:px-10">
+      <div class="flex h-full">
+        <div class="hidden xl:flex xl:flex-col xl:w-6/12 min-h-full mr-4 pl-4">
+          <AppLogo class="-enter-x" />
+          <div class="my-auto">
+            <img
+              :alt="title"
+              src="../../../assets/svg/login-box-bg.svg"
+              class="w-1/2 -mt-16 -enter-x"
+            />
+            <div class="mt-10 font-medium text-white -enter-x">
+              <span class="mt-4 text-3xl inline-block"> {{ t('sys.login.signInTitle') }}</span>
+            </div>
+            <div class="mt-5 text-md text-white font-normal dark:text-gray-500 -enter-x">
+              {{ t('sys.login.signInDesc') }}
+            </div>
+          </div>
+        </div>
+        <div class="h-full xl:h-auto flex py-5 xl:py-0 xl:my-0 w-full xl:w-6/12">
+          <div
+            class="my-auto mx-auto xl:ml-20 bg-white xl:bg-transparent px-5 py-8 sm:px-8 xl:p-0 rounded-md shadow-md xl:shadow-none w-full sm:w-3/4 lg:w-2/4 xl:w-auto enter-x relative"
+          >
+            <h2 class="font-bold text-2xl xl:text-3xl enter-x text-center xl:text-left mb-6">
+              {{ getFormTitle }}
+            </h2>
+            <LoginForm v-show="getShowLogin" />
+            <ForgetPasswordForm v-if="getShowResetPassword" />
+            <RegisterForm v-if="getShowRegister" />
+            <MobileForm v-if="getShowMobile" />
+            <QrCodeForm v-if="getShowQrCode" />
+          </div>
         </div>
       </div>
     </div>
   </div>
 </template>
 <script lang="ts">
-  import { defineComponent, reactive, ref, unref, toRaw } from 'vue';
-  import { Checkbox, Form, Input, Row, Col } from 'ant-design-vue';
+  import { defineComponent, computed } from 'vue';
 
-  import { Button } from '/@/components/Button';
+  import { AppLogo } from '/@/components/Application';
   import { AppLocalePicker } from '/@/components/Application';
+  import LoginForm from './LoginForm.vue';
+  import ForgetPasswordForm from './ForgetPasswordForm.vue';
+  import RegisterForm from './RegisterForm.vue';
+  import MobileForm from './MobileForm.vue';
+  import QrCodeForm from './QrCodeForm.vue';
 
-  import { userStore } from '/@/store/modules/user';
-
-  import { useMessage } from '/@/hooks/web/useMessage';
   import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
-  import logo from '/@/assets/images/logo.png';
   import { useI18n } from '/@/hooks/web/useI18n';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useShowLoginForm, useFormTitle } from './useLogin';
 
   export default defineComponent({
+    name: 'Login',
     components: {
-      [Checkbox.name]: Checkbox,
-      [Form.name]: Form,
-      [Form.Item.name]: Form.Item,
-      [Input.name]: Input,
-      [Input.Password.name]: Input.Password,
-      AButton: Button,
+      AppLogo,
+      LoginForm,
+      ForgetPasswordForm,
+      RegisterForm,
+      MobileForm,
+      QrCodeForm,
       AppLocalePicker,
-      [Row.name]: Row,
-      [Col.name]: Col,
     },
     setup() {
-      const formRef = ref<any>(null);
-      const autoLoginRef = ref(false);
-
       const globSetting = useGlobSetting();
+      const { getFormTitle } = useFormTitle();
+      const { prefixCls } = useDesign('login');
       const { locale } = useProjectSetting();
-      const { notification } = useMessage();
       const { t } = useI18n();
 
-      const formData = reactive({
-        account: 'vben',
-        password: '123456',
-      });
-
-      const formState = reactive({
-        loading: false,
-      });
-
-      const formRules = reactive({
-        account: [{ required: true, message: t('sys.login.accountPlaceholder'), trigger: 'blur' }],
-        password: [
-          { required: true, message: t('sys.login.passwordPlaceholder'), trigger: 'blur' },
-        ],
-      });
-
-      async function handleLogin() {
-        const form = unref(formRef);
-        if (!form) return;
-        formState.loading = true;
-        try {
-          const data = await form.validate();
-          const userInfo = await userStore.login(
-            toRaw({
-              password: data.password,
-              username: data.account,
-            })
-          );
-          if (userInfo) {
-            notification.success({
-              message: t('sys.login.loginSuccessTitle'),
-              description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
-              duration: 3,
-            });
-          }
-        } catch (error) {
-        } finally {
-          formState.loading = false;
-        }
-      }
       return {
-        formRef,
-        formData,
-        formState,
-        formRules,
-        login: handleLogin,
-        autoLogin: autoLoginRef,
-        title: globSetting && globSetting.title,
-        logo,
         t,
-        showLocale: locale.show,
+        prefixCls,
+        title: computed(() => globSetting?.title ?? ''),
+        showLocale: computed(() => locale.show),
+        getFormTitle,
+        ...useShowLoginForm(),
       };
     },
   });
 </script>
 <style lang="less">
-  .login-form__locale {
-    position: absolute;
-    top: 14px;
-    right: 14px;
-    z-index: 1;
-  }
+  @prefix-cls: ~'@{namespace}-login';
+  @logo-prefix-cls: ~'@{namespace}-app-logo';
+  @countdown-prefix-cls: ~'@{namespace}-countdown-input';
 
-  .login {
-    position: relative;
-    height: 100vh;
-    background: url(../../../assets/images/login/login-bg.png) no-repeat;
-    background-size: 100% 100%;
+  .@{prefix-cls} {
+    @media (max-width: @screen-xl) {
+      background: linear-gradient(180deg, #1c3faa, #1c3faa);
+    }
 
-    &-mask {
+    &::before {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
       height: 100%;
-      background: url(../../../assets/images/login/login-in.png) no-repeat;
-      background-position: 30% 30%;
-      background-size: 80% 80%;
+      margin-left: -48%;
+      background-image: url(/@/assets/svg/login-bg.svg);
+      background-position: 100%;
+      background-repeat: no-repeat;
+      background-size: auto 100%;
+      content: '';
+      @media (max-width: @screen-xl) {
+        display: none;
+      }
     }
 
-    &-form {
-      position: relative;
-      bottom: 60px;
-      width: 400px;
-      background: @white;
-      border: 10px solid rgba(255, 255, 255, 0.5);
-      border-width: 8px;
-      border-radius: 4px;
-      background-clip: padding-box;
-
-      &__main {
-        margin: 30px auto 0 auto !important;
+    .@{logo-prefix-cls} {
+      position: absolute;
+      top: 12px;
+      height: 30px;
+
+      &__title {
+        font-size: 16px;
+        color: #fff;
       }
 
-      &-wrap {
-        position: absolute;
-        top: 0;
-        right: 0;
-        display: flex;
-        width: 100%;
-        height: 100%;
-        align-items: center;
+      img {
+        width: 32px;
       }
+    }
+
+    .container {
+      .@{logo-prefix-cls} {
+        display: flex;
+        width: 60%;
+        height: 80px;
+
+        &__title {
+          font-size: 24px;
+          color: #fff;
+        }
 
-      &__content {
-        position: relative;
-        width: 100%;
-        height: 100%;
-        padding: 60px 0 40px 0;
-        border: 1px solid #999;
-        border-radius: 2px;
-
-        header {
-          display: flex;
-          justify-content: center;
-          align-items: center;
-
-          img {
-            display: inline-block;
-            width: 48px;
-          }
-
-          h1 {
-            margin-bottom: 0;
-            font-size: 24px;
-            text-align: center;
-          }
+        img {
+          width: 48px;
         }
+      }
+    }
+
+    &-sign-in-way {
+      .anticon {
+        font-size: 22px;
+        color: #888;
+        cursor: pointer;
 
-        form {
-          width: 80%;
+        &:hover {
+          color: @primary-color;
         }
       }
     }
+
+    input:not([type='checkbox']) {
+      min-width: 360px;
+      @media (max-width: @screen-sm) {
+        min-width: 240px;
+      }
+    }
+    .@{countdown-prefix-cls} input {
+      min-width: unset;
+    }
+
+    .ant-divider-inner-text {
+      font-size: 12px;
+      color: @text-color-secondary;
+    }
   }
 </style>

+ 171 - 0
src/views/sys/login/LoginForm.vue

@@ -0,0 +1,171 @@
+<template>
+  <Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
+    <FormItem name="account" class="enter-x">
+      <Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
+    </FormItem>
+    <FormItem name="password" class="enter-x">
+      <InputPassword
+        size="large"
+        visibilityToggle
+        v-model:value="formData.password"
+        :placeholder="t('sys.login.password')"
+      />
+    </FormItem>
+
+    <ARow class="enter-x">
+      <ACol :span="12">
+        <FormItem>
+          <!-- No logic, you need to deal with it yourself -->
+          <Checkbox v-model:checked="rememberMe" size="small">
+            {{ t('sys.login.rememberMe') }}
+          </Checkbox>
+        </FormItem>
+      </ACol>
+      <ACol :span="12">
+        <FormItem :style="{ 'text-align': 'right' }">
+          <!-- No logic, you need to deal with it yourself -->
+          <Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
+            {{ t('sys.login.forgetPassword') }}
+          </Button>
+        </FormItem>
+      </ACol>
+    </ARow>
+
+    <FormItem class="enter-x">
+      <Button
+        type="primary"
+        size="large"
+        block
+        @click="handleLogin"
+        :loading="loading"
+        class="enter-x"
+      >
+        {{ t('sys.login.loginButton') }}
+      </Button>
+      <!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
+        {{ t('sys.login.registerButton') }}
+      </Button> -->
+    </FormItem>
+    <ARow class="enter-x">
+      <ACol :span="7">
+        <Button block @click="setLoginState(LoginStateEnum.MOBILE)">
+          {{ t('sys.login.mobileSignInFormTitle') }}
+        </Button>
+      </ACol>
+      <ACol :span="8" :offset="1">
+        <Button block @click="setLoginState(LoginStateEnum.QR_CODE)">
+          {{ t('sys.login.qrSignInFormTitle') }}
+        </Button>
+      </ACol>
+      <ACol :span="7" :offset="1">
+        <Button block @click="setLoginState(LoginStateEnum.REGISTER)">
+          {{ t('sys.login.registerButton') }}
+        </Button>
+      </ACol>
+    </ARow>
+
+    <Divider>{{ t('sys.login.otherSignIn') }}</Divider>
+
+    <div class="flex justify-evenly enter-x" :class="`${prefixCls}-sign-in-way`">
+      <GithubFilled />
+      <WechatFilled />
+      <AlipayCircleFilled />
+      <GoogleCircleFilled />
+      <TwitterCircleFilled />
+    </div>
+  </Form>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive, ref, toRaw } from 'vue';
+
+  import { Checkbox, Form, Input, Row, Col, Button, Divider } from 'ant-design-vue';
+  import {
+    GithubFilled,
+    WechatFilled,
+    AlipayCircleFilled,
+    GoogleCircleFilled,
+    TwitterCircleFilled,
+  } from '@ant-design/icons-vue';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useMessage } from '/@/hooks/web/useMessage';
+
+  import { userStore } from '/@/store/modules/user';
+  import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  export default defineComponent({
+    name: 'LoginForm',
+    components: {
+      Checkbox,
+      Button,
+      Form,
+      FormItem: Form.Item,
+      Input,
+      Divider,
+      InputPassword: Input.Password,
+      [Col.name]: Col,
+      [Row.name]: Row,
+      GithubFilled,
+      WechatFilled,
+      AlipayCircleFilled,
+      GoogleCircleFilled,
+      TwitterCircleFilled,
+    },
+    setup() {
+      const { t } = useI18n();
+      const { notification } = useMessage();
+      const { prefixCls } = useDesign('login');
+
+      const { setLoginState } = useLoginState();
+      const { getFormRules } = useFormRules();
+
+      const formRef = ref<any>(null);
+      const loading = ref(false);
+      const rememberMe = ref(false);
+
+      const formData = reactive({
+        account: 'vben',
+        password: '123456',
+      });
+
+      const { validForm } = useFormValid(formRef);
+
+      async function handleLogin() {
+        const data = await validForm();
+        if (!data) return;
+        try {
+          loading.value = true;
+          const userInfo = await userStore.login(
+            toRaw({
+              password: data.password,
+              username: data.account,
+            })
+          );
+          if (userInfo) {
+            notification.success({
+              message: t('sys.login.loginSuccessTitle'),
+              description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
+              duration: 3,
+            });
+          }
+        } finally {
+          loading.value = false;
+        }
+      }
+
+      return {
+        t,
+        prefixCls,
+        formRef,
+        formData,
+        getFormRules,
+        rememberMe,
+        handleLogin,
+        loading,
+        setLoginState,
+        LoginStateEnum,
+      };
+    },
+  });
+</script>

+ 85 - 0
src/views/sys/login/MobileForm.vue

@@ -0,0 +1,85 @@
+<template>
+  <Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
+    <FormItem name="mobile" class="enter-x">
+      <Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
+    </FormItem>
+    <FormItem name="sms" class="enter-x">
+      <CountdownInput
+        size="large"
+        v-model:value="formData.sms"
+        :placeholder="t('sys.login.smsCode')"
+      />
+    </FormItem>
+
+    <FormItem class="enter-x">
+      <Button
+        type="primary"
+        size="large"
+        block
+        @click="handleLogin"
+        :loading="loading"
+        class="enter-x"
+      >
+        {{ t('sys.login.loginButton') }}
+      </Button>
+      <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
+        {{ t('sys.login.backSignIn') }}
+      </Button>
+    </FormItem>
+  </Form>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive, ref } from 'vue';
+
+  import { Form, Input, Button } from 'ant-design-vue';
+  import { CountdownInput } from '/@/components/CountDown';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
+
+  export default defineComponent({
+    name: 'MobileForm',
+    components: {
+      Button,
+      Form,
+      FormItem: Form.Item,
+      Input,
+      CountdownInput,
+    },
+    setup() {
+      const { t } = useI18n();
+      const { setLoginState } = useLoginState();
+      const { getFormRules } = useFormRules();
+
+      const formRef = ref<any>(null);
+      const loading = ref(false);
+
+      const formData = reactive({
+        mobile: '',
+        sms: '',
+      });
+
+      const { validForm } = useFormValid(formRef);
+
+      async function handleLogin() {
+        const data = await validForm();
+        if (!data) return;
+        console.log(data);
+      }
+
+      function handleBackLogin() {
+        setLoginState(LoginStateEnum.LOGIN);
+      }
+
+      return {
+        t,
+        formRef,
+        formData,
+        getFormRules,
+        handleLogin,
+        loading,
+        handleBackLogin,
+      };
+    },
+  });
+</script>

+ 40 - 0
src/views/sys/login/QrCodeForm.vue

@@ -0,0 +1,40 @@
+<template>
+  <div class="enter-x min-w-64 min-h-64">
+    <QrCode :value="qrCodeUrl" class="enter-x flex justify-center xl:justify-start" :width="280" />
+    <Divider>{{ t('sys.login.scanSign') }}</Divider>
+    <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
+      {{ t('sys.login.backSignIn') }}
+    </Button>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  import { Button, Divider } from 'ant-design-vue';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { LoginStateEnum, useLoginState } from './useLogin';
+  import { QrCode } from '/@/components/Qrcode/index';
+  const qrCodeUrl = 'https://vvbin.cn/next/login';
+  export default defineComponent({
+    name: 'QrCodeForm',
+    components: {
+      Button,
+      QrCode,
+      Divider,
+    },
+    setup() {
+      const { t } = useI18n();
+      const { setLoginState } = useLoginState();
+
+      function handleBackLogin() {
+        setLoginState(LoginStateEnum.LOGIN);
+      }
+      return {
+        t,
+        handleBackLogin,
+        qrCodeUrl,
+      };
+    },
+  });
+</script>

+ 117 - 0
src/views/sys/login/RegisterForm.vue

@@ -0,0 +1,117 @@
+<template>
+  <Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
+    <FormItem name="account" class="enter-x">
+      <Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
+    </FormItem>
+    <FormItem name="mobile" class="enter-x">
+      <Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
+    </FormItem>
+    <FormItem name="sms" class="enter-x">
+      <CountdownInput
+        size="large"
+        v-model:value="formData.sms"
+        :placeholder="t('sys.login.smsCode')"
+      />
+    </FormItem>
+    <FormItem name="password" class="enter-x">
+      <StrengthMeter
+        size="large"
+        v-model:value="formData.password"
+        :placeholder="t('sys.login.password')"
+      />
+    </FormItem>
+    <FormItem name="confirmPassword" class="enter-x">
+      <InputPassword
+        size="large"
+        visibilityToggle
+        v-model:value="formData.confirmPassword"
+        :placeholder="t('sys.login.confirmPassword')"
+      />
+    </FormItem>
+
+    <FormItem class="enter-x" name="policy">
+      <!-- No logic, you need to deal with it yourself -->
+      <Checkbox v-model:checked="formData.policy" size="small">
+        {{ t('sys.login.policy') }}
+      </Checkbox>
+    </FormItem>
+
+    <Button
+      type="primary"
+      size="large"
+      block
+      @click="handleReset"
+      :loading="loading"
+      class="enter-x"
+    >
+      {{ t('sys.login.registerButton') }}
+    </Button>
+    <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
+      {{ t('sys.login.backSignIn') }}
+    </Button>
+  </Form>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive, ref } from 'vue';
+
+  import { Form, Input, Button, Divider, Checkbox } from 'ant-design-vue';
+  import { StrengthMeter } from '/@/components/StrengthMeter';
+  import { CountdownInput } from '/@/components/CountDown';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
+
+  export default defineComponent({
+    name: 'RegisterPasswordForm',
+    components: {
+      Button,
+      Form,
+      FormItem: Form.Item,
+      Input,
+      Divider,
+      InputPassword: Input.Password,
+      Checkbox,
+      StrengthMeter,
+      CountdownInput,
+    },
+    setup() {
+      const { t } = useI18n();
+      const { setLoginState } = useLoginState();
+
+      const formRef = ref<any>(null);
+      const loading = ref(false);
+
+      const formData = reactive({
+        account: '',
+        password: '',
+        confirmPassword: '',
+        mobile: '',
+        sms: '',
+        policy: false,
+      });
+
+      const { getFormRules } = useFormRules(formData);
+      const { validForm } = useFormValid(formRef);
+
+      async function handleReset() {
+        const data = await validForm();
+        if (!data) return;
+        console.log(data);
+      }
+
+      function handleBackLogin() {
+        setLoginState(LoginStateEnum.LOGIN);
+      }
+
+      return {
+        t,
+        formRef,
+        formData,
+        getFormRules,
+        handleReset,
+        loading,
+        handleBackLogin,
+      };
+    },
+  });
+</script>

+ 134 - 0
src/views/sys/login/useLogin.ts

@@ -0,0 +1,134 @@
+import { RuleObject } from 'ant-design-vue/lib/form/interface';
+import { ref, computed, unref, Ref } from 'vue';
+import { useI18n } from '/@/hooks/web/useI18n';
+
+export enum LoginStateEnum {
+  LOGIN,
+  REGISTER,
+  RESET_PASSWORD,
+  MOBILE,
+  QR_CODE,
+}
+
+const currentState = ref(LoginStateEnum.LOGIN);
+
+export function useFormTitle() {
+  const { t } = useI18n();
+
+  const getFormTitle = computed(() => {
+    const titleObj = {
+      [LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
+      [LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
+      [LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
+      [LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
+      [LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
+    };
+    return titleObj[unref(currentState)];
+  });
+  return { getFormTitle };
+}
+
+export function useLoginState() {
+  function setLoginState(state: LoginStateEnum) {
+    currentState.value = state;
+  }
+
+  const getLoginState = computed(() => currentState.value);
+
+  return { setLoginState, getLoginState };
+}
+
+export function useShowLoginForm() {
+  const getShowLogin = computed(() => unref(currentState) === LoginStateEnum.LOGIN);
+  const getShowResetPassword = computed(
+    () => unref(currentState) === LoginStateEnum.RESET_PASSWORD
+  );
+  const getShowRegister = computed(() => unref(currentState) === LoginStateEnum.REGISTER);
+  const getShowMobile = computed(() => unref(currentState) === LoginStateEnum.MOBILE);
+  const getShowQrCode = computed(() => unref(currentState) === LoginStateEnum.QR_CODE);
+
+  return { getShowLogin, getShowResetPassword, getShowRegister, getShowMobile, getShowQrCode };
+}
+
+export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
+  async function validForm() {
+    const form = unref(formRef);
+    if (!form) return;
+    const data = await form.validate();
+    return data as T;
+  }
+
+  return { validForm };
+}
+
+export function useFormRules(formData?: Recordable) {
+  const { t } = useI18n();
+
+  const getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder')));
+  const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder')));
+  const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder')));
+  const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder')));
+
+  const validatePolicy = async (_: RuleObject, value: boolean) => {
+    return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve();
+  };
+
+  const validateConfirmPassword = (password: string) => {
+    return async (_: RuleObject, value: string) => {
+      if (!value) {
+        return Promise.reject(t('sys.login.passwordPlaceholder'));
+      }
+      if (value !== password) {
+        return Promise.reject(t('sys.login.diffPwd'));
+      }
+      return Promise.resolve();
+    };
+  };
+
+  const getFormRules = computed(() => {
+    const accountFormRule = unref(getAccountFormRule);
+    const passwordFormRule = unref(getPasswordFormRule);
+    const smsFormRule = unref(getSmsFormRule);
+    const mobileFormRule = unref(getMobileFormRule);
+
+    const mobileRule = {
+      sms: smsFormRule,
+      mobile: mobileFormRule,
+    };
+    switch (unref(currentState)) {
+      case LoginStateEnum.REGISTER:
+        return {
+          account: accountFormRule,
+          password: passwordFormRule,
+          confirmPassword: [
+            { validator: validateConfirmPassword(formData?.password), trigger: 'change' },
+          ],
+          policy: [{ validator: validatePolicy, trigger: 'change' }],
+          ...mobileRule,
+        };
+      case LoginStateEnum.RESET_PASSWORD:
+        return {
+          account: accountFormRule,
+          ...mobileRule,
+        };
+      case LoginStateEnum.MOBILE:
+        return mobileRule;
+      default:
+        return {
+          account: accountFormRule,
+          password: passwordFormRule,
+        };
+    }
+  });
+  return { getFormRules };
+}
+
+function createRule(message: string) {
+  return [
+    {
+      required: true,
+      message,
+      trigger: 'change',
+    },
+  ];
+}

+ 0 - 20
tailwind.config.js

@@ -1,20 +0,0 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
-const colors = require('windicss/colors');
-const defaultTheme = require('windicss/defaultTheme');
-module.exports = {
-  darkMode: 'class',
-  plugins: [
-    require('windicss/plugin/forms'),
-    require('windicss/plugin/typography'),
-    require('windicss/plugin/line-clamp'),
-    require('windicss/plugin/aspect-ratio'),
-  ],
-  theme: {
-    extend: {
-      colors,
-      fontFamily: {
-        sans: ['Righteous', ...defaultTheme.fontFamily.sans],
-      },
-    },
-  },
-};

+ 71 - 0
windi.config.ts

@@ -0,0 +1,71 @@
+import lineClamp from 'windicss/plugin/line-clamp';
+import colors from 'windicss/colors';
+
+import { defineConfig } from 'vite-plugin-windicss';
+
+export default defineConfig({
+  darkMode: 'class',
+  plugins: [lineClamp, createEnterPlugin()],
+  theme: {
+    extend: {
+      colors,
+    },
+
+    // screen: {
+    //   sm: '576px',
+    //   md: '768px',
+    //   lg: '992px',
+    //   xl: '1200px',
+    //   '2xl': '1600px',
+    // },
+  },
+});
+
+/**
+ * Used for animation when the element is displayed
+ * @param maxOutput The larger the maxOutput output, the larger the generated css volume
+ */
+function createEnterPlugin(maxOutput = 10) {
+  const createCss = (index: number, d = 'x') => {
+    const upd = d.toUpperCase();
+    return {
+      [`*> .enter-${d}:nth-child(${index})`]: {
+        transform: `translate${upd}(50px)`,
+      },
+      [`*> .-enter-${d}:nth-child(${index})`]: {
+        transform: `translate${upd}(-50px)`,
+      },
+      [`* > .enter-${d}:nth-child(${index}),* > .-enter-${d}:nth-child(${index})`]: {
+        'z-index': `${10 - index}`,
+        opacity: '0',
+        animation: `enter-${d}-animation 0.4s ease-in-out 0.3s`,
+        'animation-fill-mode': 'forwards',
+        'animation-delay': `${(index * 1) / 10}s`,
+      },
+    };
+  };
+  const handler = ({ addBase }) => {
+    for (let index = 1; index < maxOutput; index++) {
+      addBase({
+        ...createCss(index, 'x'),
+        ...createCss(index, 'y'),
+      });
+    }
+
+    addBase({
+      [`@keyframes enter-x-animation`]: {
+        to: {
+          opacity: '1',
+          transform: 'translateX(0)',
+        },
+      },
+      [`@keyframes enter-y-animation`]: {
+        to: {
+          opacity: '1',
+          transform: 'translateY(0)',
+        },
+      },
+    });
+  };
+  return { handler };
+}

+ 35 - 28
yarn.lock

@@ -1936,14 +1936,16 @@
   dependencies:
     vue-demi latest
 
-"@windicss/plugin-utils@0.3.12":
-  version "0.3.12"
-  resolved "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-0.3.12.tgz#69b55be1ffb45753e6f01aa236f5ecd8df7a92ee"
-  integrity sha512-XA+xeyu5KM322dIp+EEHeXnAPuK+KxuWyoGvJnxXi9U50nIp0QraqXAH7xl9ghIkVHvVrb8pmm8vHpzFvsqF2A==
+"@windicss/plugin-utils@0.4.3":
+  version "0.4.3"
+  resolved "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-0.4.3.tgz#84e85fd3cd6eaf54ca72cae276f9cf0610f45e56"
+  integrity sha512-ilddLED+sZQIA9rOwE5eYwdBEBWKREvAVkkiAOOTNf7oDcP/a1cxT3f/nE4tgfhz+MC/FKcy7NkfrqfXRdEQaQ==
   dependencies:
+    esbuild "^0.8.49"
+    esbuild-register "^2.0.0"
     fast-glob "^3.2.5"
     micromatch "^4.0.2"
-    windicss "^2.1.12"
+    windicss "^2.1.15"
 
 "@zxcvbn-ts/core@^0.2.0":
   version "0.2.0"
@@ -3870,6 +3872,11 @@ esbuild@^0.8.48:
   resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.48.tgz#a57e4dde84ec56da1c6ecaefee97e9da6c5b00b5"
   integrity sha512-lrH8lA8wWQ6Lpe1z6C7ZZaFSmRsUlcQAqe16nf7ITySQ7MV4+vI7qAqQlT/u+c3+9AL3VXmT4MXTxV2e63pO4A==
 
+esbuild@^0.8.49:
+  version "0.8.49"
+  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.49.tgz#3d33f71b3966611f822cf4c838115f3fbd16def2"
+  integrity sha512-itiFVYv5UZz4NooO7/Y0bRGVDGz/M/cxKbl6zyNI5pnKaz1mZjvZXAFhhDVz6rGCmcdTKj5oag6rh8DaaSSmfQ==
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -8454,10 +8461,10 @@ stylelint-order@^4.1.0:
     postcss "^7.0.31"
     postcss-sorting "^5.0.1"
 
-stylelint@^13.10.0:
-  version "13.10.0"
-  resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.10.0.tgz#67b0c6f378c3fa61aa569a55d38feb8570b0b587"
-  integrity sha512-eDuLrL0wzPKbl5/TbNGZcbw0lTIGbDEr5W6lCODvb1gAg0ncbgCRt7oU0C2VFDvbrcY0A3MFZOwltwTRmc0XCw==
+stylelint@^13.11.0:
+  version "13.11.0"
+  resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.11.0.tgz#591981fbdd68c9d3d3e6147a0cd6a07539fc216d"
+  integrity sha512-DhrKSWDWGZkCiQMtU+VroXM6LWJVC8hSK24nrUngTSQvXGK75yZUq4yNpynqrxD3a/fzKMED09V+XxO4z4lTbw==
   dependencies:
     "@stylelint/postcss-css-in-js" "^0.37.2"
     "@stylelint/postcss-markdown" "^0.36.2"
@@ -9262,17 +9269,17 @@ vite-plugin-purge-icons@^0.7.0:
     "@purge-icons/generated" "^0.7.0"
     rollup-plugin-purge-icons "^0.7.0"
 
-vite-plugin-pwa@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.5.1.tgz#7f94b8c4092ba0bba0a1bceb690e7420b18071e7"
-  integrity sha512-hf8BgyH0XLNEJUoMsk7ywMoE+OoQelK/+4RQoftQomZhlKXgsTWrfshFGOV7sKUbLsxMh0cVoh1DmAulQmRzKQ==
+vite-plugin-pwa@^0.5.2:
+  version "0.5.2"
+  resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.5.2.tgz#48131ebadc0c98c34a543dbf1bb1c86aeef532e0"
+  integrity sha512-4SHKxYhd5sCF/ebbgxGYlN91UHlylzh7C32a5+Y2c2vbrWzw5x62ZxsYzolQzBosdOim4Ez+e/dX4hmP3BCmow==
   dependencies:
     debug "^4.3.2"
     fast-glob "^3.2.5"
     pretty-bytes "^5.5.0"
     workbox-build "^6.1.0"
 
-vite-plugin-style-import@^0.7.2:
+vite-plugin-style-import@^0.7.3:
   version "0.7.3"
   resolved "https://registry.npmjs.org/vite-plugin-style-import/-/vite-plugin-style-import-0.7.3.tgz#4a9fb08bf5f2fc4796391c9be9a587ecb5c97e9e"
   integrity sha512-oKM6vOl7iWaB5U1HcR5oM1oPRhT1n5yJt7h4h9jwpMPCD5ckHPcSjjSU7ZlOJkoS/IWEnDkQqoZi162bOs1rTQ==
@@ -9295,13 +9302,13 @@ vite-plugin-theme@^0.4.3:
     es-module-lexer "^0.3.26"
     tinycolor2 "^1.4.2"
 
-vite-plugin-windicss@0.3.12:
-  version "0.3.12"
-  resolved "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-0.3.12.tgz#5503b4ee738268a37c857c0cf55cea41f28fa3e6"
-  integrity sha512-NuzIjSrqBQKvpbLJoU9qi8PIWBBXCqBmuLg9Dl/cFl4MB/vAHIOB6sZYJatCBFTU39Kw4UU0GhAjDBSNqzTn0w==
+vite-plugin-windicss@0.4.3:
+  version "0.4.3"
+  resolved "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-0.4.3.tgz#f86e5a3b78882caa3cdd50cba2b08770e2d627c8"
+  integrity sha512-Lnv6OhcYzcJvecTs4LIpMSfo54rSewkHrW85IVwy8hacR0krY319jXr5nwiDpSTp6HM3QJhoJ4zxHF+t5Q+Nwg==
   dependencies:
-    "@windicss/plugin-utils" "0.3.12"
-    windicss "^2.1.12"
+    "@windicss/plugin-utils" "0.4.3"
+    windicss "^2.1.15"
 
 vite@2.0.1:
   version "2.0.1"
@@ -9356,10 +9363,10 @@ vue-i18n@9.0.0-rc.2:
     "@intlify/shared" "9.0.0-rc.2"
     "@vue/devtools-api" "^6.0.0-beta.3"
 
-vue-router@^4.0.3:
-  version "4.0.3"
-  resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz#8b26050c88b2dec7e27a88835f71046b365823ec"
-  integrity sha512-AD1OjtVPyQHTSpoRsEGfPpxRQwhAhxcacOYO3zJ3KNkYP/r09mileSp6kdMQKhZWP2cFsPR3E2M3PZguSN5/ww==
+vue-router@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.4.tgz#ad9b4b7bbdad622407b4ff189b1646f48c1e9053"
+  integrity sha512-uN6PDEaYdU9aRO7mU+Dkr1uaY49hV3fucEDG/Vre/Qj8ct3RoJS16vcPrvKVzn69zDDjBV5b9Xw7fZA9r6b/Iw==
 
 vue-types@^3.0.0:
   version "3.0.1"
@@ -9434,10 +9441,10 @@ which@^2.0.1:
   dependencies:
     isexe "^2.0.0"
 
-windicss@^2.1.12:
-  version "2.1.12"
-  resolved "https://registry.npmjs.org/windicss/-/windicss-2.1.12.tgz#840b963f03af7a3e31b989d2b51de52dcd57a37a"
-  integrity sha512-VC057iG65zlvdqUI+1ynzOuKikalvYg6XqPGbG17HEAfwQ0sg1dACTk2plEp1QAEQNtKU3BnLnueWa4oKlltEQ==
+windicss@^2.1.15:
+  version "2.1.15"
+  resolved "https://registry.npmjs.org/windicss/-/windicss-2.1.15.tgz#0a5bf1a56711ab53de8093a3c855764d93ffac00"
+  integrity sha512-gBihXNJPzv/kBaelOlXvbrmWsWuv98OPSf/yUYjc8EnRGCOxDOQIRin4FYPTWCmZi91PZThh7nMjzQZiBV+MYg==
 
 wmf@~1.0.1:
   version "1.0.2"

Vissa filer visades inte eftersom för många filer har ändrats