mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 07:20:55 +08:00
[dev]upload files
This commit is contained in:
parent
518c71b859
commit
5609453eba
@ -338,7 +338,7 @@ export default function DatasetTab({ knowledgeBase }) {
|
||||
onDeleteDocument={handleDeleteDocument}
|
||||
/>
|
||||
{/* Pagination */}
|
||||
<div className='d-flex justify-content-between align-items-center mt-3'>
|
||||
{/* <div className='d-flex justify-content-between align-items-center mt-3'>
|
||||
<div>
|
||||
每页行数:
|
||||
<select className='form-select form-select d-inline-block ms-2' style={{ width: '70px' }}>
|
||||
@ -364,7 +364,7 @@ export default function DatasetTab({ knowledgeBase }) {
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* File upload modal */}
|
||||
<FileUploadModal
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { formatDate } from '../../../../utils/dateUtils';
|
||||
import { deleteKnowledgeBaseDocument } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
|
||||
@ -9,10 +9,67 @@ import DocumentPreviewModal from './DocumentPreviewModal';
|
||||
*/
|
||||
const DocumentList = ({ knowledgeBaseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { items, loading, pagination } = useSelector((state) => state.knowledgeBase.documents);
|
||||
const { items, loading, error } = useSelector((state) => state.knowledgeBase.documents);
|
||||
const [previewModalVisible, setPreviewModalVisible] = useState(false);
|
||||
const [selectedDocumentId, setSelectedDocumentId] = useState(null);
|
||||
|
||||
// 前端分页状态
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [displayedItems, setDisplayedItems] = useState([]);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
// 处理分页
|
||||
useEffect(() => {
|
||||
if (!items || items.length === 0) {
|
||||
setDisplayedItems([]);
|
||||
setTotalPages(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
const total = Math.ceil(items.length / pageSize);
|
||||
setTotalPages(total);
|
||||
|
||||
// 确保当前页有效
|
||||
let page = currentPage;
|
||||
if (page > total) {
|
||||
page = total;
|
||||
setCurrentPage(page);
|
||||
}
|
||||
|
||||
// 计算当前页显示的项目
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize, items.length);
|
||||
setDisplayedItems(items.slice(startIndex, endIndex));
|
||||
|
||||
console.log(`前端分页: 总项目 ${items.length}, 当前页 ${page}/${total}, 显示 ${startIndex + 1}-${endIndex}`);
|
||||
}, [items, currentPage, pageSize]);
|
||||
|
||||
// 分页控制
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 调试输出
|
||||
useEffect(() => {
|
||||
console.log('DocumentList 渲染 - 知识库ID:', knowledgeBaseId);
|
||||
console.log('文档列表状态:', { items, loading, error });
|
||||
console.log('文档列表项数:', items ? items.length : 0);
|
||||
}, [knowledgeBaseId, items, loading, error]);
|
||||
|
||||
const handleDeleteDocument = (documentId) => {
|
||||
if (window.confirm('确定要删除此文档吗?')) {
|
||||
dispatch(
|
||||
@ -35,6 +92,7 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
console.log('DocumentList - 加载中...');
|
||||
return (
|
||||
<div className='text-center py-4'>
|
||||
<div className='spinner-border text-primary' role='status'>
|
||||
@ -44,7 +102,8 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
if (!items || items.length === 0) {
|
||||
console.log('DocumentList - 暂无文档');
|
||||
return (
|
||||
<div className='text-center py-4 text-muted'>
|
||||
<p>暂无文档,请上传文档</p>
|
||||
@ -52,6 +111,7 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
||||
);
|
||||
}
|
||||
|
||||
console.log('DocumentList - 渲染文档列表', displayedItems);
|
||||
return (
|
||||
<>
|
||||
<div className='table-responsive'>
|
||||
@ -65,22 +125,30 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((doc) => (
|
||||
<tr key={doc.id}>
|
||||
<td>{doc.document_name}</td>
|
||||
<td>{formatDateTime(doc.create_time)}</td>
|
||||
<td>{formatDateTime(doc.update_time)}</td>
|
||||
{displayedItems.map((doc) => (
|
||||
<tr key={doc.id || doc.document_id}>
|
||||
<td>
|
||||
<div
|
||||
className='text-truncate'
|
||||
style={{ maxWidth: '250px' }}
|
||||
title={doc.document_name || doc.name}
|
||||
>
|
||||
{doc.document_name || doc.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className='text-nowrap'>{formatDateTime(doc.create_time || doc.created_at)}</td>
|
||||
<td className='text-nowrap'>{formatDateTime(doc.update_time || doc.updated_at)}</td>
|
||||
<td>
|
||||
<div className='btn-group' role='group'>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-dark me-2'
|
||||
onClick={() => handlePreviewDocument(doc.document_id)}
|
||||
onClick={() => handlePreviewDocument(doc.document_id || doc.id)}
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-danger'
|
||||
onClick={() => handleDeleteDocument(doc.document_id)}
|
||||
onClick={() => handleDeleteDocument(doc.document_id || doc.id)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
@ -91,9 +159,33 @@ const DocumentList = ({ knowledgeBaseId }) => {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{pagination.total > 0 && (
|
||||
{/* 分页控件 */}
|
||||
{items.length > 0 && (
|
||||
<div className='d-flex justify-content-between align-items-center mt-3'>
|
||||
<p className='text-muted mb-0'>共 {pagination.total} 条记录</p>
|
||||
<div className='text-muted mb-0'>
|
||||
共 {items.length} 条记录,第 {currentPage}/{totalPages} 页
|
||||
</div>
|
||||
<nav aria-label='文档列表分页'>
|
||||
<ul className='pagination dark-pagination pagination-sm mb-0'>
|
||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||
<button className='page-link' onClick={handlePrevPage}>
|
||||
上一页
|
||||
</button>
|
||||
</li>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
||||
<li key={page} className={`page-item ${page === currentPage ? 'active' : ''}`}>
|
||||
<button className='page-link' onClick={() => handlePageChange(page)}>
|
||||
{page}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||
<button className='page-link' onClick={handleNextPage}>
|
||||
下一页
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -34,6 +34,8 @@ const DocumentPreviewModal = ({ show, documentId, knowledgeBaseId, onClose }) =>
|
||||
// 兜底情况
|
||||
setDocumentContent(response);
|
||||
}
|
||||
console.log(documentContent);
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取文档内容失败:', error);
|
||||
dispatch(
|
||||
@ -88,8 +90,8 @@ const DocumentPreviewModal = ({ show, documentId, knowledgeBaseId, onClose }) =>
|
||||
</div>
|
||||
) : documentContent ? (
|
||||
<div className='document-content'>
|
||||
{documentContent.content &&
|
||||
documentContent.content.map((section, index) => {
|
||||
{documentContent?.paragraphs.length > 0 &&
|
||||
documentContent.paragraphs.map((section, index) => {
|
||||
let contentDisplay;
|
||||
try {
|
||||
// 尝试解析JSON内容
|
||||
|
@ -9,9 +9,10 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const fileInputRef = useRef(null);
|
||||
const modalRef = useRef(null);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [fileError, setFileError] = useState('');
|
||||
const [uploadResults, setUploadResults] = useState(null);
|
||||
|
||||
// 处理上传区域点击事件
|
||||
const handleUploadAreaClick = () => {
|
||||
@ -29,59 +30,68 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
handleFileSelected(e.dataTransfer.files[0]);
|
||||
handleFilesSelected(e.dataTransfer.files);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
handleFileSelected(e.target.files[0]);
|
||||
handleFilesSelected(e.target.files);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelected = (file) => {
|
||||
const handleFilesSelected = (files) => {
|
||||
setFileError('');
|
||||
setSelectedFile(file);
|
||||
// 将FileList转为数组
|
||||
const filesArray = Array.from(files);
|
||||
setSelectedFiles((prev) => [...prev, ...filesArray]);
|
||||
};
|
||||
|
||||
const removeFile = (index) => {
|
||||
setSelectedFiles((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const resetFileInput = () => {
|
||||
setSelectedFile(null);
|
||||
setSelectedFiles([]);
|
||||
setFileError('');
|
||||
setUploadResults(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!selectedFile) {
|
||||
setFileError('请选择要上传的文件');
|
||||
if (selectedFiles.length === 0) {
|
||||
setFileError('请至少选择一个要上传的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setUploadResults(null);
|
||||
|
||||
try {
|
||||
await dispatch(
|
||||
const result = await dispatch(
|
||||
uploadDocument({
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
file: selectedFile,
|
||||
files: selectedFiles,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
// 成功上传后刷新文档列表
|
||||
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId }));
|
||||
|
||||
// Reset the file input
|
||||
resetFileInput();
|
||||
// 显示上传结果
|
||||
setUploadResults(result);
|
||||
|
||||
// 上传成功后关闭模态框
|
||||
onClose();
|
||||
// 如果没有失败的文件,就在3秒后自动关闭模态窗
|
||||
if (result.failed_count === 0) {
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
setFileError('文件上传失败: ' + (error?.message || '未知错误'));
|
||||
|
||||
// 清空选中的文件
|
||||
resetFileInput();
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
@ -130,9 +140,11 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
<div
|
||||
className='modal-content bg-white rounded shadow'
|
||||
style={{
|
||||
width: '500px',
|
||||
width: '600px',
|
||||
maxWidth: '90%',
|
||||
padding: '20px',
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
|
||||
@ -162,22 +174,92 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
onChange={handleFileChange}
|
||||
accept='.pdf,.doc,.docx,.txt,.md,.csv,.xlsx,.xls'
|
||||
disabled={isUploading}
|
||||
multiple
|
||||
/>
|
||||
{selectedFile ? (
|
||||
<div>
|
||||
<p className='mb-1'>已选择文件:</p>
|
||||
<p className='fw-bold mb-0'>{selectedFile.name}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className='mb-1'>点击或拖拽文件到此处上传</p>
|
||||
<p className='text-muted small mb-0'>
|
||||
支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className='mb-1'>点击或拖拽文件到此处上传</p>
|
||||
<p className='text-muted small mb-0'>支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式</p>
|
||||
</div>
|
||||
{fileError && <div className='text-danger mt-2'>{fileError}</div>}
|
||||
</div>
|
||||
|
||||
{/* 选择的文件列表 */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className='mb-3'>
|
||||
<p className='fw-bold mb-2'>已选择 {selectedFiles.length} 个文件:</p>
|
||||
<ul className='list-group'>
|
||||
{selectedFiles.map((file, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className='list-group-item d-flex justify-content-between align-items-center'
|
||||
>
|
||||
<div
|
||||
className='d-flex align-items-center'
|
||||
style={{ maxWidth: 'calc(100% - 70px)' }}
|
||||
>
|
||||
<div className='text-truncate'>{file.name}</div>
|
||||
<div className='text-nowrap ms-2 text-muted' style={{ flexShrink: 0 }}>
|
||||
({(file.size / 1024).toFixed(0)} KB)
|
||||
</div>
|
||||
</div>
|
||||
{!isUploading && (
|
||||
<button
|
||||
className='btn btn-sm btn-outline-danger'
|
||||
onClick={() => removeFile(index)}
|
||||
style={{ minWidth: '60px' }}
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传结果显示 */}
|
||||
{uploadResults && (
|
||||
<div className='mt-3 border rounded p-3'>
|
||||
<h6 className='mb-2'>上传结果</h6>
|
||||
<p className='mb-2'>
|
||||
总文件: {uploadResults.total_files}, 成功: {uploadResults.uploaded_count}, 失败:{' '}
|
||||
{uploadResults.failed_count}
|
||||
</p>
|
||||
|
||||
{uploadResults.documents && uploadResults.documents.length > 0 && (
|
||||
<>
|
||||
<p className='mb-1 fw-bold text-success'>上传成功:</p>
|
||||
<ul className='list-group mb-2'>
|
||||
{uploadResults.documents.map((doc) => (
|
||||
<li key={doc.id} className='list-group-item py-2 d-flex align-items-center'>
|
||||
<span className='badge bg-success me-2'>✓</span>
|
||||
<span className='text-truncate'>{doc.name}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{uploadResults.failed_documents && uploadResults.failed_documents.length > 0 && (
|
||||
<>
|
||||
<p className='mb-1 fw-bold text-danger'>上传失败:</p>
|
||||
<ul className='list-group'>
|
||||
{uploadResults.failed_documents.map((doc, index) => (
|
||||
<li key={index} className='list-group-item py-2 d-flex align-items-center'>
|
||||
<span className='badge bg-danger me-2'>✗</span>
|
||||
<div className='text-truncate'>
|
||||
{doc.name}
|
||||
{doc.reason && (
|
||||
<small className='ms-2 text-danger'>({doc.reason})</small>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='modal-footer gap-2'>
|
||||
<button type='button' className='btn btn-secondary' onClick={handleClose} disabled={isUploading}>
|
||||
@ -187,7 +269,7 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
type='button'
|
||||
className='btn btn-dark'
|
||||
onClick={handleUpload}
|
||||
disabled={!selectedFile || isUploading}
|
||||
disabled={selectedFiles.length === 0 || isUploading}
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
@ -199,7 +281,7 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
'上传文档'
|
||||
`上传文档${selectedFiles.length > 0 ? ` (${selectedFiles.length})` : ''}`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -218,9 +218,13 @@ const knowledgeBaseSlice = createSlice({
|
||||
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,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
};
|
||||
console.log('文档数据已更新到store:', {
|
||||
itemsCount: state.documents.items.length,
|
||||
items: state.documents.items,
|
||||
});
|
||||
})
|
||||
.addCase(getKnowledgeBaseDocuments.rejected, (state, action) => {
|
||||
state.documents.loading = false;
|
||||
|
@ -200,32 +200,54 @@ export const requestKnowledgeBaseAccess = createAsyncThunk(
|
||||
);
|
||||
|
||||
/**
|
||||
* Upload a document to a knowledge base
|
||||
* Upload documents 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
|
||||
* @param {File[]} params.files - Files to upload
|
||||
*/
|
||||
export const uploadDocument = createAsyncThunk(
|
||||
'knowledgeBase/uploadDocument',
|
||||
async ({ knowledge_base_id, file }, { rejectWithValue, dispatch }) => {
|
||||
async ({ knowledge_base_id, files }, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 支持单文件和多文件上传
|
||||
if (Array.isArray(files)) {
|
||||
// 多文件上传
|
||||
files.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
} else {
|
||||
// 单文件上传(向后兼容)
|
||||
formData.append('files', files);
|
||||
}
|
||||
|
||||
const response = await post(`/knowledge-bases/${knowledge_base_id}/upload_document/`, formData, true);
|
||||
|
||||
// 处理新的返回格式
|
||||
if (response.data && response.data.code === 200) {
|
||||
const result = response.data.data;
|
||||
|
||||
// 使用API返回的消息作为通知
|
||||
dispatch(
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: response.data.message || `文档上传完成,成功: ${result.uploaded_count},失败: ${result.failed_count}`,
|
||||
})
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: `文档 ${file.name} 上传成功`,
|
||||
message: Array.isArray(files)
|
||||
? `${files.length} 个文档上传成功`
|
||||
: `文档 ${files.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 || '文档上传失败';
|
||||
@ -244,24 +266,33 @@ 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 }) => {
|
||||
async ({ knowledge_base_id }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get(`/knowledge-bases/${knowledge_base_id}/documents/`, {
|
||||
params: { page, page_size },
|
||||
});
|
||||
|
||||
console.log('获取知识库文档列表:', knowledge_base_id);
|
||||
const { data, code } = await get(`/knowledge-bases/${knowledge_base_id}/documents`);
|
||||
console.log('文档列表API响应:', { data, code });
|
||||
|
||||
// 处理返回格式
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data;
|
||||
if (code === 200) {
|
||||
console.log('API返回数据:', data);
|
||||
return {
|
||||
items: data || [],
|
||||
total: (data || []).length
|
||||
};
|
||||
} else {
|
||||
// 未知格式,尝试提取数据
|
||||
const items = data?.items || data || [];
|
||||
console.log('未识别格式,提取数据:', items);
|
||||
return {
|
||||
items: items,
|
||||
total: items.length
|
||||
};
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取知识库文档失败:', error);
|
||||
return rejectWithValue(error.response?.data?.message || '获取文档列表失败');
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user