echoes/frontend/themes/echoes/layout.tsx

368 lines
14 KiB
TypeScript

import { Layout } from "interface/layout";
import { ThemeModeToggle } from "hooks/ThemeMode";
import { Echoes } from "hooks/Echoes";
import { Container, Flex, Box, Link, TextField, Button } from "@radix-ui/themes";
import {
MagnifyingGlassIcon,
HamburgerMenuIcon,
Cross1Icon,
PersonIcon,
AvatarIcon,
} from "@radix-ui/react-icons";
import { Theme } from "@radix-ui/themes";
import { useState, useEffect, useCallback } from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import throttle from "lodash/throttle";
import "./styles/layouts.css";
import parse from 'html-react-parser';
// 直接导出 Layout 实例
export default new Layout(({ children, args }) => {
const [moreState, setMoreState] = useState(() => {
if (typeof window !== 'undefined') {
return window.innerWidth >= 1024 ? false : false;
}
return false;
});
const [loginState, setLoginState] = useState(true);
const [scrollProgress, setScrollProgress] = useState(0);
const handleScroll = useCallback(() => {
const container = document.querySelector('#main-content');
if (!container) return;
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
const scrolled = (scrollTop / (scrollHeight - clientHeight)) * 100;
setScrollProgress(Math.min(scrolled, 100));
}, []);
useEffect(() => {
if (typeof window === 'undefined') return;
const container = document.querySelector('#main-content');
if (container) {
container.addEventListener('scroll', handleScroll);
}
const throttledResize = throttle(() => {
requestAnimationFrame(() => {
if (window.innerWidth >= 1024) {
setMoreState(false);
}
});
}, 200);
window.addEventListener("resize", throttledResize);
return () => {
if (container) {
container.removeEventListener('scroll', handleScroll);
}
window.removeEventListener("resize", throttledResize);
throttledResize.cancel();
};
}, [handleScroll]);
const navString = typeof args === 'object' && args && 'nav' in args ? args.nav as string : '';
// 添加回到顶部的处理函数
const scrollToTop = () => {
const container = document.querySelector('#main-content');
if (container) {
container.scrollTo({
top: 0,
behavior: 'smooth'
});
}
};
// 修改移动端菜单的渲染逻辑
const mobileMenu = (
<Box className="flex lg:hidden gap-2 items-center">
{/* 添加移动端进度指示器 */}
<Box
className={`w-10 h-10 flex items-center justify-center ${
scrollProgress > 0
? 'block'
: 'hidden'
}`}
>
<Button
variant="ghost"
className="w-10 h-10 p-0 text-[--gray-12] hover:text-[--accent-9] transition-colors flex items-center justify-center [&_text]:text-[--gray-12] [&_text:hover]:text-[--accent-9]"
onClick={scrollToTop}
>
<svg
className="w-6 h-6"
viewBox="0 0 100 100"
>
<text
x="50"
y="55"
className="progress-indicator font-bold transition-colors"
dominantBaseline="middle"
textAnchor="middle"
style={{
fontSize: '56px',
fill: 'currentColor'
}}
>
{Math.round(scrollProgress)}
</text>
</svg>
</Button>
</Box>
<Button
className="w-10 h-10 p-0 hover:text-[--accent-9] transition-colors flex items-center justify-center group bg-transparent border-0"
onClick={() => setMoreState(!moreState)}
>
{moreState ? (
<Cross1Icon className="h-5 w-5 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
) : (
<HamburgerMenuIcon className="h-5 w-5 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
)}
</Button>
{/* 移动端菜单内容 */}
{moreState && (
<div
className="absolute top-full right-4 w-[180px] mt-2 rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg
animate-in fade-in-0 zoom-in-95 slide-in-from-top-2
duration-200 z-[90]"
>
<Box className="flex flex-col">
{/* 导航链接区域 */}
<Box className="flex flex-col">
<Box className="flex flex-col [&>a]:px-4 [&>a]:py-2.5 [&>a]:text-[--gray-12] [&>a]:transition-colors [&>a:hover]:bg-[--gray-a3] [&>a]:text-lg [&>a]:text-center [&>a]:border-b [&>a]:border-[--gray-a5] [&>a:first-child]:rounded-t-md [&>a:last-child]:border-b-0">
{parse(navString)}
</Box>
</Box>
{/* 搜索框区域 */}
<Box className="p-4 border-t border-[--gray-a5]">
<TextField.Root
size="2"
variant="surface"
placeholder="搜索..."
className="w-full [&_input]:pl-3 hover:border-[--accent-9] border transition-colors group"
>
<TextField.Slot
side="right"
className="p-2"
>
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
</TextField.Slot>
</TextField.Root>
</Box>
{/* 用户操作区域 */}
<Box className="p-4 border-t border-[--gray-a5]">
<Flex gap="3" align="center">
{/* 用户信息/登录按钮 - 调整为 70% 宽度 */}
<Box className="w-[70%]">
<Button
variant="ghost"
className="w-full justify-start gap-2 text-[--gray-12] hover:text-[--accent-9] hover:bg-[--gray-a3] transition-colors"
>
{loginState ? (
<>
<AvatarIcon className="w-5 h-5" />
<span></span>
</>
) : (
<>
<PersonIcon className="w-5 h-5" />
<span>/</span>
</>
)}
</Button>
</Box>
{/* 主题切换按钮 - 调整为 30% 宽度 */}
<Box className="w-[30%] flex justify-end [&_button]:w-10 [&_button]:h-10 [&_svg]:w-5 [&_svg]:h-5 [&_button]:text-[--gray-12] [&_button:hover]:text-[--accent-9]">
<ThemeModeToggle />
</Box>
</Flex>
</Box>
</Box>
</div>
)}
</Box>
);
return (
<Theme
grayColor="gray"
accentColor="indigo"
radius="large"
panelBackground="solid"
>
<Box
className="h-screen flex flex-col overflow-hidden"
id="nav"
>
{/* 导航栏 */}
<Box
asChild
className="w-full backdrop-blur-sm border-b border-[--gray-a5] z-[100] sticky top-0"
>
<nav>
<Container size="4">
<Flex
justify="between"
align="center"
className="h-20 px-4"
>
{/* Logo 区域 */}
<Flex align="center">
<Link
href="/"
className="hover-text flex items-center"
>
<Box className="w-20 h-20 [&_path]:transition-all [&_path]:duration-200 group-hover:[&_path]:stroke-[--accent-9]">
<Echoes />
</Box>
</Link>
</Flex>
{/* 右侧导航链接 */}
<Flex align="center" gap="3">
{/* 桌面端导航 */}
<Box className="hidden lg:flex items-center gap-4">
{/* 导航链接 */}
<Box className="flex items-center gap-5 [&>a]:text-[--gray-12] [&>a]:text-lg [&>a]:transition-colors [&>a:hover]:text-[--accent-9]">
{parse(navString)}
</Box>
{/* 搜索框 */}
<TextField.Root
size="3"
variant="surface"
placeholder="搜索..."
className="w-[240px] [&_input]:pl-3 hover:border-[--accent-9] border transition-colors group mr-4"
id="search"
>
<TextField.Slot side="right" className="p-2">
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
</TextField.Slot>
</TextField.Root>
{/* 用户和主题切换区域 */}
<Box className="flex items-center border-l border-[--gray-a5] pl-6">
{/* 用户头像/登录按钮 */}
<Box className="flex items-center">
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild>
<Button
variant="ghost"
className="w-10 h-10 p-0 text-[--gray-12] hover:text-[--accent-9] transition-colors flex items-center justify-center"
>
{loginState ? (
<AvatarIcon className="w-6 h-6" />
) : (
<PersonIcon className="w-6 h-6" />
)}
</Button>
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
align="end"
sideOffset={10}
className="mt-3 p-1 min-w-[180px] rounded-md bg-[--gray-1] 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 text-[--gray-12]">
</DropdownMenuPrimitive.Item>
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded text-[--gray-12]">
</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 text-[--gray-12]">
退
</DropdownMenuPrimitive.Item>
</>
) : (
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded text-[--gray-12]">
/
</DropdownMenuPrimitive.Item>
)}
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
</Box>
{/* 主题切换和进度指示器容器 */}
<Box className="flex items-center gap-2 ml-4">
{/* 主题切换按钮 */}
<Box className="w-10 h-10 flex items-center justify-center [&_button]:w-10 [&_button]:h-10 [&_svg]:w-6 [&_svg]:h-6 [&_button]:text-[--gray-12] [&_button:hover]:text-[--accent-9]">
<ThemeModeToggle />
</Box>
{/* 读进度指示器 */}
<Box
className={`w-10 h-10 flex items-center justify-center ${
scrollProgress > 0
? 'block'
: 'hidden'
}`}
>
<Button
variant="ghost"
className="w-10 h-10 p-0 text-[--gray-12] hover:text-[--accent-9] transition-colors flex items-center justify-center [&_text]:text-[--gray-12] [&_text:hover]:text-[--accent-9]"
onClick={scrollToTop}
>
<svg
className="w-6 h-6"
viewBox="0 0 100 100"
>
<text
x="50"
y="55"
className="progress-indicator font-bold transition-colors"
dominantBaseline="middle"
textAnchor="middle"
style={{
fontSize: '56px',
fill: 'currentColor'
}}
>
{Math.round(scrollProgress)}
</text>
</svg>
</Button>
</Box>
</Box>
</Box>
</Box>
{/* 移动菜单按钮 */}
{mobileMenu}
</Flex>
</Flex>
</Container>
</nav>
</Box>
{/* 主要内容区域 */}
<Box
id="main-content"
className="flex-1 w-full overflow-auto"
>
<Container
size="4"
className="py-8"
>
<main>{children}</main>
</Container>
</Box>
</Box>
</Theme>
);
});