管理端个人中心

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 fetchUserInfo = async (): Promise<API.CurrentUser | undefined> => {
const result = await getCurrent().catch(() => undefined); const { result, success } = await getCurrent();
if (result?.success && result.result) { if (success && result) {
return result.result; return result;
} }
return undefined; return undefined;
}; };

View File

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

View File

@ -17,8 +17,6 @@
*/ */
import { FieldNames, ServerExceptionStatus } from '../constant'; import { FieldNames, ServerExceptionStatus } from '../constant';
import { changeEmail, prepareChangeEmail } from '../service'; 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 type { CaptFieldRef, ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, ProFormCaptcha, ProFormText } from '@ant-design/pro-components'; import { ModalForm, ProFormCaptcha, ProFormText } from '@ant-design/pro-components';
import { App, Spin } from 'antd'; import { App, Spin } from 'antd';
@ -94,7 +92,9 @@ export default (props: {
rules={[ rules={[
{ {
required: true, 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={[ rules={[
{ {
required: true, 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', 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) => { onGetCaptcha={async (email) => {
if (!(await formRef.current?.validateFields([FieldNames.PASSWORD]))) { if (!(await formRef.current?.validateFields([FieldNames.PASSWORD]))) {
return Promise.reject(); return Promise.reject();
} }
const publicSecret = await onGetEncryptSecret(); const { success, message, result, status } = await prepareChangeEmail({
if (publicSecret !== undefined) { email: email,
//加密传输 password: formRef.current?.getFieldValue(FieldNames.PASSWORD),
const { success, message, result, status } = await prepareChangeEmail( });
aesEcbEncrypt( if (!success && status === ServerExceptionStatus.PASSWORD_VALIDATED_FAIL_ERROR) {
JSON.stringify({ formRef.current?.setFields([{ name: FieldNames.PASSWORD, errors: [`${message}`] }]);
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();
return Promise.reject(); 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 <ProFormText
label={intl.formatMessage({ id: 'page.user.profile.common.form.code' })} 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} name={FieldNames.OTP}
fieldProps={{ autoComplete: 'off' }} fieldProps={{ autoComplete: 'off' }}
rules={[ 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) * Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import {FieldNames} from '../constant'; import { changePhone, prepareChangePhone } from '../service';
import {changePhone} from '../service'; import { phoneIsValidNumber } from '@/utils/utils';
import type {CaptFieldRef, ProFormInstance} from '@ant-design/pro-components'; import { FormattedMessage } from '@@/plugin-locale/localeExports';
import {ModalForm, ProFormText, useStyle as useAntdStyle,} from '@ant-design/pro-components'; import type { CaptFieldRef, ProFormInstance } from '@ant-design/pro-components';
import {App, ConfigProvider, Spin} from 'antd'; import {
import {omit} from 'lodash'; ModalForm,
import {useContext, useEffect, useRef, useState} from 'react'; ProFormCaptcha,
import {FormLayout} from './constant'; 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 classnames from 'classnames';
import {ConfigContext} from 'antd/es/config-provider'; import { ConfigContext } from 'antd/es/config-provider';
import {useIntl} from '@@/exports'; import { useIntl } from '@@/exports';
import FormPhoneAreaCodeSelect from '@/components/FormPhoneAreaCodeSelect';
import { FieldNames, ServerExceptionStatus } from '../constant';
import * as React from 'react';
function useStyle(prefixCls: string) { function useStyle(prefixCls: string) {
const { getPrefixCls } = useContext(ConfigContext || ConfigProvider.ConfigContext); const { getPrefixCls } = useContext(ConfigContext || ConfigProvider.ConfigContext);
@ -57,8 +67,6 @@ export default (props: {
const captchaRef = useRef<CaptFieldRef>(); const captchaRef = useRef<CaptFieldRef>();
/**已发送验证码*/ /**已发送验证码*/
const [hasSendCaptcha, setHasSendCaptcha] = useState<boolean>(false); const [hasSendCaptcha, setHasSendCaptcha] = useState<boolean>(false);
/**手机区域*/
const [phoneRegion, setPhoneRegion] = useState<string>('86');
const formRef = useRef<ProFormInstance>(); const formRef = useRef<ProFormInstance>();
const { wrapSSR, hashId } = useStyle(prefixCls); const { wrapSSR, hashId } = useStyle(prefixCls);
@ -89,7 +97,9 @@ export default (props: {
}} }}
onFinish={async (formData: Record<string, any>) => { onFinish={async (formData: Record<string, any>) => {
if (!hasSendCaptcha) { 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(); return Promise.reject();
} }
const { success } = await changePhone(omit(formData, FieldNames.PASSWORD)); 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 <ProFormText
placeholder={intl.formatMessage({ id: 'page.user.profile.common.form.code.placeholder' })} placeholder={intl.formatMessage({ id: 'page.user.profile.common.form.code.placeholder' })}
label={intl.formatMessage({ id: 'page.user.profile.common.form.code' })} label={intl.formatMessage({ id: 'page.user.profile.common.form.code' })}

View File

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

View File

@ -16,17 +16,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
export default { export default {
'page.user.profile.menu.base': '基本设置', 'page.user.profile.menu.base': '基础信息',
'page.user.profile.menu.security': '安全设置', 'page.user.profile.menu.security': '安全设置',
'page.user.profile.menu.bind': '账号绑定', 'page.user.profile.menu.bind': '账号绑定',
'page.user.profile.base.avatar_title': '头像', 'page.user.profile.base.avatar_title': '头像',
'page.user.profile.base.avatar_change_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.email': '邮箱',
'page.user.profile.base.form.phone': '手机号', 'page.user.profile.base.form.phone': '手机号',
'page.user.profile.base.form.full_name': '姓名', 'page.user.profile.base.form.full_name': '姓名',
'page.user.profile.base.form.nick_name': '昵称', 'page.user.profile.base.form.nick_name': '昵称',
'page.user.profile.base.form.nick_name.rule.0': '请输入您的昵称', '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': '密码',
'page.user.profile.common.form.password.placeholder': '请输入密码', 'page.user.profile.common.form.password.placeholder': '请输入密码',
'page.user.profile.common.form.password.rule.0': '请输入密码', '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.placeholder': '请输入手机号',
'page.user.profile.common.form.phone.rule.0': '手机号未填写', 'page.user.profile.common.form.phone.rule.0': '手机号未填写',
'page.user.profile.common.form.phone.rule.1': '手机号不合法', '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': '验证码',
'page.user.profile.common.form.code.placeholder': '请输入验证码', '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.verify.placeholder': '输入密码确认身份',
'page.user.profile.bind.totp.form.bind': '绑定动态口令', 'page.user.profile.bind.totp.form.bind': '绑定动态口令',
'page.user.profile.bind.totp.form.bind.placeholder': '使用移动端认证器绑定口令', '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': 'page.user.profile.bind.totp.form.bind.paragraph':
'扫码绑定后,请您输入移动端 APP 中的六位动态口令,完成本次绑定。', '扫码绑定后,请您输入移动端 APP 中的六位动态口令,完成本次绑定。',
'page.user.profile.bind.totp.form.update_email': '修改邮箱', '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 * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ParamCheckType } from '@/constant';
import { request } from '@@/plugin-request/request'; 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`, { return request(`/api/v1/user/profile/prepare_change_phone`, {
data: { encrypt: encrypt }, data: data,
method: 'POST', method: 'POST',
skipErrorHandler: true, skipErrorHandler: true,
}).catch(({ response: { data } }) => { }).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`, { return request(`/api/v1/user/profile/prepare_change_email`, {
data: { encrypt: encrypt }, data: data,
method: 'POST', method: 'POST',
skipErrorHandler: true, skipErrorHandler: true,
}).catch(({ response: { data } }) => { }).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`, { return request(`/api/v1/user/profile/change_password`, {
data: { encrypt: encrypt }, data: data,
method: 'PUT', method: 'PUT',
skipErrorHandler: true, skipErrorHandler: true,
}).catch(({ response: { data } }) => { }).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`, { return request(`/api/v1/user/profile/change_info`, {
data: { encrypt: encrypt }, data: data,
method: 'PUT', 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; secret: string;
}; };
/** /**
* *
*/ */
@ -222,8 +221,8 @@ declare namespace AccountAPI {
appName: string; appName: string;
clientIp: string; clientIp: string;
userAgent: { userAgent: {
platformVersion:string; platformVersion: string;
platform:string platform: string;
}; };
browser: string; browser: string;
eventStatus: 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'); return request<API.ApiResult<API.CurrentUser>>('/api/v1/session/current_user');
} }