feat: 文件安全检测接入(列表检测列+悬停详情浮窗,不通过禁用分享传输,文件夹确认后仅分享通过内容,管理端复核/重新检测,检测中自动轮询,开关关闭隐藏检测列)
This commit is contained in:
@@ -168,3 +168,34 @@ export const getShareFileList = ({ shareId, ...params }: any) => {
|
|||||||
export const getDiffRiskRule = (fileCode: string) => {
|
export const getDiffRiskRule = (fileCode: string) => {
|
||||||
return http.get(`/file_risk/dify_risk_rule`, { params: { fileCode } });
|
return http.get(`/file_risk/dify_risk_rule`, { params: { fileCode } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// MeshSafe 安全检测状态:1检测中/2通过/3不通过/4检测失败
|
||||||
|
export const SecurityScanStatus = {
|
||||||
|
SCANNING: "1",
|
||||||
|
PASSED: "2",
|
||||||
|
REJECTED: "3",
|
||||||
|
FAILED: "4",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SecurityScanStatus =
|
||||||
|
(typeof SecurityScanStatus)[keyof typeof SecurityScanStatus];
|
||||||
|
|
||||||
|
// 查询文件最新安全检测结果
|
||||||
|
export const getFileSecurityDetail = (fileCode: string) => {
|
||||||
|
return http.get("/file_security/detail", { params: { fileCode } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 人工复核修改检测结果(pass通过/reject不通过)
|
||||||
|
export const reviewFileSecurity = (data: any) => {
|
||||||
|
return http.post("/file_security/review", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重新提交安全检测
|
||||||
|
export const rescanFileSecurity = (fileCode: string) => {
|
||||||
|
return http.post("/file_security/rescan", null, { params: { fileCode } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分享/传输预检:统计文件及子文件的安全检测拦截情况
|
||||||
|
export const shareSecurityCheck = (userFileId: number) => {
|
||||||
|
return http.get("/file_security/share_check", { params: { userFileId } });
|
||||||
|
};
|
||||||
|
|||||||
138
src/components/security-status-badge.tsx
Normal file
138
src/components/security-status-badge.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { useMemo, useRef, useState } from "react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { FileType, SecurityScanStatus } from "@/apis/file";
|
||||||
|
|
||||||
|
export const securityStatusOptions: Record<string, { text: string; color: string }> = {
|
||||||
|
[SecurityScanStatus.SCANNING]: {
|
||||||
|
text: "检测中",
|
||||||
|
color: "bg-amber-500/10 text-amber-500",
|
||||||
|
},
|
||||||
|
[SecurityScanStatus.PASSED]: {
|
||||||
|
text: "检测通过",
|
||||||
|
color: "bg-emerald-500/10 text-emerald-500",
|
||||||
|
},
|
||||||
|
[SecurityScanStatus.REJECTED]: {
|
||||||
|
text: "检测不通过",
|
||||||
|
color: "bg-red-500/10 text-red-500",
|
||||||
|
},
|
||||||
|
[SecurityScanStatus.FAILED]: {
|
||||||
|
text: "检测失败",
|
||||||
|
color: "bg-gray-500/10 text-gray-500",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检测动作说明
|
||||||
|
const securityActionOptions: Record<string, string> = {
|
||||||
|
pass: "机器判定:通过",
|
||||||
|
manual_review: "人工复核改判",
|
||||||
|
reject: "机器判定:不通过",
|
||||||
|
};
|
||||||
|
|
||||||
|
type SecurityStatusBadgeProps = {
|
||||||
|
status?: number | string | null;
|
||||||
|
reason?: string | null;
|
||||||
|
action?: string | null;
|
||||||
|
reviewComment?: string | null;
|
||||||
|
time?: string | null;
|
||||||
|
fileType?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 悬停浮窗展示安全检测全信息(基于Popover受控模式,鼠标可移入浮窗不消失)
|
||||||
|
export function SecurityStatusBadge({
|
||||||
|
status,
|
||||||
|
reason,
|
||||||
|
action,
|
||||||
|
reviewComment,
|
||||||
|
time,
|
||||||
|
fileType,
|
||||||
|
}: SecurityStatusBadgeProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const closeTimer = useRef<any>(null);
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
window.clearTimeout(closeTimer.current);
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
window.clearTimeout(closeTimer.current);
|
||||||
|
closeTimer.current = window.setTimeout(() => setOpen(false), 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
const option = useMemo(() => securityStatusOptions[String(status) as keyof typeof securityStatusOptions], [status]);
|
||||||
|
|
||||||
|
// 文件目录无检测概念,直接显示 -(置于hooks之后,保证hooks调用顺序稳定)
|
||||||
|
if (fileType === FileType.DIRECTORY) {
|
||||||
|
return <div>-</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!option) {
|
||||||
|
return <div className="text-black/40">未检测</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 徽章下方的原因摘要:人工复核优先展示复核说明
|
||||||
|
const summary = reviewComment || reason;
|
||||||
|
const hasDetail = !!(reason || action || reviewComment || time);
|
||||||
|
|
||||||
|
const badgeBlock = (
|
||||||
|
<div className="space-y-1 cursor-default overflow-hidden">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Badge className={clsx(option.color)}>{option.text}</Badge>
|
||||||
|
</div>
|
||||||
|
{summary && (
|
||||||
|
<div className="text-xs text-black/50 truncate" title={undefined}>
|
||||||
|
{summary}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasDetail) {
|
||||||
|
return badgeBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<div onMouseEnter={show} onMouseLeave={hide}>
|
||||||
|
{badgeBlock}
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
side="left"
|
||||||
|
align="center"
|
||||||
|
className="w-80 p-0"
|
||||||
|
onOpenAutoFocus={(e: any) => e.preventDefault()}
|
||||||
|
onMouseEnter={show}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Badge className={clsx(option.color)}>{option.text}</Badge>
|
||||||
|
{action && <span className="text-xs text-black/45">{securityActionOptions[action] || action}</span>}
|
||||||
|
</div>
|
||||||
|
{reason && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-black/40 mb-1">结论说明</div>
|
||||||
|
<div className="text-sm leading-relaxed break-all whitespace-pre-wrap">{reason}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{reviewComment && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-black/40 mb-1">人工复核说明</div>
|
||||||
|
<div className="text-sm leading-relaxed break-all">{reviewComment}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{time && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="text-xs text-black/40">检测时间:{time}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getFilePage } from "@/apis/file";
|
import { getFilePage, SecurityScanStatus } from "@/apis/file";
|
||||||
import { useReactTable } from "@tanstack/react-table";
|
import { useReactTable } from "@tanstack/react-table";
|
||||||
import { getCoreRowModel } from "@tanstack/react-table";
|
import { getCoreRowModel } from "@tanstack/react-table";
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export const useFetchFiles = ({
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 查询数据
|
// 查询数据(存在安全检测中的文件时每2秒轮询,检测结论回写后自动刷新状态)
|
||||||
const query = useQuery<any>({
|
const query = useQuery<any>({
|
||||||
queryKey: [...queryKey, pagination, parentId, params],
|
queryKey: [...queryKey, pagination, parentId, params],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@@ -35,6 +35,14 @@ export const useFetchFiles = ({
|
|||||||
pageNo: pagination.pageIndex + 1,
|
pageNo: pagination.pageIndex + 1,
|
||||||
...params,
|
...params,
|
||||||
}).then((res) => res.data),
|
}).then((res) => res.data),
|
||||||
|
refetchInterval: (q: any) => {
|
||||||
|
const list: any[] = q?.state?.data?.entityList || [];
|
||||||
|
return list.some(
|
||||||
|
(e) => e.securityStatus === SecurityScanStatus.SCANNING,
|
||||||
|
)
|
||||||
|
? 2000
|
||||||
|
: false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换上级ID
|
// 切换上级ID
|
||||||
|
|||||||
12
src/hooks/use-security-enabled.ts
Normal file
12
src/hooks/use-security-enabled.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { http } from "@/lib/http";
|
||||||
|
|
||||||
|
// MeshSafe安全检测开关(meshsafe.enabled),关闭时列表隐藏检测列
|
||||||
|
export const useSecurityEnabled = () => {
|
||||||
|
const { data } = useQuery<boolean>({
|
||||||
|
queryKey: ["securityEnabled"],
|
||||||
|
queryFn: () => http.get("/file_security/enabled").then((res: any) => res.data),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
return { securityEnabled: !!data };
|
||||||
|
};
|
||||||
39
src/routes/(user)/my/-components/SecuritySkipWarning.tsx
Normal file
39
src/routes/(user)/my/-components/SecuritySkipWarning.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { ShieldAlert } from "lucide-react";
|
||||||
|
|
||||||
|
type SecuritySkipWarningProps = {
|
||||||
|
check?: any;
|
||||||
|
actionName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件夹内存在检测未通过/检测中文件的提示块,确认后仅分享/传输通过的内容
|
||||||
|
export function SecuritySkipWarning({ check, actionName }: SecuritySkipWarningProps) {
|
||||||
|
const blocked: string[] = check?.blockedFileNames || [];
|
||||||
|
const scanning: string[] = check?.scanningFileNames || [];
|
||||||
|
if (!blocked.length && !scanning.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-red-200 bg-red-500/5 p-2.5 space-y-1.5">
|
||||||
|
<div className="flex items-start gap-1.5 text-xs text-red-500 leading-relaxed">
|
||||||
|
<ShieldAlert size={14} className="flex-none mt-0.5" />
|
||||||
|
<span>
|
||||||
|
该目录下有 {blocked.length} 个文件安全检测未通过
|
||||||
|
{scanning.length > 0 && `、${scanning.length} 个正在检测中`}
|
||||||
|
,确认后仅{actionName}检测通过的文件
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-24 overflow-auto space-y-0.5 pl-5">
|
||||||
|
{blocked.map((name: string) => (
|
||||||
|
<div key={name} className="text-xs text-red-400 truncate">
|
||||||
|
• {name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{scanning.map((name: string) => (
|
||||||
|
<div key={name} className="text-xs text-amber-500 truncate">
|
||||||
|
• {name}(检测中)
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,10 +19,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useShareFile } from "../-hooks/useShareFile";
|
import { useShareFile } from "../-hooks/useShareFile";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Folder, File } from "lucide-react";
|
import { Folder, File } from "lucide-react";
|
||||||
import { FileType } from "@/apis/file";
|
import { FileType, shareSecurityCheck } from "@/apis/file";
|
||||||
|
import { SecuritySkipWarning } from "./SecuritySkipWarning";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -50,6 +52,17 @@ export function ShareDialog({ open, setOpen, data }: ShareDialogProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isFolder = data?.fileType === FileType.DIRECTORY;
|
||||||
|
|
||||||
|
// 文件夹分享预检:存在检测未通过/检测中的文件时提示并跳过
|
||||||
|
const { data: check } = useQuery<any>({
|
||||||
|
queryKey: ["shareSecurityCheck", data?.id, open],
|
||||||
|
queryFn: () =>
|
||||||
|
shareSecurityCheck(data.id).then((res: any) => res.data),
|
||||||
|
enabled: open && isFolder,
|
||||||
|
});
|
||||||
|
const skipCount = check?.excludeFileIds?.length || 0;
|
||||||
|
|
||||||
const { share, shareIsPending } = useShareFile({
|
const { share, shareIsPending } = useShareFile({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -60,6 +73,7 @@ export function ShareDialog({ open, setOpen, data }: ShareDialogProps) {
|
|||||||
share({
|
share({
|
||||||
userFileId: data.id,
|
userFileId: data.id,
|
||||||
validDays: values.validDays,
|
validDays: values.validDays,
|
||||||
|
onlyPassed: skipCount > 0,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,6 +97,7 @@ export function ShareDialog({ open, setOpen, data }: ShareDialogProps) {
|
|||||||
)}
|
)}
|
||||||
<span className="break-all">{data?.fileName}</span>
|
<span className="break-all">{data?.fileName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{isFolder && skipCount > 0 && <SecuritySkipWarning check={check} actionName="分享" />}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Folder, File } from "lucide-react";
|
import { Folder, File } from "lucide-react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useTransferFile } from "../-hooks/useTransferFile";
|
import { useTransferFile } from "../-hooks/useTransferFile";
|
||||||
import { FileType } from "@/apis/file";
|
import { FileType, shareSecurityCheck } from "@/apis/file";
|
||||||
|
import { SecuritySkipWarning } from "./SecuritySkipWarning";
|
||||||
|
|
||||||
type TransferConfirmDialogProps = {
|
type TransferConfirmDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -25,6 +27,17 @@ export function TransferConfirmDialog({
|
|||||||
setOpen,
|
setOpen,
|
||||||
data,
|
data,
|
||||||
}: TransferConfirmDialogProps) {
|
}: TransferConfirmDialogProps) {
|
||||||
|
const isFolder = data?.fileType === FileType.DIRECTORY;
|
||||||
|
|
||||||
|
// 文件夹传输预检:存在检测未通过/检测中的文件时提示并跳过
|
||||||
|
const { data: check } = useQuery<any>({
|
||||||
|
queryKey: ["transferSecurityCheck", data?.id, open],
|
||||||
|
queryFn: () =>
|
||||||
|
shareSecurityCheck(data.id).then((res: any) => res.data),
|
||||||
|
enabled: open && isFolder,
|
||||||
|
});
|
||||||
|
const skipCount = check?.excludeFileIds?.length || 0;
|
||||||
|
|
||||||
const { transfer, transferIsPending } = useTransferFile({
|
const { transfer, transferIsPending } = useTransferFile({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -32,7 +45,10 @@ export function TransferConfirmDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
transfer({ userFileId: data?.id });
|
transfer({
|
||||||
|
userFileId: data?.id,
|
||||||
|
onlyPassed: skipCount > 0,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,6 +67,9 @@ export function TransferConfirmDialog({
|
|||||||
)}
|
)}
|
||||||
<span className="break-all">{data?.fileName}</span>
|
<span className="break-all">{data?.fileName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{isFolder && skipCount > 0 && (
|
||||||
|
<SecuritySkipWarning check={check} actionName="传输" />
|
||||||
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||||
import {
|
import { File, FileUp, Folder, FolderPlus, FolderInput } from "lucide-react";
|
||||||
File,
|
|
||||||
FileUp,
|
|
||||||
Folder,
|
|
||||||
FolderPlus,
|
|
||||||
FolderInput,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { EditFileDialog } from "./-components/EditFileDialog";
|
import { EditFileDialog } from "./-components/EditFileDialog";
|
||||||
import { CreateFileDialog } from "./-components/CreateFileDialog";
|
import { CreateFileDialog } from "./-components/CreateFileDialog";
|
||||||
@@ -14,7 +8,7 @@ import { DirectoryBreadcrumb } from "@/components/directory-breadcrumb";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { ShareDialog } from "./-components/ShareDialog";
|
import { ShareDialog } from "./-components/ShareDialog";
|
||||||
import { TransferConfirmDialog } from "./-components/TransferConfirmDialog";
|
import { TransferConfirmDialog } from "./-components/TransferConfirmDialog";
|
||||||
import { downloadFile, FileStatus, FileType, RootType } from "@/apis/file";
|
import { downloadFile, FileStatus, FileType, RootType, SecurityScanStatus } from "@/apis/file";
|
||||||
import { formatFileSize } from "@/lib/utils";
|
import { formatFileSize } from "@/lib/utils";
|
||||||
import { MoveFileDialog } from "@/components/move-file-dialog";
|
import { MoveFileDialog } from "@/components/move-file-dialog";
|
||||||
import { useFetchFiles } from "@/hooks/use-fetch-files";
|
import { useFetchFiles } from "@/hooks/use-fetch-files";
|
||||||
@@ -24,9 +18,19 @@ import { FilePreviewDialog } from "@/components/file-preview-dialog";
|
|||||||
import { useFileCategoryName } from "@/hooks/use-file-category-name";
|
import { useFileCategoryName } from "@/hooks/use-file-category-name";
|
||||||
import { FileCategoryFilter } from "@/components/file-category-filter";
|
import { FileCategoryFilter } from "@/components/file-category-filter";
|
||||||
import { useUploadConfig } from "@/hooks/use-upload-config";
|
import { useUploadConfig } from "@/hooks/use-upload-config";
|
||||||
|
import { useSecurityEnabled } from "@/hooks/use-security-enabled";
|
||||||
|
import { SecurityStatusBadge } from "@/components/security-status-badge";
|
||||||
|
|
||||||
const isInside = import.meta.env.VITE_IS_INSIDE === "true";
|
const isInside = import.meta.env.VITE_IS_INSIDE === "true";
|
||||||
|
|
||||||
|
// 检测不通过/检测中的文件禁止分享传输(下载预览不受限),与后端校验一致
|
||||||
|
export function isSecurityBlocked(record: any) {
|
||||||
|
return (
|
||||||
|
record?.fileType === FileType.FILE &&
|
||||||
|
(record?.securityStatus === SecurityScanStatus.REJECTED || record?.securityStatus === SecurityScanStatus.SCANNING)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute("/(user)/my/")({
|
export const Route = createFileRoute("/(user)/my/")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
@@ -39,6 +43,7 @@ export function useColumns({
|
|||||||
handleTransfer,
|
handleTransfer,
|
||||||
handleMove,
|
handleMove,
|
||||||
handleFilePreview,
|
handleFilePreview,
|
||||||
|
securityEnabled,
|
||||||
}: any) {
|
}: any) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -62,10 +67,7 @@ export function useColumns({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Folder size={18} className="text-amber-500 group-hover:hidden" />
|
<Folder size={18} className="text-amber-500 group-hover:hidden" />
|
||||||
<FolderInput
|
<FolderInput size={18} className="text-amber-500 group-hover:block hidden" />
|
||||||
size={18}
|
|
||||||
className="text-amber-500 group-hover:block hidden"
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span>{row.getValue("fileName")}</span>
|
<span>{row.getValue("fileName")}</span>
|
||||||
@@ -80,28 +82,45 @@ export function useColumns({
|
|||||||
{
|
{
|
||||||
accessorKey: "fileSize",
|
accessorKey: "fileSize",
|
||||||
header: "文件大小",
|
header: "文件大小",
|
||||||
cell: ({ row }: any) => (
|
cell: ({ row }: any) => <div>{formatFileSize(row.getValue("fileSize"))}</div>,
|
||||||
<div>{formatFileSize(row.getValue("fileSize"))}</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "fileCategory",
|
accessorKey: "fileCategory",
|
||||||
header: "文件种类",
|
header: "文件种类",
|
||||||
cell: ({ row }: any) => {
|
cell: ({ row }: any) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>{row.original.fileType === FileType.FILE ? useFileCategoryName(row.getValue("fileCategory")) : "-"}</div>
|
||||||
{row.original.fileType === FileType.FILE
|
|
||||||
? useFileCategoryName(row.getValue("fileCategory"))
|
|
||||||
: "-"}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(securityEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
accessorKey: "securityStatus",
|
||||||
|
header: "安全检测",
|
||||||
|
cell: ({ row }: any) => (
|
||||||
|
<SecurityStatusBadge
|
||||||
|
status={row.original.securityStatus}
|
||||||
|
reason={row.original.securityReason}
|
||||||
|
action={row.original.securityAction}
|
||||||
|
reviewComment={row.original.securityReviewComment}
|
||||||
|
time={row.original.securityTime}
|
||||||
|
fileType={row.original.fileType}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
header: "操作",
|
header: "操作",
|
||||||
cell: ({ row }: any) => {
|
cell: ({ row }: any) => {
|
||||||
|
const securityBlocked = isSecurityBlocked(row.original);
|
||||||
|
const securityTip =
|
||||||
|
row.original?.securityStatus === SecurityScanStatus.REJECTED
|
||||||
|
? "安全检测未通过,禁止分享与传输"
|
||||||
|
: "文件正在安全检测中,请稍后再试";
|
||||||
return (
|
return (
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -113,32 +132,25 @@ export function useColumns({
|
|||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<span title={securityBlocked ? securityTip : undefined}>
|
||||||
size="sm"
|
<Button size="sm" variant="outline" disabled={securityBlocked} onClick={() => handleShare(row.original)}>
|
||||||
variant="outline"
|
分享
|
||||||
onClick={() => handleShare(row.original)}
|
</Button>
|
||||||
>
|
</span>
|
||||||
分享
|
<span title={securityBlocked ? securityTip : undefined}>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
size="sm"
|
||||||
size="sm"
|
variant="outline"
|
||||||
variant="outline"
|
disabled={securityBlocked}
|
||||||
onClick={() => handleTransfer(row.original)}
|
onClick={() => handleTransfer(row.original)}
|
||||||
>
|
>
|
||||||
{isInside ? "传外网" : "传内网"}
|
{isInside ? "传外网" : "传内网"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</span>
|
||||||
size="sm"
|
<Button size="sm" variant="destructive" onClick={() => handleRemove(row.original)}>
|
||||||
variant="destructive"
|
|
||||||
onClick={() => handleRemove(row.original)}
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" variant="outline" onClick={() => handleMove(row.original)}>
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleMove(row.original)}
|
|
||||||
>
|
|
||||||
移动
|
移动
|
||||||
</Button>
|
</Button>
|
||||||
{row.original?.fileType === FileType.FILE && (
|
{row.original?.fileType === FileType.FILE && (
|
||||||
@@ -161,6 +173,8 @@ export function useColumns({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// MeshSafe安全检测开关:关闭时隐藏检测列
|
||||||
|
const { securityEnabled } = useSecurityEnabled();
|
||||||
// 机构上传配置:关闭上传时隐藏上传入口
|
// 机构上传配置:关闭上传时隐藏上传入口
|
||||||
const { data: uploadConfig } = useUploadConfig();
|
const { data: uploadConfig } = useUploadConfig();
|
||||||
const uploadDisabled = !!uploadConfig && uploadConfig.uploadEnabled === false;
|
const uploadDisabled = !!uploadConfig && uploadConfig.uploadEnabled === false;
|
||||||
@@ -196,8 +210,7 @@ function RouteComponent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 传输
|
// 传输
|
||||||
const [openTransferConfirmDialog, setOpenTransferConfirmDialog] =
|
const [openTransferConfirmDialog, setOpenTransferConfirmDialog] = useState(false);
|
||||||
useState(false);
|
|
||||||
const handleTransfer = (record?: any) => {
|
const handleTransfer = (record?: any) => {
|
||||||
setCurrentEntity(record);
|
setCurrentEntity(record);
|
||||||
setOpenTransferConfirmDialog(true);
|
setOpenTransferConfirmDialog(true);
|
||||||
@@ -226,16 +239,10 @@ function RouteComponent() {
|
|||||||
};
|
};
|
||||||
}, [fileCategory]);
|
}, [fileCategory]);
|
||||||
|
|
||||||
const {
|
const { openDirectorys, changeOpenDirectorys, parentId, table, query, reset } = useFetchFiles({
|
||||||
openDirectorys,
|
|
||||||
changeOpenDirectorys,
|
|
||||||
parentId,
|
|
||||||
table,
|
|
||||||
query,
|
|
||||||
reset,
|
|
||||||
} = useFetchFiles({
|
|
||||||
useColumns: (handleChangeParentId: any) =>
|
useColumns: (handleChangeParentId: any) =>
|
||||||
useColumns({
|
useColumns({
|
||||||
|
securityEnabled,
|
||||||
handleChangeParentId,
|
handleChangeParentId,
|
||||||
handleEditFile,
|
handleEditFile,
|
||||||
handleRemove,
|
handleRemove,
|
||||||
@@ -270,11 +277,7 @@ function RouteComponent() {
|
|||||||
data={currentEntity}
|
data={currentEntity}
|
||||||
refetch={query.refetch}
|
refetch={query.refetch}
|
||||||
/>
|
/>
|
||||||
<ShareDialog
|
<ShareDialog open={openShareConfirmDialog} setOpen={setOpenShareConfirmDialog} data={currentEntity} />
|
||||||
open={openShareConfirmDialog}
|
|
||||||
setOpen={setOpenShareConfirmDialog}
|
|
||||||
data={currentEntity}
|
|
||||||
/>
|
|
||||||
<TransferConfirmDialog
|
<TransferConfirmDialog
|
||||||
open={openTransferConfirmDialog}
|
open={openTransferConfirmDialog}
|
||||||
setOpen={setOpenTransferConfirmDialog}
|
setOpen={setOpenTransferConfirmDialog}
|
||||||
@@ -287,24 +290,14 @@ function RouteComponent() {
|
|||||||
params={params}
|
params={params}
|
||||||
refetch={query.refetch}
|
refetch={query.refetch}
|
||||||
/>
|
/>
|
||||||
<FilePreviewDialog
|
<FilePreviewDialog open={openFilePreviewDialog} setOpen={setOpenFilePreviewDialog} data={currentEntity} />
|
||||||
open={openFilePreviewDialog}
|
|
||||||
setOpen={setOpenFilePreviewDialog}
|
|
||||||
data={currentEntity}
|
|
||||||
/>
|
|
||||||
<div className="p-4 box-border min-h-full space-y-4">
|
<div className="p-4 box-border min-h-full space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-lg">我的文件</div>
|
<div className="text-lg">我的文件</div>
|
||||||
<div className="text-xs text-black/50">
|
<div className="text-xs text-black/50">您可以进行云端文件的管理、分享、内网传输</div>
|
||||||
您可以进行云端文件的管理、分享、内网传输
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FileCategoryFilter
|
<FileCategoryFilter fileCategory={fileCategory} setFileCategory={setFileCategory} reset={reset} />
|
||||||
fileCategory={fileCategory}
|
|
||||||
setFileCategory={setFileCategory}
|
|
||||||
reset={reset}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -329,28 +322,17 @@ function RouteComponent() {
|
|||||||
上传文件
|
上传文件
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button variant="default" onClick={() => handleEditFile({ fileType: FileType.DIRECTORY })}>
|
||||||
variant="default"
|
|
||||||
onClick={() =>
|
|
||||||
handleEditFile({ fileType: FileType.DIRECTORY })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FolderPlus />
|
<FolderPlus />
|
||||||
创建目录
|
创建目录
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DirectoryBreadcrumb
|
<DirectoryBreadcrumb directorys={openDirectorys} onChangeDirectorys={changeOpenDirectorys} />
|
||||||
directorys={openDirectorys}
|
|
||||||
onChangeDirectorys={changeOpenDirectorys}
|
|
||||||
/>
|
|
||||||
<div className="relative overflow-hidden">
|
<div className="relative overflow-hidden">
|
||||||
<DataTable table={table} query={query} />
|
<DataTable table={table} query={query} />
|
||||||
<Pagination
|
<Pagination table={table} className="justify-end p-2 sticky bottom-0 left-0 right-0 z-10 bg-white" />
|
||||||
table={table}
|
|
||||||
className="justify-end p-2 sticky bottom-0 left-0 right-0 z-10 bg-white"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { File, Folder } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { FileType } from "@/apis/file";
|
||||||
|
import { useReviewFileSecurity } from "../-hooks/useReviewFileSecurity";
|
||||||
|
|
||||||
|
type SecurityReviewDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
data?: any;
|
||||||
|
refetch?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 人工复核修改安全检测结果(避免误测导致文件不能分享)
|
||||||
|
export function SecurityReviewDialog({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
data,
|
||||||
|
refetch,
|
||||||
|
}: SecurityReviewDialogProps) {
|
||||||
|
const [decision, setDecision] = useState<string>("pass");
|
||||||
|
const [comment, setComment] = useState("");
|
||||||
|
const { reviewFileSecurityMutate, reviewIsPending } =
|
||||||
|
useReviewFileSecurity();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setDecision("pass");
|
||||||
|
setComment("");
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (!comment.trim()) {
|
||||||
|
toast.error("请填写复核说明");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reviewFileSecurityMutate({
|
||||||
|
fileCode: data?.fileCode,
|
||||||
|
decision,
|
||||||
|
comment: comment.trim(),
|
||||||
|
}).then(() => {
|
||||||
|
toast.success("复核结果已保存");
|
||||||
|
refetch?.();
|
||||||
|
setOpen(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="w-[460px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>安全检测复核</DialogTitle>
|
||||||
|
<div className="mt-1 flex items-center gap-1 text-sm p-2 rounded-md bg-gray-50">
|
||||||
|
{data?.fileType === FileType.FILE ? (
|
||||||
|
<File size={16} className="text-primary flex-none" />
|
||||||
|
) : (
|
||||||
|
<Folder size={16} className="text-amber-500 flex-none" />
|
||||||
|
)}
|
||||||
|
<span className="break-all">{data?.fileName}</span>
|
||||||
|
</div>
|
||||||
|
{data?.securityReason && (
|
||||||
|
<div className="text-xs text-black/50 break-all">
|
||||||
|
最近检测结论:{data.securityReason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm">复核结论</div>
|
||||||
|
<Select value={decision} onValueChange={setDecision}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="请选择复核结论" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="pass">通过(允许分享/传输)</SelectItem>
|
||||||
|
<SelectItem value="reject">不通过(禁止分享/传输)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm">复核说明(必填)</div>
|
||||||
|
<Textarea
|
||||||
|
placeholder="请填写复核说明,例如:经人工查验为误报"
|
||||||
|
value={comment}
|
||||||
|
onChange={(e: any) => setComment(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">取消</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button type="submit" disabled={reviewIsPending} onClick={onSubmit}>
|
||||||
|
{reviewIsPending && <Spinner />}确定
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ import { useState } from "react";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useReactTable } from "@tanstack/react-table";
|
import { useReactTable } from "@tanstack/react-table";
|
||||||
import { getCoreRowModel } from "@tanstack/react-table";
|
import { getCoreRowModel } from "@tanstack/react-table";
|
||||||
import { getFileRecordPageByPhone } from "@/apis/file";
|
import { getFileRecordPageByPhone, SecurityScanStatus } from "@/apis/file";
|
||||||
|
|
||||||
export const useFetchList = (useColumns: any) => {
|
export const useFetchList = (useColumns: any, ...columnArgs: any[]) => {
|
||||||
// 分页
|
// 分页
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
@@ -24,6 +24,15 @@ export const useFetchList = (useColumns: any) => {
|
|||||||
pageNo: pagination.pageIndex + 1,
|
pageNo: pagination.pageIndex + 1,
|
||||||
}).then((res) => res.data),
|
}).then((res) => res.data),
|
||||||
enabled: !!params.phone,
|
enabled: !!params.phone,
|
||||||
|
// 存在安全检测中的记录时每2秒轮询,检测结论回写后自动刷新状态
|
||||||
|
refetchInterval: (q: any) => {
|
||||||
|
const list: any[] = q?.state?.data?.entityList || [];
|
||||||
|
return list.some(
|
||||||
|
(e) => e.securityStatus === SecurityScanStatus.SCANNING,
|
||||||
|
)
|
||||||
|
? 2000
|
||||||
|
: false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 查询:应用条件并回到第一页(过滤掉空值条件)
|
// 查询:应用条件并回到第一页(过滤掉空值条件)
|
||||||
@@ -39,7 +48,7 @@ export const useFetchList = (useColumns: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 表格
|
// 表格
|
||||||
const columns = useColumns();
|
const columns = useColumns(...columnArgs);
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: query?.data?.entityList || [],
|
data: query?.data?.entityList || [],
|
||||||
columns,
|
columns,
|
||||||
|
|||||||
18
src/routes/admin/file-record/-hooks/useReviewFileSecurity.ts
Normal file
18
src/routes/admin/file-record/-hooks/useReviewFileSecurity.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { reviewFileSecurity, rescanFileSecurity } from "@/apis/file";
|
||||||
|
|
||||||
|
// 人工复核修改安全检测结果
|
||||||
|
export const useReviewFileSecurity = () => {
|
||||||
|
const { mutateAsync: reviewFileSecurityMutate, isPending: reviewIsPending } =
|
||||||
|
useMutation({ mutationFn: reviewFileSecurity });
|
||||||
|
|
||||||
|
const { mutateAsync: rescanMutate, isPending: rescanIsPending } =
|
||||||
|
useMutation({ mutationFn: (fileCode: string) => rescanFileSecurity(fileCode) });
|
||||||
|
|
||||||
|
return {
|
||||||
|
reviewFileSecurityMutate,
|
||||||
|
reviewIsPending,
|
||||||
|
rescanMutate,
|
||||||
|
rescanIsPending,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -14,9 +14,14 @@ import {
|
|||||||
import { DataTable } from "@/components/data-table";
|
import { DataTable } from "@/components/data-table";
|
||||||
import { Pagination } from "@/components/pagination";
|
import { Pagination } from "@/components/pagination";
|
||||||
import { FileStatusBadge } from "@/components/file-status-badge";
|
import { FileStatusBadge } from "@/components/file-status-badge";
|
||||||
|
import { SecurityStatusBadge } from "@/components/security-status-badge";
|
||||||
import { formatFileSize } from "@/lib/utils";
|
import { formatFileSize } from "@/lib/utils";
|
||||||
|
import { SecurityScanStatus, FileType, rescanFileSecurity } from "@/apis/file";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { SecurityReviewDialog } from "./-components/SecurityReviewDialog";
|
||||||
import { useFetchList } from "./-hooks/useFetchList";
|
import { useFetchList } from "./-hooks/useFetchList";
|
||||||
import { DateRangePicker } from "@/components/date-range-picker";
|
import { DateRangePicker } from "@/components/date-range-picker";
|
||||||
|
import { useSecurityEnabled } from "@/hooks/use-security-enabled";
|
||||||
|
|
||||||
export const Route = createFileRoute("/admin/file-record/")({
|
export const Route = createFileRoute("/admin/file-record/")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
@@ -36,7 +41,10 @@ const riskLevelOptions: Record<string, { text: string; color: string }> = {
|
|||||||
"3": { text: "轻度", color: "bg-emerald-500/10 text-emerald-500" },
|
"3": { text: "轻度", color: "bg-emerald-500/10 text-emerald-500" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useColumns() {
|
export function useColumns(
|
||||||
|
handleSecurityReview: (record?: any) => void,
|
||||||
|
securityEnabled: boolean,
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: "recordType",
|
accessorKey: "recordType",
|
||||||
@@ -117,11 +125,75 @@ export function useColumns() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(securityEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
accessorKey: "securityStatus",
|
||||||
|
header: "安全检测",
|
||||||
|
cell: ({ row }: any) => (
|
||||||
|
<SecurityStatusBadge
|
||||||
|
status={row.original.securityStatus}
|
||||||
|
reason={row.original.securityReason}
|
||||||
|
action={row.original.securityAction}
|
||||||
|
reviewComment={row.original.securityReviewComment}
|
||||||
|
time={row.original.securityTime}
|
||||||
|
fileType={row.original.fileType}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
accessorKey: "createTime",
|
accessorKey: "createTime",
|
||||||
header: "记录时间",
|
header: "记录时间",
|
||||||
cell: ({ row }: any) => <div>{row.getValue("createTime")}</div>,
|
cell: ({ row }: any) => <div>{row.getValue("createTime")}</div>,
|
||||||
},
|
},
|
||||||
|
...(securityEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
enableHiding: false,
|
||||||
|
header: "操作",
|
||||||
|
cell: ({ row }: any) => {
|
||||||
|
const record = row.original;
|
||||||
|
if (record.fileType !== FileType.FILE || !record.fileCode) {
|
||||||
|
return <div>-</div>;
|
||||||
|
}
|
||||||
|
const scanning =
|
||||||
|
record.securityStatus === SecurityScanStatus.SCANNING;
|
||||||
|
return (
|
||||||
|
<div className="space-x-2">
|
||||||
|
{record.securityStatus && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={scanning}
|
||||||
|
title={scanning ? "文件正在安全检测中" : undefined}
|
||||||
|
onClick={() => handleSecurityReview(record)}
|
||||||
|
>
|
||||||
|
复核
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={scanning}
|
||||||
|
title={scanning ? "文件正在安全检测中" : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
toast.promise(rescanFileSecurity(record.fileCode), {
|
||||||
|
loading: "正在重新提交检测...",
|
||||||
|
success: "已重新提交安全检测",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重新检测
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +208,20 @@ const initialFilters = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { table, query, search, params } = useFetchList(useColumns);
|
// 复核弹窗
|
||||||
|
const [currentEntity, setCurrentEntity] = useState(null);
|
||||||
|
const [openReviewDialog, setOpenReviewDialog] = useState(false);
|
||||||
|
const handleSecurityReview = (record?: any) => {
|
||||||
|
setCurrentEntity(record);
|
||||||
|
setOpenReviewDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { securityEnabled } = useSecurityEnabled();
|
||||||
|
const { table, query, search, params } = useFetchList(
|
||||||
|
useColumns,
|
||||||
|
handleSecurityReview,
|
||||||
|
securityEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
// 筛选条件(点击查询后生效)
|
// 筛选条件(点击查询后生效)
|
||||||
const [filters, setFilters] = useState<any>(initialFilters);
|
const [filters, setFilters] = useState<any>(initialFilters);
|
||||||
@@ -163,6 +248,12 @@ function RouteComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 box-border min-h-full space-y-4">
|
<div className="p-4 box-border min-h-full space-y-4">
|
||||||
|
<SecurityReviewDialog
|
||||||
|
open={openReviewDialog}
|
||||||
|
setOpen={setOpenReviewDialog}
|
||||||
|
data={currentEntity}
|
||||||
|
refetch={() => query.refetch()}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-lg">记录查询</div>
|
<div className="text-lg">记录查询</div>
|
||||||
<div className="text-xs text-black/50">
|
<div className="text-xs text-black/50">
|
||||||
|
|||||||
Reference in New Issue
Block a user