Compare commits

...

3 Commits

Author SHA1 Message Date
a415013f61
Merge pull request #5 from Funkoala14/dev
[PR]ADD Files api
2025-04-01 22:24:00 -04:00
6d641bb309 [dev]add docs list api 2025-04-01 22:22:53 -04:00
ba53185050 [dev]add upload file api 2025-04-01 22:05:43 -04:00
7 changed files with 504 additions and 102 deletions

View File

@ -6,6 +6,7 @@ import {
updateKnowledgeBase, updateKnowledgeBase,
deleteKnowledgeBase, deleteKnowledgeBase,
changeKnowledgeBaseType, changeKnowledgeBaseType,
getKnowledgeBaseDocuments,
} from '../../../store/knowledgeBase/knowledgeBase.thunks'; } from '../../../store/knowledgeBase/knowledgeBase.thunks';
// //
@ -13,6 +14,8 @@ import Breadcrumb from './components/Breadcrumb';
import KnowledgeBaseForm from './components/KnowledgeBaseForm'; import KnowledgeBaseForm from './components/KnowledgeBaseForm';
import DeleteConfirmModal from './components/DeleteConfirmModal'; import DeleteConfirmModal from './components/DeleteConfirmModal';
import UserPermissionsManager from './components/UserPermissionsManager'; import UserPermissionsManager from './components/UserPermissionsManager';
import FileUploadModal from './components/FileUploadModal';
import DocumentList from './components/DocumentList';
// //
const departmentGroups = { const departmentGroups = {
@ -47,6 +50,14 @@ export default function SettingsTab({ knowledgeBase }) {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [availableGroups, setAvailableGroups] = useState([]); const [availableGroups, setAvailableGroups] = useState([]);
const [showUploadModal, setShowUploadModal] = useState(false);
//
useEffect(() => {
if (knowledgeBase?.id) {
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBase.id }));
}
}, [dispatch, knowledgeBase?.id]);
// //
useEffect(() => { useEffect(() => {
@ -93,7 +104,7 @@ export default function SettingsTab({ knowledgeBase }) {
allowed = ['admin', 'member', 'private'].includes(value); allowed = ['admin', 'member', 'private'].includes(value);
} else { } else {
// private // private
allowed = ['admin', 'private'].includes(value); allowed = ['admin', 'private'].includes(value);
} }
if (!allowed) { if (!allowed) {
@ -334,6 +345,27 @@ export default function SettingsTab({ knowledgeBase }) {
availableGroups={availableGroups} availableGroups={availableGroups}
/> />
{/* Document Management Section */}
<div className='card border-0 shadow-sm mt-4'>
<div className='card-body'>
<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>
{/* File Upload Modal */}
<FileUploadModal
show={showUploadModal}
onClose={() => setShowUploadModal(false)}
knowledgeBaseId={knowledgeBase.id}
/>
{/* User Permissions Manager */} {/* User Permissions Manager */}
{/* <UserPermissionsManager knowledgeBase={knowledgeBase} /> */} {/* <UserPermissionsManager knowledgeBase={knowledgeBase} /> */}

View File

@ -1,12 +1,42 @@
import React from 'react'; 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 }) => { const DocumentList = ({ knowledgeBaseId }) => {
if (documents.length === 0) { const dispatch = useDispatch();
return <div className='alert alert-warning'>暂无数据集请上传数据集</div>; 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 ( return (
@ -14,59 +44,51 @@ const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocum
<table className='table table-hover'> <table className='table table-hover'>
<thead className='table-light'> <thead className='table-light'>
<tr> <tr>
<th scope='col' width='40'> <th scope='col'>文档名称</th>
<div className='form-check'> <th scope='col'>创建时间</th>
<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' width='100'> <th scope='col'>操作</th>
操作
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{documents.map((doc) => ( {items.map((doc) => (
<tr key={doc.id}> <tr key={doc.id}>
<td>{doc.document_name}</td>
<td>{formatDateTime(doc.create_time)}</td>
<td>{formatDateTime(doc.update_time)}</td>
<td> <td>
<div className='form-check'> <button
<input className='btn btn-sm btn-outline-danger'
className='form-check-input' onClick={() => handleDeleteDocument(doc.document_id)}
type='checkbox' >
checked={selectedDocuments.includes(doc.id)} 删除
onChange={() => onSelectDocument(doc.id)} </button>
/>
</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>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </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> </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; export default DocumentList;

View File

@ -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 = ({ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
show, const dispatch = useDispatch();
newFile,
fileErrors,
isSubmitting,
onClose,
onDescriptionChange,
onFileChange,
onFileDrop,
onDragOver,
onUploadAreaClick,
onUpload,
}) => {
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const modalRef = useRef(null); const modalRef = useRef(null);
const [selectedFile, setSelectedFile] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const [fileError, setFileError] = useState('');
// //
const handleUploadAreaClick = () => { const handleUploadAreaClick = () => {
@ -28,13 +22,76 @@ const FileUploadModal = ({
const handleDragOver = (e) => { const handleDragOver = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onDragOver?.(e);
}; };
const handleDrop = (e) => { const handleDrop = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); 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 = ({
}} }}
> >
<div className='modal-header d-flex justify-content-between align-items-center mb-3'> <div className='modal-header d-flex justify-content-between align-items-center mb-3'>
<h5 className='modal-title m-0'>上传文件</h5> <h5 className='modal-title m-0'>上传文档</h5>
<button type='button' className='btn-close' onClick={onClose} aria-label='Close'></button> <button
type='button'
className='btn-close'
onClick={handleClose}
disabled={isUploading}
aria-label='Close'
></button>
</div> </div>
<div className='modal-body'> <div className='modal-body'>
<div <div
className={`mb-3 p-4 border rounded text-center ${ className={`mb-3 p-4 border rounded text-center ${
fileErrors.file ? 'border-danger' : 'border-dashed' fileError ? 'border-danger' : 'border-dashed'
}`} }`}
style={{ cursor: 'pointer' }} style={{ cursor: isUploading ? 'not-allowed' : 'pointer' }}
onClick={handleUploadAreaClick} onClick={!isUploading ? handleUploadAreaClick : undefined}
onDrop={handleDrop} onDrop={!isUploading ? handleDrop : undefined}
onDragOver={handleDragOver} onDragOver={handleDragOver}
> >
<input <input
type='file' type='file'
ref={fileInputRef} ref={fileInputRef}
className='d-none' className='d-none'
onChange={onFileChange} onChange={handleFileChange}
accept='.pdf,.docx,.txt,.csv' accept='.pdf,.doc,.docx,.txt,.md,.csv,.xlsx,.xls'
disabled={isUploading}
/> />
{newFile.file ? ( {selectedFile ? (
<div> <div>
<p className='mb-1'>已选择文件</p> <p className='mb-1'>已选择文件</p>
<p className='fw-bold mb-0'>{newFile.file.name}</p> <p className='fw-bold mb-0'>{selectedFile.name}</p>
</div> </div>
) : ( ) : (
<div> <div>
<p className='mb-1'>点击或拖拽文件到此处上传</p> <p className='mb-1'>点击或拖拽文件到此处上传</p>
<p className='text-muted small mb-0'>支持 PDF, DOCX, TXT, CSV 等格式</p> <p className='text-muted small mb-0'>
支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式
</p>
</div> </div>
)} )}
{fileErrors.file && <div className='text-danger mt-2'>{fileErrors.file}</div>} {fileError && <div className='text-danger mt-2'>{fileError}</div>}
</div>
<div className='mb-3'>
<label htmlFor='fileDescription' className='form-label'>
文件描述
</label>
<textarea
className='form-control'
id='fileDescription'
rows='3'
value={newFile.description}
onChange={onDescriptionChange}
placeholder='请输入文件描述(可选)'
></textarea>
</div> </div>
</div> </div>
<div className='modal-footer gap-2'> <div className='modal-footer gap-2'>
<button type='button' className='btn btn-secondary' onClick={onClose}> <button type='button' className='btn btn-secondary' onClick={handleClose} disabled={isUploading}>
取消 关闭
</button> </button>
<button <button
type='button' type='button'
className='btn btn-dark' className='btn btn-primary'
onClick={onUpload} onClick={handleUpload}
disabled={!newFile.file || isSubmitting} disabled={!selectedFile || isUploading}
> >
{isSubmitting ? ( {isUploading ? (
<> <>
<span <span
className='spinner-border spinner-border-sm me-2' className='spinner-border spinner-border-sm me-2'
@ -145,7 +198,7 @@ const FileUploadModal = ({
上传中... 上传中...
</> </>
) : ( ) : (
'上传' '上传文档'
)} )}
</button> </button>
</div> </div>

View File

@ -522,8 +522,8 @@ const mockPermissionApi = {
}; };
// Mock API functions // Mock API functions
export const mockGet = async (url, config = {}) => { export const mockGet = async (url, params = {}) => {
console.log(`[MOCK API] GET ${url}`, config); console.log(`[MOCK API] GET ${url}`, params);
// Simulate network delay // Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
@ -537,7 +537,6 @@ export const mockGet = async (url, config = {}) => {
// Get knowledge bases // Get knowledge bases
if (url === '/knowledge-bases/') { if (url === '/knowledge-bases/') {
const params = config.params || { page: 1, page_size: 10 };
const result = paginate(knowledgeBases, params.page_size, params.page); const result = paginate(knowledgeBases, params.page_size, params.page);
return { return {
@ -576,7 +575,6 @@ export const mockGet = async (url, config = {}) => {
// Get chat history // Get chat history
if (url === '/chat-history/') { if (url === '/chat-history/') {
const params = config.params || { page: 1, page_size: 10 };
const result = mockGetChatHistory(params); const result = mockGetChatHistory(params);
return { return {
data: { data: {
@ -620,7 +618,7 @@ export const mockGet = async (url, config = {}) => {
// Knowledge base search // Knowledge base search
if (url === '/knowledge-bases/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( const filtered = knowledgeBases.filter(
(kb) => (kb) =>
kb.name.toLowerCase().includes(keyword.toLowerCase()) || 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' } } }; 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); console.log(`[MOCK API] POST ${url}`, data);
// Simulate network delay // 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' } } }; throw { response: { status: 404, data: { message: 'Not found' } } };
}; };
@ -966,6 +1032,22 @@ export const mockDelete = async (url) => {
return { data: mockDeleteChat(id) }; 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' } } }; throw { response: { status: 404, data: { message: 'Not found' } } };
}; };

View File

@ -8,6 +8,9 @@ import {
searchKnowledgeBases, searchKnowledgeBases,
requestKnowledgeBaseAccess, requestKnowledgeBaseAccess,
getKnowledgeBaseById, getKnowledgeBaseById,
uploadDocument,
getKnowledgeBaseDocuments,
deleteKnowledgeBaseDocument,
} from './knowledgeBase.thunks'; } from './knowledgeBase.thunks';
const initialState = { const initialState = {
@ -27,6 +30,17 @@ const initialState = {
batchLoading: false, batchLoading: false,
editStatus: 'idle', editStatus: 'idle',
requestAccessStatus: 'idle', requestAccessStatus: 'idle',
uploadStatus: 'idle',
documents: {
items: [],
loading: false,
error: null,
pagination: {
total: 0,
page: 1,
page_size: 10,
},
},
}; };
const knowledgeBaseSlice = createSlice({ const knowledgeBaseSlice = createSlice({
@ -180,6 +194,57 @@ const knowledgeBaseSlice = createSlice({
.addCase(getKnowledgeBaseById.rejected, (state, action) => { .addCase(getKnowledgeBaseById.rejected, (state, action) => {
state.loading = false; state.loading = false;
state.error = action.payload || 'Failed to get knowledge base details'; 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';
}); });
}, },
}); });

View File

@ -1,5 +1,5 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; 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'; import { showNotification } from '../notification.slice';
/** /**
@ -20,7 +20,7 @@ export const fetchKnowledgeBases = createAsyncThunk(
return response.data; return response.data;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return rejectWithValue(error.response?.data.error.message || 'Failed to fetch knowledge bases'); 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 || '文档删除失败');
}
}
);

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