Files
TERES_web_frontend/src/components/KnowledgeGridView.tsx
guangfei.zhao 836ee763e3 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
2025-10-11 17:18:40 +08:00

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;