diff --git a/frontend/app/index.css b/frontend/app/index.css index 8df6e9a..76b1470 100644 --- a/frontend/app/index.css +++ b/frontend/app/index.css @@ -6,13 +6,19 @@ :root { --transition-duration: 150ms; --transition-easing: cubic-bezier(0.4, 0, 0.2, 1); + --hljs-theme: 'github'; } -/* 基础过渡效果 */ +:root[class~="dark"] { + --hljs-theme: 'github-dark'; +} + +/* 确保 Radix UI 主题类包裹整个应用 */ .radix-themes { transition: background-color var(--transition-duration) var(--transition-easing), color var(--transition-duration) var(--transition-easing); + min-height: 100%; } /* 基础布局样式 */ @@ -21,3 +27,17 @@ body { height: 100%; } +/* 添加暗色模式支持 */ +.radix-themes-dark { + @apply dark; +} + +/* 隐藏不活跃的主题样式 */ +[data-theme="light"] .hljs-dark { + display: none; +} + +[data-theme="dark"] .hljs-light { + display: none; +} + diff --git a/frontend/app/routes.tsx b/frontend/app/routes.tsx index e95e6ce..37543b7 100644 --- a/frontend/app/routes.tsx +++ b/frontend/app/routes.tsx @@ -3,6 +3,7 @@ import layout from "themes/echoes/layout"; import article from "themes/echoes/article"; import about from "themes/echoes/about"; import { useLocation } from "react-router-dom"; +import post from "themes/echoes/post"; export default function Routes() { const location = useLocation(); @@ -11,26 +12,33 @@ export default function Routes() { const args = { title: "我的页面", theme: "dark", - nav: 'indexerrorabout', + nav: 'indexerroraboutpost', }; console.log(path); - path =path.split("/")[1]; + path = path.split("/")[1]; - if (path[1] === "error") { + if (path === "error") { return layout.render({ children: ErrorPage.render(args), args, }); } - if (path[1] === "about") { + if (path === "about") { return layout.render({ children: about.render(args), args, }); } + if (path === "post") { + return layout.render({ + children: post.render(args), + args, + }); + } + return layout.render({ children: article.render(args), args, diff --git a/frontend/hooks/ParticleImage.tsx b/frontend/hooks/ParticleImage.tsx index 56712d9..726cd0c 100644 --- a/frontend/hooks/ParticleImage.tsx +++ b/frontend/hooks/ParticleImage.tsx @@ -200,6 +200,19 @@ interface ParticleImageProps { onAnimationComplete?: () => void; } +// 修改 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)]', + size: { + container: 'w-[120px] md:w-[140px] h-[120px] md:h-[140px]' + } +}; + export const ParticleImage = ({ src, status, @@ -230,7 +243,7 @@ export const ParticleImage = ({ // 更新渲染器大小 rendererRef.current.setSize(width, height); - // 只有当尺寸变化超过阈值时才重生成粒子 + // 只有当尺寸变化超���阈值时才重生成粒子 const currentSize = Math.min(width, height); const previousSize = sceneRef.current.userData.previousSize || currentSize; const sizeChange = Math.abs(currentSize - previousSize) / previousSize; @@ -324,7 +337,7 @@ export const ParticleImage = ({ rendererRef.current = renderer; containerRef.current.appendChild(renderer.domElement); - // 检查是否应该显示笑脸 + // 检查是否应该显示笑 if (src === '') { const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height); @@ -408,7 +421,7 @@ export const ParticleImage = ({ }; } - // 建错误动��函数 + // 建错误动函数 const showErrorAnimation = () => { if (!scene) return; @@ -451,7 +464,7 @@ export const ParticleImage = ({ }); }; - // 加载图��� + // 加载图 const img = new Image(); img.crossOrigin = 'anonymous'; @@ -650,7 +663,7 @@ export const ParticleImage = ({ img.src = src || ''; - // 动画循环 + // 画循环 const animate = () => { if (renderer && scene && camera) { renderer.render(scene, camera); @@ -771,16 +784,15 @@ export const ImageLoader = ({ }, [src, preloadImage]); return ( -
-
+
+
{ - // 粒子动画完成后,延迟显示图片 setTimeout(() => { setShowImage(true); - }, 800); // 延迟时间可以根据需要调整 + }, 800); }} onAnimationComplete={() => { setShowImage(true); diff --git a/frontend/interface/post.ts b/frontend/interface/post.ts index 529c72b..7d50201 100644 --- a/frontend/interface/post.ts +++ b/frontend/interface/post.ts @@ -5,7 +5,7 @@ export interface Post { title?: string; // 标题 metaKeywords: string; // 元关键词 metaDescription: string; // 元描述 - content: string; // 内容 + content: string; // Markdown 格式的内容 status: string; // 状态 isEditor: boolean; // 是否为编辑器 draftContent?: string; // 草稿内容 diff --git a/frontend/package.json b/frontend/package.json index fc95eae..1d24f0e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,9 @@ "@remix-run/node": "^2.14.0", "@remix-run/react": "^2.14.0", "@remix-run/serve": "^2.14.0", + "@tailwindcss/typography": "^0.5.15", "@types/axios": "^0.14.4", + "@types/react-syntax-highlighter": "^15.5.13", "@types/three": "^0.170.0", "axios": "^1.7.7", "bootstrap-icons": "^1.11.3", @@ -27,8 +29,12 @@ "gsap": "^3.12.5", "html-react-parser": "^5.1.19", "isbot": "^4.1.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", "three": "^0.171.0" }, "devDependencies": { diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 0cad7d2..4af7f84 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,4 +1,5 @@ import type { Config } from "tailwindcss"; +import typography from '@tailwindcss/typography'; export default { content: [ @@ -8,7 +9,7 @@ export default { "./hooks/**/*.{js,jsx,ts,tsx}", "./themes/**/*.{js,jsx,ts,tsx}", ], - darkMode: ["class", '[data-theme="dark"]'], + darkMode: 'class', important: true, theme: { extend: { @@ -29,4 +30,7 @@ export default { }, }, }, + plugins: [ + typography, + ], } satisfies Config; diff --git a/frontend/themes/echoes/article.tsx b/frontend/themes/echoes/article.tsx index 07159b8..6692eb8 100644 --- a/frontend/themes/echoes/article.tsx +++ b/frontend/themes/echoes/article.tsx @@ -149,64 +149,65 @@ export default new Template({}, ({ http, args }) => { return ( -
+
{articleData.map((article) => ( -
- +
+
+ -
-
+
{article.title} + + + {article.content} + +
+
+ +
+
{article.category} + + + + + {article.publishedAt?.toLocaleDateString("zh-CN", { + year: "numeric", + month: "long", + day: "numeric", + })} + + · + + {article.authorName} + +
- - - - {article.publishedAt?.toLocaleDateString("zh-CN", { - year: "numeric", - month: "long", - day: "numeric", - })} - - · - - {article.authorName} - - - - - {article.content} - - - + {article.tags.map((tag) => ( {tag.name} diff --git a/frontend/themes/echoes/layout.tsx b/frontend/themes/echoes/layout.tsx index ced95cd..0e8f5ee 100644 --- a/frontend/themes/echoes/layout.tsx +++ b/frontend/themes/echoes/layout.tsx @@ -130,7 +130,7 @@ export default new Layout(({ children, args }) => { {loginState ? ( <> diff --git a/frontend/themes/echoes/post.tsx b/frontend/themes/echoes/post.tsx new file mode 100644 index 0000000..2dd7ebe --- /dev/null +++ b/frontend/themes/echoes/post.tsx @@ -0,0 +1,336 @@ +import { Template } from "interface/template"; +import ReactMarkdown from 'react-markdown'; +import { PrismLight 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 { + CalendarIcon, + HeartIcon, + ChatBubbleIcon, + Share1Icon, + BookmarkIcon, + EyeOpenIcon, + CodeIcon, +} from "@radix-ui/react-icons"; +import { Post } from "interface/post"; +import { useMemo, useState, useEffect } from "react"; +import type { Components } from 'react-markdown'; + +// 示例文章数据 +const mockPost: Post = { + id: 1, + title: "构建现代化的前端开发工作流", + content: ` +# 构建现代化的前端开发工作流 + +在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要。本文将详细介绍如何建一个现代化的前端开发工作流。 + +## 工具链选择 + +选择合适的工具链是构建高效工作流的第一步。我们需要考虑以下几方 + +- 包管理器:npm、yarn 或 pnpm +- 构建工具:Vite、webpack 或 Rollup +- 代码规范:ESLint、Prettier +- 类型检查:TypeScript + +## 开发环境配置 + +良好的开发环境配置可以大大提升开发效率: + +\`\`\`json +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true + } +} +\`\`\` + +## 自动化流程 + +通过 GitHub Actions 等工具,我们可以实现: + +- 自动化测试 +- 自动化部署 +- 代码质量检查 +`, + authorName: "张三", + publishedAt: new Date("2024-03-15"), + coverImage: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6", + metaKeywords: "前端开发,工作流,效率", + metaDescription: "探讨如何构现代的前端开发工作流,提高开发效率。", + status: "published", + isEditor: true, + createdAt: new Date("2024-03-15"), + updatedAt: new Date("2024-03-15"), +}; + +// 添加标题项接口 +interface TocItem { + id: string; + text: string; + level: number; +} + +// 在 TocItem 接口旁边添加 +interface MarkdownCodeProps { + inline?: boolean; + className?: string; + children: React.ReactNode; +} + +// 创建一个 React 组件 +function PostContent() { + 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 }; + }); + setToc(headings); + }, [mockPost.content]); + + // 监听滚动更新当前标题 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: '-80px 0px -80% 0px' } + ); + + document.querySelectorAll('h1, h2, h3, h4, h5, h6').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()} + + ))} + +
+ + {/* 右侧目录 - 优化样式 */} + + + + + 目录 + + + + {toc.map((item) => ( + 1 ? `ml-${(item.level - 1) * 4}` : ''} + ${activeId === item.id + ? 'text-[--accent-11] font-medium translate-x-1' + : 'text-[--gray-11] hover:text-[--gray-12]' + } + `} + onClick={(e) => { + e.preventDefault(); + document.getElementById(item.id)?.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + }} + > + {item.text} + + ))} + + + + +
+
+ ); +} + +// 使用模板包装组件 +export default new Template({}, ({ http, args }) => { + return ; +});