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 Header from './Header';
|
||||
import Sidebar from './Sidebar';
|
||||
import UserDataProvider from '../Provider/UserDataProvider';
|
||||
|
||||
const LayoutContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
@@ -24,6 +25,7 @@ const ContentArea = styled(Box)({
|
||||
|
||||
const MainLayout = () => {
|
||||
return (
|
||||
<UserDataProvider>
|
||||
<LayoutContainer>
|
||||
<Sidebar />
|
||||
<MainContent>
|
||||
@@ -33,6 +35,7 @@ const MainLayout = () => {
|
||||
</ContentArea>
|
||||
</MainContent>
|
||||
</LayoutContainer>
|
||||
</UserDataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import StorageOutlinedIcon from '@mui/icons-material/StorageOutlined';
|
||||
import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined';
|
||||
|
||||
const navItems = [
|
||||
{ text: 'Overview', path: '/', icon: DashboardOutlinedIcon },
|
||||
{ text: 'Knowledge Bases', path: '/kb-list', icon: LibraryBooksOutlinedIcon },
|
||||
// { text: 'Overview', path: '/', icon: DashboardOutlinedIcon },
|
||||
{ text: 'Knowledge Bases', path: '/', icon: LibraryBooksOutlinedIcon },
|
||||
{ text: 'RAG Pipeline', path: '/pipeline-config', icon: AccountTreeOutlinedIcon },
|
||||
{ text: 'Operations', path: '/dashboard', icon: SettingsOutlinedIcon },
|
||||
{ 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;
|
||||
}
|
||||
|
||||
import { AgentCategory } from '@/constants/agent';
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
import { IReference, Message } from './chat';
|
||||
|
||||
import type { AgentCategory } from '@/constants/agent';
|
||||
import type { Edge, Node } from '@xyflow/react';
|
||||
import type { IReference, Message } from './chat';
|
||||
|
||||
export type DSLComponents = Record<string, IOperator>;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RunningStatus } from '@/constants/knowledge';
|
||||
import type { RunningStatus } from '@/constants/knowledge';
|
||||
|
||||
export interface IDocumentInfo {
|
||||
chunk_num: number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
import { IReference, Message } from './chat';
|
||||
import type { IReference, Message } from './chat';
|
||||
|
||||
export type DSLComponents = Record<string, IOperator>;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RunningStatus } from '@/constants/knowledge';
|
||||
import { TreeData } from '@antv/g6/lib/types';
|
||||
import type { RunningStatus } from '@/constants/knowledge';
|
||||
|
||||
// knowledge base
|
||||
export interface IKnowledge {
|
||||
@@ -10,6 +9,7 @@ export interface IKnowledge {
|
||||
created_by: string;
|
||||
description: string;
|
||||
doc_num: number;
|
||||
language: string;
|
||||
id: string;
|
||||
name: string;
|
||||
parser_config: ParserConfig;
|
||||
@@ -164,5 +164,5 @@ export type IRenameTag = { fromTag: string; toTag: string };
|
||||
|
||||
export interface IKnowledgeGraph {
|
||||
graph: Record<string, any>;
|
||||
mind_map: TreeData;
|
||||
// mind_map: TreeData;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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}
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</Box>
|
||||
<Box>
|
||||
<StatusChip
|
||||
status={kb.status}
|
||||
label={
|
||||
kb.status === 'active' ? '活跃' :
|
||||
kb.status === 'processing' ? '处理中' : '未激活'
|
||||
}
|
||||
size="small"
|
||||
)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 知识库列表 */}
|
||||
{!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"
|
||||
color="text.secondary"
|
||||
sx={{ mt: 1, mb: 2 }}
|
||||
>
|
||||
{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
|
||||
{/* 分页组件 */}
|
||||
{totalPages > 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||
<Stack spacing={2}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={currentPage}
|
||||
onChange={handlePageChange}
|
||||
color="primary"
|
||||
aria-label="add"
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
}}
|
||||
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}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</PageContainer>
|
||||
新建知识库
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ const AppRoutes = () => {
|
||||
|
||||
{/* 使用MainLayout作为受保护路由的布局 */}
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="kb-list" element={<KnowledgeBaseList />} />
|
||||
{/* <Route index element={<Home />} /> */}
|
||||
<Route index element={<KnowledgeBaseList />} />
|
||||
<Route path="pipeline-config" element={<PipelineConfig />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="models-resources" element={<ModelsResources />} />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import api from './api';
|
||||
import request, { post } from '@/utils/request';
|
||||
import type { ITenantInfo } from '@/interfaces/database/knowledge';
|
||||
import type { IUserInfo, ITenant } from '@/interfaces/database/user-setting';
|
||||
|
||||
// 用户相关API服务
|
||||
const userService = {
|
||||
@@ -34,7 +36,7 @@ const userService = {
|
||||
},
|
||||
|
||||
// 设置租户信息
|
||||
setTenantInfo: (data: any) => {
|
||||
setTenantInfo: (data: ITenantInfo) => {
|
||||
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