485 lines
16 KiB
TypeScript
Executable File
485 lines
16 KiB
TypeScript
Executable File
import React, { useState, useMemo } from "react";
|
|
import {
|
|
Tabs,
|
|
Button,
|
|
message,
|
|
Image,
|
|
Row,
|
|
Col,
|
|
Modal,
|
|
Input,
|
|
Alert,
|
|
Pagination,
|
|
Select,
|
|
Space,
|
|
Tag,
|
|
} from "antd";
|
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
|
import { SendOutlined, DeleteOutlined, LoginOutlined } from "@ant-design/icons";
|
|
import { api } from "@nice/client";
|
|
import dayjs from "dayjs";
|
|
import { env } from "@web/src/env";
|
|
import { getFileIcon } from "@web/src/components/models/post/detail/utils";
|
|
import { formatFileSize, getCompressedImageUrl } from "@nice/utils";
|
|
import { ResourceDto, RoleName, ResourceType } from "@nice/common";
|
|
import { useAuth } from "@web/src/providers/auth-provider";
|
|
const { TabPane } = Tabs;
|
|
|
|
export function VideoContent() {
|
|
const { isAuthenticated, user, hasSomePermissions } = useAuth();
|
|
const [fileIds, setFileIds] = useState<string[]>([]);
|
|
const [uploaderKey, setUploaderKey] = useState<number>(0);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [fileType, setFileType] = useState<string>("all");
|
|
|
|
// 分页状态
|
|
const [imagePage, setImagePage] = useState(1);
|
|
const [filePage, setFilePage] = useState(1);
|
|
const pageSize = 12; // 每页显示的数量
|
|
|
|
// 检查是否为域管理员
|
|
const isDomainAdmin = useMemo(() => {
|
|
return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF");
|
|
}, [hasSomePermissions]);
|
|
|
|
// 获取资源列表
|
|
const {
|
|
data: resources,
|
|
refetch,
|
|
isLoading,
|
|
}: {
|
|
data: ResourceDto[];
|
|
refetch: () => void;
|
|
isLoading: boolean;
|
|
} = api.resource.findMany.useQuery({
|
|
where: {
|
|
deletedAt: null,
|
|
postId: null,
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
});
|
|
|
|
// 定义常见文件类型和它们的扩展名
|
|
const fileTypes = {
|
|
all: { label: "全部", extensions: [] },
|
|
document: { label: "文档", extensions: ["doc", "docx", "pdf", "txt"] },
|
|
spreadsheet: { label: "表格", extensions: ["xls", "xlsx", "csv"] },
|
|
presentation: { label: "ppt", extensions: ["ppt", "pptx"] },
|
|
video: { label: "音视频", extensions: ["mp4", "avi", "mov", "webm","mp3", "wav", "ogg"] },
|
|
archive: { label: "压缩包", extensions: ["zip", "rar", "7z"] },
|
|
};
|
|
|
|
// 修改资源处理逻辑,加入文件类型筛选
|
|
const { imageResources, fileResources, imagePagination, filePagination } =
|
|
useMemo(() => {
|
|
if (!resources) {
|
|
return {
|
|
imageResources: [],
|
|
fileResources: [],
|
|
imagePagination: { total: 0, data: [] },
|
|
filePagination: { total: 0, data: [] },
|
|
};
|
|
}
|
|
|
|
const isImage = (url: string) => /\.(png|jpg|jpeg|gif|webp)$/i.test(url);
|
|
|
|
const processedResources = resources
|
|
.map((resource) => {
|
|
if (!resource?.url) return null;
|
|
const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${resource.url}`;
|
|
const isImg = isImage(resource.url);
|
|
|
|
// 提取文件扩展名
|
|
const extension = resource.url.split(".").pop()?.toLowerCase() || "";
|
|
|
|
const displayTitle =
|
|
resource.title || resource.meta?.filename || "未命名文件";
|
|
const searchableFilename = resource.meta?.filename || "";
|
|
|
|
return {
|
|
...resource,
|
|
url: isImg ? getCompressedImageUrl(original) : original,
|
|
originalUrl: original,
|
|
isImage: isImg,
|
|
title: displayTitle,
|
|
searchableFilename: searchableFilename,
|
|
extension: extension,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
|
|
// 根据搜索词和文件类型筛选
|
|
const filteredFileResources = processedResources.filter((res) => {
|
|
// 首先检查是否符合搜索词
|
|
const matchesSearch = res.searchableFilename
|
|
.toLowerCase()
|
|
.includes(searchTerm.toLowerCase());
|
|
|
|
// 然后检查文件类型
|
|
const matchesType =
|
|
fileType === "all" ||
|
|
fileTypes[fileType]?.extensions.includes(res.extension);
|
|
|
|
return !res.isImage && matchesSearch && matchesType;
|
|
});
|
|
|
|
const allImageResources = processedResources.filter((res) => res.isImage);
|
|
|
|
// 分页处理
|
|
const imageStart = (imagePage - 1) * pageSize;
|
|
const fileStart = (filePage - 1) * pageSize;
|
|
|
|
return {
|
|
imageResources: allImageResources.slice(
|
|
imageStart,
|
|
imageStart + pageSize
|
|
),
|
|
fileResources: filteredFileResources.slice(
|
|
fileStart,
|
|
fileStart + pageSize
|
|
),
|
|
imagePagination: {
|
|
total: allImageResources.length,
|
|
data: allImageResources,
|
|
},
|
|
filePagination: {
|
|
total: filteredFileResources.length,
|
|
data: filteredFileResources,
|
|
},
|
|
};
|
|
}, [resources, imagePage, filePage, searchTerm, fileType]); // 添加fileType依赖
|
|
|
|
const createMutation = api.resource.create.useMutation({});
|
|
const handleSubmit = async () => {
|
|
if (!isAuthenticated) {
|
|
message.error("请先登录");
|
|
return;
|
|
}
|
|
|
|
if (!isDomainAdmin) {
|
|
message.error("只有管理员才能上传文件");
|
|
return;
|
|
}
|
|
|
|
if (!fileIds.length) {
|
|
message.error("请先上传文件");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 逐个上传文件,而不是使用 Promise.all
|
|
for (const fileId of fileIds) {
|
|
try {
|
|
await createMutation.mutateAsync({
|
|
data: {
|
|
fileId,
|
|
isPublic: true,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (
|
|
error instanceof Error &&
|
|
error.message.includes("Unique constraint failed")
|
|
) {
|
|
console.warn(`文件 ${fileId} 已存在,跳过`);
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
message.success("上传成功!");
|
|
setFileIds([]);
|
|
setUploaderKey((prev) => prev + 1);
|
|
refetch(); // 刷新列表
|
|
} catch (error) {
|
|
console.error("Error uploading:", error);
|
|
message.error("上传失败,请稍后重试");
|
|
}
|
|
};
|
|
|
|
// 删除资源
|
|
const deleteMutation = api.resource.softDeleteByIds.useMutation();
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!isAuthenticated) {
|
|
message.error("请先登录");
|
|
return;
|
|
}
|
|
|
|
if (!isDomainAdmin) {
|
|
message.error("只有管理员才能删除文件");
|
|
return;
|
|
}
|
|
|
|
console.log("Delete resource id:", id);
|
|
try {
|
|
const confirmed = await new Promise((resolve) => {
|
|
Modal.confirm({
|
|
title: "确认删除",
|
|
content: "确定要删除这个文件吗?此操作不可恢复。",
|
|
okText: "确认",
|
|
cancelText: "取消",
|
|
onOk: () => resolve(true),
|
|
onCancel: () => resolve(false),
|
|
});
|
|
});
|
|
|
|
if (!confirmed) return;
|
|
|
|
await deleteMutation.mutateAsync({
|
|
ids: [id],
|
|
});
|
|
} catch (error) {
|
|
console.error("Delete error:", error);
|
|
message.error("删除失败,请重试");
|
|
}
|
|
|
|
refetch();
|
|
message.success("删除成功");
|
|
};
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<header className="rounded-t-xl bg-gradient-to-r from-primary to-primary-400 text-white p-6 relative mb-4">
|
|
<div className="flex flex-col space-b-1">
|
|
{/* <h1>资源上传</h1> */}
|
|
<p>支持视频、excel、文档、ppt等多种格式文件</p>
|
|
</div>
|
|
</header>
|
|
<div className="flex flex-wrap items-center gap-4 mb-4">
|
|
<Input.Search
|
|
placeholder="搜索文件名"
|
|
allowClear
|
|
onSearch={(value) => setSearchTerm(value)}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
style={{ width: 300 }}
|
|
/>
|
|
|
|
<Select
|
|
value={fileType}
|
|
onChange={(value) => setFileType(value)}
|
|
style={{ width: 120 }}
|
|
options={Object.entries(fileTypes).map(([key, { label }]) => ({
|
|
value: key,
|
|
label: label,
|
|
}))}
|
|
/>
|
|
|
|
{/* {fileType !== "all" && (
|
|
<Tag color="blue" closable onClose={() => setFileType("all")}>
|
|
{fileTypes[fileType]?.label}
|
|
</Tag>
|
|
)} */}
|
|
</div>
|
|
{!isAuthenticated && (
|
|
<Alert
|
|
message="请先登录"
|
|
description="您需要登录后才能查看和管理文件资源"
|
|
type="warning"
|
|
showIcon
|
|
className="mb-4"
|
|
/>
|
|
)}
|
|
|
|
{isAuthenticated && !isDomainAdmin && (
|
|
<Alert
|
|
message="权限不足"
|
|
description="只有管理员才能上传和管理文件资源"
|
|
type="warning"
|
|
showIcon
|
|
className="mb-4"
|
|
/>
|
|
)}
|
|
|
|
{/* 上传区域 */}
|
|
<div className="space-y-4">
|
|
<Tabs defaultActiveKey="1">
|
|
<TabPane>
|
|
<div
|
|
className={`relative rounded-xl border hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100 ${!isDomainAdmin ? " pointer-events-none" : ""}`}
|
|
>
|
|
<TusUploader
|
|
key={uploaderKey}
|
|
value={fileIds}
|
|
onChange={(value) => {
|
|
if (!isDomainAdmin) {
|
|
message.error("只有管理员才能上传文件");
|
|
return;
|
|
}
|
|
setFileIds(value);
|
|
}}
|
|
/>
|
|
</div>
|
|
</TabPane>
|
|
</Tabs>
|
|
{isDomainAdmin && fileIds.length > 0 && (
|
|
<div className="flex items-center justify-end gap-2">
|
|
<Button
|
|
type="primary"
|
|
onClick={handleSubmit}
|
|
className="flex items-center space-x-2 bg-primary"
|
|
icon={<SendOutlined />}
|
|
>
|
|
上传
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* 文件展示区域 */}
|
|
<div className="space-y-6">
|
|
{/* 图片资源展示 */}
|
|
{/* {imagePagination?.total > 0 && (
|
|
<div className="rounded-xl border p-4 bg-white">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-medium">图片列表</h3>
|
|
<span className="text-gray-500 text-sm">
|
|
共 {imagePagination.total} 张图片
|
|
</span>
|
|
</div> */}
|
|
|
|
{/* <Row gutter={[16, 16]}>
|
|
<Image.PreviewGroup>
|
|
{imageResources.map((resource) => (
|
|
<Col key={resource.url} xs={12} sm={8} md={6} lg={4}>
|
|
<div className="relative aspect-square rounded-lg overflow-hidden flex items-center justify-center bg-gray-100">
|
|
<Image
|
|
src={resource.url}
|
|
alt={resource.title}
|
|
preview={{
|
|
src: resource.originalUrl,
|
|
}}
|
|
className="object-contain w-full h-full"
|
|
/>
|
|
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
|
<div className="flex items-center justify-between text-white">
|
|
<span className="text-sm truncate">
|
|
{resource.title}
|
|
</span>
|
|
{isDomainAdmin && (
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
className="text-white hover:text-red-500"
|
|
icon={<DeleteOutlined />}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDelete(resource.id);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Col>
|
|
))}
|
|
</Image.PreviewGroup>
|
|
</Row> */}
|
|
|
|
{/* 图片分页 */}
|
|
{/* {imagePagination.total > pageSize && (
|
|
<div className="flex justify-center mt-6">
|
|
<Pagination
|
|
current={imagePage}
|
|
pageSize={pageSize}
|
|
total={imagePagination.total}
|
|
onChange={(page) => setImagePage(page)}
|
|
showSizeChanger={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)} */}
|
|
|
|
{/* 其他文件资源展示 */}
|
|
{isLoading ? (
|
|
<div className="text-center py-8">加载中...</div>
|
|
) : filePagination?.total > 0 ? (
|
|
<div className="rounded-xl border p-4 bg-white">
|
|
<div className="flex flex-col sm:flex-row justify-between items-center mb-4 gap-4">
|
|
<h3 className="text-lg font-medium">文件列表</h3>
|
|
<span className="text-gray-500 text-sm">
|
|
共 {filePagination.total} 个文件
|
|
</span>
|
|
</div>
|
|
|
|
{fileResources.length > 0 && (
|
|
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
|
{fileResources.map((resource) => (
|
|
<div
|
|
key={resource.id}
|
|
className="flex items-center p-3 rounded-lg border hover:shadow-md transition-shadow"
|
|
>
|
|
<div className="text-primary-600 text-2xl mr-3">
|
|
{getFileIcon(resource.url)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium truncate">
|
|
{resource.meta?.filename}
|
|
</div>
|
|
{/* {resource.meta?.filetype && (
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
类型: {resource.meta?.filetype}
|
|
</div>
|
|
)} */}
|
|
<div className="text-xs text-gray-500 flex items-center gap-2">
|
|
<span>
|
|
{dayjs(resource.createdAt).format("YYYY-MM-DD")}
|
|
</span>
|
|
<span>
|
|
{resource.meta?.size &&
|
|
formatFileSize(resource.meta?.size)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
onClick={() => window.open(resource.url)}
|
|
>
|
|
下载
|
|
</Button>
|
|
{isDomainAdmin && (
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDelete(resource.id);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 文件分页 */}
|
|
{filePagination.total > pageSize && (
|
|
<div className="flex justify-center mt-6">
|
|
<Pagination
|
|
current={filePage}
|
|
pageSize={pageSize}
|
|
total={filePagination.total}
|
|
onChange={(page) => setFilePage(page)}
|
|
showSizeChanger={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-gray-500">
|
|
{searchTerm ? `未找到匹配"${searchTerm}"的文件` : "暂无文件资源"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|