Commit ec9478f76fadbcd936d9d298b2623cebb1525ca5

Authored by vben
1 parent e3851dc5

refactor: refactor login page

.vscode/extensions.json
1 1 {
2 2 "recommendations": [
3   - "johnsoncodehk.volar",
4 3 "octref.vetur",
5 4 "dbaeumer.vscode-eslint",
6 5 "stylelint.vscode-stylelint",
7   - "DavidAnson.vscode-markdownlint",
8 6 "esbenp.prettier-vscode",
9 7 "mrmlnc.vscode-less",
10 8 "antfu.i18n-ally",
11   - "cpylua.language-postcss",
12   - "Orta.vscode-jest",
13 9 "antfu.iconify",
14 10 "mikestead.dotenv",
15 11 "bradlc.vscode-tailwindcss",
16   - "heybourn.headwind",
17   - "znck.vue-language-features"
  12 + "heybourn.headwind"
18 13 ]
19 14 }
... ...
.vscode/i18n-ally-reviews.yml 0 → 100644
  1 +# Review comments generated by i18n-ally. Please commit this file.
  2 +
  3 +reviews:
  4 + sys.login.autoLogin:
  5 + description: '1'
... ...
CHANGELOG.zh_CN.md
1 1 ## Wip
2 2  
  3 +### ✨ Refactor
  4 +
  5 +- 登录页重构,新增注册页面/重置密码页面/手机登录/二维码登录
  6 +
3 7 ### ✨ Features
4 8  
5 9 - 新增 `settingButtonPosition`配置项,用于配置`设置`按钮位置
6 10 - `modal`可以通过双击头部切换全屏
  11 +- 新增`CountDownInput`组件
7 12  
8 13 ### ⚡ Performance Improvements
9 14  
... ...
build/config/themeConfig.ts
... ... @@ -94,7 +94,7 @@ export function generateModifyVars() {
94 94 'disabled-color': 'rgba(0, 0, 0, 0.25)', // Failure color
95 95 'heading-color': 'rgba(0, 0, 0, 0.85)', // Title color
96 96 'text-color': 'rgba(0, 0, 0, 0.85)', // Main text color
97   - 'text-color-secondary ': 'rgba(0, 0, 0, 0.45)', // Subtext color
  97 + 'text-color-secondary': 'rgba(0, 0, 0, 0.45)', // Subtext color
98 98 'font-size-base': '14px', // Main font size
99 99 'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', // Floating shadow
100 100 'border-color-base': '#d9d9d9', // Border color,
... ...
package.json
... ... @@ -43,7 +43,7 @@
43 43 "vditor": "^3.8.1",
44 44 "vue": "^3.0.5",
45 45 "vue-i18n": "9.0.0-rc.2",
46   - "vue-router": "^4.0.3",
  46 + "vue-router": "^4.0.4",
47 47 "vue-types": "^3.0.2",
48 48 "vuex": "^4.0.0",
49 49 "vuex-module-decorators": "^1.0.1",
... ... @@ -92,7 +92,7 @@
92 92 "pretty-quick": "^3.1.0",
93 93 "rimraf": "^3.0.2",
94 94 "rollup-plugin-visualizer": "^4.2.0",
95   - "stylelint": "^13.10.0",
  95 + "stylelint": "^13.11.0",
96 96 "stylelint-config-prettier": "^8.0.2",
97 97 "stylelint-config-standard": "^20.0.0",
98 98 "stylelint-order": "^4.1.0",
... ... @@ -104,10 +104,10 @@
104 104 "vite-plugin-imagemin": "^0.2.6",
105 105 "vite-plugin-mock": "^2.1.4",
106 106 "vite-plugin-purge-icons": "^0.7.0",
107   - "vite-plugin-pwa": "^0.5.1",
108   - "vite-plugin-style-import": "^0.7.2",
  107 + "vite-plugin-pwa": "^0.5.2",
  108 + "vite-plugin-style-import": "^0.7.3",
109 109 "vite-plugin-theme": "^0.4.3",
110   - "vite-plugin-windicss": "0.3.12",
  110 + "vite-plugin-windicss": "0.4.3",
111 111 "vue-eslint-parser": "^7.5.0",
112 112 "yargs": "^16.2.0"
113 113 },
... ...
src/assets/images/login/login-bg.png deleted 100644 → 0

180 KB

src/assets/images/login/login-in.png deleted 100644 → 0

91.5 KB

src/assets/svg/login-bg.svg 0 → 100644
  1 +<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">
  2 + <defs>
  3 + <clipPath id="clip-path">
  4 + <rect id="Rectangle_73" data-name="Rectangle 73" width="6395" height="1079" transform="translate(-5391)" fill="#fff"/>
  5 + </clipPath>
  6 + <linearGradient id="linear-gradient" x1="0.747" y1="0.222" x2="0.973" y2="0.807" gradientUnits="objectBoundingBox">
  7 + <stop offset="0" stop-color="#2b51b4"/>
  8 + <stop offset="1" stop-color="#1c3faa"/>
  9 + </linearGradient>
  10 + </defs>
  11 + <g id="Mask_Group_1" data-name="Mask Group 1" transform="translate(5391)" clip-path="url(#clip-path)">
  12 + <g id="Group_118" data-name="Group 118" transform="translate(-419.333 -1.126)">
  13 + <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"/>
  14 + <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)"/>
  15 + </g>
  16 + </g>
  17 +</svg>
... ...
src/assets/svg/login-box-bg.svg 0 → 100644
  1 +<svg id="a622e68e-7a65-46e9-94a9-d455de519afc" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="971.44" height="502" viewBox="0 0 971.44 502"><defs><linearGradient id="341b0e5e-a21f-44db-b85f-76180f33f0d3" x1="599.5" y1="668.05" x2="599.5" y2="199" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="9c19d1ba-0c1d-4cca-8c15-e6f3831a5e67" x1="485.72" y1="258.88" x2="485.72" y2="71.12" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/><linearGradient id="fe76f7c7-2126-4e48-920d-21143a22d340" x1="132" y1="515" x2="303" y2="515" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/><linearGradient id="2cf89a04-5a05-413b-983a-d2bc296cbb5e" x1="933" y1="568.28" x2="1031" y2="568.28" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/></defs><title>responsive</title><g opacity="0.7"><path d="M852.69,199H346.31A16.37,16.37,0,0,0,330,215.42V563.94a16.37,16.37,0,0,0,16.31,16.42H520.47v60.16h-7.94a8.3,8.3,0,0,0-8.27,8.33v12.07h16.21v7.14H678.53v-7.14h16.21V648.85a8.3,8.3,0,0,0-8.27-8.33H679V640h-.51V580.36H852.69A16.37,16.37,0,0,0,869,563.94V215.42A16.37,16.37,0,0,0,852.69,199Z" transform="translate(-114.28 -199)" fill="url(#341b0e5e-a21f-44db-b85f-76180f33f0d3)"/></g><rect x="407.72" y="371" width="156" height="92" fill="#bdbdbd"/><g opacity="0.1"><path d="M525.07,579H675.24c1.81-7.87,3.26-13,3.26-13h-157S523.11,571.11,525.07,579Z" transform="translate(-114.28 -199)"/></g><path d="M235.82,3h499.8a16.1,16.1,0,0,1,16.1,16.1V327a0,0,0,0,1,0,0h-532a0,0,0,0,1,0,0V19.1A16.1,16.1,0,0,1,235.82,3Z" fill="#535461"/><path d="M849.9,576H350.1A16.1,16.1,0,0,1,334,559.9V526H866v33.9A16.1,16.1,0,0,1,849.9,576Z" transform="translate(-114.28 -199)" fill="#bdbdbd"/><circle cx="485.72" cy="352" r="9" fill="#535461"/><path d="M399.89,436H571.55a8.17,8.17,0,0,1,8.17,8.17V456a0,0,0,0,1,0,0h-188a0,0,0,0,1,0,0V444.17A8.17,8.17,0,0,1,399.89,436Z" fill="#bdbdbd"/><g opacity="0.5"><rect x="320.72" y="71.12" width="330" height="187.76" rx="4.5" ry="4.5" fill="url(#9c19d1ba-0c1d-4cca-8c15-e6f3831a5e67)"/></g><rect x="324.95" y="72.5" width="321.54" height="183.96" rx="4.5" ry="4.5" fill="#fff"/><g opacity="0.5"><rect x="414.52" y="98.91" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="98.91" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="109.55" width="79.54" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><g opacity="0.5"><rect x="414.52" y="148.53" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="148.53" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="159.16" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><g opacity="0.5"><rect x="414.52" y="198.15" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="198.15" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="208.78" width="96.33" height="3.54" rx="1.59" ry="1.59" fill="#e0e0e0"/><line x1="485.72" y1="42" x2="485.72" y2="20" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="79" x2="485.72" y2="50.13" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="79" r="4" fill="#0960bd"/><circle cx="485.72" cy="46" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><line x1="485.72" y1="42" x2="485.72" y2="20" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="79" x2="485.72" y2="50.13" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="79" r="4" fill="#0960bd"/><circle cx="485.72" cy="46" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><line x1="485.72" y1="279" x2="485.72" y2="310" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="251" x2="485.72" y2="279.87" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="251" r="4" fill="#0960bd"/><line x1="305.72" y1="168.5" x2="274.22" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="333.22" y1="168.5" x2="304.35" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="333.22" cy="168.5" r="4" fill="#0960bd"/><g opacity="0.1"><rect x="408.22" y="435.5" width="156" height="3"/></g><g opacity="0.7"><path d="M293.48,566.06H221.08l1-8.14c20.46-18.37,33.69-67.31,33.69-67.31a6.78,6.78,0,0,0-.87.18c-12,2.42-20.54,7.35-26.51,13.28l2.54-21.66c37.8-8.14,52.79-58.14,52.79-58.14-24.12,5.35-39.16,13.63-48.5,21.49l3.72-31.82c25.56,8.77,52-37.82,52-37.82l-1-.21.5-.32-.76.27c-28.25-6.09-43.35,10.06-48.25,16.77l.37-3.12q-1.12,3-2.18,5.88h0l0,.08q-3,8.13-5.49,16.06l0,0h0q-2.17,6.77-4.06,13.4l0-.06s-1.17-28.46-31.18-35.95c0,0,3.15,62.07,26.93,51.91h0c-2.2,9-4,17.66-5.56,26.07h0q-1.49,8.21-2.6,16l-.14.16.14-.12-.06.41v0h0q-1,7.07-1.7,13.78c.46-8.62-1.11-33.52-30.45-56.92,0,0-39,68.54,27.5,82,.15.13.3.26.44.38l-.1-.31.6.13.27-3.52a369.39,369.39,0,0,0,.23,44.1h0c.07,1,.14,2,.21,2.95H141.37c-27.94,57.79,15.52,89.46,15.52,89.46h120C323.49,596.66,293.48,566.06,293.48,566.06Zm-78-65.68h0v0Z" transform="translate(-114.28 -199)" fill="url(#fe76f7c7-2126-4e48-920d-21143a22d340)"/></g><path d="M217,588s-19-83,23-190" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" stroke-width="3" opacity="0.6"/><path d="M143,563H290s29,37-16,92H158S116,617,143,563Z" transform="translate(-114.28 -199)" fill="#0960bd"/><path d="M237.89,403.5s14.61-26,49.61-18c0,0-28.93,49.26-55,33.13Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M228.63,431.09S227.5,404.5,198.5,397.5c0,0,3,58,26,48.5Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M219.15,470.36s5.35-27.86,61.35-39.86c0,0-17.86,57.62-63.93,55.31Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M214.61,501.63s5.89-29.13-29.11-56.13c0,0-38,64.67,27.48,76.83Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M213.56,541.67S209.5,500.5,253.5,492.5c0,0-16.07,57.49-40,67.74Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M233,419s38-29,54-34" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M216.5,485.5s46-49,64-55" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M198.5,397.5s28,38,26,48" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M185.5,445.5s15,68,27,77" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M213.5,560.5s24-66,40-68" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><g opacity="0.1"><path d="M290,563H143c-.33.67-.65,1.34-1,2H285s28.29,36.11-14.4,90H274C319,600,290,563,290,563Z" transform="translate(-114.28 -199)"/></g><rect y="455.6" width="971.44" height="32.93" fill="#e0e0e0"/><rect x="41.16" y="488.53" width="889.11" height="13.47" fill="#e0e0e0"/><rect x="41.16" y="488.53" width="889.11" height="4.49" opacity="0.1"/><line x1="690.22" y1="168.5" x2="696.22" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="637.22" y1="168.5" x2="682.1" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="637.22" cy="168.5" r="4" fill="#0960bd"/><circle cx="686.22" cy="168.5" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><g opacity="0.7"><path d="M1027,643.88l.1-.15q.31-.48.61-1l.11-.19q.29-.49.55-1l.09-.17c.2-.39.39-.78.56-1.19h0a23.79,23.79,0,0,0,.94-2.51l.1-.33c.09-.31.18-.62.26-.93l.1-.44q.1-.42.18-.85c0-.16.06-.32.09-.48s.09-.56.13-.85,0-.33.06-.49.06-.61.08-.92c0-.14,0-.29,0-.43,0-.45,0-.91,0-1.36V548h-13.85V507.52h-17V548H988.39V489.86h-17V548H965V481.55h-17V548H933V630.6c0,13.48,11.21,24.4,25,24.4H1006a25.19,25.19,0,0,0,20.24-10.06l0,0Q1026.61,644.41,1027,643.88Z" transform="translate(-114.28 -199)" fill="url(#2cf89a04-5a05-413b-983a-d2bc296cbb5e)"/></g><rect x="835.72" y="321" width="16" height="100" fill="#535461"/><rect x="835.72" y="288" width="16" height="33" fill="#3ad29f"/><rect x="857.72" y="329" width="16" height="100" fill="#535461"/><rect x="857.72" y="296" width="16" height="33" fill="#4d8af0"/><rect x="884.72" y="346" width="16" height="100" fill="#535461"/><rect x="884.72" y="313" width="16" height="33" fill="#f55f44"/><path d="M821.72,352h92a0,0,0,0,1,0,0v79.5a23.5,23.5,0,0,1-23.5,23.5h-45a23.5,23.5,0,0,1-23.5-23.5V352A0,0,0,0,1,821.72,352Z" fill="#0960bd"/><g opacity="0.1"><path d="M936,551v4h88v79.5a23.39,23.39,0,0,1-5,14.49,23.45,23.45,0,0,0,9-18.49V551Z" transform="translate(-114.28 -199)"/></g></svg>
0 2 \ No newline at end of file
... ...
src/components/Application/src/AppLogo.vue
... ... @@ -10,8 +10,13 @@
10 10 >
11 11 <img src="../../../assets/images/logo.png" />
12 12 <div
13   - class="ml-2 truncate xs:opacity-0 md:opacity-100"
14   - :class="`${prefixCls}__title`"
  13 + class="ml-2 truncate md:opacity-100"
  14 + :class="[
  15 + `${prefixCls}__title`,
  16 + {
  17 + 'xs:opacity-0': !alwaysShowTitle,
  18 + },
  19 + ]"
15 20 v-show="showTitle"
16 21 >
17 22 {{ title }}
... ... @@ -38,6 +43,7 @@
38 43 theme: propTypes.oneOf(['light', 'dark']),
39 44 // Whether to show title
40 45 showTitle: propTypes.bool.def(true),
  46 + alwaysShowTitle: propTypes.bool.def(false),
41 47 },
42 48 setup() {
43 49 const { prefixCls } = useDesign('app-logo');
... ...
src/components/CountDown/index.ts 0 → 100644
  1 +import CountButton from './src/CountButton.vue';
  2 +import CountdownInput from './src/CountdownInput.vue';
  3 +
  4 +export { CountdownInput, CountButton };
... ...
src/components/CountDown/src/CountButton.vue 0 → 100644
  1 +<template>
  2 + <Button v-bind="$attrs" :disabled="isStart" @click="handleStart" :loading="loading">
  3 + {{
  4 + !isStart
  5 + ? t('component.countdown.normalText')
  6 + : t('component.countdown.sendText', [currentCount])
  7 + }}
  8 + </Button>
  9 +</template>
  10 +<script lang="ts">
  11 + import { defineComponent, ref, PropType } from 'vue';
  12 +
  13 + import { Button } from 'ant-design-vue';
  14 +
  15 + import { useCountdown } from './useCountdown';
  16 + import { isFunction } from '/@/utils/is';
  17 + import { useI18n } from '/@/hooks/web/useI18n';
  18 +
  19 + export default defineComponent({
  20 + name: 'CountButton',
  21 + components: { Button },
  22 + props: {
  23 + count: {
  24 + type: Number,
  25 + default: 60,
  26 + },
  27 + beforeStartFunc: {
  28 + type: Function as PropType<() => boolean>,
  29 + default: null,
  30 + },
  31 + },
  32 + setup(props) {
  33 + const loading = ref(false);
  34 +
  35 + const { currentCount, isStart, start } = useCountdown(props.count);
  36 + const { t } = useI18n();
  37 + /**
  38 + * @description: Judge whether there is an external function before execution, and decide whether to start after execution
  39 + */
  40 + async function handleStart() {
  41 + const { beforeStartFunc } = props;
  42 + if (beforeStartFunc && isFunction(beforeStartFunc)) {
  43 + loading.value = true;
  44 + try {
  45 + const canStart = await beforeStartFunc();
  46 + canStart && start();
  47 + } finally {
  48 + loading.value = false;
  49 + }
  50 + } else {
  51 + start();
  52 + }
  53 + }
  54 + return { handleStart, isStart, currentCount, loading, t };
  55 + },
  56 + });
  57 +</script>
... ...
src/components/CountDown/src/CountdownInput.vue 0 → 100644
  1 +<template>
  2 + <div :class="prefixCls">
  3 + <AInput v-bind="$attrs" :size="size" v-model:value="state">
  4 + <template #addonAfter>
  5 + <CountButton :size="size" :count="count" :beforeStartFunc="sendCodeApi" />
  6 + </template>
  7 + </AInput>
  8 + </div>
  9 +</template>
  10 +<script lang="ts">
  11 + import { defineComponent, PropType } from 'vue';
  12 +
  13 + import { Input } from 'ant-design-vue';
  14 + import CountButton from './CountButton.vue';
  15 +
  16 + import { propTypes } from '/@/utils/propTypes';
  17 + import { useDesign } from '/@/hooks/web/useDesign';
  18 +
  19 + import { useRuleFormItem } from '/@/hooks/component/useFormItem';
  20 +
  21 + export default defineComponent({
  22 + name: 'CountDownInput',
  23 + components: { [Input.name]: Input, CountButton },
  24 + props: {
  25 + value: propTypes.string,
  26 + size: propTypes.oneOf(['default', 'large', 'small']),
  27 + count: propTypes.number.def(60),
  28 + sendCodeApi: {
  29 + type: Function as PropType<() => boolean>,
  30 + default: null,
  31 + },
  32 + },
  33 + setup(props) {
  34 + const { prefixCls } = useDesign('countdown-input');
  35 +
  36 + const [state] = useRuleFormItem(props);
  37 + return { prefixCls, state };
  38 + },
  39 + });
  40 +</script>
  41 +<style lang="less">
  42 + @prefix-cls: ~'@{namespace}-countdown-input';
  43 +
  44 + .@{prefix-cls} {
  45 + .ant-input-group-addon {
  46 + padding-right: 0;
  47 + background-color: transparent;
  48 + border: none;
  49 +
  50 + button {
  51 + font-size: 14px;
  52 + }
  53 + }
  54 + }
  55 +</style>
... ...
src/components/CountDown/src/useCountdown.ts 0 → 100644
  1 +import { ref, unref } from 'vue';
  2 +import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
  3 +
  4 +export function useCountdown(count: number) {
  5 + const currentCount = ref(count);
  6 +
  7 + const isStart = ref(false);
  8 +
  9 + let timerId: ReturnType<typeof setInterval> | null;
  10 +
  11 + function clear() {
  12 + timerId && window.clearInterval(timerId);
  13 + }
  14 +
  15 + function stop() {
  16 + isStart.value = false;
  17 + timerId = null;
  18 + clear();
  19 + }
  20 +
  21 + function start() {
  22 + if (unref(isStart) || !!timerId) {
  23 + return;
  24 + }
  25 + isStart.value = true;
  26 + timerId = setInterval(() => {
  27 + if (unref(currentCount) === 1) {
  28 + stop();
  29 + currentCount.value = count;
  30 + } else {
  31 + currentCount.value -= 1;
  32 + }
  33 + }, 1000);
  34 + }
  35 +
  36 + function reset() {
  37 + currentCount.value = count;
  38 + stop();
  39 + }
  40 +
  41 + function restart() {
  42 + reset();
  43 + start();
  44 + }
  45 +
  46 + tryOnUnmounted(() => {
  47 + reset();
  48 + });
  49 +
  50 + return { start, reset, restart, clear, stop, currentCount, isStart };
  51 +}
... ...
src/components/StrengthMeter/src/index.vue
1 1 <template>
2   - <div :class="prefixCls">
  2 + <div :class="prefixCls" class="relative">
3 3 <InputPassword
4 4 v-if="showInput"
5 5 v-bind="$attrs"
... ... @@ -24,15 +24,14 @@
24 24 import { Input } from 'ant-design-vue';
25 25  
26 26 import zxcvbn from '@zxcvbn-ts/core';
27   - import { propTypes } from '/@/utils/propTypes';
28 27 import { useDesign } from '/@/hooks/web/useDesign';
  28 + import { propTypes } from '/@/utils/propTypes';
29 29  
30 30 export default defineComponent({
31 31 name: 'StrengthMeter',
32 32 components: { InputPassword: Input.Password },
33 33 props: {
34 34 value: propTypes.string,
35   -
36 35 showInput: propTypes.bool.def(true),
37 36 disabled: propTypes.bool,
38 37 },
... ... @@ -43,9 +42,9 @@
43 42  
44 43 const getPasswordStrength = computed(() => {
45 44 const { disabled } = props;
46   - if (disabled) return null;
  45 + if (disabled) return -1;
47 46 const innerValue = unref(innerValueRef);
48   - const score = innerValue ? zxcvbn(unref(innerValueRef)).score : null;
  47 + const score = innerValue ? zxcvbn(unref(innerValueRef)).score : -1;
49 48 emit('score-change', score);
50 49 return score;
51 50 });
... ... @@ -57,6 +56,7 @@
57 56 watchEffect(() => {
58 57 innerValueRef.value = props.value || '';
59 58 });
  59 +
60 60 watch(
61 61 () => unref(innerValueRef),
62 62 (val) => {
... ... @@ -77,14 +77,12 @@
77 77 @prefix-cls: ~'@{namespace}-strength-meter';
78 78  
79 79 .@{prefix-cls} {
80   - position: relative;
81   -
82 80 &-bar {
83 81 position: relative;
84   - height: 4px;
  82 + height: 6px;
85 83 margin: 10px auto 6px;
86 84 background: @disabled-color;
87   - border-radius: 3px;
  85 + border-radius: 6px;
88 86  
89 87 &::before,
90 88 &::after {
... ...
src/design/ant/index.less
... ... @@ -13,10 +13,8 @@
13 13 }
14 14 }
15 15  
16   -body {
17   - .anticon:not(.app-iconify) {
18   - vertical-align: 0.1em;
19   - }
  16 +span.anticon:not(.app-iconify) {
  17 + vertical-align: 0.125em;
20 18 }
21 19  
22 20 .ant-back-top {
... ...
src/design/var/breakpoint.less
... ... @@ -2,37 +2,17 @@
2 2 // ==============屏幕断点============
3 3 // =================================
4 4  
5   -// Extra small screen / phone
6   -@screen-xs: 480px;
7   -@screen-xs-min: @screen-xs;
8   -
9 5 // Small screen / tablet
10   -@screen-sm: 576px;
11   -@screen-sm-min: @screen-sm;
  6 +@screen-sm: 640px;
12 7  
13 8 // Medium screen / desktop
14 9 @screen-md: 768px;
15   -@screen-md-min: @screen-md;
16 10  
17 11 // Large screen / wide desktop
18   -@screen-lg: 992px;
19   -@screen-lg-min: @screen-lg;
  12 +@screen-lg: 1024px;
20 13  
21 14 // Extra large screen / full hd
22   -@screen-xl: 1200px;
23   -@screen-xl-min: @screen-xl;
  15 +@screen-xl: 1280px;
24 16  
25 17 // Extra extra large screen / large desktop
26   -@screen-xxl: 1600px;
27   -@screen-xxl-min: @screen-xxl;
28   -
29   -@screen-xxxl: 1900px;
30   -@screen-xxxl-min: @screen-xxxl;
31   -
32   -// provide a maximum
33   -@screen-xs-max: (@screen-sm-min - 1px);
34   -@screen-sm-max: (@screen-md-min - 1px);
35   -@screen-md-max: (@screen-lg-min - 1px);
36   -@screen-lg-max: (@screen-xl-min - 1px);
37   -@screen-xl-max: (@screen-xxl-min - 1px);
38   -@screen-xxl-max: (@screen-xxxl-min - 1px);
  18 +@screen-2xl: 1536px;
... ...
src/layouts/default/header/components/user-dropdown/index.vue
... ... @@ -3,7 +3,7 @@
3 3 <span :class="[prefixCls, `${prefixCls}--${theme}`]">
4 4 <img :class="`${prefixCls}__header`" :src="headerImg" />
5 5 <span :class="`${prefixCls}__info`">
6   - <span :class="`${prefixCls}__name anticon`">{{ getUserInfo.realName }}</span>
  6 + <span :class="`${prefixCls}__name`" class="truncate">{{ getUserInfo.realName }}</span>
7 7 </span>
8 8 </span>
9 9  
... ...
src/locales/lang/en/component/countdown.ts 0 → 100644
  1 +export default {
  2 + normalText: 'Get SMS code',
  3 + sendText: 'Reacquire in {0}s',
  4 +};
... ...
src/locales/lang/en/sys/login.ts
1 1 export default {
2   - loginButton: 'Login',
3   - autoLogin: 'AutoLogin',
4   - forgetPassword: 'Forget Password',
  2 + backSignIn: 'Back sign in',
  3 + mobileSignInFormTitle: 'Mobile sign in',
  4 + qrSignInFormTitle: 'Qr code sign in',
  5 + signInFormTitle: 'Sign in',
  6 + signUpFormTitle: 'Sign up',
  7 + forgetFormTitle: 'Reset password',
  8 +
  9 + signInTitle: 'Backstage management system',
  10 + signInDesc: 'Enter your personal details and get started!',
  11 + policy: 'I agree to the xxx Privacy Policy',
  12 + scanSign: `scanning the code to complete the login`,
  13 +
  14 + loginButton: 'Sign in',
  15 + registerButton: 'Sign up',
  16 + rememberMe: 'Remember me',
  17 + forgetPassword: 'Forget Password?',
  18 + otherSignIn: 'Sign in with',
5 19  
6 20 // notify
7 21 loginSuccessTitle: 'Login successful',
8 22 loginSuccessDesc: 'Welcome back',
9 23  
10 24 // placeholder
11   - accountPlaceholder: 'Please input Username',
12   - passwordPlaceholder: 'Please input Password',
  25 + accountPlaceholder: 'Please input username',
  26 + passwordPlaceholder: 'Please input password',
  27 + smsPlaceholder: 'Please input sms code',
  28 + mobilePlaceholder: 'Please input mobile',
  29 + policyPlaceholder: 'Register after checking',
  30 + diffPwd: 'The two passwords are inconsistent',
  31 +
  32 + userName: 'Username',
  33 + password: 'Password',
  34 + confirmPassword: 'Confirm Password',
  35 + email: 'Email',
  36 + smsCode: 'SMS code',
  37 + mobile: 'Mobile',
13 38 };
... ...
src/locales/lang/zh_CN/component/countdown.ts 0 → 100644
  1 +export default {
  2 + normalText: '获取验证码',
  3 + sendText: '{0}秒后重新获取',
  4 +};
... ...
src/locales/lang/zh_CN/sys/login.ts
1 1 export default {
  2 + backSignIn: '返回',
  3 + signInFormTitle: '登录',
  4 + mobileSignInFormTitle: '手机登录',
  5 + qrSignInFormTitle: '二维码登录',
  6 + signUpFormTitle: '注册',
  7 + forgetFormTitle: '重置密码',
  8 +
  9 + signInTitle: '开箱即用的中后台管理系统',
  10 + signInDesc: '输入您的个人详细信息开始使用!',
  11 + policy: '我同意xxx隐私政策',
  12 + scanSign: `扫码后点击"确认",即可完成登录`,
  13 +
2 14 loginButton: '登录',
3   - autoLogin: '自动登录',
4   - forgetPassword: '忘记密码',
  15 + registerButton: '注册',
  16 + rememberMe: '记住我',
  17 + forgetPassword: '忘记密码?',
  18 + otherSignIn: '其他登录方式',
5 19  
6 20 // notify
7 21 loginSuccessTitle: '登录成功',
... ... @@ -10,4 +24,15 @@ export default {
10 24 // placeholder
11 25 accountPlaceholder: '请输入账号',
12 26 passwordPlaceholder: '请输入密码',
  27 + smsPlaceholder: '请输入验证码',
  28 + mobilePlaceholder: '请输入手机号码',
  29 + policyPlaceholder: '勾选后才能注册',
  30 + diffPwd: '两次输入密码不一致',
  31 +
  32 + userName: '账号',
  33 + password: '密码',
  34 + confirmPassword: '确认密码',
  35 + email: '邮箱',
  36 + smsCode: '短信验证码',
  37 + mobile: '手机号码',
13 38 };
... ...
src/views/sys/lock/LockPage.vue
1 1 <template>
2   - <div :class="prefixCls">
3   - <div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate">
  2 + <div
  3 + :class="prefixCls"
  4 + class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
  5 + >
  6 + <div
  7 + :class="`${prefixCls}__unlock`"
  8 + 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"
  9 + @click="handleShowForm(false)"
  10 + v-show="showDate"
  11 + >
4 12 <LockOutlined />
5 13 <span>{{ t('sys.lock.unlock') }}</span>
6 14 </div>
7 15  
8   - <div :class="`${prefixCls}__date`">
9   - <div :class="`${prefixCls}__hour`">
10   - {{ hour }}
11   - <span class="meridiem" v-show="showDate">{{ meridiem }}</span>
  16 + <div class="flex w-screen h-screen justify-center items-center">
  17 + <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
  18 + <span>{{ hour }}</span>
  19 + <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
  20 + {{ meridiem }}
  21 + </span>
12 22 </div>
13   - <div :class="`${prefixCls}__minute`">
14   - {{ minute }}
  23 + <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
  24 + <span> {{ minute }}</span>
15 25 </div>
16 26 </div>
17 27 <transition name="fade-slide">
18 28 <div :class="`${prefixCls}-entry`" v-show="!showDate">
19 29 <div :class="`${prefixCls}-entry-content`">
20   - <div :class="`${prefixCls}-entry__header`">
  30 + <div :class="`${prefixCls}-entry__header enter-x`">
21 31 <img :src="headerImg" :class="`${prefixCls}-entry__header-img`" />
22 32 <p :class="`${prefixCls}-entry__header-name`">
23 33 {{ realName }}
24 34 </p>
25 35 </div>
26   - <InputPassword :placeholder="t('sys.lock.placeholder')" v-model:value="password" />
27   - <span :class="`${prefixCls}-entry__err-msg`" v-if="errMsgRef">
  36 + <InputPassword
  37 + :placeholder="t('sys.lock.placeholder')"
  38 + class="enter-x"
  39 + v-model:value="password"
  40 + />
  41 + <span :class="`${prefixCls}-entry__err-msg enter-x`" v-if="errMsgRef">
28 42 {{ t('sys.lock.alert') }}
29 43 </span>
30   - <div :class="`${prefixCls}-entry__footer`">
  44 + <div :class="`${prefixCls}-entry__footer enter-x`">
31 45 <a-button
32 46 type="link"
33 47 size="small"
34   - class="mt-2 mr-2"
  48 + class="mt-2 mr-2 enter-x"
35 49 :disabled="loadingRef"
36 50 @click="handleShowForm(true)"
37 51 >
... ... @@ -40,7 +54,7 @@
40 54 <a-button
41 55 type="link"
42 56 size="small"
43   - class="mt-2 mr-2"
  57 + class="mt-2 mr-2 enter-x"
44 58 :disabled="loadingRef"
45 59 @click="goLogin"
46 60 >
... ... @@ -54,11 +68,11 @@
54 68 </div>
55 69 </transition>
56 70  
57   - <div :class="`${prefixCls}__footer-date`">
58   - <div class="time" v-show="!showDate">
59   - {{ hour }}:{{ minute }} <span class="meridiem">{{ meridiem }}</span>
  71 + <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
  72 + <div class="text-5xl mb-4 enter-x" v-show="!showDate">
  73 + {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
60 74 </div>
61   - <div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
  75 + <div class="text-2xl"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
62 76 </div>
63 77 </div>
64 78 </template>
... ... @@ -144,125 +158,54 @@
144 158 @prefix-cls: ~'@{namespace}-lock-page';
145 159  
146 160 .@{prefix-cls} {
147   - position: fixed;
148   - top: 0;
149   - right: 0;
150   - bottom: 0;
151   - left: 0;
152 161 z-index: @lock-page-z-index;
153   - display: flex;
154   - width: 100vw;
155   - height: 100vh;
156   - // background: rgba(23, 27, 41);
157   - background: #000;
158   - align-items: center;
159   - justify-content: center;
160 162  
161 163 &__unlock {
162   - position: absolute;
163   - top: 0;
164   - left: 50%;
165   - display: flex;
166   - height: 50px;
167   - padding-top: 20px;
168   - font-size: 18px;
169   - color: #fff;
170   - cursor: pointer;
171 164 transform: translate(-50%, 0);
172   - flex-direction: column;
173   - align-items: center;
174   - justify-content: space-between;
175   - transition: all 0.3s;
176   - }
177   -
178   - &__date {
179   - display: flex;
180   - width: 100vw;
181   - height: 100vh;
182   - align-items: center;
183   - justify-content: center;
184   - }
185   -
186   - &__hour {
187   - position: relative;
188   - margin-right: 80px;
189   -
190   - .meridiem {
191   - position: absolute;
192   - top: 20px;
193   - left: 20px;
194   - font-size: 26px;
195   - }
196   - @media (max-width: @screen-xs) {
197   - margin-right: 20px;
198   - }
199 165 }
200 166  
201 167 &__hour,
202 168 &__minute {
203 169 display: flex;
204   - width: 40%;
205   - height: 74%;
206 170 font-weight: 700;
207 171 color: #bababa;
208 172 background: #141313;
209 173 border-radius: 30px;
210 174 justify-content: center;
211 175 align-items: center;
212   - @media (min-width: @screen-xxxl-min) {
213   - font-size: 46em;
214   - }
215   - @media (min-width: @screen-xl-max) and (max-width: @screen-xxl-max) {
216   - font-size: 38em;
217   - }
218 176  
219   - @media (min-width: @screen-lg-max) and (max-width: @screen-xl-max) {
220   - font-size: 30em;
221   - }
222   - @media (min-width: @screen-md-max) and (max-width: @screen-lg-max) {
223   - font-size: 23em;
  177 + @media screen and (max-width: @screen-md) {
  178 + span:not(.meridiem) {
  179 + font-size: 160px;
  180 + }
224 181 }
225   - @media (min-width: @screen-sm-max) and (max-width: @screen-md-max) {
226   - height: 50%;
227   - font-size: 12em;
228   - border-radius: 10px;
229 182  
230   - .meridiem {
231   - font-size: 20px;
  183 + @media screen and (min-width: @screen-md) {
  184 + span:not(.meridiem) {
  185 + font-size: 160px;
232 186 }
233 187 }
234   - @media (min-width: @screen-xs-max) and (max-width: @screen-sm-max) {
235   - font-size: 13em;
236   - }
237   - @media (max-width: @screen-xs) {
238   - height: 30%;
239   - font-size: 5em;
240   - border-radius: 10px;
241 188  
242   - .meridiem {
243   - font-size: 14px;
  189 + @media screen and (max-width: @screen-sm) {
  190 + span:not(.meridiem) {
  191 + font-size: 90px;
244 192 }
245 193 }
246   - }
247   -
248   - &__footer-date {
249   - position: absolute;
250   - bottom: 20px;
251   - width: 100%;
252   - font-family: helvetica;
253   - color: #bababa;
254   - text-align: center;
255   -
256   - .time {
257   - font-size: 50px;
258   -
259   - .meridiem {
260   - font-size: 32px;
  194 + @media screen and (min-width: @screen-lg) {
  195 + span:not(.meridiem) {
  196 + font-size: 220px;
261 197 }
262 198 }
263 199  
264   - .date {
265   - font-size: 26px;
  200 + @media screen and (min-width: @screen-xl) {
  201 + span:not(.meridiem) {
  202 + font-size: 260px;
  203 + }
  204 + }
  205 + @media screen and (min-width: @screen-2xl) {
  206 + span:not(.meridiem) {
  207 + font-size: 320px;
  208 + }
266 209 }
267 210 }
268 211  
... ...
src/views/sys/login/ForgetPasswordForm.vue 0 → 100644
  1 +<template>
  2 + <Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
  3 + <FormItem name="account" class="enter-x">
  4 + <Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
  5 + </FormItem>
  6 +
  7 + <FormItem name="mobile" class="enter-x">
  8 + <Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
  9 + </FormItem>
  10 + <FormItem name="sms" class="enter-x">
  11 + <CountdownInput
  12 + size="large"
  13 + v-model:value="formData.sms"
  14 + :placeholder="t('sys.login.smsCode')"
  15 + />
  16 + </FormItem>
  17 +
  18 + <FormItem class="enter-x">
  19 + <Button
  20 + type="primary"
  21 + size="large"
  22 + block
  23 + @click="handleReset"
  24 + :loading="loading"
  25 + class="enter-x"
  26 + >
  27 + {{ t('common.resetText') }}
  28 + </Button>
  29 + <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
  30 + {{ t('sys.login.backSignIn') }}
  31 + </Button>
  32 + </FormItem>
  33 + </Form>
  34 +</template>
  35 +<script lang="ts">
  36 + import { defineComponent, reactive, ref } from 'vue';
  37 +
  38 + import { Form, Input, Button } from 'ant-design-vue';
  39 + import { CountdownInput } from '/@/components/CountDown';
  40 +
  41 + import { useI18n } from '/@/hooks/web/useI18n';
  42 + import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
  43 +
  44 + export default defineComponent({
  45 + name: 'ForgetPasswordForm',
  46 + components: {
  47 + Button,
  48 + Form,
  49 + FormItem: Form.Item,
  50 + Input,
  51 + CountdownInput,
  52 + },
  53 + setup() {
  54 + const { t } = useI18n();
  55 + const { setLoginState } = useLoginState();
  56 + const { getFormRules } = useFormRules();
  57 +
  58 + const formRef = ref<any>(null);
  59 + const loading = ref(false);
  60 +
  61 + const formData = reactive({
  62 + account: '',
  63 + mobile: '',
  64 + sms: '',
  65 + });
  66 +
  67 + const { validForm } = useFormValid(formRef);
  68 +
  69 + async function handleReset() {
  70 + const data = await validForm();
  71 + if (!data) return;
  72 + console.log(data);
  73 + }
  74 +
  75 + function handleBackLogin() {
  76 + setLoginState(LoginStateEnum.LOGIN);
  77 + }
  78 +
  79 + return {
  80 + t,
  81 + formRef,
  82 + formData,
  83 + getFormRules,
  84 + handleReset,
  85 + loading,
  86 + handleBackLogin,
  87 + };
  88 + },
  89 + });
  90 +</script>
... ...
src/views/sys/login/Login.vue
1 1 <template>
2   - <div class="login">
3   - <div class="opacity-0 login-mask lg:opacity-100"></div>
4   - <div class="justify-center login-form-wrap lg:justify-end">
5   - <div class="mx-6 login-form">
6   - <AppLocalePicker v-if="showLocale" class="login-form__locale" />
7   - <div class="px-2 py-10 login-form__content">
8   - <header>
9   - <img :src="logo" class="mr-4" />
10   - <h1>{{ title }}</h1>
11   - </header>
12   -
13   - <a-form class="login-form__main" :model="formData" :rules="formRules" ref="formRef">
14   - <a-form-item name="account">
15   - <a-input size="large" v-model:value="formData.account" placeholder="username: vben" />
16   - </a-form-item>
17   - <a-form-item name="password">
18   - <a-input-password
19   - size="large"
20   - visibilityToggle
21   - v-model:value="formData.password"
22   - placeholder="password: 123456"
23   - />
24   - </a-form-item>
25   -
26   - <a-row>
27   - <a-col :span="12">
28   - <a-form-item>
29   - <!-- No logic, you need to deal with it yourself -->
30   - <a-checkbox v-model:checked="autoLogin" size="small">{{
31   - t('sys.login.autoLogin')
32   - }}</a-checkbox>
33   - </a-form-item>
34   - </a-col>
35   - <a-col :span="12">
36   - <a-form-item :style="{ 'text-align': 'right' }">
37   - <!-- No logic, you need to deal with it yourself -->
38   - <a-button type="link" size="small">
39   - {{ t('sys.login.forgetPassword') }}
40   - </a-button>
41   - </a-form-item>
42   - </a-col>
43   - </a-row>
44   - <a-form-item>
45   - <a-button
46   - type="primary"
47   - size="large"
48   - class="rounded-sm"
49   - :block="true"
50   - @click="login"
51   - :loading="formState.loading"
52   - >
53   - {{ t('sys.login.loginButton') }}
54   - </a-button>
55   - </a-form-item>
56   - </a-form>
  2 + <div :class="prefixCls" class="relative w-full h-full px-4">
  3 + <AppLocalePicker
  4 + class="absolute top-4 right-4 enter-x text-white xl:text-gray-600"
  5 + :showText="false"
  6 + />
  7 +
  8 + <span class="-enter-x xl:hidden">
  9 + <AppLogo :alwaysShowTitle="true" />
  10 + </span>
  11 +
  12 + <div class="container relative h-full py-2 mx-auto sm:px-10">
  13 + <div class="flex h-full">
  14 + <div class="hidden xl:flex xl:flex-col xl:w-6/12 min-h-full mr-4 pl-4">
  15 + <AppLogo class="-enter-x" />
  16 + <div class="my-auto">
  17 + <img
  18 + :alt="title"
  19 + src="../../../assets/svg/login-box-bg.svg"
  20 + class="w-1/2 -mt-16 -enter-x"
  21 + />
  22 + <div class="mt-10 font-medium text-white -enter-x">
  23 + <span class="mt-4 text-3xl inline-block"> {{ t('sys.login.signInTitle') }}</span>
  24 + </div>
  25 + <div class="mt-5 text-md text-white font-normal dark:text-gray-500 -enter-x">
  26 + {{ t('sys.login.signInDesc') }}
  27 + </div>
  28 + </div>
  29 + </div>
  30 + <div class="h-full xl:h-auto flex py-5 xl:py-0 xl:my-0 w-full xl:w-6/12">
  31 + <div
  32 + 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"
  33 + >
  34 + <h2 class="font-bold text-2xl xl:text-3xl enter-x text-center xl:text-left mb-6">
  35 + {{ getFormTitle }}
  36 + </h2>
  37 + <LoginForm v-show="getShowLogin" />
  38 + <ForgetPasswordForm v-if="getShowResetPassword" />
  39 + <RegisterForm v-if="getShowRegister" />
  40 + <MobileForm v-if="getShowMobile" />
  41 + <QrCodeForm v-if="getShowQrCode" />
  42 + </div>
57 43 </div>
58 44 </div>
59 45 </div>
60 46 </div>
61 47 </template>
62 48 <script lang="ts">
63   - import { defineComponent, reactive, ref, unref, toRaw } from 'vue';
64   - import { Checkbox, Form, Input, Row, Col } from 'ant-design-vue';
  49 + import { defineComponent, computed } from 'vue';
65 50  
66   - import { Button } from '/@/components/Button';
  51 + import { AppLogo } from '/@/components/Application';
67 52 import { AppLocalePicker } from '/@/components/Application';
  53 + import LoginForm from './LoginForm.vue';
  54 + import ForgetPasswordForm from './ForgetPasswordForm.vue';
  55 + import RegisterForm from './RegisterForm.vue';
  56 + import MobileForm from './MobileForm.vue';
  57 + import QrCodeForm from './QrCodeForm.vue';
68 58  
69   - import { userStore } from '/@/store/modules/user';
70   -
71   - import { useMessage } from '/@/hooks/web/useMessage';
72 59 import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
73   - import logo from '/@/assets/images/logo.png';
74 60 import { useI18n } from '/@/hooks/web/useI18n';
  61 + import { useDesign } from '/@/hooks/web/useDesign';
  62 + import { useShowLoginForm, useFormTitle } from './useLogin';
75 63  
76 64 export default defineComponent({
  65 + name: 'Login',
77 66 components: {
78   - [Checkbox.name]: Checkbox,
79   - [Form.name]: Form,
80   - [Form.Item.name]: Form.Item,
81   - [Input.name]: Input,
82   - [Input.Password.name]: Input.Password,
83   - AButton: Button,
  67 + AppLogo,
  68 + LoginForm,
  69 + ForgetPasswordForm,
  70 + RegisterForm,
  71 + MobileForm,
  72 + QrCodeForm,
84 73 AppLocalePicker,
85   - [Row.name]: Row,
86   - [Col.name]: Col,
87 74 },
88 75 setup() {
89   - const formRef = ref<any>(null);
90   - const autoLoginRef = ref(false);
91   -
92 76 const globSetting = useGlobSetting();
  77 + const { getFormTitle } = useFormTitle();
  78 + const { prefixCls } = useDesign('login');
93 79 const { locale } = useProjectSetting();
94   - const { notification } = useMessage();
95 80 const { t } = useI18n();
96 81  
97   - const formData = reactive({
98   - account: 'vben',
99   - password: '123456',
100   - });
101   -
102   - const formState = reactive({
103   - loading: false,
104   - });
105   -
106   - const formRules = reactive({
107   - account: [{ required: true, message: t('sys.login.accountPlaceholder'), trigger: 'blur' }],
108   - password: [
109   - { required: true, message: t('sys.login.passwordPlaceholder'), trigger: 'blur' },
110   - ],
111   - });
112   -
113   - async function handleLogin() {
114   - const form = unref(formRef);
115   - if (!form) return;
116   - formState.loading = true;
117   - try {
118   - const data = await form.validate();
119   - const userInfo = await userStore.login(
120   - toRaw({
121   - password: data.password,
122   - username: data.account,
123   - })
124   - );
125   - if (userInfo) {
126   - notification.success({
127   - message: t('sys.login.loginSuccessTitle'),
128   - description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
129   - duration: 3,
130   - });
131   - }
132   - } catch (error) {
133   - } finally {
134   - formState.loading = false;
135   - }
136   - }
137 82 return {
138   - formRef,
139   - formData,
140   - formState,
141   - formRules,
142   - login: handleLogin,
143   - autoLogin: autoLoginRef,
144   - title: globSetting && globSetting.title,
145   - logo,
146 83 t,
147   - showLocale: locale.show,
  84 + prefixCls,
  85 + title: computed(() => globSetting?.title ?? ''),
  86 + showLocale: computed(() => locale.show),
  87 + getFormTitle,
  88 + ...useShowLoginForm(),
148 89 };
149 90 },
150 91 });
151 92 </script>
152 93 <style lang="less">
153   - .login-form__locale {
154   - position: absolute;
155   - top: 14px;
156   - right: 14px;
157   - z-index: 1;
158   - }
  94 + @prefix-cls: ~'@{namespace}-login';
  95 + @logo-prefix-cls: ~'@{namespace}-app-logo';
  96 + @countdown-prefix-cls: ~'@{namespace}-countdown-input';
159 97  
160   - .login {
161   - position: relative;
162   - height: 100vh;
163   - background: url(../../../assets/images/login/login-bg.png) no-repeat;
164   - background-size: 100% 100%;
  98 + .@{prefix-cls} {
  99 + @media (max-width: @screen-xl) {
  100 + background: linear-gradient(180deg, #1c3faa, #1c3faa);
  101 + }
165 102  
166   - &-mask {
  103 + &::before {
  104 + position: absolute;
  105 + top: 0;
  106 + left: 0;
  107 + width: 100%;
167 108 height: 100%;
168   - background: url(../../../assets/images/login/login-in.png) no-repeat;
169   - background-position: 30% 30%;
170   - background-size: 80% 80%;
  109 + margin-left: -48%;
  110 + background-image: url(/@/assets/svg/login-bg.svg);
  111 + background-position: 100%;
  112 + background-repeat: no-repeat;
  113 + background-size: auto 100%;
  114 + content: '';
  115 + @media (max-width: @screen-xl) {
  116 + display: none;
  117 + }
171 118 }
172 119  
173   - &-form {
174   - position: relative;
175   - bottom: 60px;
176   - width: 400px;
177   - background: @white;
178   - border: 10px solid rgba(255, 255, 255, 0.5);
179   - border-width: 8px;
180   - border-radius: 4px;
181   - background-clip: padding-box;
182   -
183   - &__main {
184   - margin: 30px auto 0 auto !important;
  120 + .@{logo-prefix-cls} {
  121 + position: absolute;
  122 + top: 12px;
  123 + height: 30px;
  124 +
  125 + &__title {
  126 + font-size: 16px;
  127 + color: #fff;
185 128 }
186 129  
187   - &-wrap {
188   - position: absolute;
189   - top: 0;
190   - right: 0;
191   - display: flex;
192   - width: 100%;
193   - height: 100%;
194   - align-items: center;
  130 + img {
  131 + width: 32px;
195 132 }
  133 + }
  134 +
  135 + .container {
  136 + .@{logo-prefix-cls} {
  137 + display: flex;
  138 + width: 60%;
  139 + height: 80px;
  140 +
  141 + &__title {
  142 + font-size: 24px;
  143 + color: #fff;
  144 + }
196 145  
197   - &__content {
198   - position: relative;
199   - width: 100%;
200   - height: 100%;
201   - padding: 60px 0 40px 0;
202   - border: 1px solid #999;
203   - border-radius: 2px;
204   -
205   - header {
206   - display: flex;
207   - justify-content: center;
208   - align-items: center;
209   -
210   - img {
211   - display: inline-block;
212   - width: 48px;
213   - }
214   -
215   - h1 {
216   - margin-bottom: 0;
217   - font-size: 24px;
218   - text-align: center;
219   - }
  146 + img {
  147 + width: 48px;
220 148 }
  149 + }
  150 + }
  151 +
  152 + &-sign-in-way {
  153 + .anticon {
  154 + font-size: 22px;
  155 + color: #888;
  156 + cursor: pointer;
221 157  
222   - form {
223   - width: 80%;
  158 + &:hover {
  159 + color: @primary-color;
224 160 }
225 161 }
226 162 }
  163 +
  164 + input:not([type='checkbox']) {
  165 + min-width: 360px;
  166 + @media (max-width: @screen-sm) {
  167 + min-width: 240px;
  168 + }
  169 + }
  170 + .@{countdown-prefix-cls} input {
  171 + min-width: unset;
  172 + }
  173 +
  174 + .ant-divider-inner-text {
  175 + font-size: 12px;
  176 + color: @text-color-secondary;
  177 + }
227 178 }
228 179 </style>
... ...
src/views/sys/login/LoginForm.vue 0 → 100644
  1 +<template>
  2 + <Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
  3 + <FormItem name="account" class="enter-x">
  4 + <Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
  5 + </FormItem>
  6 + <FormItem name="password" class="enter-x">
  7 + <InputPassword
  8 + size="large"
  9 + visibilityToggle
  10 + v-model:value="formData.password"
  11 + :placeholder="t('sys.login.password')"
  12 + />
  13 + </FormItem>
  14 +
  15 + <ARow class="enter-x">
  16 + <ACol :span="12">
  17 + <FormItem>
  18 + <!-- No logic, you need to deal with it yourself -->
  19 + <Checkbox v-model:checked="rememberMe" size="small">
  20 + {{ t('sys.login.rememberMe') }}
  21 + </Checkbox>
  22 + </FormItem>
  23 + </ACol>
  24 + <ACol :span="12">
  25 + <FormItem :style="{ 'text-align': 'right' }">
  26 + <!-- No logic, you need to deal with it yourself -->
  27 + <Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
  28 + {{ t('sys.login.forgetPassword') }}
  29 + </Button>
  30 + </FormItem>
  31 + </ACol>
  32 + </ARow>
  33 +
  34 + <FormItem class="enter-x">
  35 + <Button
  36 + type="primary"
  37 + size="large"
  38 + block
  39 + @click="handleLogin"
  40 + :loading="loading"
  41 + class="enter-x"
  42 + >
  43 + {{ t('sys.login.loginButton') }}
  44 + </Button>
  45 + <!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
  46 + {{ t('sys.login.registerButton') }}
  47 + </Button> -->
  48 + </FormItem>
  49 + <ARow class="enter-x">
  50 + <ACol :span="7">
  51 + <Button block @click="setLoginState(LoginStateEnum.MOBILE)">
  52 + {{ t('sys.login.mobileSignInFormTitle') }}
  53 + </Button>
  54 + </ACol>
  55 + <ACol :span="8" :offset="1">
  56 + <Button block @click="setLoginState(LoginStateEnum.QR_CODE)">
  57 + {{ t('sys.login.qrSignInFormTitle') }}
  58 + </Button>
  59 + </ACol>
  60 + <ACol :span="7" :offset="1">
  61 + <Button block @click="setLoginState(LoginStateEnum.REGISTER)">
  62 + {{ t('sys.login.registerButton') }}
  63 + </Button>
  64 + </ACol>
  65 + </ARow>
  66 +
  67 + <Divider>{{ t('sys.login.otherSignIn') }}</Divider>
  68 +
  69 + <div class="flex justify-evenly enter-x" :class="`${prefixCls}-sign-in-way`">
  70 + <GithubFilled />
  71 + <WechatFilled />
  72 + <AlipayCircleFilled />
  73 + <GoogleCircleFilled />
  74 + <TwitterCircleFilled />
  75 + </div>
  76 + </Form>
  77 +</template>
  78 +<script lang="ts">
  79 + import { defineComponent, reactive, ref, toRaw } from 'vue';
  80 +
  81 + import { Checkbox, Form, Input, Row, Col, Button, Divider } from 'ant-design-vue';
  82 + import {
  83 + GithubFilled,
  84 + WechatFilled,
  85 + AlipayCircleFilled,
  86 + GoogleCircleFilled,
  87 + TwitterCircleFilled,
  88 + } from '@ant-design/icons-vue';
  89 +
  90 + import { useI18n } from '/@/hooks/web/useI18n';
  91 + import { useMessage } from '/@/hooks/web/useMessage';
  92 +
  93 + import { userStore } from '/@/store/modules/user';
  94 + import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
  95 + import { useDesign } from '/@/hooks/web/useDesign';
  96 +
  97 + export default defineComponent({
  98 + name: 'LoginForm',
  99 + components: {
  100 + Checkbox,
  101 + Button,
  102 + Form,
  103 + FormItem: Form.Item,
  104 + Input,
  105 + Divider,
  106 + InputPassword: Input.Password,
  107 + [Col.name]: Col,
  108 + [Row.name]: Row,
  109 + GithubFilled,
  110 + WechatFilled,
  111 + AlipayCircleFilled,
  112 + GoogleCircleFilled,
  113 + TwitterCircleFilled,
  114 + },
  115 + setup() {
  116 + const { t } = useI18n();
  117 + const { notification } = useMessage();
  118 + const { prefixCls } = useDesign('login');
  119 +
  120 + const { setLoginState } = useLoginState();
  121 + const { getFormRules } = useFormRules();
  122 +
  123 + const formRef = ref<any>(null);
  124 + const loading = ref(false);
  125 + const rememberMe = ref(false);
  126 +
  127 + const formData = reactive({
  128 + account: 'vben',
  129 + password: '123456',
  130 + });
  131 +
  132 + const { validForm } = useFormValid(formRef);
  133 +
  134 + async function handleLogin() {
  135 + const data = await validForm();
  136 + if (!data) return;
  137 + try {
  138 + loading.value = true;
  139 + const userInfo = await userStore.login(
  140 + toRaw({
  141 + password: data.password,
  142 + username: data.account,
  143 + })
  144 + );
  145 + if (userInfo) {
  146 + notification.success({
  147 + message: t('sys.login.loginSuccessTitle'),
  148 + description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
  149 + duration: 3,
  150 + });
  151 + }
  152 + } finally {
  153 + loading.value = false;
  154 + }
  155 + }
  156 +
  157 + return {
  158 + t,
  159 + prefixCls,
  160 + formRef,
  161 + formData,
  162 + getFormRules,
  163 + rememberMe,
  164 + handleLogin,
  165 + loading,
  166 + setLoginState,
  167 + LoginStateEnum,
  168 + };
  169 + },
  170 + });
  171 +</script>
... ...
src/views/sys/login/MobileForm.vue 0 → 100644
  1 +<template>
  2 + <Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
  3 + <FormItem name="mobile" class="enter-x">
  4 + <Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
  5 + </FormItem>
  6 + <FormItem name="sms" class="enter-x">
  7 + <CountdownInput
  8 + size="large"
  9 + v-model:value="formData.sms"
  10 + :placeholder="t('sys.login.smsCode')"
  11 + />
  12 + </FormItem>
  13 +
  14 + <FormItem class="enter-x">
  15 + <Button
  16 + type="primary"
  17 + size="large"
  18 + block
  19 + @click="handleLogin"
  20 + :loading="loading"
  21 + class="enter-x"
  22 + >
  23 + {{ t('sys.login.loginButton') }}
  24 + </Button>
  25 + <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
  26 + {{ t('sys.login.backSignIn') }}
  27 + </Button>
  28 + </FormItem>
  29 + </Form>
  30 +</template>
  31 +<script lang="ts">
  32 + import { defineComponent, reactive, ref } from 'vue';
  33 +
  34 + import { Form, Input, Button } from 'ant-design-vue';
  35 + import { CountdownInput } from '/@/components/CountDown';
  36 +
  37 + import { useI18n } from '/@/hooks/web/useI18n';
  38 + import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
  39 +
  40 + export default defineComponent({
  41 + name: 'MobileForm',
  42 + components: {
  43 + Button,
  44 + Form,
  45 + FormItem: Form.Item,
  46 + Input,
  47 + CountdownInput,
  48 + },
  49 + setup() {
  50 + const { t } = useI18n();
  51 + const { setLoginState } = useLoginState();
  52 + const { getFormRules } = useFormRules();
  53 +
  54 + const formRef = ref<any>(null);
  55 + const loading = ref(false);
  56 +
  57 + const formData = reactive({
  58 + mobile: '',
  59 + sms: '',
  60 + });
  61 +
  62 + const { validForm } = useFormValid(formRef);
  63 +
  64 + async function handleLogin() {
  65 + const data = await validForm();
  66 + if (!data) return;
  67 + console.log(data);
  68 + }
  69 +
  70 + function handleBackLogin() {
  71 + setLoginState(LoginStateEnum.LOGIN);
  72 + }
  73 +
  74 + return {
  75 + t,
  76 + formRef,
  77 + formData,
  78 + getFormRules,
  79 + handleLogin,
  80 + loading,
  81 + handleBackLogin,
  82 + };
  83 + },
  84 + });
  85 +</script>
... ...
src/views/sys/login/QrCodeForm.vue 0 → 100644
  1 +<template>
  2 + <div class="enter-x min-w-64 min-h-64">
  3 + <QrCode :value="qrCodeUrl" class="enter-x flex justify-center xl:justify-start" :width="280" />
  4 + <Divider>{{ t('sys.login.scanSign') }}</Divider>
  5 + <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
  6 + {{ t('sys.login.backSignIn') }}
  7 + </Button>
  8 + </div>
  9 +</template>
  10 +<script lang="ts">
  11 + import { defineComponent } from 'vue';
  12 +
  13 + import { Button, Divider } from 'ant-design-vue';
  14 +
  15 + import { useI18n } from '/@/hooks/web/useI18n';
  16 + import { LoginStateEnum, useLoginState } from './useLogin';
  17 + import { QrCode } from '/@/components/Qrcode/index';
  18 + const qrCodeUrl = 'https://vvbin.cn/next/login';
  19 + export default defineComponent({
  20 + name: 'QrCodeForm',
  21 + components: {
  22 + Button,
  23 + QrCode,
  24 + Divider,
  25 + },
  26 + setup() {
  27 + const { t } = useI18n();
  28 + const { setLoginState } = useLoginState();
  29 +
  30 + function handleBackLogin() {
  31 + setLoginState(LoginStateEnum.LOGIN);
  32 + }
  33 + return {
  34 + t,
  35 + handleBackLogin,
  36 + qrCodeUrl,
  37 + };
  38 + },
  39 + });
  40 +</script>
... ...
src/views/sys/login/RegisterForm.vue 0 → 100644
  1 +<template>
  2 + <Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
  3 + <FormItem name="account" class="enter-x">
  4 + <Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
  5 + </FormItem>
  6 + <FormItem name="mobile" class="enter-x">
  7 + <Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
  8 + </FormItem>
  9 + <FormItem name="sms" class="enter-x">
  10 + <CountdownInput
  11 + size="large"
  12 + v-model:value="formData.sms"
  13 + :placeholder="t('sys.login.smsCode')"
  14 + />
  15 + </FormItem>
  16 + <FormItem name="password" class="enter-x">
  17 + <StrengthMeter
  18 + size="large"
  19 + v-model:value="formData.password"
  20 + :placeholder="t('sys.login.password')"
  21 + />
  22 + </FormItem>
  23 + <FormItem name="confirmPassword" class="enter-x">
  24 + <InputPassword
  25 + size="large"
  26 + visibilityToggle
  27 + v-model:value="formData.confirmPassword"
  28 + :placeholder="t('sys.login.confirmPassword')"
  29 + />
  30 + </FormItem>
  31 +
  32 + <FormItem class="enter-x" name="policy">
  33 + <!-- No logic, you need to deal with it yourself -->
  34 + <Checkbox v-model:checked="formData.policy" size="small">
  35 + {{ t('sys.login.policy') }}
  36 + </Checkbox>
  37 + </FormItem>
  38 +
  39 + <Button
  40 + type="primary"
  41 + size="large"
  42 + block
  43 + @click="handleReset"
  44 + :loading="loading"
  45 + class="enter-x"
  46 + >
  47 + {{ t('sys.login.registerButton') }}
  48 + </Button>
  49 + <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
  50 + {{ t('sys.login.backSignIn') }}
  51 + </Button>
  52 + </Form>
  53 +</template>
  54 +<script lang="ts">
  55 + import { defineComponent, reactive, ref } from 'vue';
  56 +
  57 + import { Form, Input, Button, Divider, Checkbox } from 'ant-design-vue';
  58 + import { StrengthMeter } from '/@/components/StrengthMeter';
  59 + import { CountdownInput } from '/@/components/CountDown';
  60 +
  61 + import { useI18n } from '/@/hooks/web/useI18n';
  62 + import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
  63 +
  64 + export default defineComponent({
  65 + name: 'RegisterPasswordForm',
  66 + components: {
  67 + Button,
  68 + Form,
  69 + FormItem: Form.Item,
  70 + Input,
  71 + Divider,
  72 + InputPassword: Input.Password,
  73 + Checkbox,
  74 + StrengthMeter,
  75 + CountdownInput,
  76 + },
  77 + setup() {
  78 + const { t } = useI18n();
  79 + const { setLoginState } = useLoginState();
  80 +
  81 + const formRef = ref<any>(null);
  82 + const loading = ref(false);
  83 +
  84 + const formData = reactive({
  85 + account: '',
  86 + password: '',
  87 + confirmPassword: '',
  88 + mobile: '',
  89 + sms: '',
  90 + policy: false,
  91 + });
  92 +
  93 + const { getFormRules } = useFormRules(formData);
  94 + const { validForm } = useFormValid(formRef);
  95 +
  96 + async function handleReset() {
  97 + const data = await validForm();
  98 + if (!data) return;
  99 + console.log(data);
  100 + }
  101 +
  102 + function handleBackLogin() {
  103 + setLoginState(LoginStateEnum.LOGIN);
  104 + }
  105 +
  106 + return {
  107 + t,
  108 + formRef,
  109 + formData,
  110 + getFormRules,
  111 + handleReset,
  112 + loading,
  113 + handleBackLogin,
  114 + };
  115 + },
  116 + });
  117 +</script>
... ...
src/views/sys/login/useLogin.ts 0 → 100644
  1 +import { RuleObject } from 'ant-design-vue/lib/form/interface';
  2 +import { ref, computed, unref, Ref } from 'vue';
  3 +import { useI18n } from '/@/hooks/web/useI18n';
  4 +
  5 +export enum LoginStateEnum {
  6 + LOGIN,
  7 + REGISTER,
  8 + RESET_PASSWORD,
  9 + MOBILE,
  10 + QR_CODE,
  11 +}
  12 +
  13 +const currentState = ref(LoginStateEnum.LOGIN);
  14 +
  15 +export function useFormTitle() {
  16 + const { t } = useI18n();
  17 +
  18 + const getFormTitle = computed(() => {
  19 + const titleObj = {
  20 + [LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
  21 + [LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
  22 + [LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
  23 + [LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
  24 + [LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
  25 + };
  26 + return titleObj[unref(currentState)];
  27 + });
  28 + return { getFormTitle };
  29 +}
  30 +
  31 +export function useLoginState() {
  32 + function setLoginState(state: LoginStateEnum) {
  33 + currentState.value = state;
  34 + }
  35 +
  36 + const getLoginState = computed(() => currentState.value);
  37 +
  38 + return { setLoginState, getLoginState };
  39 +}
  40 +
  41 +export function useShowLoginForm() {
  42 + const getShowLogin = computed(() => unref(currentState) === LoginStateEnum.LOGIN);
  43 + const getShowResetPassword = computed(
  44 + () => unref(currentState) === LoginStateEnum.RESET_PASSWORD
  45 + );
  46 + const getShowRegister = computed(() => unref(currentState) === LoginStateEnum.REGISTER);
  47 + const getShowMobile = computed(() => unref(currentState) === LoginStateEnum.MOBILE);
  48 + const getShowQrCode = computed(() => unref(currentState) === LoginStateEnum.QR_CODE);
  49 +
  50 + return { getShowLogin, getShowResetPassword, getShowRegister, getShowMobile, getShowQrCode };
  51 +}
  52 +
  53 +export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
  54 + async function validForm() {
  55 + const form = unref(formRef);
  56 + if (!form) return;
  57 + const data = await form.validate();
  58 + return data as T;
  59 + }
  60 +
  61 + return { validForm };
  62 +}
  63 +
  64 +export function useFormRules(formData?: Recordable) {
  65 + const { t } = useI18n();
  66 +
  67 + const getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder')));
  68 + const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder')));
  69 + const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder')));
  70 + const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder')));
  71 +
  72 + const validatePolicy = async (_: RuleObject, value: boolean) => {
  73 + return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve();
  74 + };
  75 +
  76 + const validateConfirmPassword = (password: string) => {
  77 + return async (_: RuleObject, value: string) => {
  78 + if (!value) {
  79 + return Promise.reject(t('sys.login.passwordPlaceholder'));
  80 + }
  81 + if (value !== password) {
  82 + return Promise.reject(t('sys.login.diffPwd'));
  83 + }
  84 + return Promise.resolve();
  85 + };
  86 + };
  87 +
  88 + const getFormRules = computed(() => {
  89 + const accountFormRule = unref(getAccountFormRule);
  90 + const passwordFormRule = unref(getPasswordFormRule);
  91 + const smsFormRule = unref(getSmsFormRule);
  92 + const mobileFormRule = unref(getMobileFormRule);
  93 +
  94 + const mobileRule = {
  95 + sms: smsFormRule,
  96 + mobile: mobileFormRule,
  97 + };
  98 + switch (unref(currentState)) {
  99 + case LoginStateEnum.REGISTER:
  100 + return {
  101 + account: accountFormRule,
  102 + password: passwordFormRule,
  103 + confirmPassword: [
  104 + { validator: validateConfirmPassword(formData?.password), trigger: 'change' },
  105 + ],
  106 + policy: [{ validator: validatePolicy, trigger: 'change' }],
  107 + ...mobileRule,
  108 + };
  109 + case LoginStateEnum.RESET_PASSWORD:
  110 + return {
  111 + account: accountFormRule,
  112 + ...mobileRule,
  113 + };
  114 + case LoginStateEnum.MOBILE:
  115 + return mobileRule;
  116 + default:
  117 + return {
  118 + account: accountFormRule,
  119 + password: passwordFormRule,
  120 + };
  121 + }
  122 + });
  123 + return { getFormRules };
  124 +}
  125 +
  126 +function createRule(message: string) {
  127 + return [
  128 + {
  129 + required: true,
  130 + message,
  131 + trigger: 'change',
  132 + },
  133 + ];
  134 +}
... ...
tailwind.config.js deleted 100644 → 0
1   -/* eslint-disable @typescript-eslint/no-var-requires */
2   -const colors = require('windicss/colors');
3   -const defaultTheme = require('windicss/defaultTheme');
4   -module.exports = {
5   - darkMode: 'class',
6   - plugins: [
7   - require('windicss/plugin/forms'),
8   - require('windicss/plugin/typography'),
9   - require('windicss/plugin/line-clamp'),
10   - require('windicss/plugin/aspect-ratio'),
11   - ],
12   - theme: {
13   - extend: {
14   - colors,
15   - fontFamily: {
16   - sans: ['Righteous', ...defaultTheme.fontFamily.sans],
17   - },
18   - },
19   - },
20   -};
windi.config.ts 0 → 100644
  1 +import lineClamp from 'windicss/plugin/line-clamp';
  2 +import colors from 'windicss/colors';
  3 +
  4 +import { defineConfig } from 'vite-plugin-windicss';
  5 +
  6 +export default defineConfig({
  7 + darkMode: 'class',
  8 + plugins: [lineClamp, createEnterPlugin()],
  9 + theme: {
  10 + extend: {
  11 + colors,
  12 + },
  13 +
  14 + // screen: {
  15 + // sm: '576px',
  16 + // md: '768px',
  17 + // lg: '992px',
  18 + // xl: '1200px',
  19 + // '2xl': '1600px',
  20 + // },
  21 + },
  22 +});
  23 +
  24 +/**
  25 + * Used for animation when the element is displayed
  26 + * @param maxOutput The larger the maxOutput output, the larger the generated css volume
  27 + */
  28 +function createEnterPlugin(maxOutput = 10) {
  29 + const createCss = (index: number, d = 'x') => {
  30 + const upd = d.toUpperCase();
  31 + return {
  32 + [`*> .enter-${d}:nth-child(${index})`]: {
  33 + transform: `translate${upd}(50px)`,
  34 + },
  35 + [`*> .-enter-${d}:nth-child(${index})`]: {
  36 + transform: `translate${upd}(-50px)`,
  37 + },
  38 + [`* > .enter-${d}:nth-child(${index}),* > .-enter-${d}:nth-child(${index})`]: {
  39 + 'z-index': `${10 - index}`,
  40 + opacity: '0',
  41 + animation: `enter-${d}-animation 0.4s ease-in-out 0.3s`,
  42 + 'animation-fill-mode': 'forwards',
  43 + 'animation-delay': `${(index * 1) / 10}s`,
  44 + },
  45 + };
  46 + };
  47 + const handler = ({ addBase }) => {
  48 + for (let index = 1; index < maxOutput; index++) {
  49 + addBase({
  50 + ...createCss(index, 'x'),
  51 + ...createCss(index, 'y'),
  52 + });
  53 + }
  54 +
  55 + addBase({
  56 + [`@keyframes enter-x-animation`]: {
  57 + to: {
  58 + opacity: '1',
  59 + transform: 'translateX(0)',
  60 + },
  61 + },
  62 + [`@keyframes enter-y-animation`]: {
  63 + to: {
  64 + opacity: '1',
  65 + transform: 'translateY(0)',
  66 + },
  67 + },
  68 + });
  69 + };
  70 + return { handler };
  71 +}
... ...
yarn.lock
... ... @@ -1936,14 +1936,16 @@
1936 1936 dependencies:
1937 1937 vue-demi latest
1938 1938  
1939   -"@windicss/plugin-utils@0.3.12":
1940   - version "0.3.12"
1941   - resolved "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-0.3.12.tgz#69b55be1ffb45753e6f01aa236f5ecd8df7a92ee"
1942   - integrity sha512-XA+xeyu5KM322dIp+EEHeXnAPuK+KxuWyoGvJnxXi9U50nIp0QraqXAH7xl9ghIkVHvVrb8pmm8vHpzFvsqF2A==
  1939 +"@windicss/plugin-utils@0.4.3":
  1940 + version "0.4.3"
  1941 + resolved "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-0.4.3.tgz#84e85fd3cd6eaf54ca72cae276f9cf0610f45e56"
  1942 + integrity sha512-ilddLED+sZQIA9rOwE5eYwdBEBWKREvAVkkiAOOTNf7oDcP/a1cxT3f/nE4tgfhz+MC/FKcy7NkfrqfXRdEQaQ==
1943 1943 dependencies:
  1944 + esbuild "^0.8.49"
  1945 + esbuild-register "^2.0.0"
1944 1946 fast-glob "^3.2.5"
1945 1947 micromatch "^4.0.2"
1946   - windicss "^2.1.12"
  1948 + windicss "^2.1.15"
1947 1949  
1948 1950 "@zxcvbn-ts/core@^0.2.0":
1949 1951 version "0.2.0"
... ... @@ -3870,6 +3872,11 @@ esbuild@^0.8.48:
3870 3872 resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.48.tgz#a57e4dde84ec56da1c6ecaefee97e9da6c5b00b5"
3871 3873 integrity sha512-lrH8lA8wWQ6Lpe1z6C7ZZaFSmRsUlcQAqe16nf7ITySQ7MV4+vI7qAqQlT/u+c3+9AL3VXmT4MXTxV2e63pO4A==
3872 3874  
  3875 +esbuild@^0.8.49:
  3876 + version "0.8.49"
  3877 + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.49.tgz#3d33f71b3966611f822cf4c838115f3fbd16def2"
  3878 + integrity sha512-itiFVYv5UZz4NooO7/Y0bRGVDGz/M/cxKbl6zyNI5pnKaz1mZjvZXAFhhDVz6rGCmcdTKj5oag6rh8DaaSSmfQ==
  3879 +
3873 3880 escalade@^3.1.1:
3874 3881 version "3.1.1"
3875 3882 resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
... ... @@ -8454,10 +8461,10 @@ stylelint-order@^4.1.0:
8454 8461 postcss "^7.0.31"
8455 8462 postcss-sorting "^5.0.1"
8456 8463  
8457   -stylelint@^13.10.0:
8458   - version "13.10.0"
8459   - resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.10.0.tgz#67b0c6f378c3fa61aa569a55d38feb8570b0b587"
8460   - integrity sha512-eDuLrL0wzPKbl5/TbNGZcbw0lTIGbDEr5W6lCODvb1gAg0ncbgCRt7oU0C2VFDvbrcY0A3MFZOwltwTRmc0XCw==
  8464 +stylelint@^13.11.0:
  8465 + version "13.11.0"
  8466 + resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.11.0.tgz#591981fbdd68c9d3d3e6147a0cd6a07539fc216d"
  8467 + integrity sha512-DhrKSWDWGZkCiQMtU+VroXM6LWJVC8hSK24nrUngTSQvXGK75yZUq4yNpynqrxD3a/fzKMED09V+XxO4z4lTbw==
8461 8468 dependencies:
8462 8469 "@stylelint/postcss-css-in-js" "^0.37.2"
8463 8470 "@stylelint/postcss-markdown" "^0.36.2"
... ... @@ -9262,17 +9269,17 @@ vite-plugin-purge-icons@^0.7.0:
9262 9269 "@purge-icons/generated" "^0.7.0"
9263 9270 rollup-plugin-purge-icons "^0.7.0"
9264 9271  
9265   -vite-plugin-pwa@^0.5.1:
9266   - version "0.5.1"
9267   - resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.5.1.tgz#7f94b8c4092ba0bba0a1bceb690e7420b18071e7"
9268   - integrity sha512-hf8BgyH0XLNEJUoMsk7ywMoE+OoQelK/+4RQoftQomZhlKXgsTWrfshFGOV7sKUbLsxMh0cVoh1DmAulQmRzKQ==
  9272 +vite-plugin-pwa@^0.5.2:
  9273 + version "0.5.2"
  9274 + resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.5.2.tgz#48131ebadc0c98c34a543dbf1bb1c86aeef532e0"
  9275 + integrity sha512-4SHKxYhd5sCF/ebbgxGYlN91UHlylzh7C32a5+Y2c2vbrWzw5x62ZxsYzolQzBosdOim4Ez+e/dX4hmP3BCmow==
9269 9276 dependencies:
9270 9277 debug "^4.3.2"
9271 9278 fast-glob "^3.2.5"
9272 9279 pretty-bytes "^5.5.0"
9273 9280 workbox-build "^6.1.0"
9274 9281  
9275   -vite-plugin-style-import@^0.7.2:
  9282 +vite-plugin-style-import@^0.7.3:
9276 9283 version "0.7.3"
9277 9284 resolved "https://registry.npmjs.org/vite-plugin-style-import/-/vite-plugin-style-import-0.7.3.tgz#4a9fb08bf5f2fc4796391c9be9a587ecb5c97e9e"
9278 9285 integrity sha512-oKM6vOl7iWaB5U1HcR5oM1oPRhT1n5yJt7h4h9jwpMPCD5ckHPcSjjSU7ZlOJkoS/IWEnDkQqoZi162bOs1rTQ==
... ... @@ -9295,13 +9302,13 @@ vite-plugin-theme@^0.4.3:
9295 9302 es-module-lexer "^0.3.26"
9296 9303 tinycolor2 "^1.4.2"
9297 9304  
9298   -vite-plugin-windicss@0.3.12:
9299   - version "0.3.12"
9300   - resolved "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-0.3.12.tgz#5503b4ee738268a37c857c0cf55cea41f28fa3e6"
9301   - integrity sha512-NuzIjSrqBQKvpbLJoU9qi8PIWBBXCqBmuLg9Dl/cFl4MB/vAHIOB6sZYJatCBFTU39Kw4UU0GhAjDBSNqzTn0w==
  9305 +vite-plugin-windicss@0.4.3:
  9306 + version "0.4.3"
  9307 + resolved "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-0.4.3.tgz#f86e5a3b78882caa3cdd50cba2b08770e2d627c8"
  9308 + integrity sha512-Lnv6OhcYzcJvecTs4LIpMSfo54rSewkHrW85IVwy8hacR0krY319jXr5nwiDpSTp6HM3QJhoJ4zxHF+t5Q+Nwg==
9302 9309 dependencies:
9303   - "@windicss/plugin-utils" "0.3.12"
9304   - windicss "^2.1.12"
  9310 + "@windicss/plugin-utils" "0.4.3"
  9311 + windicss "^2.1.15"
9305 9312  
9306 9313 vite@2.0.1:
9307 9314 version "2.0.1"
... ... @@ -9356,10 +9363,10 @@ vue-i18n@9.0.0-rc.2:
9356 9363 "@intlify/shared" "9.0.0-rc.2"
9357 9364 "@vue/devtools-api" "^6.0.0-beta.3"
9358 9365  
9359   -vue-router@^4.0.3:
9360   - version "4.0.3"
9361   - resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz#8b26050c88b2dec7e27a88835f71046b365823ec"
9362   - integrity sha512-AD1OjtVPyQHTSpoRsEGfPpxRQwhAhxcacOYO3zJ3KNkYP/r09mileSp6kdMQKhZWP2cFsPR3E2M3PZguSN5/ww==
  9366 +vue-router@^4.0.4:
  9367 + version "4.0.4"
  9368 + resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.4.tgz#ad9b4b7bbdad622407b4ff189b1646f48c1e9053"
  9369 + integrity sha512-uN6PDEaYdU9aRO7mU+Dkr1uaY49hV3fucEDG/Vre/Qj8ct3RoJS16vcPrvKVzn69zDDjBV5b9Xw7fZA9r6b/Iw==
9363 9370  
9364 9371 vue-types@^3.0.0:
9365 9372 version "3.0.1"
... ... @@ -9434,10 +9441,10 @@ which@^2.0.1:
9434 9441 dependencies:
9435 9442 isexe "^2.0.0"
9436 9443  
9437   -windicss@^2.1.12:
9438   - version "2.1.12"
9439   - resolved "https://registry.npmjs.org/windicss/-/windicss-2.1.12.tgz#840b963f03af7a3e31b989d2b51de52dcd57a37a"
9440   - integrity sha512-VC057iG65zlvdqUI+1ynzOuKikalvYg6XqPGbG17HEAfwQ0sg1dACTk2plEp1QAEQNtKU3BnLnueWa4oKlltEQ==
  9444 +windicss@^2.1.15:
  9445 + version "2.1.15"
  9446 + resolved "https://registry.npmjs.org/windicss/-/windicss-2.1.15.tgz#0a5bf1a56711ab53de8093a3c855764d93ffac00"
  9447 + integrity sha512-gBihXNJPzv/kBaelOlXvbrmWsWuv98OPSf/yUYjc8EnRGCOxDOQIRin4FYPTWCmZi91PZThh7nMjzQZiBV+MYg==
9441 9448  
9442 9449 wmf@~1.0.1:
9443 9450 version "1.0.2"
... ...