Commit ec9478f76fadbcd936d9d298b2623cebb1525ca5

Authored by vben
1 parent e3851dc5

refactor: refactor login page

.vscode/extensions.json
1 { 1 {
2 "recommendations": [ 2 "recommendations": [
3 - "johnsoncodehk.volar",  
4 "octref.vetur", 3 "octref.vetur",
5 "dbaeumer.vscode-eslint", 4 "dbaeumer.vscode-eslint",
6 "stylelint.vscode-stylelint", 5 "stylelint.vscode-stylelint",
7 - "DavidAnson.vscode-markdownlint",  
8 "esbenp.prettier-vscode", 6 "esbenp.prettier-vscode",
9 "mrmlnc.vscode-less", 7 "mrmlnc.vscode-less",
10 "antfu.i18n-ally", 8 "antfu.i18n-ally",
11 - "cpylua.language-postcss",  
12 - "Orta.vscode-jest",  
13 "antfu.iconify", 9 "antfu.iconify",
14 "mikestead.dotenv", 10 "mikestead.dotenv",
15 "bradlc.vscode-tailwindcss", 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 ## Wip 1 ## Wip
2 2
  3 +### ✨ Refactor
  4 +
  5 +- 登录页重构,新增注册页面/重置密码页面/手机登录/二维码登录
  6 +
3 ### ✨ Features 7 ### ✨ Features
4 8
5 - 新增 `settingButtonPosition`配置项,用于配置`设置`按钮位置 9 - 新增 `settingButtonPosition`配置项,用于配置`设置`按钮位置
6 - `modal`可以通过双击头部切换全屏 10 - `modal`可以通过双击头部切换全屏
  11 +- 新增`CountDownInput`组件
7 12
8 ### ⚡ Performance Improvements 13 ### ⚡ Performance Improvements
9 14
build/config/themeConfig.ts
@@ -94,7 +94,7 @@ export function generateModifyVars() { @@ -94,7 +94,7 @@ export function generateModifyVars() {
94 'disabled-color': 'rgba(0, 0, 0, 0.25)', // Failure color 94 'disabled-color': 'rgba(0, 0, 0, 0.25)', // Failure color
95 'heading-color': 'rgba(0, 0, 0, 0.85)', // Title color 95 'heading-color': 'rgba(0, 0, 0, 0.85)', // Title color
96 'text-color': 'rgba(0, 0, 0, 0.85)', // Main text color 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 'font-size-base': '14px', // Main font size 98 'font-size-base': '14px', // Main font size
99 'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', // Floating shadow 99 'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', // Floating shadow
100 'border-color-base': '#d9d9d9', // Border color, 100 'border-color-base': '#d9d9d9', // Border color,
package.json
@@ -43,7 +43,7 @@ @@ -43,7 +43,7 @@
43 "vditor": "^3.8.1", 43 "vditor": "^3.8.1",
44 "vue": "^3.0.5", 44 "vue": "^3.0.5",
45 "vue-i18n": "9.0.0-rc.2", 45 "vue-i18n": "9.0.0-rc.2",
46 - "vue-router": "^4.0.3", 46 + "vue-router": "^4.0.4",
47 "vue-types": "^3.0.2", 47 "vue-types": "^3.0.2",
48 "vuex": "^4.0.0", 48 "vuex": "^4.0.0",
49 "vuex-module-decorators": "^1.0.1", 49 "vuex-module-decorators": "^1.0.1",
@@ -92,7 +92,7 @@ @@ -92,7 +92,7 @@
92 "pretty-quick": "^3.1.0", 92 "pretty-quick": "^3.1.0",
93 "rimraf": "^3.0.2", 93 "rimraf": "^3.0.2",
94 "rollup-plugin-visualizer": "^4.2.0", 94 "rollup-plugin-visualizer": "^4.2.0",
95 - "stylelint": "^13.10.0", 95 + "stylelint": "^13.11.0",
96 "stylelint-config-prettier": "^8.0.2", 96 "stylelint-config-prettier": "^8.0.2",
97 "stylelint-config-standard": "^20.0.0", 97 "stylelint-config-standard": "^20.0.0",
98 "stylelint-order": "^4.1.0", 98 "stylelint-order": "^4.1.0",
@@ -104,10 +104,10 @@ @@ -104,10 +104,10 @@
104 "vite-plugin-imagemin": "^0.2.6", 104 "vite-plugin-imagemin": "^0.2.6",
105 "vite-plugin-mock": "^2.1.4", 105 "vite-plugin-mock": "^2.1.4",
106 "vite-plugin-purge-icons": "^0.7.0", 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 "vite-plugin-theme": "^0.4.3", 109 "vite-plugin-theme": "^0.4.3",
110 - "vite-plugin-windicss": "0.3.12", 110 + "vite-plugin-windicss": "0.4.3",
111 "vue-eslint-parser": "^7.5.0", 111 "vue-eslint-parser": "^7.5.0",
112 "yargs": "^16.2.0" 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 \ No newline at end of file 2 \ No newline at end of file
src/components/Application/src/AppLogo.vue
@@ -10,8 +10,13 @@ @@ -10,8 +10,13 @@
10 > 10 >
11 <img src="../../../assets/images/logo.png" /> 11 <img src="../../../assets/images/logo.png" />
12 <div 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 v-show="showTitle" 20 v-show="showTitle"
16 > 21 >
17 {{ title }} 22 {{ title }}
@@ -38,6 +43,7 @@ @@ -38,6 +43,7 @@
38 theme: propTypes.oneOf(['light', 'dark']), 43 theme: propTypes.oneOf(['light', 'dark']),
39 // Whether to show title 44 // Whether to show title
40 showTitle: propTypes.bool.def(true), 45 showTitle: propTypes.bool.def(true),
  46 + alwaysShowTitle: propTypes.bool.def(false),
41 }, 47 },
42 setup() { 48 setup() {
43 const { prefixCls } = useDesign('app-logo'); 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 <template> 1 <template>
2 - <div :class="prefixCls"> 2 + <div :class="prefixCls" class="relative">
3 <InputPassword 3 <InputPassword
4 v-if="showInput" 4 v-if="showInput"
5 v-bind="$attrs" 5 v-bind="$attrs"
@@ -24,15 +24,14 @@ @@ -24,15 +24,14 @@
24 import { Input } from 'ant-design-vue'; 24 import { Input } from 'ant-design-vue';
25 25
26 import zxcvbn from '@zxcvbn-ts/core'; 26 import zxcvbn from '@zxcvbn-ts/core';
27 - import { propTypes } from '/@/utils/propTypes';  
28 import { useDesign } from '/@/hooks/web/useDesign'; 27 import { useDesign } from '/@/hooks/web/useDesign';
  28 + import { propTypes } from '/@/utils/propTypes';
29 29
30 export default defineComponent({ 30 export default defineComponent({
31 name: 'StrengthMeter', 31 name: 'StrengthMeter',
32 components: { InputPassword: Input.Password }, 32 components: { InputPassword: Input.Password },
33 props: { 33 props: {
34 value: propTypes.string, 34 value: propTypes.string,
35 -  
36 showInput: propTypes.bool.def(true), 35 showInput: propTypes.bool.def(true),
37 disabled: propTypes.bool, 36 disabled: propTypes.bool,
38 }, 37 },
@@ -43,9 +42,9 @@ @@ -43,9 +42,9 @@
43 42
44 const getPasswordStrength = computed(() => { 43 const getPasswordStrength = computed(() => {
45 const { disabled } = props; 44 const { disabled } = props;
46 - if (disabled) return null; 45 + if (disabled) return -1;
47 const innerValue = unref(innerValueRef); 46 const innerValue = unref(innerValueRef);
48 - const score = innerValue ? zxcvbn(unref(innerValueRef)).score : null; 47 + const score = innerValue ? zxcvbn(unref(innerValueRef)).score : -1;
49 emit('score-change', score); 48 emit('score-change', score);
50 return score; 49 return score;
51 }); 50 });
@@ -57,6 +56,7 @@ @@ -57,6 +56,7 @@
57 watchEffect(() => { 56 watchEffect(() => {
58 innerValueRef.value = props.value || ''; 57 innerValueRef.value = props.value || '';
59 }); 58 });
  59 +
60 watch( 60 watch(
61 () => unref(innerValueRef), 61 () => unref(innerValueRef),
62 (val) => { 62 (val) => {
@@ -77,14 +77,12 @@ @@ -77,14 +77,12 @@
77 @prefix-cls: ~'@{namespace}-strength-meter'; 77 @prefix-cls: ~'@{namespace}-strength-meter';
78 78
79 .@{prefix-cls} { 79 .@{prefix-cls} {
80 - position: relative;  
81 -  
82 &-bar { 80 &-bar {
83 position: relative; 81 position: relative;
84 - height: 4px; 82 + height: 6px;
85 margin: 10px auto 6px; 83 margin: 10px auto 6px;
86 background: @disabled-color; 84 background: @disabled-color;
87 - border-radius: 3px; 85 + border-radius: 6px;
88 86
89 &::before, 87 &::before,
90 &::after { 88 &::after {
src/design/ant/index.less
@@ -13,10 +13,8 @@ @@ -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 .ant-back-top { 20 .ant-back-top {
src/design/var/breakpoint.less
@@ -2,37 +2,17 @@ @@ -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 // Small screen / tablet 5 // Small screen / tablet
10 -@screen-sm: 576px;  
11 -@screen-sm-min: @screen-sm; 6 +@screen-sm: 640px;
12 7
13 // Medium screen / desktop 8 // Medium screen / desktop
14 @screen-md: 768px; 9 @screen-md: 768px;
15 -@screen-md-min: @screen-md;  
16 10
17 // Large screen / wide desktop 11 // Large screen / wide desktop
18 -@screen-lg: 992px;  
19 -@screen-lg-min: @screen-lg; 12 +@screen-lg: 1024px;
20 13
21 // Extra large screen / full hd 14 // Extra large screen / full hd
22 -@screen-xl: 1200px;  
23 -@screen-xl-min: @screen-xl; 15 +@screen-xl: 1280px;
24 16
25 // Extra extra large screen / large desktop 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,7 +3,7 @@
3 <span :class="[prefixCls, `${prefixCls}--${theme}`]"> 3 <span :class="[prefixCls, `${prefixCls}--${theme}`]">
4 <img :class="`${prefixCls}__header`" :src="headerImg" /> 4 <img :class="`${prefixCls}__header`" :src="headerImg" />
5 <span :class="`${prefixCls}__info`"> 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 </span> 7 </span>
8 </span> 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 export default { 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 // notify 20 // notify
7 loginSuccessTitle: 'Login successful', 21 loginSuccessTitle: 'Login successful',
8 loginSuccessDesc: 'Welcome back', 22 loginSuccessDesc: 'Welcome back',
9 23
10 // placeholder 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 export default { 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 loginButton: '登录', 14 loginButton: '登录',
3 - autoLogin: '自动登录',  
4 - forgetPassword: '忘记密码', 15 + registerButton: '注册',
  16 + rememberMe: '记住我',
  17 + forgetPassword: '忘记密码?',
  18 + otherSignIn: '其他登录方式',
5 19
6 // notify 20 // notify
7 loginSuccessTitle: '登录成功', 21 loginSuccessTitle: '登录成功',
@@ -10,4 +24,15 @@ export default { @@ -10,4 +24,15 @@ export default {
10 // placeholder 24 // placeholder
11 accountPlaceholder: '请输入账号', 25 accountPlaceholder: '请输入账号',
12 passwordPlaceholder: '请输入密码', 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 <template> 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 <LockOutlined /> 12 <LockOutlined />
5 <span>{{ t('sys.lock.unlock') }}</span> 13 <span>{{ t('sys.lock.unlock') }}</span>
6 </div> 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 </div> 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 </div> 25 </div>
16 </div> 26 </div>
17 <transition name="fade-slide"> 27 <transition name="fade-slide">
18 <div :class="`${prefixCls}-entry`" v-show="!showDate"> 28 <div :class="`${prefixCls}-entry`" v-show="!showDate">
19 <div :class="`${prefixCls}-entry-content`"> 29 <div :class="`${prefixCls}-entry-content`">
20 - <div :class="`${prefixCls}-entry__header`"> 30 + <div :class="`${prefixCls}-entry__header enter-x`">
21 <img :src="headerImg" :class="`${prefixCls}-entry__header-img`" /> 31 <img :src="headerImg" :class="`${prefixCls}-entry__header-img`" />
22 <p :class="`${prefixCls}-entry__header-name`"> 32 <p :class="`${prefixCls}-entry__header-name`">
23 {{ realName }} 33 {{ realName }}
24 </p> 34 </p>
25 </div> 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 {{ t('sys.lock.alert') }} 42 {{ t('sys.lock.alert') }}
29 </span> 43 </span>
30 - <div :class="`${prefixCls}-entry__footer`"> 44 + <div :class="`${prefixCls}-entry__footer enter-x`">
31 <a-button 45 <a-button
32 type="link" 46 type="link"
33 size="small" 47 size="small"
34 - class="mt-2 mr-2" 48 + class="mt-2 mr-2 enter-x"
35 :disabled="loadingRef" 49 :disabled="loadingRef"
36 @click="handleShowForm(true)" 50 @click="handleShowForm(true)"
37 > 51 >
@@ -40,7 +54,7 @@ @@ -40,7 +54,7 @@
40 <a-button 54 <a-button
41 type="link" 55 type="link"
42 size="small" 56 size="small"
43 - class="mt-2 mr-2" 57 + class="mt-2 mr-2 enter-x"
44 :disabled="loadingRef" 58 :disabled="loadingRef"
45 @click="goLogin" 59 @click="goLogin"
46 > 60 >
@@ -54,11 +68,11 @@ @@ -54,11 +68,11 @@
54 </div> 68 </div>
55 </transition> 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 </div> 74 </div>
61 - <div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div> 75 + <div class="text-2xl"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
62 </div> 76 </div>
63 </div> 77 </div>
64 </template> 78 </template>
@@ -144,125 +158,54 @@ @@ -144,125 +158,54 @@
144 @prefix-cls: ~'@{namespace}-lock-page'; 158 @prefix-cls: ~'@{namespace}-lock-page';
145 159
146 .@{prefix-cls} { 160 .@{prefix-cls} {
147 - position: fixed;  
148 - top: 0;  
149 - right: 0;  
150 - bottom: 0;  
151 - left: 0;  
152 z-index: @lock-page-z-index; 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 &__unlock { 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 transform: translate(-50%, 0); 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 &__hour, 167 &__hour,
202 &__minute { 168 &__minute {
203 display: flex; 169 display: flex;
204 - width: 40%;  
205 - height: 74%;  
206 font-weight: 700; 170 font-weight: 700;
207 color: #bababa; 171 color: #bababa;
208 background: #141313; 172 background: #141313;
209 border-radius: 30px; 173 border-radius: 30px;
210 justify-content: center; 174 justify-content: center;
211 align-items: center; 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 <template> 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 </div> 43 </div>
58 </div> 44 </div>
59 </div> 45 </div>
60 </div> 46 </div>
61 </template> 47 </template>
62 <script lang="ts"> 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 import { AppLocalePicker } from '/@/components/Application'; 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 import { useGlobSetting, useProjectSetting } from '/@/hooks/setting'; 59 import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
73 - import logo from '/@/assets/images/logo.png';  
74 import { useI18n } from '/@/hooks/web/useI18n'; 60 import { useI18n } from '/@/hooks/web/useI18n';
  61 + import { useDesign } from '/@/hooks/web/useDesign';
  62 + import { useShowLoginForm, useFormTitle } from './useLogin';
75 63
76 export default defineComponent({ 64 export default defineComponent({
  65 + name: 'Login',
77 components: { 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 AppLocalePicker, 73 AppLocalePicker,
85 - [Row.name]: Row,  
86 - [Col.name]: Col,  
87 }, 74 },
88 setup() { 75 setup() {
89 - const formRef = ref<any>(null);  
90 - const autoLoginRef = ref(false);  
91 -  
92 const globSetting = useGlobSetting(); 76 const globSetting = useGlobSetting();
  77 + const { getFormTitle } = useFormTitle();
  78 + const { prefixCls } = useDesign('login');
93 const { locale } = useProjectSetting(); 79 const { locale } = useProjectSetting();
94 - const { notification } = useMessage();  
95 const { t } = useI18n(); 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 return { 82 return {
138 - formRef,  
139 - formData,  
140 - formState,  
141 - formRules,  
142 - login: handleLogin,  
143 - autoLogin: autoLoginRef,  
144 - title: globSetting && globSetting.title,  
145 - logo,  
146 t, 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 </script> 92 </script>
152 <style lang="less"> 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 height: 100%; 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 </style> 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,14 +1936,16 @@
1936 dependencies: 1936 dependencies:
1937 vue-demi latest 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 dependencies: 1943 dependencies:
  1944 + esbuild "^0.8.49"
  1945 + esbuild-register "^2.0.0"
1944 fast-glob "^3.2.5" 1946 fast-glob "^3.2.5"
1945 micromatch "^4.0.2" 1947 micromatch "^4.0.2"
1946 - windicss "^2.1.12" 1948 + windicss "^2.1.15"
1947 1949
1948 "@zxcvbn-ts/core@^0.2.0": 1950 "@zxcvbn-ts/core@^0.2.0":
1949 version "0.2.0" 1951 version "0.2.0"
@@ -3870,6 +3872,11 @@ esbuild@^0.8.48: @@ -3870,6 +3872,11 @@ esbuild@^0.8.48:
3870 resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.48.tgz#a57e4dde84ec56da1c6ecaefee97e9da6c5b00b5" 3872 resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.48.tgz#a57e4dde84ec56da1c6ecaefee97e9da6c5b00b5"
3871 integrity sha512-lrH8lA8wWQ6Lpe1z6C7ZZaFSmRsUlcQAqe16nf7ITySQ7MV4+vI7qAqQlT/u+c3+9AL3VXmT4MXTxV2e63pO4A== 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 escalade@^3.1.1: 3880 escalade@^3.1.1:
3874 version "3.1.1" 3881 version "3.1.1"
3875 resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 3882 resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -8454,10 +8461,10 @@ stylelint-order@^4.1.0: @@ -8454,10 +8461,10 @@ stylelint-order@^4.1.0:
8454 postcss "^7.0.31" 8461 postcss "^7.0.31"
8455 postcss-sorting "^5.0.1" 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 dependencies: 8468 dependencies:
8462 "@stylelint/postcss-css-in-js" "^0.37.2" 8469 "@stylelint/postcss-css-in-js" "^0.37.2"
8463 "@stylelint/postcss-markdown" "^0.36.2" 8470 "@stylelint/postcss-markdown" "^0.36.2"
@@ -9262,17 +9269,17 @@ vite-plugin-purge-icons@^0.7.0: @@ -9262,17 +9269,17 @@ vite-plugin-purge-icons@^0.7.0:
9262 "@purge-icons/generated" "^0.7.0" 9269 "@purge-icons/generated" "^0.7.0"
9263 rollup-plugin-purge-icons "^0.7.0" 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 dependencies: 9276 dependencies:
9270 debug "^4.3.2" 9277 debug "^4.3.2"
9271 fast-glob "^3.2.5" 9278 fast-glob "^3.2.5"
9272 pretty-bytes "^5.5.0" 9279 pretty-bytes "^5.5.0"
9273 workbox-build "^6.1.0" 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 version "0.7.3" 9283 version "0.7.3"
9277 resolved "https://registry.npmjs.org/vite-plugin-style-import/-/vite-plugin-style-import-0.7.3.tgz#4a9fb08bf5f2fc4796391c9be9a587ecb5c97e9e" 9284 resolved "https://registry.npmjs.org/vite-plugin-style-import/-/vite-plugin-style-import-0.7.3.tgz#4a9fb08bf5f2fc4796391c9be9a587ecb5c97e9e"
9278 integrity sha512-oKM6vOl7iWaB5U1HcR5oM1oPRhT1n5yJt7h4h9jwpMPCD5ckHPcSjjSU7ZlOJkoS/IWEnDkQqoZi162bOs1rTQ== 9285 integrity sha512-oKM6vOl7iWaB5U1HcR5oM1oPRhT1n5yJt7h4h9jwpMPCD5ckHPcSjjSU7ZlOJkoS/IWEnDkQqoZi162bOs1rTQ==
@@ -9295,13 +9302,13 @@ vite-plugin-theme@^0.4.3: @@ -9295,13 +9302,13 @@ vite-plugin-theme@^0.4.3:
9295 es-module-lexer "^0.3.26" 9302 es-module-lexer "^0.3.26"
9296 tinycolor2 "^1.4.2" 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 dependencies: 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 vite@2.0.1: 9313 vite@2.0.1:
9307 version "2.0.1" 9314 version "2.0.1"
@@ -9356,10 +9363,10 @@ vue-i18n@9.0.0-rc.2: @@ -9356,10 +9363,10 @@ vue-i18n@9.0.0-rc.2:
9356 "@intlify/shared" "9.0.0-rc.2" 9363 "@intlify/shared" "9.0.0-rc.2"
9357 "@vue/devtools-api" "^6.0.0-beta.3" 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 vue-types@^3.0.0: 9371 vue-types@^3.0.0:
9365 version "3.0.1" 9372 version "3.0.1"
@@ -9434,10 +9441,10 @@ which@^2.0.1: @@ -9434,10 +9441,10 @@ which@^2.0.1:
9434 dependencies: 9441 dependencies:
9435 isexe "^2.0.0" 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 wmf@~1.0.1: 9449 wmf@~1.0.1:
9443 version "1.0.2" 9450 version "1.0.2"