- 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
311 lines
8.4 KiB
TypeScript
311 lines
8.4 KiB
TypeScript
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; |