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