前端:解决vite不能打包,优化加载图片,更换黑暗模式背景,优化文章页面
This commit is contained in:
parent
b3c0af91b8
commit
66d47f6cfa
259
frontend/app/dashboard/layout.tsx
Normal file
259
frontend/app/dashboard/layout.tsx
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import { Layout } from "interface/layout";
|
||||||
|
import { ThemeModeToggle } from "hooks/ThemeMode";
|
||||||
|
import { Container, Flex, Box, Link, Button } from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
HamburgerMenuIcon,
|
||||||
|
Cross1Icon,
|
||||||
|
PersonIcon,
|
||||||
|
ExitIcon,
|
||||||
|
DashboardIcon,
|
||||||
|
GearIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
ImageIcon,
|
||||||
|
ReaderIcon,
|
||||||
|
LayersIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { Theme } from "@radix-ui/themes";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import throttle from "lodash/throttle";
|
||||||
|
|
||||||
|
// 定义侧边栏菜单项
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
icon: <DashboardIcon className="w-4 h-4" />,
|
||||||
|
label: "仪表盘",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FileTextIcon className="w-4 h-4" />,
|
||||||
|
label: "文章管理",
|
||||||
|
path: "/dashboard/posts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ImageIcon className="w-4 h-4" />,
|
||||||
|
label: "媒体管理",
|
||||||
|
path: "/dashboard/media",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ReaderIcon className="w-4 h-4" />,
|
||||||
|
label: "评论管理",
|
||||||
|
path: "/dashboard/comments",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <LayersIcon className="w-4 h-4" />,
|
||||||
|
label: "分类管理",
|
||||||
|
path: "/dashboard/categories",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <GearIcon className="w-4 h-4" />,
|
||||||
|
label: "系统设置",
|
||||||
|
path: "/dashboard/settings",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Layout(({ children }) => {
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = throttle(() => {
|
||||||
|
if (window.innerWidth >= 1024) {
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
handleResize.cancel();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="min-h-screen">
|
||||||
|
<Theme
|
||||||
|
grayColor="gray"
|
||||||
|
accentColor="indigo"
|
||||||
|
radius="large"
|
||||||
|
panelBackground="solid"
|
||||||
|
>
|
||||||
|
<Box className="flex h-screen">
|
||||||
|
{/* 侧边栏 */}
|
||||||
|
<Box
|
||||||
|
className={`
|
||||||
|
fixed lg:static h-full
|
||||||
|
transform lg:transform-none transition-transform duration-300
|
||||||
|
${mobileMenuOpen ? "translate-x-0" : "-translate-x-full"}
|
||||||
|
${sidebarCollapsed ? "lg:w-20" : "lg:w-64"}
|
||||||
|
bg-[--gray-1] border-r border-[--gray-6]
|
||||||
|
flex flex-col z-30
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Logo区域 */}
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="between"
|
||||||
|
className="h-16 px-4 border-b border-[--gray-6]"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/dashboard"
|
||||||
|
className={`flex items-center gap-2 transition-all ${
|
||||||
|
sidebarCollapsed ? "lg:justify-center" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Box className="w-8 h-8 rounded-lg bg-[--accent-9] flex items-center justify-center">
|
||||||
|
<span className="text-white font-bold">A</span>
|
||||||
|
</Box>
|
||||||
|
<span
|
||||||
|
className={`text-[--gray-12] font-medium ${
|
||||||
|
sidebarCollapsed ? "lg:hidden" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
后台管理
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 菜单列表区域添加滚动 */}
|
||||||
|
<Box className="flex-1 overflow-y-auto">
|
||||||
|
<Box className="py-4">
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
href={item.path}
|
||||||
|
className={`
|
||||||
|
flex items-center gap-3 px-4 py-2.5 mx-2 rounded-md
|
||||||
|
text-[--gray-11] hover:text-[--gray-12]
|
||||||
|
hover:bg-[--gray-3] transition-colors
|
||||||
|
${sidebarCollapsed ? "lg:justify-center" : ""}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<span className={sidebarCollapsed ? "lg:hidden" : ""}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 主内容区域 */}
|
||||||
|
<Box className="flex-1 flex flex-col lg:ml-0 w-full relative">
|
||||||
|
{/* 顶部导航栏 */}
|
||||||
|
<Box
|
||||||
|
className={`
|
||||||
|
h-16 border-b border-[--gray-6] bg-[--gray-1]
|
||||||
|
sticky top-0 z-20 w-full
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
justify="between"
|
||||||
|
align="center"
|
||||||
|
className="h-full px-4 lg:px-6"
|
||||||
|
>
|
||||||
|
{/* 左侧菜单按钮 */}
|
||||||
|
<Flex gap="4" align="center">
|
||||||
|
{mobileMenuOpen ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="3"
|
||||||
|
className="lg:hidden text-base"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<Cross1Icon className="w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="3"
|
||||||
|
className="lg:hidden text-base"
|
||||||
|
onClick={() => setMobileMenuOpen(true)}
|
||||||
|
>
|
||||||
|
<HamburgerMenuIcon className="w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="3"
|
||||||
|
className="hidden lg:flex items-center text-base"
|
||||||
|
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||||
|
>
|
||||||
|
<HamburgerMenuIcon className="w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 右侧用户菜单 */}
|
||||||
|
<Flex align="center" gap="4">
|
||||||
|
<Box className="flex items-center border-r border-[--gray-6] pr-4">
|
||||||
|
<ThemeModeToggle />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Root>
|
||||||
|
<DropdownMenuPrimitive.Trigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="3"
|
||||||
|
className="gap-2 text-base"
|
||||||
|
>
|
||||||
|
<PersonIcon className="w-5 h-5" />
|
||||||
|
<span className="hidden sm:inline">管理员</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuPrimitive.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
align="end"
|
||||||
|
sideOffset={5}
|
||||||
|
className="min-w-[180px] p-1 rounded-md bg-[--gray-1] border border-[--gray-6] shadow-lg"
|
||||||
|
>
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-[--gray-12] hover:bg-[--gray-3] rounded outline-none cursor-pointer text-base"
|
||||||
|
>
|
||||||
|
<GearIcon className="w-5 h-5" />
|
||||||
|
个人设置
|
||||||
|
</DropdownMenuPrimitive.Item>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Separator className="h-px my-1 bg-[--gray-6]" />
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-[--gray-12] hover:bg-[--gray-3] rounded outline-none cursor-pointer text-base"
|
||||||
|
>
|
||||||
|
<ExitIcon className="w-5 h-5" />
|
||||||
|
退出登录
|
||||||
|
</DropdownMenuPrimitive.Item>
|
||||||
|
</DropdownMenuPrimitive.Content>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
</DropdownMenuPrimitive.Root>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 页面内容区域 */}
|
||||||
|
<Box
|
||||||
|
id="main-content"
|
||||||
|
className="flex-1 overflow-y-auto bg-[--gray-2]"
|
||||||
|
>
|
||||||
|
<Container
|
||||||
|
size="4"
|
||||||
|
className="py-6 px-4"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 移动端菜单遮罩 */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<Box
|
||||||
|
className="fixed inset-0 bg-black/50 z-20 lg:hidden"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Theme>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
171
frontend/app/dashboard/login.tsx
Normal file
171
frontend/app/dashboard/login.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import "./styles/login.css";
|
||||||
|
import { Template } from "interface/template";
|
||||||
|
import { Container, Heading, Text, Box, Flex, Button } from "@radix-ui/themes";
|
||||||
|
import { PersonIcon, LockClosedIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { gsap } from "gsap";
|
||||||
|
import { AnimatedBackground } from 'hooks/Background';
|
||||||
|
import { useThemeMode, ThemeModeToggle } from 'hooks/ThemeMode';
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { mode } = useThemeMode();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
const ctx = gsap.context(() => {
|
||||||
|
// 登录框动画
|
||||||
|
gsap.from(".login-box", {
|
||||||
|
y: 30,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 1,
|
||||||
|
ease: "power3.out",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单元素动画
|
||||||
|
gsap.from(".form-element", {
|
||||||
|
x: -20,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.8,
|
||||||
|
stagger: 0.1,
|
||||||
|
ease: "power2.out",
|
||||||
|
delay: 0.3,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按钮动画
|
||||||
|
gsap.from(".login-button", {
|
||||||
|
scale: 0.9,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "back.out(1.7)",
|
||||||
|
delay: 0.8,
|
||||||
|
});
|
||||||
|
}, containerRef);
|
||||||
|
|
||||||
|
return () => ctx.revert();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里添加登录逻辑
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500)); // 模拟API请求
|
||||||
|
|
||||||
|
// 登录成功后的处理
|
||||||
|
console.log("Login successful");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login failed:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatedBackground />
|
||||||
|
<Box
|
||||||
|
className="fixed top-4 right-4 z-10 w-10 h-10 flex items-center justify-center [&_button]:w-10 [&_button]:h-10 [&_svg]:w-6 [&_svg]:h-6"
|
||||||
|
style={{
|
||||||
|
'--button-color': 'var(--gray-12)',
|
||||||
|
'--button-hover-color': 'var(--accent-9)'
|
||||||
|
} as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<ThemeModeToggle />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
ref={containerRef}
|
||||||
|
className={`h-screen w-full flex items-center justify-center transition-all duration-300 ${
|
||||||
|
isVisible ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Box className="w-full max-w-md mx-auto px-4">
|
||||||
|
<Box
|
||||||
|
className="login-box backdrop-blur-sm rounded-lg shadow-lg p-8 border transition-colors duration-300"
|
||||||
|
style={{
|
||||||
|
backgroundColor: mode === 'dark' ? 'var(--gray-2-alpha-80)' : 'var(--white-alpha-80)',
|
||||||
|
borderColor: 'var(--gray-6)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Logo */}
|
||||||
|
<Flex direction="column" align="center" className="mb-8">
|
||||||
|
<Heading size="6" className="text-center mb-2">
|
||||||
|
后台
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 登录表单 */}
|
||||||
|
<form onSubmit={handleLogin}>
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
{/* 用户名输入框 */}
|
||||||
|
<Box className="form-element relative">
|
||||||
|
<PersonIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4"
|
||||||
|
style={{ color: 'var(--gray-11)' }} />
|
||||||
|
<input
|
||||||
|
className="login-input pl-10"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 密码输入框 */}
|
||||||
|
<Box className="form-element relative">
|
||||||
|
<LockClosedIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4"
|
||||||
|
style={{ color: 'var(--gray-11)' }} />
|
||||||
|
<input
|
||||||
|
className="login-input pl-10"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 登录按钮 */}
|
||||||
|
<Button
|
||||||
|
className="login-button w-full h-10 transition-colors duration-300"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--accent-9)',
|
||||||
|
color: 'white',
|
||||||
|
'--hover-bg': 'var(--accent-10)'
|
||||||
|
} as React.CSSProperties}
|
||||||
|
size="3"
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "登录中..." : "登录"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 其他选项 */}
|
||||||
|
<Flex justify="center" className="form-element">
|
||||||
|
<Text
|
||||||
|
size="2"
|
||||||
|
className="cursor-pointer transition-colors duration-300"
|
||||||
|
style={{
|
||||||
|
color: 'var(--gray-11)',
|
||||||
|
'--hover-color': 'var(--accent-9)'
|
||||||
|
} as React.CSSProperties}
|
||||||
|
>
|
||||||
|
忘记密码?
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
64
frontend/app/dashboard/styles/login.css
Normal file
64
frontend/app/dashboard/styles/login.css
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
.login-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--gray-6);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
color: var(--gray-12);
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 黑暗模式下的输入框样式 */
|
||||||
|
:root[class~="dark"] .login-input {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-color: var(--gray-7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input:hover {
|
||||||
|
border-color: var(--gray-7);
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[class~="dark"] .login-input:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-color: var(--gray-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input:focus {
|
||||||
|
border-color: var(--accent-8);
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[class~="dark"] .login-input:focus {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-color: var(--accent-8);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input::placeholder {
|
||||||
|
color: var(--gray-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[class~="dark"] .login-input::placeholder {
|
||||||
|
color: var(--gray-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
background-color: var(--accent-9);
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
background-color: var(--accent-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
@ -5,6 +5,8 @@ import about from "themes/echoes/about";
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import post from "themes/echoes/post";
|
import post from "themes/echoes/post";
|
||||||
import { memo, useCallback } from "react";
|
import { memo, useCallback } from "react";
|
||||||
|
import login from "~/dashboard/login";
|
||||||
|
import adminLayout from "~/dashboard/layout";
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
title: "我的页面",
|
title: "我的页面",
|
||||||
@ -12,13 +14,20 @@ const args = {
|
|||||||
nav: '<a href="/">index</a><a href="/error">error</a><a href="/about">about</a><a href="/post">post</a>',
|
nav: '<a href="/">index</a><a href="/error">error</a><a href="/about">about</a><a href="/post">post</a>',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const renderLayout = (children: React.ReactNode) => {
|
// 创建布局渲染器的工厂函数
|
||||||
return layout.render({
|
const createLayoutRenderer = (layoutComponent: any) => {
|
||||||
|
return (children: React.ReactNode) => {
|
||||||
|
return layoutComponent.render({
|
||||||
children,
|
children,
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 使用工厂函数创建不同的布局渲染器
|
||||||
|
const renderLayout = createLayoutRenderer(layout);
|
||||||
|
const renderAdminLayout = createLayoutRenderer(adminLayout);
|
||||||
|
|
||||||
const Routes = memo(() => {
|
const Routes = memo(() => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const path = location.pathname.split("/")[1];
|
const path = location.pathname.split("/")[1];
|
||||||
@ -28,6 +37,11 @@ const Routes = memo(() => {
|
|||||||
return renderLayout(Component.render(args));
|
return renderLayout(Component.render(args));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 添加管理后台内容渲染函数
|
||||||
|
const renderAdminContent = useCallback((Component: any) => {
|
||||||
|
return renderAdminLayout(Component.render(args));
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 根据路径返回对应组件
|
// 根据路径返回对应组件
|
||||||
if (path === "error") {
|
if (path === "error") {
|
||||||
return renderContent(ErrorPage);
|
return renderContent(ErrorPage);
|
||||||
@ -41,6 +55,37 @@ const Routes = memo(() => {
|
|||||||
return renderContent(post);
|
return renderContent(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path === "login") {
|
||||||
|
return login.render(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加管理后台路由判断
|
||||||
|
if (path === "admin") {
|
||||||
|
// 这里可以根据实际需要添加不同的管理页面组件
|
||||||
|
const subPath = location.pathname.split("/")[2];
|
||||||
|
|
||||||
|
// 如果没有子路径,显示仪表盘
|
||||||
|
if (!subPath) {
|
||||||
|
return renderAdminLayout(<div>仪表盘内容</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据子路径返回对应的管理页面
|
||||||
|
switch (subPath) {
|
||||||
|
case "posts":
|
||||||
|
return renderAdminLayout(<div>文章管理</div>);
|
||||||
|
case "media":
|
||||||
|
return renderAdminLayout(<div>媒体管理</div>);
|
||||||
|
case "comments":
|
||||||
|
return renderAdminLayout(<div>评论管理</div>);
|
||||||
|
case "categories":
|
||||||
|
return renderAdminLayout(<div>分类管理</div>);
|
||||||
|
case "settings":
|
||||||
|
return renderAdminLayout(<div>系统设置</div>);
|
||||||
|
default:
|
||||||
|
return renderAdminLayout(<div>404 未找到页面</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return renderContent(article);
|
return renderContent(article);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
95
frontend/hooks/Background.tsx
Normal file
95
frontend/hooks/Background.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useThemeMode } from 'hooks/ThemeMode';
|
||||||
|
|
||||||
|
export const AnimatedBackground = () => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const { mode } = useThemeMode();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const canvasElement = canvas!;
|
||||||
|
|
||||||
|
// 生成随机HSL颜色
|
||||||
|
const getRandomHSLColor = () => {
|
||||||
|
const hue = Math.random() * 360;
|
||||||
|
const saturation = 70 + Math.random() * 30;
|
||||||
|
const lightness = mode === 'dark'
|
||||||
|
? 40 + Math.random() * 20
|
||||||
|
: 60 + Math.random() * 20;
|
||||||
|
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ballColor = getRandomHSLColor();
|
||||||
|
let ballRadius = 100;
|
||||||
|
let x = canvas.width / 2;
|
||||||
|
let y = canvas.height - 200;
|
||||||
|
let dx = 0.2;
|
||||||
|
let dy = -0.2;
|
||||||
|
|
||||||
|
// 设置canvas尺寸为窗口大小
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
// 保存调整前的相对位置
|
||||||
|
const relativeX = x / canvas.width;
|
||||||
|
const relativeY = y / canvas.height;
|
||||||
|
|
||||||
|
// 更新canvas尺寸
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
|
||||||
|
// 根据新尺寸更新球的位置
|
||||||
|
x = canvas.width * relativeX;
|
||||||
|
y = canvas.height * relativeY;
|
||||||
|
|
||||||
|
// 立即重绘
|
||||||
|
drawBall();
|
||||||
|
};
|
||||||
|
|
||||||
|
function drawBall() {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = ballColor;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
||||||
|
drawBall();
|
||||||
|
|
||||||
|
if (x + dx > canvasElement.width - ballRadius || x + dx < ballRadius) {
|
||||||
|
dx = -dx;
|
||||||
|
}
|
||||||
|
if (y + dy > canvasElement.height - ballRadius || y + dy < ballRadius) {
|
||||||
|
dy = -dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
x += dx;
|
||||||
|
y += dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
const interval = setInterval(draw, 10);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
window.removeEventListener('resize', resizeCanvas);
|
||||||
|
};
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 -z-10">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="w-full h-full"
|
||||||
|
style={{ filter: 'blur(150px)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -210,11 +210,6 @@ interface ParticleImageProps {
|
|||||||
|
|
||||||
// 修改 BG_CONFIG,添加尺寸配置
|
// 修改 BG_CONFIG,添加尺寸配置
|
||||||
const BG_CONFIG = {
|
const BG_CONFIG = {
|
||||||
colors: {
|
|
||||||
from: 'rgb(10,37,77)',
|
|
||||||
via: 'rgb(8,27,57)',
|
|
||||||
to: 'rgb(2,8,23)'
|
|
||||||
},
|
|
||||||
className: 'bg-gradient-to-br from-[rgb(248,250,252)] via-[rgb(241,245,249)] to-[rgb(236,241,247)] dark:from-[rgb(10,37,77)] dark:via-[rgb(8,27,57)] dark:to-[rgb(2,8,23)]'
|
className: 'bg-gradient-to-br from-[rgb(248,250,252)] via-[rgb(241,245,249)] to-[rgb(236,241,247)] dark:from-[rgb(10,37,77)] dark:via-[rgb(8,27,57)] dark:to-[rgb(2,8,23)]'
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -375,6 +370,12 @@ export const ParticleImage = ({
|
|||||||
const cleanup = useCallback(() => {
|
const cleanup = useCallback(() => {
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
|
// 检查是否应该跳过清理
|
||||||
|
if (sceneRef.current?.userData.isSmileComplete ||
|
||||||
|
sceneRef.current?.userData.isErrorComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 清理动画状态
|
// 清理动画状态
|
||||||
isAnimatingRef.current = false;
|
isAnimatingRef.current = false;
|
||||||
|
|
||||||
@ -394,43 +395,16 @@ export const ParticleImage = ({
|
|||||||
|
|
||||||
// 清理场景资源
|
// 清理场景资源
|
||||||
if (sceneRef.current) {
|
if (sceneRef.current) {
|
||||||
// 遍历场景中的所有对象
|
// 检查是否应该跳过清理
|
||||||
sceneRef.current.traverse((object) => {
|
if (!sceneRef.current.userData.isSmileComplete &&
|
||||||
if (object instanceof THREE.Points) {
|
!sceneRef.current.userData.isErrorComplete) {
|
||||||
const geometry = object.geometry;
|
cleanupResources(sceneRef.current);
|
||||||
const material = object.material as THREE.PointsMaterial;
|
|
||||||
|
|
||||||
// 清理几何体
|
|
||||||
if (geometry) {
|
|
||||||
// 清空缓冲区数据
|
|
||||||
if (geometry.attributes.position) {
|
|
||||||
geometry.attributes.position.array = new Float32Array(0);
|
|
||||||
}
|
|
||||||
if (geometry.attributes.color) {
|
|
||||||
geometry.attributes.color.array = new Float32Array(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除所有属性
|
|
||||||
geometry.deleteAttribute('position');
|
|
||||||
geometry.deleteAttribute('color');
|
|
||||||
geometry.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理材质
|
|
||||||
if (material) {
|
|
||||||
material.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 清空场景
|
|
||||||
while(sceneRef.current.children.length > 0) {
|
|
||||||
sceneRef.current.remove(sceneRef.current.children[0]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改渲染器清理逻辑
|
// 修改渲染器清理逻辑
|
||||||
if (rendererRef.current) {
|
if (rendererRef.current && !sceneRef.current?.userData.isSmileComplete &&
|
||||||
|
!sceneRef.current?.userData.isErrorComplete) {
|
||||||
const renderer = rendererRef.current;
|
const renderer = rendererRef.current;
|
||||||
|
|
||||||
// 确保在移除 DOM 元素前停止渲染
|
// 确保在移除 DOM 元素前停止渲染
|
||||||
@ -459,7 +433,8 @@ export const ParticleImage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 清理相机引用
|
// 清理相机引用
|
||||||
if (cameraRef.current) {
|
if (cameraRef.current && !sceneRef.current?.userData.isSmileComplete &&
|
||||||
|
!sceneRef.current?.userData.isErrorComplete) {
|
||||||
cameraRef.current = undefined;
|
cameraRef.current = undefined;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@ -475,7 +450,10 @@ export const ParticleImage = ({
|
|||||||
const updateParticles = useCallback((width: number, height: number) => {
|
const updateParticles = useCallback((width: number, height: number) => {
|
||||||
if (!sceneRef.current || isAnimatingRef.current || !isMountedRef.current) return;
|
if (!sceneRef.current || isAnimatingRef.current || !isMountedRef.current) return;
|
||||||
|
|
||||||
|
// 只有当src不为空时才执行cleanup
|
||||||
|
if(src !== '') {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
@ -500,8 +478,40 @@ export const ParticleImage = ({
|
|||||||
sceneRef.current.add(points);
|
sceneRef.current.add(points);
|
||||||
|
|
||||||
const positionAttribute = geometry.attributes.position as THREE.BufferAttribute;
|
const positionAttribute = geometry.attributes.position as THREE.BufferAttribute;
|
||||||
startAnimation(positionAttribute, particles, width, height);
|
|
||||||
}, [cleanup, startAnimation]);
|
// 记录完成的动画数量
|
||||||
|
let completedAnimations = 0;
|
||||||
|
const totalAnimations = particles.length;
|
||||||
|
|
||||||
|
particles.forEach((particle, i) => {
|
||||||
|
const i3 = i * 3;
|
||||||
|
const distanceToCenter = Math.sqrt(
|
||||||
|
Math.pow(particle.originalX, 2) +
|
||||||
|
Math.pow(particle.originalY, 2)
|
||||||
|
);
|
||||||
|
const maxDistance = Math.sqrt(Math.pow(width/2, 2) + Math.pow(height/2, 2));
|
||||||
|
const normalizedDistance = distanceToCenter / maxDistance;
|
||||||
|
|
||||||
|
gsap.to(positionAttribute.array, {
|
||||||
|
duration: 0.8,
|
||||||
|
delay: normalizedDistance * 0.6,
|
||||||
|
[i3]: particle.originalX,
|
||||||
|
[i3 + 1]: particle.originalY,
|
||||||
|
[i3 + 2]: 0,
|
||||||
|
ease: "sine.inOut",
|
||||||
|
onUpdate: () => {
|
||||||
|
positionAttribute.needsUpdate = true;
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
completedAnimations++;
|
||||||
|
// 当所有动画完成时设置标记
|
||||||
|
if (completedAnimations === totalAnimations && sceneRef.current) {
|
||||||
|
sceneRef.current.userData.isSmileComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [cleanup, src]);
|
||||||
|
|
||||||
// 将 resize 处理逻辑移到组件顶层
|
// 将 resize 处理逻辑移到组件顶层
|
||||||
const handleResize = useCallback(() => {
|
const handleResize = useCallback(() => {
|
||||||
@ -511,7 +521,7 @@ export const ParticleImage = ({
|
|||||||
const width = containerRef.current.offsetWidth;
|
const width = containerRef.current.offsetWidth;
|
||||||
const height = containerRef.current.offsetHeight;
|
const height = containerRef.current.offsetHeight;
|
||||||
|
|
||||||
// 更新相机视图
|
// 更新相机图
|
||||||
const camera = cameraRef.current;
|
const camera = cameraRef.current;
|
||||||
camera.left = width / -2;
|
camera.left = width / -2;
|
||||||
camera.right = width / 2;
|
camera.right = width / 2;
|
||||||
@ -563,8 +573,19 @@ export const ParticleImage = ({
|
|||||||
const renderer = new THREE.WebGLRenderer({
|
const renderer = new THREE.WebGLRenderer({
|
||||||
alpha: true,
|
alpha: true,
|
||||||
antialias: window.innerWidth > 768,
|
antialias: window.innerWidth > 768,
|
||||||
powerPreference: 'low-power'
|
powerPreference: 'low-power',
|
||||||
|
failIfMajorPerformanceCaveat: false,
|
||||||
|
canvas: document.createElement('canvas')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 在初始化渲染器后立即添加错误检查
|
||||||
|
if (!renderer.capabilities.isWebGL2) {
|
||||||
|
console.warn('WebGL2 not supported, falling back...');
|
||||||
|
renderer.dispose();
|
||||||
|
renderer.forceContextLoss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
renderer.setPixelRatio(Math.min(
|
renderer.setPixelRatio(Math.min(
|
||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
window.innerWidth <= 768 ? 2 : 3
|
window.innerWidth <= 768 ? 2 : 3
|
||||||
@ -572,12 +593,17 @@ export const ParticleImage = ({
|
|||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
rendererRef.current = renderer;
|
rendererRef.current = renderer;
|
||||||
|
|
||||||
// 确保容器仍然存在再添加渲染器
|
// 修改渲染器添加到DOM的部分
|
||||||
if (containerRef.current && isMountedRef.current) {
|
if (containerRef.current && isMountedRef.current && renderer.domElement) {
|
||||||
|
try {
|
||||||
containerRef.current.appendChild(renderer.domElement);
|
containerRef.current.appendChild(renderer.domElement);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to append renderer:', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否应该显示笑
|
// 检查是否应该显示笑脸
|
||||||
if (src === '') {
|
if (src === '') {
|
||||||
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
||||||
|
|
||||||
@ -599,10 +625,10 @@ export const ParticleImage = ({
|
|||||||
const points = new THREE.Points(geometry, material);
|
const points = new THREE.Points(geometry, material);
|
||||||
scene.add(points);
|
scene.add(points);
|
||||||
|
|
||||||
// 修改动画效果
|
// 添加这一行来获取position属性
|
||||||
const positionAttribute = geometry.attributes.position;
|
const positionAttribute = geometry.attributes.position as THREE.BufferAttribute;
|
||||||
|
|
||||||
// 算到中心的距离用于延迟
|
// 修改动画效果,添加完成回调
|
||||||
particles.forEach((particle, i) => {
|
particles.forEach((particle, i) => {
|
||||||
const i3 = i * 3;
|
const i3 = i * 3;
|
||||||
const distanceToCenter = Math.sqrt(
|
const distanceToCenter = Math.sqrt(
|
||||||
@ -621,6 +647,12 @@ export const ParticleImage = ({
|
|||||||
ease: "sine.inOut",
|
ease: "sine.inOut",
|
||||||
onUpdate: () => {
|
onUpdate: () => {
|
||||||
positionAttribute.needsUpdate = true;
|
positionAttribute.needsUpdate = true;
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
// 动画完成后设置标记,防止被清理
|
||||||
|
if(scene) {
|
||||||
|
scene.userData.isSmileComplete = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -699,6 +731,12 @@ export const ParticleImage = ({
|
|||||||
ease: "back.out(1.7)",
|
ease: "back.out(1.7)",
|
||||||
onUpdate: () => {
|
onUpdate: () => {
|
||||||
positionAttribute.needsUpdate = true;
|
positionAttribute.needsUpdate = true;
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
// 添加标记表示错误动画已完成
|
||||||
|
if(scene) {
|
||||||
|
scene.userData.isErrorComplete = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -785,7 +823,7 @@ export const ParticleImage = ({
|
|||||||
delay: normalizedDistance * 0.3
|
delay: normalizedDistance * 0.3
|
||||||
});
|
});
|
||||||
|
|
||||||
// 随机初始位置(根据距离调整范围)
|
// 随机初始位置(根据距离调范围)
|
||||||
const spread = 1 - normalizedDistance * 0.5; // 距离越远,始扩散越小
|
const spread = 1 - normalizedDistance * 0.5; // 距离越远,始扩散越小
|
||||||
positionArray.push(
|
positionArray.push(
|
||||||
(Math.random() - 0.5) * width * spread,
|
(Math.random() - 0.5) * width * spread,
|
||||||
@ -962,7 +1000,7 @@ export const ImageLoader = ({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [animationComplete, setAnimationComplete] = useState(false);
|
const [animationComplete, setAnimationComplete] = useState(false);
|
||||||
|
|
||||||
// 处理图片预加载
|
// 处理图片加载
|
||||||
const preloadImage = useCallback(() => {
|
const preloadImage = useCallback(() => {
|
||||||
if (!src || loadingRef.current) return;
|
if (!src || loadingRef.current) return;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import "styles/echoes.css";
|
import "styles/echoes.css";
|
||||||
|
|
||||||
export const Echoes: React.FC = () => {
|
export const Echoes: React.FC = () => {
|
||||||
|
@ -2,7 +2,6 @@ import { Template } from "interface/template";
|
|||||||
import { Container, Heading, Text, Flex, Card, Button, ScrollArea } from "@radix-ui/themes";
|
import { Container, Heading, Text, Flex, Card, Button, ScrollArea } from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
PersonIcon,
|
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
@ -20,7 +19,7 @@ const mockArticles: PostDisplay[] = [
|
|||||||
content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...",
|
content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...",
|
||||||
authorName: "张三",
|
authorName: "张三",
|
||||||
publishedAt: new Date("2024-03-15"),
|
publishedAt: new Date("2024-03-15"),
|
||||||
coverImage: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=500&auto=format",
|
coverImage: "https://avatars.githubusercontent.com/u/72159?v=4",
|
||||||
metaKeywords: "",
|
metaKeywords: "",
|
||||||
metaDescription: "",
|
metaDescription: "",
|
||||||
status: "published",
|
status: "published",
|
||||||
@ -42,7 +41,7 @@ const mockArticles: PostDisplay[] = [
|
|||||||
content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...",
|
content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...",
|
||||||
authorName: "李四",
|
authorName: "李四",
|
||||||
publishedAt: new Date("2024-03-14"),
|
publishedAt: new Date("2024-03-14"),
|
||||||
coverImage: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=500&auto=format",
|
coverImage: "",
|
||||||
metaKeywords: "",
|
metaKeywords: "",
|
||||||
metaDescription: "",
|
metaDescription: "",
|
||||||
status: "published",
|
status: "published",
|
||||||
@ -63,7 +62,7 @@ const mockArticles: PostDisplay[] = [
|
|||||||
content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
||||||
authorName: "王五",
|
authorName: "王五",
|
||||||
publishedAt: new Date("2024-03-13"),
|
publishedAt: new Date("2024-03-13"),
|
||||||
coverImage: "https://images.unsplash.com/photo-1592609931095-54a2168ae893?w=500&auto=format",
|
coverImage: "https://mages.unsplash.com/photo-1592609931095-54a2168ae893?w=500&auto=format",
|
||||||
metaKeywords: "",
|
metaKeywords: "",
|
||||||
metaDescription: "",
|
metaDescription: "",
|
||||||
status: "published",
|
status: "published",
|
||||||
@ -84,7 +83,7 @@ const mockArticles: PostDisplay[] = [
|
|||||||
content: "移动端开发中的各种适配问题及解决方案...",
|
content: "移动端开发中的各种适配问题及解决方案...",
|
||||||
authorName: "田六",
|
authorName: "田六",
|
||||||
publishedAt: new Date("2024-03-13"),
|
publishedAt: new Date("2024-03-13"),
|
||||||
coverImage: "https://images.unsplash.com/photo-1526498460520-4c246339dccb?w=500&auto=format",
|
coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format",
|
||||||
metaKeywords: "",
|
metaKeywords: "",
|
||||||
metaDescription: "",
|
metaDescription: "",
|
||||||
status: "published",
|
status: "published",
|
||||||
@ -173,7 +172,7 @@ const mockArticles: PostDisplay[] = [
|
|||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "性能监控" },
|
{ name: "性能监控" },
|
||||||
{ name: "懒加<EFBFBD><EFBFBD><EFBFBD>" },
|
{ name: "懒加载" },
|
||||||
{ name: "缓存策略" },
|
{ name: "缓存策略" },
|
||||||
{ name: "代码分割" }
|
{ name: "代码分割" }
|
||||||
]
|
]
|
||||||
|
@ -274,8 +274,25 @@ export default new Layout(({ children, args }) => {
|
|||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={5}
|
sideOffset={20}
|
||||||
className="mt-2 min-w-[280px] rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
|
className="min-w-[200px] rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg
|
||||||
|
data-[state=open]:animate-in
|
||||||
|
data-[state=closed]:animate-out
|
||||||
|
data-[state=closed]:fade-out-0
|
||||||
|
data-[state=open]:fade-in-0
|
||||||
|
data-[state=closed]:zoom-out-95
|
||||||
|
data-[state=open]:zoom-in-95
|
||||||
|
data-[state=closed]:slide-in-from-top-2
|
||||||
|
data-[state=left]:slide-in-from-right-2
|
||||||
|
data-[state=right]:slide-in-from-left-2
|
||||||
|
data-[state=top]:slide-in-from-bottom-2
|
||||||
|
duration-200
|
||||||
|
max-h-[calc(100vh-6rem)]
|
||||||
|
overflow-y-auto
|
||||||
|
z-50
|
||||||
|
absolute
|
||||||
|
top-full
|
||||||
|
right-0"
|
||||||
>
|
>
|
||||||
<Box className="flex flex-col">
|
<Box className="flex flex-col">
|
||||||
{/* 导航链接区域 */}
|
{/* 导航链接区域 */}
|
||||||
@ -305,8 +322,8 @@ export default new Layout(({ children, args }) => {
|
|||||||
{/* 用户操作区域 */}
|
{/* 用户操作区域 */}
|
||||||
<Box className="p-4 border-t border-[--gray-a5]">
|
<Box className="p-4 border-t border-[--gray-a5]">
|
||||||
<Flex gap="3" align="center">
|
<Flex gap="3" align="center">
|
||||||
{/* 用户信息/登录按钮 - 占据 55% 宽度 */}
|
{/* 用户信息/登录按钮 - 调整为 70% 宽度 */}
|
||||||
<Box className="w-[55%]">
|
<Box className="w-[70%]">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full justify-start gap-2 text-[--gray-12] hover:text-[--accent-9] hover:bg-[--gray-a3] transition-colors"
|
className="w-full justify-start gap-2 text-[--gray-12] hover:text-[--accent-9] hover:bg-[--gray-a3] transition-colors"
|
||||||
@ -325,8 +342,8 @@ export default new Layout(({ children, args }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 主题切换按钮 - 占据剩余空间 */}
|
{/* 主题切换按钮 - 调整为 30% 宽度 */}
|
||||||
<Box className="flex-1 flex justify-end [&_button]:w-10 [&_button]:h-10 [&_svg]:w-5 [&_svg]:h-5 [&_button]:text-[--gray-12] [&_button:hover]:text-[--accent-9]">
|
<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 />
|
<ThemeModeToggle />
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useState,useContext, useCallback, useRef, useEffect } from "react";
|
import React, { useMemo, useState, useCallback, useRef, useEffect } from "react";
|
||||||
import { Template } from "interface/template";
|
import { Template } from "interface/template";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
@ -19,6 +19,7 @@ import { getColorScheme } from "themes/echoes/utils/colorScheme";
|
|||||||
import MarkdownIt from 'markdown-it';
|
import MarkdownIt from 'markdown-it';
|
||||||
import { ComponentPropsWithoutRef } from 'react';
|
import { ComponentPropsWithoutRef } from 'react';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import type { Components } from "react-markdown";
|
||||||
|
|
||||||
// 示例文章数据
|
// 示例文章数据
|
||||||
const mockPost: PostDisplay = {
|
const mockPost: PostDisplay = {
|
||||||
@ -37,7 +38,7 @@ const mockPost: PostDisplay = {
|
|||||||
|
|
||||||
### 1.1 必备工具安装
|
### 1.1 必备工具安装
|
||||||
|
|
||||||
开发环境需要安装以下工具:
|
发环境需要安装以下工具:
|
||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# 安装 Node.js
|
# 安装 Node.js
|
||||||
@ -379,12 +380,16 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
const [activeId, setActiveId] = useState<string>("");
|
const [activeId, setActiveId] = useState<string>("");
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const [showToc, setShowToc] = useState(false);
|
const [showToc, setShowToc] = useState(false);
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(true);
|
||||||
const [headingIdsArrays, setHeadingIdsArrays] = useState<{[key: string]: string[]}>({});
|
const [headingIdsArrays, setHeadingIdsArrays] = useState<{[key: string]: string[]}>({});
|
||||||
const headingIds = useRef<string[]>([]); // 保持原有的 ref
|
const headingIds = useRef<string[]>([]); // 保持原有的 ref
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isClickScrolling = useRef(false);
|
const isClickScrolling = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
@ -437,8 +442,6 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
if (tocArray.length > 0) {
|
if (tocArray.length > 0) {
|
||||||
setActiveId(tocArray[0].id);
|
setActiveId(tocArray[0].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsMounted(true);
|
|
||||||
}, [mockPost.content, mockPost.id]);
|
}, [mockPost.content, mockPost.id]);
|
||||||
|
|
||||||
const components = useMemo(() => ({
|
const components = useMemo(() => ({
|
||||||
@ -509,21 +512,20 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
) : (
|
) : (
|
||||||
<div className="my-4 sm:my-6 mx-0 sm:mx-0">
|
<div className="my-4 sm:my-6">
|
||||||
{/* 标题栏 */}
|
<div className="flex justify-between items-center h-9 sm:h-10 px-4 sm:px-6
|
||||||
<div className="flex justify-between items-center h-9 sm:h-10 px-6
|
border-t border-x border-[--gray-6]
|
||||||
border-x border-t border-[--gray-6]
|
bg-[--gray-2] dark:bg-[--gray-2]
|
||||||
bg-white dark:bg-[--gray-1]
|
rounded-t-lg
|
||||||
rounded-t-none sm:rounded-t-lg"
|
mx-0"
|
||||||
>
|
>
|
||||||
<div className="text-sm text-[--gray-11] dark:text-[--gray-12] font-medium">{lang || "text"}</div>
|
<div className="text-sm text-[--gray-11] dark:text-[--gray-11] font-medium">{lang || "text"}</div>
|
||||||
<CopyButton code={String(children)} />
|
<CopyButton code={String(children)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 代码内容区域 */}
|
<div className="border border-[--gray-6] rounded-b-lg bg-white dark:bg-[--gray-1] mx-0">
|
||||||
<div className="overflow-x-auto border-x border-b border-[--gray-6] rounded-b-none sm:rounded-b-lg">
|
<div className="overflow-x-auto">
|
||||||
<div className="min-w-[640px]">
|
<div className="p-4 sm:p-6">
|
||||||
<div className="p-6 bg-[--gray-2] dark:bg-[--gray-3]">
|
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
language={lang || "text"}
|
language={lang || "text"}
|
||||||
style={{
|
style={{
|
||||||
@ -565,7 +567,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<div className="w-full my-4 sm:my-6 -mx-4 sm:mx-0 overflow-hidden">
|
<div className="w-full my-4 sm:my-6 -mx-4 sm:mx-0 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="min-w-[640px] sm:min-w-0">
|
<div className="min-w-[640px] sm:min-w-0">
|
||||||
<div className="border-x border-t sm:border-t border-[--gray-6] rounded-t-none sm:rounded-t-lg bg-white dark:bg-[--gray-1]">
|
<div className="border-x border-t border-b sm:border-t border-[--gray-6] rounded-none sm:rounded-lg bg-white dark:bg-[--gray-1]">
|
||||||
<table className="w-full border-collapse text-xs sm:text-sm" {...props}>
|
<table className="w-full border-collapse text-xs sm:text-sm" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
@ -602,39 +604,60 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (isClickScrolling.current) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
const container = document.querySelector("#main-content");
|
||||||
if (entry.isIntersecting) {
|
const contentBox = document.querySelector(".prose");
|
||||||
setActiveId(entry.target.id);
|
|
||||||
}
|
if (!container || !contentBox) return;
|
||||||
|
|
||||||
|
// 找出所有进入可视区域的标题
|
||||||
|
const intersectingEntries = entries.filter(entry => entry.isIntersecting);
|
||||||
|
|
||||||
|
if (intersectingEntries.length > 0) {
|
||||||
|
// 获取所有可见标题的位置信息
|
||||||
|
const visibleHeadings = intersectingEntries.map(entry => ({
|
||||||
|
id: entry.target.id,
|
||||||
|
top: entry.boundingClientRect.top
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 选择最靠近视口顶部的标题
|
||||||
|
const closestHeading = visibleHeadings.reduce((prev, current) => {
|
||||||
|
return Math.abs(current.top) < Math.abs(prev.top) ? current : prev;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setActiveId(closestHeading.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: containerRef.current,
|
root: document.querySelector("#main-content"),
|
||||||
rootMargin: '-80px 0px -80% 0px',
|
rootMargin: '-20px 0px -80% 0px',
|
||||||
threshold: 0.5
|
threshold: [0, 1]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
tocItems.forEach((item) => {
|
tocItems.forEach((item) => {
|
||||||
const element = document.getElementById(item.id);
|
const element = document.getElementById(item.id);
|
||||||
if (element) {
|
if (element) {
|
||||||
observer.observe(element);
|
observer.observe(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (isMounted) {
|
||||||
tocItems.forEach((item) => {
|
tocItems.forEach((item) => {
|
||||||
const element = document.getElementById(item.id);
|
const element = document.getElementById(item.id);
|
||||||
if (element) {
|
if (element) {
|
||||||
observer.unobserve(element);
|
observer.unobserve(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [tocItems]);
|
}, [tocItems, isMounted]);
|
||||||
|
|
||||||
// 修改点击<EFBFBD><EFBFBD>理函数
|
// 修改点击处理函数
|
||||||
const handleTocClick = useCallback((e: React.MouseEvent, itemId: string) => {
|
const handleTocClick = useCallback((e: React.MouseEvent, itemId: string) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const element = document.getElementById(itemId);
|
const element = document.getElementById(itemId);
|
||||||
@ -644,6 +667,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
if (element && container && contentBox) {
|
if (element && container && contentBox) {
|
||||||
isClickScrolling.current = true;
|
isClickScrolling.current = true;
|
||||||
|
|
||||||
|
// 计算元素相对于内容容器的偏移量
|
||||||
const elementRect = element.getBoundingClientRect();
|
const elementRect = element.getBoundingClientRect();
|
||||||
const contentBoxRect = contentBox.getBoundingClientRect();
|
const contentBoxRect = contentBox.getBoundingClientRect();
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
@ -662,8 +686,6 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
|
|
||||||
setActiveId(itemId);
|
|
||||||
|
|
||||||
// 滚动完成后重置标记
|
// 滚动完成后重置标记
|
||||||
const resetTimeout = setTimeout(() => {
|
const resetTimeout = setTimeout(() => {
|
||||||
isClickScrolling.current = false;
|
isClickScrolling.current = false;
|
||||||
@ -673,17 +695,19 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 修改<EFBFBD><EFBFBD>动端目录的渲染逻辑
|
// 修改移动端目录的渲染逻辑
|
||||||
const mobileMenu = isMounted && (
|
const mobileMenu = (
|
||||||
<>
|
<>
|
||||||
|
{isMounted && (
|
||||||
<Button
|
<Button
|
||||||
className="lg:hidden fixed bottom-6 right-6 z-50 w-12 h-12 rounded-full shadow-lg bg-[--accent-9] text-white"
|
className="lg:hidden fixed bottom-6 right-6 z-50 w-12 h-12 rounded-full shadow-lg bg-[--accent-9] text-white"
|
||||||
onClick={() => setShowToc(true)}
|
onClick={() => setShowToc(true)}
|
||||||
>
|
>
|
||||||
<CodeIcon className="w-5 h-5" />
|
<CodeIcon className="w-5 h-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{showToc && (
|
{isMounted && showToc && (
|
||||||
<div
|
<div
|
||||||
className="lg:hidden fixed inset-0 z-50 bg-black/50 transition-opacity duration-300"
|
className="lg:hidden fixed inset-0 z-50 bg-black/50 transition-opacity duration-300"
|
||||||
onClick={() => setShowToc(false)}
|
onClick={() => setShowToc(false)}
|
||||||
@ -761,11 +785,11 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
ref={containerRef} // 添加ref到最外层容器
|
ref={containerRef}
|
||||||
size={{initial: "2", sm: "3", md: "4"}}
|
size={{initial: "2", sm: "3", md: "4"}}
|
||||||
className="px-4 sm:px-6 md:px-8"
|
className="px-4 sm:px-6 md:px-8"
|
||||||
>
|
>
|
||||||
{mobileMenu}
|
{isMounted && mobileMenu}
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
className="relative flex-col lg:flex-row"
|
className="relative flex-col lg:flex-row"
|
||||||
@ -773,7 +797,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
>
|
>
|
||||||
{/* 文章主体 */}
|
{/* 文章主体 */}
|
||||||
<Box className="w-full lg:flex-1">
|
<Box className="w-full lg:flex-1">
|
||||||
<Box className="p-4 sm:p-6 md:p-8 bg-white dark:bg-[--gray-1] rounded-lg shadow-sm">
|
<Box className="p-4 sm:p-6 md:p-8">
|
||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<Box className="mb-4 sm:mb-8">
|
<Box className="mb-4 sm:mb-8">
|
||||||
<Heading
|
<Heading
|
||||||
@ -864,7 +888,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 修改片样式 */}
|
{/* 封面图片 */}
|
||||||
{mockPost.coverImage && (
|
{mockPost.coverImage && (
|
||||||
<Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg">
|
<Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg">
|
||||||
<img
|
<img
|
||||||
|
@ -1,8 +1,93 @@
|
|||||||
|
/* 基础变量 */
|
||||||
|
:root {
|
||||||
|
/* 明亮模式的基础颜色 */
|
||||||
|
--text-primary: var(--gray-12);
|
||||||
|
--text-secondary: var(--gray-11);
|
||||||
|
--text-tertiary: var(--gray-10);
|
||||||
|
|
||||||
|
/* 共用的尺寸 */
|
||||||
|
--scrollbar-size: 8px;
|
||||||
|
--border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题变量 */
|
||||||
|
:root[class~="dark"] {
|
||||||
|
/* Radix UI 暗色主题变量覆盖 */
|
||||||
|
--color-panel-solid: rgb(2, 6, 16);
|
||||||
|
--color-surface: rgb(2, 6, 16);
|
||||||
|
--color-background: rgb(2, 6, 16);
|
||||||
|
|
||||||
|
/* 覆盖灰度色板 */
|
||||||
|
--slate-1: rgb(2, 6, 16);
|
||||||
|
--slate-2: rgb(2, 6, 16);
|
||||||
|
--slate-3: rgb(2, 6, 16);
|
||||||
|
|
||||||
|
/* 覆盖 Radix 的颜色变量 */
|
||||||
|
--gray-1: rgb(2, 6, 16);
|
||||||
|
--gray-2: rgb(2, 6, 16);
|
||||||
|
--gray-3: rgb(2, 6, 16);
|
||||||
|
--gray-4: rgb(4, 10, 24);
|
||||||
|
--gray-5: rgb(5, 12, 28);
|
||||||
|
--gray-6: rgb(6, 14, 32);
|
||||||
|
|
||||||
|
/* 文本颜色 */
|
||||||
|
--gray-12: rgb(226, 232, 240);
|
||||||
|
--gray-11: rgb(203, 213, 225);
|
||||||
|
--gray-10: rgb(148, 163, 184);
|
||||||
|
|
||||||
|
/* 透明度变量 */
|
||||||
|
--gray-a1: rgba(226, 232, 240, 0.05);
|
||||||
|
--gray-a2: rgba(226, 232, 240, 0.08);
|
||||||
|
--gray-a3: rgba(226, 232, 240, 0.1);
|
||||||
|
--gray-a4: rgba(226, 232, 240, 0.12);
|
||||||
|
--gray-a5: rgba(226, 232, 240, 0.14);
|
||||||
|
--gray-a6: rgba(226, 232, 240, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题背景 */
|
||||||
|
body:has(:root[class~="dark"]) {
|
||||||
|
background: rgb(2, 6, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用卡片样式 */
|
||||||
|
.rt-Card,
|
||||||
|
[class*="rt-Card"] {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题卡片样式 */
|
||||||
|
html[class~="dark"] body .rt-Card,
|
||||||
|
html[class~="dark"] body [class*="rt-Card"] {
|
||||||
|
background-color: rgb(2, 6, 16);
|
||||||
|
border-color: rgba(148, 163, 184, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用交互元素样式 */
|
||||||
|
.rt-Button,
|
||||||
|
.rt-DialogContent,
|
||||||
|
.rt-DropdownMenuContent,
|
||||||
|
[class*="rt-Button"],
|
||||||
|
[class*="rt-DialogContent"],
|
||||||
|
[class*="rt-DropdownMenuContent"] {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题交互元素 */
|
||||||
|
html[class~="dark"] body .rt-Button,
|
||||||
|
html[class~="dark"] body .rt-DialogContent,
|
||||||
|
html[class~="dark"] body .rt-DropdownMenuContent,
|
||||||
|
html[class~="dark"] body [class*="rt-Button"],
|
||||||
|
html[class~="dark"] body [class*="rt-DialogContent"],
|
||||||
|
html[class~="dark"] body [class*="rt-DropdownMenuContent"] {
|
||||||
|
background-color: rgb(2, 6, 16);
|
||||||
|
border-color: rgba(148, 163, 184, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* 导航链接样式 */
|
/* 导航链接样式 */
|
||||||
#nav a {
|
#nav a {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
color: var(--gray-12);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav a:hover {
|
#nav a:hover {
|
||||||
@ -27,16 +112,12 @@
|
|||||||
|
|
||||||
/* 进度指示器动画 */
|
/* 进度指示器动画 */
|
||||||
@keyframes flow {
|
@keyframes flow {
|
||||||
0% {
|
0% { background-position: 0% center; }
|
||||||
background-position: 0% center;
|
100% { background-position: 200% center; }
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-indicator {
|
.progress-indicator {
|
||||||
color: var(--gray-11);
|
color: var(--text-secondary);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,34 +136,82 @@
|
|||||||
text-fill-color: transparent;
|
text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 添加以下暗色主题的自定义变量 */
|
/* 滚动条基础样式 */
|
||||||
.dark-theme-custom {
|
::-webkit-scrollbar {
|
||||||
--gray-1: hsl(220, 15%, 12%); /* 背景色,更柔和的深色 */
|
width: var(--scrollbar-size);
|
||||||
--gray-2: hsl(220, 15%, 14%);
|
height: var(--scrollbar-size);
|
||||||
--gray-3: hsl(220, 15%, 16%);
|
|
||||||
--gray-12: hsl(220, 15%, 85%); /* 文本颜色,不要太白 */
|
|
||||||
|
|
||||||
/* 减少对比度,使文字更柔和 */
|
|
||||||
--gray-11: hsl(220, 15%, 65%);
|
|
||||||
|
|
||||||
/* 边框颜色调整 */
|
|
||||||
--gray-a5: hsla(220, 15%, 50%, 0.2);
|
|
||||||
|
|
||||||
/* 重要:确保背景和文本的对比度适中 */
|
|
||||||
background-color: var(--gray-1);
|
|
||||||
color: var(--gray-12);
|
|
||||||
|
|
||||||
/* 添加微弱的蓝光过滤 */
|
|
||||||
filter: brightness(0.96) saturate(0.95);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 优化暗色主题下的阴影效果 */
|
::-webkit-scrollbar-track {
|
||||||
.dark-theme-custom [class*='shadow'] {
|
background: rgba(226, 232, 240, 0.5);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 优化链接和交互元素的高亮颜色 */
|
::-webkit-scrollbar-thumb {
|
||||||
.dark-theme-custom a:hover,
|
background: rgba(148, 163, 184, 0.5);
|
||||||
.dark-theme-custom button:hover {
|
border-radius: var(--border-radius);
|
||||||
--accent-9: hsl(226, 70%, 65%); /* 更柔和的强调色 */
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(148, 163, 184, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题滚动条 */
|
||||||
|
html[class~="dark"] ::-webkit-scrollbar-track {
|
||||||
|
background: rgba(8, 27, 57, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[class~="dark"] ::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(148, 163, 184, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[class~="dark"] ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(148, 163, 184, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阴影效果 */
|
||||||
|
[class*="shadow"] {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[class~="dark"] body [class*="shadow"] {
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes in {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes out {
|
||||||
|
from { opacity: 1; transform: translateY(0); }
|
||||||
|
to { opacity: 0; transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-in {
|
||||||
|
animation: in 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-out {
|
||||||
|
animation: out 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加缩放动画 */
|
||||||
|
@keyframes zoomIn {
|
||||||
|
from { transform: scale(0.95); }
|
||||||
|
to { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes zoomOut {
|
||||||
|
from { transform: scale(1); }
|
||||||
|
to { transform: scale(0.95); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-95 {
|
||||||
|
animation: zoomIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-out-95 {
|
||||||
|
animation: zoomOut 0.2s ease-in;
|
||||||
}
|
}
|
||||||
|
@ -89,15 +89,12 @@ export default defineConfig(async ({ mode }) => {
|
|||||||
envPrefix: "VITE_",
|
envPrefix: "VITE_",
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
// 移除 manualChunks 配置
|
||||||
manualChunks: {
|
|
||||||
three: ['three'],
|
|
||||||
gsap: ['gsap']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// 优化大型依赖的处理
|
chunkSizeWarningLimit: 1500
|
||||||
chunkSizeWarningLimit: 1000
|
},
|
||||||
|
ssr: {
|
||||||
|
noExternal: ['three', '@react-three/fiber', '@react-three/drei', 'gsap']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user