33 changed files with 1220 additions and 401 deletions
@ -1,19 +1,14 @@ |
|||||
{ |
{ |
||||
"recommendations": [ |
"recommendations": [ |
||||
"johnsoncodehk.volar", |
|
||||
"octref.vetur", |
"octref.vetur", |
||||
"dbaeumer.vscode-eslint", |
"dbaeumer.vscode-eslint", |
||||
"stylelint.vscode-stylelint", |
"stylelint.vscode-stylelint", |
||||
"DavidAnson.vscode-markdownlint", |
|
||||
"esbenp.prettier-vscode", |
"esbenp.prettier-vscode", |
||||
"mrmlnc.vscode-less", |
"mrmlnc.vscode-less", |
||||
"antfu.i18n-ally", |
"antfu.i18n-ally", |
||||
"cpylua.language-postcss", |
|
||||
"Orta.vscode-jest", |
|
||||
"antfu.iconify", |
"antfu.iconify", |
||||
"mikestead.dotenv", |
"mikestead.dotenv", |
||||
"bradlc.vscode-tailwindcss", |
"bradlc.vscode-tailwindcss", |
||||
"heybourn.headwind", |
"heybourn.headwind" |
||||
"znck.vue-language-features" |
|
||||
] |
] |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,5 @@ |
|||||
|
# Review comments generated by i18n-ally. Please commit this file. |
||||
|
|
||||
|
reviews: |
||||
|
sys.login.autoLogin: |
||||
|
description: '1' |
||||
|
Before Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
@ -0,0 +1,4 @@ |
|||||
|
import CountButton from './src/CountButton.vue'; |
||||
|
import CountdownInput from './src/CountdownInput.vue'; |
||||
|
|
||||
|
export { CountdownInput, CountButton }; |
||||
@ -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> |
||||
@ -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> |
||||
@ -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 }; |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
export default { |
||||
|
normalText: 'Get SMS code', |
||||
|
sendText: 'Reacquire in {0}s', |
||||
|
}; |
||||
@ -1,13 +1,38 @@ |
|||||
export default { |
export default { |
||||
loginButton: 'Login', |
backSignIn: 'Back sign in', |
||||
autoLogin: 'AutoLogin', |
mobileSignInFormTitle: 'Mobile sign in', |
||||
forgetPassword: 'Forget Password', |
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
|
// notify
|
||||
loginSuccessTitle: 'Login successful', |
loginSuccessTitle: 'Login successful', |
||||
loginSuccessDesc: 'Welcome back', |
loginSuccessDesc: 'Welcome back', |
||||
|
|
||||
// placeholder
|
// placeholder
|
||||
accountPlaceholder: 'Please input Username', |
accountPlaceholder: 'Please input username', |
||||
passwordPlaceholder: 'Please input Password', |
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', |
||||
}; |
}; |
||||
|
|||||
@ -0,0 +1,4 @@ |
|||||
|
export default { |
||||
|
normalText: '获取验证码', |
||||
|
sendText: '{0}秒后重新获取', |
||||
|
}; |
||||
@ -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> |
<template> |
||||
<div class="login"> |
<div :class="prefixCls" class="relative w-full h-full px-4"> |
||||
<div class="opacity-0 login-mask lg:opacity-100"></div> |
<AppLocalePicker |
||||
<div class="justify-center login-form-wrap lg:justify-end"> |
class="absolute top-4 right-4 enter-x text-white xl:text-gray-600" |
||||
<div class="mx-6 login-form"> |
:showText="false" |
||||
<AppLocalePicker v-if="showLocale" class="login-form__locale" /> |
/> |
||||
<div class="px-2 py-10 login-form__content"> |
|
||||
<header> |
<span class="-enter-x xl:hidden"> |
||||
<img :src="logo" class="mr-4" /> |
<AppLogo :alwaysShowTitle="true" /> |
||||
<h1>{{ title }}</h1> |
</span> |
||||
</header> |
|
||||
|
<div class="container relative h-full py-2 mx-auto sm:px-10"> |
||||
<a-form class="login-form__main" :model="formData" :rules="formRules" ref="formRef"> |
<div class="flex h-full"> |
||||
<a-form-item name="account"> |
<div class="hidden xl:flex xl:flex-col xl:w-6/12 min-h-full mr-4 pl-4"> |
||||
<a-input size="large" v-model:value="formData.account" placeholder="username: vben" /> |
<AppLogo class="-enter-x" /> |
||||
</a-form-item> |
<div class="my-auto"> |
||||
<a-form-item name="password"> |
<img |
||||
<a-input-password |
:alt="title" |
||||
size="large" |
src="../../../assets/svg/login-box-bg.svg" |
||||
visibilityToggle |
class="w-1/2 -mt-16 -enter-x" |
||||
v-model:value="formData.password" |
/> |
||||
placeholder="password: 123456" |
<div class="mt-10 font-medium text-white -enter-x"> |
||||
/> |
<span class="mt-4 text-3xl inline-block"> {{ t('sys.login.signInTitle') }}</span> |
||||
</a-form-item> |
</div> |
||||
|
<div class="mt-5 text-md text-white font-normal dark:text-gray-500 -enter-x"> |
||||
<a-row> |
{{ t('sys.login.signInDesc') }} |
||||
<a-col :span="12"> |
</div> |
||||
<a-form-item> |
</div> |
||||
<!-- No logic, you need to deal with it yourself --> |
</div> |
||||
<a-checkbox v-model:checked="autoLogin" size="small">{{ |
<div class="h-full xl:h-auto flex py-5 xl:py-0 xl:my-0 w-full xl:w-6/12"> |
||||
t('sys.login.autoLogin') |
<div |
||||
}}</a-checkbox> |
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" |
||||
</a-form-item> |
> |
||||
</a-col> |
<h2 class="font-bold text-2xl xl:text-3xl enter-x text-center xl:text-left mb-6"> |
||||
<a-col :span="12"> |
{{ getFormTitle }} |
||||
<a-form-item :style="{ 'text-align': 'right' }"> |
</h2> |
||||
<!-- No logic, you need to deal with it yourself --> |
<LoginForm v-show="getShowLogin" /> |
||||
<a-button type="link" size="small"> |
<ForgetPasswordForm v-if="getShowResetPassword" /> |
||||
{{ t('sys.login.forgetPassword') }} |
<RegisterForm v-if="getShowRegister" /> |
||||
</a-button> |
<MobileForm v-if="getShowMobile" /> |
||||
</a-form-item> |
<QrCodeForm v-if="getShowQrCode" /> |
||||
</a-col> |
</div> |
||||
</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> |
</div> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
</template> |
</template> |
||||
<script lang="ts"> |
<script lang="ts"> |
||||
import { defineComponent, reactive, ref, unref, toRaw } from 'vue'; |
import { defineComponent, computed } from 'vue'; |
||||
import { Checkbox, Form, Input, Row, Col } from 'ant-design-vue'; |
|
||||
|
|
||||
import { Button } from '/@/components/Button'; |
import { AppLogo } from '/@/components/Application'; |
||||
import { AppLocalePicker } 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 { useGlobSetting, useProjectSetting } from '/@/hooks/setting'; |
||||
import logo from '/@/assets/images/logo.png'; |
|
||||
import { useI18n } from '/@/hooks/web/useI18n'; |
import { useI18n } from '/@/hooks/web/useI18n'; |
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
import { useShowLoginForm, useFormTitle } from './useLogin'; |
||||
|
|
||||
export default defineComponent({ |
export default defineComponent({ |
||||
|
name: 'Login', |
||||
components: { |
components: { |
||||
[Checkbox.name]: Checkbox, |
AppLogo, |
||||
[Form.name]: Form, |
LoginForm, |
||||
[Form.Item.name]: Form.Item, |
ForgetPasswordForm, |
||||
[Input.name]: Input, |
RegisterForm, |
||||
[Input.Password.name]: Input.Password, |
MobileForm, |
||||
AButton: Button, |
QrCodeForm, |
||||
AppLocalePicker, |
AppLocalePicker, |
||||
[Row.name]: Row, |
|
||||
[Col.name]: Col, |
|
||||
}, |
}, |
||||
setup() { |
setup() { |
||||
const formRef = ref<any>(null); |
|
||||
const autoLoginRef = ref(false); |
|
||||
|
|
||||
const globSetting = useGlobSetting(); |
const globSetting = useGlobSetting(); |
||||
|
const { getFormTitle } = useFormTitle(); |
||||
|
const { prefixCls } = useDesign('login'); |
||||
const { locale } = useProjectSetting(); |
const { locale } = useProjectSetting(); |
||||
const { notification } = useMessage(); |
|
||||
const { t } = useI18n(); |
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 { |
return { |
||||
formRef, |
|
||||
formData, |
|
||||
formState, |
|
||||
formRules, |
|
||||
login: handleLogin, |
|
||||
autoLogin: autoLoginRef, |
|
||||
title: globSetting && globSetting.title, |
|
||||
logo, |
|
||||
t, |
t, |
||||
showLocale: locale.show, |
prefixCls, |
||||
|
title: computed(() => globSetting?.title ?? ''), |
||||
|
showLocale: computed(() => locale.show), |
||||
|
getFormTitle, |
||||
|
...useShowLoginForm(), |
||||
}; |
}; |
||||
}, |
}, |
||||
}); |
}); |
||||
</script> |
</script> |
||||
<style lang="less"> |
<style lang="less"> |
||||
.login-form__locale { |
@prefix-cls: ~'@{namespace}-login'; |
||||
position: absolute; |
@logo-prefix-cls: ~'@{namespace}-app-logo'; |
||||
top: 14px; |
@countdown-prefix-cls: ~'@{namespace}-countdown-input'; |
||||
right: 14px; |
|
||||
z-index: 1; |
|
||||
} |
|
||||
|
|
||||
.login { |
.@{prefix-cls} { |
||||
position: relative; |
@media (max-width: @screen-xl) { |
||||
height: 100vh; |
background: linear-gradient(180deg, #1c3faa, #1c3faa); |
||||
background: url(../../../assets/images/login/login-bg.png) no-repeat; |
} |
||||
background-size: 100% 100%; |
|
||||
|
|
||||
&-mask { |
&::before { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
height: 100%; |
height: 100%; |
||||
background: url(../../../assets/images/login/login-in.png) no-repeat; |
margin-left: -48%; |
||||
background-position: 30% 30%; |
background-image: url(/@/assets/svg/login-bg.svg); |
||||
background-size: 80% 80%; |
background-position: 100%; |
||||
|
background-repeat: no-repeat; |
||||
|
background-size: auto 100%; |
||||
|
content: ''; |
||||
|
@media (max-width: @screen-xl) { |
||||
|
display: none; |
||||
|
} |
||||
} |
} |
||||
|
|
||||
&-form { |
.@{logo-prefix-cls} { |
||||
position: relative; |
position: absolute; |
||||
bottom: 60px; |
top: 12px; |
||||
width: 400px; |
height: 30px; |
||||
background: @white; |
|
||||
border: 10px solid rgba(255, 255, 255, 0.5); |
&__title { |
||||
border-width: 8px; |
font-size: 16px; |
||||
border-radius: 4px; |
color: #fff; |
||||
background-clip: padding-box; |
|
||||
|
|
||||
&__main { |
|
||||
margin: 30px auto 0 auto !important; |
|
||||
} |
} |
||||
|
|
||||
&-wrap { |
img { |
||||
position: absolute; |
width: 32px; |
||||
top: 0; |
|
||||
right: 0; |
|
||||
display: flex; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
align-items: center; |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
.@{logo-prefix-cls} { |
||||
|
display: flex; |
||||
|
width: 60%; |
||||
|
height: 80px; |
||||
|
|
||||
|
&__title { |
||||
|
font-size: 24px; |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
&__content { |
img { |
||||
position: relative; |
width: 48px; |
||||
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; |
|
||||
} |
|
||||
} |
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-sign-in-way { |
||||
|
.anticon { |
||||
|
font-size: 22px; |
||||
|
color: #888; |
||||
|
cursor: pointer; |
||||
|
|
||||
form { |
&:hover { |
||||
width: 80%; |
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> |
</style> |
||||
|
|||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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], |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}; |
|
||||
@ -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 }; |
||||
|
} |
||||
Loading…
Reference in new issue