diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/src/app/[...path]/page.tsx b/frontend/src/app/[...path]/page.tsx new file mode 100644 index 0000000..7051332 --- /dev/null +++ b/frontend/src/app/[...path]/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ClientRoot } from "@/app/root"; + +export default function CatchAllPage() { + return ; +} diff --git a/frontend/src/app/project/[id]/page.tsx b/frontend/src/app/project/[id]/page.tsx new file mode 100644 index 0000000..41d151c --- /dev/null +++ b/frontend/src/app/project/[id]/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ClientRoot } from "@/app/root"; + +export default function ProjectPage() { + return ; +} diff --git a/frontend/src/app/projects/[id]/page.tsx b/frontend/src/app/projects/[id]/page.tsx new file mode 100644 index 0000000..41d151c --- /dev/null +++ b/frontend/src/app/projects/[id]/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ClientRoot } from "@/app/root"; + +export default function ProjectPage() { + return ; +} diff --git a/frontend/src/app/projects/page.tsx b/frontend/src/app/projects/page.tsx new file mode 100644 index 0000000..e3096bc --- /dev/null +++ b/frontend/src/app/projects/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ClientRoot } from "@/app/root"; + +export default function ProjectsPage() { + return ; +} diff --git a/frontend/src/application/route.ts b/frontend/src/application/route.ts index da55163..b2e7b6a 100644 --- a/frontend/src/application/route.ts +++ b/frontend/src/application/route.ts @@ -1,25 +1,44 @@ export type AppRoute = | { name: "home" } | { name: "projects" } - | { name: "project"; id: string }; + | { name: "project"; id: string } + | { name: "notFound" }; export function readRoute(): AppRoute { if (typeof window === "undefined") { return { name: "home" }; } const hash = window.location.hash.replace(/^#/, ""); - if (hash === "/projects") { + if (hash) { + return routeFromPath(hash) ?? { name: "notFound" }; + } + + return routeFromPath(window.location.pathname) ?? { name: "notFound" }; +} + +function routeFromPath(path: string): AppRoute | null { + const [pathOnly] = path.split(/[?#]/); + const normalized = pathOnly.replace(/\/+$/, "") || "/"; + if (normalized === "/projects") { return { name: "projects" }; } - const match = hash.match(/^\/project\/([^/]+)$/); + const match = normalized.match(/^\/projects?\/([^/]+)$/); if (match) { return { name: "project", id: decodeURIComponent(match[1]) }; } - return { name: "home" }; + if (normalized === "/") { + return { name: "home" }; + } + return null; } export function openHome() { if (typeof window === "undefined") return; + if (window.location.pathname !== "/") { + window.history.pushState(null, "", "/"); + window.dispatchEvent(new PopStateEvent("popstate")); + return; + } window.location.hash = "/"; } diff --git a/frontend/src/infrastructure/designGateway.ts b/frontend/src/infrastructure/designGateway.ts index 344378c..bd55a7e 100644 --- a/frontend/src/infrastructure/designGateway.ts +++ b/frontend/src/infrastructure/designGateway.ts @@ -168,19 +168,17 @@ export const designGateway = { agentSocket(projectId: string, payload: AgentChatPayload, handlers: ProjectEventHandlers) { let closed = false; - let closeEvents: (() => void) | null = payload.threadId ? designGateway.projectEvents(projectId, handlers, { threadId: payload.threadId }) : null; + let closeEvents: (() => void) | null = null; void designGateway .agentChat(projectId, payload) .then((response) => { if (closed) return; handlers.onProject?.(response.project); - if (!closeEvents) { - closeEvents = designGateway.projectEvents(response.project.id, handlers, { - threadId: response.threadId || payload.threadId, - eventsUrl: response.eventsUrl - }); - } + closeEvents = designGateway.projectEvents(response.project.id, handlers, { + threadId: response.threadId || payload.threadId, + eventsUrl: response.eventsUrl + }); }) .catch((error: Error) => { if (!closed) { diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index 7c9f57e..45b944e 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -2,7 +2,8 @@ import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; -import { readRoute, type AppRoute } from "@/application/route"; +import { openHome, readRoute, type AppRoute } from "@/application/route"; +import { BrandMark } from "@/ui/components/BrandMark"; import { CanvasWorkspace } from "@/ui/pages/CanvasWorkspace"; import { HomePage } from "@/ui/pages/HomePage"; import { ProjectsRoutePage } from "@/ui/pages/ProjectsRoutePage"; @@ -16,7 +17,11 @@ export function App() { setRoute(readRoute()); const handleRoute = () => setRoute(readRoute()); window.addEventListener("hashchange", handleRoute); - return () => window.removeEventListener("hashchange", handleRoute); + window.addEventListener("popstate", handleRoute); + return () => { + window.removeEventListener("hashchange", handleRoute); + window.removeEventListener("popstate", handleRoute); + }; }, []); useEffect(() => { @@ -33,8 +38,184 @@ export function App() { ); } - if (route.name === "project" && !isAuthenticated) return ; + if (route.name === "project" && !isAuthenticated) return ; if (route.name === "project") return ; if (route.name === "projects") return ; + if (route.name === "notFound") return ; return ; } + +function NotFoundPage() { + const handleHome = () => { + openHome(); + }; + + return ( +
+
+ +

404:页面未找到!

+

我们到处都找过了,但找不到您要找的页面。

+ +
+
+ ); +} + +function NotFoundMark() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function ProjectAuthPreview() { + return ( + + ); +} diff --git a/frontend/src/ui/styles.css b/frontend/src/ui/styles.css index c705968..bbce2b6 100644 --- a/frontend/src/ui/styles.css +++ b/frontend/src/ui/styles.css @@ -889,6 +889,357 @@ button:disabled { font-weight: 800; } +.not-found-shell { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 48px 24px; + color: #101827; + background: #fff; +} + +.not-found-content { + width: min(520px, 100%); + display: grid; + justify-items: center; + text-align: center; +} + +.not-found-mark { + width: min(280px, 72vw); + height: auto; + margin-bottom: 57px; +} + +.not-found-content h1 { + margin: 0; + color: #111827; + font-size: 32px; + line-height: 1.25; + font-weight: 800; + letter-spacing: 0; +} + +.not-found-content p { + margin: 16px 0 32px; + color: #6b7280; + font-size: 14px; + line-height: 1.5; + font-weight: 500; +} + +.not-found-content button { + min-width: 128px; + height: 42px; + padding: 0 26px; + border-radius: 12px; + color: #fff; + background: #030303; + font-size: 16px; + font-weight: 700; + line-height: 1; + transition: transform 0.16s ease, background 0.16s ease; +} + +.not-found-content button:hover { + background: #171717; + transform: translateY(-1px); +} + +.project-auth-shell { + width: 100vw; + height: 100vh; + overflow: hidden; + background: #f7f7f4; +} + +.project-auth-blur { + width: 100%; + height: 100%; + overflow: hidden; + filter: blur(18px); + opacity: 0.66; + transform: scale(1.04); + transform-origin: center; + pointer-events: none; + user-select: none; +} + +.project-auth-topbar { + height: 48px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 14px; + border-bottom: 1px solid #eceef1; + background: rgba(255, 255, 255, 0.86); +} + +.project-auth-title, +.project-auth-top-actions, +.project-auth-panel-head, +.project-auth-toolbar, +.project-auth-composer div { + display: flex; + align-items: center; +} + +.project-auth-title { + gap: 10px; +} + +.project-auth-mark { + width: 18px; + height: 26px; + color: #151515; +} + +.project-auth-title span, +.project-auth-thread b, +.project-auth-thread span, +.project-auth-composer span { + display: block; + border-radius: 999px; + background: #e1e3e6; +} + +.project-auth-title span { + width: 168px; + height: 14px; +} + +.project-auth-top-actions { + gap: 8px; +} + +.project-auth-top-actions i, +.project-auth-panel-head i, +.project-auth-toolbar i, +.project-auth-composer i { + display: block; + border-radius: 999px; + background: #e3e5e8; +} + +.project-auth-top-actions i { + width: 28px; + height: 28px; +} + +.project-auth-top-actions b { + width: 24px; + height: 24px; + display: block; + border-radius: 999px; + background: #151515; +} + +.project-auth-main { + height: calc(100vh - 48px); + display: grid; + grid-template-columns: minmax(0, 1fr) 400px; +} + +.project-auth-stage { + position: relative; + min-width: 0; + overflow: hidden; + background: #f7f7f4; +} + +.project-auth-grid { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(38, 45, 57, 0.055) 1px, transparent 1px), + linear-gradient(90deg, rgba(38, 45, 57, 0.055) 1px, transparent 1px); + background-size: 38px 38px; +} + +.project-auth-frame { + position: absolute; + border: 1px solid rgba(189, 193, 201, 0.9); + border-radius: 8px; + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 18px 58px rgba(17, 24, 39, 0.08); +} + +.project-auth-frame.primary { + left: 18%; + top: 20%; + width: 260px; + height: 180px; +} + +.project-auth-frame.secondary { + left: 48%; + top: 34%; + width: 220px; + height: 150px; +} + +.project-auth-frame.tertiary { + left: 31%; + top: 58%; + width: 180px; + height: 118px; +} + +.project-auth-toolbar { + position: absolute; + left: 50%; + bottom: 28px; + gap: 10px; + padding: 9px 12px; + border: 1px solid #e5e7eb; + border-radius: 16px; + background: rgba(255, 255, 255, 0.9); + transform: translateX(-50%); + box-shadow: 0 18px 48px rgba(17, 24, 39, 0.1); +} + +.project-auth-toolbar i { + width: 26px; + height: 26px; +} + +.project-auth-panel { + min-width: 0; + display: flex; + flex-direction: column; + border-left: 1px solid #eceef1; + background: rgba(255, 255, 255, 0.88); +} + +.project-auth-panel-head { + height: 56px; + gap: 10px; + padding: 0 18px; +} + +.project-auth-panel-head span { + width: 184px; + height: 16px; + display: block; + margin-right: auto; + border-radius: 999px; + background: #dfe2e6; +} + +.project-auth-panel-head i { + width: 24px; + height: 24px; +} + +.project-auth-thread { + display: flex; + flex-direction: column; + gap: 14px; + padding: 28px 22px; +} + +.project-auth-thread b { + width: 62%; + height: 18px; +} + +.project-auth-thread span { + height: 12px; +} + +.project-auth-thread span:nth-child(2) { + width: 86%; +} + +.project-auth-thread span:nth-child(3) { + width: 72%; +} + +.project-auth-thread span:nth-child(4) { + width: 48%; +} + +.project-auth-composer { + margin: auto 18px 20px; + padding: 18px; + border: 1px solid #e3e5e9; + border-radius: 18px; + background: #fff; + box-shadow: 0 20px 60px rgba(17, 24, 39, 0.08); +} + +.project-auth-composer span { + width: 72%; + height: 14px; + margin-bottom: 42px; +} + +.project-auth-composer div { + justify-content: space-between; +} + +.project-auth-composer i { + width: 28px; + height: 28px; +} + +@media (max-width: 760px) { + .project-auth-main { + grid-template-columns: minmax(0, 1fr); + } + + .project-auth-panel { + display: none; + } + + .project-auth-title span { + width: 112px; + } + + .project-auth-frame.primary { + left: 14%; + top: 23%; + width: 190px; + height: 138px; + } + + .project-auth-frame.secondary { + left: 42%; + top: 48%; + width: 150px; + height: 108px; + } + + .project-auth-frame.tertiary { + display: none; + } + + .not-found-shell { + padding: 44px 22px; + } + + .not-found-mark { + margin-bottom: 44px; + } + + .not-found-content h1 { + font-size: 28px; + } + + .not-found-content p { + margin: 14px 0 28px; + font-size: 14px; + } + + .not-found-content button { + min-width: 120px; + height: 40px; + border-radius: 12px; + font-size: 15px; + } +} + .workspace-shell { width: 100vw; height: 100vh;