281 lines
14 KiB
TypeScript
Executable File
281 lines
14 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { useTranslation } from '@nice/i18n';
|
|
import { useSetPageInfo } from '@/components/providers/dashboard-provider';
|
|
import FileUpload from '@/components/common/file-upload';
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { useTRPC } from '@fenghuo/client';
|
|
import { useMemo, useState } from 'react';
|
|
import { useAuth } from '@/components/providers/auth-provider';
|
|
import { Card, CardContent } from '@nice/ui/components/card';
|
|
import { Button } from '@nice/ui/components/button';
|
|
import { Folder, FileImage, Copy, Download } from 'lucide-react';
|
|
import { toast } from '@nice/ui/components/sonner';
|
|
import {
|
|
Pagination,
|
|
PaginationContent,
|
|
PaginationItem,
|
|
PaginationLink,
|
|
PaginationNext,
|
|
PaginationPrevious,
|
|
PaginationEllipsis,
|
|
} from '@nice/ui/components/pagination';
|
|
|
|
interface Resource {
|
|
id: string;
|
|
title: string | null;
|
|
url: string | null;
|
|
type: string | null;
|
|
[key: string]: any;
|
|
}
|
|
|
|
export default function ProfilePage() {
|
|
const { t } = useTranslation();
|
|
const { user, isSuperAdmin, isAdmin } = useAuth();
|
|
const trpc = useTRPC();
|
|
const queryClient = useQueryClient();
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(14);
|
|
|
|
// 设置页面信息
|
|
useSetPageInfo({
|
|
title: "文件媒体库",
|
|
subtitle: '上传文件资源、音视频资源、图片资源,并进行管理'
|
|
});
|
|
|
|
const searchConditions = {
|
|
ownerId: isAdmin() || isSuperAdmin() ? undefined : user?.id,
|
|
};
|
|
const { data: resources, isLoading: resourcesLoading } = useQuery({
|
|
...trpc.resource.findManyWithPagination.queryOptions({
|
|
page,
|
|
pageSize,
|
|
where: {
|
|
...searchConditions,
|
|
},
|
|
orderBy: { createdAt: 'desc' }
|
|
}),
|
|
});
|
|
|
|
const resourcesData = useMemo(() => {
|
|
return (resources?.items || []) as Resource[];
|
|
}, [resources]);
|
|
|
|
const isImageFile = (filename: string | null) => {
|
|
if (!filename) return false;
|
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
|
|
return imageExtensions.some(ext => filename.toLowerCase().endsWith(ext));
|
|
};
|
|
|
|
const handleCopyLink = async (url: string | null, e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
if (!url) {
|
|
toast("文件链接不存在");
|
|
return;
|
|
}
|
|
try {
|
|
await navigator.clipboard.writeText(url);
|
|
toast("复制成功");
|
|
} catch (err) {
|
|
toast("复制链接失败");
|
|
}
|
|
};
|
|
|
|
const handleDownload = (url: string | null, filename: string | null, e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
if (!url) {
|
|
toast("文件链接不存在");
|
|
return;
|
|
}
|
|
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename || '下载文件';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
toast("文件正在下载...");
|
|
};
|
|
|
|
const handleUploadComplete = async () => await queryClient.invalidateQueries({ queryKey: trpc.resource.pathKey() });
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* 主要内容区域 */}
|
|
<div className="flex-1 overflow-hidden">
|
|
<div className='m-4 p-4'>
|
|
<FileUpload
|
|
multiple
|
|
maxSize={10 * 1024 * 1024 * 1024}
|
|
onUploadComplete={handleUploadComplete}
|
|
/>
|
|
</div>
|
|
<div className="px-4 pb-4">
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 gap-3">
|
|
{resourcesData.map((resource) => (
|
|
<Card key={resource.id} className="h-48 !p-0 aspect-square hover:shadow-lg transition-shadow cursor-pointer group">
|
|
<CardContent className="p-0 h-full flex flex-col items-center justify-center relative">
|
|
{isImageFile(resource.title) && resource.url && resource.type === 'image' ? (
|
|
<div className="w-full h-full overflow-hidden rounded-lg">
|
|
<img
|
|
src={resource.url}
|
|
alt={resource.title || '未命名文件'}
|
|
className="w-full h-full object-cover transition-transform duration-200 hover:scale-105"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center text-center h-full">
|
|
{resource.type === 'image' ? (
|
|
<FileImage className="w-16 h-16 text-muted-foreground/60 mb-3" />
|
|
) : (
|
|
<Folder className="w-16 h-16 text-muted-foreground/60 mb-3" />
|
|
)}
|
|
<span className="text-xs font-medium text-foreground/80 truncate max-w-full leading-tight text-wrap ">
|
|
{resource.title || '未命名文件'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 悬停时显示的按钮覆盖层 */}
|
|
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center gap-2 rounded-lg">
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
className="h-8 w-8 p-0"
|
|
onClick={(e) => handleCopyLink(resource.url, e)}
|
|
title="复制下载链接"
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
className="h-8 w-8 p-0"
|
|
onClick={(e) => handleDownload(resource.url, resource.title, e)}
|
|
title="下载文件"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* 分页组件 */}
|
|
{resources && resources.totalPages > 1 && (
|
|
<div className="flex justify-center mt-6">
|
|
<Pagination>
|
|
<PaginationContent>
|
|
<PaginationItem>
|
|
<PaginationPrevious
|
|
href="#"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
if (page > 1) setPage(page - 1);
|
|
}}
|
|
className={page <= 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
|
|
/>
|
|
</PaginationItem>
|
|
|
|
{/* 页码显示逻辑 */}
|
|
{(() => {
|
|
const totalPages = resources.totalPages;
|
|
const currentPage = page;
|
|
const pages: React.ReactNode[] = [];
|
|
|
|
// 显示第一页
|
|
if (currentPage > 3) {
|
|
pages.push(
|
|
<PaginationItem key={1}>
|
|
<PaginationLink
|
|
href="#"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
setPage(1);
|
|
}}
|
|
className="cursor-pointer"
|
|
>
|
|
1
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
);
|
|
|
|
if (currentPage > 4) {
|
|
pages.push(
|
|
<PaginationItem key="ellipsis1">
|
|
<PaginationEllipsis />
|
|
</PaginationItem>
|
|
);
|
|
}
|
|
}
|
|
|
|
// 显示当前页附近的页码
|
|
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
|
|
pages.push(
|
|
<PaginationItem key={i}>
|
|
<PaginationLink
|
|
href="#"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
setPage(i);
|
|
}}
|
|
isActive={i === currentPage}
|
|
className="cursor-pointer"
|
|
>
|
|
{i}
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
);
|
|
}
|
|
|
|
// 显示最后一页
|
|
if (currentPage < totalPages - 2) {
|
|
if (currentPage < totalPages - 3) {
|
|
pages.push(
|
|
<PaginationItem key="ellipsis2">
|
|
<PaginationEllipsis />
|
|
</PaginationItem>
|
|
);
|
|
}
|
|
|
|
pages.push(
|
|
<PaginationItem key={totalPages}>
|
|
<PaginationLink
|
|
href="#"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
setPage(totalPages);
|
|
}}
|
|
className="cursor-pointer"
|
|
>
|
|
{totalPages}
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
);
|
|
}
|
|
|
|
return pages;
|
|
})()}
|
|
|
|
<PaginationItem>
|
|
<PaginationNext
|
|
href="#"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
if (page < resources.totalPages) setPage(page + 1);
|
|
}}
|
|
className={page >= resources.totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
|
|
/>
|
|
</PaginationItem>
|
|
</PaginationContent>
|
|
</Pagination>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|