Frontend start to use backend CRUD api

This commit is contained in:
Xin Wang
2026-02-08 15:08:18 +08:00
parent b9a315177a
commit 86744f0842
12 changed files with 768 additions and 154 deletions

View File

@@ -1,21 +1,43 @@
import React, { useState, useRef } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X } from 'lucide-react';
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog } from '../components/UI';
import { mockKnowledgeBases } from '../services/mockData';
import { KnowledgeBase } from '../types';
import { createKnowledgeBase, deleteKnowledgeDocument, fetchKnowledgeBases, uploadKnowledgeDocument } from '../services/backendApi';
export const KnowledgeBasePage: React.FC = () => {
const [view, setView] = useState<'list' | 'detail'>('list');
const [selectedKb, setSelectedKb] = useState<KnowledgeBase | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [kbs, setKbs] = useState(mockKnowledgeBases);
const [kbs, setKbs] = useState<KnowledgeBase[]>([]);
const [isUploadOpen, setIsUploadOpen] = useState(false);
const [isCreateKbOpen, setIsCreateKbOpen] = useState(false);
const [newKbName, setNewKbName] = useState('');
const [isLoading, setIsLoading] = useState(true);
const filteredKbs = kbs.filter(kb => kb.name.toLowerCase().includes(searchTerm.toLowerCase()));
const refreshKnowledgeBases = async () => {
setIsLoading(true);
try {
const list = await fetchKnowledgeBases();
setKbs(list);
if (selectedKb) {
const nextSelected = list.find((item) => item.id === selectedKb.id) || null;
setSelectedKb(nextSelected);
}
} catch (error) {
console.error(error);
alert('加载知识库失败,请检查后端服务。');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
refreshKnowledgeBases();
}, []);
const handleSelect = (kb: KnowledgeBase) => {
setSelectedKb(kb);
setView('detail');
@@ -25,20 +47,17 @@ export const KnowledgeBasePage: React.FC = () => {
setIsUploadOpen(true);
};
const handleCreateKb = () => {
const handleCreateKb = async () => {
if (!newKbName.trim()) return;
const newKb: KnowledgeBase = {
id: `kb_${Date.now()}`,
name: newKbName.trim(),
creator: 'Admin User',
createdAt: new Date().toISOString().split('T')[0],
documents: []
};
setKbs([newKb, ...kbs]);
setIsCreateKbOpen(false);
setNewKbName('');
try {
await createKnowledgeBase(newKbName.trim());
await refreshKnowledgeBases();
setIsCreateKbOpen(false);
setNewKbName('');
} catch (error) {
console.error(error);
alert('新建知识库失败。');
}
};
if (view === 'detail' && selectedKb) {
@@ -48,8 +67,22 @@ export const KnowledgeBasePage: React.FC = () => {
kb={selectedKb}
onBack={() => setView('list')}
onImport={handleImportClick}
onDeleteDocument={async (docId) => {
try {
await deleteKnowledgeDocument(selectedKb.id, docId);
await refreshKnowledgeBases();
} catch (error) {
console.error(error);
alert('删除文档失败。');
}
}}
/>
<UploadModal
kbId={selectedKb.id}
isOpen={isUploadOpen}
onClose={() => setIsUploadOpen(false)}
onUploaded={refreshKnowledgeBases}
/>
<UploadModal isOpen={isUploadOpen} onClose={() => setIsUploadOpen(false)} />
</div>
);
}
@@ -103,6 +136,12 @@ export const KnowledgeBasePage: React.FC = () => {
<Plus className="h-8 w-8 mb-2 opacity-50" />
<span></span>
</div>
{!isLoading && filteredKbs.length === 0 && (
<div className="col-span-full text-center text-muted-foreground py-8"></div>
)}
{isLoading && (
<div className="col-span-full text-center text-muted-foreground py-8">...</div>
)}
</div>
{/* New Knowledge Base Dialog */}
@@ -141,7 +180,8 @@ const KnowledgeBaseDetail: React.FC<{
kb: KnowledgeBase;
onBack: () => void;
onImport: () => void;
}> = ({ kb, onBack, onImport }) => {
onDeleteDocument: (docId: string) => void;
}> = ({ kb, onBack, onImport, onDeleteDocument }) => {
const [docSearch, setDocSearch] = useState('');
const filteredDocs = kb.documents.filter(d => d.name.toLowerCase().includes(docSearch.toLowerCase()));
@@ -192,7 +232,14 @@ const KnowledgeBaseDetail: React.FC<{
<TableCell className="text-muted-foreground">{doc.size}</TableCell>
<TableCell className="text-muted-foreground">{doc.uploadDate}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive/80"></Button>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive/80"
onClick={() => onDeleteDocument(doc.id)}
>
</Button>
</TableCell>
</TableRow>
)) : (
@@ -207,7 +254,7 @@ const KnowledgeBaseDetail: React.FC<{
);
};
const UploadModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, onClose }) => {
const UploadModal: React.FC<{ kbId: string; isOpen: boolean; onClose: () => void; onUploaded: () => Promise<void> }> = ({ kbId, isOpen, onClose, onUploaded }) => {
const [dragActive, setDragActive] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
@@ -242,6 +289,19 @@ const UploadModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpe
setFiles(prev => prev.filter((_, i) => i !== idx));
};
const handleUpload = async () => {
if (files.length === 0) return;
try {
await Promise.all(files.map((file) => uploadKnowledgeDocument(kbId, file)));
await onUploaded();
onClose();
setFiles([]);
} catch (error) {
console.error(error);
alert('上传失败,请稍后重试。');
}
};
return (
<Dialog
isOpen={isOpen}
@@ -250,7 +310,7 @@ const UploadModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpe
footer={
<>
<Button variant="ghost" onClick={onClose}></Button>
<Button onClick={() => { alert('Upload Started!'); onClose(); setFiles([]); }}></Button>
<Button onClick={handleUpload}></Button>
</>
}
>