mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 00:26:20 +08:00
refactor: refactor login page
This commit is contained in:
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -1,19 +1,14 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"johnsoncodehk.volar",
|
||||
"octref.vetur",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
"esbenp.prettier-vscode",
|
||||
"mrmlnc.vscode-less",
|
||||
"antfu.i18n-ally",
|
||||
"cpylua.language-postcss",
|
||||
"Orta.vscode-jest",
|
||||
"antfu.iconify",
|
||||
"mikestead.dotenv",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"heybourn.headwind",
|
||||
"znck.vue-language-features"
|
||||
"heybourn.headwind"
|
||||
]
|
||||
}
|
||||
|
5
.vscode/i18n-ally-reviews.yml
vendored
Normal file
5
.vscode/i18n-ally-reviews.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Review comments generated by i18n-ally. Please commit this file.
|
||||
|
||||
reviews:
|
||||
sys.login.autoLogin:
|
||||
description: '1'
|
@@ -1,9 +1,14 @@
|
||||
## Wip
|
||||
|
||||
### ✨ Refactor
|
||||
|
||||
- 登录页重构,新增注册页面/重置密码页面/手机登录/二维码登录
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- 新增 `settingButtonPosition`配置项,用于配置`设置`按钮位置
|
||||
- `modal`可以通过双击头部切换全屏
|
||||
- 新增`CountDownInput`组件
|
||||
|
||||
### ⚡ Performance Improvements
|
||||
|
||||
|
@@ -94,7 +94,7 @@ export function generateModifyVars() {
|
||||
'disabled-color': 'rgba(0, 0, 0, 0.25)', // Failure color
|
||||
'heading-color': 'rgba(0, 0, 0, 0.85)', // Title color
|
||||
'text-color': 'rgba(0, 0, 0, 0.85)', // Main text color
|
||||
'text-color-secondary ': 'rgba(0, 0, 0, 0.45)', // Subtext color
|
||||
'text-color-secondary': 'rgba(0, 0, 0, 0.45)', // Subtext color
|
||||
'font-size-base': '14px', // Main font size
|
||||
'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', // Floating shadow
|
||||
'border-color-base': '#d9d9d9', // Border color,
|
||||
|
10
package.json
10
package.json
@@ -43,7 +43,7 @@
|
||||
"vditor": "^3.8.1",
|
||||
"vue": "^3.0.5",
|
||||
"vue-i18n": "9.0.0-rc.2",
|
||||
"vue-router": "^4.0.3",
|
||||
"vue-router": "^4.0.4",
|
||||
"vue-types": "^3.0.2",
|
||||
"vuex": "^4.0.0",
|
||||
"vuex-module-decorators": "^1.0.1",
|
||||
@@ -92,7 +92,7 @@
|
||||
"pretty-quick": "^3.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup-plugin-visualizer": "^4.2.0",
|
||||
"stylelint": "^13.10.0",
|
||||
"stylelint": "^13.11.0",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-order": "^4.1.0",
|
||||
@@ -104,10 +104,10 @@
|
||||
"vite-plugin-imagemin": "^0.2.6",
|
||||
"vite-plugin-mock": "^2.1.4",
|
||||
"vite-plugin-purge-icons": "^0.7.0",
|
||||
"vite-plugin-pwa": "^0.5.1",
|
||||
"vite-plugin-style-import": "^0.7.2",
|
||||
"vite-plugin-pwa": "^0.5.2",
|
||||
"vite-plugin-style-import": "^0.7.3",
|
||||
"vite-plugin-theme": "^0.4.3",
|
||||
"vite-plugin-windicss": "0.3.12",
|
||||
"vite-plugin-windicss": "0.4.3",
|
||||
"vue-eslint-parser": "^7.5.0",
|
||||
"yargs": "^16.2.0"
|
||||
},
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 180 KiB |
Binary file not shown.
Before Width: | Height: | Size: 92 KiB |
17
src/assets/svg/login-bg.svg
Normal file
17
src/assets/svg/login-bg.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="6395" height="1079" viewBox="0 0 6395 1079">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="Rectangle_73" data-name="Rectangle 73" width="6395" height="1079" transform="translate(-5391)" fill="#fff"/>
|
||||
</clipPath>
|
||||
<linearGradient id="linear-gradient" x1="0.747" y1="0.222" x2="0.973" y2="0.807" gradientUnits="objectBoundingBox">
|
||||
<stop offset="0" stop-color="#2b51b4"/>
|
||||
<stop offset="1" stop-color="#1c3faa"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Mask_Group_1" data-name="Mask Group 1" transform="translate(5391)" clip-path="url(#clip-path)">
|
||||
<g id="Group_118" data-name="Group 118" transform="translate(-419.333 -1.126)">
|
||||
<path id="Path_142" data-name="Path 142" d="M6271.734-6.176s-222.478,187.809-55.349,583.254c44.957,106.375,81.514,205.964,84.521,277,8.164,192.764-156.046,268.564-156.046,268.564l-653.53-26.8L5475.065-21.625Z" transform="translate(-4876.383 0)" fill="#f1f5f8"/>
|
||||
<path id="Union_6" data-name="Union 6" d="M-2631.1,1081.8v-1.6H-8230.9V.022H-2631.1V0H-1871.4s-187.845,197.448-91.626,488.844c49.167,148.9,96.309,256.289,104.683,362.118,7.979,100.852-57.98,201.711-168.644,254.286-65.858,31.29-144.552,42.382-223.028,42.383C-2441.2,1147.632-2631.1,1081.8-2631.1,1081.8Z" transform="translate(3259.524 0.803)" fill="url(#linear-gradient)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/svg/login-box-bg.svg
Normal file
1
src/assets/svg/login-box-bg.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.7 KiB |
@@ -10,8 +10,13 @@
|
||||
>
|
||||
<img src="../../../assets/images/logo.png" />
|
||||
<div
|
||||
class="ml-2 truncate xs:opacity-0 md:opacity-100"
|
||||
:class="`${prefixCls}__title`"
|
||||
class="ml-2 truncate md:opacity-100"
|
||||
:class="[
|
||||
`${prefixCls}__title`,
|
||||
{
|
||||
'xs:opacity-0': !alwaysShowTitle,
|
||||
},
|
||||
]"
|
||||
v-show="showTitle"
|
||||
>
|
||||
{{ title }}
|
||||
@@ -38,6 +43,7 @@
|
||||
theme: propTypes.oneOf(['light', 'dark']),
|
||||
// Whether to show title
|
||||
showTitle: propTypes.bool.def(true),
|
||||
alwaysShowTitle: propTypes.bool.def(false),
|
||||
},
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('app-logo');
|
||||
|
4
src/components/CountDown/index.ts
Normal file
4
src/components/CountDown/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import CountButton from './src/CountButton.vue';
|
||||
import CountdownInput from './src/CountdownInput.vue';
|
||||
|
||||
export { CountdownInput, CountButton };
|
57
src/components/CountDown/src/CountButton.vue
Normal file
57
src/components/CountDown/src/CountButton.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<Button v-bind="$attrs" :disabled="isStart" @click="handleStart" :loading="loading">
|
||||
{{
|
||||
!isStart
|
||||
? t('component.countdown.normalText')
|
||||
: t('component.countdown.sendText', [currentCount])
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, PropType } from 'vue';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useCountdown } from './useCountdown';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CountButton',
|
||||
components: { Button },
|
||||
props: {
|
||||
count: {
|
||||
type: Number,
|
||||
default: 60,
|
||||
},
|
||||
beforeStartFunc: {
|
||||
type: Function as PropType<() => boolean>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const loading = ref(false);
|
||||
|
||||
const { currentCount, isStart, start } = useCountdown(props.count);
|
||||
const { t } = useI18n();
|
||||
/**
|
||||
* @description: Judge whether there is an external function before execution, and decide whether to start after execution
|
||||
*/
|
||||
async function handleStart() {
|
||||
const { beforeStartFunc } = props;
|
||||
if (beforeStartFunc && isFunction(beforeStartFunc)) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const canStart = await beforeStartFunc();
|
||||
canStart && start();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
return { handleStart, isStart, currentCount, loading, t };
|
||||
},
|
||||
});
|
||||
</script>
|
55
src/components/CountDown/src/CountdownInput.vue
Normal file
55
src/components/CountDown/src/CountdownInput.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<AInput v-bind="$attrs" :size="size" v-model:value="state">
|
||||
<template #addonAfter>
|
||||
<CountButton :size="size" :count="count" :beforeStartFunc="sendCodeApi" />
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
import CountButton from './CountButton.vue';
|
||||
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CountDownInput',
|
||||
components: { [Input.name]: Input, CountButton },
|
||||
props: {
|
||||
value: propTypes.string,
|
||||
size: propTypes.oneOf(['default', 'large', 'small']),
|
||||
count: propTypes.number.def(60),
|
||||
sendCodeApi: {
|
||||
type: Function as PropType<() => boolean>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { prefixCls } = useDesign('countdown-input');
|
||||
|
||||
const [state] = useRuleFormItem(props);
|
||||
return { prefixCls, state };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-countdown-input';
|
||||
|
||||
.@{prefix-cls} {
|
||||
.ant-input-group-addon {
|
||||
padding-right: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
button {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
51
src/components/CountDown/src/useCountdown.ts
Normal file
51
src/components/CountDown/src/useCountdown.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ref, unref } from 'vue';
|
||||
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
|
||||
|
||||
export function useCountdown(count: number) {
|
||||
const currentCount = ref(count);
|
||||
|
||||
const isStart = ref(false);
|
||||
|
||||
let timerId: ReturnType<typeof setInterval> | null;
|
||||
|
||||
function clear() {
|
||||
timerId && window.clearInterval(timerId);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
isStart.value = false;
|
||||
timerId = null;
|
||||
clear();
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (unref(isStart) || !!timerId) {
|
||||
return;
|
||||
}
|
||||
isStart.value = true;
|
||||
timerId = setInterval(() => {
|
||||
if (unref(currentCount) === 1) {
|
||||
stop();
|
||||
currentCount.value = count;
|
||||
} else {
|
||||
currentCount.value -= 1;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentCount.value = count;
|
||||
stop();
|
||||
}
|
||||
|
||||
function restart() {
|
||||
reset();
|
||||
start();
|
||||
}
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
return { start, reset, restart, clear, stop, currentCount, isStart };
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<div :class="prefixCls" class="relative">
|
||||
<InputPassword
|
||||
v-if="showInput"
|
||||
v-bind="$attrs"
|
||||
@@ -24,15 +24,14 @@
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
import zxcvbn from '@zxcvbn-ts/core';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StrengthMeter',
|
||||
components: { InputPassword: Input.Password },
|
||||
props: {
|
||||
value: propTypes.string,
|
||||
|
||||
showInput: propTypes.bool.def(true),
|
||||
disabled: propTypes.bool,
|
||||
},
|
||||
@@ -43,9 +42,9 @@
|
||||
|
||||
const getPasswordStrength = computed(() => {
|
||||
const { disabled } = props;
|
||||
if (disabled) return null;
|
||||
if (disabled) return -1;
|
||||
const innerValue = unref(innerValueRef);
|
||||
const score = innerValue ? zxcvbn(unref(innerValueRef)).score : null;
|
||||
const score = innerValue ? zxcvbn(unref(innerValueRef)).score : -1;
|
||||
emit('score-change', score);
|
||||
return score;
|
||||
});
|
||||
@@ -57,6 +56,7 @@
|
||||
watchEffect(() => {
|
||||
innerValueRef.value = props.value || '';
|
||||
});
|
||||
|
||||
watch(
|
||||
() => unref(innerValueRef),
|
||||
(val) => {
|
||||
@@ -77,14 +77,12 @@
|
||||
@prefix-cls: ~'@{namespace}-strength-meter';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
|
||||
&-bar {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
height: 6px;
|
||||
margin: 10px auto 6px;
|
||||
background: @disabled-color;
|
||||
border-radius: 3px;
|
||||
border-radius: 6px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
|
@@ -13,10 +13,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
.anticon:not(.app-iconify) {
|
||||
vertical-align: 0.1em;
|
||||
}
|
||||
span.anticon:not(.app-iconify) {
|
||||
vertical-align: 0.125em;
|
||||
}
|
||||
|
||||
.ant-back-top {
|
||||
|
@@ -2,37 +2,17 @@
|
||||
// ==============屏幕断点============
|
||||
// =================================
|
||||
|
||||
// Extra small screen / phone
|
||||
@screen-xs: 480px;
|
||||
@screen-xs-min: @screen-xs;
|
||||
|
||||
// Small screen / tablet
|
||||
@screen-sm: 576px;
|
||||
@screen-sm-min: @screen-sm;
|
||||
@screen-sm: 640px;
|
||||
|
||||
// Medium screen / desktop
|
||||
@screen-md: 768px;
|
||||
@screen-md-min: @screen-md;
|
||||
|
||||
// Large screen / wide desktop
|
||||
@screen-lg: 992px;
|
||||
@screen-lg-min: @screen-lg;
|
||||
@screen-lg: 1024px;
|
||||
|
||||
// Extra large screen / full hd
|
||||
@screen-xl: 1200px;
|
||||
@screen-xl-min: @screen-xl;
|
||||
@screen-xl: 1280px;
|
||||
|
||||
// Extra extra large screen / large desktop
|
||||
@screen-xxl: 1600px;
|
||||
@screen-xxl-min: @screen-xxl;
|
||||
|
||||
@screen-xxxl: 1900px;
|
||||
@screen-xxxl-min: @screen-xxxl;
|
||||
|
||||
// provide a maximum
|
||||
@screen-xs-max: (@screen-sm-min - 1px);
|
||||
@screen-sm-max: (@screen-md-min - 1px);
|
||||
@screen-md-max: (@screen-lg-min - 1px);
|
||||
@screen-lg-max: (@screen-xl-min - 1px);
|
||||
@screen-xl-max: (@screen-xxl-min - 1px);
|
||||
@screen-xxl-max: (@screen-xxxl-min - 1px);
|
||||
@screen-2xl: 1536px;
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<span :class="[prefixCls, `${prefixCls}--${theme}`]">
|
||||
<img :class="`${prefixCls}__header`" :src="headerImg" />
|
||||
<span :class="`${prefixCls}__info`">
|
||||
<span :class="`${prefixCls}__name anticon`">{{ getUserInfo.realName }}</span>
|
||||
<span :class="`${prefixCls}__name`" class="truncate">{{ getUserInfo.realName }}</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
4
src/locales/lang/en/component/countdown.ts
Normal file
4
src/locales/lang/en/component/countdown.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
normalText: 'Get SMS code',
|
||||
sendText: 'Reacquire in {0}s',
|
||||
};
|
@@ -1,13 +1,38 @@
|
||||
export default {
|
||||
loginButton: 'Login',
|
||||
autoLogin: 'AutoLogin',
|
||||
forgetPassword: 'Forget Password',
|
||||
backSignIn: 'Back sign in',
|
||||
mobileSignInFormTitle: 'Mobile sign in',
|
||||
qrSignInFormTitle: 'Qr code sign in',
|
||||
signInFormTitle: 'Sign in',
|
||||
signUpFormTitle: 'Sign up',
|
||||
forgetFormTitle: 'Reset password',
|
||||
|
||||
signInTitle: 'Backstage management system',
|
||||
signInDesc: 'Enter your personal details and get started!',
|
||||
policy: 'I agree to the xxx Privacy Policy',
|
||||
scanSign: `scanning the code to complete the login`,
|
||||
|
||||
loginButton: 'Sign in',
|
||||
registerButton: 'Sign up',
|
||||
rememberMe: 'Remember me',
|
||||
forgetPassword: 'Forget Password?',
|
||||
otherSignIn: 'Sign in with',
|
||||
|
||||
// notify
|
||||
loginSuccessTitle: 'Login successful',
|
||||
loginSuccessDesc: 'Welcome back',
|
||||
|
||||
// placeholder
|
||||
accountPlaceholder: 'Please input Username',
|
||||
passwordPlaceholder: 'Please input Password',
|
||||
accountPlaceholder: 'Please input username',
|
||||
passwordPlaceholder: 'Please input password',
|
||||
smsPlaceholder: 'Please input sms code',
|
||||
mobilePlaceholder: 'Please input mobile',
|
||||
policyPlaceholder: 'Register after checking',
|
||||
diffPwd: 'The two passwords are inconsistent',
|
||||
|
||||
userName: 'Username',
|
||||
password: 'Password',
|
||||
confirmPassword: 'Confirm Password',
|
||||
email: 'Email',
|
||||
smsCode: 'SMS code',
|
||||
mobile: 'Mobile',
|
||||
};
|
||||
|
4
src/locales/lang/zh_CN/component/countdown.ts
Normal file
4
src/locales/lang/zh_CN/component/countdown.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
normalText: '获取验证码',
|
||||
sendText: '{0}秒后重新获取',
|
||||
};
|
@@ -1,7 +1,21 @@
|
||||
export default {
|
||||
backSignIn: '返回',
|
||||
signInFormTitle: '登录',
|
||||
mobileSignInFormTitle: '手机登录',
|
||||
qrSignInFormTitle: '二维码登录',
|
||||
signUpFormTitle: '注册',
|
||||
forgetFormTitle: '重置密码',
|
||||
|
||||
signInTitle: '开箱即用的中后台管理系统',
|
||||
signInDesc: '输入您的个人详细信息开始使用!',
|
||||
policy: '我同意xxx隐私政策',
|
||||
scanSign: `扫码后点击"确认",即可完成登录`,
|
||||
|
||||
loginButton: '登录',
|
||||
autoLogin: '自动登录',
|
||||
forgetPassword: '忘记密码',
|
||||
registerButton: '注册',
|
||||
rememberMe: '记住我',
|
||||
forgetPassword: '忘记密码?',
|
||||
otherSignIn: '其他登录方式',
|
||||
|
||||
// notify
|
||||
loginSuccessTitle: '登录成功',
|
||||
@@ -10,4 +24,15 @@ export default {
|
||||
// placeholder
|
||||
accountPlaceholder: '请输入账号',
|
||||
passwordPlaceholder: '请输入密码',
|
||||
smsPlaceholder: '请输入验证码',
|
||||
mobilePlaceholder: '请输入手机号码',
|
||||
policyPlaceholder: '勾选后才能注册',
|
||||
diffPwd: '两次输入密码不一致',
|
||||
|
||||
userName: '账号',
|
||||
password: '密码',
|
||||
confirmPassword: '确认密码',
|
||||
email: '邮箱',
|
||||
smsCode: '短信验证码',
|
||||
mobile: '手机号码',
|
||||
};
|
||||
|
@@ -1,37 +1,51 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate">
|
||||
<div
|
||||
:class="prefixCls"
|
||||
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
|
||||
>
|
||||
<div
|
||||
:class="`${prefixCls}__unlock`"
|
||||
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
|
||||
@click="handleShowForm(false)"
|
||||
v-show="showDate"
|
||||
>
|
||||
<LockOutlined />
|
||||
<span>{{ t('sys.lock.unlock') }}</span>
|
||||
</div>
|
||||
|
||||
<div :class="`${prefixCls}__date`">
|
||||
<div :class="`${prefixCls}__hour`">
|
||||
{{ hour }}
|
||||
<span class="meridiem" v-show="showDate">{{ meridiem }}</span>
|
||||
<div class="flex w-screen h-screen justify-center items-center">
|
||||
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
|
||||
<span>{{ hour }}</span>
|
||||
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
|
||||
{{ meridiem }}
|
||||
</span>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__minute`">
|
||||
{{ minute }}
|
||||
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
|
||||
<span> {{ minute }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade-slide">
|
||||
<div :class="`${prefixCls}-entry`" v-show="!showDate">
|
||||
<div :class="`${prefixCls}-entry-content`">
|
||||
<div :class="`${prefixCls}-entry__header`">
|
||||
<div :class="`${prefixCls}-entry__header enter-x`">
|
||||
<img :src="headerImg" :class="`${prefixCls}-entry__header-img`" />
|
||||
<p :class="`${prefixCls}-entry__header-name`">
|
||||
{{ realName }}
|
||||
</p>
|
||||
</div>
|
||||
<InputPassword :placeholder="t('sys.lock.placeholder')" v-model:value="password" />
|
||||
<span :class="`${prefixCls}-entry__err-msg`" v-if="errMsgRef">
|
||||
<InputPassword
|
||||
:placeholder="t('sys.lock.placeholder')"
|
||||
class="enter-x"
|
||||
v-model:value="password"
|
||||
/>
|
||||
<span :class="`${prefixCls}-entry__err-msg enter-x`" v-if="errMsgRef">
|
||||
{{ t('sys.lock.alert') }}
|
||||
</span>
|
||||
<div :class="`${prefixCls}-entry__footer`">
|
||||
<div :class="`${prefixCls}-entry__footer enter-x`">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
class="mt-2 mr-2"
|
||||
class="mt-2 mr-2 enter-x"
|
||||
:disabled="loadingRef"
|
||||
@click="handleShowForm(true)"
|
||||
>
|
||||
@@ -40,7 +54,7 @@
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
class="mt-2 mr-2"
|
||||
class="mt-2 mr-2 enter-x"
|
||||
:disabled="loadingRef"
|
||||
@click="goLogin"
|
||||
>
|
||||
@@ -54,11 +68,11 @@
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div :class="`${prefixCls}__footer-date`">
|
||||
<div class="time" v-show="!showDate">
|
||||
{{ hour }}:{{ minute }} <span class="meridiem">{{ meridiem }}</span>
|
||||
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
|
||||
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
|
||||
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
|
||||
</div>
|
||||
<div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
|
||||
<div class="text-2xl"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -144,125 +158,54 @@
|
||||
@prefix-cls: ~'@{namespace}-lock-page';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: @lock-page-z-index;
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
// background: rgba(23, 27, 41);
|
||||
background: #000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__unlock {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
height: 50px;
|
||||
padding-top: 20px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, 0);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&__date {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__hour {
|
||||
position: relative;
|
||||
margin-right: 80px;
|
||||
|
||||
.meridiem {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 26px;
|
||||
}
|
||||
@media (max-width: @screen-xs) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__hour,
|
||||
&__minute {
|
||||
display: flex;
|
||||
width: 40%;
|
||||
height: 74%;
|
||||
font-weight: 700;
|
||||
color: #bababa;
|
||||
background: #141313;
|
||||
border-radius: 30px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@media (min-width: @screen-xxxl-min) {
|
||||
font-size: 46em;
|
||||
}
|
||||
@media (min-width: @screen-xl-max) and (max-width: @screen-xxl-max) {
|
||||
font-size: 38em;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-lg-max) and (max-width: @screen-xl-max) {
|
||||
font-size: 30em;
|
||||
}
|
||||
@media (min-width: @screen-md-max) and (max-width: @screen-lg-max) {
|
||||
font-size: 23em;
|
||||
}
|
||||
@media (min-width: @screen-sm-max) and (max-width: @screen-md-max) {
|
||||
height: 50%;
|
||||
font-size: 12em;
|
||||
border-radius: 10px;
|
||||
|
||||
.meridiem {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
@media (min-width: @screen-xs-max) and (max-width: @screen-sm-max) {
|
||||
font-size: 13em;
|
||||
}
|
||||
@media (max-width: @screen-xs) {
|
||||
height: 30%;
|
||||
font-size: 5em;
|
||||
border-radius: 10px;
|
||||
|
||||
.meridiem {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer-date {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
width: 100%;
|
||||
font-family: helvetica;
|
||||
color: #bababa;
|
||||
text-align: center;
|
||||
|
||||
.time {
|
||||
font-size: 50px;
|
||||
|
||||
.meridiem {
|
||||
font-size: 32px;
|
||||
@media screen and (max-width: @screen-md) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 26px;
|
||||
@media screen and (min-width: @screen-md) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 90px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: @screen-lg) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: @screen-xl) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 260px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: @screen-2xl) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 320px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
90
src/views/sys/login/ForgetPasswordForm.vue
Normal file
90
src/views/sys/login/ForgetPasswordForm.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
|
||||
<FormItem name="account" class="enter-x">
|
||||
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="mobile" class="enter-x">
|
||||
<Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
|
||||
</FormItem>
|
||||
<FormItem name="sms" class="enter-x">
|
||||
<CountdownInput
|
||||
size="large"
|
||||
v-model:value="formData.sms"
|
||||
:placeholder="t('sys.login.smsCode')"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem class="enter-x">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
@click="handleReset"
|
||||
:loading="loading"
|
||||
class="enter-x"
|
||||
>
|
||||
{{ t('common.resetText') }}
|
||||
</Button>
|
||||
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
|
||||
{{ t('sys.login.backSignIn') }}
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
import { Form, Input, Button } from 'ant-design-vue';
|
||||
import { CountdownInput } from '/@/components/CountDown';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ForgetPasswordForm',
|
||||
components: {
|
||||
Button,
|
||||
Form,
|
||||
FormItem: Form.Item,
|
||||
Input,
|
||||
CountdownInput,
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { setLoginState } = useLoginState();
|
||||
const { getFormRules } = useFormRules();
|
||||
|
||||
const formRef = ref<any>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
mobile: '',
|
||||
sms: '',
|
||||
});
|
||||
|
||||
const { validForm } = useFormValid(formRef);
|
||||
|
||||
async function handleReset() {
|
||||
const data = await validForm();
|
||||
if (!data) return;
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
function handleBackLogin() {
|
||||
setLoginState(LoginStateEnum.LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
formRef,
|
||||
formData,
|
||||
getFormRules,
|
||||
handleReset,
|
||||
loading,
|
||||
handleBackLogin,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -1,228 +1,179 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="opacity-0 login-mask lg:opacity-100"></div>
|
||||
<div class="justify-center login-form-wrap lg:justify-end">
|
||||
<div class="mx-6 login-form">
|
||||
<AppLocalePicker v-if="showLocale" class="login-form__locale" />
|
||||
<div class="px-2 py-10 login-form__content">
|
||||
<header>
|
||||
<img :src="logo" class="mr-4" />
|
||||
<h1>{{ title }}</h1>
|
||||
</header>
|
||||
<div :class="prefixCls" class="relative w-full h-full px-4">
|
||||
<AppLocalePicker
|
||||
class="absolute top-4 right-4 enter-x text-white xl:text-gray-600"
|
||||
:showText="false"
|
||||
/>
|
||||
|
||||
<a-form class="login-form__main" :model="formData" :rules="formRules" ref="formRef">
|
||||
<a-form-item name="account">
|
||||
<a-input size="large" v-model:value="formData.account" placeholder="username: vben" />
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password
|
||||
size="large"
|
||||
visibilityToggle
|
||||
v-model:value="formData.password"
|
||||
placeholder="password: 123456"
|
||||
/>
|
||||
</a-form-item>
|
||||
<span class="-enter-x xl:hidden">
|
||||
<AppLogo :alwaysShowTitle="true" />
|
||||
</span>
|
||||
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-form-item>
|
||||
<!-- No logic, you need to deal with it yourself -->
|
||||
<a-checkbox v-model:checked="autoLogin" size="small">{{
|
||||
t('sys.login.autoLogin')
|
||||
}}</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :style="{ 'text-align': 'right' }">
|
||||
<!-- No logic, you need to deal with it yourself -->
|
||||
<a-button type="link" size="small">
|
||||
{{ t('sys.login.forgetPassword') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="rounded-sm"
|
||||
:block="true"
|
||||
@click="login"
|
||||
:loading="formState.loading"
|
||||
>
|
||||
{{ t('sys.login.loginButton') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="container relative h-full py-2 mx-auto sm:px-10">
|
||||
<div class="flex h-full">
|
||||
<div class="hidden xl:flex xl:flex-col xl:w-6/12 min-h-full mr-4 pl-4">
|
||||
<AppLogo class="-enter-x" />
|
||||
<div class="my-auto">
|
||||
<img
|
||||
:alt="title"
|
||||
src="../../../assets/svg/login-box-bg.svg"
|
||||
class="w-1/2 -mt-16 -enter-x"
|
||||
/>
|
||||
<div class="mt-10 font-medium text-white -enter-x">
|
||||
<span class="mt-4 text-3xl inline-block"> {{ t('sys.login.signInTitle') }}</span>
|
||||
</div>
|
||||
<div class="mt-5 text-md text-white font-normal dark:text-gray-500 -enter-x">
|
||||
{{ t('sys.login.signInDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full xl:h-auto flex py-5 xl:py-0 xl:my-0 w-full xl:w-6/12">
|
||||
<div
|
||||
class="my-auto mx-auto xl:ml-20 bg-white xl:bg-transparent px-5 py-8 sm:px-8 xl:p-0 rounded-md shadow-md xl:shadow-none w-full sm:w-3/4 lg:w-2/4 xl:w-auto enter-x relative"
|
||||
>
|
||||
<h2 class="font-bold text-2xl xl:text-3xl enter-x text-center xl:text-left mb-6">
|
||||
{{ getFormTitle }}
|
||||
</h2>
|
||||
<LoginForm v-show="getShowLogin" />
|
||||
<ForgetPasswordForm v-if="getShowResetPassword" />
|
||||
<RegisterForm v-if="getShowRegister" />
|
||||
<MobileForm v-if="getShowMobile" />
|
||||
<QrCodeForm v-if="getShowQrCode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, unref, toRaw } from 'vue';
|
||||
import { Checkbox, Form, Input, Row, Col } from 'ant-design-vue';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
|
||||
import { Button } from '/@/components/Button';
|
||||
import { AppLogo } from '/@/components/Application';
|
||||
import { AppLocalePicker } from '/@/components/Application';
|
||||
import LoginForm from './LoginForm.vue';
|
||||
import ForgetPasswordForm from './ForgetPasswordForm.vue';
|
||||
import RegisterForm from './RegisterForm.vue';
|
||||
import MobileForm from './MobileForm.vue';
|
||||
import QrCodeForm from './QrCodeForm.vue';
|
||||
|
||||
import { userStore } from '/@/store/modules/user';
|
||||
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
|
||||
import logo from '/@/assets/images/logo.png';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useShowLoginForm, useFormTitle } from './useLogin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Login',
|
||||
components: {
|
||||
[Checkbox.name]: Checkbox,
|
||||
[Form.name]: Form,
|
||||
[Form.Item.name]: Form.Item,
|
||||
[Input.name]: Input,
|
||||
[Input.Password.name]: Input.Password,
|
||||
AButton: Button,
|
||||
AppLogo,
|
||||
LoginForm,
|
||||
ForgetPasswordForm,
|
||||
RegisterForm,
|
||||
MobileForm,
|
||||
QrCodeForm,
|
||||
AppLocalePicker,
|
||||
[Row.name]: Row,
|
||||
[Col.name]: Col,
|
||||
},
|
||||
setup() {
|
||||
const formRef = ref<any>(null);
|
||||
const autoLoginRef = ref(false);
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const { getFormTitle } = useFormTitle();
|
||||
const { prefixCls } = useDesign('login');
|
||||
const { locale } = useProjectSetting();
|
||||
const { notification } = useMessage();
|
||||
const { t } = useI18n();
|
||||
|
||||
const formData = reactive({
|
||||
account: 'vben',
|
||||
password: '123456',
|
||||
});
|
||||
|
||||
const formState = reactive({
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const formRules = reactive({
|
||||
account: [{ required: true, message: t('sys.login.accountPlaceholder'), trigger: 'blur' }],
|
||||
password: [
|
||||
{ required: true, message: t('sys.login.passwordPlaceholder'), trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
|
||||
async function handleLogin() {
|
||||
const form = unref(formRef);
|
||||
if (!form) return;
|
||||
formState.loading = true;
|
||||
try {
|
||||
const data = await form.validate();
|
||||
const userInfo = await userStore.login(
|
||||
toRaw({
|
||||
password: data.password,
|
||||
username: data.account,
|
||||
})
|
||||
);
|
||||
if (userInfo) {
|
||||
notification.success({
|
||||
message: t('sys.login.loginSuccessTitle'),
|
||||
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
formState.loading = false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
formRef,
|
||||
formData,
|
||||
formState,
|
||||
formRules,
|
||||
login: handleLogin,
|
||||
autoLogin: autoLoginRef,
|
||||
title: globSetting && globSetting.title,
|
||||
logo,
|
||||
t,
|
||||
showLocale: locale.show,
|
||||
prefixCls,
|
||||
title: computed(() => globSetting?.title ?? ''),
|
||||
showLocale: computed(() => locale.show),
|
||||
getFormTitle,
|
||||
...useShowLoginForm(),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.login-form__locale {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
z-index: 1;
|
||||
}
|
||||
@prefix-cls: ~'@{namespace}-login';
|
||||
@logo-prefix-cls: ~'@{namespace}-app-logo';
|
||||
@countdown-prefix-cls: ~'@{namespace}-countdown-input';
|
||||
|
||||
.login {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
background: url(../../../assets/images/login/login-bg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
|
||||
&-mask {
|
||||
height: 100%;
|
||||
background: url(../../../assets/images/login/login-in.png) no-repeat;
|
||||
background-position: 30% 30%;
|
||||
background-size: 80% 80%;
|
||||
.@{prefix-cls} {
|
||||
@media (max-width: @screen-xl) {
|
||||
background: linear-gradient(180deg, #1c3faa, #1c3faa);
|
||||
}
|
||||
|
||||
&-form {
|
||||
position: relative;
|
||||
bottom: 60px;
|
||||
width: 400px;
|
||||
background: @white;
|
||||
border: 10px solid rgba(255, 255, 255, 0.5);
|
||||
border-width: 8px;
|
||||
border-radius: 4px;
|
||||
background-clip: padding-box;
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: -48%;
|
||||
background-image: url(/@/assets/svg/login-bg.svg);
|
||||
background-position: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: auto 100%;
|
||||
content: '';
|
||||
@media (max-width: @screen-xl) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
margin: 30px auto 0 auto !important;
|
||||
.@{logo-prefix-cls} {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
height: 30px;
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
img {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
.@{logo-prefix-cls} {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
width: 60%;
|
||||
height: 80px;
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 60px 0 40px 0;
|
||||
border: 1px solid #999;
|
||||
border-radius: 2px;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
&__title {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 80%;
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-sign-in-way {
|
||||
.anticon {
|
||||
font-size: 22px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input:not([type='checkbox']) {
|
||||
min-width: 360px;
|
||||
@media (max-width: @screen-sm) {
|
||||
min-width: 240px;
|
||||
}
|
||||
}
|
||||
.@{countdown-prefix-cls} input {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.ant-divider-inner-text {
|
||||
font-size: 12px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
171
src/views/sys/login/LoginForm.vue
Normal file
171
src/views/sys/login/LoginForm.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
|
||||
<FormItem name="account" class="enter-x">
|
||||
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
|
||||
</FormItem>
|
||||
<FormItem name="password" class="enter-x">
|
||||
<InputPassword
|
||||
size="large"
|
||||
visibilityToggle
|
||||
v-model:value="formData.password"
|
||||
:placeholder="t('sys.login.password')"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<ARow class="enter-x">
|
||||
<ACol :span="12">
|
||||
<FormItem>
|
||||
<!-- No logic, you need to deal with it yourself -->
|
||||
<Checkbox v-model:checked="rememberMe" size="small">
|
||||
{{ t('sys.login.rememberMe') }}
|
||||
</Checkbox>
|
||||
</FormItem>
|
||||
</ACol>
|
||||
<ACol :span="12">
|
||||
<FormItem :style="{ 'text-align': 'right' }">
|
||||
<!-- No logic, you need to deal with it yourself -->
|
||||
<Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
|
||||
{{ t('sys.login.forgetPassword') }}
|
||||
</Button>
|
||||
</FormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
|
||||
<FormItem class="enter-x">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
@click="handleLogin"
|
||||
:loading="loading"
|
||||
class="enter-x"
|
||||
>
|
||||
{{ t('sys.login.loginButton') }}
|
||||
</Button>
|
||||
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
|
||||
{{ t('sys.login.registerButton') }}
|
||||
</Button> -->
|
||||
</FormItem>
|
||||
<ARow class="enter-x">
|
||||
<ACol :span="7">
|
||||
<Button block @click="setLoginState(LoginStateEnum.MOBILE)">
|
||||
{{ t('sys.login.mobileSignInFormTitle') }}
|
||||
</Button>
|
||||
</ACol>
|
||||
<ACol :span="8" :offset="1">
|
||||
<Button block @click="setLoginState(LoginStateEnum.QR_CODE)">
|
||||
{{ t('sys.login.qrSignInFormTitle') }}
|
||||
</Button>
|
||||
</ACol>
|
||||
<ACol :span="7" :offset="1">
|
||||
<Button block @click="setLoginState(LoginStateEnum.REGISTER)">
|
||||
{{ t('sys.login.registerButton') }}
|
||||
</Button>
|
||||
</ACol>
|
||||
</ARow>
|
||||
|
||||
<Divider>{{ t('sys.login.otherSignIn') }}</Divider>
|
||||
|
||||
<div class="flex justify-evenly enter-x" :class="`${prefixCls}-sign-in-way`">
|
||||
<GithubFilled />
|
||||
<WechatFilled />
|
||||
<AlipayCircleFilled />
|
||||
<GoogleCircleFilled />
|
||||
<TwitterCircleFilled />
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, toRaw } from 'vue';
|
||||
|
||||
import { Checkbox, Form, Input, Row, Col, Button, Divider } from 'ant-design-vue';
|
||||
import {
|
||||
GithubFilled,
|
||||
WechatFilled,
|
||||
AlipayCircleFilled,
|
||||
GoogleCircleFilled,
|
||||
TwitterCircleFilled,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
import { userStore } from '/@/store/modules/user';
|
||||
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LoginForm',
|
||||
components: {
|
||||
Checkbox,
|
||||
Button,
|
||||
Form,
|
||||
FormItem: Form.Item,
|
||||
Input,
|
||||
Divider,
|
||||
InputPassword: Input.Password,
|
||||
[Col.name]: Col,
|
||||
[Row.name]: Row,
|
||||
GithubFilled,
|
||||
WechatFilled,
|
||||
AlipayCircleFilled,
|
||||
GoogleCircleFilled,
|
||||
TwitterCircleFilled,
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { notification } = useMessage();
|
||||
const { prefixCls } = useDesign('login');
|
||||
|
||||
const { setLoginState } = useLoginState();
|
||||
const { getFormRules } = useFormRules();
|
||||
|
||||
const formRef = ref<any>(null);
|
||||
const loading = ref(false);
|
||||
const rememberMe = ref(false);
|
||||
|
||||
const formData = reactive({
|
||||
account: 'vben',
|
||||
password: '123456',
|
||||
});
|
||||
|
||||
const { validForm } = useFormValid(formRef);
|
||||
|
||||
async function handleLogin() {
|
||||
const data = await validForm();
|
||||
if (!data) return;
|
||||
try {
|
||||
loading.value = true;
|
||||
const userInfo = await userStore.login(
|
||||
toRaw({
|
||||
password: data.password,
|
||||
username: data.account,
|
||||
})
|
||||
);
|
||||
if (userInfo) {
|
||||
notification.success({
|
||||
message: t('sys.login.loginSuccessTitle'),
|
||||
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
prefixCls,
|
||||
formRef,
|
||||
formData,
|
||||
getFormRules,
|
||||
rememberMe,
|
||||
handleLogin,
|
||||
loading,
|
||||
setLoginState,
|
||||
LoginStateEnum,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
85
src/views/sys/login/MobileForm.vue
Normal file
85
src/views/sys/login/MobileForm.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
|
||||
<FormItem name="mobile" class="enter-x">
|
||||
<Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
|
||||
</FormItem>
|
||||
<FormItem name="sms" class="enter-x">
|
||||
<CountdownInput
|
||||
size="large"
|
||||
v-model:value="formData.sms"
|
||||
:placeholder="t('sys.login.smsCode')"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem class="enter-x">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
@click="handleLogin"
|
||||
:loading="loading"
|
||||
class="enter-x"
|
||||
>
|
||||
{{ t('sys.login.loginButton') }}
|
||||
</Button>
|
||||
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
|
||||
{{ t('sys.login.backSignIn') }}
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
import { Form, Input, Button } from 'ant-design-vue';
|
||||
import { CountdownInput } from '/@/components/CountDown';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MobileForm',
|
||||
components: {
|
||||
Button,
|
||||
Form,
|
||||
FormItem: Form.Item,
|
||||
Input,
|
||||
CountdownInput,
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { setLoginState } = useLoginState();
|
||||
const { getFormRules } = useFormRules();
|
||||
|
||||
const formRef = ref<any>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const formData = reactive({
|
||||
mobile: '',
|
||||
sms: '',
|
||||
});
|
||||
|
||||
const { validForm } = useFormValid(formRef);
|
||||
|
||||
async function handleLogin() {
|
||||
const data = await validForm();
|
||||
if (!data) return;
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
function handleBackLogin() {
|
||||
setLoginState(LoginStateEnum.LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
formRef,
|
||||
formData,
|
||||
getFormRules,
|
||||
handleLogin,
|
||||
loading,
|
||||
handleBackLogin,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
40
src/views/sys/login/QrCodeForm.vue
Normal file
40
src/views/sys/login/QrCodeForm.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="enter-x min-w-64 min-h-64">
|
||||
<QrCode :value="qrCodeUrl" class="enter-x flex justify-center xl:justify-start" :width="280" />
|
||||
<Divider>{{ t('sys.login.scanSign') }}</Divider>
|
||||
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
|
||||
{{ t('sys.login.backSignIn') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { Button, Divider } from 'ant-design-vue';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { LoginStateEnum, useLoginState } from './useLogin';
|
||||
import { QrCode } from '/@/components/Qrcode/index';
|
||||
const qrCodeUrl = 'https://vvbin.cn/next/login';
|
||||
export default defineComponent({
|
||||
name: 'QrCodeForm',
|
||||
components: {
|
||||
Button,
|
||||
QrCode,
|
||||
Divider,
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { setLoginState } = useLoginState();
|
||||
|
||||
function handleBackLogin() {
|
||||
setLoginState(LoginStateEnum.LOGIN);
|
||||
}
|
||||
return {
|
||||
t,
|
||||
handleBackLogin,
|
||||
qrCodeUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
117
src/views/sys/login/RegisterForm.vue
Normal file
117
src/views/sys/login/RegisterForm.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
|
||||
<FormItem name="account" class="enter-x">
|
||||
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
|
||||
</FormItem>
|
||||
<FormItem name="mobile" class="enter-x">
|
||||
<Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
|
||||
</FormItem>
|
||||
<FormItem name="sms" class="enter-x">
|
||||
<CountdownInput
|
||||
size="large"
|
||||
v-model:value="formData.sms"
|
||||
:placeholder="t('sys.login.smsCode')"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="password" class="enter-x">
|
||||
<StrengthMeter
|
||||
size="large"
|
||||
v-model:value="formData.password"
|
||||
:placeholder="t('sys.login.password')"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="confirmPassword" class="enter-x">
|
||||
<InputPassword
|
||||
size="large"
|
||||
visibilityToggle
|
||||
v-model:value="formData.confirmPassword"
|
||||
:placeholder="t('sys.login.confirmPassword')"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem class="enter-x" name="policy">
|
||||
<!-- No logic, you need to deal with it yourself -->
|
||||
<Checkbox v-model:checked="formData.policy" size="small">
|
||||
{{ t('sys.login.policy') }}
|
||||
</Checkbox>
|
||||
</FormItem>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
@click="handleReset"
|
||||
:loading="loading"
|
||||
class="enter-x"
|
||||
>
|
||||
{{ t('sys.login.registerButton') }}
|
||||
</Button>
|
||||
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
|
||||
{{ t('sys.login.backSignIn') }}
|
||||
</Button>
|
||||
</Form>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
import { Form, Input, Button, Divider, Checkbox } from 'ant-design-vue';
|
||||
import { StrengthMeter } from '/@/components/StrengthMeter';
|
||||
import { CountdownInput } from '/@/components/CountDown';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RegisterPasswordForm',
|
||||
components: {
|
||||
Button,
|
||||
Form,
|
||||
FormItem: Form.Item,
|
||||
Input,
|
||||
Divider,
|
||||
InputPassword: Input.Password,
|
||||
Checkbox,
|
||||
StrengthMeter,
|
||||
CountdownInput,
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { setLoginState } = useLoginState();
|
||||
|
||||
const formRef = ref<any>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
mobile: '',
|
||||
sms: '',
|
||||
policy: false,
|
||||
});
|
||||
|
||||
const { getFormRules } = useFormRules(formData);
|
||||
const { validForm } = useFormValid(formRef);
|
||||
|
||||
async function handleReset() {
|
||||
const data = await validForm();
|
||||
if (!data) return;
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
function handleBackLogin() {
|
||||
setLoginState(LoginStateEnum.LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
formRef,
|
||||
formData,
|
||||
getFormRules,
|
||||
handleReset,
|
||||
loading,
|
||||
handleBackLogin,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
134
src/views/sys/login/useLogin.ts
Normal file
134
src/views/sys/login/useLogin.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { RuleObject } from 'ant-design-vue/lib/form/interface';
|
||||
import { ref, computed, unref, Ref } from 'vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
export enum LoginStateEnum {
|
||||
LOGIN,
|
||||
REGISTER,
|
||||
RESET_PASSWORD,
|
||||
MOBILE,
|
||||
QR_CODE,
|
||||
}
|
||||
|
||||
const currentState = ref(LoginStateEnum.LOGIN);
|
||||
|
||||
export function useFormTitle() {
|
||||
const { t } = useI18n();
|
||||
|
||||
const getFormTitle = computed(() => {
|
||||
const titleObj = {
|
||||
[LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
|
||||
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
|
||||
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
|
||||
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
|
||||
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
|
||||
};
|
||||
return titleObj[unref(currentState)];
|
||||
});
|
||||
return { getFormTitle };
|
||||
}
|
||||
|
||||
export function useLoginState() {
|
||||
function setLoginState(state: LoginStateEnum) {
|
||||
currentState.value = state;
|
||||
}
|
||||
|
||||
const getLoginState = computed(() => currentState.value);
|
||||
|
||||
return { setLoginState, getLoginState };
|
||||
}
|
||||
|
||||
export function useShowLoginForm() {
|
||||
const getShowLogin = computed(() => unref(currentState) === LoginStateEnum.LOGIN);
|
||||
const getShowResetPassword = computed(
|
||||
() => unref(currentState) === LoginStateEnum.RESET_PASSWORD
|
||||
);
|
||||
const getShowRegister = computed(() => unref(currentState) === LoginStateEnum.REGISTER);
|
||||
const getShowMobile = computed(() => unref(currentState) === LoginStateEnum.MOBILE);
|
||||
const getShowQrCode = computed(() => unref(currentState) === LoginStateEnum.QR_CODE);
|
||||
|
||||
return { getShowLogin, getShowResetPassword, getShowRegister, getShowMobile, getShowQrCode };
|
||||
}
|
||||
|
||||
export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
|
||||
async function validForm() {
|
||||
const form = unref(formRef);
|
||||
if (!form) return;
|
||||
const data = await form.validate();
|
||||
return data as T;
|
||||
}
|
||||
|
||||
return { validForm };
|
||||
}
|
||||
|
||||
export function useFormRules(formData?: Recordable) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder')));
|
||||
const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder')));
|
||||
const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder')));
|
||||
const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder')));
|
||||
|
||||
const validatePolicy = async (_: RuleObject, value: boolean) => {
|
||||
return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve();
|
||||
};
|
||||
|
||||
const validateConfirmPassword = (password: string) => {
|
||||
return async (_: RuleObject, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject(t('sys.login.passwordPlaceholder'));
|
||||
}
|
||||
if (value !== password) {
|
||||
return Promise.reject(t('sys.login.diffPwd'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
};
|
||||
|
||||
const getFormRules = computed(() => {
|
||||
const accountFormRule = unref(getAccountFormRule);
|
||||
const passwordFormRule = unref(getPasswordFormRule);
|
||||
const smsFormRule = unref(getSmsFormRule);
|
||||
const mobileFormRule = unref(getMobileFormRule);
|
||||
|
||||
const mobileRule = {
|
||||
sms: smsFormRule,
|
||||
mobile: mobileFormRule,
|
||||
};
|
||||
switch (unref(currentState)) {
|
||||
case LoginStateEnum.REGISTER:
|
||||
return {
|
||||
account: accountFormRule,
|
||||
password: passwordFormRule,
|
||||
confirmPassword: [
|
||||
{ validator: validateConfirmPassword(formData?.password), trigger: 'change' },
|
||||
],
|
||||
policy: [{ validator: validatePolicy, trigger: 'change' }],
|
||||
...mobileRule,
|
||||
};
|
||||
case LoginStateEnum.RESET_PASSWORD:
|
||||
return {
|
||||
account: accountFormRule,
|
||||
...mobileRule,
|
||||
};
|
||||
case LoginStateEnum.MOBILE:
|
||||
return mobileRule;
|
||||
default:
|
||||
return {
|
||||
account: accountFormRule,
|
||||
password: passwordFormRule,
|
||||
};
|
||||
}
|
||||
});
|
||||
return { getFormRules };
|
||||
}
|
||||
|
||||
function createRule(message: string) {
|
||||
return [
|
||||
{
|
||||
required: true,
|
||||
message,
|
||||
trigger: 'change',
|
||||
},
|
||||
];
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const colors = require('windicss/colors');
|
||||
const defaultTheme = require('windicss/defaultTheme');
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
plugins: [
|
||||
require('windicss/plugin/forms'),
|
||||
require('windicss/plugin/typography'),
|
||||
require('windicss/plugin/line-clamp'),
|
||||
require('windicss/plugin/aspect-ratio'),
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors,
|
||||
fontFamily: {
|
||||
sans: ['Righteous', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
71
windi.config.ts
Normal file
71
windi.config.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import lineClamp from 'windicss/plugin/line-clamp';
|
||||
import colors from 'windicss/colors';
|
||||
|
||||
import { defineConfig } from 'vite-plugin-windicss';
|
||||
|
||||
export default defineConfig({
|
||||
darkMode: 'class',
|
||||
plugins: [lineClamp, createEnterPlugin()],
|
||||
theme: {
|
||||
extend: {
|
||||
colors,
|
||||
},
|
||||
|
||||
// screen: {
|
||||
// sm: '576px',
|
||||
// md: '768px',
|
||||
// lg: '992px',
|
||||
// xl: '1200px',
|
||||
// '2xl': '1600px',
|
||||
// },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Used for animation when the element is displayed
|
||||
* @param maxOutput The larger the maxOutput output, the larger the generated css volume
|
||||
*/
|
||||
function createEnterPlugin(maxOutput = 10) {
|
||||
const createCss = (index: number, d = 'x') => {
|
||||
const upd = d.toUpperCase();
|
||||
return {
|
||||
[`*> .enter-${d}:nth-child(${index})`]: {
|
||||
transform: `translate${upd}(50px)`,
|
||||
},
|
||||
[`*> .-enter-${d}:nth-child(${index})`]: {
|
||||
transform: `translate${upd}(-50px)`,
|
||||
},
|
||||
[`* > .enter-${d}:nth-child(${index}),* > .-enter-${d}:nth-child(${index})`]: {
|
||||
'z-index': `${10 - index}`,
|
||||
opacity: '0',
|
||||
animation: `enter-${d}-animation 0.4s ease-in-out 0.3s`,
|
||||
'animation-fill-mode': 'forwards',
|
||||
'animation-delay': `${(index * 1) / 10}s`,
|
||||
},
|
||||
};
|
||||
};
|
||||
const handler = ({ addBase }) => {
|
||||
for (let index = 1; index < maxOutput; index++) {
|
||||
addBase({
|
||||
...createCss(index, 'x'),
|
||||
...createCss(index, 'y'),
|
||||
});
|
||||
}
|
||||
|
||||
addBase({
|
||||
[`@keyframes enter-x-animation`]: {
|
||||
to: {
|
||||
opacity: '1',
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
},
|
||||
[`@keyframes enter-y-animation`]: {
|
||||
to: {
|
||||
opacity: '1',
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
return { handler };
|
||||
}
|
63
yarn.lock
63
yarn.lock
@@ -1936,14 +1936,16 @@
|
||||
dependencies:
|
||||
vue-demi latest
|
||||
|
||||
"@windicss/plugin-utils@0.3.12":
|
||||
version "0.3.12"
|
||||
resolved "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-0.3.12.tgz#69b55be1ffb45753e6f01aa236f5ecd8df7a92ee"
|
||||
integrity sha512-XA+xeyu5KM322dIp+EEHeXnAPuK+KxuWyoGvJnxXi9U50nIp0QraqXAH7xl9ghIkVHvVrb8pmm8vHpzFvsqF2A==
|
||||
"@windicss/plugin-utils@0.4.3":
|
||||
version "0.4.3"
|
||||
resolved "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-0.4.3.tgz#84e85fd3cd6eaf54ca72cae276f9cf0610f45e56"
|
||||
integrity sha512-ilddLED+sZQIA9rOwE5eYwdBEBWKREvAVkkiAOOTNf7oDcP/a1cxT3f/nE4tgfhz+MC/FKcy7NkfrqfXRdEQaQ==
|
||||
dependencies:
|
||||
esbuild "^0.8.49"
|
||||
esbuild-register "^2.0.0"
|
||||
fast-glob "^3.2.5"
|
||||
micromatch "^4.0.2"
|
||||
windicss "^2.1.12"
|
||||
windicss "^2.1.15"
|
||||
|
||||
"@zxcvbn-ts/core@^0.2.0":
|
||||
version "0.2.0"
|
||||
@@ -3870,6 +3872,11 @@ esbuild@^0.8.48:
|
||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.48.tgz#a57e4dde84ec56da1c6ecaefee97e9da6c5b00b5"
|
||||
integrity sha512-lrH8lA8wWQ6Lpe1z6C7ZZaFSmRsUlcQAqe16nf7ITySQ7MV4+vI7qAqQlT/u+c3+9AL3VXmT4MXTxV2e63pO4A==
|
||||
|
||||
esbuild@^0.8.49:
|
||||
version "0.8.49"
|
||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.49.tgz#3d33f71b3966611f822cf4c838115f3fbd16def2"
|
||||
integrity sha512-itiFVYv5UZz4NooO7/Y0bRGVDGz/M/cxKbl6zyNI5pnKaz1mZjvZXAFhhDVz6rGCmcdTKj5oag6rh8DaaSSmfQ==
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
@@ -8454,10 +8461,10 @@ stylelint-order@^4.1.0:
|
||||
postcss "^7.0.31"
|
||||
postcss-sorting "^5.0.1"
|
||||
|
||||
stylelint@^13.10.0:
|
||||
version "13.10.0"
|
||||
resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.10.0.tgz#67b0c6f378c3fa61aa569a55d38feb8570b0b587"
|
||||
integrity sha512-eDuLrL0wzPKbl5/TbNGZcbw0lTIGbDEr5W6lCODvb1gAg0ncbgCRt7oU0C2VFDvbrcY0A3MFZOwltwTRmc0XCw==
|
||||
stylelint@^13.11.0:
|
||||
version "13.11.0"
|
||||
resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.11.0.tgz#591981fbdd68c9d3d3e6147a0cd6a07539fc216d"
|
||||
integrity sha512-DhrKSWDWGZkCiQMtU+VroXM6LWJVC8hSK24nrUngTSQvXGK75yZUq4yNpynqrxD3a/fzKMED09V+XxO4z4lTbw==
|
||||
dependencies:
|
||||
"@stylelint/postcss-css-in-js" "^0.37.2"
|
||||
"@stylelint/postcss-markdown" "^0.36.2"
|
||||
@@ -9262,17 +9269,17 @@ vite-plugin-purge-icons@^0.7.0:
|
||||
"@purge-icons/generated" "^0.7.0"
|
||||
rollup-plugin-purge-icons "^0.7.0"
|
||||
|
||||
vite-plugin-pwa@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.5.1.tgz#7f94b8c4092ba0bba0a1bceb690e7420b18071e7"
|
||||
integrity sha512-hf8BgyH0XLNEJUoMsk7ywMoE+OoQelK/+4RQoftQomZhlKXgsTWrfshFGOV7sKUbLsxMh0cVoh1DmAulQmRzKQ==
|
||||
vite-plugin-pwa@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.5.2.tgz#48131ebadc0c98c34a543dbf1bb1c86aeef532e0"
|
||||
integrity sha512-4SHKxYhd5sCF/ebbgxGYlN91UHlylzh7C32a5+Y2c2vbrWzw5x62ZxsYzolQzBosdOim4Ez+e/dX4hmP3BCmow==
|
||||
dependencies:
|
||||
debug "^4.3.2"
|
||||
fast-glob "^3.2.5"
|
||||
pretty-bytes "^5.5.0"
|
||||
workbox-build "^6.1.0"
|
||||
|
||||
vite-plugin-style-import@^0.7.2:
|
||||
vite-plugin-style-import@^0.7.3:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.npmjs.org/vite-plugin-style-import/-/vite-plugin-style-import-0.7.3.tgz#4a9fb08bf5f2fc4796391c9be9a587ecb5c97e9e"
|
||||
integrity sha512-oKM6vOl7iWaB5U1HcR5oM1oPRhT1n5yJt7h4h9jwpMPCD5ckHPcSjjSU7ZlOJkoS/IWEnDkQqoZi162bOs1rTQ==
|
||||
@@ -9295,13 +9302,13 @@ vite-plugin-theme@^0.4.3:
|
||||
es-module-lexer "^0.3.26"
|
||||
tinycolor2 "^1.4.2"
|
||||
|
||||
vite-plugin-windicss@0.3.12:
|
||||
version "0.3.12"
|
||||
resolved "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-0.3.12.tgz#5503b4ee738268a37c857c0cf55cea41f28fa3e6"
|
||||
integrity sha512-NuzIjSrqBQKvpbLJoU9qi8PIWBBXCqBmuLg9Dl/cFl4MB/vAHIOB6sZYJatCBFTU39Kw4UU0GhAjDBSNqzTn0w==
|
||||
vite-plugin-windicss@0.4.3:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-0.4.3.tgz#f86e5a3b78882caa3cdd50cba2b08770e2d627c8"
|
||||
integrity sha512-Lnv6OhcYzcJvecTs4LIpMSfo54rSewkHrW85IVwy8hacR0krY319jXr5nwiDpSTp6HM3QJhoJ4zxHF+t5Q+Nwg==
|
||||
dependencies:
|
||||
"@windicss/plugin-utils" "0.3.12"
|
||||
windicss "^2.1.12"
|
||||
"@windicss/plugin-utils" "0.4.3"
|
||||
windicss "^2.1.15"
|
||||
|
||||
vite@2.0.1:
|
||||
version "2.0.1"
|
||||
@@ -9356,10 +9363,10 @@ vue-i18n@9.0.0-rc.2:
|
||||
"@intlify/shared" "9.0.0-rc.2"
|
||||
"@vue/devtools-api" "^6.0.0-beta.3"
|
||||
|
||||
vue-router@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz#8b26050c88b2dec7e27a88835f71046b365823ec"
|
||||
integrity sha512-AD1OjtVPyQHTSpoRsEGfPpxRQwhAhxcacOYO3zJ3KNkYP/r09mileSp6kdMQKhZWP2cFsPR3E2M3PZguSN5/ww==
|
||||
vue-router@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.4.tgz#ad9b4b7bbdad622407b4ff189b1646f48c1e9053"
|
||||
integrity sha512-uN6PDEaYdU9aRO7mU+Dkr1uaY49hV3fucEDG/Vre/Qj8ct3RoJS16vcPrvKVzn69zDDjBV5b9Xw7fZA9r6b/Iw==
|
||||
|
||||
vue-types@^3.0.0:
|
||||
version "3.0.1"
|
||||
@@ -9434,10 +9441,10 @@ which@^2.0.1:
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
windicss@^2.1.12:
|
||||
version "2.1.12"
|
||||
resolved "https://registry.npmjs.org/windicss/-/windicss-2.1.12.tgz#840b963f03af7a3e31b989d2b51de52dcd57a37a"
|
||||
integrity sha512-VC057iG65zlvdqUI+1ynzOuKikalvYg6XqPGbG17HEAfwQ0sg1dACTk2plEp1QAEQNtKU3BnLnueWa4oKlltEQ==
|
||||
windicss@^2.1.15:
|
||||
version "2.1.15"
|
||||
resolved "https://registry.npmjs.org/windicss/-/windicss-2.1.15.tgz#0a5bf1a56711ab53de8093a3c855764d93ffac00"
|
||||
integrity sha512-gBihXNJPzv/kBaelOlXvbrmWsWuv98OPSf/yUYjc8EnRGCOxDOQIRin4FYPTWCmZi91PZThh7nMjzQZiBV+MYg==
|
||||
|
||||
wmf@~1.0.1:
|
||||
version "1.0.2"
|
||||
|
Reference in New Issue
Block a user