fix: redirect /canvas to new canvas editor instead of list page
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

- Replace full-screen canvas list with auto-redirect to new canvas
- Ensures users always enter canvas editor mode directly
- Use Link component for reliable navigation in top navbar
This commit is contained in:
Your Name 2026-01-26 21:24:25 +08:00
parent 676be1c70c
commit 2566c00cb1
2 changed files with 31 additions and 308 deletions

View File

@ -1,289 +1,25 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { Plus, LayoutGrid, Trash2, Edit2, MoreVertical, Clock, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import { canvasService } from "@/services/canvas";
import { Loader2 } from "lucide-react";
interface CanvasProject {
id: string;
title: string;
description?: string;
thumbnail?: string;
createdAt: string;
updatedAt: string;
}
export default function CanvasListPage() {
// 自动重定向到新画布
export default function CanvasRedirectPage() {
const router = useRouter();
const [projects, setProjects] = useState<CanvasProject[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isCreating, setIsCreating] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [projectToDelete, setProjectToDelete] = useState<CanvasProject | null>(null);
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [projectToRename, setProjectToRename] = useState<CanvasProject | null>(null);
const [newTitle, setNewTitle] = useState("");
// 加载画布列表
const loadProjects = async () => {
setIsLoading(true);
const result = await canvasService.getProjects();
if (result.success && result.data) {
setProjects(result.data);
} else {
toast.error(result.error || "加载失败");
}
setIsLoading(false);
};
useEffect(() => {
loadProjects();
}, []);
// 创建新画布
const handleCreateProject = async () => {
setIsCreating(true);
const result = await canvasService.createProject({
title: "未命名画布",
});
if (result.success && result.data) {
toast.success("创建成功");
router.push(`/canvas/${result.data.id}`);
} else {
toast.error(result.error || "创建失败");
setIsCreating(false);
}
};
// 删除画布
const handleDeleteProject = async () => {
if (!projectToDelete) return;
const result = await canvasService.deleteProject(projectToDelete.id);
if (result.success) {
toast.success("删除成功");
setProjects(projects.filter(p => p.id !== projectToDelete.id));
} else {
toast.error(result.error || "删除失败");
}
setDeleteDialogOpen(false);
setProjectToDelete(null);
};
// 重命名画布
const handleRenameProject = async () => {
if (!projectToRename || !newTitle.trim()) return;
const result = await canvasService.updateProject(projectToRename.id, {
title: newTitle.trim(),
});
if (result.success) {
toast.success("重命名成功");
setProjects(projects.map(p =>
p.id === projectToRename.id ? { ...p, title: newTitle.trim() } : p
));
} else {
toast.error(result.error || "重命名失败");
}
setRenameDialogOpen(false);
setProjectToRename(null);
setNewTitle("");
};
// 格式化时间
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
// 生成新的画布 ID 并跳转
const newId = crypto.randomUUID();
router.replace(`/canvas/${newId}`);
}, [router]);
return (
<div className="flex flex-col h-full">
{/* 页面标题 */}
<div className="flex items-center justify-between p-6 border-b">
<div className="flex items-center gap-3">
<LayoutGrid className="h-6 w-6 text-purple-500" />
<h1 className="text-2xl font-bold">无限画布</h1>
</div>
<Button onClick={handleCreateProject} disabled={isCreating}>
{isCreating ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Plus className="h-4 w-4 mr-2" />
)}
新建画布
</Button>
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">正在创建新画布...</span>
</div>
{/* 画布列表 */}
<div className="flex-1 overflow-auto p-6">
{isLoading ? (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : projects.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-center">
<LayoutGrid className="h-16 w-16 text-muted-foreground/50 mb-4" />
<h3 className="text-lg font-medium mb-2">还没有画布</h3>
<p className="text-muted-foreground mb-4">
创建一个新画布,开始你的创作之旅
</p>
<Button onClick={handleCreateProject} disabled={isCreating}>
{isCreating ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Plus className="h-4 w-4 mr-2" />
)}
新建画布
</Button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{projects.map((project) => (
<Card
key={project.id}
className="group cursor-pointer hover:shadow-lg transition-shadow"
onClick={() => router.push(`/canvas/${project.id}`)}
>
<CardHeader className="p-4 pb-2">
<div className="flex items-start justify-between">
<CardTitle className="text-base font-medium line-clamp-1">
{project.title}
</CardTitle>
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setProjectToRename(project);
setNewTitle(project.title);
setRenameDialogOpen(true);
}}
>
<Edit2 className="h-4 w-4 mr-2" />
重命名
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive"
onClick={(e) => {
e.stopPropagation();
setProjectToDelete(project);
setDeleteDialogOpen(true);
}}
>
<Trash2 className="h-4 w-4 mr-2" />
删除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="p-4 pt-0">
<div className="aspect-video bg-muted rounded-md flex items-center justify-center overflow-hidden">
{project.thumbnail ? (
<img
src={project.thumbnail}
alt={project.title}
className="w-full h-full object-cover"
/>
) : (
<LayoutGrid className="h-12 w-12 text-muted-foreground/30" />
)}
</div>
</CardContent>
<CardFooter className="p-4 pt-0">
<div className="flex items-center text-xs text-muted-foreground">
<Clock className="h-3 w-3 mr-1" />
{formatDate(project.updatedAt)}
</div>
</CardFooter>
</Card>
))}
</div>
)}
</div>
{/* 删除确认对话框 */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>确认删除</DialogTitle>
<DialogDescription>
确定要删除画布 "{projectToDelete?.title}" 吗?此操作无法撤销。
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
取消
</Button>
<Button variant="destructive" onClick={handleDeleteProject}>
删除
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 重命名对话框 */}
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>重命名画布</DialogTitle>
</DialogHeader>
<Input
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="输入新名称"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleRenameProject();
}
}}
/>
<DialogFooter>
<Button variant="outline" onClick={() => setRenameDialogOpen(false)}>
取消
</Button>
<Button onClick={handleRenameProject} disabled={!newTitle.trim()}>
确定
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -174,17 +174,13 @@ export function TopNavbar({
)}
{/* AI智能精选 */}
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
onClick={() => router.push("/featured")}
type="button"
title="AI智能精选"
<Link
href="/featured"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
>
<Sparkles className="h-4 w-4 text-yellow-400" />
<span className="text-sm font-medium">AI精选</span>
</Button>
<span>AI精选</span>
</Link>
{/* 精选榜单 */}
<DropdownMenu open={rankingOpen} onOpenChange={setRankingOpen}>
@ -255,33 +251,27 @@ export function TopNavbar({
</Button>
{/* 无限画布 */}
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
<button
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
onClick={() => {
const newId = crypto.randomUUID();
router.push(`/canvas/${newId}`);
window.location.href = `/canvas/${newId}`;
}}
type="button"
title="无限画布"
>
<LayoutGrid className="h-4 w-4 text-purple-400" />
<span className="text-sm font-medium">无限画布</span>
</Button>
<span>无限画布</span>
</button>
{/* 我的图册 */}
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
onClick={() => router.push("/gallery")}
type="button"
title="我的图册"
<Link
href="/gallery"
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
>
<Images className="h-4 w-4" />
<span className="text-sm font-medium">我的图册</span>
</Button>
<span>我的图册</span>
</Link>
{/* 分隔线 (如果有链接或图册按钮,且是管理员) */}
{isAdmin && (
@ -290,16 +280,13 @@ export function TopNavbar({
{/* 系统设置(仅管理员可见)*/}
{isAdmin && (
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-white hover:bg-white/15 hover:text-white rounded-full"
onClick={() => router.push("/settings")}
type="button"
<Link
href="/settings"
className="flex items-center justify-center h-8 w-8 text-white hover:bg-white/15 hover:text-white rounded-full"
title="系统设置"
>
<Settings className="h-4 w-4" />
</Button>
</Link>
)}
</div>