feat(pkb): Complete PKB module frontend migration with V3 design

Summary:
- Implement PKB Dashboard and Workspace pages based on V3 prototype
- Add single-layer header with integrated Tab navigation
- Implement 3 work modes: Full Text, Deep Read, Batch Processing
- Integrate Ant Design X Chat component for AI conversations
- Create BatchModeComplete with template selection and document processing
- Add compact work mode selector with dropdown design

Backend:
- Migrate PKB controllers and services to /modules/pkb structure
- Register v2 API routes at /api/v2/pkb/knowledge
- Maintain dual API routes for backward compatibility

Technical details:
- Use Zustand for state management
- Handle SSE streaming responses for AI chat
- Support document selection for Deep Read mode
- Implement batch processing with progress tracking

Known issues:
- Batch processing API integration pending
- Knowledge assets page navigation needs optimization

Status: Frontend functional, pending refinement
This commit is contained in:
2026-01-06 22:15:42 +08:00
parent b31255031e
commit 5a17d096a7
226 changed files with 14899 additions and 224 deletions

View File

@@ -0,0 +1,204 @@
import React from 'react';
import { Card, Button, Empty, Tag, Popconfirm, Space, Typography } from 'antd';
import {
PlusOutlined,
FolderOutlined,
FileTextOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons';
import type { KnowledgeBase } from '../../api/knowledgeBaseApi';
const { Title, Text, Paragraph } = Typography;
interface KnowledgeBaseListProps {
knowledgeBases: KnowledgeBase[];
loading: boolean;
onCreateClick: () => void;
onEditClick: (kb: KnowledgeBase) => void;
onDeleteClick: (kb: KnowledgeBase) => void;
onSelectClick: (kb: KnowledgeBase) => void;
}
const KnowledgeBaseList: React.FC<KnowledgeBaseListProps> = ({
knowledgeBases,
loading,
onCreateClick,
onEditClick,
onDeleteClick,
onSelectClick,
}) => {
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const canCreateMore = knowledgeBases.length < 3;
return (
<div>
{/* 标题和创建按钮 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24
}}>
<div>
<Title level={4} style={{ margin: 0 }}></Title>
<Text type="secondary">
使 {knowledgeBases.length}/3
</Text>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onCreateClick}
disabled={!canCreateMore || loading}
>
</Button>
</div>
{/* 配额提示 */}
{!canCreateMore && (
<div style={{ marginBottom: 16 }}>
<Tag color="warning">
3
</Tag>
</div>
)}
{/* 知识库列表 */}
{knowledgeBases.length === 0 ? (
<Card>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="还没有知识库,创建第一个吧!"
>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onCreateClick}
>
</Button>
</Empty>
</Card>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
gap: 16
}}>
{knowledgeBases.map((kb) => (
<Card
key={kb.id}
hoverable
onClick={() => onSelectClick(kb)}
style={{ cursor: 'pointer' }}
actions={[
<Button
key="edit"
type="text"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
onEditClick(kb);
}}
>
</Button>,
<Popconfirm
key="delete"
title="确认删除?"
description={`删除知识库 "${kb.name}" 将同时删除其中的所有文档,此操作不可恢复。`}
onConfirm={(e) => {
e?.stopPropagation();
onDeleteClick(kb);
}}
okText="确认"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
>
</Button>
</Popconfirm>,
]}
>
<Card.Meta
avatar={
<div style={{
width: 48,
height: 48,
background: '#1890ff20',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<FolderOutlined style={{ fontSize: 24, color: '#1890ff' }} />
</div>
}
title={
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Text strong ellipsis style={{ flex: 1 }}>
{kb.name}
</Text>
</div>
}
description={
<div>
<Paragraph
ellipsis={{ rows: 2 }}
type="secondary"
style={{ marginBottom: 12, minHeight: 44 }}
>
{kb.description || '暂无描述'}
</Paragraph>
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<FileTextOutlined style={{ marginRight: 8, color: '#8c8c8c' }} />
<Text type="secondary" style={{ fontSize: 13 }}>
{kb._count?.documents || kb.fileCount || 0}
</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Text type="secondary" style={{ fontSize: 13 }}>
: {formatFileSize(kb.totalSizeBytes)}
</Text>
</div>
<div style={{ marginTop: 4 }}>
<Text type="secondary" style={{ fontSize: 12 }}>
{new Date(kb.createdAt).toLocaleDateString()}
</Text>
</div>
</Space>
</div>
}
/>
</Card>
))}
</div>
)}
</div>
);
};
export default KnowledgeBaseList;