From 0628d5588f725f8352e011078c6d90c0fda22d99 Mon Sep 17 00:00:00 2001 From: lsy Date: Fri, 6 Dec 2024 15:25:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=87=E7=AB=A0=E5=B1=95=E7=A4=BA=E9=A1=B5?= =?UTF-8?q?=E7=9A=84=E6=8E=92=E7=89=88=E5=92=8C=E6=A0=B7=E5=BC=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/hooks/ParticleImage.tsx | 67 +++- frontend/themes/echoes/layout.tsx | 38 +- frontend/themes/echoes/post.tsx | 465 +++++++++++++--------- frontend/themes/echoes/styles/layouts.css | 31 +- 4 files changed, 384 insertions(+), 217 deletions(-) diff --git a/frontend/hooks/ParticleImage.tsx b/frontend/hooks/ParticleImage.tsx index a647545..47cf3fd 100644 --- a/frontend/hooks/ParticleImage.tsx +++ b/frontend/hooks/ParticleImage.tsx @@ -718,6 +718,7 @@ export const ImageLoader = ({ const timeoutRef = useRef(); const loadingRef = useRef(false); const imageRef = useRef(null); + const containerRef = useRef(null); // 处理图片预加载 const preloadImage = useCallback(() => { @@ -744,9 +745,51 @@ export const ImageLoader = ({ clearTimeout(timeoutRef.current); } + // 在图片加载成功后,立即创建和缓存一个适应容器大小的图片 + if (containerRef.current) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (ctx) { + const containerWidth = containerRef.current.offsetWidth; + const containerHeight = containerRef.current.offsetHeight; + + canvas.width = containerWidth; + canvas.height = containerHeight; + + // 保持比例绘制图片 + const targetAspect = containerWidth / containerHeight; + const imgAspect = img.width / img.height; + + let sourceWidth = img.width; + let sourceHeight = img.height; + let sourceX = 0; + let sourceY = 0; + + if (imgAspect > targetAspect) { + sourceWidth = img.height * targetAspect; + sourceX = (img.width - sourceWidth) / 2; + } else { + sourceHeight = img.width / targetAspect; + sourceY = (img.height - sourceHeight) / 2; + } + + ctx.drawImage( + img, + sourceX, sourceY, sourceWidth, sourceHeight, + 0, 0, containerWidth, containerHeight + ); + + // 创建新的图片对象,使用调整后的canvas数据 + const adjustedImage = new Image(); + adjustedImage.src = canvas.toDataURL(); + imageRef.current = adjustedImage; + } + } else { + imageRef.current = img; + } + loadingRef.current = false; - // 图片加载成功后,不立即显示,等待粒子动画完成 - imageRef.current = img; setStatus({ isLoading: false, hasError: false, @@ -766,7 +809,10 @@ export const ImageLoader = ({ }); }; - img.src = src; + // 确保src存在再设置 + if (src) { + img.src = src; + } }, [src]); useEffect(() => { @@ -781,18 +827,23 @@ export const ImageLoader = ({ }, [src, preloadImage]); return ( -
+
{ - setTimeout(() => { - setShowImage(true); - }, 800); + // 确保图片已经准备好 + if (imageRef.current) { + setTimeout(() => { + setShowImage(true); + }, 800); + } }} onAnimationComplete={() => { - setShowImage(true); + if (imageRef.current) { + setShowImage(true); + } }} />
diff --git a/frontend/themes/echoes/layout.tsx b/frontend/themes/echoes/layout.tsx index 0e8f5ee..44fa5f7 100644 --- a/frontend/themes/echoes/layout.tsx +++ b/frontend/themes/echoes/layout.tsx @@ -54,7 +54,7 @@ export default new Layout(({ children, args }) => { return ( @@ -78,9 +78,9 @@ export default new Layout(({ children, args }) => { - + @@ -98,31 +98,33 @@ export default new Layout(({ children, args }) => { size="2" variant="surface" placeholder="搜索..." - className="w-[240px] [&_input]:pl-3 hover:opacity-70 transition-opacity" + className="w-[240px] [&_input]:pl-3 hover:border-[--accent-9] border transition-colors group" id="search" > - + - {parse(navString)} + + {parse(navString)} + @@ -134,19 +136,19 @@ export default new Layout(({ children, args }) => { > {loginState ? ( <> - + 个人中心 - + 设置 - + 退出登录 ) : ( - + 登录/注册 )} @@ -166,12 +168,12 @@ export default new Layout(({ children, args }) => { @@ -187,7 +189,7 @@ export default new Layout(({ children, args }) => { sideOffset={5} className="mt-2 p-3 min-w-[280px] rounded-md bg-[--color-panel] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2" > - + {parse(navString)} @@ -213,7 +215,7 @@ export default new Layout(({ children, args }) => { )} - {/* 主题切换按钮 */} + {/* 题切换按钮 */} diff --git a/frontend/themes/echoes/post.tsx b/frontend/themes/echoes/post.tsx index 2dd7ebe..c8504b0 100644 --- a/frontend/themes/echoes/post.tsx +++ b/frontend/themes/echoes/post.tsx @@ -1,6 +1,6 @@ import { Template } from "interface/template"; import ReactMarkdown from 'react-markdown'; -import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { Container, @@ -27,19 +27,20 @@ import { import { Post } from "interface/post"; import { useMemo, useState, useEffect } from "react"; import type { Components } from 'react-markdown'; +import type { MetaFunction } from "@remix-run/node"; // 示例文章数据 const mockPost: Post = { id: 1, title: "构建现代化的前端开发工作流", content: ` -# 构建现代化的前端开发工作流 +# 构建现代化的前端开发工作流sssssssssssssssss 在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要。本文将详细介绍如何建一个现代化的前端开发工作流。 ## 工具链选择 -选择合适的工具链是构建高效工作流的第一步。我们需要考虑以下几方 +选择合适的工具链效工作流的第一步。我们需要考虑 - 包管理器:npm、yarn 或 pnpm - 构建工具:Vite、webpack 或 Rollup @@ -47,6 +48,7 @@ const mockPost: Post = { - 类型检查:TypeScript ## 开发环境配置 +### 开发环境配置 良好的开发环境配置可以大大提升开发效率: @@ -62,6 +64,11 @@ const mockPost: Post = { } \`\`\` + +\`\`\`JavaScript +let a=1 +\`\`\` + ## 自动化流程 通过 GitHub Actions 等工具,我们可以实现: @@ -72,7 +79,7 @@ const mockPost: Post = { `, authorName: "张三", publishedAt: new Date("2024-03-15"), - coverImage: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6", + coverImage: "", metaKeywords: "前端开发,工作流,效率", metaDescription: "探讨如何构现代的前端开发工作流,提高开发效率。", status: "published", @@ -95,27 +102,71 @@ interface MarkdownCodeProps { children: React.ReactNode; } -// 创建一个 React 组件 -function PostContent() { +// 添加 meta 函数 +export const meta: MetaFunction = () => { + return [ + { title: mockPost.title }, + { name: "description", content: mockPost.metaDescription }, + { name: "keywords", content: mockPost.metaKeywords }, + // 添加 Open Graph 标签 + { property: "og:title", content: mockPost.title }, + { property: "og:description", content: mockPost.metaDescription }, + { property: "og:image", content: mockPost.coverImage }, + { property: "og:type", content: "article" }, + // 添加 Twitter 卡片标签 + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: mockPost.title }, + { name: "twitter:description", content: mockPost.metaDescription }, + { name: "twitter:image", content: mockPost.coverImage }, + ]; +}; + +// 添加接口定义 +interface HeadingProps extends React.HTMLAttributes { + node?: any; + level?: number; + ordered?: boolean; + children: React.ReactNode; +} + +// 创建一 React 组件 +export default new Template({}, ({ http, args }) => { const [toc, setToc] = useState([]); const [activeId, setActiveId] = useState(''); // 解析文章内容生成目录 useEffect(() => { - const headings = mockPost.content.split('\n') - .filter(line => line.startsWith('#')) - .map(line => { - const match = line.match(/^#+/); - const level = match ? match[0].length : 1; - const text = line.replace(/^#+\s+/, ''); - const id = text.toLowerCase().replace(/\s+/g, '-'); - return { id, text, level }; + const parseHeadings = (content: string) => { + const lines = content.split('\n'); + const headings: TocItem[] = []; + let counts = new Map(); + + 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 }); + } }); - setToc(headings); + + return headings; + }; + + setToc(parseHeadings(mockPost.content)); }, [mockPost.content]); // 监听滚动更新当前标题 useEffect(() => { + const headings = document.querySelectorAll('h1[id], h2[id], h3[id]'); + const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { @@ -124,198 +175,245 @@ function PostContent() { } }); }, - { rootMargin: '-80px 0px -80% 0px' } + { + rootMargin: '-80px 0px -80% 0px', + threshold: 0.5 + } ); - document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((heading) => { - observer.observe(heading); - }); + headings.forEach((heading) => observer.observe(heading)); return () => observer.disconnect(); }, []); return ( - - - {/* 左侧文章主体 */} - - {/* 文章头部 - 增加间距和样式 */} - - - {mockPost.title} - - - - - {mockPost.authorName} - · - - - - {mockPost.publishedAt?.toLocaleDateString("zh-CN", { - year: "numeric", - month: "long", - day: "numeric", - })} - - - - - - {/* 修改封面图片样式 */} - {mockPost.coverImage && ( - - {mockPost.title} - - )} - - {/* 文章内容 - 优化排版和间距 */} - - { - const text = children?.toString() || ''; - return ( -

- {children} -

- ); - }, - h2: ({ children }) => { - const text = children?.toString() || ''; - return ( -

- {children} -

- ); - }, - code: ({ inline, className, children }: MarkdownCodeProps) => { - const match = /language-(\w+)/.exec(className || ''); - const lang = match ? match[1] : ''; - - return inline ? ( - - {children} - - ) : ( -
- {lang && ( -
- {lang} -
- )} - code[class*="language-"]': { - background: 'transparent', - border: 'none', - boxShadow: 'none', - }, - 'pre[class*="language-"]::-moz-selection': { - background: 'transparent', - }, - 'pre[class*="language-"] ::-moz-selection': { - background: 'transparent', - }, - 'code[class*="language-"]::-moz-selection': { - background: 'transparent', - }, - 'code[class*="language-"] ::-moz-selection': { - background: 'transparent', - } - }} - customStyle={{ - margin: 0, - padding: '1.5rem', - background: 'var(--gray-2)', - fontSize: '0.95rem', - lineHeight: '1.5', - border: '1px solid var(--gray-a5)', - borderRadius: '0.75rem', - }} - > - {String(children).replace(/\n$/, '')} - -
- ); - } - } as Partial} - > - {mockPost.content} -
-
- - {/* 优化文章底部标签样式 */} - - {mockPost.metaKeywords.split(',').map((tag) => ( - + + {/* 文章主体 */} + + + {/* 章头部 */} + + - {tag.trim()} - - ))} - + {mockPost.title} + + + + + {mockPost.authorName} + · + + + + {mockPost.publishedAt?.toLocaleDateString("zh-CN", { + year: "numeric", + month: "long", + day: "numeric", + })} + + + +
+ + {/* 修改封面图片样式 */} + {mockPost.coverImage && ( + + {mockPost.title} + + )} + + {/* 文章内容 - 优化排版和间距 */} + + { + 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 && (
+                          
+ {lang} +
+ )} + + {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} +
    +
    +
    - {/* 右侧目录 - 优化样式 */} - - + {/* 右侧目录 */} + + - + 目录 - + {toc.map((item) => ( 1 ? `ml-${(item.level - 1) * 4}` : ''} + block text-xs leading-relaxed transition-all + border-l-2 + ${item.level === 1 ? 'pl-3 border-[--gray-8]' : ''} + ${item.level === 2 ? 'pl-3 ml-4 border-[--gray-7]' : ''} + ${item.level === 3 ? 'pl-3 ml-8 border-[--gray-6]' : ''} + ${item.level >= 4 ? 'pl-3 ml-12 border-[--gray-5]' : ''} ${activeId === item.id - ? 'text-[--accent-11] font-medium translate-x-1' - : 'text-[--gray-11] hover:text-[--gray-12]' + ? 'text-[--accent-11] font-medium border-[--accent-9]' + : 'text-[--gray-11] hover:text-[--gray-12] hover:border-[--gray-8]' } `} onClick={(e) => { e.preventDefault(); - document.getElementById(item.id)?.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); + const element = document.getElementById(item.id); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + setActiveId(item.id); + } }} > {item.text} @@ -328,9 +426,4 @@ function PostContent() {
    ); -} - -// 使用模板包装组件 -export default new Template({}, ({ http, args }) => { - return ; -}); +}) diff --git a/frontend/themes/echoes/styles/layouts.css b/frontend/themes/echoes/styles/layouts.css index 888d280..ef46243 100644 --- a/frontend/themes/echoes/styles/layouts.css +++ b/frontend/themes/echoes/styles/layouts.css @@ -4,11 +4,12 @@ #nav a { position: relative; - transition: opacity 0.2s ease; + transition: all 0.2s ease; + color: var(--gray-11); } #nav a:hover { - opacity: 0.8; + color: var(--accent-9); } #nav a::after { @@ -18,7 +19,7 @@ bottom: -3px; width: 100%; height: 2px; - background-color: var(--gray-a11); + background-color: var(--accent-9); transform: scaleX(0); transition: transform 0.3s ease; } @@ -27,7 +28,27 @@ transform: scaleX(1); } - #search { - color: var(--gray-a12); + color: var(--gray-12); +} + +a:not(#nav a) { + transition: color 0.2s ease; + color: var(--gray-12); +} + +a:not(#nav a):hover { + color: var(--accent-12); +} + +button:hover { + color: var(--accent-12); +} + +.card:hover { + border-color: var(--accent-9); +} + +.card:hover .card-title { + color: var(--accent-12); }