feat(user): add user data management system with global state
- Implement user store with Zustand for global state management - Create UserDataProvider component to initialize user data on app load - Add useUserData hook for accessing and managing user data - Refactor knowledge base list page to use new hooks
This commit is contained in:
311
src/components/KnowledgeGridView.tsx
Normal file
311
src/components/KnowledgeGridView.tsx
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Grid,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
Avatar,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
MoreVert as MoreVertIcon,
|
||||||
|
Folder as FolderIcon,
|
||||||
|
ArrowForward as ArrowForwardIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import type { IKnowledge } from '@/interfaces/database/knowledge';
|
||||||
|
|
||||||
|
interface KnowledgeGridViewProps {
|
||||||
|
knowledgeBases: IKnowledge[];
|
||||||
|
maxItems?: number;
|
||||||
|
showSeeAll?: boolean;
|
||||||
|
onSeeAll?: () => void;
|
||||||
|
onEdit?: (kb: IKnowledge) => void;
|
||||||
|
onDelete?: (kb: IKnowledge) => void;
|
||||||
|
onView?: (kb: IKnowledge) => void;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KnowledgeCardProps {
|
||||||
|
knowledge: IKnowledge;
|
||||||
|
onMenuClick: (event: React.MouseEvent<HTMLElement>, kb: IKnowledge) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KnowledgeCard: React.FC<KnowledgeCardProps> = ({ knowledge, onMenuClick }) => {
|
||||||
|
const getStatusInfo = (permission: string) => {
|
||||||
|
switch (permission) {
|
||||||
|
case 'me':
|
||||||
|
return { label: '私有', color: '#E3F2FD', textColor: '#1976D2' };
|
||||||
|
case 'team':
|
||||||
|
return { label: '团队', color: '#E8F5E8', textColor: '#388E3C' };
|
||||||
|
default:
|
||||||
|
return { label: '公开', color: '#FFF3E0', textColor: '#F57C00' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusInfo = getStatusInfo(knowledge.permission || 'me');
|
||||||
|
|
||||||
|
// 格式化更新时间
|
||||||
|
const formatUpdateTime = (timestamp: number) => {
|
||||||
|
if (!timestamp) return '未知';
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化token数量
|
||||||
|
const formatTokenNum = (tokenNum: number) => {
|
||||||
|
if (tokenNum < 1000) return tokenNum.toString();
|
||||||
|
if (tokenNum < 1000000) return `${(tokenNum / 1000).toFixed(1)}K`;
|
||||||
|
return `${(tokenNum / 1000000).toFixed(1)}M`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
border: '1px solid #E5E5E5',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
||||||
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
{/* 显示avatar */}
|
||||||
|
{knowledge.avatar ? (
|
||||||
|
<Avatar
|
||||||
|
src={knowledge.avatar}
|
||||||
|
sx={{ width: 32, height: 32 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FolderIcon color="primary" />
|
||||||
|
)}
|
||||||
|
<Typography variant="h6" fontWeight={600} noWrap>
|
||||||
|
{knowledge.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Chip
|
||||||
|
label={statusInfo.label}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
height: '24px',
|
||||||
|
backgroundColor: statusInfo.color,
|
||||||
|
color: statusInfo.textColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => onMenuClick(e, knowledge)}
|
||||||
|
>
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
mb: 2,
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{knowledge.description || '暂无描述'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
mt: 2,
|
||||||
|
p: 1.5,
|
||||||
|
backgroundColor: '#F8F9FA',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ fontSize: '1.25rem', fontWeight: 600, color: 'primary.main' }}
|
||||||
|
>
|
||||||
|
{knowledge.doc_num || 0}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
文档数量
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ fontSize: '1.25rem', fontWeight: 600, color: 'primary.main' }}
|
||||||
|
>
|
||||||
|
{knowledge.chunk_num || 0}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
分块数量
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ fontSize: '1.25rem', fontWeight: 600, color: 'primary.main' }}
|
||||||
|
>
|
||||||
|
{formatTokenNum(knowledge.token_num || 0)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Token数量
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 显示更新时间 */}
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||||
|
最后更新: {formatUpdateTime(knowledge.update_time)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* 显示创建者 */}
|
||||||
|
{knowledge.nickname && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||||
|
创建者: {knowledge.nickname}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 显示语言 */}
|
||||||
|
{knowledge.language && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||||
|
语言: {knowledge.language}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const KnowledgeGridView: React.FC<KnowledgeGridViewProps> = ({
|
||||||
|
knowledgeBases,
|
||||||
|
maxItems,
|
||||||
|
showSeeAll = false,
|
||||||
|
onSeeAll,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onView,
|
||||||
|
loading = false,
|
||||||
|
}) => {
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
const [selectedKB, setSelectedKB] = React.useState<IKnowledge | null>(null);
|
||||||
|
|
||||||
|
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, kb: IKnowledge) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
setSelectedKB(kb);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setSelectedKB(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
if (selectedKB && onEdit) {
|
||||||
|
onEdit(selectedKB);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedKB && onDelete) {
|
||||||
|
onDelete(selectedKB);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = () => {
|
||||||
|
if (selectedKB && onView) {
|
||||||
|
onView(selectedKB);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayedKBs = maxItems ? knowledgeBases.slice(0, maxItems) : knowledgeBases;
|
||||||
|
const hasMore = maxItems && knowledgeBases.length > maxItems;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<Typography>加载中...</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (knowledgeBases.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<FolderIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
暂无知识库
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
创建您的第一个知识库开始使用
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{displayedKBs.map((kb) => (
|
||||||
|
<Grid key={kb.id} size={{ xs: 12, sm: 6, md: 4 }}>
|
||||||
|
<KnowledgeCard knowledge={kb} onMenuClick={handleMenuClick} />
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{showSeeAll && hasMore && (
|
||||||
|
<Box sx={{ textAlign: 'center', mt: 3 }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
endIcon={<ArrowForwardIcon />}
|
||||||
|
onClick={onSeeAll}
|
||||||
|
sx={{ borderRadius: 2 }}
|
||||||
|
>
|
||||||
|
查看全部 ({knowledgeBases.length})
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleView}>查看详情</MenuItem>
|
||||||
|
<MenuItem onClick={handleEdit}>编辑</MenuItem>
|
||||||
|
<MenuItem onClick={handleMenuClose}>导出</MenuItem>
|
||||||
|
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
|
||||||
|
删除
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KnowledgeGridView;
|
||||||
@@ -2,6 +2,7 @@ import { Box, styled } from '@mui/material';
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
|
import UserDataProvider from '../Provider/UserDataProvider';
|
||||||
|
|
||||||
const LayoutContainer = styled(Box)({
|
const LayoutContainer = styled(Box)({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -24,6 +25,7 @@ const ContentArea = styled(Box)({
|
|||||||
|
|
||||||
const MainLayout = () => {
|
const MainLayout = () => {
|
||||||
return (
|
return (
|
||||||
|
<UserDataProvider>
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<MainContent>
|
<MainContent>
|
||||||
@@ -33,6 +35,7 @@ const MainLayout = () => {
|
|||||||
</ContentArea>
|
</ContentArea>
|
||||||
</MainContent>
|
</MainContent>
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
|
</UserDataProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import StorageOutlinedIcon from '@mui/icons-material/StorageOutlined';
|
|||||||
import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined';
|
import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ text: 'Overview', path: '/', icon: DashboardOutlinedIcon },
|
// { text: 'Overview', path: '/', icon: DashboardOutlinedIcon },
|
||||||
{ text: 'Knowledge Bases', path: '/kb-list', icon: LibraryBooksOutlinedIcon },
|
{ text: 'Knowledge Bases', path: '/', icon: LibraryBooksOutlinedIcon },
|
||||||
{ text: 'RAG Pipeline', path: '/pipeline-config', icon: AccountTreeOutlinedIcon },
|
{ text: 'RAG Pipeline', path: '/pipeline-config', icon: AccountTreeOutlinedIcon },
|
||||||
{ text: 'Operations', path: '/dashboard', icon: SettingsOutlinedIcon },
|
{ text: 'Operations', path: '/dashboard', icon: SettingsOutlinedIcon },
|
||||||
{ text: 'Models & Resources', path: '/models-resources', icon: StorageOutlinedIcon },
|
{ text: 'Models & Resources', path: '/models-resources', icon: StorageOutlinedIcon },
|
||||||
|
|||||||
30
src/components/Provider/UserDataProvider.tsx
Normal file
30
src/components/Provider/UserDataProvider.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React, { useEffect, type PropsWithChildren } from 'react';
|
||||||
|
import { useUserData } from '@/hooks/useUserData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户数据提供者组件
|
||||||
|
* 负责在应用启动时初始化用户数据
|
||||||
|
*/
|
||||||
|
const UserDataProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
const { initializeUserData, isLoading, error } = useUserData();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 检查是否已登录(通过localStorage中的Authorization判断)
|
||||||
|
const authorization = localStorage.getItem('Authorization');
|
||||||
|
|
||||||
|
if (authorization && authorization !== '') {
|
||||||
|
// 如果已登录,初始化用户数据
|
||||||
|
initializeUserData();
|
||||||
|
}
|
||||||
|
}, [initializeUserData]);
|
||||||
|
|
||||||
|
// 可以在这里添加全局加载状态或错误处理的UI
|
||||||
|
// 但为了不影响现有的UI,这里只是静默处理
|
||||||
|
if (error) {
|
||||||
|
console.warn('用户数据初始化警告:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserDataProvider;
|
||||||
145
src/components/UserDataDebug.tsx
Normal file
145
src/components/UserDataDebug.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Card, CardContent, Typography, Button, Chip, Divider } from '@mui/material';
|
||||||
|
import { useUserData } from '@/hooks/useUserData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户数据调试组件
|
||||||
|
* 用于测试和查看全局用户状态
|
||||||
|
*/
|
||||||
|
const UserDataDebug: React.FC = () => {
|
||||||
|
const {
|
||||||
|
userInfo,
|
||||||
|
tenantInfo,
|
||||||
|
tenantList,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
initializeUserData,
|
||||||
|
refreshUserData,
|
||||||
|
logout,
|
||||||
|
} = useUserData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2, maxWidth: 800, mx: 'auto' }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
用户数据状态调试
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* 控制按钮 */}
|
||||||
|
<Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={initializeUserData}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
初始化数据
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={refreshUserData}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
刷新数据
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={logout}
|
||||||
|
>
|
||||||
|
清除数据
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 状态指示器 */}
|
||||||
|
<Box sx={{ mb: 2, display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||||
|
<Chip
|
||||||
|
label={isLoading ? '加载中' : '空闲'}
|
||||||
|
color={isLoading ? 'warning' : 'success'}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<Chip
|
||||||
|
label={`错误: ${error}`}
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 用户信息 */}
|
||||||
|
<Card sx={{ mb: 2 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
用户信息 (UserInfo)
|
||||||
|
</Typography>
|
||||||
|
{userInfo ? (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2"><strong>ID:</strong> {userInfo.id}</Typography>
|
||||||
|
<Typography variant="body2"><strong>邮箱:</strong> {userInfo.email}</Typography>
|
||||||
|
<Typography variant="body2"><strong>昵称:</strong> {userInfo.nickname}</Typography>
|
||||||
|
<Typography variant="body2"><strong>语言:</strong> {userInfo.language}</Typography>
|
||||||
|
<Typography variant="body2"><strong>时区:</strong> {userInfo.timezone}</Typography>
|
||||||
|
<Typography variant="body2"><strong>是否超级用户:</strong> {userInfo.is_superuser ? '是' : '否'}</Typography>
|
||||||
|
<Typography variant="body2"><strong>最后登录:</strong> {userInfo.last_login_time}</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
暂无用户信息
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 租户信息 */}
|
||||||
|
<Card sx={{ mb: 2 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
租户信息 (TenantInfo)
|
||||||
|
</Typography>
|
||||||
|
{tenantInfo ? (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2"><strong>租户ID:</strong> {tenantInfo.tenant_id}</Typography>
|
||||||
|
<Typography variant="body2"><strong>名称:</strong> {tenantInfo.name}</Typography>
|
||||||
|
<Typography variant="body2"><strong>角色:</strong> {tenantInfo.role}</Typography>
|
||||||
|
<Typography variant="body2"><strong>ASR ID:</strong> {tenantInfo.asr_id}</Typography>
|
||||||
|
<Typography variant="body2"><strong>嵌入模型ID:</strong> {tenantInfo.embd_id}</Typography>
|
||||||
|
<Typography variant="body2"><strong>LLM ID:</strong> {tenantInfo.llm_id}</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
暂无租户信息
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 租户列表 */}
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
租户列表 (TenantList) - 共 {tenantList.length} 个
|
||||||
|
</Typography>
|
||||||
|
{tenantList.length > 0 ? (
|
||||||
|
<Box>
|
||||||
|
{tenantList.map((tenant, index) => (
|
||||||
|
<Box key={tenant.tenant_id} sx={{ mb: 1 }}>
|
||||||
|
{index > 0 && <Divider sx={{ my: 1 }} />}
|
||||||
|
<Typography variant="body2"><strong>租户ID:</strong> {tenant.tenant_id}</Typography>
|
||||||
|
<Typography variant="body2"><strong>邮箱:</strong> {tenant.email}</Typography>
|
||||||
|
<Typography variant="body2"><strong>昵称:</strong> {tenant.nickname}</Typography>
|
||||||
|
<Typography variant="body2"><strong>角色:</strong> {tenant.role}</Typography>
|
||||||
|
<Typography variant="body2"><strong>更新时间:</strong> {tenant.update_date}</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
暂无租户列表
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserDataDebug;
|
||||||
197
src/hooks/knowledge_hooks.ts
Normal file
197
src/hooks/knowledge_hooks.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
|
import type { IKnowledge } from '@/interfaces/database/knowledge';
|
||||||
|
|
||||||
|
// 知识库列表查询参数接口
|
||||||
|
export interface KnowledgeListParams {
|
||||||
|
keywords?: string;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库列表响应接口
|
||||||
|
export interface KnowledgeListResponse {
|
||||||
|
kbs: IKnowledge[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库列表Hook状态接口
|
||||||
|
export interface UseKnowledgeListState {
|
||||||
|
knowledgeBases: IKnowledge[];
|
||||||
|
total: number;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
keywords: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库列表Hook返回值接口
|
||||||
|
export interface UseKnowledgeListReturn extends UseKnowledgeListState {
|
||||||
|
fetchKnowledgeBases: (params?: KnowledgeListParams) => Promise<void>;
|
||||||
|
setKeywords: (keywords: string) => void;
|
||||||
|
setCurrentPage: (page: number) => void;
|
||||||
|
setPageSize: (size: number) => void;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库列表数据管理Hook
|
||||||
|
* 支持关键词搜索、分页等功能
|
||||||
|
*/
|
||||||
|
export const useKnowledgeList = (
|
||||||
|
initialParams?: KnowledgeListParams
|
||||||
|
): UseKnowledgeListReturn => {
|
||||||
|
const [knowledgeBases, setKnowledgeBases] = useState<IKnowledge[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
|
||||||
|
const [pageSize, setPageSize] = useState(initialParams?.page_size || 10);
|
||||||
|
const [keywords, setKeywords] = useState(initialParams?.keywords || '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库列表
|
||||||
|
*/
|
||||||
|
const fetchKnowledgeBases = useCallback(async (params?: KnowledgeListParams) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// 合并参数
|
||||||
|
const queryParams = {
|
||||||
|
keywords: params?.keywords ?? keywords,
|
||||||
|
page: params?.page ?? currentPage,
|
||||||
|
page_size: params?.page_size ?? pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
const requestBody: any = {};
|
||||||
|
if (queryParams.keywords) {
|
||||||
|
requestBody.keywords = queryParams.keywords;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建查询参数
|
||||||
|
const requestParams: any = {};
|
||||||
|
if (queryParams.page) {
|
||||||
|
requestParams.page = queryParams.page;
|
||||||
|
}
|
||||||
|
if (queryParams.page_size) {
|
||||||
|
requestParams.page_size = queryParams.page_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await knowledgeService.getKnowledgeList(
|
||||||
|
Object.keys(requestParams).length > 0 ? requestParams : undefined,
|
||||||
|
Object.keys(requestBody).length > 0 ? requestBody : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
const data = response.data.data as KnowledgeListResponse;
|
||||||
|
setKnowledgeBases(data.kbs || []);
|
||||||
|
setTotal(data.total || 0);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '获取知识库列表失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = err.response?.data?.message || err.message || '获取知识库列表失败';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Failed to fetch knowledge bases:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [keywords, currentPage, pageSize]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新当前页面数据
|
||||||
|
*/
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
return fetchKnowledgeBases();
|
||||||
|
}, [fetchKnowledgeBases]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置关键词并重置到第一页
|
||||||
|
*/
|
||||||
|
const handleSetKeywords = useCallback((newKeywords: string) => {
|
||||||
|
setKeywords(newKeywords);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前页
|
||||||
|
*/
|
||||||
|
const handleSetCurrentPage = useCallback((page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置页面大小并重置到第一页
|
||||||
|
*/
|
||||||
|
const handleSetPageSize = useCallback((size: number) => {
|
||||||
|
setPageSize(size);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 初始化时获取数据
|
||||||
|
useEffect(() => {
|
||||||
|
fetchKnowledgeBases();
|
||||||
|
}, [fetchKnowledgeBases]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
knowledgeBases,
|
||||||
|
total,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
keywords,
|
||||||
|
fetchKnowledgeBases,
|
||||||
|
setKeywords: handleSetKeywords,
|
||||||
|
setCurrentPage: handleSetCurrentPage,
|
||||||
|
setPageSize: handleSetPageSize,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库详情Hook
|
||||||
|
*/
|
||||||
|
export const useKnowledgeDetail = (kbId: string) => {
|
||||||
|
const [knowledge, setKnowledge] = useState<IKnowledge | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchKnowledgeDetail = useCallback(async () => {
|
||||||
|
if (!kbId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await knowledgeService.getKnowledgeDetail({ kb_id: kbId });
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
setKnowledge(response.data.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '获取知识库详情失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = err.response?.data?.message || err.message || '获取知识库详情失败';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Failed to fetch knowledge detail:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [kbId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchKnowledgeDetail();
|
||||||
|
}, [fetchKnowledgeDetail]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
knowledge,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh: fetchKnowledgeDetail,
|
||||||
|
};
|
||||||
|
};
|
||||||
154
src/hooks/useUserData.ts
Normal file
154
src/hooks/useUserData.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
|
import { useUserStore } from '@/stores/userStore';
|
||||||
|
import userService from '@/services/user_service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户数据管理Hook
|
||||||
|
* 负责在登录后获取用户相关的全局数据
|
||||||
|
*/
|
||||||
|
export const useUserData = () => {
|
||||||
|
const {
|
||||||
|
userInfo,
|
||||||
|
tenantInfo,
|
||||||
|
tenantList,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
setUserInfo,
|
||||||
|
setTenantInfo,
|
||||||
|
setTenantList,
|
||||||
|
setLoading,
|
||||||
|
setError,
|
||||||
|
clearUserData,
|
||||||
|
} = useUserStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
const fetchUserInfo = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await userService.getUserInfo();
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
setUserInfo(response.data.data);
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '获取用户信息失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [setUserInfo]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取租户信息
|
||||||
|
*/
|
||||||
|
const fetchTenantInfo = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await userService.getTenantInfo();
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
setTenantInfo(response.data.data);
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '获取租户信息失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取租户信息失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [setTenantInfo]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取租户列表
|
||||||
|
*/
|
||||||
|
const fetchTenantList = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await userService.listTenant();
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
setTenantList(response.data.data || []);
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '获取租户列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取租户列表失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [setTenantList]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化所有用户数据
|
||||||
|
* 并行请求三个API以提高性能
|
||||||
|
*/
|
||||||
|
const initializeUserData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 并行请求三个API
|
||||||
|
const [userInfoResult, tenantInfoResult, tenantListResult] = await Promise.allSettled([
|
||||||
|
fetchUserInfo(),
|
||||||
|
fetchTenantInfo(),
|
||||||
|
fetchTenantList(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 检查是否有失败的请求
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (userInfoResult.status === 'rejected') {
|
||||||
|
errors.push(`用户信息: ${userInfoResult.reason.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tenantInfoResult.status === 'rejected') {
|
||||||
|
errors.push(`租户信息: ${tenantInfoResult.reason.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tenantListResult.status === 'rejected') {
|
||||||
|
errors.push(`租户列表: ${tenantListResult.reason.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const errorMessage = `部分数据获取失败: ${errors.join(', ')}`;
|
||||||
|
setError(errorMessage);
|
||||||
|
console.warn(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '初始化用户数据失败';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('初始化用户数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [fetchUserInfo, fetchTenantInfo, fetchTenantList, setLoading, setError]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新用户数据
|
||||||
|
*/
|
||||||
|
const refreshUserData = useCallback(async () => {
|
||||||
|
await initializeUserData();
|
||||||
|
}, [initializeUserData]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除用户数据
|
||||||
|
*/
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
clearUserData();
|
||||||
|
}, [clearUserData]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
userInfo,
|
||||||
|
tenantInfo,
|
||||||
|
tenantList,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
initializeUserData,
|
||||||
|
refreshUserData,
|
||||||
|
fetchUserInfo,
|
||||||
|
fetchTenantInfo,
|
||||||
|
fetchTenantList,
|
||||||
|
logout,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -30,9 +30,10 @@ export interface ISwitchForm {
|
|||||||
no: string;
|
no: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { AgentCategory } from '@/constants/agent';
|
|
||||||
import { Edge, Node } from '@xyflow/react';
|
import type { AgentCategory } from '@/constants/agent';
|
||||||
import { IReference, Message } from './chat';
|
import type { Edge, Node } from '@xyflow/react';
|
||||||
|
import type { IReference, Message } from './chat';
|
||||||
|
|
||||||
export type DSLComponents = Record<string, IOperator>;
|
export type DSLComponents = Record<string, IOperator>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RunningStatus } from '@/constants/knowledge';
|
import type { RunningStatus } from '@/constants/knowledge';
|
||||||
|
|
||||||
export interface IDocumentInfo {
|
export interface IDocumentInfo {
|
||||||
chunk_num: number;
|
chunk_num: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Edge, Node } from '@xyflow/react';
|
import { Edge, Node } from '@xyflow/react';
|
||||||
import { IReference, Message } from './chat';
|
import type { IReference, Message } from './chat';
|
||||||
|
|
||||||
export type DSLComponents = Record<string, IOperator>;
|
export type DSLComponents = Record<string, IOperator>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { RunningStatus } from '@/constants/knowledge';
|
import type { RunningStatus } from '@/constants/knowledge';
|
||||||
import { TreeData } from '@antv/g6/lib/types';
|
|
||||||
|
|
||||||
// knowledge base
|
// knowledge base
|
||||||
export interface IKnowledge {
|
export interface IKnowledge {
|
||||||
@@ -10,6 +9,7 @@ export interface IKnowledge {
|
|||||||
created_by: string;
|
created_by: string;
|
||||||
description: string;
|
description: string;
|
||||||
doc_num: number;
|
doc_num: number;
|
||||||
|
language: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
parser_config: ParserConfig;
|
parser_config: ParserConfig;
|
||||||
@@ -164,5 +164,5 @@ export type IRenameTag = { fromTag: string; toTag: string };
|
|||||||
|
|
||||||
export interface IKnowledgeGraph {
|
export interface IKnowledgeGraph {
|
||||||
graph: Record<string, any>;
|
graph: Record<string, any>;
|
||||||
mind_map: TreeData;
|
// mind_map: TreeData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import {
|
|||||||
CheckCircle as CheckCircleIcon,
|
CheckCircle as CheckCircleIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
|
import KnowledgeGridView from '@/components/KnowledgeGridView';
|
||||||
|
import UserDataDebug from '@/components/UserDataDebug';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const PageContainer = styled(Box)(({ theme }) => ({
|
const PageContainer = styled(Box)(({ theme }) => ({
|
||||||
padding: '1.5rem',
|
padding: '1.5rem',
|
||||||
@@ -128,6 +131,96 @@ const mockRecentQueries = [
|
|||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const [timeRange, setTimeRange] = useState('24h');
|
const [timeRange, setTimeRange] = useState('24h');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 模拟知识库数据
|
||||||
|
const mockKnowledgeBases = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: '产品文档库',
|
||||||
|
description: '包含所有产品相关的技术文档和用户手册',
|
||||||
|
status: '1',
|
||||||
|
doc_num: 156,
|
||||||
|
chunk_num: 1240,
|
||||||
|
size: 2400000000, // 2.4GB
|
||||||
|
update_date: '2024-01-15',
|
||||||
|
created_by: 'admin',
|
||||||
|
nickname: '管理员',
|
||||||
|
create_date: '2024-01-01',
|
||||||
|
create_time: 1704067200,
|
||||||
|
tenant_id: 'tenant1',
|
||||||
|
token_num: 50000,
|
||||||
|
parser_config: {},
|
||||||
|
parser_id: 'parser1',
|
||||||
|
pipeline_id: 'pipeline1',
|
||||||
|
pipeline_name: 'Default Pipeline',
|
||||||
|
pipeline_avatar: '',
|
||||||
|
permission: 'read',
|
||||||
|
similarity_threshold: 0.8,
|
||||||
|
update_time: 1705305600,
|
||||||
|
vector_similarity_weight: 0.7,
|
||||||
|
embd_id: 'embd1',
|
||||||
|
operator_permission: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: '客服FAQ',
|
||||||
|
description: '常见问题解答和客服对话记录',
|
||||||
|
status: '1',
|
||||||
|
doc_num: 89,
|
||||||
|
chunk_num: 670,
|
||||||
|
size: 1100000000, // 1.1GB
|
||||||
|
update_date: '2024-01-14',
|
||||||
|
created_by: 'support',
|
||||||
|
nickname: '客服团队',
|
||||||
|
create_date: '2024-01-02',
|
||||||
|
create_time: 1704153600,
|
||||||
|
tenant_id: 'tenant1',
|
||||||
|
token_num: 30000,
|
||||||
|
parser_config: {},
|
||||||
|
parser_id: 'parser1',
|
||||||
|
pipeline_id: 'pipeline1',
|
||||||
|
pipeline_name: 'Default Pipeline',
|
||||||
|
pipeline_avatar: '',
|
||||||
|
permission: 'read',
|
||||||
|
similarity_threshold: 0.8,
|
||||||
|
update_time: 1705219200,
|
||||||
|
vector_similarity_weight: 0.7,
|
||||||
|
embd_id: 'embd1',
|
||||||
|
operator_permission: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: '法律合规',
|
||||||
|
description: '法律条文、合规要求和相关政策文档',
|
||||||
|
status: '0',
|
||||||
|
doc_num: 234,
|
||||||
|
chunk_num: 1890,
|
||||||
|
size: 3700000000, // 3.7GB
|
||||||
|
update_date: '2024-01-13',
|
||||||
|
created_by: 'legal',
|
||||||
|
nickname: '法务部',
|
||||||
|
create_date: '2024-01-03',
|
||||||
|
create_time: 1704240000,
|
||||||
|
tenant_id: 'tenant1',
|
||||||
|
token_num: 75000,
|
||||||
|
parser_config: {},
|
||||||
|
parser_id: 'parser1',
|
||||||
|
pipeline_id: 'pipeline1',
|
||||||
|
pipeline_name: 'Default Pipeline',
|
||||||
|
pipeline_avatar: '',
|
||||||
|
permission: 'read',
|
||||||
|
similarity_threshold: 0.8,
|
||||||
|
update_time: 1705132800,
|
||||||
|
vector_similarity_weight: 0.7,
|
||||||
|
embd_id: 'embd1',
|
||||||
|
operator_permission: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSeeAllKnowledgeBases = () => {
|
||||||
|
navigate('/knowledge');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
@@ -253,6 +346,30 @@ const Dashboard: React.FC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* 知识库概览 */}
|
||||||
|
<Card sx={{ border: '1px solid #E5E5E5', mb: 3 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||||
|
<Typography variant="h6" fontWeight={600}>
|
||||||
|
知识库概览
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={handleSeeAllKnowledgeBases}
|
||||||
|
>
|
||||||
|
查看全部
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<KnowledgeGridView
|
||||||
|
knowledgeBases={mockKnowledgeBases}
|
||||||
|
maxItems={3}
|
||||||
|
showSeeAll={true}
|
||||||
|
onSeeAll={handleSeeAllKnowledgeBases}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 系统状态 */}
|
{/* 系统状态 */}
|
||||||
<Grid container spacing={3} sx={{ mb: 3 }}>
|
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
@@ -362,6 +479,15 @@ const Dashboard: React.FC = () => {
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 用户数据调试组件 - 仅在开发环境显示 */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<Card sx={{ border: '1px solid #E5E5E5', mt: 3 }}>
|
||||||
|
<CardContent>
|
||||||
|
<UserDataDebug />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,264 +1,239 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Grid,
|
|
||||||
Chip,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
TextField,
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Button,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
Fab,
|
Pagination,
|
||||||
|
Stack,
|
||||||
|
CircularProgress,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
Add as AddIcon,
|
Add as AddIcon,
|
||||||
MoreVert as MoreVertIcon,
|
Refresh as RefreshIcon,
|
||||||
Folder as FolderIcon,
|
|
||||||
Description as DocumentIcon,
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { styled } from '@mui/material/styles';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useKnowledgeList } from '@/hooks/knowledge_hooks';
|
||||||
const PageContainer = styled(Box)(({ theme }) => ({
|
import KnowledgeGridView from '@/components/KnowledgeGridView';
|
||||||
padding: '1.5rem',
|
import type { IKnowledge } from '@/interfaces/database/knowledge';
|
||||||
backgroundColor: '#F8F9FA',
|
|
||||||
minHeight: 'calc(100vh - 60px)',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const PageHeader = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: '1.5rem',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const SearchContainer = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
gap: '1rem',
|
|
||||||
marginBottom: '1.5rem',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const KBCard = styled(Card)(({ theme }) => ({
|
|
||||||
height: '100%',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
border: '1px solid #E5E5E5',
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
|
||||||
transform: 'translateY(-2px)',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StatusChip = styled(Chip)<{ status: string }>(({ status, theme }) => ({
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
height: '24px',
|
|
||||||
backgroundColor:
|
|
||||||
status === 'active' ? '#E8F5E8' :
|
|
||||||
status === 'processing' ? '#FFF3CD' : '#F8D7DA',
|
|
||||||
color:
|
|
||||||
status === 'active' ? '#155724' :
|
|
||||||
status === 'processing' ? '#856404' : '#721C24',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StatsBox = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginTop: '1rem',
|
|
||||||
padding: '0.75rem',
|
|
||||||
backgroundColor: '#F8F9FA',
|
|
||||||
borderRadius: '6px',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StatItem = styled(Box)(({ theme }) => ({
|
|
||||||
textAlign: 'center',
|
|
||||||
'& .number': {
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
'& .label': {
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: '#666',
|
|
||||||
marginTop: '0.25rem',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockKnowledgeBases = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: '产品文档库',
|
|
||||||
description: '包含所有产品相关的技术文档和用户手册',
|
|
||||||
status: 'active',
|
|
||||||
documents: 156,
|
|
||||||
size: '2.3 GB',
|
|
||||||
lastUpdated: '2024-01-15',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '客服FAQ',
|
|
||||||
description: '常见问题解答和客服对话记录',
|
|
||||||
status: 'processing',
|
|
||||||
documents: 89,
|
|
||||||
size: '1.1 GB',
|
|
||||||
lastUpdated: '2024-01-14',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '法律合规',
|
|
||||||
description: '法律条文、合规要求和相关政策文档',
|
|
||||||
status: 'active',
|
|
||||||
documents: 234,
|
|
||||||
size: '3.7 GB',
|
|
||||||
lastUpdated: '2024-01-13',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: '培训资料',
|
|
||||||
description: '员工培训材料和学习资源',
|
|
||||||
status: 'inactive',
|
|
||||||
documents: 67,
|
|
||||||
size: '890 MB',
|
|
||||||
lastUpdated: '2024-01-10',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const KnowledgeBaseList: React.FC = () => {
|
const KnowledgeBaseList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 搜索和筛选状态
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [teamFilter, setTeamFilter] = useState('all');
|
||||||
const [selectedKB, setSelectedKB] = useState<number | null>(null);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const pageSize = 12; // 每页显示12个知识库
|
||||||
|
|
||||||
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, kbId: number) => {
|
// 使用knowledge_hooks获取数据
|
||||||
setAnchorEl(event.currentTarget);
|
const {
|
||||||
setSelectedKB(kbId);
|
knowledgeBases,
|
||||||
};
|
loading,
|
||||||
|
error,
|
||||||
|
refresh,
|
||||||
|
} = useKnowledgeList({
|
||||||
|
keywords: searchTerm,
|
||||||
|
page: currentPage,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
const handleMenuClose = () => {
|
// 处理搜索
|
||||||
setAnchorEl(null);
|
const handleSearch = useCallback((value: string) => {
|
||||||
setSelectedKB(null);
|
setSearchTerm(value);
|
||||||
};
|
setCurrentPage(1); // 搜索时重置到第一页
|
||||||
|
}, []);
|
||||||
|
|
||||||
const filteredKBs = mockKnowledgeBases.filter(kb =>
|
// 处理团队筛选
|
||||||
kb.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const handleTeamFilterChange = useCallback((value: string) => {
|
||||||
kb.description.toLowerCase().includes(searchTerm.toLowerCase())
|
setTeamFilter(value);
|
||||||
);
|
setCurrentPage(1); // 筛选时重置到第一页
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理分页变化
|
||||||
|
const handlePageChange = useCallback((_: React.ChangeEvent<unknown>, page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理刷新
|
||||||
|
const handleRefresh = useCallback(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
// 处理创建知识库
|
||||||
|
const handleCreateKnowledge = useCallback(() => {
|
||||||
|
navigate('/knowledge/create');
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// 处理编辑知识库
|
||||||
|
const handleEditKnowledge = useCallback((kb: IKnowledge) => {
|
||||||
|
navigate(`/knowledge/${kb.id}/edit`);
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// 处理查看知识库详情
|
||||||
|
const handleViewKnowledge = useCallback((kb: IKnowledge) => {
|
||||||
|
navigate(`/knowledge/${kb.id}`);
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// 处理删除知识库
|
||||||
|
const handleDeleteKnowledge = useCallback((kb: IKnowledge) => {
|
||||||
|
// TODO: 实现删除逻辑
|
||||||
|
console.log('删除知识库:', kb.id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 根据团队筛选过滤知识库
|
||||||
|
const filteredKnowledgeBases = React.useMemo(() => {
|
||||||
|
if (!knowledgeBases) return [];
|
||||||
|
|
||||||
|
if (teamFilter === 'all') {
|
||||||
|
return knowledgeBases;
|
||||||
|
}
|
||||||
|
|
||||||
|
return knowledgeBases.filter(kb => {
|
||||||
|
if (teamFilter === 'me') return kb.permission === 'me';
|
||||||
|
if (teamFilter === 'team') return kb.permission === 'team';
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [knowledgeBases, teamFilter]);
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
const totalPages = Math.ceil((knowledgeBases?.length || 0) / pageSize);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<Box sx={{ p: 3 }}>
|
||||||
<PageHeader>
|
{/* 页面标题 */}
|
||||||
<Typography variant="h4" fontWeight={600} color="#333">
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
|
<Typography variant="h4" fontWeight={600}>
|
||||||
知识库管理
|
知识库管理
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
sx={{ borderRadius: '6px' }}
|
onClick={handleCreateKnowledge}
|
||||||
|
sx={{ borderRadius: 2 }}
|
||||||
>
|
>
|
||||||
新建知识库
|
新建知识库
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</Box>
|
||||||
|
|
||||||
<SearchContainer>
|
{/* 搜索和筛选区域 */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mb: 3, alignItems: 'center' }}>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="搜索知识库..."
|
placeholder="搜索知识库..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
sx={{ flex: 1, maxWidth: 400 }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
<SearchIcon color="action" />
|
<SearchIcon />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
sx={{ width: '400px' }}
|
|
||||||
/>
|
/>
|
||||||
</SearchContainer>
|
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<FormControl sx={{ minWidth: 120 }}>
|
||||||
{filteredKBs.map((kb) => (
|
<InputLabel>团队筛选</InputLabel>
|
||||||
<Grid key={kb.id} size={{xs:12, sm:6, md:4}}>
|
<Select
|
||||||
<KBCard>
|
value={teamFilter}
|
||||||
<CardContent>
|
label="团队筛选"
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
onChange={(e) => handleTeamFilterChange(e.target.value)}
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
>
|
||||||
<FolderIcon color="primary" />
|
<MenuItem value="all">全部</MenuItem>
|
||||||
<Typography variant="h6" fontWeight={600}>
|
<MenuItem value="me">我的</MenuItem>
|
||||||
{kb.name}
|
<MenuItem value="team">团队</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{error && (
|
||||||
|
<Box sx={{ mb: 3, p: 2, bgcolor: 'error.light', borderRadius: 1 }}>
|
||||||
|
<Typography color="error">
|
||||||
|
加载知识库列表失败: {error}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
)}
|
||||||
<StatusChip
|
|
||||||
status={kb.status}
|
{/* 加载状态 */}
|
||||||
label={
|
{loading && (
|
||||||
kb.status === 'active' ? '活跃' :
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||||
kb.status === 'processing' ? '处理中' : '未激活'
|
<CircularProgress />
|
||||||
}
|
</Box>
|
||||||
size="small"
|
)}
|
||||||
|
|
||||||
|
{/* 知识库列表 */}
|
||||||
|
{!loading && (
|
||||||
|
<>
|
||||||
|
<KnowledgeGridView
|
||||||
|
knowledgeBases={filteredKnowledgeBases}
|
||||||
|
onEdit={handleEditKnowledge}
|
||||||
|
onDelete={handleDeleteKnowledge}
|
||||||
|
onView={handleViewKnowledge}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={(e) => handleMenuClick(e, kb.id)}
|
|
||||||
>
|
|
||||||
<MoreVertIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography
|
{/* 分页组件 */}
|
||||||
variant="body2"
|
{totalPages > 1 && (
|
||||||
color="text.secondary"
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||||
sx={{ mt: 1, mb: 2 }}
|
<Stack spacing={2}>
|
||||||
>
|
<Pagination
|
||||||
{kb.description}
|
count={totalPages}
|
||||||
</Typography>
|
page={currentPage}
|
||||||
|
onChange={handlePageChange}
|
||||||
<StatsBox>
|
|
||||||
<StatItem>
|
|
||||||
<div className="number">{kb.documents}</div>
|
|
||||||
<div className="label">文档数量</div>
|
|
||||||
</StatItem>
|
|
||||||
<StatItem>
|
|
||||||
<div className="number">{kb.size}</div>
|
|
||||||
<div className="label">存储大小</div>
|
|
||||||
</StatItem>
|
|
||||||
</StatsBox>
|
|
||||||
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
|
||||||
最后更新: {kb.lastUpdated}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</KBCard>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={handleMenuClose}
|
|
||||||
>
|
|
||||||
<MenuItem onClick={handleMenuClose}>编辑</MenuItem>
|
|
||||||
<MenuItem onClick={handleMenuClose}>查看详情</MenuItem>
|
|
||||||
<MenuItem onClick={handleMenuClose}>导出</MenuItem>
|
|
||||||
<MenuItem onClick={handleMenuClose} sx={{ color: 'error.main' }}>
|
|
||||||
删除
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<Fab
|
|
||||||
color="primary"
|
color="primary"
|
||||||
aria-label="add"
|
size="large"
|
||||||
sx={{
|
showFirstButton
|
||||||
position: 'fixed',
|
showLastButton
|
||||||
bottom: 24,
|
/>
|
||||||
right: 24,
|
<Typography variant="body2" color="text.secondary" textAlign="center">
|
||||||
}}
|
共 {knowledgeBases?.length || 0} 个知识库,第 {currentPage} 页,共 {totalPages} 页
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 空状态 */}
|
||||||
|
{!loading && filteredKnowledgeBases.length === 0 && !error && (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||||
|
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||||
|
{searchTerm || teamFilter !== 'all' ? '没有找到匹配的知识库' : '暂无知识库'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
{searchTerm || teamFilter !== 'all'
|
||||||
|
? '尝试调整搜索条件或筛选器'
|
||||||
|
: '创建您的第一个知识库开始使用'
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
{(!searchTerm && teamFilter === 'all') && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={handleCreateKnowledge}
|
||||||
>
|
>
|
||||||
<AddIcon />
|
新建知识库
|
||||||
</Fab>
|
</Button>
|
||||||
</PageContainer>
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const AppRoutes = () => {
|
|||||||
|
|
||||||
{/* 使用MainLayout作为受保护路由的布局 */}
|
{/* 使用MainLayout作为受保护路由的布局 */}
|
||||||
<Route path="/" element={<MainLayout />}>
|
<Route path="/" element={<MainLayout />}>
|
||||||
<Route index element={<Home />} />
|
{/* <Route index element={<Home />} /> */}
|
||||||
<Route path="kb-list" element={<KnowledgeBaseList />} />
|
<Route index element={<KnowledgeBaseList />} />
|
||||||
<Route path="pipeline-config" element={<PipelineConfig />} />
|
<Route path="pipeline-config" element={<PipelineConfig />} />
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
<Route path="dashboard" element={<Dashboard />} />
|
||||||
<Route path="models-resources" element={<ModelsResources />} />
|
<Route path="models-resources" element={<ModelsResources />} />
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import api from './api';
|
import api from './api';
|
||||||
import request, { post } from '@/utils/request';
|
import request, { post } from '@/utils/request';
|
||||||
|
import type { ITenantInfo } from '@/interfaces/database/knowledge';
|
||||||
|
import type { IUserInfo, ITenant } from '@/interfaces/database/user-setting';
|
||||||
|
|
||||||
// 用户相关API服务
|
// 用户相关API服务
|
||||||
const userService = {
|
const userService = {
|
||||||
@@ -34,7 +36,7 @@ const userService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 设置租户信息
|
// 设置租户信息
|
||||||
setTenantInfo: (data: any) => {
|
setTenantInfo: (data: ITenantInfo) => {
|
||||||
return post(api.set_tenant_info, data);
|
return post(api.set_tenant_info, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
69
src/stores/userStore.ts
Normal file
69
src/stores/userStore.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import type { IUserInfo, ITenant } from '@/interfaces/database/user-setting';
|
||||||
|
import type { ITenantInfo } from '@/interfaces/database/knowledge';
|
||||||
|
|
||||||
|
interface UserState {
|
||||||
|
// 用户信息
|
||||||
|
userInfo: IUserInfo | null;
|
||||||
|
// 租户信息
|
||||||
|
tenantInfo: ITenantInfo | null;
|
||||||
|
// 租户列表
|
||||||
|
tenantList: ITenant[];
|
||||||
|
// 加载状态
|
||||||
|
isLoading: boolean;
|
||||||
|
// 错误信息
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setUserInfo: (userInfo: IUserInfo | null) => void;
|
||||||
|
setTenantInfo: (tenantInfo: ITenantInfo | null) => void;
|
||||||
|
setTenantList: (tenantList: ITenant[]) => void;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
setError: (error: string | null) => void;
|
||||||
|
clearUserData: () => void;
|
||||||
|
|
||||||
|
// 初始化用户数据
|
||||||
|
initializeUserData: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<UserState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
// 初始状态
|
||||||
|
userInfo: null,
|
||||||
|
tenantInfo: null,
|
||||||
|
tenantList: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setUserInfo: (userInfo) => set({ userInfo }),
|
||||||
|
setTenantInfo: (tenantInfo) => set({ tenantInfo }),
|
||||||
|
setTenantList: (tenantList) => set({ tenantList }),
|
||||||
|
setLoading: (isLoading) => set({ isLoading }),
|
||||||
|
setError: (error) => set({ error }),
|
||||||
|
|
||||||
|
clearUserData: () => set({
|
||||||
|
userInfo: null,
|
||||||
|
tenantInfo: null,
|
||||||
|
tenantList: [],
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 初始化用户数据方法
|
||||||
|
initializeUserData: async () => {
|
||||||
|
// 初始化时会在 UserDataProvider 中调用
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'user-store', // 持久化存储的key
|
||||||
|
partialize: (state) => ({
|
||||||
|
// 只持久化这些字段
|
||||||
|
userInfo: state.userInfo,
|
||||||
|
tenantInfo: state.tenantInfo,
|
||||||
|
tenantList: state.tenantList,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user