diff --git a/src/icons/icons.js b/src/icons/icons.js index a5424d5..79c8a41 100644 --- a/src/icons/icons.js +++ b/src/icons/icons.js @@ -101,4 +101,5 @@ export const icons = { chat: ``, 'arrowup-upload': ``, send: ``, + search: `` }; diff --git a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx index 51dab0f..9f9e027 100644 --- a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx +++ b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx @@ -1,13 +1,21 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Link } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { showNotification } from '../../../store/notification.slice'; import SvgIcon from '../../../components/SvgIcon'; +// 导入拆分的组件 +import Breadcrumb from './components/Breadcrumb'; +import DocumentList from './components/DocumentList'; +import FileUploadModal from './components/FileUploadModal'; + export default function DatasetTab({ knowledgeBase }) { + const dispatch = useDispatch(); const [searchQuery, setSearchQuery] = useState(''); const [selectedDocuments, setSelectedDocuments] = useState([]); const [selectAll, setSelectAll] = useState(false); const [showBatchDropdown, setShowBatchDropdown] = useState(false); const [showAddFileModal, setShowAddFileModal] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const [newFile, setNewFile] = useState({ name: '', description: '', @@ -17,23 +25,13 @@ export default function DatasetTab({ knowledgeBase }) { const dropdownRef = useRef(null); const fileInputRef = useRef(null); - // Convert documents to state so we can update it - const [documents, setDocuments] = useState([ - { - id: '1001', - name: '测试数据集 001', - description: '产品相关的所有文档和说明', - size: '124kb', - updatedAt: '2023-05-15', - }, - { - id: '1002', - name: '产品分析数据', - description: '技术架构和API文档', - size: '89kb', - updatedAt: '2023-05-10', - }, - ]); + // Use documents from knowledge base or empty array if not available + const [documents, setDocuments] = useState(knowledgeBase.documents || []); + + // Update documents when knowledgeBase changes + useEffect(() => { + setDocuments(knowledgeBase.documents || []); + }, [knowledgeBase]); // Handle click outside dropdown useEffect(() => { @@ -51,6 +49,11 @@ export default function DatasetTab({ knowledgeBase }) { }; }, [dropdownRef]); + // Handle search input change + const handleSearchChange = (e) => { + setSearchQuery(e.target.value); + }; + // Handle select all checkbox const handleSelectAll = () => { if (selectAll) { @@ -84,6 +87,14 @@ export default function DatasetTab({ knowledgeBase }) { // Update documents state by removing selected documents setDocuments((prevDocuments) => prevDocuments.filter((doc) => !selectedDocuments.includes(doc.id))); + // Show notification + dispatch( + showNotification({ + message: '已删除选中的数据集', + type: 'success', + }) + ); + // Reset selection setSelectedDocuments([]); setSelectAll(false); @@ -166,35 +177,48 @@ export default function DatasetTab({ knowledgeBase }) { return; } + setIsSubmitting(true); + // Here you would typically call an API to upload the file console.log('Uploading file:', newFile); - // Generate a new ID for the document - const newId = (Math.max(...documents.map((doc) => parseInt(doc.id)), 0) + 1).toString(); + // Simulate API call + setTimeout(() => { + // Generate a new ID for the document + const newId = Date.now().toString(); - // Format file size - const fileSizeKB = newFile.file ? (newFile.file.size / 1024).toFixed(0) + 'kb' : '0kb'; + // Format file size + const fileSizeKB = newFile.file ? (newFile.file.size / 1024).toFixed(0) + 'kb' : '0kb'; - // Get current date - const today = new Date(); - const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String( - today.getDate() - ).padStart(2, '0')}`; + // Get current date + const today = new Date(); + const formattedDate = today.toISOString(); - // Create new document object - const newDocument = { - id: newId, - name: newFile.name, - description: newFile.description || '无描述', - size: fileSizeKB, - updatedAt: formattedDate, - }; + // Create new document object + const newDocument = { + id: newId, + name: newFile.name, + description: newFile.description || '无描述', + size: fileSizeKB, + create_time: formattedDate, + update_time: formattedDate, + }; - // Add new document to the documents array - setDocuments((prevDocuments) => [...prevDocuments, newDocument]); + // Add new document to the documents array + setDocuments((prevDocuments) => [...prevDocuments, newDocument]); - // Reset form and close modal - handleCloseAddFileModal(); + // Show notification + dispatch( + showNotification({ + message: '数据集上传成功', + type: 'success', + }) + ); + + setIsSubmitting(false); + // Reset form and close modal + handleCloseAddFileModal(); + }, 1000); }; // Open file selector when clicking on the upload area @@ -220,299 +244,103 @@ export default function DatasetTab({ knowledgeBase }) { // Update documents state by removing the deleted document setDocuments((prevDocuments) => prevDocuments.filter((doc) => doc.id !== docId)); + + // Show notification + dispatch( + showNotification({ + message: '数据集已删除', + type: 'success', + }) + ); }; + // Filter documents based on search query + const filteredDocuments = documents.filter( + (doc) => + doc.name.toLowerCase().includes(searchQuery.toLowerCase()) || + (doc.description && doc.description.toLowerCase().includes(searchQuery.toLowerCase())) + ); + return ( <> {/* Breadcrumb navigation */} -
- -
+ - {/* Action bar */} + {/* Toolbar */}
-
-
- - {showBatchDropdown && ( -
    -
  • - -
  • -
- )} -
-
- setSearchQuery(e.target.value)} - /> - - - -
-
- -
- - {/* Documents table */} -
-
- - - - - - - - - - - - - - {documents.map((doc) => ( - - - - - - - - - - ))} - -
-
- -
-
ID名称描述文档大小更新日期操作
-
- handleSelectDocument(doc.id)} - /> -
-
#{doc.id}{doc.name}{doc.description}{doc.size}{doc.updatedAt} -
- - -
-
-
-
- - {/* Pagination */} -
-
- 每页行数: - -
-
- 1-5 of 10 - -
-
- - {/* Add File Modal */} - {showAddFileModal && ( -
-
+ + {selectedDocuments.length > 0 && ( +
-
-
-
- - - 文件名称将自动填充为上传文件的名称 -
-
- - -
-
- -
- - {newFile.file ? ( -
-

{newFile.name}

-

- {(newFile.file.size / 1024).toFixed(2)} KB -

-
- ) : ( -
- -

点击或拖拽文件到此处上传

-

支持 PDF, DOCX, TXT, CSV 等格式

-
- )} -
- {fileErrors.file &&
{fileErrors.file}
} -
-
-
- - + {showBatchDropdown && ( +
    +
  • + +
  • +
+ )}
-
+ )}
- )} +
+ +
+ + + {/* Document list */} + + + {/* File upload modal */} + ); } diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx index f08a19e..0b2705f 100644 --- a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx +++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx @@ -1,11 +1,27 @@ import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; -import SvgIcon from '../../../components/SvgIcon'; +import { useNavigate } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { showNotification } from '../../../store/notification.slice'; +import { updateKnowledgeBase, deleteKnowledgeBase } from '../../../store/knowledgeBase/knowledgeBase.thunks'; + +// 导入拆分的组件 +import Breadcrumb from './components/Breadcrumb'; +import KnowledgeBaseForm from './components/KnowledgeBaseForm'; +import DeleteConfirmModal from './components/DeleteConfirmModal'; export default function SettingsTab({ knowledgeBase }) { const dispatch = useDispatch(); + const navigate = useNavigate(); + + // State for knowledge base form + const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({ + name: knowledgeBase.name, + desc: knowledgeBase.desc, + }); + const [formErrors, setFormErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + // State for pagination const [currentPage, setCurrentPage] = useState(1); const usersPerPage = 10; @@ -13,7 +29,6 @@ export default function SettingsTab({ knowledgeBase }) { // State for edit modal const [showEditModal, setShowEditModal] = useState(false); const [editUser, setEditUser] = useState(null); - const [formErrors, setFormErrors] = useState({}); // Mock data for users with permissions - convert to state so we can update it const [users, setUsers] = useState([ @@ -109,17 +124,108 @@ export default function SettingsTab({ knowledgeBase }) { const currentUsers = users.slice(indexOfFirstUser, indexOfLastUser); const totalPages = Math.ceil(users.length / usersPerPage); + // Handle knowledge base form input change + const handleInputChange = (e) => { + const { name, value } = e.target; + setKnowledgeBaseForm((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error if exists + if (formErrors[name]) { + setFormErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + // Validate knowledge base form + const validateForm = () => { + const errors = {}; + + if (!knowledgeBaseForm.name.trim()) { + errors.name = '请输入知识库名称'; + } + + if (!knowledgeBaseForm.desc.trim()) { + errors.desc = '请输入知识库描述'; + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + // Handle form submission const handleSubmit = (e) => { e.preventDefault(); - // Here you would typically call an API to update the knowledge base settings - console.log('Updating knowledge base settings'); + + // Validate form + if (!validateForm()) { + return; + } + + setIsSubmitting(true); + + // Dispatch update knowledge base action + dispatch( + updateKnowledgeBase({ + id: knowledgeBase.id, + data: { + name: knowledgeBaseForm.name, + desc: knowledgeBaseForm.desc, + }, + }) + ) + .unwrap() + .then(() => { + dispatch( + showNotification({ + message: '知识库更新成功', + type: 'success', + }) + ); + setIsSubmitting(false); + }) + .catch((error) => { + dispatch( + showNotification({ + message: `更新失败: ${error.message || '未知错误'}`, + type: 'danger', + }) + ); + setIsSubmitting(false); + }); }; // Handle knowledge base deletion const handleDelete = () => { - // Here you would typically call an API to delete the knowledge base - console.log('Deleting knowledge base:', knowledgeBase.id); + setIsSubmitting(true); + + // Dispatch delete knowledge base action + dispatch(deleteKnowledgeBase(knowledgeBase.id)) + .unwrap() + .then(() => { + dispatch( + showNotification({ + message: '知识库已删除', + type: 'success', + }) + ); + // Navigate back to knowledge base list + navigate('/knowledge-base'); + }) + .catch((error) => { + dispatch( + showNotification({ + message: `删除失败: ${error.message || '未知错误'}`, + type: 'danger', + }) + ); + setIsSubmitting(false); + setShowDeleteConfirm(false); + }); }; // Handle edit user permissions @@ -202,206 +308,27 @@ export default function SettingsTab({ knowledgeBase }) { return ( <> {/* Breadcrumb navigation */} -
- -
+ - {/* Settings form */} -
-
-
知识库设置
+ {/* Knowledge Base Form */} + setShowDeleteConfirm(true)} + /> -
-
- - -
- -
- - -
- -
- -
- - -
-
- - -
-
- -
- -
-
- - - - - - - - - - - - - {currentUsers.map((user) => ( - - - - - - - - - ))} - -
ID用户名邮箱权限类型访问时长操作
#{user.id}{user.username}{user.email} - - {user.permissionType} - - {user.accessDuration} -
- - -
-
-
-
- - {/* Pagination */} - {users.length > usersPerPage && ( -
- -
- )} - - -
- -
- - -
-
-
-
+ {/* Delete confirmation modal */} + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + /> {/* Edit User Permissions Modal */} {showEditModal && ( diff --git a/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx new file mode 100644 index 0000000..4d3b6ec --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +/** + * 面包屑导航组件 + */ +const Breadcrumb = ({ knowledgeBase, activeTab }) => { + return ( +
+ +
+ ); +}; + +export default Breadcrumb; diff --git a/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx b/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx new file mode 100644 index 0000000..e8550f1 --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx @@ -0,0 +1,70 @@ +import React from 'react'; + +/** + * 删除确认模态框组件 + */ +const DeleteConfirmModal = ({ show, title, isSubmitting, onCancel, onConfirm }) => { + if (!show) return null; + + return ( +
+
+
+
确认删除
+ +
+
+

您确定要删除知识库 "{title}" 吗?此操作不可撤销。

+
+
+ + +
+
+
+ ); +}; + +export default DeleteConfirmModal; diff --git a/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx new file mode 100644 index 0000000..747832a --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import SvgIcon from '../../../../components/SvgIcon'; + +/** + * 文档列表组件 + */ +const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocument, onDeleteDocument, selectAll }) => { + if (documents.length === 0) { + return
暂无数据集,请上传数据集
; + } + + return ( +
+ + + + + + + + + + + + + {documents.map((doc) => ( + + + + + + + + + ))} + +
+
+ +
+
名称描述大小更新时间 + 操作 +
+
+ onSelectDocument(doc.id)} + /> +
+
{doc.name}{doc.description}{doc.size}{new Date(doc.update_time).toLocaleDateString()} +
+ +
+
+
+ ); +}; + +export default DocumentList; diff --git a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx new file mode 100644 index 0000000..0ccbb0e --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx @@ -0,0 +1,118 @@ +import React, { useRef } from 'react'; + +/** + * 文件上传模态框组件 + */ +const FileUploadModal = ({ + show, + newFile, + fileErrors, + isSubmitting, + onClose, + onDescriptionChange, + onFileChange, + onFileDrop, + onDragOver, + onUploadAreaClick, + onUpload, +}) => { + const fileInputRef = useRef(null); + + if (!show) return null; + + return ( +
+
+
+
上传文件
+ +
+
+
+ + {newFile.file ? ( +
+

已选择文件:

+

{newFile.file.name}

+
+ ) : ( +
+

点击或拖拽文件到此处上传

+

支持 PDF, DOCX, TXT, CSV 等格式

+
+ )} + {fileErrors.file &&
{fileErrors.file}
} +
+
+ + +
+
+
+ + +
+
+
+ ); +}; + +export default FileUploadModal; diff --git a/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx b/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx new file mode 100644 index 0000000..761ee0d --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx @@ -0,0 +1,116 @@ +import React from 'react'; + +/** + * 知识库表单组件 + */ +const KnowledgeBaseForm = ({ + formData, + formErrors, + isSubmitting, + knowledgeBase, + onInputChange, + onSubmit, + onDelete, +}) => { + return ( +
+
+
知识库设置
+ +
+
+ + + {formErrors.name &&
{formErrors.name}
} +
+ +
+ + + {formErrors.desc &&
{formErrors.desc}
} +
+ +
+ +
+ + +
+
+ + +
+
+ +
+ +

{new Date(knowledgeBase.create_time).toLocaleString()}

+
+ +
+ +

{new Date(knowledgeBase.update_time).toLocaleString()}

+
+ +
+ + +
+
+
+
+ ); +}; + +export default KnowledgeBaseForm; diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index 389a3a6..a0e65c0 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -1,10 +1,21 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import KnowledgeCard from './KnowledgeCard'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { showNotification } from '../../store/notification.slice'; +import { + fetchKnowledgeBases, + searchKnowledgeBases, + createKnowledgeBase, +} from '../../store/knowledgeBase/knowledgeBase.thunks'; +import { resetSearchState } from '../../store/knowledgeBase/knowledgeBase.slice'; import SvgIcon from '../../components/SvgIcon'; +// 导入拆分的组件 +import SearchBar from './components/SearchBar'; +import Pagination from './components/Pagination'; +import CreateKnowledgeBaseModal from './components/CreateKnowledgeBaseModal'; +import KnowledgeBaseList from './components/KnowledgeBaseList'; + export default function KnowledgeBase() { const dispatch = useDispatch(); const navigate = useNavigate(); @@ -21,36 +32,146 @@ export default function KnowledgeBase() { reason: '', }); const [newKnowledgeBase, setNewKnowledgeBase] = useState({ - title: '', - description: '', + name: '', + desc: '', }); - const knowledgeList = [ - { - id: '1', - title: '产品开发知识库', - description: '产品开发流程及规范说明文档', - documents: 24, - date: '2025-02-15', - access: 'full', - }, - { - id: '2', - title: '市场分析知识库', - description: '2025年Q1市场分析总结', - documents: 12, - date: '2025-02-10', - access: 'read', - }, - { - id: '3', - title: '财务知识库', - description: '月度财务分析报告', - documents: 8, - date: '2025-02-01', - access: 'none', - }, - ]; + // Search state + const [searchKeyword, setSearchKeyword] = useState(''); + const [isSearching, setIsSearching] = useState(false); + + // Pagination state + const [pagination, setPagination] = useState({ + page: 1, + page_size: 10, + }); + + // Get knowledge bases from Redux store + const { items: knowledgeBases, total, status, error } = useSelector((state) => state.knowledgeBase.list); + const { + items: searchResults, + total: searchTotal, + status: searchStatus, + error: searchError, + keyword: storeKeyword, + } = useSelector((state) => state.knowledgeBase.search); + const { status: operationStatus, error: operationError } = useSelector((state) => state.knowledgeBase.operations); + + // Determine which data to display based on search state + const displayData = isSearching ? searchResults : knowledgeBases; + const displayTotal = isSearching ? searchTotal : total; + const displayStatus = isSearching ? searchStatus : status; + const displayError = isSearching ? searchError : error; + + // Fetch knowledge bases when component mounts or pagination changes + useEffect(() => { + if (!isSearching) { + dispatch(fetchKnowledgeBases(pagination)); + } else if (searchKeyword.trim()) { + dispatch( + searchKnowledgeBases({ + keyword: searchKeyword, + page: pagination.page, + page_size: pagination.page_size, + }) + ); + } + }, [dispatch, pagination.page, pagination.page_size, isSearching, searchKeyword]); + + // Handle search input change + const handleSearchInputChange = (e) => { + setSearchKeyword(e.target.value); + }; + + // Handle search submit + const handleSearch = (e) => { + e.preventDefault(); + + if (searchKeyword.trim()) { + setIsSearching(true); + setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page + dispatch( + searchKnowledgeBases({ + keyword: searchKeyword, + page: 1, + page_size: pagination.page_size, + }) + ); + } else { + // If search is empty, reset to normal list view + handleClearSearch(); + } + }; + + // Handle clear search + const handleClearSearch = () => { + setSearchKeyword(''); + setIsSearching(false); + setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page + dispatch(resetSearchState()); + }; + + // Show loading state while fetching data + const isLoading = displayStatus === 'loading'; + + // Show error notification if fetch fails + useEffect(() => { + if (displayStatus === 'failed' && displayError) { + dispatch( + showNotification({ + message: `获取知识库列表失败: ${displayError.message || displayError}`, + type: 'danger', + }) + ); + } + }, [displayStatus, displayError, dispatch]); + + // Show notification for operation status + useEffect(() => { + if (operationStatus === 'succeeded') { + dispatch( + showNotification({ + message: '操作成功', + type: 'success', + }) + ); + // Refresh the list after successful operation + if (isSearching && searchKeyword.trim()) { + dispatch( + searchKnowledgeBases({ + keyword: searchKeyword, + page: pagination.page, + page_size: pagination.page_size, + }) + ); + } else { + dispatch(fetchKnowledgeBases(pagination)); + } + } else if (operationStatus === 'failed' && operationError) { + dispatch( + showNotification({ + message: `操作失败: ${operationError.message || operationError}`, + type: 'danger', + }) + ); + } + }, [operationStatus, operationError, dispatch, pagination, isSearching, searchKeyword]); + + // Handle pagination change + const handlePageChange = (newPage) => { + setPagination((prev) => ({ + ...prev, + page: newPage, + })); + }; + + // Handle page size change + const handlePageSizeChange = (newPageSize) => { + setPagination({ + page: 1, // Reset to first page when changing page size + page_size: newPageSize, + }); + }; const handleInputChange = (e) => { const { name, value } = e.target; @@ -87,12 +208,12 @@ export default function KnowledgeBase() { const validateCreateForm = () => { const errors = {}; - if (!newKnowledgeBase.title.trim()) { - errors.title = '请输入知识库名称'; + if (!newKnowledgeBase.name.trim()) { + errors.name = '请输入知识库名称'; } - if (!newKnowledgeBase.description.trim()) { - errors.description = '请输入知识库描述'; + if (!newKnowledgeBase.desc.trim()) { + errors.desc = '请输入知识库描述'; } setFormErrors(errors); @@ -120,27 +241,22 @@ export default function KnowledgeBase() { return; } - // For now, just show a success notification + // Dispatch create knowledge base action dispatch( - showNotification({ - message: '知识库创建成功', - type: 'success', + createKnowledgeBase({ + name: newKnowledgeBase.name, + desc: newKnowledgeBase.desc, + type: 'private', // Default type }) ); - // In a real application, you would get the ID from the API response - // For now, we'll generate a mock ID - const newId = Date.now().toString(); - // Reset form and close modal - setNewKnowledgeBase({ title: '', description: '' }); + setNewKnowledgeBase({ name: '', desc: '' }); setFormErrors({}); setShowCreateModal(false); - - // Navigate to the newly created knowledge base with datasets tab - navigate(`/knowledge-base/${newId}/datasets`); }; + // Handle card click to navigate to knowledge base detail const handleCardClick = (id) => { navigate(`/knowledge-base/${id}/datasets`); }; @@ -179,10 +295,19 @@ export default function KnowledgeBase() { setShowAccessRequestModal(false); }; + // Calculate total pages + const totalPages = Math.ceil(displayTotal / pagination.page_size); + return (
- +
-
- {knowledgeList.map((item) => ( - - handleCardClick(item.id)} - onRequestAccess={handleRequestAccess} - /> - - ))} -
- - {/* 新建知识库弹窗 */} - {showCreateModal && ( -
-
-
-
新建知识库
- -
-
-
- - - {formErrors.title &&
{formErrors.title}
} -
-
- - - {formErrors.description && ( -
{formErrors.description}
- )} -
-
-
- - -
-
+ {isSearching && ( +
+ 搜索结果: "{storeKeyword}" - 找到 {displayTotal} 个知识库
)} - {/* 申请访问权限弹窗 */} + {isLoading ? ( +
+
+ 加载中... +
+
+ ) : ( + <> + + + {/* Pagination */} + {totalPages > 1 && ( + + )} + + )} + + {/* 新建知识库弹窗 */} + setShowCreateModal(false)} + onChange={handleInputChange} + onSubmit={handleCreateKnowledgeBase} + /> + + {/* 申请权限弹窗 */} {showAccessRequestModal && (
-
- -
-
- setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' })) - } - > -
只读访问
-
仅查看数据集内容
-
-
- setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' })) - } - > -
完全访问
-
查看、编辑和管理数据
-
-
-
-
- + + +
+
+ + +
+
+
-
-
-
-
-
+
-
diff --git a/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx b/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx index d1a3b28..651820c 100644 --- a/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx @@ -1,5 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { showNotification } from '../../store/notification.slice'; import SvgIcon from '../../components/SvgIcon'; import DatasetTab from './Detail/DatasetTab'; import SettingsTab from './Detail/SettingsTab'; @@ -7,8 +9,13 @@ import SettingsTab from './Detail/SettingsTab'; export default function KnowledgeBaseDetail() { const { id, tab } = useParams(); const navigate = useNavigate(); + const dispatch = useDispatch(); const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets'); + // Get knowledge base details from Redux store + const { items: knowledgeBases } = useSelector((state) => state.knowledgeBase.list); + const knowledgeBase = knowledgeBases.find((kb) => kb.id === id); + // Update active tab when URL changes useEffect(() => { if (tab) { @@ -16,15 +23,18 @@ export default function KnowledgeBaseDetail() { } }, [tab]); - // Mock data for the knowledge base details - const knowledgeBase = { - id: id, - title: '知识库 1', - description: '知识库详细信息', - createdAt: '2023-05-01', - updatedAt: '2023-05-15', - documentsCount: 24, - }; + // If knowledge base not found in Redux store, show notification and redirect + useEffect(() => { + if (!knowledgeBase && knowledgeBases.length > 0) { + dispatch( + showNotification({ + message: '未找到知识库,请返回知识库列表', + type: 'warning', + }) + ); + navigate('/knowledge-base'); + } + }, [knowledgeBase, knowledgeBases, dispatch, navigate]); // Handle tab change const handleTabChange = (tab) => { @@ -32,14 +42,25 @@ export default function KnowledgeBaseDetail() { navigate(`/knowledge-base/${id}/${tab}`); }; + // Show loading state if knowledge base not loaded yet + if (!knowledgeBase) { + return ( +
+
+ 加载中... +
+
+ ); + } + return (
{/* Sidebar */}
-
{knowledgeBase.title}
-

{knowledgeBase.description}

+
{knowledgeBase.name}
+

{knowledgeBase.desc}


diff --git a/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx b/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx new file mode 100644 index 0000000..c5af8f4 --- /dev/null +++ b/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx @@ -0,0 +1,92 @@ +import React from 'react'; +import SvgIcon from '../../../components/SvgIcon'; + +/** + * 创建知识库模态框组件 + */ +const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, onClose, onChange, onSubmit }) => { + if (!show) return null; + + return ( +
+
+
+
新建知识库
+ +
+
+
+ + + {formErrors.name &&
{formErrors.name}
} +
+
+ + + {formErrors.desc &&
{formErrors.desc}
} +
+
+
+ + +
+
+
+ ); +}; + +export default CreateKnowledgeBaseModal; diff --git a/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx new file mode 100644 index 0000000..eb042e5 --- /dev/null +++ b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import KnowledgeCard from '../KnowledgeCard'; + +/** + * 知识库列表组件 + */ +const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequestAccess }) => { + if (knowledgeBases.length === 0) { + return ( +
+ {isSearching ? '没有找到匹配的知识库' : '暂无知识库,请创建新的知识库'} +
+ ); + } + + return ( +
+ {knowledgeBases.map((item) => ( + + onCardClick(item.id)} + onRequestAccess={onRequestAccess} + /> + + ))} +
+ ); +}; + +export default KnowledgeBaseList; diff --git a/src/pages/KnowledgeBase/components/Pagination.jsx b/src/pages/KnowledgeBase/components/Pagination.jsx new file mode 100644 index 0000000..8c8f4b1 --- /dev/null +++ b/src/pages/KnowledgeBase/components/Pagination.jsx @@ -0,0 +1,53 @@ +import React from 'react'; + +/** + * 分页组件 + */ +const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => { + return ( +
+
+ +
+ +
+ ); +}; + +export default Pagination; diff --git a/src/pages/KnowledgeBase/components/SearchBar.jsx b/src/pages/KnowledgeBase/components/SearchBar.jsx new file mode 100644 index 0000000..d20a2e6 --- /dev/null +++ b/src/pages/KnowledgeBase/components/SearchBar.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import SvgIcon from '../../../components/SvgIcon'; + +/** + * 知识库搜索栏组件 + */ +const SearchBar = ({ searchKeyword, isSearching, onSearchChange, onSearch, onClearSearch }) => { + return ( +
+ + {/* */} + {isSearching && ( + + )} +
+ ); +}; + +export default SearchBar; diff --git a/src/services/api.js b/src/services/api.js index 7f69131..80e61ea 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -34,7 +34,7 @@ api.interceptors.response.use( // Handle errors in the response if (error.response) { // monitor /verify - if (error.response.status === 401 && error.config.url === '/check-token/') { + if (error.response.status === 401 && error.config.url === '/auth/verify-token/') { if (window.location.pathname !== '/login' && window.location.pathname !== '/signup') { window.location.href = '/login'; } diff --git a/src/store/auth/auth.thunk.js b/src/store/auth/auth.thunk.js index a29ace2..8efd5bb 100644 --- a/src/store/auth/auth.thunk.js +++ b/src/store/auth/auth.thunk.js @@ -10,15 +10,16 @@ export const loginThunk = createAsyncThunk( 'auth/login', async ({ username, password }, { rejectWithValue, dispatch }) => { try { - const { message, user, token } = await post('/login/', { username, password }); - if (!user) { + const { message, data } = await post('/auth/login/', { username, password }); + if (!data) { throw new Error(message || 'Something went wrong'); } + const { token } = data; // encrypt token const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString(); sessionStorage.setItem('token', encryptedToken); - return user; + return data; } catch (error) { const errorMessage = error.response?.data?.message || 'Something went wrong'; dispatch( @@ -51,9 +52,9 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje } }); -export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectWithValue, dispatch }) => { +export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => { try { - const { user, message } = await get('/check-token/'); + const { user, message } = await get('/auth/verify-token/'); if (!user) { dispatch(logout()); throw new Error(message || 'No token found'); @@ -69,7 +70,7 @@ export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectW export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWithValue, dispatch }) => { try { // Send the logout request to the server (this assumes your server clears any session-related info) - await post('/logout/'); + await post('/auth/logout/'); dispatch(logout()); } catch (error) { const errorMessage = error.response?.data?.message || 'Log out failed'; diff --git a/src/store/knowledgeBase/knowledgeBase.slice.js b/src/store/knowledgeBase/knowledgeBase.slice.js new file mode 100644 index 0000000..d37445a --- /dev/null +++ b/src/store/knowledgeBase/knowledgeBase.slice.js @@ -0,0 +1,198 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { + fetchKnowledgeBases, + searchKnowledgeBases, + createKnowledgeBase, + getKnowledgeBaseById, + updateKnowledgeBase, + deleteKnowledgeBase, +} from './knowledgeBase.thunks'; + +const initialState = { + // List state + list: { + items: [], + total: 0, + page: 1, + page_size: 10, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // Search state + search: { + items: [], + total: 0, + page: 1, + page_size: 10, + keyword: '', + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // Current knowledge base details + current: { + data: null, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // Create/update/delete operations status + operations: { + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + operationType: null, // 'create' | 'update' | 'delete' + }, +}; + +const knowledgeBaseSlice = createSlice({ + name: 'knowledgeBase', + initialState, + reducers: { + resetOperationStatus: (state) => { + state.operations = { + status: 'idle', + error: null, + operationType: null, + }; + }, + resetCurrentKnowledgeBase: (state) => { + state.current = { + data: null, + status: 'idle', + error: null, + }; + }, + resetSearchState: (state) => { + state.search = { + items: [], + total: 0, + page: 1, + page_size: 10, + keyword: '', + status: 'idle', + error: null, + }; + }, + }, + extraReducers: (builder) => { + builder + // Fetch knowledge bases + .addCase(fetchKnowledgeBases.pending, (state) => { + state.list.status = 'loading'; + }) + .addCase(fetchKnowledgeBases.fulfilled, (state, action) => { + state.list.status = 'succeeded'; + state.list.items = action.payload.items; + state.list.total = action.payload.total; + state.list.page = action.payload.page; + state.list.page_size = action.payload.page_size; + state.list.error = null; + }) + .addCase(fetchKnowledgeBases.rejected, (state, action) => { + state.list.status = 'failed'; + state.list.error = action.payload; + }) + + // Search knowledge bases + .addCase(searchKnowledgeBases.pending, (state, action) => { + state.search.status = 'loading'; + // Store the keyword for reference + if (action.meta.arg.keyword) { + state.search.keyword = action.meta.arg.keyword; + } + }) + .addCase(searchKnowledgeBases.fulfilled, (state, action) => { + state.search.status = 'succeeded'; + state.search.items = action.payload.items; + state.search.total = action.payload.total; + state.search.page = action.payload.page; + state.search.page_size = action.payload.page_size; + state.search.error = null; + }) + .addCase(searchKnowledgeBases.rejected, (state, action) => { + state.search.status = 'failed'; + state.search.error = action.payload; + }) + + // Get knowledge base by ID + .addCase(getKnowledgeBaseById.pending, (state) => { + state.current.status = 'loading'; + }) + .addCase(getKnowledgeBaseById.fulfilled, (state, action) => { + state.current.status = 'succeeded'; + state.current.data = action.payload; + state.current.error = null; + }) + .addCase(getKnowledgeBaseById.rejected, (state, action) => { + state.current.status = 'failed'; + state.current.error = action.payload; + }) + + // Create knowledge base + .addCase(createKnowledgeBase.pending, (state) => { + state.operations.status = 'loading'; + state.operations.operationType = 'create'; + }) + .addCase(createKnowledgeBase.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + // Don't add to list here - better to refetch the list to ensure consistency + state.operations.error = null; + }) + .addCase(createKnowledgeBase.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload; + }) + + // Update knowledge base + .addCase(updateKnowledgeBase.pending, (state) => { + state.operations.status = 'loading'; + state.operations.operationType = 'update'; + }) + .addCase(updateKnowledgeBase.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + // Update in list if present + const index = state.list.items.findIndex((item) => item.id === action.payload.id); + if (index !== -1) { + state.list.items[index] = action.payload; + } + // Update in search results if present + const searchIndex = state.search.items.findIndex((item) => item.id === action.payload.id); + if (searchIndex !== -1) { + state.search.items[searchIndex] = action.payload; + } + // Update current if it's the same knowledge base + if (state.current.data && state.current.data.id === action.payload.id) { + state.current.data = action.payload; + } + state.operations.error = null; + }) + .addCase(updateKnowledgeBase.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload; + }) + + // Delete knowledge base + .addCase(deleteKnowledgeBase.pending, (state) => { + state.operations.status = 'loading'; + state.operations.operationType = 'delete'; + }) + .addCase(deleteKnowledgeBase.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + // Remove from list if present + state.list.items = state.list.items.filter((item) => item.id !== action.payload); + // Remove from search results if present + state.search.items = state.search.items.filter((item) => item.id !== action.payload); + // Reset current if it's the same knowledge base + if (state.current.data && state.current.data.id === action.payload) { + state.current.data = null; + } + state.operations.error = null; + }) + .addCase(deleteKnowledgeBase.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload; + }); + }, +}); + +export const { resetOperationStatus, resetCurrentKnowledgeBase, resetSearchState } = knowledgeBaseSlice.actions; +const knowledgeBaseReducer = knowledgeBaseSlice.reducer; +export default knowledgeBaseReducer; diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js new file mode 100644 index 0000000..e961002 --- /dev/null +++ b/src/store/knowledgeBase/knowledgeBase.thunks.js @@ -0,0 +1,107 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post, put, del } from '../../services/api'; + +/** + * Fetch knowledge bases with pagination + * @param {Object} params - Pagination parameters + * @param {number} params.page - Page number (default: 1) + * @param {number} params.page_size - Page size (default: 10) + */ +export const fetchKnowledgeBases = createAsyncThunk( + 'knowledgeBase/fetchKnowledgeBases', + async ({ page = 1, page_size = 10 } = {}, { rejectWithValue }) => { + try { + const response = await get('/knowledge-bases/', { page, page_size }); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to fetch knowledge bases'); + } + } +); + +/** + * Search knowledge bases + * @param {Object} params - Search parameters + * @param {string} params.keyword - Search keyword + * @param {number} params.page - Page number (default: 1) + * @param {number} params.page_size - Page size (default: 10) + */ +export const searchKnowledgeBases = createAsyncThunk( + 'knowledgeBase/searchKnowledgeBases', + async ({ keyword, page = 1, page_size = 10 }, { rejectWithValue }) => { + try { + const response = await get('/knowledge-bases/search/', { + keyword, + page, + page_size, + }); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to search knowledge bases'); + } + } +); + +/** + * Create a new knowledge base + */ +export const createKnowledgeBase = createAsyncThunk( + 'knowledgeBase/createKnowledgeBase', + async (knowledgeBaseData, { rejectWithValue }) => { + try { + const response = await post('/knowledge-bases/', knowledgeBaseData); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to create knowledge base'); + } + } +); + +/** + * Get knowledge base details by ID + */ +export const getKnowledgeBaseById = createAsyncThunk( + 'knowledgeBase/getKnowledgeBaseById', + async (id, { rejectWithValue }) => { + try { + const response = await get(`/knowledge-bases/${id}/`); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to get knowledge base details'); + } + } +); + +/** + * Update knowledge base + * @param {Object} params - Update parameters + * @param {string} params.id - Knowledge base ID + * @param {Object} params.data - Update data (name, desc) + */ +export const updateKnowledgeBase = createAsyncThunk( + 'knowledgeBase/updateKnowledgeBase', + async ({ id, data }, { rejectWithValue }) => { + try { + const response = await put(`/knowledge-bases/${id}/`, data); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to update knowledge base'); + } + } +); + +/** + * Delete knowledge base + * @param {string} id - Knowledge base ID + */ +export const deleteKnowledgeBase = createAsyncThunk( + 'knowledgeBase/deleteKnowledgeBase', + async (id, { rejectWithValue }) => { + try { + await del(`/knowledge-bases/${id}/`); + return id; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to delete knowledge base'); + } + } +); diff --git a/src/store/store.js b/src/store/store.js index 6e06a4d..f83d727 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -3,10 +3,12 @@ import { persistReducer, persistStore } from 'redux-persist'; import sessionStorage from 'redux-persist/lib/storage/session'; import notificationReducer from './notification.slice.js'; import authReducer from './auth/auth.slice.js'; +import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js'; const rootRducer = combineReducers({ auth: authReducer, notification: notificationReducer, + knowledgeBase: knowledgeBaseReducer, }); const persistConfig = {