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:
@@ -29,6 +29,9 @@ import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
} from '@mui/icons-material';
|
||||
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 }) => ({
|
||||
padding: '1.5rem',
|
||||
@@ -128,6 +131,96 @@ const mockRecentQueries = [
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
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 (
|
||||
<PageContainer>
|
||||
@@ -253,6 +346,30 @@ const Dashboard: React.FC = () => {
|
||||
</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 item xs={12} md={6}>
|
||||
@@ -362,6 +479,15 @@ const Dashboard: React.FC = () => {
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 用户数据调试组件 - 仅在开发环境显示 */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Card sx={{ border: '1px solid #E5E5E5', mt: 3 }}>
|
||||
<CardContent>
|
||||
<UserDataDebug />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,264 +1,239 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
Chip,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Button,
|
||||
InputAdornment,
|
||||
Fab,
|
||||
Pagination,
|
||||
Stack,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
Add as AddIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
Folder as FolderIcon,
|
||||
Description as DocumentIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
const PageContainer = styled(Box)(({ theme }) => ({
|
||||
padding: '1.5rem',
|
||||
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',
|
||||
},
|
||||
];
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useKnowledgeList } from '@/hooks/knowledge_hooks';
|
||||
import KnowledgeGridView from '@/components/KnowledgeGridView';
|
||||
import type { IKnowledge } from '@/interfaces/database/knowledge';
|
||||
|
||||
const KnowledgeBaseList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 搜索和筛选状态
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedKB, setSelectedKB] = useState<number | null>(null);
|
||||
const [teamFilter, setTeamFilter] = useState('all');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 12; // 每页显示12个知识库
|
||||
|
||||
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, kbId: number) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedKB(kbId);
|
||||
};
|
||||
// 使用knowledge_hooks获取数据
|
||||
const {
|
||||
knowledgeBases,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
} = useKnowledgeList({
|
||||
keywords: searchTerm,
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
});
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedKB(null);
|
||||
};
|
||||
// 处理搜索
|
||||
const handleSearch = useCallback((value: string) => {
|
||||
setSearchTerm(value);
|
||||
setCurrentPage(1); // 搜索时重置到第一页
|
||||
}, []);
|
||||
|
||||
const filteredKBs = mockKnowledgeBases.filter(kb =>
|
||||
kb.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
kb.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
// 处理团队筛选
|
||||
const handleTeamFilterChange = useCallback((value: string) => {
|
||||
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 (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<Typography variant="h4" fontWeight={600} color="#333">
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* 页面标题 */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h4" fontWeight={600}>
|
||||
知识库管理
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
sx={{ borderRadius: '6px' }}
|
||||
onClick={handleCreateKnowledge}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
新建知识库
|
||||
</Button>
|
||||
</PageHeader>
|
||||
</Box>
|
||||
|
||||
<SearchContainer>
|
||||
{/* 搜索和筛选区域 */}
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3, alignItems: 'center' }}>
|
||||
<TextField
|
||||
placeholder="搜索知识库..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
sx={{ flex: 1, maxWidth: 400 }}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon color="action" />
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
sx={{ width: '400px' }}
|
||||
/>
|
||||
</SearchContainer>
|
||||
|
||||
<FormControl sx={{ minWidth: 120 }}>
|
||||
<InputLabel>团队筛选</InputLabel>
|
||||
<Select
|
||||
value={teamFilter}
|
||||
label="团队筛选"
|
||||
onChange={(e) => handleTeamFilterChange(e.target.value)}
|
||||
>
|
||||
<MenuItem value="all">全部</MenuItem>
|
||||
<MenuItem value="me">我的</MenuItem>
|
||||
<MenuItem value="team">团队</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{filteredKBs.map((kb) => (
|
||||
<Grid key={kb.id} size={{xs:12, sm:6, md:4}}>
|
||||
<KBCard>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<FolderIcon color="primary" />
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
{kb.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<StatusChip
|
||||
status={kb.status}
|
||||
label={
|
||||
kb.status === 'active' ? '活跃' :
|
||||
kb.status === 'processing' ? '处理中' : '未激活'
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleMenuClick(e, kb.id)}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ mt: 1, mb: 2 }}
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<Box sx={{ mb: 3, p: 2, bgcolor: 'error.light', borderRadius: 1 }}>
|
||||
<Typography color="error">
|
||||
加载知识库列表失败: {error}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 知识库列表 */}
|
||||
{!loading && (
|
||||
<>
|
||||
<KnowledgeGridView
|
||||
knowledgeBases={filteredKnowledgeBases}
|
||||
onEdit={handleEditKnowledge}
|
||||
onDelete={handleDeleteKnowledge}
|
||||
onView={handleViewKnowledge}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{/* 分页组件 */}
|
||||
{totalPages > 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||
<Stack spacing={2}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={currentPage}
|
||||
onChange={handlePageChange}
|
||||
color="primary"
|
||||
size="large"
|
||||
showFirstButton
|
||||
showLastButton
|
||||
/>
|
||||
<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}
|
||||
>
|
||||
{kb.description}
|
||||
</Typography>
|
||||
|
||||
<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"
|
||||
aria-label="add"
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</PageContainer>
|
||||
新建知识库
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user