前端:解决vite不能打包,优化加载图片,更换黑暗模式背景,优化文章页面

This commit is contained in:
lsy 2024-12-08 19:19:54 +08:00
parent b3c0af91b8
commit 66d47f6cfa
12 changed files with 1005 additions and 167 deletions

View 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>
);
});

View 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>
</>
);
});

View 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;
}

View File

@ -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) => {
children, return (children: React.ReactNode) => {
args, return layoutComponent.render({
}); children,
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);
}); });

View 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>
);
};

View File

@ -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;
cleanup(); // 只有当src不为空时才执行cleanup
if(src !== '') {
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) {
containerRef.current.appendChild(renderer.domElement); try {
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;

View File

@ -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 = () => {

View File

@ -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: "代码分割" }
] ]

View File

@ -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>

View File

@ -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;
const container = document.querySelector("#main-content");
const contentBox = document.querySelector(".prose");
if (!container || !contentBox) return;
entries.forEach((entry) => { // 找出所有进入可视区域的标题
if (entry.isIntersecting) { const intersectingEntries = entries.filter(entry => entry.isIntersecting);
setActiveId(entry.target.id);
} 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]
} }
); );
tocItems.forEach((item) => { if (isMounted) {
const element = document.getElementById(item.id);
if (element) {
observer.observe(element);
}
});
return () => {
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.observe(element);
} }
}); });
}; }
}, [tocItems]);
// 修改点击<E782B9><E587BB>理函数 return () => {
if (isMounted) {
tocItems.forEach((item) => {
const element = document.getElementById(item.id);
if (element) {
observer.unobserve(element);
}
});
}
};
}, [tocItems, isMounted]);
// 修改点击处理函数
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 = (
<> <>
<Button {isMounted && (
className="lg:hidden fixed bottom-6 right-6 z-50 w-12 h-12 rounded-full shadow-lg bg-[--accent-9] text-white" <Button
onClick={() => setShowToc(true)} 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> <CodeIcon className="w-5 h-5" />
</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

View File

@ -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;
} }

View File

@ -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']
} }
}; };
}); });