From 1694f32e8ecd44d552d9377cfa4521f79fbd0291 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Tue, 21 Oct 2025 20:11:07 +0800 Subject: [PATCH] Fix: Profile page UI adjustment #9869 (#10706) ### What problem does this PR solve? Fix: Profile page UI adjustment ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- web/src/components/avatar-upload.tsx | 18 +- web/src/components/ui/form.tsx | 6 +- web/src/components/ui/modal/modal.tsx | 3 +- web/src/locales/en.ts | 4 +- web/src/locales/zh.ts | 2 +- .../profile/hooks/use-profile.ts | 151 ++++ .../pages/profile-setting/profile/index.tsx | 745 +++++++++--------- 7 files changed, 524 insertions(+), 405 deletions(-) create mode 100644 web/src/pages/profile-setting/profile/hooks/use-profile.ts diff --git a/web/src/components/avatar-upload.tsx b/web/src/components/avatar-upload.tsx index 0071ba36a..7a85e08de 100644 --- a/web/src/components/avatar-upload.tsx +++ b/web/src/components/avatar-upload.tsx @@ -1,5 +1,5 @@ import { transformFile2Base64 } from '@/utils/file-util'; -import { Pencil, Upload, XIcon } from 'lucide-react'; +import { Pencil, Plus, XIcon } from 'lucide-react'; import { ChangeEventHandler, forwardRef, @@ -12,10 +12,14 @@ import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; import { Button } from './ui/button'; import { Input } from './ui/input'; -type AvatarUploadProps = { value?: string; onChange?: (value: string) => void }; +type AvatarUploadProps = { + value?: string; + onChange?: (value: string) => void; + tips?: string; +}; export const AvatarUpload = forwardRef( - function AvatarUpload({ value, onChange }, ref) { + function AvatarUpload({ value, onChange, tips }, ref) { const { t } = useTranslation(); const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 @@ -47,9 +51,9 @@ export const AvatarUpload = forwardRef(
{!avatarBase64Str ? ( -
+
- +

{t('common.upload')}

@@ -86,8 +90,8 @@ export const AvatarUpload = forwardRef( ref={ref} />
-
- {t('knowledgeConfiguration.photoTip')} +
+ {tips ?? t('knowledgeConfiguration.photoTip')}
); diff --git a/web/src/components/ui/form.tsx b/web/src/components/ui/form.tsx index a35030542..e0ca30845 100644 --- a/web/src/components/ui/form.tsx +++ b/web/src/components/ui/form.tsx @@ -106,8 +106,10 @@ const FormLabel = React.forwardRef< htmlFor={formItemId} {...props} > - {required && *} - {props.children} +
+ {required && *} + {props.children} +
{tooltip && } ); diff --git a/web/src/components/ui/modal/modal.tsx b/web/src/components/ui/modal/modal.tsx index 2e9e004f9..9d0cf0ec4 100644 --- a/web/src/components/ui/modal/modal.tsx +++ b/web/src/components/ui/modal/modal.tsx @@ -140,6 +140,7 @@ const Modal: ModalType = ({
); }, [ + disabled, footer, cancelText, t, @@ -158,7 +159,7 @@ const Modal: ModalType = ({ onClick={() => maskClosable && onOpenChange?.(false)} > e.stopPropagation()} > {/* title */} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index c5e5de483..980407b89 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -137,7 +137,7 @@ export default { completed: 'Completed', datasetLog: 'Dataset Log', created: 'Created', - learnMore: 'Learn More', + learnMore: 'Built-in pipeline introduction', general: 'General', chunkMethodTab: 'Chunk Method', testResults: 'Test Results', @@ -697,7 +697,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s system: 'System', logout: 'Log out', api: 'API', - username: 'Username', + username: 'Name', usernameMessage: 'Please input your username!', photo: 'Your photo', photoDescription: 'This will be displayed on your profile.', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index e5c117521..303e01083 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -125,7 +125,7 @@ export default { completed: '已完成', datasetLog: '知识库日志', created: '创建于', - learnMore: '了解更多', + learnMore: '内置pipeline简介', general: '通用', chunkMethodTab: '切片方法', testResults: '测试结果', diff --git a/web/src/pages/profile-setting/profile/hooks/use-profile.ts b/web/src/pages/profile-setting/profile/hooks/use-profile.ts new file mode 100644 index 000000000..5b8bdf7b5 --- /dev/null +++ b/web/src/pages/profile-setting/profile/hooks/use-profile.ts @@ -0,0 +1,151 @@ +// src/hooks/useProfile.ts +import { + useFetchUserInfo, + useSaveSetting, +} from '@/hooks/use-user-setting-request'; +import { rsaPsw } from '@/utils'; +import { useCallback, useEffect, useState } from 'react'; + +interface ProfileData { + userName: string; + timeZone: string; + currPasswd?: string; + newPasswd?: string; + avatar: string; + email: string; + confirmPasswd?: string; +} + +export const EditType = { + editName: 'editName', + editTimeZone: 'editTimeZone', + editPassword: 'editPassword', +} as const; + +export type IEditType = keyof typeof EditType; + +export const modalTitle = { + [EditType.editName]: 'Edit Name', + [EditType.editTimeZone]: 'Edit Time Zone', + [EditType.editPassword]: 'Edit Password', +} as const; + +export const useProfile = () => { + const { data: userInfo } = useFetchUserInfo(); + const [profile, setProfile] = useState({ + userName: '', + avatar: '', + timeZone: '', + email: '', + currPasswd: '', + }); + + const [editType, setEditType] = useState(EditType.editName); + const [isEditing, setIsEditing] = useState(false); + const [editForm, setEditForm] = useState>({}); + const { + saveSetting, + loading: submitLoading, + data: saveSettingData, + } = useSaveSetting(); + + useEffect(() => { + // form.setValue('currPasswd', ''); // current password + const profile = { + userName: userInfo.nickname, + timeZone: userInfo.timezone, + avatar: userInfo.avatar || '', + email: userInfo.email, + currPasswd: userInfo.password, + }; + setProfile(profile); + }, [userInfo, setProfile]); + + useEffect(() => { + if (saveSettingData === 0) { + setIsEditing(false); + setEditForm({}); + } + }, [saveSettingData]); + const onSubmit = (newProfile: ProfileData) => { + const payload: Partial<{ + nickname: string; + password: string; + new_password: string; + avatar: string; + timezone: string; + }> = { + nickname: newProfile.userName, + avatar: newProfile.avatar, + timezone: newProfile.timeZone, + }; + + if ( + 'currPasswd' in newProfile && + 'newPasswd' in newProfile && + newProfile.currPasswd && + newProfile.newPasswd + ) { + payload.password = rsaPsw(newProfile.currPasswd!) as string; + payload.new_password = rsaPsw(newProfile.newPasswd!) as string; + } + console.log('payload', payload); + if (editType === EditType.editName && payload.nickname) { + saveSetting({ nickname: payload.nickname }); + setProfile(newProfile); + } + if (editType === EditType.editTimeZone && payload.timezone) { + saveSetting({ timezone: payload.timezone }); + setProfile(newProfile); + } + if (editType === EditType.editPassword && payload.password) { + saveSetting({ + password: payload.password, + new_password: payload.new_password, + }); + setProfile(newProfile); + } + // saveSetting(payload); + }; + + const handleEditClick = useCallback( + (type: IEditType) => { + setEditForm(profile); + setEditType(type); + setIsEditing(true); + }, + [profile], + ); + + const handleCancel = useCallback(() => { + setIsEditing(false); + setEditForm({}); + }, []); + + const handleSave = (data: ProfileData) => { + console.log('handleSave', data); + const newProfile = { ...profile, ...data }; + + onSubmit(newProfile); + // setIsEditing(false); + // setEditForm({}); + }; + + const handleAvatarUpload = (avatar: string) => { + setProfile((prev) => ({ ...prev, avatar })); + saveSetting({ avatar }); + }; + + return { + profile, + setProfile, + submitLoading: submitLoading, + isEditing, + editType, + editForm, + handleEditClick, + handleCancel, + handleSave, + handleAvatarUpload, + }; +}; diff --git a/web/src/pages/profile-setting/profile/index.tsx b/web/src/pages/profile-setting/profile/index.tsx index d494e9c48..9b6b0b4c4 100644 --- a/web/src/pages/profile-setting/profile/index.tsx +++ b/web/src/pages/profile-setting/profile/index.tsx @@ -1,5 +1,6 @@ +// src/components/ProfilePage.tsx +import { AvatarUpload } from '@/components/avatar-upload'; import PasswordInput from '@/components/originui/password-input'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { Form, @@ -10,6 +11,7 @@ import { FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; +import { Modal } from '@/components/ui/modal/modal'; import { Select, SelectContent, @@ -18,434 +20,393 @@ import { SelectValue, } from '@/components/ui/select'; import { useTranslate } from '@/hooks/common-hooks'; -import { useFetchUserInfo, useSaveSetting } from '@/hooks/user-setting-hooks'; import { TimezoneList } from '@/pages/user-setting/constants'; -import { rsaPsw } from '@/utils'; -import { transformFile2Base64 } from '@/utils/file-util'; import { zodResolver } from '@hookform/resolvers/zod'; -import { TFunction } from 'i18next'; -import { Loader2Icon, Pencil, Upload } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { t } from 'i18next'; +import { Loader2Icon, PenLine } from 'lucide-react'; +import { FC, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -function defineSchema( - t: TFunction<'translation', string>, - showPasswordForm = false, -) { - const baseSchema = z.object({ - userName: z - .string() - .min(1, { message: t('usernameMessage') }) - .trim(), - avatarUrl: z.string().trim(), - timeZone: z - .string() - .trim() - .min(1, { message: t('timezonePlaceholder') }), - email: z - .string({ required_error: 'Please select an email to display.' }) - .trim() - .regex(/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/, { - message: 'Enter a valid email address.', - }), - }); +import { EditType, modalTitle, useProfile } from './hooks/use-profile'; - if (showPasswordForm) { - return baseSchema - .extend({ - currPasswd: z - .string({ - required_error: t('currentPasswordMessage'), - }) - .trim() - .min(1, { message: t('currentPasswordMessage') }), - newPasswd: z - .string({ - required_error: t('confirmPasswordMessage'), - }) - .trim() - .min(8, { message: t('confirmPasswordMessage') }), - confirmPasswd: z - .string({ - required_error: t('newPasswordDescription'), - }) - .trim() - .min(8, { message: t('newPasswordDescription') }), +const baseSchema = z.object({ + userName: z + .string() + .min(1, { message: t('setting.usernameMessage') }) + .trim(), + timeZone: z + .string() + .trim() + .min(1, { message: t('setting.timezonePlaceholder') }), +}); + +const nameSchema = baseSchema.extend({ + currPasswd: z.string().optional(), + newPasswd: z.string().optional(), + confirmPasswd: z.string().optional(), +}); + +const passwordSchema = baseSchema + .extend({ + currPasswd: z + .string({ + required_error: t('setting.currentPasswordMessage'), }) - .refine((data) => data.newPasswd === data.confirmPasswd, { - message: t('confirmPasswordNonMatchMessage'), + .trim(), + newPasswd: z + .string({ + required_error: t('setting.newPasswordMessage'), + }) + .trim() + .min(8, { message: t('setting.newPasswordDescription') }), + confirmPasswd: z + .string({ + required_error: t('setting.confirmPasswordMessage'), + }) + .trim() + .min(8, { message: t('setting.newPasswordDescription') }), + }) + .superRefine((data, ctx) => { + if ( + data.newPasswd && + data.confirmPasswd && + data.newPasswd !== data.confirmPasswd + ) { + ctx.addIssue({ path: ['confirmPasswd'], + message: t('setting.confirmPasswordNonMatchMessage'), + code: z.ZodIssueCode.custom, }); - } - - return baseSchema; -} -export default function Profile() { - const [avatarFile, setAvatarFile] = useState(null); - const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 - const { data: userInfo } = useFetchUserInfo(); - const { - saveSetting, - loading: submitLoading, - data: saveUserData, - } = useSaveSetting(); - + } + }); +const ProfilePage: FC = () => { const { t } = useTranslate('setting'); - const [showPasswordForm, setShowPasswordForm] = useState(false); - const FormSchema = defineSchema(t, showPasswordForm); - const form = useForm>({ - resolver: zodResolver(FormSchema), + + const { + profile, + editType, + isEditing, + submitLoading, + editForm, + handleEditClick, + handleCancel, + handleSave, + handleAvatarUpload, + } = useProfile(); + + const form = useForm>({ + resolver: zodResolver( + editType === EditType.editPassword ? passwordSchema : nameSchema, + ), defaultValues: { userName: '', - avatarUrl: '', timeZone: '', - email: '', - // currPasswd: '', - // newPasswd: '', - // confirmPasswd: '', }, - shouldUnregister: true, + // shouldUnregister: true, }); - useEffect(() => { - // init user info when mounted - form.setValue('email', userInfo?.email); // email - form.setValue('userName', userInfo?.nickname); // nickname - form.setValue('timeZone', userInfo?.timezone); // time zone - // form.setValue('currPasswd', ''); // current password - setAvatarBase64Str(userInfo?.avatar ?? ''); - }, [userInfo]); + form.reset({ ...editForm, currPasswd: undefined }); + }, [editForm, form]); - useEffect(() => { - if (saveUserData === 0) { - setShowPasswordForm(false); - form.resetField('currPasswd'); - form.resetField('newPasswd'); - form.resetField('confirmPasswd'); - } - console.log('saveUserData', saveUserData); - }, [saveUserData]); + // const ModalContent: FC = () => { + // // let content = null; + // // if (editType === EditType.editName) { + // // content = editName(); + // // } + // return ( + // <> - useEffect(() => { - if (avatarFile) { - // make use of img compression transformFile2Base64 - (async () => { - setAvatarBase64Str(await transformFile2Base64(avatarFile)); - })(); - } - }, [avatarFile]); + // + // ); + // }; - function onSubmit(data: z.infer) { - const payload: Partial<{ - nickname: string; - password: string; - new_password: string; - avatar: string; - timezone: string; - }> = { - nickname: data.userName, - avatar: avatarBase64Str, - timezone: data.timeZone, - }; - - if (showPasswordForm && 'currPasswd' in data && 'newPasswd' in data) { - payload.password = rsaPsw(data.currPasswd!) as string; - payload.new_password = rsaPsw(data.newPasswd!) as string; - } - saveSetting(payload); - } - - useEffect(() => { - if (showPasswordForm) { - form.register('currPasswd'); - form.register('newPasswd'); - form.register('confirmPasswd'); - } else { - form.unregister(['currPasswd', 'newPasswd', 'confirmPasswd']); - } - }, [showPasswordForm]); return ( -
-

{t('profile')}

-
- {t('profileDescription')} -
-
-
- - {/* Username Field */} - ( - -
- - * - {t('username')} - - - - -
-
-
- -
-
- )} - /> +
+ {/* Header */} +
+

{t('profile')}

+
+ {t('profileDescription')} +
+
- {/* Avatar Field */} - ( - -
- - Avatar - - -
-
- {!avatarBase64Str ? ( -
-
- -

Upload

-
-
- ) : ( -
- - - - -
- -
-
- )} + {/* Main Content */} +
+ {/* Name */} +
+ +
+
+ {profile.userName} +
+ +
+
+ + {/* Avatar */} +
+ +
+ +
+
+ + {/* Time Zone */} +
+ +
+
+ {profile.timeZone} +
+ +
+
+ + {/* Email Address */} +
+ +
+
+ {profile.email} +
+ + {t('emailDescription')} + +
+
+ + {/* Password */} +
+ +
+
+ {profile.currPasswd ? '********' : ''} +
+ +
+
+
+ + {editType && ( + { + if (!open) { + handleCancel(); + } + }} + className="!w-[480px]" + > + {/* */} + + handleSave(data as any))} + className="flex flex-col mt-6 mb-8 ml-2 space-y-6 " + > + {editType === EditType.editName && ( + ( + +
+ + {t('username')} + + { - const file = ev.target?.files?.[0]; - if ( - /\.(jpg|jpeg|png|webp|bmp)$/i.test( - file?.name ?? '', - ) - ) { - setAvatarFile(file!); - } - ev.target.value = ''; - }} + className="bg-bg-input border-border-default" /> -
-
- {t('avatarTip')} -
+
- -
-
-
- -
- +
+
+ +
+ + )} + /> )} - /> - {/* Time Zone Field */} - ( - -
- - * - {t('timezone')} - - -
-
-
- -
-
- )} - /> - - {/* Email Address Field */} - ( -
- -
- - {t('email')} - - - <>{field.value} - -
-
-
- -
-
-
-

 

-

- {t('emailDescription')} -

-
-
- )} - /> - - {/* Password Section */} -
-
-

{t('password')}

- -
-
- {t('passwordDescription')} -
-
- {/* Password Form */} - {showPasswordForm && ( - <> + {editType === EditType.editTimeZone && ( ( -
- - * - {t('currentPassword')} +
+ + {t('timezone')} - - - +
-
-
+
+
)} /> - ( - -
- - * - {t('newPassword')} - - - - -
-
-
- -
-
- )} - /> - ( - -
- - * - {t('confirmPassword')} - - - { - form.trigger('confirmPasswd'); - }} - onChange={(ev) => { - form.setValue( - 'confirmPasswd', - ev.target.value.trim(), - ); - }} - /> - -
-
-
-   + )} + + {editType === EditType.editPassword && ( + <> + ( + +
+ + {t('currentPassword')} + + + +
- -
- - )} - /> - - )} -
- - -
- - -
-
+
+ +
+ + )} + /> + ( + +
+ + {t('newPassword')} + + + + +
+
+ +
+
+ )} + /> + ( + +
+ + {t('confirmPassword')} + + + { + form.trigger('confirmPasswd'); + }} + onChange={(ev) => { + form.setValue( + 'confirmPasswd', + ev.target.value.trim(), + ); + }} + /> + +
+
+ +
+
+ )} + /> + + )} + +
+ + +
+ + + + )} +
); -} +}; + +export default ProfilePage;