diff --git a/src/assets/css/constants.scss b/src/assets/css/constants.scss index 923b3e5..aa21c9f 100644 --- a/src/assets/css/constants.scss +++ b/src/assets/css/constants.scss @@ -10,6 +10,7 @@ $background-color: #F5F5F5; $font-main-color: #4D4D4D; $font-secondary-color: #9E9E9E; $focus-color: #DDDDDD; +$divide-color: rgba(204, 204, 204, 0.66); $border-color: rgba(204, 204, 204, 0.33); $url-color: rgba(102, 102, 102, .8); $url-active-color: #ccc; diff --git a/src/assets/css/pages/user/index.scss b/src/assets/css/pages/user/index.scss new file mode 100644 index 0000000..ca80775 --- /dev/null +++ b/src/assets/css/pages/user/index.scss @@ -0,0 +1,135 @@ +@use '@/assets/css/constants' as constants; + +[data-component=user] .root-content { + padding: { + top: 80px; + left: 30px; + right: 30px; + bottom: 30px; + }; + + .card-box { + width: 100%; + height: 100%; + overflow: visible; + align-items: center; + min-width: 900px; + padding-bottom: 20px; + + > :not(:first-child) { + padding: { + left: 60px; + right: 60px; + }; + } + + .divide { + height: 1px; + width: calc(100% - 120px); + background-color: constants.$divide-color; + margin: { + left: 60px; + right: 60px; + }; + } + + .info { + margin-left: 40px; + transform: translateY(-40px); + + > * { + flex: 0 0 auto; + } + + .avatar-box { + background-color: white; + padding: 4px; + border-radius: 50%; + box-shadow: 5px 5px 15px 0 rgba(0, 0, 0, 0.1); + + .avatar { + background-color: transparent !important; + } + } + + .info-name { + margin: { + top: 20px; + left: 24px; + }; + justify-content: center; + + > * { + flex: 0 0 auto; + } + + .nickname { + font-size: 2.4em; + font-weight: bolder; + color: constants.$production-color; + } + + .url { + > span { + margin-left: 8px; + } + } + } + } + + .title { + align-items: center; + + .content { + padding: { + bottom: 30px; + }; + justify-content: space-between; + align-items: center; + width: 100%; + + > * { + flex: 0 0 auto; + } + + .text { + font-size: 1.6em; + font-weight: bolder; + } + + .operation { + gap: 10px; + } + } + } + + .table { + gap: 24px; + + padding: { + top: 30px; + bottom: 20px; + }; + + .row { + > * { + flex: 0 0 auto; + } + + .label { + font-size: 1.4em; + font-weight: bolder; + width: 400px; + } + + .input { + width: 400px; + + > * { + width: 100%; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/assets/svg/share.svg b/src/assets/svg/share.svg new file mode 100644 index 0000000..f3dfe23 --- /dev/null +++ b/src/assets/svg/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index e543e28..8562706 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -59,6 +59,11 @@ interface TokenVo { token: string } +interface UserInfoUpdateParam { + avatar?: string + nickname?: string +} + interface RegisterParam { username: string email: string @@ -89,6 +94,11 @@ interface LoginParam { captchaCode: string } +interface UserChangePasswordParam { + originalPassword: string + newPassword: string +} + interface UserWithInfoVo { id: string username: string @@ -243,7 +253,7 @@ interface UserAddEditParam { groupIds: number[] } -interface UserChangePasswordParam { +interface UserUpdatePasswordParam { id: string password: string credentialsExpiration?: string diff --git a/src/pages/System/User.tsx b/src/pages/System/User.tsx index 05a0255..d4a4ea2 100644 --- a/src/pages/System/User.tsx +++ b/src/pages/System/User.tsx @@ -30,7 +30,7 @@ import HideScrollbar from '@/components/common/HideScrollbar' import FlexBox from '@/components/common/FlexBox' import Card from '@/components/common/Card' -interface ChangePasswordFields extends UserChangePasswordParam { +interface ChangePasswordFields extends UserUpdatePasswordParam { passwordConfirm: string needChangePassword: boolean } @@ -200,7 +200,7 @@ const User = () => { style={{ color: COLOR_PRODUCTION }} onClick={handleOnChangePasswordBtnClick(record)} > - 修改密码 + 更改密码 diff --git a/src/pages/User/index.tsx b/src/pages/User/index.tsx index aa61947..cf8147a 100644 --- a/src/pages/User/index.tsx +++ b/src/pages/User/index.tsx @@ -1,5 +1,394 @@ +import Icon from '@ant-design/icons' +import '@/assets/css/pages/user/index.scss' +import { + COLOR_BACKGROUND, + COLOR_ERROR, + COLOR_PRODUCTION, + DATABASE_UPDATE_SUCCESS, + PERMISSION_ACCESS_DENIED, + PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR +} from '@/constants/common.constants' +import { utcToLocalTime } from '@/util/datetime' +import { getUserInfo, removeToken } from '@/util/auth' +import { r_sys_user_info_change_password, r_sys_user_info_update } from '@/services/system' +import { r_api_avatar_random_base64 } from '@/services/api/avatar' +import FitFullscreen from '@/components/common/FitFullscreen' +import Card from '@/components/common/Card' +import FlexBox from '@/components/common/FlexBox' +import HideScrollbar from '@/components/common/HideScrollbar' + +interface ChangePasswordFields extends UserUpdatePasswordParam { + newPasswordConfirm: string +} + const User = () => { - return <>> + const [modal, contextHolder] = AntdModal.useModal() + const [form] = AntdForm.useForm() + const formValues = AntdForm.useWatch([], form) + const [isLoading, setIsLoading] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isSubmittable, setIsSubmittable] = useState(false) + const [isGettingAvatar, setIsGettingAvatar] = useState(false) + const [avatar, setAvatar] = useState('') + const [userWithPowerInfoVo, setUserWithPowerInfoVo] = useState() + const [changePasswordForm] = AntdForm.useForm() + + const handleOnReset = () => { + getProfile() + } + + const handleOnSave = () => { + if (isSubmitting) { + return + } + setIsSubmitting(true) + void message.loading({ content: '保存中', key: 'LOADING', duration: 0 }) + + void r_sys_user_info_update({ avatar, nickname: formValues.nickname }) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + void message.success('保存成功') + getProfile() + break + default: + void message.error('保存失败,请稍后重试') + } + }) + .finally(() => { + setIsSubmitting(false) + void message.destroy('LOADING') + }) + } + + const handleOnChangeAvatar = () => { + if (isLoading || isGettingAvatar) { + return + } + setIsGettingAvatar(true) + void r_api_avatar_random_base64() + .then((res) => { + const response = res.data + if (response.success) { + response.data?.base64 && setAvatar(response.data.base64) + } + }) + .finally(() => { + setIsGettingAvatar(false) + }) + } + + const handleOnChangePassword = () => { + changePasswordForm.resetFields() + + void modal.confirm({ + icon: <>>, + title: ( + <> + + 修改密码 + > + ), + maskClosable: true, + content: ( + + + + + + + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) { + return Promise.resolve() + } + return Promise.reject(new Error('两次密码输入必须一致')) + } + }) + ]} + > + + + + ), + onOk: () => + changePasswordForm.validateFields().then( + () => { + return new Promise((resolve, reject) => { + void r_sys_user_info_change_password({ + originalPassword: changePasswordForm.getFieldValue( + 'originalPassword' + ) as string, + newPassword: changePasswordForm.getFieldValue( + 'newPassword' + ) as string + }).then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + void message.success('密码修改成功,请重新登录') + removeToken() + notification.info({ + message: '已退出登录', + icon: ( + + ) + }) + setTimeout(() => { + window.location.reload() + }, 1500) + resolve() + break + case PERMISSION_ACCESS_DENIED: + void message.error('拒绝访问') + resolve() + break + case PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR: + void message.warning('原密码错误,请重新输入') + reject(response.msg) + break + default: + void message.error('出错了,请稍后重试') + resolve() + } + }) + }) + }, + () => { + return new Promise((_, reject) => { + reject('输入有误') + }) + } + ) + }) + } + + const getProfile = () => { + if (isLoading) { + return + } + setIsLoading(true) + + void getUserInfo(true) + .then((userWithPowerInfoVo) => { + setAvatar(userWithPowerInfoVo.userInfo.avatar) + form.setFieldValue('nickname', userWithPowerInfoVo.userInfo.nickname) + setUserWithPowerInfoVo(userWithPowerInfoVo) + }) + .finally(() => { + setIsLoading(false) + }) + } + + useEffect(() => { + form.validateFields({ validateOnly: true }).then( + () => { + setIsSubmittable(true) + }, + () => { + setIsSubmittable(false) + } + ) + }, [formValues]) + + useEffect(() => { + getProfile() + }, []) + + return ( + <> + + + + + + + + } + size={144} + style={{ + background: COLOR_BACKGROUND, + cursor: 'pointer' + }} + className={'avatar'} + onClick={handleOnChangeAvatar} + /> + + + + + {userWithPowerInfoVo?.userInfo.nickname} + + + {userWithPowerInfoVo?.username && + new URL( + `/view/${userWithPowerInfoVo.username}`, + location.href + ).href} + + + + + + + 档案管理 + + + 重置 + + + 保存 + + + + + + + + 昵称 + + + + + + + + + + 用户名 + + + + + + 邮箱 + + + + + + 注册时间 + + + + + + + + + 上次登录 IP + + + + + + 上次登录时间 + + + + + + 密码 + + + + + + + + + + + + + + {contextHolder} + > + ) } export default User diff --git a/src/services/system.tsx b/src/services/system.tsx index 616fa42..d5dd6af 100644 --- a/src/services/system.tsx +++ b/src/services/system.tsx @@ -24,7 +24,13 @@ import { } from '@/constants/urls.constants' import request from '@/services/index' -export const r_sys_user_info = () => request.get(URL_SYS_USER_INFO) +export const r_sys_user_info_get = () => request.get(URL_SYS_USER_INFO) + +export const r_sys_user_info_update = (param: UserInfoUpdateParam) => + request.patch(URL_SYS_USER_INFO, param) + +export const r_sys_user_info_change_password = (param: UserChangePasswordParam) => + request.post(URL_SYS_USER_INFO, param) export const r_sys_user_get = (param: UserGetParam) => request.get>(URL_SYS_USER, param) @@ -33,7 +39,7 @@ export const r_sys_user_add = (param: UserAddEditParam) => request.post(URL_SYS_ export const r_sys_user_update = (param: UserAddEditParam) => request.put(URL_SYS_USER, param) -export const r_sys_user_change_password = (param: UserChangePasswordParam) => +export const r_sys_user_change_password = (param: UserUpdatePasswordParam) => request.patch(URL_SYS_USER, param) export const r_sys_user_delete = (id: string) => request.delete(`${URL_SYS_USER}/${id}`) diff --git a/src/util/auth.tsx b/src/util/auth.tsx index 7ec4687..fab8f1c 100644 --- a/src/util/auth.tsx +++ b/src/util/auth.tsx @@ -7,7 +7,7 @@ import { import { floorNumber, randomColor, randomFloat, randomInt } from '@/util/common' import { getLocalStorage, removeLocalStorage, setLocalStorage } from '@/util/browser' import { getFullTitle } from '@/util/route' -import { r_sys_user_info } from '@/services/system' +import { r_sys_user_info_get } from '@/services/system' let captcha: Captcha @@ -90,7 +90,7 @@ export const getUserInfo = async (force = false): Promise = export const requestUserInfo = async () => { let user: UserWithPowerInfoVo | null - await r_sys_user_info().then((value) => { + await r_sys_user_info_get().then((value) => { const response = value.data if (response.code === DATABASE_SELECT_SUCCESS) { user = response.data