| @@ -37,8 +37,7 @@ export async function getInitialState(): Promise<GlobalInitialState> { | |||
| ...response.user, | |||
| avatar: response.user.avatar || require('@/assets/img/avatar-default.png'), | |||
| permissions: response.permissions, | |||
| roles: response.roles, | |||
| roleNames: response.user.roles, | |||
| roleNames: response.roles, | |||
| } as API.CurrentUser; | |||
| } catch (error) { | |||
| console.error('getInitialState', error); | |||
| @@ -1,4 +1,5 @@ | |||
| import { clearSessionToken } from '@/access'; | |||
| import DefaultAvatar from '@/assets/img/avatar-default.png'; | |||
| import { getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||
| import { setRemoteMenu } from '@/services/session'; | |||
| import { logout } from '@/services/system/auth'; | |||
| @@ -56,7 +57,14 @@ const AvatarLogo = () => { | |||
| }, | |||
| }; | |||
| }); | |||
| return <Avatar size="small" className={avatarClassName} src={currentUser?.avatar} alt="avatar" />; | |||
| return ( | |||
| <Avatar | |||
| size="small" | |||
| className={avatarClassName} | |||
| src={currentUser?.avatar || DefaultAvatar} | |||
| alt="avatar" | |||
| /> | |||
| ); | |||
| }; | |||
| const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { | |||
| @@ -1,91 +1,107 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { updateUserProfile } from '@/services/system/user'; | |||
| import { ProForm, ProFormRadio, ProFormText } from '@ant-design/pro-components'; | |||
| import { FormattedMessage, useIntl } from '@umijs/max'; | |||
| import { Form, message, Row } from 'antd'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Form, Input, message, Radio } from 'antd'; | |||
| import React from 'react'; | |||
| export type BaseInfoProps = { | |||
| values: Partial<API.CurrentUser> | undefined; | |||
| values: Partial<API.CurrentUser>; | |||
| open: boolean; | |||
| onFinished?: (isSuccess: boolean) => void; | |||
| }; | |||
| const BaseInfo: React.FC<BaseInfoProps> = (props) => { | |||
| const BaseInfo: React.FC<BaseInfoProps> = ({ open, onFinished, values: initialValues }) => { | |||
| const [form] = Form.useForm(); | |||
| const intl = useIntl(); | |||
| const handleFinish = async (values: Record<string, any>) => { | |||
| const data = { ...props.values, ...values } as API.CurrentUser; | |||
| const resp = await updateUserProfile(data); | |||
| if (resp.code === 200) { | |||
| const handleFinish = async (formData: Record<string, any>) => { | |||
| const data = { userId: initialValues.userId, ...formData } as API.CurrentUser; | |||
| const [res] = await to(updateUserProfile(data)); | |||
| if (res) { | |||
| message.success('修改成功'); | |||
| } else { | |||
| message.warning(resp.msg); | |||
| onFinished?.(true); | |||
| } | |||
| }; | |||
| return ( | |||
| <> | |||
| <ProForm form={form} onFinish={handleFinish} initialValues={props.values}> | |||
| <Row> | |||
| <ProFormText | |||
| name="nickName" | |||
| label={intl.formatMessage({ | |||
| id: 'system.user.nick_name', | |||
| defaultMessage: '用户昵称', | |||
| })} | |||
| width="xl" | |||
| placeholder="请输入用户昵称" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: ( | |||
| <FormattedMessage id="请输入用户昵称!" defaultMessage="请输入用户昵称!" /> | |||
| ), | |||
| }, | |||
| ]} | |||
| /> | |||
| </Row> | |||
| <Row> | |||
| <ProFormText | |||
| name="phonenumber" | |||
| label={intl.formatMessage({ | |||
| id: 'system.user.phonenumber', | |||
| defaultMessage: '手机号码', | |||
| })} | |||
| width="xl" | |||
| placeholder="请输入手机号码" | |||
| rules={[ | |||
| { | |||
| required: false, | |||
| message: ( | |||
| <FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" /> | |||
| ), | |||
| }, | |||
| ]} | |||
| /> | |||
| </Row> | |||
| <Row> | |||
| <ProFormText | |||
| name="email" | |||
| label={intl.formatMessage({ | |||
| id: 'system.user.email', | |||
| defaultMessage: '邮箱', | |||
| })} | |||
| width="xl" | |||
| placeholder="请输入邮箱" | |||
| rules={[ | |||
| { | |||
| type: 'email', | |||
| message: '无效的邮箱地址!', | |||
| }, | |||
| { | |||
| required: false, | |||
| message: <FormattedMessage id="请输入邮箱!" defaultMessage="请输入邮箱!" />, | |||
| }, | |||
| ]} | |||
| /> | |||
| </Row> | |||
| <Row> | |||
| <ProFormRadio.Group | |||
| <KFModal | |||
| width={800} | |||
| title="修改基本信息" | |||
| open={open} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'basic-info-form', | |||
| }} | |||
| onCancel={() => onFinished?.(false)} | |||
| destroyOnClose | |||
| > | |||
| <Form | |||
| name="basic-info-form" | |||
| form={form} | |||
| layout="vertical" | |||
| size="large" | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| initialValues={initialValues} | |||
| onFinish={handleFinish} | |||
| > | |||
| <Form.Item | |||
| name="nickName" | |||
| label="用户昵称" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入用户昵称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入用户昵称" allowClear></Input> | |||
| </Form.Item> | |||
| <Form.Item | |||
| name="phonenumber" | |||
| label="手机号码" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入手机号码', | |||
| }, | |||
| { | |||
| pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, | |||
| message: '请输入正确的手机号码', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入手机号码" allowClear></Input> | |||
| </Form.Item> | |||
| <Form.Item | |||
| name="email" | |||
| label="邮箱" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入邮箱', | |||
| }, | |||
| { | |||
| type: 'email', | |||
| message: '请输入正确的邮箱地址', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入邮箱" allowClear></Input> | |||
| </Form.Item> | |||
| <Form.Item | |||
| name="sex" | |||
| label="性别" | |||
| rules={[ | |||
| { | |||
| required: false, | |||
| message: '请选择性别', | |||
| }, | |||
| ]} | |||
| > | |||
| <Radio.Group | |||
| options={[ | |||
| { | |||
| label: '男', | |||
| @@ -96,22 +112,10 @@ const BaseInfo: React.FC<BaseInfoProps> = (props) => { | |||
| value: '1', | |||
| }, | |||
| ]} | |||
| name="sex" | |||
| label={intl.formatMessage({ | |||
| id: 'system.user.sex', | |||
| defaultMessage: 'sex', | |||
| })} | |||
| width="xl" | |||
| rules={[ | |||
| { | |||
| required: false, | |||
| message: <FormattedMessage id="请输入性别!" defaultMessage="请输入性别!" />, | |||
| }, | |||
| ]} | |||
| /> | |||
| </Row> | |||
| </ProForm> | |||
| </> | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| }; | |||
| @@ -1,81 +1,89 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { updateUserPwd } from '@/services/system/user'; | |||
| import { ProForm, ProFormText } from '@ant-design/pro-components'; | |||
| import { FormattedMessage, useIntl } from '@umijs/max'; | |||
| import { Form, message } from 'antd'; | |||
| import React from 'react'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Form, Input, message } from 'antd'; | |||
| const ResetPassword: React.FC = () => { | |||
| export type ResetPasswordProps = { | |||
| open: boolean; | |||
| onFinished?: (isSuccess: boolean) => void; | |||
| }; | |||
| const ResetPassword = ({ open, onFinished }: ResetPasswordProps) => { | |||
| const [form] = Form.useForm(); | |||
| const intl = useIntl(); | |||
| const handleFinish = async (values: Record<string, any>) => { | |||
| const resp = await updateUserPwd(values.oldPassword, values.newPassword); | |||
| if (resp.code === 200) { | |||
| message.success('密码重置成功。'); | |||
| } else { | |||
| message.warning(resp.msg); | |||
| const [res] = await to(updateUserPwd(values.oldPassword, values.newPassword)); | |||
| if (res) { | |||
| message.success('密码重置成功'); | |||
| onFinished?.(true); | |||
| } | |||
| }; | |||
| const checkPassword = (_rule: any, value: string) => { | |||
| const login_password = form.getFieldValue('newPassword'); | |||
| if (value === login_password) { | |||
| return Promise.resolve(); | |||
| if (!value) { | |||
| return Promise.reject(new Error('请输入确认密码')); | |||
| } else if (value !== login_password) { | |||
| return Promise.reject(new Error('两次密码输入不一致')); | |||
| } | |||
| return Promise.reject(new Error('两次密码输入不一致')); | |||
| return Promise.resolve(); | |||
| }; | |||
| return ( | |||
| <> | |||
| <ProForm form={form} onFinish={handleFinish}> | |||
| <ProFormText.Password | |||
| <KFModal | |||
| width={800} | |||
| title="重置密码" | |||
| open={open} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'reset-pwd-form', | |||
| }} | |||
| onCancel={() => onFinished?.(false)} | |||
| destroyOnClose | |||
| > | |||
| <Form | |||
| form={form} | |||
| name="reset-pwd-form" | |||
| layout="vertical" | |||
| size="large" | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| onFinish={handleFinish} | |||
| > | |||
| <Form.Item | |||
| name="oldPassword" | |||
| label={intl.formatMessage({ | |||
| id: 'system.user.old_password', | |||
| defaultMessage: '旧密码', | |||
| })} | |||
| width="xl" | |||
| placeholder="请输入旧密码" | |||
| label="旧密码" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: <FormattedMessage id="请输入旧密码!" defaultMessage="请输入旧密码!" />, | |||
| message: '请输入旧密码', | |||
| }, | |||
| ]} | |||
| /> | |||
| <ProFormText.Password | |||
| > | |||
| <Input.Password placeholder="请输入旧密码" allowClear></Input.Password> | |||
| </Form.Item> | |||
| <Form.Item | |||
| name="newPassword" | |||
| label={intl.formatMessage({ | |||
| id: 'system.user.new_password', | |||
| defaultMessage: '新密码', | |||
| })} | |||
| width="xl" | |||
| placeholder="请输入新密码" | |||
| label="新密码" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: <FormattedMessage id="请输入新密码!" defaultMessage="请输入新密码!" />, | |||
| message: '请输入新密码', | |||
| }, | |||
| ]} | |||
| /> | |||
| <ProFormText.Password | |||
| > | |||
| <Input.Password placeholder="请输入新密码" allowClear></Input.Password> | |||
| </Form.Item> | |||
| <Form.Item | |||
| name="confirmPassword" | |||
| label={intl.formatMessage({ | |||
| id: 'system.user.confirm_password', | |||
| defaultMessage: '确认密码', | |||
| })} | |||
| width="xl" | |||
| placeholder="请输入确认密码" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: <FormattedMessage id="请输入确认密码!" defaultMessage="请输入确认密码!" />, | |||
| }, | |||
| { validator: checkPassword }, | |||
| ]} | |||
| /> | |||
| </ProForm> | |||
| </> | |||
| label="确认密码" | |||
| required | |||
| rules={[{ validator: checkPassword }]} | |||
| > | |||
| <Input.Password placeholder="请输入确认密码" allowClear></Input.Password> | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| }; | |||
| @@ -2,7 +2,7 @@ | |||
| position: relative; | |||
| display: inline-block; | |||
| height: 120px; | |||
| margin-bottom: 16px; | |||
| margin-bottom: 30px; | |||
| text-align: center; | |||
| & > img { | |||
| @@ -30,31 +30,34 @@ | |||
| } | |||
| } | |||
| .teamTitle { | |||
| margin-bottom: 12px; | |||
| color: @heading-color; | |||
| font-weight: 500; | |||
| } | |||
| .user-center { | |||
| height: calc(100% - 50px - 120px); | |||
| padding: 30px; | |||
| background: white; | |||
| border-radius: 8px; | |||
| width: 50%; | |||
| margin: 60px auto 0; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| overflow-y: auto; | |||
| .team { | |||
| :global { | |||
| .ant-avatar { | |||
| margin-right: 12px; | |||
| .ant-list { | |||
| width: 100%; | |||
| .ant-list-item { | |||
| height: 80px; | |||
| border-block-end: 1px solid rgba(5, 5, 5, 0.06); | |||
| font-size: 16px; | |||
| } | |||
| } | |||
| } | |||
| a { | |||
| display: block; | |||
| margin-bottom: 24px; | |||
| overflow: hidden; | |||
| color: @text-color; | |||
| white-space: nowrap; | |||
| text-overflow: ellipsis; | |||
| word-break: break-all; | |||
| transition: color 0.3s; | |||
| &:hover { | |||
| color: @primary-color; | |||
| } | |||
| &__buttons { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-top: 60px; | |||
| flex-direction: row; | |||
| } | |||
| } | |||
| @@ -1,6 +1,9 @@ | |||
| import { getUserInfo } from '@/services/session'; | |||
| import DefaultAvatar from '@/assets/img/avatar-default.png'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { to } from '@/utils/promise'; | |||
| import { | |||
| ClusterOutlined, | |||
| HeartOutlined, | |||
| MailOutlined, | |||
| ManOutlined, | |||
| MobileOutlined, | |||
| @@ -8,45 +11,52 @@ import { | |||
| UserOutlined, | |||
| } from '@ant-design/icons'; | |||
| import { PageLoading } from '@ant-design/pro-components'; | |||
| import { useRequest } from '@umijs/max'; | |||
| import { Card, Col, Divider, List, Row } from 'antd'; | |||
| import React, { useState } from 'react'; | |||
| import styles from './Center.less'; | |||
| import { useModel } from '@umijs/max'; | |||
| import { List } from 'antd'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { flushSync } from 'react-dom'; | |||
| import AvatarCropper from './components/AvatarCropper'; | |||
| import BaseInfo from './components/BaseInfo'; | |||
| import ResetPassword from './components/ResetPassword'; | |||
| import BaseInfoModal from './components/BaseInfo'; | |||
| import ResetPasswordModal from './components/ResetPassword'; | |||
| import styles from './index.less'; | |||
| const operationTabList = [ | |||
| { | |||
| key: 'base', | |||
| tab: <span>基本资料</span>, | |||
| }, | |||
| { | |||
| key: 'password', | |||
| tab: <span>重置密码</span>, | |||
| }, | |||
| ]; | |||
| const Center = () => { | |||
| const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false); | |||
| const [infoModalOpen, setInfoModalOpen] = useState<boolean>(false); | |||
| const [resetModalOpen, setRestModalOpen] = useState<boolean>(false); | |||
| export type tabKeyType = 'base' | 'password'; | |||
| const { initialState, setInitialState } = useModel('@@initialState'); | |||
| const { currentUser, fetchUserInfo } = initialState || {}; | |||
| const Center: React.FC = () => { | |||
| const [tabKey, setTabKey] = useState<tabKeyType>('base'); | |||
| const refreshUserInfo = useCallback(async () => { | |||
| if (fetchUserInfo) { | |||
| const [res] = await to(fetchUserInfo()); | |||
| if (res) { | |||
| flushSync(() => { | |||
| setInitialState((s) => ({ ...s, currentUser: res })); | |||
| }); | |||
| } | |||
| } | |||
| }, [setInitialState, fetchUserInfo]); | |||
| const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false); | |||
| const handleBaseInfoChange = (success: boolean) => { | |||
| setInfoModalOpen(false); | |||
| // 获取用户信息 | |||
| const { data: userInfo, loading } = useRequest(async () => { | |||
| return { data: await getUserInfo() }; | |||
| }); | |||
| if (loading) { | |||
| return <div>loading...</div>; | |||
| } | |||
| if (success) { | |||
| refreshUserInfo(); | |||
| } | |||
| }; | |||
| const currentUser = userInfo?.user; | |||
| const handleResetPassword = (success: boolean) => { | |||
| setRestModalOpen(false); | |||
| if (success) { | |||
| } | |||
| }; | |||
| // 渲染用户信息 | |||
| const renderUserInfo = ({ | |||
| userName, | |||
| nickName, | |||
| phonenumber, | |||
| email, | |||
| sex, | |||
| @@ -65,6 +75,17 @@ const Center: React.FC = () => { | |||
| </div> | |||
| <div>{userName}</div> | |||
| </List.Item> | |||
| <List.Item> | |||
| <div> | |||
| <HeartOutlined | |||
| style={{ | |||
| marginRight: 8, | |||
| }} | |||
| /> | |||
| 昵称 | |||
| </div> | |||
| <div>{nickName}</div> | |||
| </List.Item> | |||
| <List.Item> | |||
| <div> | |||
| <ManOutlined | |||
| @@ -109,75 +130,53 @@ const Center: React.FC = () => { | |||
| </div> | |||
| <div>{dept?.deptName}</div> | |||
| </List.Item> | |||
| <List.Item> | |||
| <div> | |||
| <TeamOutlined | |||
| style={{ | |||
| marginRight: 8, | |||
| }} | |||
| /> | |||
| 角色 | |||
| </div> | |||
| <div>{currentUser?.roles?.map((item: any) => item.roleName)?.join(',')}</div> | |||
| </List.Item> | |||
| </List> | |||
| ); | |||
| }; | |||
| // 渲染tab切换 | |||
| const renderChildrenByTabKey = (tabValue: tabKeyType) => { | |||
| if (tabValue === 'base') { | |||
| return <BaseInfo values={currentUser} />; | |||
| } | |||
| if (tabValue === 'password') { | |||
| return <ResetPassword />; | |||
| } | |||
| return null; | |||
| }; | |||
| if (!currentUser) { | |||
| return <PageLoading />; | |||
| } | |||
| return ( | |||
| <div> | |||
| <Row gutter={[16, 24]}> | |||
| <Col lg={8} md={24}> | |||
| <Card title="个人信息" bordered={false} loading={loading}> | |||
| {!loading && ( | |||
| <div style={{ textAlign: 'center' }}> | |||
| <div | |||
| className={styles.avatarHolder} | |||
| onClick={() => { | |||
| setCropperModalOpen(true); | |||
| }} | |||
| > | |||
| <img src={currentUser.avatar} draggable={false} alt="" /> | |||
| </div> | |||
| {renderUserInfo(currentUser)} | |||
| <Divider dashed /> | |||
| <div className={styles.team}> | |||
| <div className={styles.teamTitle}>角色</div> | |||
| <Row gutter={36}> | |||
| {currentUser.roles && | |||
| currentUser.roles.map((item: any) => ( | |||
| <Col key={item.roleId} lg={24} xl={12}> | |||
| <TeamOutlined | |||
| style={{ | |||
| marginRight: 8, | |||
| }} | |||
| /> | |||
| {item.roleName} | |||
| </Col> | |||
| ))} | |||
| </Row> | |||
| </div> | |||
| </div> | |||
| )} | |||
| </Card> | |||
| </Col> | |||
| <Col lg={16} md={24}> | |||
| <Card | |||
| bordered={false} | |||
| tabList={operationTabList} | |||
| activeTabKey={tabKey} | |||
| onTabChange={(_tabKey: string) => { | |||
| setTabKey(_tabKey as tabKeyType); | |||
| }} | |||
| <div style={{ height: '100%' }}> | |||
| <PageTitle title="个人中心"></PageTitle> | |||
| <div className={styles['user-center']}> | |||
| <div | |||
| className={styles.avatarHolder} | |||
| onClick={() => { | |||
| setCropperModalOpen(true); | |||
| }} | |||
| > | |||
| <img src={currentUser.avatar || DefaultAvatar} draggable={false} alt="" /> | |||
| </div> | |||
| {renderUserInfo(currentUser)} | |||
| {/* <div className={styles['user-center__buttons']}> | |||
| <Button | |||
| type="primary" | |||
| size="large" | |||
| style={{ marginRight: 50 }} | |||
| onClick={() => setInfoModalOpen(true)} | |||
| > | |||
| {renderChildrenByTabKey(tabKey)} | |||
| </Card> | |||
| </Col> | |||
| </Row> | |||
| 修改基本信息 | |||
| </Button> | |||
| <Button type="primary" size="large" onClick={() => setRestModalOpen(true)}> | |||
| 重置密码 | |||
| </Button> | |||
| </div> */} | |||
| </div> | |||
| <AvatarCropper | |||
| onFinished={() => { | |||
| setCropperModalOpen(false); | |||
| @@ -185,6 +184,17 @@ const Center: React.FC = () => { | |||
| open={cropperModalOpen} | |||
| data={currentUser.avatar} | |||
| /> | |||
| <BaseInfoModal | |||
| open={infoModalOpen} | |||
| values={currentUser} | |||
| onFinished={handleBaseInfoChange} | |||
| ></BaseInfoModal> | |||
| <ResetPasswordModal | |||
| open={resetModalOpen} | |||
| onFinished={handleResetPassword} | |||
| ></ResetPasswordModal> | |||
| </div> | |||
| ); | |||
| }; | |||
| @@ -31,7 +31,7 @@ function UserSpace({ users = [] }: UserSpaceProps) { | |||
| } | |||
| ></Avatar> | |||
| <div className={styles['user-space__name']}>{currentUser?.nickName}</div> | |||
| <div className={styles['user-space__role']}>{currentUser?.roleNames?.[0]?.roleName}</div> | |||
| <div className={styles['user-space__role']}>{currentUser?.roles?.[0]?.roleName}</div> | |||
| <Divider | |||
| dashed | |||
| style={{ borderColor: 'rgba(22, 100, 255, 0.19)', margin: '20px 0' }} | |||