1.同步包版本。
2.交叉评查的任务中上传文件。 3.添加dify库名解决保存配置失败的问题。
This commit is contained in:
@@ -362,10 +362,81 @@ export function generateFileId(): string {
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 向已有任务上传新文档
|
||||
*
|
||||
* POST /api/v2/cross_review/tasks/{task_id}/upload_documents
|
||||
*
|
||||
* @param params 上传参数
|
||||
* @returns 上传结果
|
||||
*/
|
||||
export async function uploadDocumentToTask(params: {
|
||||
taskId: number;
|
||||
file: File;
|
||||
jwtToken?: string | null;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
}> {
|
||||
const { taskId, file, jwtToken } = params;
|
||||
|
||||
try {
|
||||
console.log('[上传文档到任务] 开始上传:', { taskId, fileName: file.name });
|
||||
|
||||
const formData = new FormData();
|
||||
// 添加文件(使用 files 字段名)
|
||||
formData.append('files', file, file.name);
|
||||
|
||||
const uploadEndpoint = `/api/v2/cross_review/tasks/${taskId}/upload_documents`;
|
||||
const uploadUrl = API_BASE_URL + uploadEndpoint;
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (jwtToken) {
|
||||
headers['Authorization'] = `Bearer ${jwtToken}`;
|
||||
}
|
||||
|
||||
const response = await axios.post(uploadUrl, formData, { headers });
|
||||
const result = response.data;
|
||||
|
||||
// 新接口响应格式: { code: 0, success: true, message: "...", data: {...} }
|
||||
if (result && (result.success || result.code === 0)) {
|
||||
console.log('[上传文档到任务] 上传成功:', result.message);
|
||||
return { success: true, data: result.data };
|
||||
} else {
|
||||
console.error('[上传文档到任务] 上传失败:', result.detail || result.message);
|
||||
return { success: false, error: result.detail || result.message || '上传失败' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[上传文档到任务] 请求失败:', error);
|
||||
|
||||
let errorMessage = '上传文档失败';
|
||||
if (axios.isAxiosError(error)) {
|
||||
// 新接口错误格式: { detail: "错误信息" }
|
||||
if (error.response?.data?.detail) {
|
||||
errorMessage = error.response.data.detail;
|
||||
} else if (error.response?.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
} else if (error.response?.status === 403) {
|
||||
errorMessage = '无权操作:只有任务创建者或主要负责人可以上传文档';
|
||||
} else if (error.response?.status === 400) {
|
||||
errorMessage = '请求参数错误';
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message || errorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { API_BASE_URL } from '../../config/api-config';
|
||||
import { postgrestPut, postgrestGet } from '../postgrest-client';
|
||||
import axios from 'axios';
|
||||
export { uploadDocumentToTask } from './cross-files-upload';
|
||||
|
||||
// 交叉评查任务状态枚举
|
||||
export enum CrossCheckingTaskStatus {
|
||||
|
||||
@@ -8,13 +8,16 @@ import { ResultStats } from '../ui/ResultStats';
|
||||
import { toastService } from '../ui/Toast';
|
||||
import { AttachmentUploadModal } from '../ui/AttachmentUploadModal';
|
||||
import { TemplateUploadModal } from '../ui/TemplateUploadModal';
|
||||
import { UploadArea, type UploadAreaRef } from '../ui/UploadArea';
|
||||
import { formatDate } from '~/utils';
|
||||
import {
|
||||
type CrossReviewDocumentWithVersion,
|
||||
type CrossReviewHistoryVersion,
|
||||
appendTaskDocumentAttachments,
|
||||
uploadCrossReviewDocumentTemplate,
|
||||
uploadDocumentToTask,
|
||||
} from '~/api/cross-checking/cross-files';
|
||||
import { formatFileSize } from '~/api/cross-checking/cross-files-upload';
|
||||
|
||||
// 导出样式链接
|
||||
export const links = () => [];
|
||||
@@ -69,15 +72,6 @@ const crossReviewAuditStatusMapping: Record<string, { label: string; color: stri
|
||||
"1": { label: "已评查", color: "green", icon: "ri-check-line" },
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
export function DocumentListModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -124,6 +118,12 @@ export function DocumentListModal({
|
||||
const [showTemplateUpload, setShowTemplateUpload] = useState(false);
|
||||
const [templateUploading, setTemplateUploading] = useState(false);
|
||||
|
||||
// 上传文件模态框状态
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||
const [isFileUploading, setIsFileUploading] = useState(false);
|
||||
const uploadAreaRef = useRef<UploadAreaRef>(null);
|
||||
|
||||
// 同步外部文档数据到本地
|
||||
useEffect(() => {
|
||||
setLocalDocuments(documents.map(doc => ({ ...doc, isExpanded: expandedRows.has(doc.id) })));
|
||||
@@ -217,6 +217,78 @@ export function DocumentListModal({
|
||||
setSelectedDocumentPath(null);
|
||||
};
|
||||
|
||||
// 打开上传文件模态框
|
||||
const handleOpenUploadModal = () => {
|
||||
setShowUploadModal(true);
|
||||
setUploadedFile(null);
|
||||
};
|
||||
|
||||
// 关闭上传文件模态框
|
||||
const handleCloseUploadModal = () => {
|
||||
setShowUploadModal(false);
|
||||
setUploadedFile(null);
|
||||
uploadAreaRef.current?.resetFileInput();
|
||||
};
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelected = (files: FileList) => {
|
||||
if (files.length === 0) return;
|
||||
const file = files[0];
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isValidType = file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
||||
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
||||
file.type === 'application/x-zip-compressed';
|
||||
// || file.type === 'application/x-7z-compressed' || fileName.endsWith('.7z');
|
||||
|
||||
if (!isValidType) {
|
||||
// toastService.error('只能上传 PDF、DOCX 文件或 ZIP、7Z 压缩包');
|
||||
toastService.error('只能上传 PDF、DOCX 文件或 ZIP 压缩包');
|
||||
return;
|
||||
}
|
||||
setUploadedFile(file);
|
||||
};
|
||||
|
||||
// 删除选中的文件
|
||||
const handleRemoveFile = () => {
|
||||
setUploadedFile(null);
|
||||
uploadAreaRef.current?.resetFileInput();
|
||||
};
|
||||
|
||||
// 确认上传文件
|
||||
const handleUploadFile = async () => {
|
||||
if (!uploadedFile || !taskId) {
|
||||
toastService.error('缺少必要参数');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsFileUploading(true);
|
||||
try {
|
||||
const result = await uploadDocumentToTask({
|
||||
taskId,
|
||||
file: uploadedFile,
|
||||
jwtToken: frontendJWT
|
||||
});
|
||||
|
||||
if (!result.success || result.error) {
|
||||
throw new Error(result.error || '上传文件失败');
|
||||
}
|
||||
|
||||
toastService.success('文件上传成功!正在后台处理中');
|
||||
handleCloseUploadModal();
|
||||
|
||||
// 刷新文档列表
|
||||
if (onSearch) {
|
||||
onSearch(searchKeyword);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error);
|
||||
toastService.error(error instanceof Error ? error.message : '上传文件失败');
|
||||
} finally {
|
||||
setIsFileUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理追加附件上传
|
||||
const handleAttachmentUpload = async (files: File[], _mergeMode: 'overwrite' | 'new', remark: string) => {
|
||||
if (!taskId || !selectedDocumentId) {
|
||||
@@ -642,30 +714,43 @@ export function DocumentListModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:搜索框 */}
|
||||
{onSearch && (
|
||||
<div className="flex items-center">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索文件名称或文档编号"
|
||||
value={searchKeyword}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md text-sm w-64 focus:outline-none focus:ring-0"
|
||||
/>
|
||||
<i className="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
{searchKeyword && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
onClick={() => handleSearchChange('')}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
)}
|
||||
{/* 右侧:搜索框 + 上传按钮 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 上传文件按钮 - 仅负责人可见 */}
|
||||
{isProposer && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-green-800 text-white rounded-md text-sm hover:bg-green-700 transition-colors"
|
||||
onClick={handleOpenUploadModal}
|
||||
>
|
||||
<i className="ri-upload-cloud-2-line"></i>
|
||||
上传文件
|
||||
</button>
|
||||
)}
|
||||
{onSearch && (
|
||||
<div className="flex items-center">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索文件名称或文档编号"
|
||||
value={searchKeyword}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md text-sm w-64 focus:outline-none focus:ring-0"
|
||||
/>
|
||||
<i className="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
{searchKeyword && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
onClick={() => handleSearchChange('')}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
@@ -780,6 +865,150 @@ export function DocumentListModal({
|
||||
title="上传合同模板"
|
||||
supportedFormatsDesc="支持.pdf、.docx格式,用于与合同文档进行结构对比"
|
||||
/>
|
||||
|
||||
{/* 上传文件模态框 */}
|
||||
<Modal
|
||||
isOpen={showUploadModal}
|
||||
onClose={handleCloseUploadModal}
|
||||
title="上传文档到任务"
|
||||
size="medium"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex gap-6">
|
||||
{/* 左侧:上传区域 */}
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-700 mb-3">上传文件</div>
|
||||
<UploadArea
|
||||
ref={uploadAreaRef}
|
||||
onFilesSelected={handleFileSelected}
|
||||
className="custom-upload-area"
|
||||
// accept=".pdf,.docx,.zip,.7z"
|
||||
accept=".pdf,.docx,.zip"
|
||||
multiple={false}
|
||||
icon="ri-upload-cloud-2-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="text-gray-500 text-xs mt-2">
|
||||
<div>支持文件类型:</div>
|
||||
<div className="flex flex-wrap gap-2 mt-1 justify-center">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded bg-red-50 text-red-600">
|
||||
<i className="ri-file-pdf-line mr-1"></i>PDF
|
||||
</span>
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded bg-blue-50 text-blue-600">
|
||||
<i className="ri-file-word-2-line mr-1"></i>DOCX
|
||||
</span>
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded bg-orange-50 text-orange-600">
|
||||
<i className="ri-folder-zip-line mr-1"></i>ZIP
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
disabled={isFileUploading || uploadedFile !== null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 右侧:文件信息展示 */}
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-700 mb-3">文件信息</div>
|
||||
<div className="border border-gray-200 rounded-lg bg-gray-50 min-h-[200px] p-4">
|
||||
{uploadedFile ? (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* 文件图标和类型 */}
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
{(() => {
|
||||
const fileName = uploadedFile.name.toLowerCase();
|
||||
if (fileName.endsWith('.pdf')) return <i className="ri-file-pdf-line text-5xl text-red-500"></i>;
|
||||
if (fileName.endsWith('.docx')) return <i className="ri-file-word-2-line text-5xl text-blue-500"></i>;
|
||||
if (fileName.endsWith('.zip') || fileName.endsWith('.7z')) return <i className="ri-folder-zip-line text-5xl text-orange-500"></i>;
|
||||
return <i className="ri-file-line text-5xl text-gray-500"></i>;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 文件详情 */}
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="bg-white rounded-md p-3 border border-gray-200">
|
||||
<div className="text-xs text-gray-500 mb-1">文件名</div>
|
||||
<div className="text-sm font-medium text-gray-800 truncate" title={uploadedFile.name}>
|
||||
{uploadedFile.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-md p-3 border border-gray-200">
|
||||
<div className="text-xs text-gray-500 mb-1">文件大小</div>
|
||||
<div className="text-sm font-medium text-gray-800">
|
||||
{formatFileSize(uploadedFile.size)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<div className="mt-4 pt-3 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveFile}
|
||||
disabled={isFileUploading}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 text-sm text-red-600 bg-red-50 hover:bg-red-100 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i>
|
||||
删除文件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-gray-400">
|
||||
<i className="ri-file-line text-4xl mb-2"></i>
|
||||
<span className="text-sm">暂未选择文件</span>
|
||||
<span className="text-xs mt-1">请在左侧上传区域选择文件</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 按钮区域 */}
|
||||
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseUploadModal}
|
||||
disabled={isFileUploading}
|
||||
className="px-4 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUploadFile}
|
||||
disabled={!uploadedFile || isFileUploading}
|
||||
className="px-4 py-2 text-sm text-white bg-green-800 rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
{isFileUploading ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin"></i>
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="ri-upload-line"></i>
|
||||
确认上传
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 上传进度提示 */}
|
||||
{isFileUploading && (
|
||||
<div className="mt-4">
|
||||
<div className="bg-blue-50 p-4 rounded-md border border-blue-100">
|
||||
<div className="flex items-center justify-center text-blue-800">
|
||||
<i className="ri-loader-4-line animate-spin text-xl mr-2"></i>
|
||||
<span className="font-medium">正在上传文件,请稍候...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,8 +220,10 @@ const configs: Record<string, ApiConfig> = {
|
||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||
},
|
||||
dify: {
|
||||
rerankingProviderName: 'langgenius/tongyi/tongyi',
|
||||
rerankingModelName: 'gte-rerank'
|
||||
// rerankingProviderName: 'langgenius/tongyi/tongyi',
|
||||
rerankingProviderName: 'langgenius/xinference/xinference',
|
||||
// rerankingModelName: 'gte-rerank'
|
||||
rerankingModelName: 'bge-reranker-v2-m3'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ export function useDatasetSettings(
|
||||
|
||||
// 仅更新检索设置
|
||||
await updateDatasetSettings(dataset.id, {
|
||||
name: dataset.name,
|
||||
retrieval_model: formValuesToRetrievalModel(retrievalSettings),
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[API] Dataset Detail:', { datasetId });
|
||||
// console.log('[API] Dataset Detail:', { datasetId });
|
||||
|
||||
// 转发请求到 FastAPI -> Dify API
|
||||
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`;
|
||||
@@ -96,6 +96,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
if (method === 'PATCH') {
|
||||
const body = await request.json();
|
||||
|
||||
// console.log('[API] body:', body);
|
||||
|
||||
// name 是必填字段
|
||||
if (!body.name || typeof body.name !== 'string') {
|
||||
return new Response(
|
||||
@@ -121,6 +123,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
if (body.retrieval_model && typeof body.retrieval_model === 'object') {
|
||||
const rm = body.retrieval_model;
|
||||
|
||||
const name = body.name || '';
|
||||
|
||||
// 验证 search_method
|
||||
const validSearchMethods = ['keyword_search', 'semantic_search', 'full_text_search', 'hybrid_search'];
|
||||
if (rm.search_method && !validSearchMethods.includes(rm.search_method)) {
|
||||
@@ -130,6 +134,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
allowedBody.name = name;
|
||||
|
||||
allowedBody.retrieval_model = {
|
||||
search_method: rm.search_method,
|
||||
reranking_enable: rm.reranking_enable ?? false,
|
||||
@@ -145,7 +151,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[API] Update Dataset Settings:', { datasetId, body: allowedBody });
|
||||
// console.log('[API] Update Dataset Settings:', { datasetId, body: allowedBody });
|
||||
|
||||
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`;
|
||||
const response = await fetch(apiUrl, {
|
||||
|
||||
@@ -749,11 +749,11 @@ export default function CrossCheckingIndex() {
|
||||
<span className="text-sm text-gray-600">总任务数:</span>
|
||||
<span className="stat-value">{stats.totalTasks}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
{/* <div className="stat-item">
|
||||
<i className="ri-time-line stat-icon"></i>
|
||||
<span className="text-sm text-gray-600">待开始:</span>
|
||||
<span className="stat-value">{stats.pendingTasks}</span>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="stat-item">
|
||||
<i className="ri-play-circle-line stat-icon"></i>
|
||||
<span className="text-sm text-gray-600">进行中:</span>
|
||||
@@ -806,7 +806,7 @@ export default function CrossCheckingIndex() {
|
||||
name="status"
|
||||
value={searchParams.get('status') || ''}
|
||||
options={[
|
||||
{ value: CrossCheckingTaskStatus.PENDING, label: "未开始" },
|
||||
// { value: CrossCheckingTaskStatus.PENDING, label: "未开始" },
|
||||
{ value: CrossCheckingTaskStatus.IN_PROGRESS, label: "进行中" },
|
||||
{ value: CrossCheckingTaskStatus.COMPLETED, label: "已完成" }
|
||||
]}
|
||||
|
||||
@@ -404,6 +404,7 @@ export default function Login() {
|
||||
</div>
|
||||
|
||||
{/* 管理员登录链接 */}
|
||||
{/* <div className="admin-login-link hidden"> */}
|
||||
<div className="admin-login-link">
|
||||
<button
|
||||
onClick={handleAdminLogin}
|
||||
|
||||
Reference in New Issue
Block a user