22 changed files with 639 additions and 64 deletions
@ -0,0 +1,47 @@ |
|||||
|
import type { |
||||
|
VbenFormSchema as FormSchema, |
||||
|
VbenFormProps, |
||||
|
} from '@vben/common-ui'; |
||||
|
|
||||
|
import type { ComponentType } from './component'; |
||||
|
|
||||
|
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
setupVbenForm<ComponentType>({ |
||||
|
config: { |
||||
|
// ant design vue组件库默认都是 v-model:value
|
||||
|
baseModelPropName: 'value', |
||||
|
|
||||
|
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
|
modelPropNameMap: { |
||||
|
Checkbox: 'checked', |
||||
|
Radio: 'checked', |
||||
|
Switch: 'checked', |
||||
|
Upload: 'fileList', |
||||
|
}, |
||||
|
}, |
||||
|
defineRules: { |
||||
|
// 输入项目必填国际化适配
|
||||
|
required: (value, _params, ctx) => { |
||||
|
if (value === undefined || value === null || value.length === 0) { |
||||
|
return $t('ui.formRules.required', [ctx.label]); |
||||
|
} |
||||
|
return true; |
||||
|
}, |
||||
|
// 选择项目必填国际化适配
|
||||
|
selectRequired: (value, _params, ctx) => { |
||||
|
if (value === undefined || value === null) { |
||||
|
return $t('ui.formRules.selectRequired', [ctx.label]); |
||||
|
} |
||||
|
return true; |
||||
|
}, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const useVbenForm = useForm<ComponentType>; |
||||
|
|
||||
|
export { useVbenForm, z }; |
||||
|
|
||||
|
export type VbenFormSchema = FormSchema<ComponentType>; |
||||
|
export type { VbenFormProps }; |
||||
@ -0,0 +1,206 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { TwoFactorProvider } from '@abp/account'; |
||||
|
import type { FormExpose } from 'ant-design-vue/es/form/Form'; |
||||
|
|
||||
|
import { computed, ref, toValue, useTemplateRef } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { useAccountApi } from '@abp/account'; |
||||
|
import { Button, Form, Input, Select } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useAuthStore } from '#/store/auth'; |
||||
|
|
||||
|
const FormItem = Form.Item; |
||||
|
|
||||
|
interface TwoFactorState { |
||||
|
username: string; |
||||
|
password: string; |
||||
|
twoFactorToken: string; |
||||
|
userId: string; |
||||
|
twoFactorProvider: string; |
||||
|
code: string; |
||||
|
email: string; |
||||
|
phoneNumber: string; |
||||
|
} |
||||
|
|
||||
|
const formModel = ref({} as TwoFactorState); |
||||
|
const form = useTemplateRef<FormExpose>('formRef'); |
||||
|
const twoFactorProviders = ref<TwoFactorProvider[]>([]); |
||||
|
const sendValidCodeLoading = ref(false); |
||||
|
const sendValidCodeInternal = ref(0); |
||||
|
const getSendValidCodeLoading = computed(() => { |
||||
|
return sendValidCodeInternal.value > 0; |
||||
|
}); |
||||
|
const getSendValidCodeTitle = computed(() => { |
||||
|
if (sendValidCodeInternal.value > 0) { |
||||
|
return `${sendValidCodeInternal.value}`; |
||||
|
} |
||||
|
return $t('abp.oauth.twoFactor.getCode'); |
||||
|
}); |
||||
|
|
||||
|
const authStore = useAuthStore(); |
||||
|
const { |
||||
|
getTwoFactorProvidersApi, |
||||
|
sendEmailSigninCodeApi, |
||||
|
sendPhoneSigninCodeApi, |
||||
|
} = useAccountApi(); |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
title: $t('abp.oauth.twoFactor.title'), |
||||
|
fullscreenButton: false, |
||||
|
closeOnClickModal: false, |
||||
|
closeOnPressEscape: false, |
||||
|
onConfirm: onLogin, |
||||
|
async onOpenChange(isOpen) { |
||||
|
if (isOpen) { |
||||
|
const state = modalApi.getData<TwoFactorState>(); |
||||
|
formModel.value = state; |
||||
|
const { items } = await getTwoFactorProvidersApi({ |
||||
|
userId: state.userId, |
||||
|
}); |
||||
|
twoFactorProviders.value = items; |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
/** 二次认证登陆 */ |
||||
|
async function onLogin() { |
||||
|
modalApi.setState({ |
||||
|
confirmLoading: true, |
||||
|
closable: false, |
||||
|
}); |
||||
|
try { |
||||
|
await form.value?.validate(); |
||||
|
const model = toValue(formModel); |
||||
|
await authStore.authLogin({ |
||||
|
username: model.username, |
||||
|
password: model.password, |
||||
|
TwoFactorProvider: model.twoFactorProvider, |
||||
|
TwoFactorCode: model.code, |
||||
|
}); |
||||
|
} finally { |
||||
|
modalApi.setState({ |
||||
|
confirmLoading: false, |
||||
|
closable: true, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
/** 发送邮件验证代码 */ |
||||
|
async function onSendEmail() { |
||||
|
await form.value?.validateFields('email'); |
||||
|
await onSendCode(() => |
||||
|
sendEmailSigninCodeApi({ |
||||
|
emailAddress: formModel.value.email, |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
/** 发送短信验证代码 */ |
||||
|
async function onSendSms() { |
||||
|
await form.value?.validateFields('phoneNumber'); |
||||
|
await onSendCode(() => |
||||
|
sendPhoneSigninCodeApi({ |
||||
|
phoneNumber: formModel.value.phoneNumber, |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
/** 发送验证代码 */ |
||||
|
async function onSendCode(api: () => Promise<void>) { |
||||
|
try { |
||||
|
sendValidCodeLoading.value = true; |
||||
|
sendValidCodeInternal.value = 60; |
||||
|
await api(); |
||||
|
setInterval(() => { |
||||
|
if (sendValidCodeInternal.value <= 0) { |
||||
|
return; |
||||
|
} |
||||
|
sendValidCodeInternal.value -= 1; |
||||
|
}, 1000); |
||||
|
} catch { |
||||
|
sendValidCodeInternal.value = 0; |
||||
|
} finally { |
||||
|
sendValidCodeLoading.value = false; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal> |
||||
|
<Form |
||||
|
ref="formRef" |
||||
|
:label-col="{ span: 6 }" |
||||
|
:model="formModel" |
||||
|
:wrapper-col="{ span: 18 }" |
||||
|
> |
||||
|
<!-- 选择验证器 --> |
||||
|
<FormItem |
||||
|
:label="$t('abp.oauth.twoFactor.authenticator')" |
||||
|
name="twoFactorProvider" |
||||
|
required |
||||
|
> |
||||
|
<Select |
||||
|
v-model:value="formModel.twoFactorProvider" |
||||
|
:field-names="{ label: 'name', value: 'value' }" |
||||
|
:options="twoFactorProviders" |
||||
|
allow-clear |
||||
|
/> |
||||
|
</FormItem> |
||||
|
<!-- 邮件验证 --> |
||||
|
<template v-if="formModel.twoFactorProvider === 'Email'"> |
||||
|
<FormItem |
||||
|
:label="$t('abp.oauth.twoFactor.emailAddress')" |
||||
|
name="email" |
||||
|
required |
||||
|
> |
||||
|
<Input |
||||
|
v-model:value="formModel.email" |
||||
|
autocomplete="off" |
||||
|
type="email" |
||||
|
/> |
||||
|
</FormItem> |
||||
|
<FormItem :label="$t('abp.oauth.twoFactor.code')" name="code" required> |
||||
|
<div class="flex flex-row gap-4"> |
||||
|
<Input v-model:value="formModel.code" autocomplete="off" /> |
||||
|
<Button |
||||
|
:disabled="getSendValidCodeLoading" |
||||
|
:loading="sendValidCodeLoading" |
||||
|
@click="onSendEmail" |
||||
|
> |
||||
|
{{ getSendValidCodeTitle }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</FormItem> |
||||
|
</template> |
||||
|
<!-- 手机号码验证 --> |
||||
|
<template v-if="formModel.twoFactorProvider === 'Phone'"> |
||||
|
<FormItem |
||||
|
:label="$t('abp.oauth.twoFactor.phoneNumber')" |
||||
|
name="phoneNumber" |
||||
|
required |
||||
|
> |
||||
|
<Input v-model:value="formModel.phoneNumber" autocomplete="off" /> |
||||
|
</FormItem> |
||||
|
<FormItem :label="$t('abp.oauth.twoFactor.code')" name="code" required> |
||||
|
<div class="flex flex-row gap-4"> |
||||
|
<Input v-model:value="formModel.code" autocomplete="off" /> |
||||
|
<Button |
||||
|
:disabled="getSendValidCodeLoading" |
||||
|
:loading="sendValidCodeLoading" |
||||
|
@click="onSendSms" |
||||
|
> |
||||
|
{{ getSendValidCodeTitle }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</FormItem> |
||||
|
</template> |
||||
|
<!-- 身份验证程序验证 --> |
||||
|
<template v-else-if="formModel.twoFactorProvider === 'Authenticator'"> |
||||
|
<FormItem :label="$t('abp.oauth.twoFactor.code')" name="code" required> |
||||
|
<Input v-model:value="formModel.code" autocomplete="off" /> |
||||
|
</FormItem> |
||||
|
</template> |
||||
|
</Form> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -1,3 +1,4 @@ |
|||||
|
export { useAccountApi } from './useAccountApi'; |
||||
export { useProfileApi } from './useProfileApi'; |
export { useProfileApi } from './useProfileApi'; |
||||
export { useTokenApi } from './useTokenApi'; |
export { useTokenApi } from './useTokenApi'; |
||||
export { useUserInfoApi } from './useUserInfoApi'; |
export { useUserInfoApi } from './useUserInfoApi'; |
||||
|
|||||
@ -0,0 +1,63 @@ |
|||||
|
import type { ListResultDto } from '@abp/core'; |
||||
|
|
||||
|
import type { |
||||
|
GetTwoFactorProvidersInput, |
||||
|
SendEmailSigninCodeDto, |
||||
|
SendPhoneSigninCodeDto, |
||||
|
TwoFactorProvider, |
||||
|
} from '../types/account'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useAccountApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
/** |
||||
|
* 获取可用的二次认证验证器 |
||||
|
* @param input 参数 |
||||
|
*/ |
||||
|
function getTwoFactorProvidersApi( |
||||
|
input: GetTwoFactorProvidersInput, |
||||
|
): Promise<ListResultDto<TwoFactorProvider>> { |
||||
|
return request<ListResultDto<TwoFactorProvider>>( |
||||
|
'/api/account/two-factor-providers', |
||||
|
{ |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送登陆验证邮件 |
||||
|
* @param input 参数 |
||||
|
*/ |
||||
|
function sendEmailSigninCodeApi( |
||||
|
input: SendEmailSigninCodeDto, |
||||
|
): Promise<void> { |
||||
|
return request('/api/account/email/send-signin-code', { |
||||
|
data: input, |
||||
|
method: 'POST', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送登陆验证短信 |
||||
|
* @param input 参数 |
||||
|
*/ |
||||
|
function sendPhoneSigninCodeApi( |
||||
|
input: SendPhoneSigninCodeDto, |
||||
|
): Promise<void> { |
||||
|
return request('/api/account/phone/send-signin-code', { |
||||
|
data: input, |
||||
|
method: 'POST', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getTwoFactorProvidersApi, |
||||
|
sendEmailSigninCodeApi, |
||||
|
sendPhoneSigninCodeApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { Form, Input, message } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useProfileApi } from '../../api/useProfileApi'; |
||||
|
|
||||
|
const FormItem = Form.Item; |
||||
|
|
||||
|
interface State { |
||||
|
confirmToken: string; |
||||
|
email: string; |
||||
|
returnUrl?: string; |
||||
|
userId: string; |
||||
|
} |
||||
|
const formModel = ref({} as State); |
||||
|
|
||||
|
const { cancel, confirmEmailApi } = useProfileApi(); |
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
onClosed: cancel, |
||||
|
onConfirm: onSubmit, |
||||
|
onOpenChange(isOpen) { |
||||
|
if (isOpen) { |
||||
|
const state = modalApi.getData<State>(); |
||||
|
formModel.value = state; |
||||
|
} |
||||
|
}, |
||||
|
title: $t('AbpAccount.EmailConfirm'), |
||||
|
}); |
||||
|
async function onSubmit() { |
||||
|
try { |
||||
|
modalApi.setState({ confirmLoading: true }); |
||||
|
await confirmEmailApi({ |
||||
|
confirmToken: encodeURIComponent(formModel.value.confirmToken), |
||||
|
}); |
||||
|
message.success($t('AbpAccount.YourEmailIsSuccessfullyConfirm')); |
||||
|
modalApi.close(); |
||||
|
if (formModel.value.returnUrl) { |
||||
|
window.location.href = formModel.value.returnUrl; |
||||
|
} |
||||
|
} finally { |
||||
|
modalApi.setState({ confirmLoading: false }); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal> |
||||
|
<Form :model="formModel"> |
||||
|
<FormItem :label="$t('AbpAccount.DisplayName:Email')"> |
||||
|
<Input v-model:value="formModel.email" readonly /> |
||||
|
</FormItem> |
||||
|
</Form> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,22 @@ |
|||||
|
import type { NameValue } from '@abp/core'; |
||||
|
|
||||
|
interface GetTwoFactorProvidersInput { |
||||
|
userId: string; |
||||
|
} |
||||
|
|
||||
|
interface SendEmailSigninCodeDto { |
||||
|
emailAddress: string; |
||||
|
} |
||||
|
|
||||
|
interface SendPhoneSigninCodeDto { |
||||
|
phoneNumber: string; |
||||
|
} |
||||
|
|
||||
|
type TwoFactorProvider = NameValue<string>; |
||||
|
|
||||
|
export type { |
||||
|
GetTwoFactorProvidersInput, |
||||
|
SendEmailSigninCodeDto, |
||||
|
SendPhoneSigninCodeDto, |
||||
|
TwoFactorProvider, |
||||
|
}; |
||||
@ -1,3 +1,4 @@ |
|||||
|
export * from './account'; |
||||
export * from './profile'; |
export * from './profile'; |
||||
export * from './token'; |
export * from './token'; |
||||
export * from './user'; |
export * from './user'; |
||||
|
|||||
Loading…
Reference in new issue