管理端个人中心

pull/65/head
awenes 2023-10-06 17:25:09 +08:00
parent 8823f5c206
commit 9fce81d45d
9 changed files with 190 additions and 115 deletions

View File

@ -53,9 +53,9 @@ const goLogin = () => {
*
*/
const fetchUserInfo = async (): Promise<API.CurrentUser | undefined> => {
const result = await getCurrent().catch(() => undefined);
if (result?.success && result.result) {
return result.result;
const { result, success } = await getCurrent();
if (success && result) {
return result;
}
return undefined;
};

View File

@ -16,17 +16,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { UploadOutlined } from '@ant-design/icons';
import {
ProForm,
ProFormText,
useStyle as useAntdStyle,
} from '@ant-design/pro-components';
import { ProForm, ProFormText, useStyle as useAntdStyle } from '@ant-design/pro-components';
import { App, Avatar, Button, Form, Skeleton, Upload } from 'antd';
import { useState } from 'react';
import { changeBaseInfo } from '../service';
import { aesEcbEncrypt } from '@/utils/aes';
import { onGetEncryptSecret } from '@/utils/utils';
import { useAsyncEffect } from 'ahooks';
import ImgCrop from 'antd-img-crop';
import { uploadFile } from '@/services/upload';
@ -114,7 +108,7 @@ const BaseView = () => {
const useApp = App.useApp();
const { wrapSSR, hashId } = useStyle();
const [loading, setLoading] = useState<boolean>();
const { initialState } = useModel('@@initialState');
const { initialState, setInitialState } = useModel('@@initialState');
const [avatarURL, setAvatarURL] = useState<string | undefined>(initialState?.currentUser?.avatar);
const [name, setName] = useState<string>('');
@ -123,28 +117,22 @@ const BaseView = () => {
if (initialState && initialState.currentUser) {
setAvatarURL(initialState?.currentUser?.avatar);
setName(initialState?.currentUser?.fullName || initialState?.currentUser?.username);
setLoading(false);
setTimeout(async () => {
setLoading(false);
}, 500);
}
}, [initialState]);
const handleFinish = async (values: Record<string, string>) => {
//加密传输
const publicSecret = await onGetEncryptSecret();
if (publicSecret) {
const { success } = await changeBaseInfo(
aesEcbEncrypt(
JSON.stringify({
fullName: values.fullName,
nickName: values.nickName,
personalProfile: values.personalProfile,
avatar: avatarURL,
}),
publicSecret,
),
);
if (success) {
useApp.message.success(intl.formatMessage({ id: 'app.update_success' }));
}
const { success } = await changeBaseInfo({
fullName: values.fullName,
nikeName: values.nikeName,
});
if (success) {
useApp.message.success(intl.formatMessage({ id: 'app.update_success' }));
//获取当前用户信息
const currentUser = await initialState?.fetchUserInfo?.();
await setInitialState((s: any) => ({ ...s, currentUser: currentUser }));
}
};
@ -218,7 +206,7 @@ const BaseView = () => {
return wrapSSR(
<div className={classnames(`${prefixCls}`, hashId)}>
{loading ? (
<Skeleton paragraph={{ rows: 8 }} />
<Skeleton paragraph={{ rows: 8 }} active />
) : (
<>
<div className={classnames(`${prefixCls}-left`, hashId)}>
@ -232,7 +220,9 @@ const BaseView = () => {
return <Form.Item wrapperCol={{ span: 19, offset: 5 }}>{dom}</Form.Item>;
},
searchConfig: {
submitText: intl.formatMessage({ id: 'app.save' }),
submitText: intl.formatMessage({
id: 'page.user.profile.base.form.update_button',
}),
},
resetButtonProps: {
style: {
@ -246,6 +236,12 @@ const BaseView = () => {
}}
requiredMark={false}
>
<ProFormText
width="md"
name="accountId"
readonly
label={intl.formatMessage({ id: 'page.user.profile.base.form.account_id' })}
/>
<ProFormText
width="md"
name="username"

View File

@ -17,8 +17,6 @@
*/
import { FieldNames, ServerExceptionStatus } from '../constant';
import { changeEmail, prepareChangeEmail } from '../service';
import { aesEcbEncrypt } from '@/utils/aes';
import { onGetEncryptSecret } from '@/utils/utils';
import type { CaptFieldRef, ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, ProFormCaptcha, ProFormText } from '@ant-design/pro-components';
import { App, Spin } from 'antd';
@ -94,7 +92,9 @@ export default (props: {
rules={[
{
required: true,
message: intl.formatMessage({ id: 'page.user.profile.common.form.password.rule.0' }),
message: intl.formatMessage({
id: 'page.user.profile.common.form.password.rule.0',
}),
},
]}
/>
@ -110,49 +110,44 @@ export default (props: {
rules={[
{
required: true,
message: intl.formatMessage({ id: 'page.user.profile.modify_email.form.email.rule.0' }),
message: intl.formatMessage({
id: 'page.user.profile.modify_email.form.email.rule.0',
}),
},
{
type: 'email',
message: intl.formatMessage({ id: 'page.user.profile.modify_email.form.email.rule.1' }),
message: intl.formatMessage({
id: 'page.user.profile.modify_email.form.email.rule.1',
}),
},
]}
onGetCaptcha={async (email) => {
if (!(await formRef.current?.validateFields([FieldNames.PASSWORD]))) {
return Promise.reject();
}
const publicSecret = await onGetEncryptSecret();
if (publicSecret !== undefined) {
//加密传输
const { success, message, result, status } = await prepareChangeEmail(
aesEcbEncrypt(
JSON.stringify({
email: email,
password: formRef.current?.getFieldValue(FieldNames.PASSWORD),
}),
publicSecret,
),
);
if (!success && status === ServerExceptionStatus.PASSWORD_VALIDATED_FAIL_ERROR) {
formRef.current?.setFields([
{ name: FieldNames.PASSWORD, errors: [`${message}`] },
]);
return Promise.reject();
}
if (success && result) {
setHasSendCaptcha(true);
useApp.message.success(intl.formatMessage({ id: 'app.send_successfully' }));
return Promise.resolve();
}
useApp.message.error(message);
captchaRef.current?.endTiming();
const { success, message, result, status } = await prepareChangeEmail({
email: email,
password: formRef.current?.getFieldValue(FieldNames.PASSWORD),
});
if (!success && status === ServerExceptionStatus.PASSWORD_VALIDATED_FAIL_ERROR) {
formRef.current?.setFields([{ name: FieldNames.PASSWORD, errors: [`${message}`] }]);
return Promise.reject();
}
if (success && result) {
setHasSendCaptcha(true);
useApp.message.success(intl.formatMessage({ id: 'app.send_successfully' }));
return Promise.resolve();
}
useApp.message.error(message);
captchaRef.current?.endTiming();
return Promise.reject();
}}
/>
<ProFormText
label={intl.formatMessage({ id: 'page.user.profile.common.form.code' })}
placeholder={intl.formatMessage({ id: 'page.user.profile.common.form.code.placeholder' })}
placeholder={intl.formatMessage({
id: 'page.user.profile.common.form.code.placeholder',
})}
name={FieldNames.OTP}
fieldProps={{ autoComplete: 'off' }}
rules={[

View File

@ -1,5 +1,5 @@
/*
* eiam-console - Employee Identity and Access Management
* eiam-portal - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
@ -15,17 +15,27 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {FieldNames} from '../constant';
import {changePhone} from '../service';
import type {CaptFieldRef, ProFormInstance} from '@ant-design/pro-components';
import {ModalForm, ProFormText, useStyle as useAntdStyle,} from '@ant-design/pro-components';
import {App, ConfigProvider, Spin} from 'antd';
import {omit} from 'lodash';
import {useContext, useEffect, useRef, useState} from 'react';
import {FormLayout} from './constant';
import { changePhone, prepareChangePhone } from '../service';
import { phoneIsValidNumber } from '@/utils/utils';
import { FormattedMessage } from '@@/plugin-locale/localeExports';
import type { CaptFieldRef, ProFormInstance } from '@ant-design/pro-components';
import {
ModalForm,
ProFormCaptcha,
ProFormDependency,
ProFormText,
useStyle as useAntdStyle,
} from '@ant-design/pro-components';
import { App, ConfigProvider, Spin } from 'antd';
import { omit } from 'lodash';
import { useContext, useEffect, useRef, useState } from 'react';
import { FormLayout } from './constant';
import classnames from 'classnames';
import {ConfigContext} from 'antd/es/config-provider';
import {useIntl} from '@@/exports';
import { ConfigContext } from 'antd/es/config-provider';
import { useIntl } from '@@/exports';
import FormPhoneAreaCodeSelect from '@/components/FormPhoneAreaCodeSelect';
import { FieldNames, ServerExceptionStatus } from '../constant';
import * as React from 'react';
function useStyle(prefixCls: string) {
const { getPrefixCls } = useContext(ConfigContext || ConfigProvider.ConfigContext);
@ -57,8 +67,6 @@ export default (props: {
const captchaRef = useRef<CaptFieldRef>();
/**已发送验证码*/
const [hasSendCaptcha, setHasSendCaptcha] = useState<boolean>(false);
/**手机区域*/
const [phoneRegion, setPhoneRegion] = useState<string>('86');
const formRef = useRef<ProFormInstance>();
const { wrapSSR, hashId } = useStyle(prefixCls);
@ -89,7 +97,9 @@ export default (props: {
}}
onFinish={async (formData: Record<string, any>) => {
if (!hasSendCaptcha) {
useApp.message.error(intl.formatMessage({ id: 'page.user.profile.please_send_code.message' }));
useApp.message.error(
intl.formatMessage({ id: 'page.user.profile.please_send_code.message' }),
);
return Promise.reject();
}
const { success } = await changePhone(omit(formData, FieldNames.PASSWORD));
@ -118,7 +128,84 @@ export default (props: {
},
]}
/>
<ProFormDependency name={['phoneAreaCode']}>
{({ phoneAreaCode }) => {
return (
<ProFormCaptcha
name={FieldNames.PHONE}
placeholder={intl.formatMessage({
id: 'page.user.profile.common.form.phone.placeholder',
})}
label={intl.formatMessage({ id: 'page.user.profile.common.form.phone' })}
fieldProps={{ autoComplete: 'off' }}
fieldRef={captchaRef}
formItemProps={{ className: classnames(`${prefixCls}-captcha`, hashId) }}
rules={[
{
required: true,
message: <FormattedMessage id={'page.user.profile.common.form.phone.rule.0'} />,
},
{
validator: async (rule, value) => {
if (!value) {
return Promise.resolve();
}
//校验手机号格式
const isValidNumber = await phoneIsValidNumber(value, phoneAreaCode);
if (!isValidNumber) {
return Promise.reject<any>(
new Error(
intl.formatMessage({
id: 'page.user.profile.common.form.phone.rule.1',
}),
),
);
}
},
validateTrigger: ['onBlur'],
},
]}
phoneName={FieldNames.PHONE}
addonBefore={
<FormPhoneAreaCodeSelect
name={'phoneAreaCode'}
showSearch
noStyle
allowClear={false}
style={{ maxWidth: '200px' }}
fieldProps={{
placement: 'bottomLeft',
}}
/>
}
onGetCaptcha={async (mobile) => {
if (!(await formRef.current?.validateFields([FieldNames.PASSWORD]))) {
return Promise.reject();
}
const { success, message, status, result } = await prepareChangePhone({
phone: mobile as string,
phoneRegion: phoneAreaCode,
password: formRef.current?.getFieldValue(FieldNames.PASSWORD),
});
if (!success && status === ServerExceptionStatus.PASSWORD_VALIDATED_FAIL_ERROR) {
formRef.current?.setFields([
{ name: FieldNames.PASSWORD, errors: [`${message}`] },
]);
return Promise.reject();
}
if (success && result) {
setHasSendCaptcha(true);
useApp.message.success(intl.formatMessage({ id: 'app.send_successfully' }));
return Promise.resolve();
}
useApp.message.error(message);
captchaRef.current?.endTiming();
return Promise.reject();
}}
/>
);
}}
</ProFormDependency>
<ProFormText
placeholder={intl.formatMessage({ id: 'page.user.profile.common.form.code.placeholder' })}
label={intl.formatMessage({ id: 'page.user.profile.common.form.code' })}

View File

@ -39,6 +39,8 @@ export enum FieldNames {
PHONE = 'phone',
/**邮箱 */
EMAIL = 'email',
/**旧密码 */
OLD_PASSWORD = 'oldPassword',
/**新密码 */
NEW_PASSWORD = 'newPassword',
/**验证码*/

View File

@ -16,17 +16,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export default {
'page.user.profile.menu.base': '基本设置',
'page.user.profile.menu.base': '基础信息',
'page.user.profile.menu.security': '安全设置',
'page.user.profile.menu.bind': '账号绑定',
'page.user.profile.base.avatar_title': '头像',
'page.user.profile.base.avatar_change_title': '更换头像',
'page.user.profile.base.form.username': '用户名称',
'page.user.profile.base.form.account_id': '账户ID',
'page.user.profile.base.form.username': '用户名',
'page.user.profile.base.form.email': '邮箱',
'page.user.profile.base.form.phone': '手机号',
'page.user.profile.base.form.full_name': '姓名',
'page.user.profile.base.form.nick_name': '昵称',
'page.user.profile.base.form.nick_name.rule.0': '请输入您的昵称',
'page.user.profile.base.form.update_button': '更新',
'page.user.profile.common.form.password': '密码',
'page.user.profile.common.form.password.placeholder': '请输入密码',
'page.user.profile.common.form.password.rule.0': '请输入密码',
@ -35,7 +37,6 @@ export default {
'page.user.profile.common.form.phone.placeholder': '请输入手机号',
'page.user.profile.common.form.phone.rule.0': '手机号未填写',
'page.user.profile.common.form.phone.rule.1': '手机号不合法',
'page.user.profile.common.form.phone.rule.2': '手机号已存在',
'page.user.profile.common.form.code': '验证码',
'page.user.profile.common.form.code.placeholder': '请输入验证码',
@ -50,7 +51,8 @@ export default {
'page.user.profile.bind.totp.form.verify.placeholder': '输入密码确认身份',
'page.user.profile.bind.totp.form.bind': '绑定动态口令',
'page.user.profile.bind.totp.form.bind.placeholder': '使用移动端认证器绑定口令',
'page.user.profile.bind.totp.form.bind.alert': '请使用市面常见认证器 APP扫描下方二维码完成绑定。',
'page.user.profile.bind.totp.form.bind.alert':
'请使用市面常见认证器 APP扫描下方二维码完成绑定。',
'page.user.profile.bind.totp.form.bind.paragraph':
'扫码绑定后,请您输入移动端 APP 中的六位动态口令,完成本次绑定。',
'page.user.profile.bind.totp.form.update_email': '修改邮箱',

View File

@ -15,17 +15,20 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ParamCheckType } from '@/constant';
import { request } from '@@/plugin-request/request';
/**
*
*
* @param encrypt
* @param data
*/
export async function prepareChangePhone(encrypt: string): Promise<API.ApiResult<boolean>> {
export async function prepareChangePhone(data: {
phone: string;
phoneRegion: string;
password: string;
}): Promise<API.ApiResult<boolean>> {
return request(`/api/v1/user/profile/prepare_change_phone`, {
data: { encrypt: encrypt },
data: data,
method: 'POST',
skipErrorHandler: true,
}).catch(({ response: { data } }) => {
@ -48,11 +51,14 @@ export async function changePhone(data: Record<string, string>): Promise<API.Api
/**
*
*
* @param encrypt
* @param data
*/
export async function prepareChangeEmail(encrypt: string): Promise<API.ApiResult<boolean>> {
export async function prepareChangeEmail(data: {
password: string;
email: string;
}): Promise<API.ApiResult<boolean>> {
return request(`/api/v1/user/profile/prepare_change_email`, {
data: { encrypt: encrypt },
data: data,
method: 'POST',
skipErrorHandler: true,
}).catch(({ response: { data } }) => {
@ -75,11 +81,14 @@ export async function changeEmail(data: Record<string, string>): Promise<API.Api
/**
*
*
* @param encrypt
* @param data
*/
export async function changePassword(encrypt: string): Promise<API.ApiResult<boolean>> {
export async function changePassword(data: {
oldPassword: string;
newPassword: string;
}): Promise<API.ApiResult<boolean>> {
return request(`/api/v1/user/profile/change_password`, {
data: { encrypt: encrypt },
data: data,
method: 'PUT',
skipErrorHandler: true,
}).catch(({ response: { data } }) => {
@ -90,32 +99,17 @@ export async function changePassword(encrypt: string): Promise<API.ApiResult<boo
/**
*
*
* @param encrypt
* @param data
*/
export async function changeBaseInfo(encrypt: string): Promise<API.ApiResult<boolean>> {
export async function changeBaseInfo(
data: Record<string, string | undefined>,
): Promise<API.ApiResult<boolean>> {
return request(`/api/v1/user/profile/change_info`, {
data: { encrypt: encrypt },
data: data,
method: 'PUT',
});
}
/**
*
*
* @param type
* @param value
* @param id
*/
export async function userParamCheck(
type: ParamCheckType,
value: string,
id?: string,
): Promise<API.ApiResult<boolean>> {
return request(`/api/v1/user/param_check`, {
params: { id, type, value },
method: 'GET',
});
}
/**
*

View File

@ -67,7 +67,6 @@ declare namespace API {
secret: string;
};
/**
*
*/
@ -222,8 +221,8 @@ declare namespace AccountAPI {
appName: string;
clientIp: string;
userAgent: {
platformVersion:string;
platform:string
platformVersion: string;
platform: string;
};
browser: string;
eventStatus: string;

View File

@ -20,6 +20,6 @@ import { request } from '@umijs/max';
/**
*
*/
export async function getCurrent(): Promise<API.ApiResult<API.CurrentUser> | undefined> {
export async function getCurrent(): Promise<API.ApiResult<API.CurrentUser>> {
return request<API.ApiResult<API.CurrentUser>>('/api/v1/session/current_user');
}