mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-03 07:21:13 +08:00
feat: migrate from Next.js to Vite and React Router, updating routing and environment variable handling
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { BookOpen, Bot, Home, ImageIcon, Images, List, Menu, Music2, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video } from "lucide-react";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
@@ -220,9 +220,9 @@ function ConnectionCreateOption({ theme, icon, title, description, onClick }: {
|
||||
function InfiniteCanvasPage() {
|
||||
const { message, modal } = App.useApp();
|
||||
const params = useParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = params.id;
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = params.id || "";
|
||||
const localAgentConnected = useCanvasAgentStore((state) => state.connected);
|
||||
const localAgentActivity = useCanvasAgentStore((state) => state.activity);
|
||||
const localAgentEnabled = useCanvasAgentStore((state) => state.enabled);
|
||||
@@ -399,7 +399,7 @@ function InfiniteCanvasPage() {
|
||||
setProjectLoaded(false);
|
||||
const project = openProject(projectId);
|
||||
if (!project) {
|
||||
router.replace("/canvas");
|
||||
navigate("/canvas", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -430,7 +430,7 @@ function InfiniteCanvasPage() {
|
||||
setProjectLoaded(true);
|
||||
};
|
||||
void restore();
|
||||
}, [hydrated, openProject, projectId, router]);
|
||||
}, [hydrated, navigate, openProject, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectLoaded || !["new", "recent", "choose"].includes(searchParams.get("mode") || "")) return;
|
||||
@@ -1032,14 +1032,14 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const createAndOpenProject = useCallback(() => {
|
||||
const id = createProject(`无限画布 ${useCanvasStore.getState().projects.length + 1}`);
|
||||
router.push(`/canvas/${id}`);
|
||||
}, [createProject, router]);
|
||||
navigate(`/canvas/${id}`);
|
||||
}, [createProject, navigate]);
|
||||
|
||||
const deleteCurrentProject = useCallback(() => {
|
||||
deleteProjects([projectId]);
|
||||
cleanupAssetImages();
|
||||
router.push("/canvas");
|
||||
}, [cleanupAssetImages, deleteProjects, projectId, router]);
|
||||
navigate("/canvas");
|
||||
}, [cleanupAssetImages, deleteProjects, navigate, projectId]);
|
||||
|
||||
const handleCanvasMouseDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
@@ -2503,8 +2503,8 @@ function InfiniteCanvasPage() {
|
||||
onCancelTitleEditing={() => setTitleEditing(false)}
|
||||
canUndo={historyState.canUndo}
|
||||
canRedo={historyState.canRedo}
|
||||
onHome={() => router.push("/")}
|
||||
onProjects={() => router.push("/canvas")}
|
||||
onHome={() => navigate("/")}
|
||||
onProjects={() => navigate("/canvas")}
|
||||
onCreateProject={createAndOpenProject}
|
||||
onDeleteProject={deleteCurrentProject}
|
||||
onImportImage={() => handleUploadRequest()}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { App, Button, Input, Segmented, Tooltip } from "antd";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import { Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, PlugZap, Plus, RefreshCw, RotateCcw, Terminal, Trash2 } from "lucide-react";
|
||||
@@ -45,7 +45,7 @@ export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedde
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const user = useUserStore((state) => state.user);
|
||||
const { message, modal } = App.useApp();
|
||||
const searchParams = useSearchParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { width, url, token, connected, enabled, prompt, attachments, sending, waiting, messages, eventLogs, threads, activeThreadId, workspacePath, loadingThreads, activeTab, confirmTools, activity, connectError, pendingTool, setAgentState, addMessage: pushMessage, addEventLog: pushEventLog, clearEventLogs } = useCanvasAgentStore();
|
||||
const [resizing, setResizing] = useState(false);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Check, Download, Pencil, Trash2, X } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Button, Input } from "antd";
|
||||
|
||||
import { useCanvasStore, type CanvasProject } from "../stores/use-canvas-store";
|
||||
@@ -9,8 +9,8 @@ import { useCanvasUiStore } from "../stores/use-canvas-ui-store";
|
||||
import { exportCanvasProjects } from "../utils/canvas-export";
|
||||
|
||||
export function CanvasProjectCard({ project }: { project: CanvasProject }) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const renameProject = useCanvasStore((state) => state.renameProject);
|
||||
const selectedIds = useCanvasUiStore((state) => state.selectedProjectIds);
|
||||
const editingId = useCanvasUiStore((state) => state.editingProjectId);
|
||||
@@ -22,7 +22,7 @@ export function CanvasProjectCard({ project }: { project: CanvasProject }) {
|
||||
const setDeleteIds = useCanvasUiStore((state) => state.setDeleteProjectIds);
|
||||
const editing = editingId === project.id;
|
||||
const selected = selectedIds.includes(project.id);
|
||||
const open = () => router.push(`/canvas/${project.id}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`);
|
||||
const open = () => navigate(`/canvas/${project.id}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`);
|
||||
const saveTitle = () => {
|
||||
renameProject(project.id, editingTitle);
|
||||
stopEditing();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { App, Button } from "antd";
|
||||
import { Download, FileUp, Plus } from "lucide-react";
|
||||
|
||||
@@ -17,8 +17,8 @@ import { exportCanvasProjects } from "./utils/canvas-export";
|
||||
|
||||
export default function CanvasPage() {
|
||||
const { message } = App.useApp();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const autoOpenRef = useRef(false);
|
||||
const hydrated = useCanvasStore((state) => state.hydrated);
|
||||
@@ -32,7 +32,7 @@ export default function CanvasPage() {
|
||||
const agentMode = mode === "new" || mode === "recent" || mode === "choose";
|
||||
const agentQuery = agentMode ? `?${searchParams.toString()}` : "";
|
||||
const enterProject = (id: string) => {
|
||||
router.push(`/canvas/${id}${agentQuery}`);
|
||||
navigate(`/canvas/${id}${agentQuery}`);
|
||||
};
|
||||
const createAndEnter = () => enterProject(createProject(`无限画布 ${projects.length + 1}`));
|
||||
const importCanvas = async (file?: File) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { App, Button, Image, Tag } from "antd";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { fetchPrompts, type Prompt } from "@/services/api/prompts";
|
||||
import { navigationTools } from "@/constant/navigation-tools";
|
||||
@@ -23,6 +24,7 @@ function Highlighter({ action, color, children }: { action: "highlight" | "under
|
||||
|
||||
export default function IndexPage() {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const [primaryTool] = navigationTools;
|
||||
const [promptShowcase, setPromptShowcase] = useState<Prompt[]>([]);
|
||||
const [previewIndex, setPreviewIndex] = useState(0);
|
||||
@@ -54,10 +56,10 @@ export default function IndexPage() {
|
||||
,让创作从单次生成变成连续推演。
|
||||
</p>
|
||||
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
|
||||
<Button type="primary" size="large" href={`/${primaryTool.slug}`} icon={<ArrowRight className="size-4" />} iconPlacement="end">
|
||||
<Button type="primary" size="large" onClick={() => navigate(`/${primaryTool.slug}`)} icon={<ArrowRight className="size-4" />} iconPlacement="end">
|
||||
开始使用
|
||||
</Button>
|
||||
<Button size="large" href="/canvas">
|
||||
<Button size="large" onClick={() => navigate("/canvas")}>
|
||||
打开画布
|
||||
</Button>
|
||||
</div>
|
||||
@@ -70,7 +72,7 @@ export default function IndexPage() {
|
||||
<h2 className="text-3xl font-semibold text-stone-950 dark:text-stone-100">沉淀每一次好结果</h2>
|
||||
<p className="mt-3 text-base leading-7 text-stone-500 dark:text-stone-400">收藏稳定出图的提示词、参考风格和结果图片,让下一次创作从已有经验开始。</p>
|
||||
</div>
|
||||
<Button type="link" href="/prompts" className="justify-self-center md:justify-self-end" icon={<ArrowRight className="size-4" />} iconPlacement="end">
|
||||
<Button type="link" onClick={() => navigate("/prompts")} className="justify-self-center md:justify-self-end" icon={<ArrowRight className="size-4" />} iconPlacement="end">
|
||||
查看提示词库
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { AntdRegistry } from "@ant-design/nextjs-registry";
|
||||
import { AppProviders } from "@/components/layout/app-providers";
|
||||
import "antd/dist/reset.css";
|
||||
import "./globals.css";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "无限画布",
|
||||
description: "一个无限画布创作工具",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning className="font-sans">
|
||||
<body
|
||||
className="bg-background text-foreground antialiased"
|
||||
style={{
|
||||
fontFamily: '"SF Pro Display","SF Pro Text","PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif',
|
||||
}}
|
||||
>
|
||||
<Script
|
||||
id="theme-script"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `try{var s=JSON.parse(localStorage.getItem("infinite-canvas:theme_store")||"{}");var t=s.state&&s.state.theme==="light"?"light":"dark";document.documentElement.classList.toggle("dark",t==="dark");document.documentElement.style.colorScheme=t}catch(e){}`,
|
||||
}}
|
||||
/>
|
||||
<AntdRegistry>
|
||||
<AppProviders>{children}</AppProviders>
|
||||
</AntdRegistry>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Home } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
@@ -10,7 +10,7 @@ export default function NotFound() {
|
||||
<h1 className="text-3xl font-semibold tracking-normal">页面不存在</h1>
|
||||
<p className="mt-3 text-sm leading-6 text-stone-500 dark:text-stone-400">这个地址没有对应的页面,可能已经移动或被合并到其他入口。</p>
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||
<Link href="/" className="inline-flex h-10 items-center gap-2 rounded-lg bg-stone-950 px-4 text-sm font-medium text-white transition hover:bg-stone-800 dark:bg-stone-100 dark:text-stone-950 dark:hover:bg-stone-200">
|
||||
<Link to="/" className="inline-flex h-10 items-center gap-2 rounded-lg bg-stone-950 px-4 text-sm font-medium text-white transition hover:bg-stone-800 dark:bg-stone-100 dark:text-stone-950 dark:hover:bg-stone-200">
|
||||
<Home className="size-4" />
|
||||
返回首页
|
||||
</Link>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Menu } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools";
|
||||
import { AppConfigModal } from "@/components/layout/app-config-modal";
|
||||
@@ -12,7 +11,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
export function AppTopNav() {
|
||||
const pathname = usePathname();
|
||||
const { pathname } = useLocation();
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||
const hideHeader = /^\/canvas\/[^/]+/.test(pathname);
|
||||
const slug = pathname.split("/").filter(Boolean)[0];
|
||||
@@ -24,7 +23,7 @@ export function AppTopNav() {
|
||||
<header className="sticky top-0 z-20 h-16 shrink-0 border-b border-stone-200 bg-background/90 backdrop-blur-xl dark:border-stone-800">
|
||||
<div className="mx-auto flex h-full max-w-7xl items-stretch justify-between gap-5 px-6">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<Link href="/" className="flex h-full shrink-0 items-center gap-2 text-sm font-semibold leading-none tracking-tight text-stone-950 transition hover:text-stone-600 dark:text-stone-100 dark:hover:text-stone-300">
|
||||
<Link to="/" className="flex h-full shrink-0 items-center gap-2 text-sm font-semibold leading-none tracking-tight text-stone-950 transition hover:text-stone-600 dark:text-stone-100 dark:hover:text-stone-300">
|
||||
<span
|
||||
className="size-5 shrink-0 bg-current"
|
||||
style={{
|
||||
@@ -52,7 +51,7 @@ export function AppTopNav() {
|
||||
return (
|
||||
<Link
|
||||
key={tool.slug}
|
||||
href={`/${tool.slug}`}
|
||||
to={`/${tool.slug}`}
|
||||
className={cn(
|
||||
"relative flex h-16 shrink-0 items-center gap-2 text-sm leading-6 transition after:absolute after:inset-x-0 after:bottom-0 after:h-px",
|
||||
active
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Drawer } from "antd";
|
||||
import Link from "next/link";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -22,7 +22,7 @@ export function MobileNavDrawer({ open, activeToolSlug, onClose }: MobileNavDraw
|
||||
return (
|
||||
<Link
|
||||
key={tool.slug}
|
||||
href={`/${tool.slug}`}
|
||||
to={`/${tool.slug}`}
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-3 text-base transition",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
||||
export const APP_VERSION = __APP_VERSION__ || "dev";
|
||||
|
||||
export const DOCS_URL = process.env.NEXT_PUBLIC_DOC_URL || "https://docs.canvas.best";
|
||||
export const DOCS_URL = import.meta.env.VITE_DOC_URL || "https://docs.canvas.best";
|
||||
|
||||
@@ -7,11 +7,7 @@ const latestVersionUrl = "https://raw.githubusercontent.com/basketikun/infinite-
|
||||
const latestChangelogUrl = "https://raw.githubusercontent.com/basketikun/infinite-canvas/main/CHANGELOG.md";
|
||||
|
||||
function readLocalReleases(): ReleaseInfo[] {
|
||||
try {
|
||||
return JSON.parse(process.env.NEXT_PUBLIC_APP_RELEASES || "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return __APP_RELEASES__ || [];
|
||||
}
|
||||
|
||||
function toVersionParts(version: string) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "antd/dist/reset.css";
|
||||
import "./app/globals.css";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
import { AppProviders } from "@/components/layout/app-providers";
|
||||
import { router } from "@/router";
|
||||
|
||||
document.body.style.fontFamily = '"SF Pro Display","SF Pro Text","PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif';
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<AppProviders>
|
||||
<RouterProvider router={router} />
|
||||
</AppProviders>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createBrowserRouter, Outlet } from "react-router-dom";
|
||||
|
||||
import NotFound from "@/app/not-found";
|
||||
import UserLayout from "@/app/(user)/layout";
|
||||
import IndexPage from "@/app/(user)/page";
|
||||
import ImagePage from "@/app/(user)/image/page";
|
||||
import VideoPage from "@/app/(user)/video/page";
|
||||
import AssetsPage from "@/app/(user)/assets/page";
|
||||
import PromptsPage from "@/app/(user)/prompts/page";
|
||||
import CanvasPage from "@/app/(user)/canvas/page";
|
||||
import CanvasProjectPage from "@/app/(user)/canvas/[id]/page";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
element: (
|
||||
<UserLayout>
|
||||
<Outlet />
|
||||
</UserLayout>
|
||||
),
|
||||
children: [
|
||||
{ path: "/", element: <IndexPage /> },
|
||||
{ path: "/image", element: <ImagePage /> },
|
||||
{ path: "/video", element: <VideoPage /> },
|
||||
{ path: "/assets", element: <AssetsPage /> },
|
||||
{ path: "/prompts", element: <PromptsPage /> },
|
||||
{ path: "/canvas", element: <CanvasPage /> },
|
||||
{ path: "/canvas/:id", element: <CanvasProjectPage /> },
|
||||
],
|
||||
},
|
||||
{ path: "*", element: <NotFound /> },
|
||||
]);
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
declare const __APP_RELEASES__: import("@/lib/release").ReleaseInfo[];
|
||||
Reference in New Issue
Block a user