Fix: Optimize the style of the personal center sidebar component #9869 (#10723)

### What problem does this PR solve?

fix: Optimize the style of the personal center sidebar component

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-10-22 16:55:16 +08:00
committed by GitHub
parent 4d1255b231
commit 318cb7d792
14 changed files with 784 additions and 127 deletions

View File

@ -0,0 +1,48 @@
import { ThemeEnum } from '@/constants/common';
import { Moon, Sun } from 'lucide-react';
import { FC, useCallback } from 'react';
import { useIsDarkTheme, useTheme } from './theme-provider';
import { Button } from './ui/button';
const ThemeToggle: FC = () => {
const { setTheme } = useTheme();
const isDarkTheme = useIsDarkTheme();
const handleThemeChange = useCallback(
(checked: boolean) => {
setTheme(checked ? ThemeEnum.Dark : ThemeEnum.Light);
},
[setTheme],
);
return (
<Button
type="button"
onClick={() => handleThemeChange(!isDarkTheme)}
className="relative inline-flex h-6 w-14 items-center rounded-full transition-colors p-0.5 border-none focus:border-none bg-bg-card hover:bg-bg-card"
// aria-label={isDarkTheme ? 'Switch to light mode' : 'Switch to dark mode'}
>
<div className="inline-flex h-full w-full items-center">
<div
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
isDarkTheme
? ' text-text-disabled h-4 w-5'
: ' text-text-primary bg-bg-base h-full w-8 flex-1'
}`}
>
<Sun />
</div>
<div
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
isDarkTheme
? ' text-text-primary bg-bg-base h-full w-8 flex-1'
: 'text-text-disabled h-4 w-5'
}`}
>
<Moon />
</div>
</div>
</Button>
);
};
export default ThemeToggle;

View File

@ -1792,6 +1792,8 @@ Important structured information may include: names, dates, locations, events, k
result: 'Result',
parseSummary: 'Parse Summary',
parseSummaryTip: 'Parserdeepdoc',
parserMethod: 'Parser Method',
outputFormat: 'Output Format',
rerunFromCurrentStep: 'Rerun From Current Step',
rerunFromCurrentStepTip: 'Changes detected. Click to re-run.',
confirmRerun: 'Confirm Rerun Process',

View File

@ -1680,6 +1680,8 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
result: '结果',
parseSummary: '解析摘要',
parseSummaryTip: '解析器: deepdoc',
parserMethod: '解析方法',
outputFormat: '输出格式',
rerunFromCurrentStep: '从当前步骤重新运行',
rerunFromCurrentStepTip: '已修改,点击重新运行。',
confirmRerun: '确认重新运行流程',

View File

@ -345,10 +345,10 @@ export const useSummaryInfo = (
const { output_format, parse_method } = setups;
const res = [];
if (parse_method) {
res.push(`${t('dataflow.parserMethod')}: ${parse_method}`);
res.push(`${t('dataflowParser.parserMethod')}: ${parse_method}`);
}
if (output_format) {
res.push(`${t('dataflow.outputFormat')}: ${output_format}`);
res.push(`${t('dataflowParser.outputFormat')}: ${output_format}`);
}
return res.join(' ');
}

View File

@ -121,7 +121,7 @@ const ProfilePage: FC = () => {
// };
return (
<div className="min-h-screen bg-bg-base text-text-secondary p-5">
<div className="h-full bg-bg-base text-text-secondary p-5">
{/* Header */}
<header className="flex flex-col gap-1 justify-between items-start mb-6">
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>

View File

@ -1,10 +1,11 @@
import { useLogout } from '@/hooks/login-hooks';
import { Routes } from '@/routes';
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { useNavigate } from 'umi';
export const useHandleMenuClick = () => {
const navigate = useNavigate();
const [active, setActive] = useState<Routes>();
const { logout } = useLogout();
const handleMenuClick = useCallback(
@ -12,11 +13,12 @@ export const useHandleMenuClick = () => {
if (key === Routes.Logout) {
logout();
} else {
setActive(key);
navigate(`${Routes.ProfileSetting}${key}`);
}
},
[logout, navigate],
);
return { handleMenuClick };
return { handleMenuClick, active };
};

View File

@ -1,10 +1,9 @@
import { useIsDarkTheme, useTheme } from '@/components/theme-provider';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import ThemeToggle from '@/components/theme-toggle';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { ThemeEnum } from '@/constants/common';
import { useLogout } from '@/hooks/login-hooks';
import { useSecondPathName } from '@/hooks/route-hook';
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import {
@ -12,11 +11,9 @@ import {
Banknote,
Box,
FileCog,
LayoutGrid,
LogOut,
User,
Users,
} from 'lucide-react';
import { useCallback } from 'react';
import { useHandleMenuClick } from './hooks';
const menuItems = [
@ -24,7 +21,7 @@ const menuItems = [
section: 'Account & collaboration',
items: [
{ icon: User, label: 'Profile', key: Routes.Profile },
{ icon: LayoutGrid, label: 'Team', key: Routes.Team },
{ icon: Users, label: 'Team', key: Routes.Team },
{ icon: Banknote, label: 'Plan', key: Routes.Plan },
{ icon: Banknote, label: 'MCP', key: Routes.Mcp },
],
@ -53,66 +50,62 @@ const menuItems = [
export function SideBar() {
const pathName = useSecondPathName();
const { handleMenuClick } = useHandleMenuClick();
const { setTheme } = useTheme();
const isDarkTheme = useIsDarkTheme();
const { data: userInfo } = useFetchUserInfo();
const { handleMenuClick, active } = useHandleMenuClick();
const { logout } = useLogout();
const handleThemeChange = useCallback(
(checked: boolean) => {
setTheme(checked ? ThemeEnum.Dark : ThemeEnum.Light);
},
[setTheme],
);
return (
<aside className="w-[303px] bg-background border-r flex flex-col">
<aside className="w-[303px] bg-bg-base flex flex-col">
<div className="px-6 flex gap-2 items-center">
<RAGFlowAvatar
avatar={userInfo?.avatar}
name={userInfo?.nickname}
isPerson
/>
<p className="text-sm text-text-primary">{userInfo?.email}</p>
</div>
<div className="flex-1 overflow-auto">
{menuItems.map((section, idx) => (
<div key={idx}>
<h2 className="p-6 text-sm font-semibold">{section.section}</h2>
{/* <h2 className="p-6 text-sm font-semibold">{section.section}</h2> */}
{section.items.map((item, itemIdx) => {
const active = pathName === item.key;
const hoverKey = pathName === item.key;
return (
<Button
key={itemIdx}
variant={active ? 'secondary' : 'ghost'}
className={cn('w-full justify-start gap-2.5 p-6 relative')}
onClick={handleMenuClick(item.key)}
>
<item.icon className="w-6 h-6" />
<span>{item.label}</span>
{active && (
<div key={itemIdx} className="mx-6 my-5 ">
<Button
variant={hoverKey ? 'secondary' : 'ghost'}
className={cn('w-full justify-start gap-2.5 p-3 relative', {
'bg-bg-card text-text-primary': active === item.key,
'bg-bg-base text-text-secondary': active !== item.key,
})}
onClick={handleMenuClick(item.key)}
>
<item.icon className="w-6 h-6" />
<span>{item.label}</span>
{/* {active && (
<div className="absolute right-0 w-[5px] h-[66px] bg-primary rounded-l-xl shadow-[0_0_5.94px_#7561ff,0_0_11.88px_#7561ff,0_0_41.58px_#7561ff,0_0_83.16px_#7561ff,0_0_142.56px_#7561ff,0_0_249.48px_#7561ff]" />
)}
</Button>
)} */}
</Button>
</div>
);
})}
</div>
))}
</div>
<div className="p-6 mt-auto border-t">
<div className="flex items-center gap-2 mb-6">
<Switch
id="dark-mode"
onCheckedChange={handleThemeChange}
checked={isDarkTheme}
/>
<Label htmlFor="dark-mode" className="text-sm">
Dark
</Label>
<div className="p-6 mt-auto ">
<div className="flex items-center gap-2 mb-6 justify-end">
<ThemeToggle />
</div>
<Button
variant="outline"
className="w-full gap-3"
className="w-full gap-3 !bg-bg-base border !border-border-button !text-text-secondary"
onClick={() => {
logout();
}}
>
<LogOut className="w-6 h-6" />
Logout
Log Out
</Button>
</div>
</aside>

View File

@ -1,5 +1,5 @@
import { Outlet } from 'umi';
import SideBar from './sidebar';
import { SideBar } from './sidebar';
import { PageHeader } from '@/components/page-header';
import {

View File

@ -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<ProfileData>({
userName: '',
avatar: '',
timeZone: '',
email: '',
currPasswd: '',
});
const [editType, setEditType] = useState<IEditType>(EditType.editName);
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState<Partial<ProfileData>>({});
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,
};
};

View File

@ -0,0 +1,413 @@
// src/components/ProfilePage.tsx
import { AvatarUpload } from '@/components/avatar-upload';
import PasswordInput from '@/components/originui/password-input';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal/modal';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useTranslate } from '@/hooks/common-hooks';
import { TimezoneList } from '@/pages/user-setting/constants';
import { zodResolver } from '@hookform/resolvers/zod';
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';
import { EditType, modalTitle, useProfile } from './hooks/use-profile';
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'),
})
.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,
});
}
});
const ProfilePage: FC = () => {
const { t } = useTranslate('setting');
const {
profile,
editType,
isEditing,
submitLoading,
editForm,
handleEditClick,
handleCancel,
handleSave,
handleAvatarUpload,
} = useProfile();
const form = useForm<z.infer<typeof baseSchema | typeof passwordSchema>>({
resolver: zodResolver(
editType === EditType.editPassword ? passwordSchema : nameSchema,
),
defaultValues: {
userName: '',
timeZone: '',
},
// shouldUnregister: true,
});
useEffect(() => {
form.reset({ ...editForm, currPasswd: undefined });
}, [editForm, form]);
// const ModalContent: FC = () => {
// // let content = null;
// // if (editType === EditType.editName) {
// // content = editName();
// // }
// return (
// <>
// </>
// );
// };
return (
<div className="h-full w-full bg-bg-base text-text-secondary p-5">
{/* Header */}
<header className="flex flex-col gap-1 justify-between items-start mb-6">
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>
<div className="text-sm text-text-secondary mb-6">
{t('profileDescription')}
</div>
</header>
{/* Main Content */}
<div className="max-w-3xl space-y-11 w-3/4">
{/* Name */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium">
{t('username')}
</label>
<div className="flex-1 flex items-center gap-4 min-w-60">
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
{profile.userName}
</div>
<Button
variant={'secondary'}
type="button"
onClick={() => handleEditClick(EditType.editName)}
className="text-sm text-text-secondary flex gap-1 px-1"
>
<PenLine size={12} /> Edit
</Button>
</div>
</div>
{/* Avatar */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium">{t('avatar')}</label>
<div className="flex items-center gap-4">
<AvatarUpload
value={profile.avatar}
onChange={handleAvatarUpload}
tips={'This will be displayed on your profile.'}
/>
</div>
</div>
{/* Time Zone */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium">
{t('timezone')}
</label>
<div className="flex-1 flex items-center gap-4">
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
{profile.timeZone}
</div>
<Button
variant={'secondary'}
type="button"
onClick={() => handleEditClick(EditType.editTimeZone)}
className="text-sm text-text-secondary flex gap-1 px-1"
>
<PenLine size={12} /> Edit
</Button>
</div>
</div>
{/* Email Address */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium"> {t('email')}</label>
<div className="flex-1 flex flex-col items-start gap-2">
<div className="text-sm text-text-primary flex-1 rounded-md py-1.5 ">
{profile.email}
</div>
<span className="text-text-secondary text-xs">
{t('emailDescription')}
</span>
</div>
</div>
{/* Password */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium">
{t('password')}
</label>
<div className="flex-1 flex items-center gap-4">
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
{profile.currPasswd ? '********' : ''}
</div>
<Button
variant={'secondary'}
type="button"
onClick={() => handleEditClick(EditType.editPassword)}
className="text-sm text-text-secondary flex gap-1 px-1"
>
<PenLine size={12} /> Edit
</Button>
</div>
</div>
</div>
{editType && (
<Modal
title={modalTitle[editType]}
open={isEditing}
showfooter={false}
titleClassName="text-base"
onOpenChange={(open) => {
if (!open) {
handleCancel();
}
}}
className="!w-[480px]"
>
{/* <ModalContent /> */}
<Form {...form}>
<form
onSubmit={form.handleSubmit((data) => handleSave(data as any))}
className="flex flex-col mt-6 mb-8 ml-2 space-y-6 "
>
{editType === EditType.editName && (
<FormField
control={form.control}
name="userName"
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="flex flex-col w-full gap-2">
<FormLabel className="text-sm text-text-secondary whitespace-nowrap">
{t('username')}
</FormLabel>
<FormControl className="w-full">
<Input
placeholder=""
{...field}
className="bg-bg-input border-border-default"
/>
</FormControl>
</div>
<div className="flex w-full pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
)}
{editType === EditType.editTimeZone && (
<FormField
control={form.control}
name="timeZone"
render={({ field }) => (
<FormItem className="items-center space-y-0">
<div className="flex flex-col w-full gap-2">
<FormLabel className="text-sm text-text-secondary whitespace-nowrap">
{t('timezone')}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl className="w-full bg-bg-input border-border-default">
<SelectTrigger>
<SelectValue placeholder="Select a timeZone" />
</SelectTrigger>
</FormControl>
<SelectContent>
{TimezoneList.map((timeStr) => (
<SelectItem key={timeStr} value={timeStr}>
{timeStr}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-full pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
)}
{editType === EditType.editPassword && (
<>
<FormField
control={form.control}
name="currPasswd"
render={({ field }) => (
<FormItem className="items-center space-y-0">
<div className="flex flex-col w-full gap-2">
<FormLabel
required
className="text-sm flex justify-between text-text-secondary whitespace-nowrap"
>
{t('currentPassword')}
</FormLabel>
<FormControl className="w-full">
<PasswordInput
{...field}
autoComplete="current-password"
className="bg-bg-input border-border-default"
/>
</FormControl>
</div>
<div className="flex w-full pt-1">
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPasswd"
render={({ field }) => (
<FormItem className=" items-center space-y-0">
<div className="flex flex-col w-full gap-2">
<FormLabel
required
className="text-sm text-text-secondary whitespace-nowrap"
>
{t('newPassword')}
</FormLabel>
<FormControl className="w-full">
<PasswordInput
{...field}
autoComplete="new-password"
className="bg-bg-input border-border-default"
/>
</FormControl>
</div>
<div className="flex w-full pt-1">
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPasswd"
render={({ field }) => (
<FormItem className=" items-center space-y-0">
<div className="flex flex-col w-full gap-2">
<FormLabel
required
className="text-sm text-text-secondary whitespace-nowrap"
>
{t('confirmPassword')}
</FormLabel>
<FormControl className="w-full">
<PasswordInput
{...field}
className="bg-bg-input border-border-default"
autoComplete="new-password"
onBlur={() => {
form.trigger('confirmPasswd');
}}
onChange={(ev) => {
form.setValue(
'confirmPasswd',
ev.target.value.trim(),
);
}}
/>
</FormControl>
</div>
<div className="flex w-full pt-1">
<FormMessage />
</div>
</FormItem>
)}
/>
</>
)}
<div className="w-full text-right space-x-4 !mt-11">
<Button type="reset" variant="secondary" onClick={handleCancel}>
{t('cancel')}
</Button>
<Button type="submit" disabled={submitLoading}>
{submitLoading && <Loader2Icon className="animate-spin" />}
{t('save', { keyPrefix: 'common' })}
</Button>
</div>
</form>
</Form>
</Modal>
)}
</div>
);
};
export default ProfilePage;

View File

@ -0,0 +1,24 @@
import { useLogout } from '@/hooks/login-hooks';
import { Routes } from '@/routes';
import { useCallback, useState } from 'react';
import { useNavigate } from 'umi';
export const useHandleMenuClick = () => {
const navigate = useNavigate();
const [active, setActive] = useState<Routes>();
const { logout } = useLogout();
const handleMenuClick = useCallback(
(key: Routes) => () => {
if (key === Routes.Logout) {
logout();
} else {
setActive(key);
navigate(`${Routes.UserSetting}${key}`);
}
},
[logout, navigate],
);
return { handleMenuClick, active };
};

View File

@ -1,5 +0,0 @@
.sideBarWrapper {
.version {
color: rgb(17, 206, 17);
}
}

View File

@ -1,84 +1,109 @@
import { IconFontFill } from '@/components/icon-font';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import ThemeToggle from '@/components/theme-toggle';
import { Button } from '@/components/ui/button';
import { Domain } from '@/constants/common';
import { useTranslate } from '@/hooks/common-hooks';
import { useLogout } from '@/hooks/login-hooks';
import { useSecondPathName } from '@/hooks/route-hook';
import { useFetchSystemVersion } from '@/hooks/user-setting-hooks';
import type { MenuProps } from 'antd';
import { Flex, Menu } from 'antd';
import React, { useEffect, useMemo } from 'react';
import { useNavigate } from 'umi';
import {
UserSettingBaseKey,
UserSettingIconMap,
UserSettingRouteKey,
} from '../constants';
import styles from './index.less';
useFetchSystemVersion,
useFetchUserInfo,
} from '@/hooks/use-user-setting-request';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { Banknote, Box, Cog, Unplug, User, Users } from 'lucide-react';
import { useEffect } from 'react';
import { useHandleMenuClick } from './hooks';
type MenuItem = Required<MenuProps>['items'][number];
const menuItems = [
{ icon: User, label: 'Profile', key: Routes.Profile },
{ icon: Users, label: 'Team', key: Routes.Team },
{ icon: Box, label: 'Model Providers', key: Routes.Model },
{ icon: Unplug, label: 'API', key: Routes.Api },
// {
// icon: MessageSquareQuote,
// label: 'Prompt Templates',
// key: Routes.Profile,
// },
// { icon: TextSearch, label: 'Retrieval Templates', key: Routes.Profile },
{ icon: Cog, label: 'System', key: Routes.System },
// { icon: Banknote, label: 'Plan', key: Routes.Plan },
{ icon: Banknote, label: 'MCP', key: Routes.Mcp },
];
const SideBar = () => {
const navigate = useNavigate();
export function SideBar() {
const pathName = useSecondPathName();
const { logout } = useLogout();
const { t } = useTranslate('setting');
const { data: userInfo } = useFetchUserInfo();
const { handleMenuClick, active } = useHandleMenuClick();
const { version, fetchSystemVersion } = useFetchSystemVersion();
useEffect(() => {
if (location.host !== Domain) {
fetchSystemVersion();
}
}, [fetchSystemVersion]);
function getItem(
label: string,
key: React.Key,
icon?: React.ReactNode,
children?: MenuItem[],
type?: 'group',
): MenuItem {
return {
key,
icon,
children,
label: (
<Flex justify={'space-between'}>
{t(label)}
<span className={styles.version}>
{label === 'system' && version}
</span>
</Flex>
),
type,
} as MenuItem;
}
const items: MenuItem[] = Object.values(UserSettingRouteKey).map((value) =>
getItem(value, value, UserSettingIconMap[value]),
);
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
if (key === UserSettingRouteKey.Logout) {
logout();
} else {
navigate(`/${UserSettingBaseKey}/${key}`);
}
};
const selectedKeys = useMemo(() => {
return [pathName];
}, [pathName]);
const { logout } = useLogout();
return (
<section className={styles.sideBarWrapper}>
<Menu
selectedKeys={selectedKeys}
mode="inline"
items={items}
onClick={handleMenuClick}
style={{ width: 312 }}
/>
</section>
);
};
<aside className="w-[303px] bg-bg-base flex flex-col">
<div className="px-6 flex gap-2 items-center">
<RAGFlowAvatar
avatar={userInfo?.avatar}
name={userInfo?.nickname}
isPerson
/>
<p className="text-sm text-text-primary">{userInfo?.email}</p>
</div>
<div className="flex-1 overflow-auto">
{menuItems.map((item, idx) => {
const hoverKey = pathName === item.key;
return (
<div key={idx}>
<div key={idx} className="mx-6 my-5 ">
<Button
variant={hoverKey ? 'secondary' : 'ghost'}
className={cn('w-full justify-between gap-2.5 p-3 relative', {
'bg-bg-card text-text-primary': active === item.key,
'bg-bg-base text-text-secondary': active !== item.key,
})}
onClick={handleMenuClick(item.key)}
>
<section className="flex items-center gap-2.5">
{item.key === Routes.Mcp ? (
<IconFontFill name={'mcp'} className="size-4 w-4 h-4" />
) : (
<item.icon className="w-6 h-6" />
)}
<span>{item.label}</span>
</section>
{item.key === Routes.System && (
<div className="mr-2 px-2 bg-accent-primary-5 text-accent-primary rounded-md">
{version}
</div>
)}
{/* {active && (
<div className="absolute right-0 w-[5px] h-[66px] bg-primary rounded-l-xl shadow-[0_0_5.94px_#7561ff,0_0_11.88px_#7561ff,0_0_41.58px_#7561ff,0_0_83.16px_#7561ff,0_0_142.56px_#7561ff,0_0_249.48px_#7561ff]" />
)} */}
</Button>
</div>
</div>
);
})}
</div>
export default SideBar;
<div className="p-6 mt-auto ">
<div className="flex items-center gap-2 mb-6 justify-end">
<ThemeToggle />
</div>
<Button
variant="outline"
className="w-full gap-3 !bg-bg-base border !border-border-button !text-text-secondary"
onClick={() => {
logout();
}}
>
Log Out
</Button>
</div>
</aside>
);
}

View File

@ -18,9 +18,11 @@ export enum Routes {
Files = '/files',
ProfileSetting = '/profile-setting',
Profile = '/profile',
Api = '/api',
Mcp = '/mcp',
Team = '/team',
Plan = '/plan',
System = '/system',
Model = '/model',
Prompt = '/prompt',
ProfileMcp = `${ProfileSetting}${Mcp}`,
@ -362,7 +364,7 @@ const routes = [
{
path: '/user-setting/profile',
// component: '@/pages/user-setting/setting-profile',
component: '@/pages/user-setting/setting-profile',
component: '@/pages/user-setting/profile',
},
{
path: '/user-setting/locale',
@ -381,11 +383,11 @@ const routes = [
component: '@/pages/user-setting/setting-team',
},
{
path: '/user-setting/system',
path: `/user-setting${Routes.System}`,
component: '@/pages/user-setting/setting-system',
},
{
path: '/user-setting/api',
path: `/user-setting${Routes.Api}`,
component: '@/pages/user-setting/setting-api',
},
{