23 changed files with 1192 additions and 7 deletions
@ -0,0 +1,15 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
|
|||
import { MyProfile } from '@abp/account'; |
|||
|
|||
defineOptions({ |
|||
name: 'MyProfiles', |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Page> |
|||
<MyProfile /> |
|||
</Page> |
|||
</template> |
|||
@ -0,0 +1,15 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
|
|||
import { MySetting } from '@abp/account'; |
|||
|
|||
defineOptions({ |
|||
name: 'MySettings', |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Page> |
|||
<MySetting /> |
|||
</Page> |
|||
</template> |
|||
@ -1,2 +1,3 @@ |
|||
export { useProfileApi } from './useProfileApi'; |
|||
export { useTokenApi } from './useTokenApi'; |
|||
export { useUserInfoApi } from './useUserInfoApi'; |
|||
|
|||
@ -0,0 +1,117 @@ |
|||
import type { |
|||
AuthenticatorDto, |
|||
AuthenticatorRecoveryCodeDto, |
|||
ChangePasswordInput, |
|||
ProfileDto, |
|||
TwoFactorEnabledDto, |
|||
UpdateProfileDto, |
|||
VerifyAuthenticatorCodeInput, |
|||
} from '../types/profile'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useProfileApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 查询个人信息 |
|||
* @returns 个人信息 |
|||
*/ |
|||
function getApi(): Promise<ProfileDto> { |
|||
return request<ProfileDto>('/api/account/my-profile', { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 更新个人信息 |
|||
* @param input 参数 |
|||
* @returns 个人信息 |
|||
*/ |
|||
function updateApi(input: UpdateProfileDto): Promise<ProfileDto> { |
|||
return request<ProfileDto>('/api/account/my-profile', { |
|||
data: input, |
|||
method: 'PUT', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 修改密码 |
|||
* @param input 参数 |
|||
*/ |
|||
function changePasswordApi(input: ChangePasswordInput): Promise<void> { |
|||
return request('/api/account/my-profile/change-password', { |
|||
data: input, |
|||
method: 'POST', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 获取二次认证启用状态 |
|||
*/ |
|||
function getTwoFactorEnabledApi(): Promise<TwoFactorEnabledDto> { |
|||
return request<TwoFactorEnabledDto>('/api/account/my-profile/two-factor', { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 设置二次认证启用状态 |
|||
*/ |
|||
function changeTwoFactorEnabledApi( |
|||
input: TwoFactorEnabledDto, |
|||
): Promise<void> { |
|||
return request('/api/account/my-profile/change-two-factor', { |
|||
data: input, |
|||
method: 'PUT', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 获取身份验证器配置 |
|||
* @returns 身份验证器 |
|||
*/ |
|||
function getAuthenticatorApi(): Promise<AuthenticatorDto> { |
|||
return request<AuthenticatorDto>('/api/account/my-profile/authenticator', { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 验证身份验证代码 |
|||
* @param input 参数 |
|||
* @returns 重置代码 |
|||
*/ |
|||
function verifyAuthenticatorCodeApi( |
|||
input: VerifyAuthenticatorCodeInput, |
|||
): Promise<AuthenticatorRecoveryCodeDto> { |
|||
return request<AuthenticatorRecoveryCodeDto>( |
|||
'/api/account/my-profile/verify-authenticator-code', |
|||
{ |
|||
data: input, |
|||
method: 'POST', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 重置验证器 |
|||
*/ |
|||
function resetAuthenticatorApi(): Promise<void> { |
|||
return request('/api/account/my-profile/reset-authenticator', { |
|||
method: 'POST', |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
changePasswordApi, |
|||
changeTwoFactorEnabledApi, |
|||
getApi, |
|||
getAuthenticatorApi, |
|||
getTwoFactorEnabledApi, |
|||
resetAuthenticatorApi, |
|||
updateApi, |
|||
verifyAuthenticatorCodeApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,181 @@ |
|||
<script setup lang="ts"> |
|||
import type { UploadChangeParam } from 'ant-design-vue'; |
|||
import type { FileType } from 'ant-design-vue/es/upload/interface'; |
|||
|
|||
import type { ProfileDto } from '../types/profile'; |
|||
|
|||
import { computed, onMounted, reactive, ref, toValue } from 'vue'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useUserStore } from '@vben/stores'; |
|||
|
|||
import { useSettings } from '@abp/core'; |
|||
import { UploadOutlined } from '@ant-design/icons-vue'; |
|||
import { |
|||
Avatar, |
|||
Button, |
|||
Card, |
|||
Form, |
|||
Input, |
|||
Menu, |
|||
message, |
|||
Modal, |
|||
Upload, |
|||
} from 'ant-design-vue'; |
|||
|
|||
import { useProfileApi } from '../api/useProfileApi'; |
|||
|
|||
const FormItem = Form.Item; |
|||
|
|||
const selectedMenuKeys = ref<string[]>(['basic']); |
|||
const formModel = ref({} as ProfileDto); |
|||
const menuItems = reactive([ |
|||
{ |
|||
key: 'basic', |
|||
label: $t('abp.account.settings.basicSettings'), |
|||
}, |
|||
]); |
|||
const userStore = useUserStore(); |
|||
const { getApi, updateApi } = useProfileApi(); |
|||
const { isTrue } = useSettings(); |
|||
|
|||
const avatar = computed(() => { |
|||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; |
|||
}); |
|||
async function onGet() { |
|||
const profile = await getApi(); |
|||
formModel.value = profile; |
|||
} |
|||
function onAvatarChange(_param: UploadChangeParam) { |
|||
console.warn('等待oss模块集成完成...'); |
|||
} |
|||
function onBeforeUpload(_file: FileType) { |
|||
console.warn('等待oss模块集成完成...'); |
|||
return false; |
|||
} |
|||
async function onSubmit() { |
|||
Modal.confirm({ |
|||
centered: true, |
|||
content: $t('AbpAccount.PersonalSettingsSaved'), |
|||
onOk: async () => { |
|||
const profile = await updateApi(toValue(formModel)); |
|||
message.success( |
|||
$t('AbpAccount.PersonalSettingsChangedConfirmationModalTitle'), |
|||
); |
|||
formModel.value = profile; |
|||
}, |
|||
title: $t('AbpUi.AreYouSure'), |
|||
}); |
|||
} |
|||
onMounted(onGet); |
|||
</script> |
|||
|
|||
<template> |
|||
<Card> |
|||
<div class="flex"> |
|||
<div class="basis-1/6"> |
|||
<Menu |
|||
v-model:selected-keys="selectedMenuKeys" |
|||
:items="menuItems" |
|||
mode="inline" |
|||
/> |
|||
</div> |
|||
<div class="basis-5/6"> |
|||
<Card |
|||
v-if="selectedMenuKeys[0] === 'basic'" |
|||
:bordered="false" |
|||
:title="$t('abp.account.settings.basicSettings')" |
|||
> |
|||
<div class="flex flex-row"> |
|||
<div class="basis-2/4"> |
|||
<Form |
|||
:label-col="{ span: 6 }" |
|||
:model="formModel" |
|||
:wrapper-col="{ span: 18 }" |
|||
> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:UserName')" |
|||
name="userName" |
|||
required |
|||
> |
|||
<Input |
|||
v-model:value="formModel.userName" |
|||
:disabled=" |
|||
!isTrue('Abp.Identity.User.IsUserNameUpdateEnabled') |
|||
" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:Email')" |
|||
name="email" |
|||
required |
|||
> |
|||
<Input |
|||
v-model:value="formModel.email" |
|||
:disabled=" |
|||
!isTrue('Abp.Identity.User.IsEmailUpdateEnabled') |
|||
" |
|||
type="email" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:PhoneNumber')" |
|||
name="phoneNumber" |
|||
> |
|||
<Input v-model:value="formModel.phoneNumber" /> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:Surname')" |
|||
name="surname" |
|||
> |
|||
<Input v-model:value="formModel.surname" /> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:Name')" |
|||
name="name" |
|||
> |
|||
<Input v-model:value="formModel.name" /> |
|||
</FormItem> |
|||
<FormItem> |
|||
<div class="flex flex-col items-center"> |
|||
<Button |
|||
style="min-width: 100px" |
|||
type="primary" |
|||
@click="onSubmit" |
|||
> |
|||
{{ $t('AbpUi.Submit') }} |
|||
</Button> |
|||
</div> |
|||
</FormItem> |
|||
</Form> |
|||
</div> |
|||
<div class="basis-2/4"> |
|||
<div class="flex flex-col items-center"> |
|||
<p>{{ $t('AbpUi.ProfilePicture') }}</p> |
|||
<Avatar :size="100"> |
|||
<template #icon> |
|||
<img :src="avatar" alt="" /> |
|||
</template> |
|||
</Avatar> |
|||
<Upload |
|||
:before-upload="onBeforeUpload" |
|||
:file-list="[]" |
|||
name="file" |
|||
@change="onAvatarChange" |
|||
> |
|||
<Button class="mt-4"> |
|||
<UploadOutlined /> |
|||
{{ $t('abp.account.settings.changeAvatar') }} |
|||
</Button> |
|||
</Upload> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,128 @@ |
|||
<script setup lang="ts"> |
|||
import type { ProfileDto, UpdateProfileDto } from '../types/profile'; |
|||
import type { UserInfo } from '../types/user'; |
|||
|
|||
import { computed, onMounted, reactive, ref } from 'vue'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
import { useUserStore } from '@vben/stores'; |
|||
|
|||
import { Card, Menu, message, Modal } from 'ant-design-vue'; |
|||
|
|||
import { useProfileApi } from '../api/useProfileApi'; |
|||
import AuthenticatorSettings from './components/AuthenticatorSettings.vue'; |
|||
import BasicSettings from './components/BasicSettings.vue'; |
|||
import BindSettings from './components/BindSettings.vue'; |
|||
import NoticeSettings from './components/NoticeSettings.vue'; |
|||
import SecuritySettings from './components/SecuritySettings.vue'; |
|||
|
|||
const { getApi, updateApi } = useProfileApi(); |
|||
const userStore = useUserStore(); |
|||
|
|||
const selectedMenuKeys = ref<string[]>(['basic']); |
|||
const myProfile = ref({} as ProfileDto); |
|||
const menuItems = reactive([ |
|||
{ |
|||
key: 'basic', |
|||
label: $t('abp.account.settings.basic.title'), |
|||
}, |
|||
{ |
|||
key: 'security', |
|||
label: $t('abp.account.settings.security.title'), |
|||
}, |
|||
{ |
|||
key: 'bind', |
|||
label: $t('abp.account.settings.bindSettings'), |
|||
}, |
|||
{ |
|||
key: 'notice', |
|||
label: $t('abp.account.settings.noticeSettings'), |
|||
}, |
|||
{ |
|||
key: 'authenticator', |
|||
label: $t('abp.account.settings.authenticatorSettings'), |
|||
}, |
|||
]); |
|||
const getUserInfo = computed((): null | UserInfo => { |
|||
if (!userStore.userInfo) { |
|||
return null; |
|||
} |
|||
return { |
|||
email: userStore.userInfo.email, |
|||
emailVerified: userStore.userInfo.emailVerified, |
|||
name: userStore.userInfo.name, |
|||
phoneNumber: userStore.userInfo.phoneNumber, |
|||
phoneNumberVerified: userStore.userInfo.phoneNumberVerified, |
|||
preferredUsername: userStore.userInfo.username, |
|||
role: userStore.userInfo.roles!, |
|||
sub: userStore.userInfo.userId, |
|||
uniqueName: userStore.userInfo.username, |
|||
}; |
|||
}); |
|||
async function onGetProfile() { |
|||
const profile = await getApi(); |
|||
myProfile.value = profile; |
|||
} |
|||
async function onUpdateProfile(input: UpdateProfileDto) { |
|||
Modal.confirm({ |
|||
centered: true, |
|||
content: $t('AbpAccount.PersonalSettingsSaved'), |
|||
onOk: async () => { |
|||
const profile = await updateApi(input); |
|||
message.success( |
|||
$t('AbpAccount.PersonalSettingsChangedConfirmationModalTitle'), |
|||
); |
|||
myProfile.value = profile; |
|||
}, |
|||
title: $t('AbpUi.AreYouSure'), |
|||
}); |
|||
} |
|||
function onChangePassword() { |
|||
// TODO: onChangePassword 暂时未实现! |
|||
console.warn('onChangePassword 暂时未实现!'); |
|||
} |
|||
function onChangePhoneNumber() { |
|||
// TODO: onChangePhoneNumber 暂时未实现! |
|||
console.warn('onChangePhoneNumber 暂时未实现!'); |
|||
} |
|||
function onValidateEmail() { |
|||
// TODO: onValidateEmail 暂时未实现! |
|||
console.warn('onValidateEmail 暂时未实现!'); |
|||
} |
|||
onMounted(onGetProfile); |
|||
</script> |
|||
|
|||
<template> |
|||
<Card> |
|||
<div class="flex"> |
|||
<div class="basis-1/6"> |
|||
<Menu |
|||
v-model:selected-keys="selectedMenuKeys" |
|||
:items="menuItems" |
|||
mode="inline" |
|||
/> |
|||
</div> |
|||
<div class="basis-5/6"> |
|||
<BasicSettings |
|||
v-if="selectedMenuKeys[0] === 'basic'" |
|||
:profile="myProfile" |
|||
@submit="onUpdateProfile" |
|||
/> |
|||
<BindSettings v-else-if="selectedMenuKeys[0] === 'bind'" /> |
|||
<SecuritySettings |
|||
v-else-if="selectedMenuKeys[0] === 'security'" |
|||
:user-info="getUserInfo" |
|||
@change-password="onChangePassword" |
|||
@change-phone-number="onChangePhoneNumber" |
|||
@validate-email="onValidateEmail" |
|||
/> |
|||
<NoticeSettings v-else-if="selectedMenuKeys[0] === 'notice'" /> |
|||
<AuthenticatorSettings |
|||
v-else-if="selectedMenuKeys[0] === 'authenticator'" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,70 @@ |
|||
<script setup lang="ts"> |
|||
import type { AuthenticatorDto } from '../../types'; |
|||
|
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { Button, Card, List, message, Modal, Skeleton } from 'ant-design-vue'; |
|||
|
|||
import { useProfileApi } from '../../api/useProfileApi'; |
|||
import AuthenticatorSteps from './AuthenticatorSteps.vue'; |
|||
|
|||
const ListItem = List.Item; |
|||
const ListItemMeta = List.Item.Meta; |
|||
|
|||
const { getAuthenticatorApi, resetAuthenticatorApi } = useProfileApi(); |
|||
|
|||
const authenticator = ref<AuthenticatorDto>(); |
|||
async function onGet() { |
|||
const dto = await getAuthenticatorApi(); |
|||
authenticator.value = dto; |
|||
} |
|||
async function onReset() { |
|||
Modal.confirm({ |
|||
centered: true, |
|||
content: $t('AbpAccount.ResetAuthenticatorWarning'), |
|||
iconType: 'warning', |
|||
onOk: async () => { |
|||
await resetAuthenticatorApi(); |
|||
await onGet(); |
|||
message.success($t('AbpAccount.YourAuthenticatorIsSuccessfullyReset')); |
|||
}, |
|||
title: $t('AbpUi.AreYouSure'), |
|||
}); |
|||
} |
|||
onMounted(onGet); |
|||
</script> |
|||
|
|||
<template> |
|||
<Card |
|||
:bordered="false" |
|||
:title="$t('abp.account.settings.authenticatorSettings')" |
|||
> |
|||
<AuthenticatorSteps |
|||
v-if="authenticator?.isAuthenticated === false" |
|||
:authenticator="authenticator" |
|||
@done="onGet" |
|||
/> |
|||
<List v-else-if="authenticator?.isAuthenticated === true"> |
|||
<ListItem> |
|||
<template #extra> |
|||
<Button type="primary" @click="onReset"> |
|||
{{ $t('AbpAccount.ResetAuthenticator') }} |
|||
</Button> |
|||
</template> |
|||
<ListItemMeta |
|||
:description="$t('AbpAccount.ResetAuthenticatorDesc')" |
|||
:title="$t('AbpAccount.ResetAuthenticator')" |
|||
/> |
|||
</ListItem> |
|||
</List> |
|||
<Skeleton v-else /> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.mh-350 { |
|||
min-height: 350px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,210 @@ |
|||
<script setup lang="ts"> |
|||
import type { FormExpose } from 'ant-design-vue/es/form/Form'; |
|||
|
|||
import type { |
|||
AuthenticatorDto, |
|||
VerifyAuthenticatorCodeInput, |
|||
} from '../../types'; |
|||
|
|||
import { computed, reactive, ref, toValue, useTemplateRef } from 'vue'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { useClipboard } from '@vueuse/core'; |
|||
import { useQRCode } from '@vueuse/integrations/useQRCode'; |
|||
import { Button, Card, Form, Input, message, Steps } from 'ant-design-vue'; |
|||
|
|||
import { useProfileApi } from '../../api/useProfileApi'; |
|||
|
|||
const props = defineProps<{ |
|||
authenticator: AuthenticatorDto; |
|||
}>(); |
|||
const emits = defineEmits<{ |
|||
(event: 'done'): void; |
|||
}>(); |
|||
const FormItem = Form.Item; |
|||
|
|||
const { verifyAuthenticatorCodeApi } = useProfileApi(); |
|||
const { copy } = useClipboard({ legacy: true }); |
|||
|
|||
const form = useTemplateRef<FormExpose>('formRef'); |
|||
const validCodeInput = ref({} as VerifyAuthenticatorCodeInput); |
|||
const authenSteps = reactive([ |
|||
{ |
|||
title: $t('AbpAccount.Authenticator'), |
|||
}, |
|||
{ |
|||
title: $t('AbpAccount.ValidAuthenticator'), |
|||
}, |
|||
{ |
|||
title: $t('AbpAccount.RecoveryCode'), |
|||
}, |
|||
]); |
|||
const recoveryCodes = ref<string[]>([]); |
|||
const codeValidated = ref(false); |
|||
const currentStep = ref(0); |
|||
const loading = ref(false); |
|||
const getQrcodeUrl = computed(() => { |
|||
return props.authenticator.authenticatorUri; |
|||
}); |
|||
const qrcode = useQRCode(getQrcodeUrl); |
|||
|
|||
function onPreStep() { |
|||
currentStep.value -= 1; |
|||
} |
|||
function onNextStep() { |
|||
currentStep.value += 1; |
|||
} |
|||
async function onCopy(text?: string) { |
|||
if (!text) { |
|||
return; |
|||
} |
|||
await copy(text); |
|||
message.success($t('AbpUi.CopiedToTheClipboard')); |
|||
} |
|||
async function onValidCode() { |
|||
await form.value?.validate(); |
|||
try { |
|||
loading.value = true; |
|||
const dto = await verifyAuthenticatorCodeApi(toValue(validCodeInput)); |
|||
recoveryCodes.value = dto.recoveryCodes; |
|||
codeValidated.value = true; |
|||
onNextStep(); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Card :bordered="false"> |
|||
<!-- 步骤表单 --> |
|||
<Steps :current="currentStep" :items="authenSteps" /> |
|||
<!-- 步骤一: 扫描二维码 --> |
|||
<Card v-if="currentStep === 0" class="mt-4"> |
|||
<template #title> |
|||
<div class="flex h-16 flex-col justify-center"> |
|||
<span class="text-lg font-normal">{{ |
|||
$t('AbpAccount.Authenticator') |
|||
}}</span> |
|||
<span class="text-sm font-light">{{ |
|||
$t('AbpAccount.AuthenticatorDesc') |
|||
}}</span> |
|||
</div> |
|||
</template> |
|||
<div class="mt-2 flex flex-row"> |
|||
<div class="basis-1/2"> |
|||
<Card |
|||
:title="$t('AbpAccount.Authenticator:UseQrCode')" |
|||
class="mh-350" |
|||
type="inner" |
|||
> |
|||
<div class="flex justify-center"> |
|||
<img :src="qrcode" /> |
|||
</div> |
|||
</Card> |
|||
</div> |
|||
<div class="basis-1/2"> |
|||
<Card |
|||
:title="$t('AbpAccount.Authenticator:InputCode')" |
|||
class="mh-350" |
|||
type="inner" |
|||
> |
|||
<template #extra> |
|||
<Button type="primary" @click="onCopy(authenticator?.sharedKey)"> |
|||
{{ $t('AbpAccount.Authenticator:CopyToClipboard') }} |
|||
</Button> |
|||
</template> |
|||
<div |
|||
class="flex items-center justify-center rounded-lg bg-[#dac6c6]" |
|||
> |
|||
<div class="m-4 text-xl font-bold text-blue-600"> |
|||
{{ authenticator?.sharedKey }} |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
<!-- 步骤二: 验证代码 --> |
|||
<Card v-if="currentStep === 1" class="mt-4"> |
|||
<template #title> |
|||
<div class="flex h-16 flex-col justify-center"> |
|||
<span class="text-lg font-normal">{{ |
|||
$t('AbpAccount.ValidAuthenticator') |
|||
}}</span> |
|||
<span class="text-sm font-light">{{ |
|||
$t('AbpAccount.ValidAuthenticatorDesc') |
|||
}}</span> |
|||
</div> |
|||
</template> |
|||
<div class="flex flex-row"> |
|||
<div class="basis-2/3"> |
|||
<Form ref="formRef" :model="validCodeInput"> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:AuthenticatorCode')" |
|||
name="authenticatorCode" |
|||
required |
|||
> |
|||
<Input v-model:value="validCodeInput.authenticatorCode" /> |
|||
</FormItem> |
|||
</Form> |
|||
</div> |
|||
<div class="ml-4 basis-2/3"> |
|||
<Button :loading="loading" type="primary" @click="onValidCode"> |
|||
{{ $t('AbpAccount.Validation') }} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
<!-- 步骤三: 恢复代码 --> |
|||
<Card v-if="currentStep === 2" class="mt-4"> |
|||
<template #title> |
|||
<div class="flex h-16 flex-col justify-center"> |
|||
<span class="text-lg font-normal">{{ |
|||
$t('AbpAccount.RecoveryCode') |
|||
}}</span> |
|||
<span class="text-sm font-light">{{ |
|||
$t('AbpAccount.RecoveryCodeDesc') |
|||
}}</span> |
|||
</div> |
|||
</template> |
|||
<template #extra> |
|||
<Button type="primary" @click="onCopy(recoveryCodes.join('\r'))"> |
|||
{{ $t('AbpAccount.Authenticator:CopyToClipboard') }} |
|||
</Button> |
|||
</template> |
|||
<div |
|||
class="flex flex-col items-center justify-center rounded-lg bg-[#dac6c6]" |
|||
> |
|||
<div class="m-2 text-xl font-bold text-blue-600"> |
|||
{{ recoveryCodes.slice(0, 5).join('\r\n') }} |
|||
</div> |
|||
<div class="m-2 text-xl font-bold text-blue-600"> |
|||
{{ recoveryCodes.slice(5).join('\r\n') }} |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
<!-- 底部控制按钮 --> |
|||
<template #actions> |
|||
<div class="flex flex-row justify-end gap-2 pr-2"> |
|||
<Button v-if="currentStep > 0 && !codeValidated" @click="onPreStep"> |
|||
{{ $t('AbpAccount.Steps:PreStep') }} |
|||
</Button> |
|||
<Button |
|||
v-if="currentStep < 2" |
|||
:disabled="currentStep === 1 && !codeValidated" |
|||
type="primary" |
|||
@click="onNextStep" |
|||
> |
|||
{{ $t('AbpAccount.Steps:NextStep') }} |
|||
</Button> |
|||
<Button v-if="currentStep === 2" type="primary" @click="emits('done')"> |
|||
{{ $t('AbpAccount.Steps:Done') }} |
|||
</Button> |
|||
</div> |
|||
</template> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,130 @@ |
|||
<script setup lang="ts"> |
|||
import type { UploadChangeParam } from 'ant-design-vue'; |
|||
import type { FileType } from 'ant-design-vue/es/upload/interface'; |
|||
|
|||
import type { ProfileDto, UpdateProfileDto } from '../../types/profile'; |
|||
|
|||
import { computed, ref, toValue, watchEffect } from 'vue'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useUserStore } from '@vben/stores'; |
|||
|
|||
import { useSettings } from '@abp/core'; |
|||
import { UploadOutlined } from '@ant-design/icons-vue'; |
|||
import { Avatar, Button, Card, Form, Input, Upload } from 'ant-design-vue'; |
|||
|
|||
const props = defineProps<{ |
|||
profile: ProfileDto; |
|||
}>(); |
|||
const emits = defineEmits<{ |
|||
(event: 'submit', profile: UpdateProfileDto): void; |
|||
}>(); |
|||
const FormItem = Form.Item; |
|||
|
|||
const formModel = ref({} as ProfileDto); |
|||
|
|||
const userStore = useUserStore(); |
|||
const { isTrue } = useSettings(); |
|||
|
|||
const avatar = computed(() => { |
|||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; |
|||
}); |
|||
function onAvatarChange(_param: UploadChangeParam) { |
|||
// TODO: oss模块集成后完成 |
|||
console.warn('等待oss模块集成完成...'); |
|||
} |
|||
function onBeforeUpload(_file: FileType) { |
|||
console.warn('等待oss模块集成完成...'); |
|||
return false; |
|||
} |
|||
function onSubmit() { |
|||
emits('submit', toValue(formModel)); |
|||
} |
|||
watchEffect(() => { |
|||
formModel.value = { ...props.profile }; |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Card :bordered="false" :title="$t('abp.account.settings.basic.title')"> |
|||
<div class="flex flex-row"> |
|||
<div class="basis-2/4"> |
|||
<Form |
|||
:label-col="{ span: 6 }" |
|||
:model="formModel" |
|||
:wrapper-col="{ span: 18 }" |
|||
> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:UserName')" |
|||
name="userName" |
|||
required |
|||
> |
|||
<Input |
|||
v-model:value="formModel.userName" |
|||
:disabled="!isTrue('Abp.Identity.User.IsUserNameUpdateEnabled')" |
|||
autocomplete="off" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:Email')" |
|||
name="email" |
|||
required |
|||
> |
|||
<Input |
|||
v-model:value="formModel.email" |
|||
:disabled="!isTrue('Abp.Identity.User.IsEmailUpdateEnabled')" |
|||
autocomplete="off" |
|||
type="email" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:PhoneNumber')" |
|||
name="phoneNumber" |
|||
> |
|||
<Input v-model:value="formModel.phoneNumber" autocomplete="off" /> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpAccount.DisplayName:Surname')" |
|||
name="surname" |
|||
> |
|||
<Input v-model:value="formModel.surname" autocomplete="off" /> |
|||
</FormItem> |
|||
<FormItem :label="$t('AbpAccount.DisplayName:Name')" name="name"> |
|||
<Input v-model:value="formModel.name" autocomplete="off" /> |
|||
</FormItem> |
|||
<FormItem> |
|||
<div class="flex flex-col items-center"> |
|||
<Button style="min-width: 100px" type="primary" @click="onSubmit"> |
|||
{{ $t('AbpUi.Submit') }} |
|||
</Button> |
|||
</div> |
|||
</FormItem> |
|||
</Form> |
|||
</div> |
|||
<div class="basis-2/4"> |
|||
<div class="flex flex-col items-center"> |
|||
<p>{{ $t('AbpUi.ProfilePicture') }}</p> |
|||
<Avatar :size="100"> |
|||
<template #icon> |
|||
<img :src="avatar" alt="" /> |
|||
</template> |
|||
</Avatar> |
|||
<Upload |
|||
:before-upload="onBeforeUpload" |
|||
:file-list="[]" |
|||
name="file" |
|||
@change="onAvatarChange" |
|||
> |
|||
<Button class="mt-4"> |
|||
<UploadOutlined /> |
|||
{{ $t('abp.account.settings.changeAvatar') }} |
|||
</Button> |
|||
</Upload> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,11 @@ |
|||
<script setup lang="ts"> |
|||
import { Card, Empty } from 'ant-design-vue'; |
|||
</script> |
|||
|
|||
<template> |
|||
<Card :bordered="false" :title="$t('abp.account.settings.bindSettings')"> |
|||
<Empty /> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,11 @@ |
|||
<script setup lang="ts"> |
|||
import { Card, Empty } from 'ant-design-vue'; |
|||
</script> |
|||
|
|||
<template> |
|||
<Card :bordered="false" :title="$t('abp.account.settings.noticeSettings')"> |
|||
<Empty /> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,131 @@ |
|||
<script setup lang="ts"> |
|||
import type { TwoFactorEnabledDto } from '../../types'; |
|||
import type { UserInfo } from '../../types/user'; |
|||
|
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { Button, Card, List, Switch, Tag } from 'ant-design-vue'; |
|||
|
|||
import { useProfileApi } from '../../api/useProfileApi'; |
|||
|
|||
defineProps<{ |
|||
userInfo: null | UserInfo; |
|||
}>(); |
|||
const emits = defineEmits<{ |
|||
(event: 'changePassword'): void; |
|||
(event: 'changePhoneNumber'): void; |
|||
(event: 'validateEmail'): void; |
|||
}>(); |
|||
const ListItem = List.Item; |
|||
const ListItemMeta = List.Item.Meta; |
|||
|
|||
const { changeTwoFactorEnabledApi, getTwoFactorEnabledApi } = useProfileApi(); |
|||
const twoFactor = ref<TwoFactorEnabledDto>(); |
|||
const loading = ref(false); |
|||
|
|||
async function onGet() { |
|||
const dto = await getTwoFactorEnabledApi(); |
|||
twoFactor.value = dto; |
|||
} |
|||
async function onTwoFactorChange(enabled: boolean) { |
|||
try { |
|||
loading.value = true; |
|||
await changeTwoFactorEnabledApi({ enabled }); |
|||
} catch { |
|||
twoFactor.value!.enabled = !enabled; |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
onMounted(onGet); |
|||
</script> |
|||
|
|||
<template> |
|||
<Card :bordered="false" :title="$t('abp.account.settings.security.title')"> |
|||
<List item-layout="horizontal"> |
|||
<!-- 密码 --> |
|||
<ListItem> |
|||
<template #extra> |
|||
<Button type="link" @click="emits('changePassword')"> |
|||
{{ $t('AbpUi.Edit') }} |
|||
</Button> |
|||
</template> |
|||
<ListItemMeta |
|||
:description="$t('abp.account.settings.security.passwordDesc')" |
|||
> |
|||
<template #title> |
|||
<a href="https://www.antdv.com/">{{ |
|||
$t('abp.account.settings.security.password') |
|||
}}</a> |
|||
</template> |
|||
</ListItemMeta> |
|||
</ListItem> |
|||
<!-- 手机号码 --> |
|||
<ListItem> |
|||
<template #extra> |
|||
<Button type="link" @click="emits('changePhoneNumber')"> |
|||
{{ $t('AbpUi.Edit') }} |
|||
</Button> |
|||
</template> |
|||
<ListItemMeta> |
|||
<template #title> |
|||
{{ $t('abp.account.settings.security.phoneNumber') }} |
|||
</template> |
|||
<template #description> |
|||
{{ userInfo?.phoneNumber }} |
|||
<Tag v-if="userInfo?.phoneNumberVerified" color="success"> |
|||
{{ $t('abp.account.settings.security.verified') }} |
|||
</Tag> |
|||
<Tag v-else color="warning"> |
|||
{{ $t('abp.account.settings.security.unVerified') }} |
|||
</Tag> |
|||
</template> |
|||
</ListItemMeta> |
|||
</ListItem> |
|||
<!-- 邮件 --> |
|||
<ListItem> |
|||
<template #extra> |
|||
<Button |
|||
v-if="userInfo?.email && !userInfo?.emailVerified" |
|||
type="link" |
|||
@click="emits('validateEmail')" |
|||
> |
|||
{{ $t('AbpAccount.ClickToValidation') }} |
|||
</Button> |
|||
</template> |
|||
<ListItemMeta> |
|||
<template #title> |
|||
{{ $t('abp.account.settings.security.email') }} |
|||
</template> |
|||
<template #description> |
|||
{{ userInfo?.email }} |
|||
<Tag v-if="userInfo?.emailVerified" color="success"> |
|||
{{ $t('abp.account.settings.security.verified') }} |
|||
</Tag> |
|||
<Tag v-else color="warning"> |
|||
{{ $t('abp.account.settings.security.unVerified') }} |
|||
</Tag> |
|||
</template> |
|||
</ListItemMeta> |
|||
</ListItem> |
|||
<!-- 二次认证 --> |
|||
<ListItem v-if="twoFactor"> |
|||
<template #extra> |
|||
<Switch |
|||
v-model:checked="twoFactor.enabled" |
|||
:loading="loading" |
|||
@change="(checked) => onTwoFactorChange(Boolean(checked))" |
|||
/> |
|||
</template> |
|||
<ListItemMeta |
|||
:description="$t('AbpAccount.TwoFactor')" |
|||
:title="$t('AbpAccount.TwoFactor')" |
|||
/> |
|||
</ListItem> |
|||
</List> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,2 @@ |
|||
export { default as MyProfile } from './MyProfile.vue'; |
|||
export { default as MySetting } from './MySetting.vue'; |
|||
@ -1,2 +1,3 @@ |
|||
export * from './api'; |
|||
export * from './components'; |
|||
export * from './types'; |
|||
|
|||
@ -1,2 +1,3 @@ |
|||
export * from './profile'; |
|||
export * from './token'; |
|||
export * from './user'; |
|||
|
|||
@ -0,0 +1,66 @@ |
|||
import type { ExtensibleObject, IHasConcurrencyStamp } from '@abp/core'; |
|||
|
|||
interface ProfileDto extends ExtensibleObject, IHasConcurrencyStamp { |
|||
/** 电子邮件 */ |
|||
email: string; |
|||
hasPassword: boolean; |
|||
/** 是否外部用户 */ |
|||
isExternal: boolean; |
|||
/** 名称 */ |
|||
name?: string; |
|||
/** 手机号码 */ |
|||
phoneNumber?: string; |
|||
/** 姓氏 */ |
|||
surname?: string; |
|||
/** 用户名 */ |
|||
userName: string; |
|||
} |
|||
|
|||
interface UpdateProfileDto extends ExtensibleObject, IHasConcurrencyStamp { |
|||
/** 电子邮件 */ |
|||
email: string; |
|||
/** 名称 */ |
|||
name?: string; |
|||
/** 手机号码 */ |
|||
phoneNumber?: string; |
|||
/** 姓氏 */ |
|||
surname?: string; |
|||
/** 用户名 */ |
|||
userName: string; |
|||
} |
|||
|
|||
interface ChangePasswordInput { |
|||
/** 当前密码 */ |
|||
currentPassword: string; |
|||
/** 新密码 */ |
|||
newPassword: string; |
|||
} |
|||
|
|||
interface TwoFactorEnabledDto { |
|||
/** 是否启用二次认证 */ |
|||
enabled: boolean; |
|||
} |
|||
|
|||
interface AuthenticatorDto { |
|||
authenticatorUri: string; |
|||
isAuthenticated: boolean; |
|||
sharedKey: string; |
|||
} |
|||
|
|||
interface VerifyAuthenticatorCodeInput { |
|||
authenticatorCode: string; |
|||
} |
|||
|
|||
interface AuthenticatorRecoveryCodeDto { |
|||
recoveryCodes: string[]; |
|||
} |
|||
|
|||
export type { |
|||
AuthenticatorDto, |
|||
AuthenticatorRecoveryCodeDto, |
|||
ChangePasswordInput, |
|||
ProfileDto, |
|||
TwoFactorEnabledDto, |
|||
UpdateProfileDto, |
|||
VerifyAuthenticatorCodeInput, |
|||
}; |
|||
Loading…
Reference in new issue