前端:移除不必要的动画样式,优化错误页面样式,增加窗口尺寸变化监听,保证状态栏能正确渲染
This commit is contained in:
parent
43e21e1d49
commit
b4ded09c61
@ -21,121 +21,3 @@ body {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import ErrorPage from "hooks/error";
|
||||
import Layout from "themes/echoes/layout";
|
||||
import layout from "themes/echoes/layout";
|
||||
|
||||
export default function Routes() {
|
||||
return Layout.render({
|
||||
return layout.element({
|
||||
children: <></>,
|
||||
args: {
|
||||
title: "我的页面",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import "styles/echoes.css"
|
||||
|
||||
export const Echoes: React.FC = () => {
|
||||
return (
|
||||
|
@ -20,18 +20,18 @@ const ErrorPage = () => {
|
||||
}, []);
|
||||
|
||||
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">
|
||||
<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}
|
||||
<span className="animate-pulse">|</span>
|
||||
</h1>
|
||||
<p className="text-custom-p-light dark:text-custom-p-dark text-xl">
|
||||
<p className="text-[--muted-foreground] text-xl">
|
||||
抱歉,您访问的页面已经离家出走了
|
||||
</p>
|
||||
<button
|
||||
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>
|
||||
|
@ -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 = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={toggleTheme}
|
||||
className={`p-2 rounded-lg transition-all duration-300 transform ${
|
||||
visible ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0"
|
||||
}`}
|
||||
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{isDark ? (
|
||||
<SunIcon width="24" height="24" />
|
||||
<SunIcon className="w-full h-full"/>
|
||||
) : (
|
||||
<MoonIcon width="24" height="24" />
|
||||
<MoonIcon className="w-full h-full" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
@ -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;
|
@ -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",
|
||||
|
72
frontend/styles/echoes.css
Normal file
72
frontend/styles/echoes.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<Theme
|
||||
grayColor="gray"
|
||||
accentColor="gray"
|
||||
radius="medium"
|
||||
radius="large"
|
||||
panelBackground="solid"
|
||||
>
|
||||
<Box className="min-h-screen flex flex-col">
|
||||
<Box className="min-h-screen flex flex-col" id="nav">
|
||||
{/* 导航栏 */}
|
||||
<Box
|
||||
asChild
|
||||
className="fixed top-0 w-full backdrop-blur-sm border-b border-[--gray-a5] z-50"
|
||||
id="nav"
|
||||
className="fixed top-0 w-full backdrop-blur-sm border-b border-[--gray-a5] z-60"
|
||||
>
|
||||
<Container size="4">
|
||||
<Flex justify="between" align="center" className="h-16 px-4">
|
||||
{/* Logo 区域 */}
|
||||
<Flex align="center">
|
||||
<Link href="/" className="flex items-center">
|
||||
<Box className="w-20 h-20">
|
||||
<Echoes />
|
||||
</Box>
|
||||
</Link>
|
||||
</Flex>
|
||||
<nav>
|
||||
<Container size="4">
|
||||
<Flex
|
||||
justify="between"
|
||||
align="center"
|
||||
className="h-16 px-4"
|
||||
>
|
||||
{/* Logo 区域 */}
|
||||
<Flex align="center">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center hover:opacity-80 transition-all"
|
||||
>
|
||||
<Box className="w-16 h-16">
|
||||
<Echoes />
|
||||
</Box>
|
||||
</Link>
|
||||
</Flex>
|
||||
|
||||
{/* 右侧导航链接 */}
|
||||
<Flex align="center" gap="5">
|
||||
{/* 桌面端搜索框和用户图标 */}
|
||||
<Box
|
||||
id="nav-desktop"
|
||||
className="hidden lg:flex items-center gap-5"
|
||||
{/* 右侧导航链接 */}
|
||||
<Flex
|
||||
align="center"
|
||||
gap="5"
|
||||
>
|
||||
<TextField.Root
|
||||
size="2"
|
||||
variant="surface"
|
||||
placeholder="搜索..."
|
||||
className="w-[200px]"
|
||||
>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--accent-a11]" />
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Box className="hover:opacity-70 transition-opacity p-2">
|
||||
{loginState ? (
|
||||
<AvatarIcon className="w-5 h-5 text-current opacity-70" />
|
||||
) : (
|
||||
<div>
|
||||
<PersonIcon className="w-5 h-5 text-current opacity-80" />
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</DropdownMenu.Trigger>
|
||||
</DropdownMenu.Root>
|
||||
</Box>
|
||||
|
||||
{/* 移动端菜单按钮和下拉搜索框 */}
|
||||
<Box id="nav-mobile" className="lg:hidden">
|
||||
<DropdownMenu.Root
|
||||
onOpenChange={() => setMoreState(!moreState)}
|
||||
>
|
||||
<DropdownMenu.Trigger>
|
||||
<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"
|
||||
className="mt-2 p-3 min-w-[250px]"
|
||||
>
|
||||
{/* 桌面端导航 */}
|
||||
{device === "desktop" && (
|
||||
<Box className="flex items-center gap-6">
|
||||
<TextField.Root
|
||||
size="2"
|
||||
variant="surface"
|
||||
placeholder="搜索..."
|
||||
className="w-[240px] [&_input]:pl-3"
|
||||
id="search"
|
||||
>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--accent-a11]" />
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
className="p-2"
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-a12]" />
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Box>
|
||||
|
||||
{/* 主题切换按钮 */}
|
||||
<Box>
|
||||
<ThemeModeToggle />
|
||||
</Box>
|
||||
<Box className="flex items-center gap-6">
|
||||
<a href="h">首页</a>
|
||||
</Box>
|
||||
|
||||
<DropdownMenuPrimitive.Root>
|
||||
<DropdownMenuPrimitive.Trigger asChild>
|
||||
<button className="hover:opacity-70 transition-opacity cursor-pointer">
|
||||
{loginState ? (
|
||||
<AvatarIcon className="w-6 h-6 text-[--gray-a12]" />
|
||||
) : (
|
||||
<PersonIcon className="w-6 h-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-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
|
||||
size="2"
|
||||
variant="surface"
|
||||
placeholder="搜索..."
|
||||
className="w-full [&_input]:pl-3"
|
||||
>
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
className="p-2"
|
||||
>
|
||||
<MagnifyingGlassIcon
|
||||
className="h-4 w-4 text-[--gray-a12]"
|
||||
id="search"
|
||||
/>
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Box>
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</Theme>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
</DropdownMenuPrimitive.Root>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 主题切换按钮 */}
|
||||
<Box className="w-6 h-6 flex items-center justify-center">
|
||||
<ThemeModeToggle />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Container>
|
||||
</nav>
|
||||
</Box>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<Box className="flex-1 w-full mt-16">
|
||||
<Container size="4" className="py-8">
|
||||
<Tide />
|
||||
<Container
|
||||
size="4"
|
||||
className="py-8"
|
||||
>
|
||||
<main>{children}</main>
|
||||
</Container>
|
||||
</Box>
|
||||
@ -133,3 +233,5 @@ export default new Layout(({ children, args }) => {
|
||||
</Theme>
|
||||
);
|
||||
});
|
||||
|
||||
export default EchoesLayout;
|
||||
|
33
frontend/themes/echoes/styles/layouts.css
Normal file
33
frontend/themes/echoes/styles/layouts.css
Normal 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user