前端:解决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 post from "themes/echoes/post";
|
||||
import { memo, useCallback } from "react";
|
||||
import login from "~/dashboard/login";
|
||||
import adminLayout from "~/dashboard/layout";
|
||||
|
||||
const args = {
|
||||
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>',
|
||||
} as const;
|
||||
|
||||
const renderLayout = (children: React.ReactNode) => {
|
||||
return layout.render({
|
||||
// 创建布局渲染器的工厂函数
|
||||
const createLayoutRenderer = (layoutComponent: any) => {
|
||||
return (children: React.ReactNode) => {
|
||||
return layoutComponent.render({
|
||||
children,
|
||||
args,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// 使用工厂函数创建不同的布局渲染器
|
||||
const renderLayout = createLayoutRenderer(layout);
|
||||
const renderAdminLayout = createLayoutRenderer(adminLayout);
|
||||
|
||||
const Routes = memo(() => {
|
||||
const location = useLocation();
|
||||
const path = location.pathname.split("/")[1];
|
||||
@ -28,6 +37,11 @@ const Routes = memo(() => {
|
||||
return renderLayout(Component.render(args));
|
||||
}, []);
|
||||
|
||||
// 添加管理后台内容渲染函数
|
||||
const renderAdminContent = useCallback((Component: any) => {
|
||||
return renderAdminLayout(Component.render(args));
|
||||
}, []);
|
||||
|
||||
// 根据路径返回对应组件
|
||||
if (path === "error") {
|
||||
return renderContent(ErrorPage);
|
||||
@ -41,6 +55,37 @@ const Routes = memo(() => {
|
||||
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);
|
||||
});
|
||||
|
||||
|
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,添加尺寸配置
|
||||
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)]'
|
||||
};
|
||||
|
||||
@ -375,6 +370,12 @@ export const ParticleImage = ({
|
||||
const cleanup = useCallback(() => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 检查是否应该跳过清理
|
||||
if (sceneRef.current?.userData.isSmileComplete ||
|
||||
sceneRef.current?.userData.isErrorComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清理动画状态
|
||||
isAnimatingRef.current = false;
|
||||
|
||||
@ -394,43 +395,16 @@ export const ParticleImage = ({
|
||||
|
||||
// 清理场景资源
|
||||
if (sceneRef.current) {
|
||||
// 遍历场景中的所有对象
|
||||
sceneRef.current.traverse((object) => {
|
||||
if (object instanceof THREE.Points) {
|
||||
const geometry = object.geometry;
|
||||
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 (!sceneRef.current.userData.isSmileComplete &&
|
||||
!sceneRef.current.userData.isErrorComplete) {
|
||||
cleanupResources(sceneRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
// 修改渲染器清理逻辑
|
||||
if (rendererRef.current) {
|
||||
if (rendererRef.current && !sceneRef.current?.userData.isSmileComplete &&
|
||||
!sceneRef.current?.userData.isErrorComplete) {
|
||||
const renderer = rendererRef.current;
|
||||
|
||||
// 确保在移除 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;
|
||||
}
|
||||
}, []);
|
||||
@ -475,7 +450,10 @@ export const ParticleImage = ({
|
||||
const updateParticles = useCallback((width: number, height: number) => {
|
||||
if (!sceneRef.current || isAnimatingRef.current || !isMountedRef.current) return;
|
||||
|
||||
// 只有当src不为空时才执行cleanup
|
||||
if(src !== '') {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
@ -500,8 +478,40 @@ export const ParticleImage = ({
|
||||
sceneRef.current.add(points);
|
||||
|
||||
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 处理逻辑移到组件顶层
|
||||
const handleResize = useCallback(() => {
|
||||
@ -511,7 +521,7 @@ export const ParticleImage = ({
|
||||
const width = containerRef.current.offsetWidth;
|
||||
const height = containerRef.current.offsetHeight;
|
||||
|
||||
// 更新相机视图
|
||||
// 更新相机图
|
||||
const camera = cameraRef.current;
|
||||
camera.left = width / -2;
|
||||
camera.right = width / 2;
|
||||
@ -563,8 +573,19 @@ export const ParticleImage = ({
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
alpha: true,
|
||||
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(
|
||||
window.devicePixelRatio,
|
||||
window.innerWidth <= 768 ? 2 : 3
|
||||
@ -572,12 +593,17 @@ export const ParticleImage = ({
|
||||
renderer.setSize(width, height);
|
||||
rendererRef.current = renderer;
|
||||
|
||||
// 确保容器仍然存在再添加渲染器
|
||||
if (containerRef.current && isMountedRef.current) {
|
||||
// 修改渲染器添加到DOM的部分
|
||||
if (containerRef.current && isMountedRef.current && renderer.domElement) {
|
||||
try {
|
||||
containerRef.current.appendChild(renderer.domElement);
|
||||
} catch (e) {
|
||||
console.warn('Failed to append renderer:', e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否应该显示笑
|
||||
// 检查是否应该显示笑脸
|
||||
if (src === '') {
|
||||
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
||||
|
||||
@ -599,10 +625,10 @@ export const ParticleImage = ({
|
||||
const points = new THREE.Points(geometry, material);
|
||||
scene.add(points);
|
||||
|
||||
// 修改动画效果
|
||||
const positionAttribute = geometry.attributes.position;
|
||||
// 添加这一行来获取position属性
|
||||
const positionAttribute = geometry.attributes.position as THREE.BufferAttribute;
|
||||
|
||||
// 算到中心的距离用于延迟
|
||||
// 修改动画效果,添加完成回调
|
||||
particles.forEach((particle, i) => {
|
||||
const i3 = i * 3;
|
||||
const distanceToCenter = Math.sqrt(
|
||||
@ -621,6 +647,12 @@ export const ParticleImage = ({
|
||||
ease: "sine.inOut",
|
||||
onUpdate: () => {
|
||||
positionAttribute.needsUpdate = true;
|
||||
},
|
||||
onComplete: () => {
|
||||
// 动画完成后设置标记,防止被清理
|
||||
if(scene) {
|
||||
scene.userData.isSmileComplete = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -699,6 +731,12 @@ export const ParticleImage = ({
|
||||
ease: "back.out(1.7)",
|
||||
onUpdate: () => {
|
||||
positionAttribute.needsUpdate = true;
|
||||
},
|
||||
onComplete: () => {
|
||||
// 添加标记表示错误动画已完成
|
||||
if(scene) {
|
||||
scene.userData.isErrorComplete = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -785,7 +823,7 @@ export const ParticleImage = ({
|
||||
delay: normalizedDistance * 0.3
|
||||
});
|
||||
|
||||
// 随机初始位置(根据距离调整范围)
|
||||
// 随机初始位置(根据距离调范围)
|
||||
const spread = 1 - normalizedDistance * 0.5; // 距离越远,始扩散越小
|
||||
positionArray.push(
|
||||
(Math.random() - 0.5) * width * spread,
|
||||
@ -962,7 +1000,7 @@ export const ImageLoader = ({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [animationComplete, setAnimationComplete] = useState(false);
|
||||
|
||||
// 处理图片预加载
|
||||
// 处理图片加载
|
||||
const preloadImage = useCallback(() => {
|
||||
if (!src || loadingRef.current) return;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import "styles/echoes.css";
|
||||
|
||||
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 {
|
||||
CalendarIcon,
|
||||
PersonIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
@ -20,7 +19,7 @@ const mockArticles: PostDisplay[] = [
|
||||
content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...",
|
||||
authorName: "张三",
|
||||
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: "",
|
||||
metaDescription: "",
|
||||
status: "published",
|
||||
@ -42,7 +41,7 @@ const mockArticles: PostDisplay[] = [
|
||||
content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...",
|
||||
authorName: "李四",
|
||||
publishedAt: new Date("2024-03-14"),
|
||||
coverImage: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=500&auto=format",
|
||||
coverImage: "",
|
||||
metaKeywords: "",
|
||||
metaDescription: "",
|
||||
status: "published",
|
||||
@ -63,7 +62,7 @@ const mockArticles: PostDisplay[] = [
|
||||
content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
||||
authorName: "王五",
|
||||
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: "",
|
||||
metaDescription: "",
|
||||
status: "published",
|
||||
@ -84,7 +83,7 @@ const mockArticles: PostDisplay[] = [
|
||||
content: "移动端开发中的各种适配问题及解决方案...",
|
||||
authorName: "田六",
|
||||
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: "",
|
||||
metaDescription: "",
|
||||
status: "published",
|
||||
@ -173,7 +172,7 @@ const mockArticles: PostDisplay[] = [
|
||||
],
|
||||
tags: [
|
||||
{ name: "性能监控" },
|
||||
{ name: "懒加<EFBFBD><EFBFBD><EFBFBD>" },
|
||||
{ name: "懒加载" },
|
||||
{ name: "缓存策略" },
|
||||
{ name: "代码分割" }
|
||||
]
|
||||
|
@ -274,8 +274,25 @@ export default new Layout(({ children, args }) => {
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
align="end"
|
||||
sideOffset={5}
|
||||
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"
|
||||
sideOffset={20}
|
||||
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">
|
||||
{/* 导航链接区域 */}
|
||||
@ -305,8 +322,8 @@ export default new Layout(({ children, args }) => {
|
||||
{/* 用户操作区域 */}
|
||||
<Box className="p-4 border-t border-[--gray-a5]">
|
||||
<Flex gap="3" align="center">
|
||||
{/* 用户信息/登录按钮 - 占据 55% 宽度 */}
|
||||
<Box className="w-[55%]">
|
||||
{/* 用户信息/登录按钮 - 调整为 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"
|
||||
@ -325,8 +342,8 @@ export default new Layout(({ children, args }) => {
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* 主题切换按钮 - 占据剩余空间 */}
|
||||
<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]">
|
||||
{/* 主题切换按钮 - 调整为 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>
|
||||
|
@ -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 ReactMarkdown from "react-markdown";
|
||||
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 { ComponentPropsWithoutRef } from 'react';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { Components } from "react-markdown";
|
||||
|
||||
// 示例文章数据
|
||||
const mockPost: PostDisplay = {
|
||||
@ -37,7 +38,7 @@ const mockPost: PostDisplay = {
|
||||
|
||||
### 1.1 必备工具安装
|
||||
|
||||
开发环境需要安装以下工具:
|
||||
发环境需要安装以下工具:
|
||||
|
||||
\`\`\`bash
|
||||
# 安装 Node.js
|
||||
@ -379,12 +380,16 @@ export default new Template({}, ({ http, args }) => {
|
||||
const [activeId, setActiveId] = useState<string>("");
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [showToc, setShowToc] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(true);
|
||||
const [headingIdsArrays, setHeadingIdsArrays] = useState<{[key: string]: string[]}>({});
|
||||
const headingIds = useRef<string[]>([]); // 保持原有的 ref
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isClickScrolling = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
@ -437,8 +442,6 @@ export default new Template({}, ({ http, args }) => {
|
||||
if (tocArray.length > 0) {
|
||||
setActiveId(tocArray[0].id);
|
||||
}
|
||||
|
||||
setIsMounted(true);
|
||||
}, [mockPost.content, mockPost.id]);
|
||||
|
||||
const components = useMemo(() => ({
|
||||
@ -509,21 +512,20 @@ export default new Template({}, ({ http, args }) => {
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<div className="my-4 sm:my-6 mx-0 sm:mx-0">
|
||||
{/* 标题栏 */}
|
||||
<div className="flex justify-between items-center h-9 sm:h-10 px-6
|
||||
border-x border-t border-[--gray-6]
|
||||
bg-white dark:bg-[--gray-1]
|
||||
rounded-t-none sm:rounded-t-lg"
|
||||
<div className="my-4 sm:my-6">
|
||||
<div className="flex justify-between items-center h-9 sm:h-10 px-4 sm:px-6
|
||||
border-t border-x border-[--gray-6]
|
||||
bg-[--gray-2] dark:bg-[--gray-2]
|
||||
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)} />
|
||||
</div>
|
||||
|
||||
{/* 代码内容区域 */}
|
||||
<div className="overflow-x-auto border-x border-b border-[--gray-6] rounded-b-none sm:rounded-b-lg">
|
||||
<div className="min-w-[640px]">
|
||||
<div className="p-6 bg-[--gray-2] dark:bg-[--gray-3]">
|
||||
<div className="border border-[--gray-6] rounded-b-lg bg-white dark:bg-[--gray-1] mx-0">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="p-4 sm:p-6">
|
||||
<SyntaxHighlighter
|
||||
language={lang || "text"}
|
||||
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="overflow-x-auto">
|
||||
<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}>
|
||||
{children}
|
||||
</table>
|
||||
@ -602,39 +604,60 @@ export default new Template({}, ({ http, args }) => {
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (isClickScrolling.current) return;
|
||||
if (!isMounted) return;
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveId(entry.target.id);
|
||||
}
|
||||
const container = document.querySelector("#main-content");
|
||||
const contentBox = document.querySelector(".prose");
|
||||
|
||||
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,
|
||||
rootMargin: '-80px 0px -80% 0px',
|
||||
threshold: 0.5
|
||||
root: document.querySelector("#main-content"),
|
||||
rootMargin: '-20px 0px -80% 0px',
|
||||
threshold: [0, 1]
|
||||
}
|
||||
);
|
||||
|
||||
if (isMounted) {
|
||||
tocItems.forEach((item) => {
|
||||
const element = document.getElementById(item.id);
|
||||
if (element) {
|
||||
observer.observe(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isMounted) {
|
||||
tocItems.forEach((item) => {
|
||||
const element = document.getElementById(item.id);
|
||||
if (element) {
|
||||
observer.unobserve(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [tocItems]);
|
||||
}, [tocItems, isMounted]);
|
||||
|
||||
// 修改点击<EFBFBD><EFBFBD>理函数
|
||||
// 修改点击处理函数
|
||||
const handleTocClick = useCallback((e: React.MouseEvent, itemId: string) => {
|
||||
e.preventDefault();
|
||||
const element = document.getElementById(itemId);
|
||||
@ -644,6 +667,7 @@ export default new Template({}, ({ http, args }) => {
|
||||
if (element && container && contentBox) {
|
||||
isClickScrolling.current = true;
|
||||
|
||||
// 计算元素相对于内容容器的偏移量
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const contentBoxRect = contentBox.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
@ -662,8 +686,6 @@ export default new Template({}, ({ http, args }) => {
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
setActiveId(itemId);
|
||||
|
||||
// 滚动完成后重置标记
|
||||
const resetTimeout = setTimeout(() => {
|
||||
isClickScrolling.current = false;
|
||||
@ -673,17 +695,19 @@ export default new Template({}, ({ http, args }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 修改<EFBFBD><EFBFBD>动端目录的渲染逻辑
|
||||
const mobileMenu = isMounted && (
|
||||
// 修改移动端目录的渲染逻辑
|
||||
const mobileMenu = (
|
||||
<>
|
||||
{isMounted && (
|
||||
<Button
|
||||
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)}
|
||||
>
|
||||
<CodeIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showToc && (
|
||||
{isMounted && showToc && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 z-50 bg-black/50 transition-opacity duration-300"
|
||||
onClick={() => setShowToc(false)}
|
||||
@ -761,11 +785,11 @@ export default new Template({}, ({ http, args }) => {
|
||||
|
||||
return (
|
||||
<Container
|
||||
ref={containerRef} // 添加ref到最外层容器
|
||||
ref={containerRef}
|
||||
size={{initial: "2", sm: "3", md: "4"}}
|
||||
className="px-4 sm:px-6 md:px-8"
|
||||
>
|
||||
{mobileMenu}
|
||||
{isMounted && mobileMenu}
|
||||
|
||||
<Flex
|
||||
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="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">
|
||||
<Heading
|
||||
@ -864,7 +888,7 @@ export default new Template({}, ({ http, args }) => {
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 修改片样式 */}
|
||||
{/* 封面图片 */}
|
||||
{mockPost.coverImage && (
|
||||
<Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg">
|
||||
<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 {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--gray-12);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#nav a:hover {
|
||||
@ -27,16 +112,12 @@
|
||||
|
||||
/* 进度指示器动画 */
|
||||
@keyframes flow {
|
||||
0% {
|
||||
background-position: 0% center;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% center;
|
||||
}
|
||||
0% { background-position: 0% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
.progress-indicator {
|
||||
color: var(--gray-11);
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@ -55,34 +136,82 @@
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* 添加以下暗色主题的自定义变量 */
|
||||
.dark-theme-custom {
|
||||
--gray-1: hsl(220, 15%, 12%); /* 背景色,更柔和的深色 */
|
||||
--gray-2: hsl(220, 15%, 14%);
|
||||
--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 {
|
||||
width: var(--scrollbar-size);
|
||||
height: var(--scrollbar-size);
|
||||
}
|
||||
|
||||
/* 优化暗色主题下的阴影效果 */
|
||||
.dark-theme-custom [class*='shadow'] {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(226, 232, 240, 0.5);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* 优化链接和交互元素的高亮颜色 */
|
||||
.dark-theme-custom a:hover,
|
||||
.dark-theme-custom button:hover {
|
||||
--accent-9: hsl(226, 70%, 65%); /* 更柔和的强调色 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.5);
|
||||
border-radius: var(--border-radius);
|
||||
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_",
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
three: ['three'],
|
||||
gsap: ['gsap']
|
||||
}
|
||||
}
|
||||
// 移除 manualChunks 配置
|
||||
},
|
||||
// 优化大型依赖的处理
|
||||
chunkSizeWarningLimit: 1000
|
||||
chunkSizeWarningLimit: 1500
|
||||
},
|
||||
ssr: {
|
||||
noExternal: ['three', '@react-three/fiber', '@react-three/drei', 'gsap']
|
||||
}
|
||||
};
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user