[dev]add docs list api

This commit is contained in:
susie-laptop 2025-04-01 22:22:53 -04:00
parent ba53185050
commit 6d641bb309
7 changed files with 313 additions and 61 deletions

View File

@ -6,6 +6,7 @@ import {
updateKnowledgeBase,
deleteKnowledgeBase,
changeKnowledgeBaseType,
getKnowledgeBaseDocuments,
} from '../../../store/knowledgeBase/knowledgeBase.thunks';
//
@ -14,6 +15,7 @@ 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 = {
@ -50,6 +52,13 @@ export default function SettingsTab({ knowledgeBase }) {
const [availableGroups, setAvailableGroups] = useState([]);
const [showUploadModal, setShowUploadModal] = useState(false);
//
useEffect(() => {
if (knowledgeBase?.id) {
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBase.id }));
}
}, [dispatch, knowledgeBase?.id]);
//
useEffect(() => {
if (knowledgeBaseForm.department && departmentGroups[knowledgeBaseForm.department]) {
@ -336,16 +345,17 @@ export default function SettingsTab({ knowledgeBase }) {
availableGroups={availableGroups}
/>
{/* Document Upload Section */}
{/* Document Management Section */}
<div className='card border-0 shadow-sm mt-4'>
<div className='card-body'>
<h5 className='card-title mb-4'>文档管理</h5>
<p className='text-muted mb-3'>
上传文档到知识库支持PDFWordExcelTXTMarkdown和CSV等格式
</p>
<button className='btn btn-primary' onClick={() => setShowUploadModal(true)}>
上传文档
</button>
<div className='d-flex justify-content-between align-items-center mb-4'>
<h5 className='card-title m-0'>文档管理</h5>
<button className='btn btn-primary' onClick={() => setShowUploadModal(true)}>
上传文档
</button>
</div>
<DocumentList knowledgeBaseId={knowledgeBase.id} />
</div>
</div>

View File

@ -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 <div className='alert alert-warning'>暂无数据集请上传数据集</div>;
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 (
<div className='text-center py-4'>
<div className='spinner-border text-primary' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
</div>
);
}
if (items.length === 0) {
return (
<div className='text-center py-4 text-muted'>
<p>暂无文档请上传文档</p>
</div>
);
}
return (
@ -14,59 +44,51 @@ const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocum
<table className='table table-hover'>
<thead className='table-light'>
<tr>
<th scope='col' width='40'>
<div className='form-check'>
<input
className='form-check-input'
type='checkbox'
checked={selectAll}
onChange={onSelectAll}
/>
</div>
</th>
<th scope='col'>名称</th>
<th scope='col'>描述</th>
<th scope='col'>大小</th>
<th scope='col'>文档名称</th>
<th scope='col'>创建时间</th>
<th scope='col'>更新时间</th>
<th scope='col' width='100'>
操作
</th>
<th scope='col'>操作</th>
</tr>
</thead>
<tbody>
{documents.map((doc) => (
{items.map((doc) => (
<tr key={doc.id}>
<td>{doc.document_name}</td>
<td>{formatDateTime(doc.create_time)}</td>
<td>{formatDateTime(doc.update_time)}</td>
<td>
<div className='form-check'>
<input
className='form-check-input'
type='checkbox'
checked={selectedDocuments.includes(doc.id)}
onChange={() => onSelectDocument(doc.id)}
/>
</div>
</td>
<td>{doc.name}</td>
<td>{doc.description}</td>
<td>{doc.size}</td>
<td>{new Date(doc.update_time).toLocaleDateString()}</td>
<td>
<div className='d-flex gap-1'>
<button
className='btn btn-sm text-danger'
title='删除'
onClick={() => onDeleteDocument(doc.id)}
>
<SvgIcon className='trash' width='16' height='16' />
</button>
</div>
<button
className='btn btn-sm btn-outline-danger'
onClick={() => handleDeleteDocument(doc.document_id)}
>
删除
</button>
</td>
</tr>
))}
</tbody>
</table>
{pagination.total > 0 && (
<div className='d-flex justify-content-between align-items-center mt-3'>
<p className='text-muted mb-0'> {pagination.total} 条记录</p>
</div>
)}
</div>
);
};
// 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;

View File

@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { uploadDocument, getKnowledgeBaseById } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
import { uploadDocument, getKnowledgeBaseDocuments } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
/**
* 文件上传模态框组件
@ -68,8 +68,8 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
})
).unwrap();
//
dispatch(getKnowledgeBaseById(knowledgeBaseId));
//
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId }));
// Reset the file input
resetFileInput();

View File

@ -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,6 +669,54 @@ 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' } } };
};
@ -986,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' } } };
};

View File

@ -9,6 +9,8 @@ import {
requestKnowledgeBaseAccess,
getKnowledgeBaseById,
uploadDocument,
getKnowledgeBaseDocuments,
deleteKnowledgeBaseDocument,
} from './knowledgeBase.thunks';
const initialState = {
@ -29,6 +31,16 @@ const initialState = {
editStatus: 'idle',
requestAccessStatus: 'idle',
uploadStatus: 'idle',
documents: {
items: [],
loading: false,
error: null,
pagination: {
total: 0,
page: 1,
page_size: 10,
},
},
};
const knowledgeBaseSlice = createSlice({
@ -194,6 +206,45 @@ const knowledgeBaseSlice = createSlice({
.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';
});
},
});

View File

@ -237,3 +237,62 @@ export const uploadDocument = createAsyncThunk(
}
}
);
/**
* 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 || '文档删除失败');
}
}
);

48
src/utils/dateUtils.js Normal file
View File

@ -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}`;
};