Browse Source

Merge pull request #1078 from colinin/user-settings

 feat: 增加个人设置功能.
pull/1091/head
yx lin 1 year ago
committed by GitHub
parent
commit
18981ba55a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 22
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  2. 23
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  3. 23
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  4. 20
      apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts
  5. 12
      apps/vben5/apps/app-antd/src/store/auth.ts
  6. 15
      apps/vben5/apps/app-antd/src/views/account/my-profile/index.vue
  7. 15
      apps/vben5/apps/app-antd/src/views/account/my-settings/index.vue
  8. 7
      apps/vben5/packages/@abp/account/package.json
  9. 1
      apps/vben5/packages/@abp/account/src/api/index.ts
  10. 117
      apps/vben5/packages/@abp/account/src/api/useProfileApi.ts
  11. 181
      apps/vben5/packages/@abp/account/src/components/MyProfile.vue
  12. 128
      apps/vben5/packages/@abp/account/src/components/MySetting.vue
  13. 70
      apps/vben5/packages/@abp/account/src/components/components/AuthenticatorSettings.vue
  14. 210
      apps/vben5/packages/@abp/account/src/components/components/AuthenticatorSteps.vue
  15. 130
      apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue
  16. 11
      apps/vben5/packages/@abp/account/src/components/components/BindSettings.vue
  17. 11
      apps/vben5/packages/@abp/account/src/components/components/NoticeSettings.vue
  18. 131
      apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue
  19. 2
      apps/vben5/packages/@abp/account/src/components/index.ts
  20. 1
      apps/vben5/packages/@abp/account/src/index.ts
  21. 1
      apps/vben5/packages/@abp/account/src/types/index.ts
  22. 66
      apps/vben5/packages/@abp/account/src/types/profile.ts
  23. 2
      apps/vben5/packages/@abp/account/src/types/user.ts

22
apps/vben5/apps/app-antd/src/layouts/basic.vue

@ -2,11 +2,17 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import {
BookOpenText,
CircleHelp,
createIconifyIcon,
MdiGithub,
} from '@vben/icons';
import {
BasicLayout,
LockScreen,
@ -21,6 +27,8 @@ import { $t } from '#/locales';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const UserSettingsIcon = createIconifyIcon('tdesign:user-setting');
const notifications = ref<NotificationItem[]>([
{
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
@ -52,6 +60,7 @@ const notifications = ref<NotificationItem[]>([
},
]);
const { replace } = useRouter();
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
@ -61,6 +70,13 @@ const showDot = computed(() =>
);
const menus = computed(() => [
{
handler: () => {
replace('/account/my-settings');
},
icon: UserSettingsIcon,
text: $t('abp.account.settings.title'),
},
{
handler: () => {
openWindow(VBEN_DOC_URL, {
@ -127,10 +143,10 @@ watch(
<template #user-dropdown>
<UserDropdown
:avatar
:description="userStore.userInfo?.email"
:menus
:tag-text="userStore.userInfo?.username"
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
/>
</template>

23
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -28,5 +28,28 @@
"authorizations": "Authorizations",
"scopes": "Scopes",
"tokens": "Tokens"
},
"account": {
"title": "Account",
"settings": {
"title": "My Settings",
"basic": {
"title": "Basic Settings"
},
"security": {
"title": "Security Settings",
"verified": "Verified",
"unVerified": "Not Verified",
"email": "Email",
"password": "Password",
"passwordDesc": "Reset My Password",
"phoneNumber": "PhoneNumber"
},
"bindSettings": "Bind Settings",
"noticeSettings": "Notice Settings",
"authenticatorSettings": "Authenticator Settings",
"changeAvatar": "Change Avatar"
},
"profile": "My Profile"
}
}

23
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -28,5 +28,28 @@
"authorizations": "授权管理",
"scopes": "范围管理",
"tokens": "授权令牌"
},
"account": {
"title": "账户管理",
"settings": {
"title": "个人设置",
"basic": {
"title": "基本设置"
},
"security": {
"title": "安全设置",
"verified": "已验证",
"unVerified": "未验证",
"email": "电子邮件",
"password": "密码",
"passwordDesc": "重置我的密码",
"phoneNumber": "手机号码"
},
"bindSettings": "账号绑定",
"noticeSettings": "新消息通知",
"authenticatorSettings": "身份验证程序",
"changeAvatar": "更改头像"
},
"profile": "个人中心"
}
}

20
apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts

@ -198,6 +198,26 @@ const routes: RouteRecordRaw[] = [
},
],
},
{
name: 'Account',
path: '/account',
meta: {
title: $t('abp.account.title'),
icon: 'mdi:account-outline',
hideInMenu: true,
},
children: [
{
meta: {
title: $t('abp.account.settings.title'),
icon: 'tdesign:user-setting',
},
name: 'MySettings',
path: '/account/my-settings',
component: () => import('#/views/account/my-settings/index.vue'),
},
],
},
],
},
];

12
apps/vben5/apps/app-antd/src/store/auth.ts

@ -99,12 +99,18 @@ export const useAuthStore = defineStore('auth', () => {
const userInfoRes = await getUserInfoApi();
const abpConfig = await getConfigApi();
userInfo = {
userId: userInfoRes.sub,
username: userInfoRes.uniqueName,
realName: userInfoRes.name,
userId: userInfoRes.sub ?? abpConfig.currentUser.id,
username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName,
realName: userInfoRes.name ?? abpConfig.currentUser.name,
avatar: userInfoRes.avatarUrl ?? userInfoRes.picture,
desc: userInfoRes.uniqueName ?? userInfoRes.name,
email: userInfoRes.email ?? userInfoRes.email,
emailVerified:
userInfoRes.emailVerified ?? abpConfig.currentUser.emailVerified,
phoneNumber: userInfoRes.phoneNumber ?? abpConfig.currentUser.phoneNumber,
phoneNumberVerified:
userInfoRes.phoneNumberVerified ??
abpConfig.currentUser.phoneNumberVerified,
token: '',
roles: abpConfig.currentUser.roles,
homePath: '/',

15
apps/vben5/apps/app-antd/src/views/account/my-profile/index.vue

@ -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>

15
apps/vben5/apps/app-antd/src/views/account/my-settings/index.vue

@ -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>

7
apps/vben5/packages/@abp/account/package.json

@ -23,9 +23,16 @@
"@abp/core": "workspace:*",
"@abp/request": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",
"@vben-core/shadcn-ui": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/stores": "workspace:*",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"ant-design-vue": "catalog:",
"vue": "catalog:*"
}

1
apps/vben5/packages/@abp/account/src/api/index.ts

@ -1,2 +1,3 @@
export { useProfileApi } from './useProfileApi';
export { useTokenApi } from './useTokenApi';
export { useUserInfoApi } from './useUserInfoApi';

117
apps/vben5/packages/@abp/account/src/api/useProfileApi.ts

@ -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,
};
}

181
apps/vben5/packages/@abp/account/src/components/MyProfile.vue

@ -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>

128
apps/vben5/packages/@abp/account/src/components/MySetting.vue

@ -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>

70
apps/vben5/packages/@abp/account/src/components/components/AuthenticatorSettings.vue

@ -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>

210
apps/vben5/packages/@abp/account/src/components/components/AuthenticatorSteps.vue

@ -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>

130
apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue

@ -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>

11
apps/vben5/packages/@abp/account/src/components/components/BindSettings.vue

@ -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>

11
apps/vben5/packages/@abp/account/src/components/components/NoticeSettings.vue

@ -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>

131
apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue

@ -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>

2
apps/vben5/packages/@abp/account/src/components/index.ts

@ -0,0 +1,2 @@
export { default as MyProfile } from './MyProfile.vue';
export { default as MySetting } from './MySetting.vue';

1
apps/vben5/packages/@abp/account/src/index.ts

@ -1,2 +1,3 @@
export * from './api';
export * from './components';
export * from './types';

1
apps/vben5/packages/@abp/account/src/types/index.ts

@ -1,2 +1,3 @@
export * from './profile';
export * from './token';
export * from './user';

66
apps/vben5/packages/@abp/account/src/types/profile.ts

@ -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,
};

2
apps/vben5/packages/@abp/account/src/types/user.ts

@ -11,7 +11,7 @@ interface UserInfo {
/**
*
*/
givenName: string;
givenName?: string;
/**
*
*/

Loading…
Cancel
Save