feat(models): add models page and refactor profile form
This commit is contained in:
@@ -18,17 +18,17 @@ import {
|
|||||||
Person as PersonIcon,
|
Person as PersonIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import LanguageSwitcher from '../LanguageSwitcher';
|
import LanguageSwitcher from '../LanguageSwitcher';
|
||||||
|
import ProfileFormDialog from '@/components/ProfileFormDialog';
|
||||||
import { useAuth } from '@/hooks/login-hooks';
|
import { useAuth } from '@/hooks/login-hooks';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { userInfo, logout } = useAuth();
|
const { userInfo, logout } = useAuth();
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleAvatarClick = (event: React.MouseEvent<HTMLElement>) => {
|
const handleAvatarClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -39,7 +39,7 @@ const Header = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleProfileClick = () => {
|
const handleProfileClick = () => {
|
||||||
navigate('/setting/profile');
|
setProfileDialogOpen(true);
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,14 +198,6 @@ const Header = () => {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('setting.personalProfile')}</ListItemText>
|
<ListItemText>{t('setting.personalProfile')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/* 模型配置 */}
|
|
||||||
<MenuItem onClick={() => navigate('/setting/models')} sx={{ py: 1 }}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<SettingsIcon fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>{t('setting.modelSettings')}</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<MenuItem onClick={handleLogout} sx={{ py: 1, color: '#d32f2f' }}>
|
<MenuItem onClick={handleLogout} sx={{ py: 1, color: '#d32f2f' }}>
|
||||||
@@ -216,6 +208,10 @@ const Header = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
<ProfileFormDialog
|
||||||
|
open={profileDialogOpen}
|
||||||
|
onClose={() => setProfileDialogOpen(false)}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,66 @@
|
|||||||
import { Box, List, ListItemButton, ListItemText, Typography } from '@mui/material';
|
import { Box, List, ListItemButton, ListItemText, Typography, Collapse, ListItemIcon } from '@mui/material';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
LibraryBooksOutlined as KnowledgeBasesIcon,
|
LibraryBooksOutlined as KnowledgeBasesIcon,
|
||||||
AccountTreeOutlined as AgentIcon,
|
AccountTreeOutlined as AgentIcon,
|
||||||
|
SettingsOutlined as SettingIcon,
|
||||||
|
Person as PersonIcon,
|
||||||
|
Group as GroupIcon,
|
||||||
|
ExpandMore as ExpandMoreIcon,
|
||||||
|
ExpandLess as ExpandLessIcon,
|
||||||
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
type MenuChild = {
|
||||||
|
text: string;
|
||||||
|
path: string;
|
||||||
|
icon?: React.ComponentType<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MenuItem = {
|
||||||
|
text: string;
|
||||||
|
path: string;
|
||||||
|
icon: React.ComponentType<any>;
|
||||||
|
children?: MenuChild[];
|
||||||
|
};
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const navItems = [
|
const navItems: MenuItem[] = useMemo(() => ([
|
||||||
{ text: t('header.knowledgeBase'), path: '/knowledge', icon: KnowledgeBasesIcon },
|
{ 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<Record<string, boolean>>({
|
||||||
|
'/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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -43,42 +90,99 @@ const Sidebar = () => {
|
|||||||
<List>
|
<List>
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const IconComponent = item.icon;
|
const IconComponent = item.icon;
|
||||||
let isActive = location.pathname === item.path;
|
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
|
||||||
if (item.path === '/knowledge' && location.pathname === '/') {
|
const isActive = isActivePath(item.path);
|
||||||
isActive = true;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Box key={item.path}>
|
||||||
key={item.path}
|
{hasChildren ? (
|
||||||
to={item.path}
|
<ListItemButton
|
||||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
onClick={() => toggleGroup(item.path)}
|
||||||
>
|
|
||||||
<ListItemButton
|
|
||||||
sx={{
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
sx={{
|
sx={{
|
||||||
color: 'primary.main',
|
color: isActive ? '#FFF' : '#B9B9C2',
|
||||||
marginRight: '12px',
|
backgroundColor: isActive ? 'rgba(226,0,116,0.12)' : 'transparent',
|
||||||
fontSize: '1.2rem'
|
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',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<ListItemText primary={item.text} />
|
<IconComponent sx={{ color: 'primary.main', marginRight: '12px', fontSize: '1.2rem' }} />
|
||||||
</ListItemButton>
|
<ListItemText primary={item.text} />
|
||||||
</Link>
|
{openGroup[item.path] ? (
|
||||||
|
<ExpandLessIcon sx={{ marginLeft: 'auto', color: '#B9B9C2' }} />
|
||||||
|
) : (
|
||||||
|
<ExpandMoreIcon sx={{ marginLeft: 'auto', color: '#B9B9C2' }} />
|
||||||
|
)}
|
||||||
|
</ListItemButton>
|
||||||
|
) : (
|
||||||
|
<Link to={item.path} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
|
<ListItemButton
|
||||||
|
sx={{
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconComponent sx={{ color: 'primary.main', marginRight: '12px', fontSize: '1.2rem' }} />
|
||||||
|
<ListItemText primary={item.text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasChildren && (
|
||||||
|
<Collapse in={!!openGroup[item.path]} timeout="auto" unmountOnExit>
|
||||||
|
<List component="div" disablePadding>
|
||||||
|
{item.children!.map((child) => {
|
||||||
|
const ChildIcon = child.icon || IconComponent;
|
||||||
|
const childActive = location.pathname === child.path;
|
||||||
|
return (
|
||||||
|
<Link key={child.path} to={child.path} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
|
<ListItemButton
|
||||||
|
sx={{
|
||||||
|
pl: 6,
|
||||||
|
color: childActive ? '#FFF' : '#B9B9C2',
|
||||||
|
backgroundColor: childActive ? 'rgba(226,0,116,0.08)' : 'transparent',
|
||||||
|
borderLeft: childActive ? '4px solid' : '4px solid transparent',
|
||||||
|
borderLeftColor: childActive ? 'primary.main' : 'transparent',
|
||||||
|
fontWeight: childActive ? 600 : 'normal',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.04)',
|
||||||
|
color: '#FFF',
|
||||||
|
},
|
||||||
|
'& .MuiListItemText-primary': {
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => navigate(child.path)}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ minWidth: 32 }}>
|
||||||
|
<ChildIcon sx={{ color: 'primary.main', fontSize: '1.1rem' }} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={child.text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
43
src/components/ProfileFormDialog.tsx
Normal file
43
src/components/ProfileFormDialog.tsx
Normal file
@@ -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<ProfileFormDialogProps> = ({ open, onClose }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { userInfo, updateUserInfo } = useProfileSetting();
|
||||||
|
const formRef = useRef<ProfileFormHandle>(null);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async (data: Partial<IUserInfo>) => {
|
||||||
|
await updateUserInfo(data);
|
||||||
|
onClose();
|
||||||
|
}, [updateUserInfo, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||||
|
<DialogTitle>{t('setting.personalProfile')}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<ProfileForm ref={formRef} userInfo={userInfo} onSubmit={handleSubmit} showSaveButton={false} />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>{t('setting.cancel')}</Button>
|
||||||
|
<Button variant="contained" onClick={() => formRef.current?.submit()}>{t('setting.save')}</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileFormDialog.displayName = 'ProfileFormDialog';
|
||||||
|
|
||||||
|
export default ProfileFormDialog;
|
||||||
@@ -278,10 +278,12 @@ export default {
|
|||||||
register: 'Register',
|
register: 'Register',
|
||||||
signin: 'Sign in',
|
signin: 'Sign in',
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
setting: 'User settings',
|
setting: 'Settings',
|
||||||
logout: 'Log out',
|
logout: 'Log out',
|
||||||
fileManager: 'File Management',
|
fileManager: 'File Management',
|
||||||
agent: 'Agent',
|
agents: 'Agents',
|
||||||
|
models: 'Models',
|
||||||
|
mcp: 'MCP',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
welcome: 'Welcome to',
|
welcome: 'Welcome to',
|
||||||
},
|
},
|
||||||
@@ -1477,6 +1479,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
|||||||
error: 'Error',
|
error: 'Error',
|
||||||
success: 'Success',
|
success: 'Success',
|
||||||
info: 'Info',
|
info: 'Info',
|
||||||
|
processing: 'Processing',
|
||||||
confirmDelete: 'Confirm Delete',
|
confirmDelete: 'Confirm Delete',
|
||||||
confirmDeleteMessage: 'This operation cannot be undone. Are you sure you want to delete?',
|
confirmDeleteMessage: 'This operation cannot be undone. Are you sure you want to delete?',
|
||||||
operationSuccess: 'Operation successful',
|
operationSuccess: 'Operation successful',
|
||||||
|
|||||||
@@ -260,10 +260,12 @@ export default {
|
|||||||
register: '注册',
|
register: '注册',
|
||||||
signin: '登录',
|
signin: '登录',
|
||||||
home: '首页',
|
home: '首页',
|
||||||
setting: '用户设置',
|
setting: '设置',
|
||||||
logout: '登出',
|
logout: '登出',
|
||||||
fileManager: '文件管理',
|
fileManager: '文件管理',
|
||||||
agent: '智能体',
|
agents: '智能体',
|
||||||
|
models: '模型',
|
||||||
|
mcp: 'MCP',
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
welcome: '欢迎来到',
|
welcome: '欢迎来到',
|
||||||
},
|
},
|
||||||
@@ -1441,6 +1443,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
|||||||
error: '错误',
|
error: '错误',
|
||||||
success: '成功',
|
success: '成功',
|
||||||
info: '信息',
|
info: '信息',
|
||||||
|
processing: '处理中',
|
||||||
confirmDelete: '确认删除',
|
confirmDelete: '确认删除',
|
||||||
confirmDeleteMessage: '此操作不可撤销,确定要删除吗?',
|
confirmDeleteMessage: '此操作不可撤销,确定要删除吗?',
|
||||||
operationSuccess: '操作成功',
|
operationSuccess: '操作成功',
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ function CreateKnowledgeDialog({ open, onClose, onSuccess }: CreateKnowledgeDial
|
|||||||
// 解析相关字段:后端已支持 parser_id / pipeline_id
|
// 解析相关字段:后端已支持 parser_id / pipeline_id
|
||||||
parser_id: data.parser_id,
|
parser_id: data.parser_id,
|
||||||
pipeline_id: data.pipeline_id,
|
pipeline_id: data.pipeline_id,
|
||||||
|
parse_type: Number.parseInt(parseType) ?? 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
await createKnowledge(requestData);
|
await createKnowledge(requestData);
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLlmModelSetting } from '@/hooks/setting-hooks';
|
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 type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm';
|
||||||
import LLMFactoryCard, { MODEL_TYPE_COLORS } from './components/LLMFactoryCard';
|
import LLMFactoryCard, { MODEL_TYPE_COLORS } from '../setting/components/LLMFactoryCard';
|
||||||
import { ModelDialogs } from './components/ModelDialogs';
|
import { ModelDialogs } from '../setting/components/ModelDialogs';
|
||||||
import { useDialog } from '@/hooks/useDialog';
|
import { useDialog } from '@/hooks/useDialog';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { LLM_FACTORY_LIST, LocalLlmFactories, type LLMFactory } from '@/constants/llm';
|
import { LLM_FACTORY_LIST, LocalLlmFactories, type LLMFactory } from '@/constants/llm';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -36,20 +36,29 @@ const languageOptions = [
|
|||||||
// 时区选项
|
// 时区选项
|
||||||
const timezoneOptions = TimezoneList.map(x => ({ value: x, label: x }));
|
const timezoneOptions = TimezoneList.map(x => ({ value: x, label: x }));
|
||||||
|
|
||||||
interface ProfileFormProps {
|
export interface ProfileFormProps {
|
||||||
userInfo: IUserInfo | null;
|
userInfo: IUserInfo | null;
|
||||||
onSubmit: (data: Partial<IUserInfo>) => Promise<void>;
|
onSubmit: (data: Partial<IUserInfo>) => Promise<void>;
|
||||||
|
showSaveButton?: boolean;
|
||||||
|
onSave?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProfileFormHandle = {
|
||||||
|
submit: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个人信息表单
|
* 个人信息表单
|
||||||
*/
|
*/
|
||||||
function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
const ProfileFormInner = (
|
||||||
|
{ userInfo, onSubmit, showSaveButton = true, onSave }: ProfileFormProps,
|
||||||
|
ref: React.ForwardedRef<ProfileFormHandle>
|
||||||
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const showMessage = useMessage();
|
const showMessage = useMessage();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<Partial<IUserInfo>>({
|
const [formData, setFormData] = useState<Partial<IUserInfo>>({
|
||||||
nickname: userInfo?.nickname || '',
|
nickname: userInfo?.nickname || '',
|
||||||
avatar: userInfo?.avatar || null,
|
avatar: userInfo?.avatar || null,
|
||||||
@@ -96,7 +105,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
showMessage.error(t('setting.pleaseSelectImageFile'));
|
showMessage.error(t('setting.pleaseSelectImageFile'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查文件大小 (限制为2MB)
|
// 检查文件大小 (限制为2MB)
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
showMessage.error(t('setting.imageSizeLimit'));
|
showMessage.error(t('setting.imageSizeLimit'));
|
||||||
@@ -145,20 +154,25 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 向父级暴露提交方法
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
submit: handleSave,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper elevation={0} sx={{ p: 3, backgroundColor: 'transparent' }}>
|
<Paper elevation={0} sx={{ p: 3, backgroundColor: 'transparent' }}>
|
||||||
<Typography variant="h6" gutterBottom sx={{ mb: 3, fontWeight: 600 }}>
|
<Typography variant="h6" gutterBottom sx={{ mb: 3, fontWeight: 600 }}>
|
||||||
{t('setting.personalProfile')}
|
{t('setting.personalProfile')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{/* 头像部分 */}
|
{/* 头像部分 */}
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={formData.avatar}
|
src={formData.avatar}
|
||||||
sx={{
|
sx={{
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
border: '2px solid',
|
border: '2px solid',
|
||||||
borderColor: 'divider'
|
borderColor: 'divider'
|
||||||
@@ -174,7 +188,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={triggerFileSelect}
|
onClick={triggerFileSelect}
|
||||||
sx={{
|
sx={{
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: 'primary.main',
|
borderColor: 'primary.main',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@@ -260,21 +274,33 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* 保存按钮 */}
|
{/* 保存按钮(可选) */}
|
||||||
<Grid size={12}>
|
{showSaveButton && (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
|
<Grid size={12}>
|
||||||
<Button
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
|
||||||
variant="contained"
|
<Button
|
||||||
onClick={handleSave}
|
variant="contained"
|
||||||
sx={{ minWidth: 120 }}
|
onClick={async () => {
|
||||||
>
|
if (onSave) {
|
||||||
{t('setting.save')}
|
await onSave();
|
||||||
</Button>
|
} else {
|
||||||
</Box>
|
await handleSave();
|
||||||
</Grid>
|
}
|
||||||
|
}}
|
||||||
|
sx={{ minWidth: 120 }}
|
||||||
|
>
|
||||||
|
{t('setting.save')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const ProfileForm = forwardRef<ProfileFormHandle, ProfileFormProps>(ProfileFormInner);
|
||||||
|
|
||||||
|
ProfileForm.displayName = 'ProfileForm';
|
||||||
|
|
||||||
export default ProfileForm;
|
export default ProfileForm;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export { default as ModelsSetting } from './models';
|
|
||||||
export { default as SystemSetting } from './system';
|
export { default as SystemSetting } from './system';
|
||||||
export { default as TeamsSetting } from './teams';
|
export { default as TeamsSetting } from './teams';
|
||||||
export { default as ProfileSetting } from './profile';
|
export { default as ProfileSetting } from './profile';
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { Routes, Route, Navigate } from 'react-router-dom';
|
|||||||
import MainLayout from '@/components/Layout/MainLayout';
|
import MainLayout from '@/components/Layout/MainLayout';
|
||||||
import SettingLayout from '@/components/Layout/SettingLayout';
|
import SettingLayout from '@/components/Layout/SettingLayout';
|
||||||
import Login from '@/pages/login/Login';
|
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 AgentList from '@/pages/agent-mui/list';
|
||||||
import {
|
import {
|
||||||
KnowledgeBaseList,
|
KnowledgeBaseList,
|
||||||
@@ -15,13 +12,12 @@ import {
|
|||||||
KnowledgeLogsPage
|
KnowledgeLogsPage
|
||||||
} from '@/pages/knowledge';
|
} from '@/pages/knowledge';
|
||||||
import {
|
import {
|
||||||
ModelsSetting,
|
|
||||||
SystemSetting,
|
SystemSetting,
|
||||||
TeamsSetting,
|
TeamsSetting,
|
||||||
ProfileSetting,
|
ProfileSetting,
|
||||||
MCPSetting,
|
MCPSetting,
|
||||||
} from '@/pages/setting';
|
} from '@/pages/setting';
|
||||||
import MCP from '@/pages/MCP';
|
import ModelsSetting from '@/pages/models/models';
|
||||||
import FormFieldTest from '@/pages/FormFieldTest';
|
import FormFieldTest from '@/pages/FormFieldTest';
|
||||||
import ChunkParsedResult from '@/pages/chunk/parsed-result';
|
import ChunkParsedResult from '@/pages/chunk/parsed-result';
|
||||||
import DocumentPreview from '@/pages/chunk/document-preview';
|
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 RagflowLayout from '@/pages/ragflow/layout';
|
||||||
import RagflowIframePage from '@/pages/ragflow/iframe';
|
import RagflowIframePage from '@/pages/ragflow/iframe';
|
||||||
import RagflowAgentPage from '@/pages/ragflow/agent';
|
import RagflowAgentPage from '@/pages/ragflow/agent';
|
||||||
|
import McpSettingPage from '@/pages/setting/mcp';
|
||||||
|
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
return (
|
return (
|
||||||
@@ -53,6 +50,16 @@ const AppRoutes = () => {
|
|||||||
<Route path="agents">
|
<Route path="agents">
|
||||||
<Route index element={<AgentList />} />
|
<Route index element={<AgentList />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path='models'>
|
||||||
|
<Route index element={<ModelsSetting />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='mcp'>
|
||||||
|
<Route index element={<McpSettingPage />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='setting'>
|
||||||
|
<Route path="teams" element={<TeamsSetting />} />
|
||||||
|
<Route path="profile" element={<ProfileSetting />} />
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="agent">
|
<Route path="agent">
|
||||||
<Route path=":id" element={<AgentDetailPage />} />
|
<Route path=":id" element={<AgentDetailPage />} />
|
||||||
@@ -63,15 +70,6 @@ const AppRoutes = () => {
|
|||||||
{/* 文档预览页面路由 */}
|
{/* 文档预览页面路由 */}
|
||||||
<Route path="document-preview/:kb_id/:doc_id" element={<DocumentPreview />} />
|
<Route path="document-preview/:kb_id/:doc_id" element={<DocumentPreview />} />
|
||||||
</Route>
|
</Route>
|
||||||
{/* setting 相关路由 */}
|
|
||||||
<Route path="setting" element={<SettingLayout />}>
|
|
||||||
<Route index element={<ModelsSetting />} />
|
|
||||||
<Route path="models" element={<ModelsSetting />} />
|
|
||||||
<Route path="system" element={<SystemSetting />} />
|
|
||||||
<Route path="teams" element={<TeamsSetting />} />
|
|
||||||
<Route path="profile" element={<ProfileSetting />} />
|
|
||||||
<Route path="mcp" element={<MCPSetting />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* 通过 iframe 承载 ragflow 应用,避免路由冲突并保持同源 */}
|
{/* 通过 iframe 承载 ragflow 应用,避免路由冲突并保持同源 */}
|
||||||
{/* ragflow 路由分组:layout 承载,agent 为特定页面,其余走通用 iframe */}
|
{/* ragflow 路由分组:layout 承载,agent 为特定页面,其余走通用 iframe */}
|
||||||
|
|||||||
Reference in New Issue
Block a user