diff --git a/ui/api-templates/http-client.ejs b/ui/api-templates/http-client.ejs index 19b8903..6df6482 100644 --- a/ui/api-templates/http-client.ejs +++ b/ui/api-templates/http-client.ejs @@ -77,11 +77,11 @@ export class HttpClient { (err) => { if (err?.response?.status === 401) { if (whitePathnameList.includes(location.pathname)) { - return; + return Promise.reject('尚未登录'); } Message.error('尚未登录') redirectToLogin(); - return + return Promise.reject('尚未登录'); } // 手动取消请求 if (err.code === 'ERR_CANCELED') { diff --git a/ui/src/api/httpClient.ts b/ui/src/api/httpClient.ts index 750955e..26497ca 100644 --- a/ui/src/api/httpClient.ts +++ b/ui/src/api/httpClient.ts @@ -105,11 +105,11 @@ export class HttpClient { (err) => { if (err?.response?.status === 401) { if (whitePathnameList.includes(location.pathname)) { - return; + return Promise.reject("尚未登录"); } Message.error("尚未登录"); redirectToLogin(); - return; + return Promise.reject("尚未登录"); } // 手动取消请求 if (err.code === "ERR_CANCELED") { diff --git a/ui/src/pages/user/setting/index.tsx b/ui/src/pages/user/setting/index.tsx new file mode 100644 index 0000000..5adccc0 --- /dev/null +++ b/ui/src/pages/user/setting/index.tsx @@ -0,0 +1,473 @@ +import React, { useState, useEffect } from 'react'; +import { + Stack, + Box, + Button, + TextField, + Typography, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Divider, +} from '@mui/material'; +import EditIcon from '@mui/icons-material/Edit'; +import PhotoCameraIcon from '@mui/icons-material/PhotoCamera'; +import { message, Modal } from '@c-x/ui'; +import { useForm, Controller } from 'react-hook-form'; +import { useRequest } from 'ahooks'; +import Card from '@/components/card'; +import { FormItem } from '@/components/form'; +import Avatar from '@/components/avatar'; +import { useAuthContext } from '@/hooks/context'; +import { getUserProfile, putUserUpdateProfile } from '@/api/UserManage'; +import { DomainProfileUpdateReq, DomainUser } from '@/api/types'; + +interface PasswordFormData { + oldPassword: string; + newPassword: string; + confirmPassword: string; +} + +interface ProfileFormData { + username: string; + avatar: string; +} + +const UserSetting = () => { + const [user, { setUser, refreshUser }] = useAuthContext(); + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); + const [avatarDialogOpen, setAvatarDialogOpen] = useState(false); + const [avatarUrl, setAvatarUrl] = useState(''); + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(''); + + // 用户信息表单 + const { + control: profileControl, + handleSubmit: handleProfileSubmit, + setValue: setProfileValue, + watch: watchProfile, + } = useForm({ + defaultValues: { + username: '', + avatar: '', + }, + }); + + // 密码修改表单 + const { + control: passwordControl, + handleSubmit: handlePasswordSubmit, + formState: { errors: passwordErrors }, + reset: resetPasswordForm, + watch: watchPassword, + } = useForm({ + defaultValues: { + oldPassword: '', + newPassword: '', + confirmPassword: '', + }, + }); + + // 获取用户信息 + const { loading: profileLoading } = useRequest(getUserProfile, { + onSuccess: (res) => { + if (res) { + setProfileValue('username', res.username || ''); + setProfileValue('avatar', res.avatar_url || ''); + setAvatarUrl(res.avatar_url || ''); + } + }, + }); + + // 重置头像弹窗状态 + const resetAvatarDialog = () => { + setAvatarDialogOpen(false); + setSelectedFile(null); + setPreviewUrl(''); + setAvatarUrl(currentAvatar); + }; + + // 更新用户信息 + const { loading: updateLoading, run: updateProfile } = useRequest( + putUserUpdateProfile, + { + manual: true, + onSuccess: (res) => { + message.success('更新成功'); + if (res) { + setUser(res); + refreshUser(); + } + }, + } + ); + + // 提交用户信息更新 + const onProfileSubmit = (data: ProfileFormData) => { + const params: DomainProfileUpdateReq = { + username: data.username, + }; + + if (data.avatar !== (user as DomainUser)?.avatar_url) { + params.avatar = data.avatar; + } + + updateProfile(params); + }; + + // 提交密码修改 + const onPasswordSubmit = (data: PasswordFormData) => { + const params: DomainProfileUpdateReq = { + old_password: data.oldPassword, + password: data.newPassword, + }; + + updateProfile(params); + setPasswordDialogOpen(false); + resetPasswordForm(); + }; + + // 处理文件选择 + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + // 检查文件类型 + if (!file.type.startsWith('image/')) { + message.error('请选择图片文件'); + return; + } + + // 检查文件大小(限制为2MB) + if (file.size > 2 * 1024 * 1024) { + message.error('图片大小不能超过2MB'); + return; + } + + setSelectedFile(file); + + // 创建预览URL + const reader = new FileReader(); + reader.onload = (e) => { + const result = e.target?.result as string; + setPreviewUrl(result); + setAvatarUrl(result); + }; + reader.readAsDataURL(file); + + setAvatarDialogOpen(true); + }; + + // 头像提交处理 + const handleAvatarSubmit = () => { + if (previewUrl) { + setProfileValue('avatar', previewUrl); + } else { + setProfileValue('avatar', avatarUrl); + } + resetAvatarDialog(); + }; + + // 打开文件选择对话框 + const triggerFileSelect = () => { + const fileInput = document.getElementById( + 'avatar-file-input' + ) as HTMLInputElement; + fileInput?.click(); + }; + + const currentUsername = watchProfile('username'); + const currentAvatar = watchProfile('avatar'); + const newPassword = watchPassword('newPassword'); + + return ( + + {/* 页面标题 */} + + 账户设置 + + + {/* 头像设置 */} + + + 头像 + + + + + + + + + + + 更改头像 + + + 点击头像或相机图标来选择本地图片文件 + + + {/* 隐藏的文件输入控件 */} + + + + + {/* 基本信息 */} + + + 基本信息 + +
+ + + ( + + )} + /> + + + + + + + + + + +
+
+ + {/* 密码设置 */} + + + 密码设置 + + + + + 修改密码 + + + 为了账户安全,建议定期更换密码 + + + + + + + {/* 修改密码弹窗 */} + setPasswordDialogOpen(false)} + width={600} + onOk={handlePasswordSubmit(onPasswordSubmit)} + > +
+ + + ( + + )} + /> + + + + ( + + )} + /> + + + + { + return value === newPassword || '两次输入的密码不一致'; + }, + }} + render={({ field, fieldState }) => ( + + )} + /> + + +
+
+ + {/* 头像设置弹窗 */} + + + + + {selectedFile && ( + + 已选择文件:{selectedFile.name} + + )} + + + + + + + {!selectedFile && ( + setAvatarUrl(e.target.value)} + placeholder='请输入头像图片链接' + helperText='支持 http:// 或 https:// 开头的图片链接' + /> + )} + + +
+ ); +}; + +export default UserSetting; diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 7c29808..240d2f0 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -43,6 +43,7 @@ const UserChat = LazyLoadable(lazy(() => import('@/pages/user/chat'))); const UserCompletion = LazyLoadable( lazy(() => import('@/pages/user/completion')) ); +const UserSetting = LazyLoadable(lazy(() => import('@/pages/user/setting'))); const UserDashboard = LazyLoadable( lazy(() => import('@/pages/user/dashboard')) @@ -108,6 +109,10 @@ const routerConfig = [ path: 'completion', element: , }, + { + path: 'setting', + element: , + }, ], }, {