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

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

View File

@@ -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,15 +25,17 @@ const ContentArea = styled(Box)({
const MainLayout = () => {
return (
<LayoutContainer>
<Sidebar />
<MainContent>
<Header />
<ContentArea>
<Outlet />
</ContentArea>
</MainContent>
</LayoutContainer>
<UserDataProvider>
<LayoutContainer>
<Sidebar />
<MainContent>
<Header />
<ContentArea>
<Outlet />
</ContentArea>
</MainContent>
</LayoutContainer>
</UserDataProvider>
);
};

View File

@@ -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 },

View 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;

View 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;

View 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
View 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,
};
};

View File

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

View File

@@ -1,4 +1,4 @@
import { RunningStatus } from '@/constants/knowledge';
import type { RunningStatus } from '@/constants/knowledge';
export interface IDocumentInfo {
chunk_num: number;

View File

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

View File

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

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

View File

@@ -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 />} />

View File

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