diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx
index 98454f6..abca74b 100644
--- a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx
+++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx
@@ -6,6 +6,7 @@ import {
updateKnowledgeBase,
deleteKnowledgeBase,
changeKnowledgeBaseType,
+ getKnowledgeBaseDocuments,
} from '../../../store/knowledgeBase/knowledgeBase.thunks';
// 导入拆分的组件
@@ -13,6 +14,8 @@ import Breadcrumb from './components/Breadcrumb';
import KnowledgeBaseForm from './components/KnowledgeBaseForm';
import DeleteConfirmModal from './components/DeleteConfirmModal';
import UserPermissionsManager from './components/UserPermissionsManager';
+import FileUploadModal from './components/FileUploadModal';
+import DocumentList from './components/DocumentList';
// 部门和组别的映射关系
const departmentGroups = {
@@ -47,6 +50,14 @@ export default function SettingsTab({ knowledgeBase }) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [availableGroups, setAvailableGroups] = useState([]);
+ const [showUploadModal, setShowUploadModal] = useState(false);
+
+ // 获取文档列表
+ useEffect(() => {
+ if (knowledgeBase?.id) {
+ dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBase.id }));
+ }
+ }, [dispatch, knowledgeBase?.id]);
// 当部门变化时,更新可选的组别
useEffect(() => {
@@ -93,7 +104,7 @@ export default function SettingsTab({ knowledgeBase }) {
allowed = ['admin', 'member', 'private'].includes(value);
} else {
// 普通成员只能选择公共和private
- allowed = ['admin', 'private'].includes(value);
+ allowed = ['admin', 'private'].includes(value);
}
if (!allowed) {
@@ -334,6 +345,27 @@ export default function SettingsTab({ knowledgeBase }) {
availableGroups={availableGroups}
/>
+ {/* Document Management Section */}
+
+
+
+
文档管理
+
+
+
+
+
+
+
+ {/* File Upload Modal */}
+ setShowUploadModal(false)}
+ knowledgeBaseId={knowledgeBase.id}
+ />
+
{/* User Permissions Manager */}
{/* */}
diff --git a/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx
index 747832a..8f78038 100644
--- a/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx
+++ b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx
@@ -1,12 +1,42 @@
import React from 'react';
-import SvgIcon from '../../../../components/SvgIcon';
+import { useDispatch, useSelector } from 'react-redux';
+import { formatDate } from '../../../../utils/dateUtils';
+import { deleteKnowledgeBaseDocument } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
/**
- * 文档列表组件
+ * 知识库文档列表组件
*/
-const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocument, onDeleteDocument, selectAll }) => {
- if (documents.length === 0) {
- return 暂无数据集,请上传数据集
;
+const DocumentList = ({ knowledgeBaseId }) => {
+ const dispatch = useDispatch();
+ const { items, loading, pagination } = useSelector((state) => state.knowledgeBase.documents);
+
+ const handleDeleteDocument = (documentId) => {
+ if (window.confirm('确定要删除此文档吗?')) {
+ dispatch(
+ deleteKnowledgeBaseDocument({
+ knowledge_base_id: knowledgeBaseId,
+ document_id: documentId,
+ })
+ );
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (items.length === 0) {
+ return (
+
+ );
}
return (
@@ -14,59 +44,51 @@ const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocum
+
+ {pagination.total > 0 && (
+
+
共 {pagination.total} 条记录
+
+ )}
);
};
+// Helper function to format date string
+const formatDateTime = (dateString) => {
+ if (!dateString) return '-';
+
+ // If the utility function exists, use it, otherwise format manually
+ try {
+ return formatDate(dateString);
+ } catch (error) {
+ const date = new Date(dateString);
+ return date.toLocaleString();
+ }
+};
+
export default DocumentList;
diff --git a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
index 915f85e..974d21d 100644
--- a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
+++ b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
@@ -1,23 +1,17 @@
-import React, { useRef, useEffect } from 'react';
+import React, { useRef, useState, useEffect } from 'react';
+import { useDispatch } from 'react-redux';
+import { uploadDocument, getKnowledgeBaseDocuments } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
/**
* 文件上传模态框组件
*/
-const FileUploadModal = ({
- show,
- newFile,
- fileErrors,
- isSubmitting,
- onClose,
- onDescriptionChange,
- onFileChange,
- onFileDrop,
- onDragOver,
- onUploadAreaClick,
- onUpload,
-}) => {
+const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
+ const dispatch = useDispatch();
const fileInputRef = useRef(null);
const modalRef = useRef(null);
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [isUploading, setIsUploading] = useState(false);
+ const [fileError, setFileError] = useState('');
// 处理上传区域点击事件
const handleUploadAreaClick = () => {
@@ -28,13 +22,76 @@ const FileUploadModal = ({
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
- onDragOver?.(e);
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
- onFileDrop?.(e);
+
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
+ handleFileSelected(e.dataTransfer.files[0]);
+ }
+ };
+
+ const handleFileChange = (e) => {
+ if (e.target.files && e.target.files.length > 0) {
+ handleFileSelected(e.target.files[0]);
+ }
+ };
+
+ const handleFileSelected = (file) => {
+ setFileError('');
+ setSelectedFile(file);
+ };
+
+ const resetFileInput = () => {
+ setSelectedFile(null);
+ setFileError('');
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ const handleUpload = async () => {
+ if (!selectedFile) {
+ setFileError('请选择要上传的文件');
+ return;
+ }
+
+ setIsUploading(true);
+
+ try {
+ await dispatch(
+ uploadDocument({
+ knowledge_base_id: knowledgeBaseId,
+ file: selectedFile,
+ })
+ ).unwrap();
+
+ // 成功上传后刷新文档列表
+ dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId }));
+
+ // Reset the file input
+ resetFileInput();
+
+ // 不要在上传成功后关闭模态框,允许用户继续上传或手动关闭
+ } catch (error) {
+ console.error('Upload failed:', error);
+ setFileError('文件上传失败: ' + (error?.message || '未知错误'));
+
+ // 清空选中的文件
+ resetFileInput();
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ const handleClose = () => {
+ // 只有在非上传状态才允许关闭
+ if (!isUploading) {
+ resetFileInput();
+ onClose();
+ }
};
// 清理函数
@@ -78,64 +135,60 @@ const FileUploadModal = ({
}}
>
-
上传文件
-
+ 上传文档
+
- {newFile.file ? (
+ {selectedFile ? (
已选择文件:
-
{newFile.file.name}
+
{selectedFile.name}
) : (
点击或拖拽文件到此处上传
-
支持 PDF, DOCX, TXT, CSV 等格式
+
+ 支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式
+
)}
- {fileErrors.file &&
{fileErrors.file}
}
-
-
-
-
+ {fileError &&
{fileError}
}
-
diff --git a/src/services/mockApi.js b/src/services/mockApi.js
index 46c9355..0eae4f9 100644
--- a/src/services/mockApi.js
+++ b/src/services/mockApi.js
@@ -522,8 +522,8 @@ const mockPermissionApi = {
};
// Mock API functions
-export const mockGet = async (url, config = {}) => {
- console.log(`[MOCK API] GET ${url}`, config);
+export const mockGet = async (url, params = {}) => {
+ console.log(`[MOCK API] GET ${url}`, params);
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 500));
@@ -537,7 +537,6 @@ export const mockGet = async (url, config = {}) => {
// Get knowledge bases
if (url === '/knowledge-bases/') {
- const params = config.params || { page: 1, page_size: 10 };
const result = paginate(knowledgeBases, params.page_size, params.page);
return {
@@ -576,7 +575,6 @@ export const mockGet = async (url, config = {}) => {
// Get chat history
if (url === '/chat-history/') {
- const params = config.params || { page: 1, page_size: 10 };
const result = mockGetChatHistory(params);
return {
data: {
@@ -620,7 +618,7 @@ export const mockGet = async (url, config = {}) => {
// Knowledge base search
if (url === '/knowledge-bases/search/') {
- const { keyword = '', page = 1, page_size = 10 } = config.params || {};
+ const { keyword = '', page = 1, page_size = 10 } = params.params || {};
const filtered = knowledgeBases.filter(
(kb) =>
kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
@@ -671,10 +669,58 @@ export const mockGet = async (url, config = {}) => {
};
}
+ // 获取知识库文档列表
+ if (url.match(/\/knowledge-bases\/([^/]+)\/documents\//)) {
+ const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/documents\//)[1];
+ const page = params?.params?.page || 1;
+ const page_size = params?.params?.page_size || 10;
+
+ // 模拟文档列表数据
+ const mockDocuments = [
+ {
+ id: 'df6d2c2b-895c-4c56-83c8-1644345e654d',
+ document_id: '772044ae-0ecf-11f0-8082-0242ac120002',
+ document_name: '产品说明书.pdf',
+ external_id: '772044ae-0ecf-11f0-8082-0242ac120002',
+ create_time: '2023-04-01 08:01:06',
+ update_time: '2023-04-01 08:01:06',
+ },
+ {
+ id: 'eba8f519-debf-461c-b4fd-87177d94bece',
+ document_id: '429a2c08-0ea3-11f0-bdec-0242ac120002',
+ document_name: '用户手册.docx',
+ external_id: '429a2c08-0ea3-11f0-bdec-0242ac120002',
+ create_time: '2023-04-01 02:44:38',
+ update_time: '2023-04-01 02:44:38',
+ },
+ {
+ id: '7a9e4c31-5b2d-437e-9a8f-2b5c7e8a9d1e',
+ document_id: 'c9a8f2b5-7e8a-9d1e-7a9e-4c315b2d437e',
+ document_name: '技术文档.txt',
+ external_id: 'c9a8f2b5-7e8a-9d1e-7a9e-4c315b2d437e',
+ create_time: '2023-03-15 10:23:45',
+ update_time: '2023-03-15 10:23:45',
+ },
+ ];
+
+ return {
+ data: {
+ code: 200,
+ message: '获取文档列表成功',
+ data: {
+ total: mockDocuments.length,
+ page: page,
+ page_size: page_size,
+ items: mockDocuments,
+ },
+ },
+ };
+ }
+
throw { response: { status: 404, data: { message: 'Not found' } } };
};
-export const mockPost = async (url, data) => {
+export const mockPost = async (url, data, isMultipart = false) => {
console.log(`[MOCK API] POST ${url}`, data);
// Simulate network delay
@@ -857,6 +903,26 @@ export const mockPost = async (url, data) => {
};
}
+ // 上传知识库文档
+ if (url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)) {
+ const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)[1];
+ const file = isMultipart ? data.get('file') : null;
+
+ return {
+ data: {
+ code: 200,
+ message: 'Document uploaded successfully',
+ data: {
+ id: `doc-${Date.now()}`,
+ knowledge_base_id: knowledge_base_id,
+ filename: file ? file.name : 'mock-document.pdf',
+ status: 'processing',
+ created_at: new Date().toISOString(),
+ },
+ },
+ };
+ }
+
throw { response: { status: 404, data: { message: 'Not found' } } };
};
@@ -966,6 +1032,22 @@ export const mockDelete = async (url) => {
return { data: mockDeleteChat(id) };
}
+ // 删除知识库文档
+ if (url.match(/\/knowledge-bases\/([^/]+)\/documents\/([^/]+)/)) {
+ const matches = url.match(/\/knowledge-bases\/([^/]+)\/documents\/([^/]+)/);
+ const knowledge_base_id = matches[1];
+ const document_id = matches[2];
+
+ console.log(`[MOCK API] Deleting document ${document_id} from knowledge base ${knowledge_base_id}`);
+
+ return {
+ data: {
+ code: 200,
+ message: '文档删除成功',
+ },
+ };
+ }
+
throw { response: { status: 404, data: { message: 'Not found' } } };
};
diff --git a/src/store/knowledgeBase/knowledgeBase.slice.js b/src/store/knowledgeBase/knowledgeBase.slice.js
index c95515c..95c45dd 100644
--- a/src/store/knowledgeBase/knowledgeBase.slice.js
+++ b/src/store/knowledgeBase/knowledgeBase.slice.js
@@ -8,6 +8,9 @@ import {
searchKnowledgeBases,
requestKnowledgeBaseAccess,
getKnowledgeBaseById,
+ uploadDocument,
+ getKnowledgeBaseDocuments,
+ deleteKnowledgeBaseDocument,
} from './knowledgeBase.thunks';
const initialState = {
@@ -27,6 +30,17 @@ const initialState = {
batchLoading: false,
editStatus: 'idle',
requestAccessStatus: 'idle',
+ uploadStatus: 'idle',
+ documents: {
+ items: [],
+ loading: false,
+ error: null,
+ pagination: {
+ total: 0,
+ page: 1,
+ page_size: 10,
+ },
+ },
};
const knowledgeBaseSlice = createSlice({
@@ -180,6 +194,57 @@ const knowledgeBaseSlice = createSlice({
.addCase(getKnowledgeBaseById.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || 'Failed to get knowledge base details';
+ })
+
+ // 上传文档
+ .addCase(uploadDocument.pending, (state) => {
+ state.uploadStatus = 'loading';
+ })
+ .addCase(uploadDocument.fulfilled, (state) => {
+ state.uploadStatus = 'successful';
+ })
+ .addCase(uploadDocument.rejected, (state, action) => {
+ state.uploadStatus = 'failed';
+ state.error = action.payload || 'Failed to upload document';
+ })
+
+ // 获取知识库文档列表
+ .addCase(getKnowledgeBaseDocuments.pending, (state) => {
+ state.documents.loading = true;
+ state.documents.error = null;
+ })
+ .addCase(getKnowledgeBaseDocuments.fulfilled, (state, action) => {
+ state.documents.loading = false;
+ state.documents.items = action.payload.items || [];
+ state.documents.pagination = {
+ total: action.payload.total || 0,
+ page: action.payload.page || 1,
+ page_size: action.payload.page_size || 10,
+ };
+ })
+ .addCase(getKnowledgeBaseDocuments.rejected, (state, action) => {
+ state.documents.loading = false;
+ state.documents.error = action.payload || 'Failed to get documents';
+ })
+
+ // 删除知识库文档
+ .addCase(deleteKnowledgeBaseDocument.pending, (state) => {
+ state.documents.loading = true;
+ state.documents.error = null;
+ })
+ .addCase(deleteKnowledgeBaseDocument.fulfilled, (state, action) => {
+ state.documents.loading = false;
+ const deletedDocId = action.payload;
+ state.documents.items = state.documents.items.filter(
+ (doc) => doc.document_id !== deletedDocId
+ );
+ if (state.documents.pagination.total > 0) {
+ state.documents.pagination.total -= 1;
+ }
+ })
+ .addCase(deleteKnowledgeBaseDocument.rejected, (state, action) => {
+ state.documents.loading = false;
+ state.documents.error = action.payload || 'Failed to delete document';
});
},
});
diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js
index 187af7f..a9f95b9 100644
--- a/src/store/knowledgeBase/knowledgeBase.thunks.js
+++ b/src/store/knowledgeBase/knowledgeBase.thunks.js
@@ -1,5 +1,5 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
-import { get, post, put, del } from '../../services/api';
+import { get, post, put, del, upload } from '../../services/api';
import { showNotification } from '../notification.slice';
/**
@@ -20,7 +20,7 @@ export const fetchKnowledgeBases = createAsyncThunk(
return response.data;
} catch (error) {
console.log(error);
-
+
return rejectWithValue(error.response?.data.error.message || 'Failed to fetch knowledge bases');
}
}
@@ -196,3 +196,103 @@ export const requestKnowledgeBaseAccess = createAsyncThunk(
}
}
);
+
+/**
+ * Upload a document to a knowledge base
+ * @param {Object} params - Upload parameters
+ * @param {string} params.knowledge_base_id - Knowledge base ID
+ * @param {File} params.file - File to upload
+ */
+export const uploadDocument = createAsyncThunk(
+ 'knowledgeBase/uploadDocument',
+ async ({ knowledge_base_id, file }, { rejectWithValue, dispatch }) => {
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await post(`/knowledge-bases/${knowledge_base_id}/upload_document/`, formData, true);
+
+ dispatch(
+ showNotification({
+ type: 'success',
+ message: `文档 ${file.name} 上传成功`,
+ })
+ );
+
+ // 处理新的返回格式
+ if (response.data && response.data.code === 200) {
+ return response.data.data;
+ }
+
+ return response.data;
+ } catch (error) {
+ const errorMessage = error.response?.data?.message || error.message || '文档上传失败';
+ dispatch(
+ showNotification({
+ type: 'danger',
+ message: errorMessage,
+ })
+ );
+ return rejectWithValue(errorMessage);
+ }
+ }
+);
+
+/**
+ * Get documents list for a knowledge base
+ * @param {Object} params - Parameters
+ * @param {string} params.knowledge_base_id - Knowledge base ID
+ * @param {number} params.page - Page number (default: 1)
+ * @param {number} params.page_size - Page size (default: 10)
+ */
+export const getKnowledgeBaseDocuments = createAsyncThunk(
+ 'knowledgeBase/getDocuments',
+ async ({ knowledge_base_id, page = 1, page_size = 10 }, { rejectWithValue }) => {
+ try {
+ const response = await get(`/knowledge-bases/${knowledge_base_id}/documents/`, {
+ params: { page, page_size }
+ });
+
+ // 处理返回格式
+ if (response.data && response.data.code === 200) {
+ return response.data.data;
+ }
+
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(error.response?.data?.message || '获取文档列表失败');
+ }
+ }
+);
+
+/**
+ * Delete a document from a knowledge base
+ * @param {Object} params - Parameters
+ * @param {string} params.knowledge_base_id - Knowledge base ID
+ * @param {string} params.document_id - Document ID
+ */
+export const deleteKnowledgeBaseDocument = createAsyncThunk(
+ 'knowledgeBase/deleteDocument',
+ async ({ knowledge_base_id, document_id }, { rejectWithValue, dispatch }) => {
+ try {
+ await del(`/knowledge-bases/${knowledge_base_id}/documents/${document_id}/`);
+
+ dispatch(
+ showNotification({
+ type: 'success',
+ message: '文档删除成功',
+ })
+ );
+
+ return document_id;
+ } catch (error) {
+ dispatch(
+ showNotification({
+ type: 'danger',
+ message: error.response?.data?.message || '文档删除失败',
+ })
+ );
+ return rejectWithValue(error.response?.data?.message || '文档删除失败');
+ }
+ }
+);
diff --git a/src/utils/dateUtils.js b/src/utils/dateUtils.js
new file mode 100644
index 0000000..9fd7a4a
--- /dev/null
+++ b/src/utils/dateUtils.js
@@ -0,0 +1,48 @@
+/**
+ * 格式化日期时间
+ * @param {string} dateString - 日期字符串
+ * @returns {string} 格式化后的日期字符串
+ */
+export const formatDate = (dateString) => {
+ if (!dateString) return '-';
+
+ const date = new Date(dateString);
+
+ // 检查日期是否有效
+ if (isNaN(date.getTime())) {
+ return dateString;
+ }
+
+ // 格式化为 YYYY-MM-DD HH:MM:SS
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ const seconds = String(date.getSeconds()).padStart(2, '0');
+
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+};
+
+/**
+ * 格式化日期(仅日期部分)
+ * @param {string} dateString - 日期字符串
+ * @returns {string} 格式化后的日期字符串
+ */
+export const formatDateOnly = (dateString) => {
+ if (!dateString) return '-';
+
+ const date = new Date(dateString);
+
+ // 检查日期是否有效
+ if (isNaN(date.getTime())) {
+ return dateString;
+ }
+
+ // 格式化为 YYYY-MM-DD
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+
+ return `${year}-${month}-${day}`;
+};