203 lines
7.1 KiB
TypeScript
203 lines
7.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
import { zhCN } from "date-fns/locale";
|
|
import { Trash2, FileImage, Loader2, Plus } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import { canvasService } from "@/services/canvas";
|
|
import { toast } from "sonner";
|
|
import { cn } from "@/lib/utils";
|
|
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
|
import type { CanvasProject } from "@/types/canvas";
|
|
|
|
interface CanvasFileListProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
currentId: string;
|
|
}
|
|
|
|
export function CanvasFileList({ open, onOpenChange, currentId }: CanvasFileListProps) {
|
|
const router = useRouter();
|
|
const [projects, setProjects] = useState<CanvasProject[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// 加载项目列表
|
|
const loadProjects = async () => {
|
|
setIsLoading(true);
|
|
const result = await canvasService.getProjects();
|
|
if (result.success && result.data) {
|
|
// 按更新时间降序排序
|
|
const sorted = result.data.sort((a, b) =>
|
|
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
);
|
|
setProjects(sorted);
|
|
}
|
|
setIsLoading(false);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
loadProjects();
|
|
}
|
|
}, [open]);
|
|
|
|
// 创建新画布
|
|
const handleCreateNew = async () => {
|
|
// 生成一个新 ID 并跳转,具体的创建逻辑由页面组件处理
|
|
const newId = crypto.randomUUID();
|
|
router.push(`/canvas/${newId}`);
|
|
onOpenChange(false);
|
|
};
|
|
|
|
// 切换画布
|
|
const handleSwitch = (id: string) => {
|
|
if (id === currentId) {
|
|
onOpenChange(false);
|
|
return;
|
|
}
|
|
// 先导航,再关闭弹窗(使用 setTimeout 确保导航生效)
|
|
const targetUrl = `/canvas/${id}`;
|
|
onOpenChange(false);
|
|
// 使用 setTimeout 确保在 React 状态更新完成后再导航
|
|
setTimeout(() => {
|
|
window.location.assign(targetUrl);
|
|
}, 100);
|
|
};
|
|
|
|
// 删除画布
|
|
const handleDelete = async (e: React.MouseEvent, id: string) => {
|
|
e.stopPropagation();
|
|
if (!confirm("确定要删除这个画布吗?")) return;
|
|
|
|
const result = await canvasService.deleteProject(id);
|
|
if (result.success) {
|
|
setProjects(prev => prev.filter(p => p.id !== id));
|
|
toast.success("删除成功");
|
|
|
|
// 如果删除的是当前画布,跳转到列表第一个或首页
|
|
if (id === currentId) {
|
|
const next = projects.find(p => p.id !== id);
|
|
if (next) {
|
|
router.push(`/canvas/${next.id}`);
|
|
} else {
|
|
router.push('/canvas'); // 或者 handleCreateNew()
|
|
}
|
|
}
|
|
} else {
|
|
toast.error("删除失败");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-md max-h-[80vh] flex flex-col p-0 gap-0 overflow-hidden bg-background/95 backdrop-blur-sm">
|
|
<DialogHeader className="p-4 border-b">
|
|
<div className="flex items-center justify-between">
|
|
<DialogTitle>已生成文件列表</DialogTitle>
|
|
<Button size="sm" onClick={handleCreateNew} className="h-8">
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
新建画布
|
|
</Button>
|
|
</div>
|
|
<DialogDescription className="hidden">画布列表</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<ScrollArea className="flex-1 p-4">
|
|
{isLoading ? (
|
|
<div className="flex justify-center py-8">
|
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : projects.length === 0 ? (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<FileImage className="h-12 w-12 mx-auto mb-3 opacity-20" />
|
|
<p>暂无画布历史</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{projects.map((project) => (
|
|
<div
|
|
key={project.id}
|
|
onClick={() => handleSwitch(project.id)}
|
|
className={cn(
|
|
"flex items-center gap-3 p-2 rounded-xl cursor-pointer transition-all border",
|
|
"hover:bg-muted/50 hover:shadow-sm",
|
|
project.id === currentId
|
|
? "bg-primary/5 border-primary/20 ring-1 ring-primary/10"
|
|
: "bg-card border-transparent"
|
|
)}
|
|
>
|
|
{/* 缩略图 */}
|
|
<div className="h-12 w-12 rounded-lg bg-muted/50 overflow-hidden shrink-0 border">
|
|
{project.thumbnail ? (
|
|
<img
|
|
src={buildImageProxyUrl(project.thumbnail, {
|
|
width: 144,
|
|
quality: 82,
|
|
format: "webp",
|
|
})}
|
|
alt={project.title}
|
|
className="h-full w-full object-cover"
|
|
loading="lazy"
|
|
decoding="async"
|
|
onError={(event) => {
|
|
if (event.currentTarget.src !== project.thumbnail) {
|
|
event.currentTarget.src = project.thumbnail || "";
|
|
}
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="h-full w-full flex items-center justify-center">
|
|
<FileImage className="h-5 w-5 text-muted-foreground/40" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 信息 */}
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className={cn(
|
|
"font-medium text-sm truncate",
|
|
project.id === currentId && "text-primary"
|
|
)}>
|
|
{project.title || "未命名画布"}
|
|
</h4>
|
|
<p className="text-xs text-muted-foreground">
|
|
{formatDistanceToNow(new Date(project.updatedAt), {
|
|
addSuffix: true,
|
|
locale: zhCN
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 操作 */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100"
|
|
onClick={(e) => handleDelete(e, project.id)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
|
|
<div className="text-center py-4 text-xs text-muted-foreground/50">
|
|
到底了
|
|
</div>
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|