From 66d47f6cfa431102cfa5c7251dc611f4504e9fcb Mon Sep 17 00:00:00 2001 From: lsy Date: Sun, 8 Dec 2024 19:19:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E8=A7=A3=E5=86=B3vi?= =?UTF-8?q?te=E4=B8=8D=E8=83=BD=E6=89=93=E5=8C=85=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=8A=A0=E8=BD=BD=E5=9B=BE=E7=89=87=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=8D=A2=E9=BB=91=E6=9A=97=E6=A8=A1=E5=BC=8F=E8=83=8C=E6=99=AF?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E6=96=87=E7=AB=A0=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/dashboard/layout.tsx | 259 ++++++++++++++++++++++ frontend/app/dashboard/login.tsx | 171 ++++++++++++++ frontend/app/dashboard/styles/login.css | 64 ++++++ frontend/app/routes.tsx | 55 ++++- frontend/hooks/Background.tsx | 95 ++++++++ frontend/hooks/ParticleImage.tsx | 146 +++++++----- frontend/hooks/echoes.tsx | 2 +- frontend/themes/echoes/article.tsx | 11 +- frontend/themes/echoes/layout.tsx | 29 ++- frontend/themes/echoes/post.tsx | 130 ++++++----- frontend/themes/echoes/styles/layouts.css | 197 +++++++++++++--- frontend/vite.config.ts | 13 +- 12 files changed, 1005 insertions(+), 167 deletions(-) create mode 100644 frontend/app/dashboard/layout.tsx create mode 100644 frontend/app/dashboard/login.tsx create mode 100644 frontend/app/dashboard/styles/login.css create mode 100644 frontend/hooks/Background.tsx diff --git a/frontend/app/dashboard/layout.tsx b/frontend/app/dashboard/layout.tsx new file mode 100644 index 0000000..5309088 --- /dev/null +++ b/frontend/app/dashboard/layout.tsx @@ -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: , + label: "仪表盘", + path: "/dashboard", + }, + { + icon: , + label: "文章管理", + path: "/dashboard/posts", + }, + { + icon: , + label: "媒体管理", + path: "/dashboard/media", + }, + { + icon: , + label: "评论管理", + path: "/dashboard/comments", + }, + { + icon: , + label: "分类管理", + path: "/dashboard/categories", + }, + { + icon: , + 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 ( + + + + {/* 侧边栏 */} + + {/* Logo区域 */} + + + + A + + + 后台管理 + + + + + {/* 菜单列表区域添加滚动 */} + + + {menuItems.map((item) => ( + + {item.icon} + + {item.label} + + + ))} + + + + + {/* 主内容区域 */} + + {/* 顶部导航栏 */} + + + {/* 左侧菜单按钮 */} + + {mobileMenuOpen ? ( + + ) : ( + + )} + + + + + {/* 右侧用户菜单 */} + + + + + + + + + + + + + + + 个人设置 + + + + + + + 退出登录 + + + + + + + + + {/* 页面内容区域 */} + + + {children} + + + + + + {/* 移动端菜单遮罩 */} + {mobileMenuOpen && ( + setMobileMenuOpen(false)} + /> + )} + + + ); +}); diff --git a/frontend/app/dashboard/login.tsx b/frontend/app/dashboard/login.tsx new file mode 100644 index 0000000..b9d1212 --- /dev/null +++ b/frontend/app/dashboard/login.tsx @@ -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(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 ( + <> + + + + + + + + + {/* Logo */} + + + 后台 + + + + + {/* 登录表单 */} +
+ + {/* 用户名输入框 */} + + + setUsername(e.target.value)} + required + /> + + + {/* 密码输入框 */} + + + setPassword(e.target.value)} + required + /> + + + {/* 登录按钮 */} + + + {/* 其他选项 */} + + + 忘记密码? + + + +
+
+
+
+ + ); +}); \ No newline at end of file diff --git a/frontend/app/dashboard/styles/login.css b/frontend/app/dashboard/styles/login.css new file mode 100644 index 0000000..900529f --- /dev/null +++ b/frontend/app/dashboard/styles/login.css @@ -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; +} \ No newline at end of file diff --git a/frontend/app/routes.tsx b/frontend/app/routes.tsx index 2ee5caf..1a4b7ba 100644 --- a/frontend/app/routes.tsx +++ b/frontend/app/routes.tsx @@ -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: 'indexerroraboutpost', } as const; -const renderLayout = (children: React.ReactNode) => { - return layout.render({ - children, - args, - }); +// 创建布局渲染器的工厂函数 +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(
仪表盘内容
); + } + + // 根据子路径返回对应的管理页面 + switch (subPath) { + case "posts": + return renderAdminLayout(
文章管理
); + case "media": + return renderAdminLayout(
媒体管理
); + case "comments": + return renderAdminLayout(
评论管理
); + case "categories": + return renderAdminLayout(
分类管理
); + case "settings": + return renderAdminLayout(
系统设置
); + default: + return renderAdminLayout(
404 未找到页面
); + } + } + return renderContent(article); }); diff --git a/frontend/hooks/Background.tsx b/frontend/hooks/Background.tsx new file mode 100644 index 0000000..7199703 --- /dev/null +++ b/frontend/hooks/Background.tsx @@ -0,0 +1,95 @@ +import { useEffect, useRef } from 'react'; +import { useThemeMode } from 'hooks/ThemeMode'; + +export const AnimatedBackground = () => { + const canvasRef = useRef(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 ( +
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/hooks/ParticleImage.tsx b/frontend/hooks/ParticleImage.tsx index c91c995..465007f 100644 --- a/frontend/hooks/ParticleImage.tsx +++ b/frontend/hooks/ParticleImage.tsx @@ -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; - cleanup(); + // 只有当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) { - containerRef.current.appendChild(renderer.domElement); + // 修改渲染器添加到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(null); const [animationComplete, setAnimationComplete] = useState(false); - // 处理图片预加载 + // 处理图片加载 const preloadImage = useCallback(() => { if (!src || loadingRef.current) return; diff --git a/frontend/hooks/echoes.tsx b/frontend/hooks/echoes.tsx index f678ca5..59e6d7c 100644 --- a/frontend/hooks/echoes.tsx +++ b/frontend/hooks/echoes.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import "styles/echoes.css"; export const Echoes: React.FC = () => { diff --git a/frontend/themes/echoes/article.tsx b/frontend/themes/echoes/article.tsx index 9bec6f1..838ebd5 100644 --- a/frontend/themes/echoes/article.tsx +++ b/frontend/themes/echoes/article.tsx @@ -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: "懒加���" }, + { name: "懒加载" }, { name: "缓存策略" }, { name: "代码分割" } ] diff --git a/frontend/themes/echoes/layout.tsx b/frontend/themes/echoes/layout.tsx index 335a3b8..ec4090d 100644 --- a/frontend/themes/echoes/layout.tsx +++ b/frontend/themes/echoes/layout.tsx @@ -274,8 +274,25 @@ export default new Layout(({ children, args }) => { {/* 导航链接区域 */} @@ -305,8 +322,8 @@ export default new Layout(({ children, args }) => { {/* 用户操作区域 */} - {/* 用户信息/登录按钮 - 占据 55% 宽度 */} - + {/* 用户信息/登录按钮 - 调整为 70% 宽度 */} + - {/* 主题切换按钮 - 占据剩余空间 */} - + {/* 主题切换按钮 - 调整为 30% 宽度 */} + diff --git a/frontend/themes/echoes/post.tsx b/frontend/themes/echoes/post.tsx index 37994d4..e10f627 100644 --- a/frontend/themes/echoes/post.tsx +++ b/frontend/themes/echoes/post.tsx @@ -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(""); const contentRef = useRef(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([]); // 保持原有的 ref const containerRef = useRef(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} ) : ( -
- {/* 标题栏 */} -
+
-
{lang || "text"}
+
{lang || "text"}
- {/* 代码内容区域 */} -
-
-
+
+
+
{
-
+
{children}
@@ -602,39 +604,60 @@ export default new Template({}, ({ http, args }) => { const observer = new IntersectionObserver( (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) { - setActiveId(entry.target.id); - } - }); + // 找出所有进入可视区域的标题 + 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] } ); - 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); + observer.observe(element); } }); - }; - }, [tocItems]); + } - // 修改点击��理函数 + 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) => { 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 }) => { } }, []); - // 修改��动端目录的渲染逻辑 - const mobileMenu = isMounted && ( + // 修改移动端目录的渲染逻辑 + const mobileMenu = ( <> - + {isMounted && ( + + )} - {showToc && ( + {isMounted && showToc && (
setShowToc(false)} @@ -761,11 +785,11 @@ export default new Template({}, ({ http, args }) => { return ( - {mobileMenu} + {isMounted && mobileMenu} { > {/* 文章主体 */} - + {/* 头部 */} { - {/* 修改片样式 */} + {/* 封面图片 */} {mockPost.coverImage && ( { 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'] } }; });