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 { useTokenApi } from './useTokenApi'; |
||||
export { useUserInfoApi } from './useUserInfoApi'; |
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 './api'; |
||||
|
export * from './components'; |
||||
export * from './types'; |
export * from './types'; |
||||
|
|||||
@ -1,2 +1,3 @@ |
|||||
|
export * from './profile'; |
||||
export * from './token'; |
export * from './token'; |
||||
export * from './user'; |
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