From ef8076d87f21d4d4c4ede9fd7d293f11b88916fa Mon Sep 17 00:00:00 2001 From: "guangfei.zhao" Date: Fri, 14 Nov 2025 16:48:42 +0800 Subject: [PATCH] feat(models): add models page and refactor profile form --- src/components/Layout/Header.tsx | 20 +- src/components/Layout/Sidebar.tsx | 180 ++++++++++++++---- src/components/ProfileFormDialog.tsx | 43 +++++ src/locales/en.ts | 7 +- src/locales/zh.ts | 7 +- .../components/CreateKnowledgeDialog.tsx | 1 + src/pages/{setting => models}/models.tsx | 6 +- src/pages/setting/components/ProfileForm.tsx | 70 ++++--- src/pages/setting/index.tsx | 1 - src/routes/index.tsx | 26 ++- 10 files changed, 267 insertions(+), 94 deletions(-) create mode 100644 src/components/ProfileFormDialog.tsx rename src/pages/{setting => models}/models.tsx (98%) diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 88586eb..b86334c 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -18,17 +18,17 @@ import { Person as PersonIcon, } from '@mui/icons-material'; import LanguageSwitcher from '../LanguageSwitcher'; +import ProfileFormDialog from '@/components/ProfileFormDialog'; import { useAuth } from '@/hooks/login-hooks'; -import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; const Header = () => { const { userInfo, logout } = useAuth(); const [anchorEl, setAnchorEl] = useState(null); + const [profileDialogOpen, setProfileDialogOpen] = useState(false); const open = Boolean(anchorEl); - const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const handleAvatarClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -39,7 +39,7 @@ const Header = () => { }; const handleProfileClick = () => { - navigate('/setting/profile'); + setProfileDialogOpen(true); setAnchorEl(null); }; @@ -198,14 +198,6 @@ const Header = () => { {t('setting.personalProfile')} - {/* 模型配置 */} - navigate('/setting/models')} sx={{ py: 1 }}> - - - - {t('setting.modelSettings')} - - @@ -216,6 +208,10 @@ const Header = () => { + setProfileDialogOpen(false)} + /> ); }; diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index 81814d4..d135b5b 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -1,19 +1,66 @@ -import { Box, List, ListItemButton, ListItemText, Typography } from '@mui/material'; -import { Link, useLocation } from 'react-router-dom'; +import { Box, List, ListItemButton, ListItemText, Typography, Collapse, ListItemIcon } from '@mui/material'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { LibraryBooksOutlined as KnowledgeBasesIcon, AccountTreeOutlined as AgentIcon, + SettingsOutlined as SettingIcon, + Person as PersonIcon, + Group as GroupIcon, + ExpandMore as ExpandMoreIcon, + ExpandLess as ExpandLessIcon, + } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; +import { useMemo, useState } from 'react'; + +type MenuChild = { + text: string; + path: string; + icon?: React.ComponentType; +}; + +type MenuItem = { + text: string; + path: string; + icon: React.ComponentType; + children?: MenuChild[]; +}; const Sidebar = () => { const location = useLocation(); + const navigate = useNavigate(); const { t } = useTranslation(); - const navItems = [ + const navItems: MenuItem[] = useMemo(() => ([ { text: t('header.knowledgeBase'), path: '/knowledge', icon: KnowledgeBasesIcon }, - { text: t('header.agent'), path: '/agents', icon: AgentIcon }, - ]; + { text: t('header.agents'), path: '/agents', icon: AgentIcon }, + { text: t('header.models'), path: '/models', icon: AgentIcon }, + { text: t('header.mcp'), path: '/mcp', icon: AgentIcon }, + { + text: t('header.setting'), + path: '/setting', + icon: SettingIcon, + children: [ + { text: t('setting.profile'), path: '/setting/profile', icon: PersonIcon }, + { text: t('setting.team'), path: '/setting/teams', icon: GroupIcon }, + ], + }, + ]), [t]); + + const [openGroup, setOpenGroup] = useState>({ + '/setting': location.pathname.startsWith('/setting'), + }); + + const toggleGroup = (path: string) => { + setOpenGroup((prev) => ({ ...prev, [path]: !prev[path] })); + }; + + const isActivePath = (itemPath: string) => { + if (itemPath === '/knowledge' && (location.pathname === '/' || location.pathname.startsWith('/knowledge'))) { + return true; + } + return location.pathname === itemPath || location.pathname.startsWith(itemPath + '/'); + }; return ( { {navItems.map((item) => { const IconComponent = item.icon; - let isActive = location.pathname === item.path; - if (item.path === '/knowledge' && location.pathname === '/') { - isActive = true; - } + const hasChildren = Array.isArray(item.children) && item.children.length > 0; + const isActive = isActivePath(item.path); + return ( - - - + {hasChildren ? ( + toggleGroup(item.path)} sx={{ - color: 'primary.main', - marginRight: '12px', - fontSize: '1.2rem' + color: isActive ? '#FFF' : '#B9B9C2', + backgroundColor: isActive ? 'rgba(226,0,116,0.12)' : 'transparent', + borderLeft: isActive ? '4px solid' : '4px solid transparent', + borderLeftColor: isActive ? 'primary.main' : 'transparent', + fontWeight: isActive ? 600 : 'normal', + '&:hover': { + backgroundColor: 'rgba(255,255,255,0.05)', + color: '#FFF', + }, + '& .MuiListItemText-primary': { + fontSize: '0.9rem', + }, }} - /> - - - + > + + + {openGroup[item.path] ? ( + + ) : ( + + )} + + ) : ( + + + + + + + )} + + {hasChildren && ( + + + {item.children!.map((child) => { + const ChildIcon = child.icon || IconComponent; + const childActive = location.pathname === child.path; + return ( + + navigate(child.path)} + > + + + + + + + ); + })} + + + )} + ); })} diff --git a/src/components/ProfileFormDialog.tsx b/src/components/ProfileFormDialog.tsx new file mode 100644 index 0000000..c92aac3 --- /dev/null +++ b/src/components/ProfileFormDialog.tsx @@ -0,0 +1,43 @@ +import React, { useCallback, useRef } from 'react'; +import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import ProfileForm, { type ProfileFormHandle } from '@/pages/setting/components/ProfileForm'; +import { useProfileSetting } from '@/hooks/setting-hooks'; +import type { IUserInfo } from '@/interfaces/database/user-setting'; + +interface ProfileFormDialogProps { + open: boolean; + onClose: () => void; +} + +/** + * 个人资料编辑对话框 + * 复用已有 ProfileForm,并使用 useProfileSetting 提供的 userInfo 与更新方法 + */ +const ProfileFormDialog: React.FC = ({ open, onClose }) => { + const { t } = useTranslation(); + const { userInfo, updateUserInfo } = useProfileSetting(); + const formRef = useRef(null); + + const handleSubmit = useCallback(async (data: Partial) => { + await updateUserInfo(data); + onClose(); + }, [updateUserInfo, onClose]); + + return ( + + {t('setting.personalProfile')} + + + + + + + + + ); +}; + +ProfileFormDialog.displayName = 'ProfileFormDialog'; + +export default ProfileFormDialog; \ No newline at end of file diff --git a/src/locales/en.ts b/src/locales/en.ts index 6baee33..d149e73 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -278,10 +278,12 @@ export default { register: 'Register', signin: 'Sign in', home: 'Home', - setting: 'User settings', + setting: 'Settings', logout: 'Log out', fileManager: 'File Management', - agent: 'Agent', + agents: 'Agents', + models: 'Models', + mcp: 'MCP', search: 'Search', welcome: 'Welcome to', }, @@ -1477,6 +1479,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s error: 'Error', success: 'Success', info: 'Info', + processing: 'Processing', confirmDelete: 'Confirm Delete', confirmDeleteMessage: 'This operation cannot be undone. Are you sure you want to delete?', operationSuccess: 'Operation successful', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 868b556..87a3657 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -260,10 +260,12 @@ export default { register: '注册', signin: '登录', home: '首页', - setting: '用户设置', + setting: '设置', logout: '登出', fileManager: '文件管理', - agent: '智能体', + agents: '智能体', + models: '模型', + mcp: 'MCP', search: '搜索', welcome: '欢迎来到', }, @@ -1441,6 +1443,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 error: '错误', success: '成功', info: '信息', + processing: '处理中', confirmDelete: '确认删除', confirmDeleteMessage: '此操作不可撤销,确定要删除吗?', operationSuccess: '操作成功', diff --git a/src/pages/knowledge/components/CreateKnowledgeDialog.tsx b/src/pages/knowledge/components/CreateKnowledgeDialog.tsx index f7050a2..c971e2f 100644 --- a/src/pages/knowledge/components/CreateKnowledgeDialog.tsx +++ b/src/pages/knowledge/components/CreateKnowledgeDialog.tsx @@ -69,6 +69,7 @@ function CreateKnowledgeDialog({ open, onClose, onSuccess }: CreateKnowledgeDial // 解析相关字段:后端已支持 parser_id / pipeline_id parser_id: data.parser_id, pipeline_id: data.pipeline_id, + parse_type: Number.parseInt(parseType) ?? 1, }; await createKnowledge(requestData); diff --git a/src/pages/setting/models.tsx b/src/pages/models/models.tsx similarity index 98% rename from src/pages/setting/models.tsx rename to src/pages/models/models.tsx index e5d19b8..cc5f51e 100644 --- a/src/pages/setting/models.tsx +++ b/src/pages/models/models.tsx @@ -25,10 +25,10 @@ import { } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; import { useLlmModelSetting } from '@/hooks/setting-hooks'; -import { useModelDialogs } from './hooks/useModelDialogs'; +import { useModelDialogs } from '../setting/hooks/useModelDialogs'; import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm'; -import LLMFactoryCard, { MODEL_TYPE_COLORS } from './components/LLMFactoryCard'; -import { ModelDialogs } from './components/ModelDialogs'; +import LLMFactoryCard, { MODEL_TYPE_COLORS } from '../setting/components/LLMFactoryCard'; +import { ModelDialogs } from '../setting/components/ModelDialogs'; import { useDialog } from '@/hooks/useDialog'; import logger from '@/utils/logger'; import { LLM_FACTORY_LIST, LocalLlmFactories, type LLMFactory } from '@/constants/llm'; diff --git a/src/pages/setting/components/ProfileForm.tsx b/src/pages/setting/components/ProfileForm.tsx index 6f15039..091c238 100644 --- a/src/pages/setting/components/ProfileForm.tsx +++ b/src/pages/setting/components/ProfileForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react'; import { Box, TextField, @@ -36,20 +36,29 @@ const languageOptions = [ // 时区选项 const timezoneOptions = TimezoneList.map(x => ({ value: x, label: x })); -interface ProfileFormProps { +export interface ProfileFormProps { userInfo: IUserInfo | null; onSubmit: (data: Partial) => Promise; + showSaveButton?: boolean; + onSave?: () => void | Promise; } +export type ProfileFormHandle = { + submit: () => Promise; +}; + /** * 个人信息表单 */ -function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) { +const ProfileFormInner = ( + { userInfo, onSubmit, showSaveButton = true, onSave }: ProfileFormProps, + ref: React.ForwardedRef +) => { const { t } = useTranslation(); const showMessage = useMessage(); const fileInputRef = useRef(null); - + const [formData, setFormData] = useState>({ nickname: userInfo?.nickname || '', avatar: userInfo?.avatar || null, @@ -96,7 +105,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) { showMessage.error(t('setting.pleaseSelectImageFile')); return; } - + // 检查文件大小 (限制为2MB) if (file.size > 2 * 1024 * 1024) { showMessage.error(t('setting.imageSizeLimit')); @@ -145,20 +154,25 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) { } }; + // 向父级暴露提交方法 + useImperativeHandle(ref, () => ({ + submit: handleSave, + })); + return ( {t('setting.personalProfile')} - + {/* 头像部分 */} - {/* 保存按钮 */} - - - - - + {/* 保存按钮(可选) */} + {showSaveButton && ( + + + + + + )} ); -} +}; + +const ProfileForm = forwardRef(ProfileFormInner); + +ProfileForm.displayName = 'ProfileForm'; export default ProfileForm; \ No newline at end of file diff --git a/src/pages/setting/index.tsx b/src/pages/setting/index.tsx index 6b56cd4..9588a81 100644 --- a/src/pages/setting/index.tsx +++ b/src/pages/setting/index.tsx @@ -1,4 +1,3 @@ -export { default as ModelsSetting } from './models'; export { default as SystemSetting } from './system'; export { default as TeamsSetting } from './teams'; export { default as ProfileSetting } from './profile'; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 89f83f3..25e4042 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -2,9 +2,6 @@ import { Routes, Route, Navigate } from 'react-router-dom'; import MainLayout from '@/components/Layout/MainLayout'; import SettingLayout from '@/components/Layout/SettingLayout'; import Login from '@/pages/login/Login'; -import PipelineConfig from '@/pages/PipelineConfig'; -import Dashboard from '@/pages/Dashboard'; -import ModelsResources from '@/pages/ModelsResources'; import AgentList from '@/pages/agent-mui/list'; import { KnowledgeBaseList, @@ -15,13 +12,12 @@ import { KnowledgeLogsPage } from '@/pages/knowledge'; import { - ModelsSetting, SystemSetting, TeamsSetting, ProfileSetting, MCPSetting, } from '@/pages/setting'; -import MCP from '@/pages/MCP'; +import ModelsSetting from '@/pages/models/models'; import FormFieldTest from '@/pages/FormFieldTest'; import ChunkParsedResult from '@/pages/chunk/parsed-result'; import DocumentPreview from '@/pages/chunk/document-preview'; @@ -30,6 +26,7 @@ import AgentDetailPage from '@/pages/agent-mui/detail'; import RagflowLayout from '@/pages/ragflow/layout'; import RagflowIframePage from '@/pages/ragflow/iframe'; import RagflowAgentPage from '@/pages/ragflow/agent'; +import McpSettingPage from '@/pages/setting/mcp'; const AppRoutes = () => { return ( @@ -53,6 +50,16 @@ const AppRoutes = () => { } /> + + } /> + + + } /> + + + } /> + } /> + } /> @@ -63,15 +70,6 @@ const AppRoutes = () => { {/* 文档预览页面路由 */} } /> - {/* setting 相关路由 */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - {/* 通过 iframe 承载 ragflow 应用,避免路由冲突并保持同源 */} {/* ragflow 路由分组:layout 承载,agent 为特定页面,其余走通用 iframe */}