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 && (
+
+
+
+ )}
+
+ {/* 文章内容 - 优化排版和间距 */}
+
+ {
+ 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 ;
+});