前端:移除不必要的动画样式,优化错误页面样式,增加窗口尺寸变化监听,保证状态栏能正确渲染

This commit is contained in:
lsy 2024-12-04 19:57:59 +08:00
parent 43e21e1d49
commit b4ded09c61
10 changed files with 308 additions and 467 deletions

View File

@ -21,121 +21,3 @@ body {
height: 100%; height: 100%;
} }
/* Logo 动画 */
.animated-text {
max-width: 100%;
height: auto;
}
.animated-text path {
fill: transparent;
stroke: currentColor;
stroke-width: 2;
stroke-dasharray: var(--path-length);
stroke-dashoffset: var(--path-length);
animation: logo-anim 15s cubic-bezier(0.4, 0, 0.2, 1) infinite;
transform-origin: center;
stroke-linecap: round;
stroke-linejoin: round;
}
@keyframes logo-anim {
0% {
stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
opacity: 0;
}
5% {
opacity: 1;
stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length);
}
50% {
stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
}
60%, 75% {
stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length);
fill: currentColor;
opacity: 1;
}
85% {
stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
opacity: 1;
}
95% {
stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
opacity: 1;
}
100% {
stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
opacity: 0;
}
}
/* 确保在暗色模式下的颜色正确 */
@media (prefers-color-scheme: dark) {
.animated-text path {
stroke: currentColor;
}
}
/* 先确保基本动画工作后再添加脉动效果 */
.root {
fill: none;
stroke: var(--accent-9);
stroke-width: 1px;
stroke-linecap: round;
opacity: 0;
stroke-dasharray: 50;
stroke-dashoffset: 50;
animation: rootGrow 0.8s ease-out forwards var(--delay);
}
@keyframes rootGrow {
0% {
opacity: 0;
stroke-dashoffset: 50;
stroke-width: 0.5px;
}
100% {
opacity: 0.6;
stroke-dashoffset: 0;
stroke-width: 1px;
}
}
@keyframes growPath {
0% {
stroke-dashoffset: 100%;
}
100% {
stroke-dashoffset: 0;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 0.6;
}
}

View File

@ -1,8 +1,8 @@
import ErrorPage from "hooks/error"; import ErrorPage from "hooks/error";
import Layout from "themes/echoes/layout"; import layout from "themes/echoes/layout";
export default function Routes() { export default function Routes() {
return Layout.render({ return layout.element({
children: <></>, children: <></>,
args: { args: {
title: "我的页面", title: "我的页面",

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import "styles/echoes.css"
export const Echoes: React.FC = () => { export const Echoes: React.FC = () => {
return ( return (

View File

@ -20,18 +20,18 @@ const ErrorPage = () => {
}, []); }, []);
return ( return (
<div className="min-h-screen flex items-center justify-center bg-custom-bg-light dark:bg-custom-bg-dark transition-colors duration-300"> <div className="min-h-screen flex items-center justify-center bg-[--background] transition-colors duration-300">
<div className="text-center"> <div className="text-center">
<h1 className="text-6xl font-bold text-custom-title-light dark:text-custom-title-dark mb-4"> <h1 className="text-6xl font-bold text-[--foreground] mb-4">
{text} {text}
<span className="animate-pulse">|</span> <span className="animate-pulse">|</span>
</h1> </h1>
<p className="text-custom-p-light dark:text-custom-p-dark text-xl"> <p className="text-[--muted-foreground] text-xl">
访 访
</p> </p>
<button <button
onClick={() => (window.location.href = "/")} onClick={() => (window.location.href = "/")}
className="mt-8 px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors duration-300" className="mt-8 px-6 py-3 bg-[--primary] hover:bg-[--primary-foreground] text-[--primary-foreground] hover:text-[--primary] rounded-lg transition-colors duration-300"
> >
</button> </button>

View File

@ -6,7 +6,6 @@ const THEME_KEY = "theme-preference";
export const ThemeModeToggle: React.FC = () => { export const ThemeModeToggle: React.FC = () => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [visible, setVisible] = useState(true);
const [isDark, setIsDark] = useState(false); const [isDark, setIsDark] = useState(false);
useEffect(() => { useEffect(() => {
@ -24,15 +23,7 @@ export const ThemeModeToggle: React.FC = () => {
document.documentElement.className = initialTheme; document.documentElement.className = initialTheme;
} }
let lastScroll = 0;
const handleScroll = () => {
const currentScroll = window.scrollY;
setVisible(currentScroll <= lastScroll || currentScroll < 50);
lastScroll = currentScroll;
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []); }, []);
const toggleTheme = () => { const toggleTheme = () => {
@ -49,15 +40,13 @@ export const ThemeModeToggle: React.FC = () => {
<Button <Button
variant="ghost" variant="ghost"
onClick={toggleTheme} onClick={toggleTheme}
className={`p-2 rounded-lg transition-all duration-300 transform ${ className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
visible ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0"
}`}
aria-label="Toggle theme" aria-label="Toggle theme"
> >
{isDark ? ( {isDark ? (
<SunIcon width="24" height="24" /> <SunIcon className="w-full h-full"/>
) : ( ) : (
<MoonIcon width="24" height="24" /> <MoonIcon className="w-full h-full" />
)} )}
</Button> </Button>
); );

View File

@ -1,239 +0,0 @@
"use client";
import React, { useEffect, useRef } from "react";
const Tide: React.FC = () => {
const svgRef = useRef<SVGSVGElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const dimensionsRef = useRef({ width: 1000, height: 800 });
const pathCountRef = useRef(0);
useEffect(() => {
const updateDimensions = () => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
dimensionsRef.current = {
width: rect.width,
height: rect.height,
};
if (svgRef.current) {
svgRef.current.setAttribute(
"viewBox",
`0 0 ${dimensionsRef.current.width} ${dimensionsRef.current.height}`,
);
}
};
const createLine = (
startX: number,
startY: number,
endX: number,
endY: number,
width: number,
alpha: number = 0.3,
animationDelay: number = 0,
) => {
if (!svgRef.current || pathCountRef.current > 500) return;
const path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path",
);
const midX = (startX + endX) / 2;
const midY = (startY + endY) / 2;
const controlX = midX + (Math.random() - 0.5) * 2;
const controlY = midY + (Math.random() - 0.5) * 2;
const d = `M ${startX} ${startY} Q ${controlX} ${controlY}, ${endX} ${endY}`;
path.setAttribute("d", d);
path.setAttribute("stroke", "var(--accent-9)");
path.setAttribute("stroke-width", "1");
path.setAttribute("stroke-linecap", "round");
path.setAttribute("fill", "none");
const length = path.getTotalLength();
path.style.strokeDasharray = `${length}`;
path.style.strokeDashoffset = `${length}`;
path.style.opacity = "0";
path.style.transition = `
stroke-dashoffset 0.8s ease-out ${animationDelay}s,
opacity 0.8s ease-out ${animationDelay}s
`;
svgRef.current.appendChild(path);
pathCountRef.current += 1;
setTimeout(() => {
path.style.strokeDashoffset = "0";
path.style.opacity = "0.6";
}, 10);
};
const createRoot = (
startX: number,
startY: number,
baseAngle: number,
length: number,
width: number,
depth: number,
animationDelay: number = 0,
) => {
if (depth <= 0 || !svgRef.current || pathCountRef.current > 600) return;
const endX = startX + Math.cos(baseAngle) * length;
const endY = startY - Math.sin(baseAngle) * length;
if (
endX < 0 ||
endX > dimensionsRef.current.width ||
endY < 0 ||
endY > dimensionsRef.current.height
)
return;
createLine(startX, startY, endX, endY, width, 0.6, animationDelay);
const growthDelay = 0.3;
const newDelay = animationDelay + growthDelay;
setTimeout(() => {
if (depth > 0) {
createRoot(
endX,
endY,
baseAngle + (Math.random() * 0.08 - 0.04),
length * 0.99,
width * 0.99,
depth - 1,
newDelay,
);
}
const branchProbability = depth > 20 ? 0.3 : 0.2;
if (depth > 5 && depth < 35 && Math.random() < branchProbability) {
const direction = Math.random() > 0.5 ? 1 : -1;
const branchAngle =
baseAngle +
direction * (Math.PI / 6 + (Math.random() * Math.PI) / 12);
setTimeout(() => {
createRoot(
endX,
endY,
branchAngle,
length * 0.85,
width * 0.85,
Math.floor(depth * 0.8),
newDelay + 0.2,
);
}, 150);
}
}, growthDelay * 1000);
};
const startGrowth = () => {
if (!svgRef.current) return;
svgRef.current.innerHTML = "";
pathCountRef.current = 0;
updateDimensions();
const { width, height } = dimensionsRef.current;
const edge = Math.floor(Math.random() * 4);
let startX, startY, baseAngle;
const margin = 50;
const randomPos = Math.random();
switch (edge) {
case 0:
startX = margin + (width - 2 * margin) * randomPos;
startY = 0;
baseAngle = Math.PI / 2;
break;
case 1:
startX = width;
startY = margin + (height - 2 * margin) * randomPos;
baseAngle = Math.PI;
break;
case 2:
startX = margin + (width - 2 * margin) * randomPos;
startY = height;
baseAngle = -Math.PI / 2;
break;
default:
startX = 0;
startY = margin + (height - 2 * margin) * randomPos;
baseAngle = 0;
break;
}
const angleVariation = Math.random() * 0.4 - 0.2;
const minDepth = 25;
const maxDepth = 45;
const depth =
minDepth + Math.floor(Math.random() * (maxDepth - minDepth));
const initialLength = 15 + Math.random() * 5;
const initialWidth = 0.8 + Math.random() * 0.4;
createRoot(
startX,
startY,
baseAngle + angleVariation,
initialLength,
initialWidth,
depth,
0,
);
};
if (typeof window !== "undefined") {
const resizeObserver = new ResizeObserver(() => {
updateDimensions();
startGrowth();
});
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
setTimeout(startGrowth, 100);
return () => resizeObserver.disconnect();
}
}, []);
return (
<div
ref={containerRef}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 999,
pointerEvents: "none",
background: "transparent",
}}
>
<svg
ref={svgRef}
width="100%"
height="100%"
style={{
display: "block",
width: "100%",
height: "100%",
}}
/>
</div>
);
};
export default Tide;

View File

@ -31,6 +31,7 @@
"@remix-run/dev": "^2.14.0", "@remix-run/dev": "^2.14.0",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/lodash": "^4.17.13",
"@types/react": "^18.2.20", "@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/eslint-plugin": "^6.7.4",

View File

@ -0,0 +1,72 @@
.animated-text {
max-width: 100%;
height: auto;
}
.animated-text path {
fill: transparent;
stroke: currentColor;
stroke-width: 2;
stroke-dasharray: var(--path-length);
stroke-dashoffset: var(--path-length);
animation: logo-anim 10s cubic-bezier(0.4, 0, 0.2, 1) infinite;
transform-origin: center;
stroke-linecap: round;
stroke-linejoin: round;
}
@keyframes logo-anim {
0% {
stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
opacity: 0;
}
5% {
opacity: 1;
stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length);
}
50% {
stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
}
60%, 75% {
stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length);
fill: currentColor;
opacity: 1;
}
85% {
stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
opacity: 1;
}
95% {
stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
opacity: 1;
}
100% {
stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent;
opacity: 0;
}
}
@media (prefers-color-scheme: dark) {
.animated-text path {
stroke: currentColor;
}
}

View File

@ -1,131 +1,231 @@
import { Layout } from "interface/layout"; import { Layout } from "interface/layout";
import { ThemeModeToggle } from "hooks/themeMode"; import { ThemeModeToggle } from "hooks/themeMode";
import { Echoes } from "hooks/echoes"; import { Echoes } from "hooks/echoes";
import Tide from "hooks/tide"; import { Container, Flex, Box, Link, TextField } from "@radix-ui/themes";
import {
Container,
Flex,
Box,
Link,
TextField,
DropdownMenu,
} from "@radix-ui/themes";
import { import {
MagnifyingGlassIcon, MagnifyingGlassIcon,
HamburgerMenuIcon, HamburgerMenuIcon,
Cross1Icon, Cross1Icon,
PersonIcon, PersonIcon,
CheckIcon,
AvatarIcon, AvatarIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { Theme } from "@radix-ui/themes"; import { Theme } from "@radix-ui/themes";
import { useState } from "react"; import { useState, useEffect } from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import throttle from "lodash/throttle";
import "./styles/layouts.css";
export default new Layout(({ children, args }) => { // 直接导出 Layout 实例
const EchoesLayout = new Layout(({ children, args }) => {
const [moreState, setMoreState] = useState(false); const [moreState, setMoreState] = useState(false);
const [loginState, setLoginState] = useState(false); const [loginState, setLoginState] = useState(true);
const [device, setDevice] = useState("");
// 添加窗口尺寸变化监听
useEffect(() => {
// 立即执行一次设备检测
if (window.innerWidth >= 1024) {
setDevice("desktop");
} else {
setDevice("mobile");
}
// 创建节流函数200ms 内只执行一次
const handleResize = throttle(() => {
if (window.innerWidth >= 1024) {
setDevice("desktop");
} else {
setDevice("mobile");
}
}, 200);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
handleResize.cancel();
};
}, []);
return ( return (
<Theme <Theme
grayColor="gray" grayColor="gray"
accentColor="gray" accentColor="gray"
radius="medium" radius="large"
panelBackground="solid" panelBackground="solid"
> >
<Box className="min-h-screen flex flex-col"> <Box className="min-h-screen flex flex-col" id="nav">
{/* 导航栏 */} {/* 导航栏 */}
<Box <Box
asChild asChild
className="fixed top-0 w-full backdrop-blur-sm border-b border-[--gray-a5] z-50" className="fixed top-0 w-full backdrop-blur-sm border-b border-[--gray-a5] z-60"
id="nav"
> >
<nav>
<Container size="4"> <Container size="4">
<Flex justify="between" align="center" className="h-16 px-4"> <Flex
justify="between"
align="center"
className="h-16 px-4"
>
{/* Logo 区域 */} {/* Logo 区域 */}
<Flex align="center"> <Flex align="center">
<Link href="/" className="flex items-center"> <Link
<Box className="w-20 h-20"> href="/"
className="flex items-center hover:opacity-80 transition-all"
>
<Box className="w-16 h-16">
<Echoes /> <Echoes />
</Box> </Box>
</Link> </Link>
</Flex> </Flex>
{/* 右侧导航链接 */} {/* 右侧导航链接 */}
<Flex align="center" gap="5"> <Flex
{/* 桌面端搜索框和用户图标 */} align="center"
<Box gap="5"
id="nav-desktop"
className="hidden lg:flex items-center gap-5"
> >
{/* 桌面端导航 */}
{device === "desktop" && (
<Box className="flex items-center gap-6">
<TextField.Root <TextField.Root
size="2" size="2"
variant="surface" variant="surface"
placeholder="搜索..." placeholder="搜索..."
className="w-[200px]" className="w-[240px] [&_input]:pl-3"
id="search"
> >
<TextField.Slot> <TextField.Slot
<MagnifyingGlassIcon className="h-4 w-4 text-[--accent-a11]" /> side="right"
className="p-2"
>
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-a12]" />
</TextField.Slot> </TextField.Slot>
</TextField.Root> </TextField.Root>
<DropdownMenu.Root> <Box className="flex items-center gap-6">
<DropdownMenu.Trigger> <a href="h"></a>
<Box className="hover:opacity-70 transition-opacity p-2"> </Box>
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild>
<button className="hover:opacity-70 transition-opacity cursor-pointer">
{loginState ? ( {loginState ? (
<AvatarIcon className="w-5 h-5 text-current opacity-70" /> <AvatarIcon className="w-6 h-6 text-[--gray-a12]" />
) : ( ) : (
<div> <PersonIcon className="w-6 h-6 text-[--gray-a12]" />
<PersonIcon className="w-5 h-5 text-current opacity-80" />
</div>
)} )}
</Box> </button>
</DropdownMenu.Trigger> </DropdownMenuPrimitive.Trigger>
</DropdownMenu.Root> <DropdownMenuPrimitive.Portal>
</Box> <Theme
grayColor="gray"
{/* 移动端菜单按钮和下拉搜索框 */} accentColor="gray"
<Box id="nav-mobile" className="lg:hidden"> radius="large"
<DropdownMenu.Root panelBackground="solid"
onOpenChange={() => setMoreState(!moreState)}
> >
<DropdownMenu.Trigger> <DropdownMenuPrimitive.Content
<Box className="hover:opacity-70 transition-opacity p-2">
{moreState ? (
<Cross1Icon className="h-5 w-5 text-[--accent-a11]" />
) : (
<HamburgerMenuIcon className="h-5 w-5 text-[--accent-a11]" />
)}
</Box>
</DropdownMenu.Trigger>
<DropdownMenu.Content
align="end" align="end"
className="mt-2 p-3 min-w-[250px]" sideOffset={5}
className="mt-2 p-1 min-w-[180px] rounded-md bg-[--color-background] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
> >
{loginState ? (
<>
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded">
</DropdownMenuPrimitive.Item>
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded">
</DropdownMenuPrimitive.Item>
<DropdownMenuPrimitive.Separator className="h-px bg-[--gray-a5] my-1" />
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded">
退
</DropdownMenuPrimitive.Item>
</>
) : (
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded">
/
</DropdownMenuPrimitive.Item>
)}
</DropdownMenuPrimitive.Content>
</Theme>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
</Box>
)}
{/* 移动端菜单 */}
{device === "mobile" && (
<Box className="flex gap-3">
<DropdownMenuPrimitive.Root
open={moreState}
onOpenChange={setMoreState}
>
<DropdownMenuPrimitive.Trigger asChild>
<button className="hover:opacity-70 transition-opacity p-2">
{moreState ? (
<Cross1Icon className="h-6 w-6 text-[--gray-a12]" />
) : (
<HamburgerMenuIcon className="h-6 w-6 text-[--gray-a12]" />
)}
</button>
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Portal>
<Theme
grayColor="gray"
accentColor="gray"
radius="large"
panelBackground="solid"
>
<DropdownMenuPrimitive.Content
align="end"
sideOffset={5}
className="mt-2 p-3 min-w-[280px] rounded-md bg-[--color-background] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
>
<Box className="flex flex-col gap-2">
<a href="h" ></a>
<a href="h"></a>
</Box>
<Box className="mt-3 pt-3 border-t border-[--gray-a5]">
<TextField.Root <TextField.Root
size="2" size="2"
variant="surface" variant="surface"
placeholder="搜索..." placeholder="搜索..."
className="w-full [&_input]:pl-3"
> >
<TextField.Slot> <TextField.Slot
<MagnifyingGlassIcon className="h-4 w-4 text-[--accent-a11]" /> side="right"
className="p-2"
>
<MagnifyingGlassIcon
className="h-4 w-4 text-[--gray-a12]"
id="search"
/>
</TextField.Slot> </TextField.Slot>
</TextField.Root> </TextField.Root>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Box> </Box>
</DropdownMenuPrimitive.Content>
</Theme>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
</Box>
)}
{/* 主题切换按钮 */} {/* 主题切换按钮 */}
<Box> <Box className="w-6 h-6 flex items-center justify-center">
<ThemeModeToggle /> <ThemeModeToggle />
</Box> </Box>
</Flex> </Flex>
</Flex> </Flex>
</Container> </Container>
</nav>
</Box> </Box>
{/* 主要内容区域 */} {/* 主要内容区域 */}
<Box className="flex-1 w-full mt-16"> <Box className="flex-1 w-full mt-16">
<Container size="4" className="py-8"> <Container
<Tide /> size="4"
className="py-8"
>
<main>{children}</main> <main>{children}</main>
</Container> </Container>
</Box> </Box>
@ -133,3 +233,5 @@ export default new Layout(({ children, args }) => {
</Theme> </Theme>
); );
}); });
export default EchoesLayout;

View File

@ -0,0 +1,33 @@
* {
color: var(--gray-a12);
}
#nav a {
position: relative;
transition: opacity 0.2s ease;
}
#nav a:hover {
opacity: 0.8;
}
#nav a::after {
content: "";
position: absolute;
left: 0;
bottom: -3px;
width: 100%;
height: 2px;
background-color: var(--gray-a11);
transform: scaleX(0);
transition: transform 0.3s ease;
}
#nav a:hover::after {
transform: scaleX(1);
}
#search {
color: var(--gray-a12);
}