-
+
{text}
|
-
+
抱歉,您访问的页面已经离家出走了
diff --git a/frontend/hooks/themeMode.tsx b/frontend/hooks/themeMode.tsx
index 6c4af42..ea10875 100644
--- a/frontend/hooks/themeMode.tsx
+++ b/frontend/hooks/themeMode.tsx
@@ -6,7 +6,6 @@ const THEME_KEY = "theme-preference";
export const ThemeModeToggle: React.FC = () => {
const [mounted, setMounted] = useState(false);
- const [visible, setVisible] = useState(true);
const [isDark, setIsDark] = useState(false);
useEffect(() => {
@@ -24,15 +23,7 @@ export const ThemeModeToggle: React.FC = () => {
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 = () => {
@@ -49,15 +40,13 @@ export const ThemeModeToggle: React.FC = () => {
);
diff --git a/frontend/hooks/tide.tsx b/frontend/hooks/tide.tsx
deleted file mode 100644
index 090d89c..0000000
--- a/frontend/hooks/tide.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-"use client";
-import React, { useEffect, useRef } from "react";
-
-const Tide: React.FC = () => {
- const svgRef = useRef
(null);
- const containerRef = useRef(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 (
-
-
-
- );
-};
-
-export default Tide;
diff --git a/frontend/package.json b/frontend/package.json
index b55053f..de9543c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -31,6 +31,7 @@
"@remix-run/dev": "^2.14.0",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
+ "@types/lodash": "^4.17.13",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
diff --git a/frontend/styles/echoes.css b/frontend/styles/echoes.css
new file mode 100644
index 0000000..022bd66
--- /dev/null
+++ b/frontend/styles/echoes.css
@@ -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;
+ }
+ }
+
\ No newline at end of file
diff --git a/frontend/themes/echoes/layout.tsx b/frontend/themes/echoes/layout.tsx
index 7c7c425..59ce81e 100644
--- a/frontend/themes/echoes/layout.tsx
+++ b/frontend/themes/echoes/layout.tsx
@@ -1,131 +1,231 @@
import { Layout } from "interface/layout";
import { ThemeModeToggle } from "hooks/themeMode";
import { Echoes } from "hooks/echoes";
-import Tide from "hooks/tide";
-import {
- Container,
- Flex,
- Box,
- Link,
- TextField,
- DropdownMenu,
-} from "@radix-ui/themes";
+import { Container, Flex, Box, Link, TextField } from "@radix-ui/themes";
import {
MagnifyingGlassIcon,
HamburgerMenuIcon,
Cross1Icon,
PersonIcon,
- CheckIcon,
AvatarIcon,
} from "@radix-ui/react-icons";
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 [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 (
-
+
{/* 导航栏 */}
-
-
- {/* Logo 区域 */}
-
-
-
-
-
-
-
+
+ )}
+
+ {/* 移动端菜单 */}
+ {device === "mobile" && (
+
+
+
+
+
+
+
+
+
+ 首页
+ 首页
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* 主题切换按钮 */}
+
+
+
+
-
-
+
+
{/* 主要内容区域 */}
-
-
+
{children}
@@ -133,3 +233,5 @@ export default new Layout(({ children, args }) => {
);
});
+
+export default EchoesLayout;
diff --git a/frontend/themes/echoes/styles/layouts.css b/frontend/themes/echoes/styles/layouts.css
new file mode 100644
index 0000000..888d280
--- /dev/null
+++ b/frontend/themes/echoes/styles/layouts.css
@@ -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);
+}