fix(platform): 工作区体验 - 避免页面切换全屏加载并优化启动状态与积分摘要

This commit is contained in:
ddCat
2026-08-15 13:51:15 +08:00
parent b2a7eef35d
commit d3dda8998c
8 changed files with 397 additions and 133 deletions

View File

@@ -1,6 +1,5 @@
import { motion, useReducedMotion } from "motion/react";
import { useEffect, useState } from "react";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { cn } from "@/lib/utils";
type FullScreenLoaderProps = {
@@ -9,69 +8,91 @@ type FullScreenLoaderProps = {
className?: string;
};
export function FullScreenLoader({ label = "正在恢复创作空间", detail = "同步账号、模型和项目数据", className }: FullScreenLoaderProps) {
const reducedMotion = useReducedMotion();
export function FullScreenLoader({ label = "正在恢复工作区", detail = "同步账号、模型和项目数据", className }: FullScreenLoaderProps) {
return (
<motion.div
<div
data-full-screen-loader
role="status"
aria-live="polite"
aria-label={`${label},${detail}`}
initial={reducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: aceternityMotion.duration.state, ease: aceternityMotion.easing.enter }}
className={cn("fixed inset-0 z-[var(--loader-z)] grid min-h-dvh place-items-center overflow-hidden bg-[#f5f6f8] px-6 text-stone-950 dark:bg-[#0b0c0f] dark:text-white", className)}
className={cn("full-screen-loader", className)}
>
<div
aria-hidden
className="absolute inset-0 bg-[linear-gradient(rgba(15,23,42,.045)_1px,transparent_1px),linear-gradient(90deg,rgba(15,23,42,.045)_1px,transparent_1px)] bg-[size:44px_44px] dark:bg-[linear-gradient(rgba(255,255,255,.035)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.035)_1px,transparent_1px)]"
/>
<div aria-hidden className="absolute inset-0 overflow-hidden opacity-70">
{[0, 1, 2].map((index) => (
<motion.span
key={index}
className="absolute left-[-20%] h-px w-[140%] origin-center bg-current opacity-[0.08] dark:opacity-[0.12]"
style={{ top: `${30 + index * 20}%`, rotate: index % 2 ? -7 : 7 }}
animate={reducedMotion ? undefined : { x: ["-8%", "8%", "-8%"] }}
transition={{ duration: 4.8 + index * 0.8, repeat: Infinity, ease: "easeInOut", delay: index * -1.1 }}
/>
))}
</div>
<div className="full-screen-loader-shell">
<header className="full-screen-loader-topbar" aria-hidden="true">
<div className="full-screen-loader-topbar-group">
<span className="loader-placeholder loader-placeholder-control" />
<span className="loader-placeholder loader-placeholder-mark" />
<span className="loader-placeholder loader-placeholder-wordmark" />
</div>
<div className="full-screen-loader-topbar-group">
<span className="loader-placeholder loader-placeholder-control" />
<span className="loader-placeholder loader-placeholder-control" />
<span className="loader-placeholder loader-placeholder-control" />
</div>
</header>
<div className="relative flex w-full max-w-sm flex-col items-center text-center">
<div className="relative grid size-24 place-items-center">
<motion.span
aria-hidden
className="absolute inset-1 rounded-[28px] border border-cyan-500/28 dark:border-cyan-300/22"
animate={reducedMotion ? undefined : { rotate: 360 }}
transition={{ duration: 3.2, repeat: Infinity, ease: "linear" }}
style={{ clipPath: "polygon(0 0, 68% 0, 68% 2px, 0 2px, 0 100%)" }}
/>
<motion.span
aria-hidden
className="absolute inset-3 rounded-[var(--panel-radius-wide-tight)] border border-amber-500/30 dark:border-amber-300/22"
animate={reducedMotion ? undefined : { rotate: -360 }}
transition={{ duration: 4.1, repeat: Infinity, ease: "linear" }}
style={{ clipPath: "polygon(32% 0, 100% 0, 100% 100%, 98% 100%, 98% 2px, 32% 2px)" }}
/>
<motion.span
className="size-10 bg-current"
style={{ mask: "url(/logo.svg) center / contain no-repeat", WebkitMask: "url(/logo.svg) center / contain no-repeat" }}
animate={reducedMotion ? undefined : { opacity: [0.58, 1, 0.58], scale: [0.96, 1, 0.96] }}
transition={{ duration: 1.8, repeat: Infinity, ease: "easeInOut" }}
/>
</div>
<div className="full-screen-loader-main">
<aside className="full-screen-loader-rail" aria-hidden="true">
<span className="loader-placeholder loader-placeholder-rail-item" />
<span className="loader-placeholder loader-placeholder-rail-item" />
<span className="loader-placeholder loader-placeholder-rail-item" />
<span className="loader-placeholder loader-placeholder-rail-item" />
<span className="loader-placeholder loader-placeholder-rail-item" />
</aside>
<p className="mt-6 text-base font-semibold tracking-normal">{label}</p>
<p className="mt-2 text-sm text-stone-500 dark:text-white/42">{detail}</p>
<div aria-hidden className="relative mt-6 h-px w-44 overflow-hidden bg-stone-900/10 dark:bg-white/10">
<motion.span
className="absolute inset-y-0 left-0 w-16 bg-cyan-500 dark:bg-cyan-300"
animate={reducedMotion ? { x: 56 } : { x: [-64, 176] }}
transition={reducedMotion ? undefined : { duration: 1.15, repeat: Infinity, ease: aceternityMotion.easing.enter }}
/>
<main className="full-screen-loader-stage">
<div className="full-screen-loader-stage-header">
<div className="full-screen-loader-status">
<LoadingSignal />
<span>{label}</span>
</div>
<span className="loader-placeholder loader-placeholder-action" aria-hidden="true" />
</div>
<div className="full-screen-loader-stage-grid" aria-hidden="true">
<LoaderSurface />
<LoaderSurface />
<LoaderSurface />
<LoaderSurface />
<LoaderSurface />
<LoaderSurface />
<LoaderSurface />
<LoaderSurface />
</div>
</main>
</div>
</div>
</motion.div>
</div>
);
}
export function WorkspaceRouteLoader({ label = "正在打开页面" }: { label?: string }) {
const [visible, setVisible] = useState(false);
useEffect(() => {
const timer = window.setTimeout(() => setVisible(true), 140);
return () => window.clearTimeout(timer);
}, []);
return (
<section data-workspace-route-loader className={cn("workspace-route-loader", visible && "is-visible")} role="status" aria-live="polite" aria-label={label}>
<div className="workspace-route-loader-content">
<LoadingSignal />
<span>{label}</span>
</div>
</section>
);
}
function LoadingSignal() {
return <span className="loading-signal" aria-hidden="true" />;
}
function LoaderSurface() {
return (
<div className="full-screen-loader-surface">
<span className="loader-placeholder loader-placeholder-media" />
<span className="loader-placeholder loader-placeholder-title" />
<span className="loader-placeholder loader-placeholder-meta" />
</div>
);
}