From 836ee763e3e13206a3c650f9a74cb1c2bf03b4dd Mon Sep 17 00:00:00 2001 From: "guangfei.zhao" Date: Sat, 11 Oct 2025 17:18:40 +0800 Subject: [PATCH] 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 --- src/components/KnowledgeGridView.tsx | 311 ++++++++++++++ src/components/Layout/MainLayout.tsx | 21 +- src/components/Layout/Sidebar.tsx | 4 +- src/components/Provider/UserDataProvider.tsx | 30 ++ src/components/UserDataDebug.tsx | 145 +++++++ src/hooks/knowledge_hooks.ts | 197 +++++++++ src/hooks/useUserData.ts | 154 +++++++ src/interfaces/database/agent.ts | 7 +- src/interfaces/database/document.ts | 2 +- src/interfaces/database/flow.ts | 2 +- src/interfaces/database/knowledge.ts | 6 +- src/pages/Dashboard.tsx | 126 ++++++ src/pages/knowledge/KnowledgeBaseList.tsx | 417 +++++++++---------- src/routes/index.tsx | 4 +- src/services/user_service.ts | 4 +- src/stores/userStore.ts | 69 +++ 16 files changed, 1256 insertions(+), 243 deletions(-) create mode 100644 src/components/KnowledgeGridView.tsx create mode 100644 src/components/Provider/UserDataProvider.tsx create mode 100644 src/components/UserDataDebug.tsx create mode 100644 src/hooks/knowledge_hooks.ts create mode 100644 src/hooks/useUserData.ts create mode 100644 src/stores/userStore.ts diff --git a/src/components/KnowledgeGridView.tsx b/src/components/KnowledgeGridView.tsx new file mode 100644 index 0000000..af114f0 --- /dev/null +++ b/src/components/KnowledgeGridView.tsx @@ -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, kb: IKnowledge) => void; +} + +const KnowledgeCard: React.FC = ({ 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 ( + + + + + {/* 显示avatar */} + {knowledge.avatar ? ( + + ) : ( + + )} + + {knowledge.name} + + + + + onMenuClick(e, knowledge)} + > + + + + + + + {knowledge.description || '暂无描述'} + + + + + + {knowledge.doc_num || 0} + + + 文档数量 + + + + + {knowledge.chunk_num || 0} + + + 分块数量 + + + + + {formatTokenNum(knowledge.token_num || 0)} + + + Token数量 + + + + + {/* 显示更新时间 */} + + 最后更新: {formatUpdateTime(knowledge.update_time)} + + + {/* 显示创建者 */} + {knowledge.nickname && ( + + 创建者: {knowledge.nickname} + + )} + + {/* 显示语言 */} + {knowledge.language && ( + + 语言: {knowledge.language} + + )} + + + ); +}; + +const KnowledgeGridView: React.FC = ({ + knowledgeBases, + maxItems, + showSeeAll = false, + onSeeAll, + onEdit, + onDelete, + onView, + loading = false, +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const [selectedKB, setSelectedKB] = React.useState(null); + + const handleMenuClick = (event: React.MouseEvent, 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 ( + + 加载中... + + ); + } + + if (knowledgeBases.length === 0) { + return ( + + + + 暂无知识库 + + + 创建您的第一个知识库开始使用 + + + ); + } + + return ( + + + {displayedKBs.map((kb) => ( + + + + ))} + + + {showSeeAll && hasMore && ( + + + + )} + + + 查看详情 + 编辑 + 导出 + + 删除 + + + + ); +}; + +export default KnowledgeGridView; \ No newline at end of file diff --git a/src/components/Layout/MainLayout.tsx b/src/components/Layout/MainLayout.tsx index 91f4027..69fab2d 100644 --- a/src/components/Layout/MainLayout.tsx +++ b/src/components/Layout/MainLayout.tsx @@ -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 ( - - - -
- - - - - + + + + +
+ + + + + + ); }; diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index 0a1070f..386ace0 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -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 }, diff --git a/src/components/Provider/UserDataProvider.tsx b/src/components/Provider/UserDataProvider.tsx new file mode 100644 index 0000000..1369431 --- /dev/null +++ b/src/components/Provider/UserDataProvider.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, type PropsWithChildren } from 'react'; +import { useUserData } from '@/hooks/useUserData'; + +/** + * 用户数据提供者组件 + * 负责在应用启动时初始化用户数据 + */ +const UserDataProvider: React.FC = ({ 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; \ No newline at end of file diff --git a/src/components/UserDataDebug.tsx b/src/components/UserDataDebug.tsx new file mode 100644 index 0000000..1265799 --- /dev/null +++ b/src/components/UserDataDebug.tsx @@ -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 ( + + + 用户数据状态调试 + + + {/* 控制按钮 */} + + + + + + + {/* 状态指示器 */} + + + {error && ( + + )} + + + {/* 用户信息 */} + + + + 用户信息 (UserInfo) + + {userInfo ? ( + + ID: {userInfo.id} + 邮箱: {userInfo.email} + 昵称: {userInfo.nickname} + 语言: {userInfo.language} + 时区: {userInfo.timezone} + 是否超级用户: {userInfo.is_superuser ? '是' : '否'} + 最后登录: {userInfo.last_login_time} + + ) : ( + + 暂无用户信息 + + )} + + + + {/* 租户信息 */} + + + + 租户信息 (TenantInfo) + + {tenantInfo ? ( + + 租户ID: {tenantInfo.tenant_id} + 名称: {tenantInfo.name} + 角色: {tenantInfo.role} + ASR ID: {tenantInfo.asr_id} + 嵌入模型ID: {tenantInfo.embd_id} + LLM ID: {tenantInfo.llm_id} + + ) : ( + + 暂无租户信息 + + )} + + + + {/* 租户列表 */} + + + + 租户列表 (TenantList) - 共 {tenantList.length} 个 + + {tenantList.length > 0 ? ( + + {tenantList.map((tenant, index) => ( + + {index > 0 && } + 租户ID: {tenant.tenant_id} + 邮箱: {tenant.email} + 昵称: {tenant.nickname} + 角色: {tenant.role} + 更新时间: {tenant.update_date} + + ))} + + ) : ( + + 暂无租户列表 + + )} + + + + ); +}; + +export default UserDataDebug; \ No newline at end of file diff --git a/src/hooks/knowledge_hooks.ts b/src/hooks/knowledge_hooks.ts new file mode 100644 index 0000000..d7ba73c --- /dev/null +++ b/src/hooks/knowledge_hooks.ts @@ -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; + setKeywords: (keywords: string) => void; + setCurrentPage: (page: number) => void; + setPageSize: (size: number) => void; + refresh: () => Promise; +} + +/** + * 知识库列表数据管理Hook + * 支持关键词搜索、分页等功能 + */ +export const useKnowledgeList = ( + initialParams?: KnowledgeListParams +): UseKnowledgeListReturn => { + const [knowledgeBases, setKnowledgeBases] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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, + }; +}; \ No newline at end of file diff --git a/src/hooks/useUserData.ts b/src/hooks/useUserData.ts new file mode 100644 index 0000000..2185f5e --- /dev/null +++ b/src/hooks/useUserData.ts @@ -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, + }; +}; \ No newline at end of file diff --git a/src/interfaces/database/agent.ts b/src/interfaces/database/agent.ts index 8bd33e8..39fef8d 100644 --- a/src/interfaces/database/agent.ts +++ b/src/interfaces/database/agent.ts @@ -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; diff --git a/src/interfaces/database/document.ts b/src/interfaces/database/document.ts index 5edbad1..1efc672 100644 --- a/src/interfaces/database/document.ts +++ b/src/interfaces/database/document.ts @@ -1,4 +1,4 @@ -import { RunningStatus } from '@/constants/knowledge'; +import type { RunningStatus } from '@/constants/knowledge'; export interface IDocumentInfo { chunk_num: number; diff --git a/src/interfaces/database/flow.ts b/src/interfaces/database/flow.ts index a17d215..abcf060 100644 --- a/src/interfaces/database/flow.ts +++ b/src/interfaces/database/flow.ts @@ -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; diff --git a/src/interfaces/database/knowledge.ts b/src/interfaces/database/knowledge.ts index d768d8e..31cdd5a 100644 --- a/src/interfaces/database/knowledge.ts +++ b/src/interfaces/database/knowledge.ts @@ -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; - mind_map: TreeData; + // mind_map: TreeData; } diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index f675e66..43ea551 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -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 ( @@ -253,6 +346,30 @@ const Dashboard: React.FC = () => { + {/* 知识库概览 */} + + + + + 知识库概览 + + + + + + + {/* 系统状态 */} @@ -362,6 +479,15 @@ const Dashboard: React.FC = () => { + + {/* 用户数据调试组件 - 仅在开发环境显示 */} + {process.env.NODE_ENV === 'development' && ( + + + + + + )} ); }; diff --git a/src/pages/knowledge/KnowledgeBaseList.tsx b/src/pages/knowledge/KnowledgeBaseList.tsx index 2cb1364..c3c13d1 100644 --- a/src/pages/knowledge/KnowledgeBaseList.tsx +++ b/src/pages/knowledge/KnowledgeBaseList.tsx @@ -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); - const [selectedKB, setSelectedKB] = useState(null); + const [teamFilter, setTeamFilter] = useState('all'); + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 12; // 每页显示12个知识库 - const handleMenuClick = (event: React.MouseEvent, 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, 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 ( - - - + + {/* 页面标题 */} + + 知识库管理 - + - + {/* 搜索和筛选区域 */} + setSearchTerm(e.target.value)} + onChange={(e) => handleSearch(e.target.value)} + sx={{ flex: 1, maxWidth: 400 }} InputProps={{ startAdornment: ( - + ), }} - sx={{ width: '400px' }} /> - + + + 团队筛选 + + - - {filteredKBs.map((kb) => ( - - - - - - - - {kb.name} - - - - - handleMenuClick(e, kb.id)} - > - - - - + + - + + 加载知识库列表失败: {error} + + + )} + + {/* 加载状态 */} + {loading && ( + + + + )} + + {/* 知识库列表 */} + {!loading && ( + <> + + + {/* 分页组件 */} + {totalPages > 1 && ( + + + + + 共 {knowledgeBases?.length || 0} 个知识库,第 {currentPage} 页,共 {totalPages} 页 + + + + )} + + {/* 空状态 */} + {!loading && filteredKnowledgeBases.length === 0 && !error && ( + + + {searchTerm || teamFilter !== 'all' ? '没有找到匹配的知识库' : '暂无知识库'} + + + {searchTerm || teamFilter !== 'all' + ? '尝试调整搜索条件或筛选器' + : '创建您的第一个知识库开始使用' + } + + {(!searchTerm && teamFilter === 'all') && ( + + )} + + )} + + )} + ); }; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 4b9dbfe..eade56f 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -15,8 +15,8 @@ const AppRoutes = () => { {/* 使用MainLayout作为受保护路由的布局 */} }> - } /> - } /> + {/* } /> */} + } /> } /> } /> } /> diff --git a/src/services/user_service.ts b/src/services/user_service.ts index 2c52d4c..e27df1b 100644 --- a/src/services/user_service.ts +++ b/src/services/user_service.ts @@ -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); }, diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts new file mode 100644 index 0000000..b1dcd0a --- /dev/null +++ b/src/stores/userStore.ts @@ -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; +} + +export const useUserStore = create()( + 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, + }), + } + ) +); \ No newline at end of file