diff --git a/frontend/app/entry.client.tsx b/frontend/app/entry.client.tsx index 6129132..84ae194 100644 --- a/frontend/app/entry.client.tsx +++ b/frontend/app/entry.client.tsx @@ -11,8 +11,6 @@ import { hydrateRoot } from "react-dom/client"; startTransition(() => { hydrateRoot( document, - - - , + , ); }); diff --git a/frontend/app/init.tsx b/frontend/app/init.tsx index ba43738..6e25c63 100644 --- a/frontend/app/init.tsx +++ b/frontend/app/init.tsx @@ -1,7 +1,7 @@ -import React, { createContext, useState } from "react"; +import React, { createContext, useEffect, useState } from "react"; import { DEFAULT_CONFIG } from "app/env"; import { HttpClient } from "core/http"; -import { ThemeModeToggle } from "hooks/themeMode"; +import { ThemeModeToggle } from "hooks/ThemeMode"; import { Theme, Button, @@ -13,8 +13,8 @@ import { Box, TextField, } from "@radix-ui/themes"; -import { toast } from "hooks/notification"; -import { Echoes } from "hooks/echoes"; +import { toast } from "hooks/Notification"; +import { Echoes } from "hooks/Echoes"; interface SetupContextType { currentStep: number; @@ -36,7 +36,7 @@ const StepContainer: React.FC<{ title: string; children: React.ReactNode }> = ({ children, }) => ( - + {title} @@ -237,15 +237,27 @@ const DatabaseConfig: React.FC = ({ onNext }) => {
- 数据库类型 + 数据库类型 * - - + + - PostgreSQL - MySQL - SQLite + + + PostgreSQL + + + + + MySQL + + + + + SQLite + + @@ -461,9 +473,12 @@ const SetupComplete: React.FC = () => ( ); export default function SetupPage() { - const [currentStep, setCurrentStep] = useState(() => { - return Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1; - }); + const [currentStep, setCurrentStep] = useState(1); + useEffect(() => { + // 在客户端组件挂载后更新状态 + const initStatus = Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1; + setCurrentStep(initStatus); + }, []); return ( indexerroraboutpost', +} as const; + +const renderLayout = (children: React.ReactNode) => { + return layout.render({ + children, + args, + }); +}; + +const Routes = memo(() => { const location = useLocation(); - let path = location.pathname; + const path = location.pathname.split("/")[1]; - const args = { - title: "我的页面", - theme: "dark", - nav: 'indexerroraboutpost', - }; - - console.log(path); - path = path.split("/")[1]; + // 使用 useCallback 缓存渲染函数 + const renderContent = useCallback((Component: any) => { + return renderLayout(Component.render(args)); + }, []); + // 根据路径返回对应组件 if (path === "error") { - return layout.render({ - children: ErrorPage.render(args), - args, - }); + return renderContent(ErrorPage); } if (path === "about") { - return layout.render({ - children: about.render(args), - args, - }); + return renderContent(about); } if (path === "post") { - return layout.render({ - children: post.render(args), - args, - }); + return renderContent(post); } - return layout.render({ - children: article.render(args), - args, - }); -} + return renderContent(article); +}); + +export default Routes; diff --git a/frontend/hooks/ParticleImage.tsx b/frontend/hooks/ParticleImage.tsx index 83e8d3f..c91c995 100644 --- a/frontend/hooks/ParticleImage.tsx +++ b/frontend/hooks/ParticleImage.tsx @@ -224,7 +224,7 @@ const getOptimalImageParams = (width: number, height: number) => { const pixelRatio = window.devicePixelRatio || 1; const isMobile = window.innerWidth <= 768; - // 移动端使用更大的采样间隔来减少���数量 + // 移动端使用更大的采样间隔来减少数量 let samplingGap = isMobile ? Math.ceil(Math.max(width, height) / 60) // 移动端降低采样密度 : Math.ceil(Math.max(width, height) / 120); // 桌面端保持较高采密度 @@ -394,30 +394,74 @@ export const ParticleImage = ({ // 清理场景资源 if (sceneRef.current) { - cleanupResources(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 (rendererRef.current) { const renderer = rendererRef.current; + + // 确保在移除 DOM 元素前停止渲染 + renderer.setAnimationLoop(null); + + // 清理渲染器上下文 + renderer.dispose(); + renderer.forceContextLoss(); + + // 安全地移除 DOM 元素 const domElement = renderer.domElement; - - // 使用 requestAnimationFrame 确保在一帧进�� DOM 操作 - requestAnimationFrame(() => { - if (isMountedRef.current && containerRef.current?.contains(domElement)) { - try { - containerRef.current.removeChild(domElement); - } catch (e) { - console.warn('清理渲染器DOM元素失败:', e); + if (containerRef.current?.contains(domElement)) { + requestAnimationFrame(() => { + if (isMountedRef.current && containerRef.current?.contains(domElement)) { + try { + containerRef.current.removeChild(domElement); + } catch (e) { + console.warn('清理渲染器DOM元素失败:', e); + } } - } - - renderer.dispose(); - renderer.forceContextLoss(); - }); + }); + } + // 清空引用 rendererRef.current = undefined; } + + // 清理相机引用 + if (cameraRef.current) { + cameraRef.current = undefined; + } }, []); // 修改 useEffect 的清理 @@ -1013,7 +1057,7 @@ export const ImageLoader = ({ }); }; - // 确保src存在再设��� + // 确保src存在再设置 if (src) { img.src = src; } diff --git a/frontend/hooks/loading.tsx b/frontend/hooks/loading.tsx deleted file mode 100644 index 848f2b0..0000000 --- a/frontend/hooks/loading.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { createContext, useState, useContext } from "react"; - -interface LoadingContextType { - isLoading: boolean; - showLoading: () => void; - hideLoading: () => void; -} - -const LoadingContext = createContext({ - isLoading: false, - showLoading: () => {}, - hideLoading: () => {}, -}); - -export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { - const [isLoading, setIsLoading] = useState(false); - - const showLoading = () => setIsLoading(true); - const hideLoading = () => setIsLoading(false); - - return ( - - {children} - {isLoading && ( -
-
-
- 加载中... -
-
- )} - - - ); -}; - -// 全局loading实例 -let globalShowLoading: (() => void) | null = null; -let globalHideLoading: (() => void) | null = null; - -export const LoadingContainer: React.FC = () => { - const { showLoading, hideLoading } = useContext(LoadingContext); - - React.useEffect(() => { - globalShowLoading = showLoading; - globalHideLoading = hideLoading; - return () => { - globalShowLoading = null; - globalHideLoading = null; - }; - }, [showLoading, hideLoading]); - - return null; -}; - -// 导出loading方法 -export const loading = { - show: () => { - if (!globalShowLoading) { - console.warn("Loading system not initialized"); - return; - } - globalShowLoading(); - }, - hide: () => { - if (!globalHideLoading) { - console.warn("Loading system not initialized"); - return; - } - globalHideLoading(); - }, -}; diff --git a/frontend/interface/layout.ts b/frontend/interface/layout.ts index 9a0eb32..21570dc 100644 --- a/frontend/interface/layout.ts +++ b/frontend/interface/layout.ts @@ -1,10 +1,17 @@ import { HttpClient } from "core/http"; import { CapabilityService } from "core/capability"; import { Serializable } from "interface/serializableType"; +import { createElement, memo } from 'react'; export class Layout { private http: HttpClient; private capability: CapabilityService; + private readonly MemoizedElement: React.MemoExoticComponent<(props: { + children: React.ReactNode; + args?: Serializable; + onTouchStart?: (e: TouchEvent) => void; + onTouchEnd?: (e: TouchEvent) => void; + }) => React.ReactNode>; constructor( public element: (props: { @@ -20,6 +27,7 @@ export class Layout { ) { this.http = services?.http || HttpClient.getInstance(); this.capability = services?.capability || CapabilityService.getInstance(); + this.MemoizedElement = memo(element); } render(props: { @@ -28,7 +36,7 @@ export class Layout { onTouchStart?: (e: TouchEvent) => void; onTouchEnd?: (e: TouchEvent) => void; }) { - return this.element({ + return createElement(this.MemoizedElement, { ...props, onTouchStart: props.onTouchStart, onTouchEnd: props.onTouchEnd diff --git a/frontend/package.json b/frontend/package.json index 1d24f0e..c90c2ff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,12 +29,16 @@ "gsap": "^3.12.5", "html-react-parser": "^5.1.19", "isbot": "^4.1.0", + "markdown-it": "^14.1.0", + "markdown-it-toc-done-right": "^4.2.0", "r": "^0.0.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.6.1", "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.0", + "remark-parse": "^11.0.0", "three": "^0.171.0" }, "devDependencies": { @@ -42,6 +46,7 @@ "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/lodash": "^4.17.13", + "@types/markdown-it": "^14.1.2", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.7.4", diff --git a/frontend/themes/echoes/about.tsx b/frontend/themes/echoes/about.tsx index 4dffd3a..6ea0d01 100644 --- a/frontend/themes/echoes/about.tsx +++ b/frontend/themes/echoes/about.tsx @@ -8,7 +8,7 @@ import { } from "@radix-ui/react-icons"; import { useEffect, useRef, useState } from "react"; import { gsap } from "gsap"; -import { ParticleImage } from "hooks/particleImage"; +import { ImageLoader } from "hooks/ParticleImage"; const socialLinks = [ { @@ -93,7 +93,11 @@ export default new Template({}, ({ http, args }) => {
- +
diff --git a/frontend/themes/echoes/article.tsx b/frontend/themes/echoes/article.tsx index a8b1039..9bec6f1 100644 --- a/frontend/themes/echoes/article.tsx +++ b/frontend/themes/echoes/article.tsx @@ -9,7 +9,7 @@ import { import { Post, PostDisplay, Tag } from "interface/fields"; import { useMemo } from "react"; -import { ImageLoader } from "hooks/particleImage"; +import { ImageLoader } from "hooks/ParticleImage"; import { getColorScheme, hashString } from "themes/echoes/utils/colorScheme"; // 修改模拟文章列表数据 @@ -20,7 +20,7 @@ const mockArticles: PostDisplay[] = [ content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...", authorName: "张三", publishedAt: new Date("2024-03-15"), - coverImage: "", + coverImage: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=500&auto=format", metaKeywords: "", metaDescription: "", status: "published", @@ -42,7 +42,7 @@ const mockArticles: PostDisplay[] = [ content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...", authorName: "李四", publishedAt: new Date("2024-03-14"), - coverImage: "https://haowallpaper.com/link/common/file/previewFileIm", + coverImage: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=500&auto=format", metaKeywords: "", metaDescription: "", status: "published", @@ -63,7 +63,7 @@ const mockArticles: PostDisplay[] = [ content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...", authorName: "王五", publishedAt: new Date("2024-03-13"), - coverImage: "https://haowallpaper.com/link/common/file/previewFileImg/15789130517090624", + coverImage: "https://images.unsplash.com/photo-1592609931095-54a2168ae893?w=500&auto=format", metaKeywords: "", metaDescription: "", status: "published", @@ -84,7 +84,7 @@ const mockArticles: PostDisplay[] = [ content: "移动端开发中的各种适配问题及解决方案...", authorName: "田六", publishedAt: new Date("2024-03-13"), - coverImage: "https://avatars.githubusercontent.com/u/2?v=4", + coverImage: "https://images.unsplash.com/photo-1526498460520-4c246339dccb?w=500&auto=format", metaKeywords: "", metaDescription: "", status: "published", @@ -105,7 +105,7 @@ const mockArticles: PostDisplay[] = [ content: "本文将深入探讨现代全栈开发的各个方面,包括前端框架选择、后端架构设计、数据库优化、微服务部署以及云原生实践...", authorName: "赵七", publishedAt: new Date("2024-03-12"), - coverImage: "https://avatars.githubusercontent.com/u/3?v=4", + coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format", metaKeywords: "", metaDescription: "", status: "published", @@ -136,7 +136,7 @@ const mockArticles: PostDisplay[] = [ content: "探索 TypeScript 的高级类型系统、装饰器、类型编程等特性,以及在大型项目中的最佳实践...", authorName: "孙八", publishedAt: new Date("2024-03-11"), - coverImage: "https://avatars.githubusercontent.com/u/4?v=4", + coverImage: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format", metaKeywords: "", metaDescription: "", status: "published", @@ -160,7 +160,7 @@ const mockArticles: PostDisplay[] = [ content: "全面解析 Web 性能优化策略,包括资源加载优化、渲染性能优化、网络优化等多个维度...", authorName: "周九", publishedAt: new Date("2024-03-10"), - coverImage: "https://avatars.githubusercontent.com/u/5?v=4", + coverImage: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=500&auto=format", metaKeywords: "", metaDescription: "", status: "published", @@ -173,7 +173,7 @@ const mockArticles: PostDisplay[] = [ ], tags: [ { name: "性能监控" }, - { name: "懒加载" }, + { name: "懒加���" }, { name: "缓存策略" }, { name: "代码分割" } ] @@ -184,7 +184,7 @@ const mockArticles: PostDisplay[] = [ content: "详细介绍微前端的架构设计、实现方案、应用集成以及实际项目中的经验总结...", authorName: "吴十", publishedAt: new Date("2024-03-09"), - coverImage: "https://avatars.githubusercontent.com/u/6?v=4", + coverImage: "https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=500&auto=format", metaKeywords: "", metaDescription: "", status: "published", diff --git a/frontend/themes/echoes/layout.tsx b/frontend/themes/echoes/layout.tsx index ea6b3bb..335a3b8 100644 --- a/frontend/themes/echoes/layout.tsx +++ b/frontend/themes/echoes/layout.tsx @@ -1,6 +1,6 @@ import { Layout } from "interface/layout"; -import { ThemeModeToggle } from "hooks/themeMode"; -import { Echoes } from "hooks/echoes"; +import { ThemeModeToggle } from "hooks/ThemeMode"; +import { Echoes } from "hooks/Echoes"; import { Container, Flex, Box, Link, TextField, Button } from "@radix-ui/themes"; import { MagnifyingGlassIcon, @@ -187,8 +187,8 @@ export default new Layout(({ children, args }) => { 0 - ? 'relative translate-x-0 opacity-100 transition-all duration-300 ease-out' - : 'pointer-events-none absolute translate-x-2 opacity-0 transition-all duration-300 ease-in' + ? 'block' + : 'hidden' }`} > + + + {/* 主题切换按钮 - 占据剩余空间 */} + + + +
+ diff --git a/frontend/themes/echoes/post.tsx b/frontend/themes/echoes/post.tsx index 558c1f8..37994d4 100644 --- a/frontend/themes/echoes/post.tsx +++ b/frontend/themes/echoes/post.tsx @@ -1,115 +1,304 @@ +import React, { useMemo, useState,useContext, useCallback, useRef, useEffect } from "react"; import { Template } from "interface/template"; -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; -import { - Container, - Heading, - Text, - Flex, - Box, - Avatar, - Button, - Code, - ScrollArea, - Tabs, - Card, -} from "@radix-ui/themes"; +import ReactMarkdown from "react-markdown"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { oneLight } from "react-syntax-highlighter/dist/cjs/styles/prism"; import { - CalendarIcon, - HeartIcon, - ChatBubbleIcon, - Share1Icon, - BookmarkIcon, - EyeOpenIcon, - CodeIcon, -} from "@radix-ui/react-icons"; -import { Post, Category, Tag, PostDisplay } from "interface/fields"; -import { useMemo, useState, useEffect } from "react"; -import type { Components } from 'react-markdown'; + Container, + Heading, + Text, + Flex, + Box, + Button, + ScrollArea, +} from "@radix-ui/themes"; +import { CalendarIcon, CodeIcon } from "@radix-ui/react-icons"; +import type { PostDisplay } from "interface/fields"; import type { MetaFunction } from "@remix-run/node"; -import { getColorScheme, hashString } from "themes/echoes/utils/colorScheme"; +import { getColorScheme } from "themes/echoes/utils/colorScheme"; +import MarkdownIt from 'markdown-it'; +import { ComponentPropsWithoutRef } from 'react'; +import remarkGfm from 'remark-gfm'; // 示例文章数据 const mockPost: PostDisplay = { id: 1, - title: "构建现代化的前端开发工作流", + title: "现代前端开发完全指南", content: ` -# 构建现代化的前端开发工作流 +# 现代前端开发完全指南 -在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要。本文将详细介绍如何建一个现代化的前端开发工作流。 +前端开发已经成为软件开发中最重要的领域之一。本全面介绍现代前端开发的各个方面。 -## 工具链选择 +![Modern Frontend Development](https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=1200&h=600) -选择合适的工具链工作流的第一我们需要考虑 +## 1. 开发环境搭建 -- 包管理器:npm、yarn 或 pnpm -- 构建工具:Vite、webpack 或 Rollup -- 代码规范:ESLint、Prettier -- 类型检查:TypeScript +在开始前端开发之前,我们要搭建合适的开发环境。 -## 开发环境配置 -### 开发环境配置 +### 1.1 必备工具安装 -良好的开发环境配置可以大大提升开发效率: +开发环境需要安装以下工具: -\`\`\`json -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true +\`\`\`bash +# 安装 Node.js +brew install node + +# 安装包管理器 +npm install -g pnpm + +# 安装开发工具 +pnpm install -g typescript vite +\`\`\` + +### 1.2 编辑器配置 + +推荐使用 VS Code 作为开发工具,需要安装以下插件: + +- ESLint +- Prettier +- TypeScript Vue Plugin +- Tailwind CSS IntelliSense + +![VS Code Setup](https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=1200&h=600) + +## 2. 项目架构设计 + +### 2.1 目录结构 +### 2.1 目录结构 +### 2.1 目录结构 +### 2.1 目录结构 +### 2.1 目录结构 +### 2.1 目录结构 +### 2.1 目录结构 +### 2.1 目录结构 + +一个良好的项目结构对于项目的可维护性至关重要。 + +\`\`\`typescript +// 推荐的项目结构 +interface ProjectStructure { + src: { + components: { + common: string[]; // 通用组件 + features: string[]; // 功能组件 + layouts: string[]; // 布局组件 + }; + pages: string[]; // 页面组件 + hooks: string[]; // 定 hooks + utils: string[]; // 工具函数 + types: string[]; // 类型定义 + styles: string[]; // 样式文件 } } \`\`\` +### 2.2 状态管理 -\`\`\`JavaScript -let a=1 +现代前端应用需要高效的状态管理方案: + +![State Management](https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?w=1200&h=600) + +## 3. 性能优化 + +### 3.1 加载性能 + +关键的加载性能指标: + +| 指标 | 目标值 | 优化方法 | +|------|--------|----------| +| FCP | < 2s | 路由懒加载 | +| TTI | < 3.5s | 代码分割 | +| LCP | < 2.5s | 图片优化 | + +### 3.2 运行时性能 + +#### 3.2.1 虚拟列表 + +处理大数据列表时的示例代码: + +\`\`\`typescript +interface VirtualListProps { + items: any[]; + height: number; + itemHeight: number; + renderItem: (item: any) => React.ReactNode; +} + +const VirtualList: React.FC = ({ + items, + height, + itemHeight, + renderItem +}) => { + // 现码... +}; \`\`\` -## 自动化流程 +#### 3.2.2 防抖与节流 -通过 GitHub Actions 等工具,我们可以实现: +\`\`\`typescript +// 防抖函数实现 +function debounce any>( + fn: T, + delay: number +): (...args: Parameters) => void { + let timeoutId: NodeJS.Timeout; + + return function (...args: Parameters) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(...args), delay); + }; +} +\`\`\` -- 自动化测试 -- 自动化部署 -- 代码质量检查 +### 3.3 构建优化 + +![Build Optimization](https://images.unsplash.com/photo-1551033406-611cf9a28f67?w=1200&h=600) + +## 4. 测试略 + +### 4.1 单元测试 + +使用 Jest 进行单元测试: + +\`typescript +describe('Utils', () => { + test('debounce should work correctly', (done) => { + let counter = 0; + const increment = () => counter++; + const debouncedIncrement = debounce(increment, 100); + + debouncedIncrement(); + debouncedIncrement(); + debouncedIncrement(); + + expect(counter).toBe(0); + + setTimeout(() => { + expect(counter).toBe(1); + done(); + }, 150); + }); +}); +\`\`\` + +### 4.2 集成测试 + +使用 Cypress 进行端到端测试。 + +![Testing](https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=1200&h=600) + +## 5. 部署与监控 + +### 5.1 CI/CD 配置 + +\`\`\`yaml +name: Deploy +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: pnpm install + - name: Build + run: pnpm build + - name: Deploy + run: pnpm deploy +\`\`\` + +### 5.2 监控系统 + +#### 5.2.1 性能监控 + +关键指标监控: + +- 页面加载时间 +- 首次内容绘制 +- 首次大内容绘制 +- 首次输入延迟 + +#### 5.2.2 错误监控 + +错误示例: + +\`\`\`typescript +interface ErrorReport { + type: 'error' | 'warning'; + message: string; + stack?: string; + timestamp: number; + userAgent: string; +} + +function reportError(error: Error): void { + const report: ErrorReport = { + type: 'error', + message: error.message, + stack: error.stack, + timestamp: Date.now(), + userAgent: navigator.userAgent + }; + + // 发送错误报告 + sendErrorReport(report); +} +\`\` + +## 6. 安全最佳实践 + +### 6.1 XSS 防护 + +\`\`\`typescript +// 安全的 HTML 转义函数 +function escapeHtml(unsafe: string): string { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} +\`\`\` + +### 6.2 CSRF 防护 + +![Security](https://images.unsplash.com/photo-1555949963-aa79dcee981c?w=1200&h=600) + +## 总结 + +现代前端开发是一个复杂的系统工程,需要我们在以下方面不断精进: + +1. 工程化能力 +2. 性能优化 +3. 测试覆盖 +4. 全防护 +5. 部署监控 + +> 持续学习实践是提高端开发水平的关键。 + +相关资源: +- [MDN Web Docs](https://developer.mozilla.org/) +- [Web.dev](https://web.dev/) +- [GitHub](https://github.com/) `, authorName: "张三", publishedAt: new Date("2024-03-15"), coverImage: "", - metaKeywords: "前端开发,工作流,效率", - metaDescription: "探讨如何构现代的前端开发工作流,提高开发效率。", + metaKeywords: "前端开发,工程,效率", + metaDescription: "探讨如何构建高效的前端开发高开发效率", status: "published", isEditor: true, createdAt: new Date("2024-03-15"), updatedAt: new Date("2024-03-15"), - categories: [ - { name: "前端开发" } - ], - tags: [ - { name: "工程化" }, - { name: "效率提升" }, - { name: "开发工具" } - ] + categories: [{ name: "前端开发" }], + tags: [{ name: "工程化" }, { name: "效率提升" }, { name: "发工具" }], }; -// 添加标题项接口 -interface TocItem { - id: string; - text: string; - level: number; -} - -// 在 TocItem 接口旁添加 -interface MarkdownCodeProps { - inline?: boolean; - className?: string; - children: React.ReactNode; -} // 添 meta 函数 export const meta: MetaFunction = () => { @@ -117,7 +306,7 @@ export const meta: MetaFunction = () => { { title: mockPost.title }, { name: "description", content: mockPost.metaDescription }, { name: "keywords", content: mockPost.metaKeywords }, - // 添加 Open Graph 标签 + // 添加 Open Graph 标 { property: "og:title", content: mockPost.title }, { property: "og:description", content: mockPost.metaDescription }, { property: "og:image", content: mockPost.coverImage }, @@ -130,20 +319,13 @@ export const meta: MetaFunction = () => { ]; }; -// 添加接口定义 -interface HeadingProps extends React.HTMLAttributes { - node?: any; - level?: number; - ordered?: boolean; - children: React.ReactNode; -} -// 添加复制功能的接口 +// 添加复制能的接口 interface CopyButtonProps { code: string; } -// 添加 CopyButton 组件 +// 加 CopyButton 组件 const CopyButton: React.FC = ({ code }) => { const [copied, setCopied] = useState(false); @@ -154,56 +336,274 @@ const CopyButton: React.FC = ({ code }) => { }; return ( - ); }; -// 创建一 React 组件 -export default new Template({}, ({ http, args }) => { - const [toc, setToc] = useState([]); - const [activeId, setActiveId] = useState(''); +interface TocItem { + id: string; + text: string; + level: number; +} + +// 修改 generateSequentialId 函数的实现 +const generateSequentialId = (() => { + const idMap = new Map(); + + return (postId: string, reset = false) => { + if (reset) { + idMap.delete(postId); + return ''; + } + + if (!idMap.has(postId)) { + idMap.set(postId, 0); + } + + const counter = idMap.get(postId)!; + const id = `heading-${postId}-${counter}`; + idMap.set(postId, counter + 1); + return id; + }; +})(); + +export default new Template({}, ({ http, args }) => { + const [toc, setToc] = useState([]); + const [tocItems, setTocItems] = useState([]); + const [activeId, setActiveId] = useState(""); + const contentRef = useRef(null); + const [showToc, setShowToc] = useState(false); + const [isMounted, setIsMounted] = useState(false); + const [headingIdsArrays, setHeadingIdsArrays] = useState<{[key: string]: string[]}>({}); + const headingIds = useRef([]); // 保持原有的 ref + const containerRef = useRef(null); + const isClickScrolling = useRef(false); - // 解析文章内容生成目录 useEffect(() => { - const parseHeadings = (content: string) => { - const lines = content.split('\n'); - const headings: TocItem[] = []; - let counts = new Map(); + if (typeof window === 'undefined') return; + + const md = new MarkdownIt(); + const tocArray: TocItem[] = []; + + // 重置计数器,传入文章ID + generateSequentialId(mockPost.id.toString(), true); + + let isInCodeBlock = false; + + md.renderer.rules.fence = (tokens, idx, options, env, self) => { + isInCodeBlock = true; + const result = self.renderToken(tokens, idx, options); + isInCodeBlock = false; + return result; + }; + + md.renderer.rules.heading_open = (tokens, idx) => { + const token = tokens[idx]; + const level = parseInt(token.tag.slice(1)); - lines.forEach((line) => { - const match = line.match(/^(#{1,6})\s+(.+)$/); - if (match) { - const level = match[1].length; - const text = match[2]; - - // 为每个级别维护计数 - const count = (counts.get(level) || 0) + 1; - counts.set(level, count); - - // 生成唯一 ID - const id = `heading-${level}-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${count}`; - headings.push({ id, text, level }); - } - }); - - return headings; + if (level <= 3 && !isInCodeBlock) { + const content = tokens[idx + 1].content; + // 生成ID时传入文章ID + const id = generateSequentialId(mockPost.id.toString()); + + token.attrSet('id', id); + tocArray.push({ + id, + text: content, + level + }); + } + return md.renderer.renderToken(tokens, idx, md.options); }; - setToc(parseHeadings(mockPost.content)); - }, [mockPost.content]); - - // 监听滚动更新当前标题 - useEffect(() => { - const headings = document.querySelectorAll('h1[id], h2[id], h3[id]'); + md.render(mockPost.content); + const newIds = tocArray.map(item => item.id); + headingIds.current = [...newIds]; + setHeadingIdsArrays(prev => ({ + ...prev, + [mockPost.id]: [...newIds] + })); + + setToc(newIds); + setTocItems(tocArray); + + if (tocArray.length > 0) { + setActiveId(tocArray[0].id); + } + + setIsMounted(true); + }, [mockPost.content, mockPost.id]); + + const components = useMemo(() => ({ + h1: ({ children, ...props }: ComponentPropsWithoutRef<'h1'> & { node?: any }) => { + if (headingIdsArrays[mockPost.id] && headingIds.current.length === 0) { + headingIds.current = [...headingIdsArrays[mockPost.id]]; + } + const headingId = headingIds.current.shift(); + return ( +

+ {children} +

+ ); + }, + h2: ({ children, ...props }: ComponentPropsWithoutRef<'h2'> & { node?: any }) => { + if (headingIdsArrays[mockPost.id] && headingIds.current.length === 0) { + headingIds.current = [...headingIdsArrays[mockPost.id]]; + } + const headingId = headingIds.current.shift(); + return ( +

+ {children} +

+ ); + }, + h3: ({ children, ...props }: ComponentPropsWithoutRef<'h3'> & { node?: any }) => { + if (headingIdsArrays[mockPost.id] && headingIds.current.length === 0) { + headingIds.current = [...headingIdsArrays[mockPost.id]]; + } + const headingId = headingIds.current.shift(); + return ( +

+ {children} +

+ ); + }, + p: ({ children, ...props }: ComponentPropsWithoutRef<'p'>) => ( +

+ {children} +

+ ), + ul: ({ children, ...props }: ComponentPropsWithoutRef<'ul'>) => ( +
    + {children} +
+ ), + ol: ({ children, ...props }: ComponentPropsWithoutRef<'ol'>) => ( +
    + {children} +
+ ), + li: ({ children, ...props }: ComponentPropsWithoutRef<'li'>) => ( +
  • + {children} +
  • + ), + blockquote: ({ children, ...props }: ComponentPropsWithoutRef<'blockquote'>) => ( +
    + {children} +
    + ), + code: ({ inline, className, children, ...props }: ComponentPropsWithoutRef<'code'> & { inline?: boolean }) => { + const match = /language-(\w+)/.exec(className || ""); + const lang = match ? match[1].toLowerCase() : ""; + + return inline ? ( + + {children} + + ) : ( +
    + {/* 标题栏 */} +
    +
    {lang || "text"}
    + +
    + + {/* 代码内容区域 */} +
    +
    +
    + + {String(children).replace(/\n$/, "")} + +
    +
    +
    +
    + ); + }, + // 修改表格相关组件的响应式设计 + table: ({ children, ...props }: ComponentPropsWithoutRef<'table'>) => ( +
    +
    +
    +
    + + {children} +
    +
    +
    +
    +
    + ), + + th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => ( + + {children} + + ), + + td: ({ children, ...props }: ComponentPropsWithoutRef<'td'>) => ( + + {children} + + ), + }), [mockPost.id, headingIdsArrays]); + + // 修改滚动监听逻辑 + useEffect(() => { + if (typeof window === 'undefined') return; + const observer = new IntersectionObserver( (entries) => { + if (isClickScrolling.current) return; + entries.forEach((entry) => { if (entry.isIntersecting) { setActiveId(entry.target.id); @@ -211,43 +611,198 @@ export default new Template({}, ({ http, args }) => { }); }, { + root: containerRef.current, rootMargin: '-80px 0px -80% 0px', threshold: 0.5 } ); - headings.forEach((heading) => observer.observe(heading)); + tocItems.forEach((item) => { + const element = document.getElementById(item.id); + if (element) { + observer.observe(element); + } + }); - return () => observer.disconnect(); + return () => { + tocItems.forEach((item) => { + const element = document.getElementById(item.id); + if (element) { + observer.unobserve(element); + } + }); + }; + }, [tocItems]); + + // 修改点击��理函数 + const handleTocClick = useCallback((e: React.MouseEvent, itemId: string) => { + e.preventDefault(); + const element = document.getElementById(itemId); + const container = document.querySelector("#main-content"); + const contentBox = document.querySelector(".prose"); // 获取实际内容容器 + + if (element && container && contentBox) { + isClickScrolling.current = true; + + const elementRect = element.getBoundingClientRect(); + const contentBoxRect = contentBox.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + // 计算元素相对于内容容器的偏移量 + const relativeTop = elementRect.top - contentBoxRect.top; + + // 计算内容容器相对于滚动容器的偏移量 + const contentOffset = contentBoxRect.top - containerRect.top; + + // 计算最终滚动距离 + const scrollDistance = container.scrollTop + relativeTop + contentOffset; + + container.scrollTo({ + top: scrollDistance, + behavior: "smooth", + }); + + setActiveId(itemId); + + // 滚动完成后重置标记 + const resetTimeout = setTimeout(() => { + isClickScrolling.current = false; + }, 100); + + return () => clearTimeout(resetTimeout); + } }, []); + // 修改��动端目录的渲染逻辑 + const mobileMenu = isMounted && ( + <> + + + {showToc && ( +
    setShowToc(false)} + > +
    e.stopPropagation()} + > +
    + + 目录 + + +
    + + + + +
    +
    + )} + + ); + return ( - - + + {mobileMenu} + + {/* 文章主体 */} - - - {/* 章头部 */} - - + + {/* 头部 */} + + {mockPost.title} - + {/* 作者名字 */} - + {mockPost.authorName} - {/* 分隔符 */} {/* 发布日期 */} - + {mockPost.publishedAt?.toLocaleDateString("zh-CN", { @@ -266,9 +821,9 @@ export default new Template({}, ({ http, args }) => { {mockPost.categories?.map((category) => { const color = getColorScheme(category.name); return ( - { {mockPost.tags?.map((tag) => { const color = getColorScheme(tag.name); return ( - - {tag.name} @@ -309,7 +864,7 @@ export default new Template({}, ({ http, args }) => { - {/* 修改封面图片样式 */} + {/* 修改片样式 */} {mockPost.coverImage && ( { )} - {/* 文章内容 - 优化排版和间距 */} - - { - const text = children?.toString() || ''; - const id = `heading-1-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-1`; - return ( - - {children} - - ); - }, - h2: ({ children, node, ...props }: HeadingProps) => { - const text = children?.toString() || ''; - const id = `heading-2-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-1`; - return ( - - {children} - - ); - }, - h3: ({ children, node, ...props }: HeadingProps) => { - const text = children?.toString() || ''; - const id = `heading-3-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-1`; - return ( - - {children} - - ); - }, - code: ({ inline, className, children }: MarkdownCodeProps) => { - const match = /language-(\w+)/.exec(className || ''); - const lang = match ? match[1].toLowerCase() : ''; - - return inline ? ( - - {children} - - ) : ( -
    -                        
    - {/* 左侧语言类型 */} -
    - {lang || 'text'} -
    - {/* 右侧复制按钮 */} - -
    - - {String(children).replace(/\n$/, '')} - -
    - ); - }, - p: ({ children }) => ( - - {children} - - ), - ul: ({ children }) => ( -
      - {children} -
    - ), - ol: ({ children }) => ( -
      - {children} -
    - ), - li: ({ children }) => ( -
  • - {children} -
  • - ), - blockquote: ({ children }) => ( -
    - {children} -
    - ), - strong: ({ children }) => ( - - {children} - - ), - a: ({ children, href }) => ( - - {children} - - ), - } as Partial} - > - {mockPost.content} -
    + {/* 内容区域 */} + +
    + + {mockPost.content} + +
    - {/* 右侧目录 */} - + {/* 侧边目录 */} + - 目录 - - - {toc.map((item) => ( - = 4 ? 'pl-3 ml-12 border-[--gray-5]' : ''} - ${activeId === item.id - ? 'text-[--accent-11] font-medium border-[--accent-9]' - : 'text-[--gray-11] hover:text-[--gray-12] hover:border-[--gray-8]' - } - `} - onClick={(e) => { - e.preventDefault(); - const element = document.getElementById(item.id); - if (element) { - element.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); - setActiveId(item.id); - } - }} - > - {item.text} - - ))} + + + {tocItems.map((item, index) => { + if (item.level > 3) return null; + return ( + handleTocClick(e, item.id)} + > + {item.text} + + ); + })} @@ -516,4 +951,4 @@ export default new Template({}, ({ http, args }) => {
    ); -}) +}); diff --git a/frontend/themes/echoes/utils/colorScheme.ts b/frontend/themes/echoes/utils/colorScheme.ts index ca0b162..21f1bc0 100644 --- a/frontend/themes/echoes/utils/colorScheme.ts +++ b/frontend/themes/echoes/utils/colorScheme.ts @@ -11,18 +11,26 @@ export function hashString(str: string): number { export function getColorScheme(name: string) { const colorSchemes = [ - 'amber', 'blue', 'crimson', 'cyan', 'grass', - 'green', 'indigo', 'orange', 'pink', 'purple' + { name: 'amber', bg: 'bg-[--amber-a3]', text: 'text-[--amber-11]', border: 'border-[--amber-a6]' }, + { name: 'blue', bg: 'bg-[--blue-a3]', text: 'text-[--blue-11]', border: 'border-[--blue-a6]' }, + { name: 'crimson', bg: 'bg-[--crimson-a3]', text: 'text-[--crimson-11]', border: 'border-[--crimson-a6]' }, + { name: 'cyan', bg: 'bg-[--cyan-a3]', text: 'text-[--cyan-11]', border: 'border-[--cyan-a6]' }, + { name: 'grass', bg: 'bg-[--grass-a3]', text: 'text-[--grass-11]', border: 'border-[--grass-a6]' }, + { name: 'mint', bg: 'bg-[--mint-a3]', text: 'text-[--mint-11]', border: 'border-[--mint-a6]' }, + { name: 'orange', bg: 'bg-[--orange-a3]', text: 'text-[--orange-11]', border: 'border-[--orange-a6]' }, + { name: 'pink', bg: 'bg-[--pink-a3]', text: 'text-[--pink-11]', border: 'border-[--pink-a6]' }, + { name: 'plum', bg: 'bg-[--plum-a3]', text: 'text-[--plum-11]', border: 'border-[--plum-a6]' }, + { name: 'violet', bg: 'bg-[--violet-a3]', text: 'text-[--violet-11]', border: 'border-[--violet-a6]' } ]; const index = hashString(name) % colorSchemes.length; - const color = colorSchemes[index]; + const scheme = colorSchemes[index]; return { - bg: `bg-[--${color}-4]`, - text: `text-[--${color}-11]`, - border: `border-[--${color}-6]`, - hover: `hover:bg-[--${color}-5]`, + bg: scheme.bg, + text: scheme.text, + border: scheme.border, + hover: `hover:${scheme.bg.replace('3', '4')}`, dot: `bg-current` }; } \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1726108..ea87d91 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -87,5 +87,17 @@ export default defineConfig(async ({ mode }) => { }, publicDir: resolve(__dirname, "public"), envPrefix: "VITE_", + build: { + rollupOptions: { + output: { + manualChunks: { + three: ['three'], + gsap: ['gsap'] + } + } + }, + // 优化大型依赖的处理 + chunkSizeWarningLimit: 1000 + } }; });