前端:移除不必要的动画样式,优化错误页面样式,增加窗口尺寸变化监听,保证状态栏能正确渲染
This commit is contained in:
parent
43e21e1d49
commit
b4ded09c61
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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: "我的页面",
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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",
|
"@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",
|
||||||
|
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 { 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;
|
||||||
|
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