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:
2025-10-11 17:18:40 +08:00
parent 6f0332c1ff
commit 836ee763e3
16 changed files with 1256 additions and 243 deletions

View File

@@ -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>
);
};

View File

@@ -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>
);
};