fix(platform): 工作区体验 - 避免页面切换全屏加载并优化启动状态与积分摘要
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user