前端:更新依赖项以支持Tailwind CSS排版插件,更新配色,文章展示页排版,新增文章
This commit is contained in:
parent
caa23c6ac5
commit
9eada9af3b
@ -6,13 +6,19 @@
|
|||||||
:root {
|
:root {
|
||||||
--transition-duration: 150ms;
|
--transition-duration: 150ms;
|
||||||
--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--hljs-theme: 'github';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 基础过渡效果 */
|
:root[class~="dark"] {
|
||||||
|
--hljs-theme: 'github-dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保 Radix UI 主题类包裹整个应用 */
|
||||||
.radix-themes {
|
.radix-themes {
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition-duration) var(--transition-easing),
|
background-color var(--transition-duration) var(--transition-easing),
|
||||||
color var(--transition-duration) var(--transition-easing);
|
color var(--transition-duration) var(--transition-easing);
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 基础布局样式 */
|
/* 基础布局样式 */
|
||||||
@ -21,3 +27,17 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 添加暗色模式支持 */
|
||||||
|
.radix-themes-dark {
|
||||||
|
@apply dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏不活跃的主题样式 */
|
||||||
|
[data-theme="light"] .hljs-dark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .hljs-light {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import layout from "themes/echoes/layout";
|
|||||||
import article from "themes/echoes/article";
|
import article from "themes/echoes/article";
|
||||||
import about from "themes/echoes/about";
|
import about from "themes/echoes/about";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import post from "themes/echoes/post";
|
||||||
|
|
||||||
export default function Routes() {
|
export default function Routes() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -11,26 +12,33 @@ export default function Routes() {
|
|||||||
const args = {
|
const args = {
|
||||||
title: "我的页面",
|
title: "我的页面",
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
nav: '<a href="/">index</a><a href="error">error</a><a href="about">about</a>',
|
nav: '<a href="/">index</a><a href="/error">error</a><a href="/about">about</a><a href="/post">post</a>',
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(path);
|
console.log(path);
|
||||||
path =path.split("/")[1];
|
path = path.split("/")[1];
|
||||||
|
|
||||||
if (path[1] === "error") {
|
if (path === "error") {
|
||||||
return layout.render({
|
return layout.render({
|
||||||
children: ErrorPage.render(args),
|
children: ErrorPage.render(args),
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path[1] === "about") {
|
if (path === "about") {
|
||||||
return layout.render({
|
return layout.render({
|
||||||
children: about.render(args),
|
children: about.render(args),
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path === "post") {
|
||||||
|
return layout.render({
|
||||||
|
children: post.render(args),
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return layout.render({
|
return layout.render({
|
||||||
children: article.render(args),
|
children: article.render(args),
|
||||||
args,
|
args,
|
||||||
|
@ -200,6 +200,19 @@ interface ParticleImageProps {
|
|||||||
onAnimationComplete?: () => void;
|
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 = ({
|
export const ParticleImage = ({
|
||||||
src,
|
src,
|
||||||
status,
|
status,
|
||||||
@ -230,7 +243,7 @@ export const ParticleImage = ({
|
|||||||
// 更新渲染器大小
|
// 更新渲染器大小
|
||||||
rendererRef.current.setSize(width, height);
|
rendererRef.current.setSize(width, height);
|
||||||
|
|
||||||
// 只有当尺寸变化超过阈值时才重生成粒子
|
// 只有当尺寸变化超<EFBFBD><EFBFBD><EFBFBD>阈值时才重生成粒子
|
||||||
const currentSize = Math.min(width, height);
|
const currentSize = Math.min(width, height);
|
||||||
const previousSize = sceneRef.current.userData.previousSize || currentSize;
|
const previousSize = sceneRef.current.userData.previousSize || currentSize;
|
||||||
const sizeChange = Math.abs(currentSize - previousSize) / previousSize;
|
const sizeChange = Math.abs(currentSize - previousSize) / previousSize;
|
||||||
@ -324,7 +337,7 @@ export const ParticleImage = ({
|
|||||||
rendererRef.current = renderer;
|
rendererRef.current = renderer;
|
||||||
containerRef.current.appendChild(renderer.domElement);
|
containerRef.current.appendChild(renderer.domElement);
|
||||||
|
|
||||||
// 检查是否应该显示笑脸
|
// 检查是否应该显示笑
|
||||||
if (src === '') {
|
if (src === '') {
|
||||||
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
||||||
|
|
||||||
@ -408,7 +421,7 @@ export const ParticleImage = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 建错误动<EFBFBD><EFBFBD>函数
|
// 建错误动函数
|
||||||
const showErrorAnimation = () => {
|
const showErrorAnimation = () => {
|
||||||
if (!scene) return;
|
if (!scene) return;
|
||||||
|
|
||||||
@ -451,7 +464,7 @@ export const ParticleImage = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载图<EFBFBD><EFBFBD><EFBFBD>
|
// 加载图
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = 'anonymous';
|
img.crossOrigin = 'anonymous';
|
||||||
|
|
||||||
@ -650,7 +663,7 @@ export const ParticleImage = ({
|
|||||||
|
|
||||||
img.src = src || '';
|
img.src = src || '';
|
||||||
|
|
||||||
// 动画循环
|
// 画循环
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
if (renderer && scene && camera) {
|
if (renderer && scene && camera) {
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
@ -771,16 +784,15 @@ export const ImageLoader = ({
|
|||||||
}, [src, preloadImage]);
|
}, [src, preloadImage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-[140px] md:w-[180px] h-[140px] md:h-[180px] shrink-0 overflow-hidden">
|
<div className={`relative ${BG_CONFIG.size.container} shrink-0 overflow-hidden`}>
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[rgb(10,37,77)] via-[rgb(8,27,57)] to-[rgb(2,8,23)] rounded-lg overflow-hidden">
|
<div className={`absolute inset-0 ${BG_CONFIG.className} rounded-lg overflow-hidden`}>
|
||||||
<ParticleImage
|
<ParticleImage
|
||||||
src={src}
|
src={src}
|
||||||
status={status}
|
status={status}
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
// 粒子动画完成后,延迟显示图片
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowImage(true);
|
setShowImage(true);
|
||||||
}, 800); // 延迟时间可以根据需要调整
|
}, 800);
|
||||||
}}
|
}}
|
||||||
onAnimationComplete={() => {
|
onAnimationComplete={() => {
|
||||||
setShowImage(true);
|
setShowImage(true);
|
||||||
|
@ -5,7 +5,7 @@ export interface Post {
|
|||||||
title?: string; // 标题
|
title?: string; // 标题
|
||||||
metaKeywords: string; // 元关键词
|
metaKeywords: string; // 元关键词
|
||||||
metaDescription: string; // 元描述
|
metaDescription: string; // 元描述
|
||||||
content: string; // 内容
|
content: string; // Markdown 格式的内容
|
||||||
status: string; // 状态
|
status: string; // 状态
|
||||||
isEditor: boolean; // 是否为编辑器
|
isEditor: boolean; // 是否为编辑器
|
||||||
draftContent?: string; // 草稿内容
|
draftContent?: string; // 草稿内容
|
||||||
|
@ -18,7 +18,9 @@
|
|||||||
"@remix-run/node": "^2.14.0",
|
"@remix-run/node": "^2.14.0",
|
||||||
"@remix-run/react": "^2.14.0",
|
"@remix-run/react": "^2.14.0",
|
||||||
"@remix-run/serve": "^2.14.0",
|
"@remix-run/serve": "^2.14.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/axios": "^0.14.4",
|
"@types/axios": "^0.14.4",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/three": "^0.170.0",
|
"@types/three": "^0.170.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
@ -27,8 +29,12 @@
|
|||||||
"gsap": "^3.12.5",
|
"gsap": "^3.12.5",
|
||||||
"html-react-parser": "^5.1.19",
|
"html-react-parser": "^5.1.19",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
|
"r": "^0.0.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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"
|
"three": "^0.171.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
import typography from '@tailwindcss/typography';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
@ -8,7 +9,7 @@ export default {
|
|||||||
"./hooks/**/*.{js,jsx,ts,tsx}",
|
"./hooks/**/*.{js,jsx,ts,tsx}",
|
||||||
"./themes/**/*.{js,jsx,ts,tsx}",
|
"./themes/**/*.{js,jsx,ts,tsx}",
|
||||||
],
|
],
|
||||||
darkMode: ["class", '[data-theme="dark"]'],
|
darkMode: 'class',
|
||||||
important: true,
|
important: true,
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
@ -29,4 +30,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
typography,
|
||||||
|
],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
@ -149,64 +149,65 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="3" className="pt-2 pb-4 md:pb-6 relative">
|
<Container size="3" className="pt-2 pb-4 md:pb-6 relative">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 px-4 md:px-0">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6 px-4 md:px-0">
|
||||||
{articleData.map((article) => (
|
{articleData.map((article) => (
|
||||||
<Card
|
<Card
|
||||||
key={article.id}
|
key={article.id}
|
||||||
className="group cursor-pointer hover:shadow-lg transition-all duration-300 border border-[--gray-5] hover:border-[--accent-8] relative overflow-hidden"
|
className="group cursor-pointer hover:shadow-lg transition-all duration-300 border border-[--gray-5] hover:border-[--accent-8] relative overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className={`p-5 relative flex gap-4`}>
|
<div className="p-4 relative flex flex-col gap-4">
|
||||||
<ImageLoader
|
<div className="flex gap-4">
|
||||||
src={article.coverImage}
|
<ImageLoader
|
||||||
alt={article.title || ""}
|
src={article.coverImage}
|
||||||
className="group-hover:scale-105 transition-transform duration-500 relative z-[1] w-[140px] h-[140px] md:w-[180px] md:h-[180px] object-cover rounded-lg shrink-0"
|
alt={article.title || ""}
|
||||||
/>
|
className="group-hover:scale-105 transition-transform duration-500 relative z-[1] object-cover rounded-lg shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-start justify-between gap-3 mb-2">
|
|
||||||
<Heading
|
<Heading
|
||||||
size="3"
|
size="3"
|
||||||
className="group-hover:text-[--accent-9] transition-colors duration-200 line-clamp-2 text-base md:text-lg flex-1"
|
className="group-hover:text-[--accent-9] transition-colors duration-200 line-clamp-2 text-base mb-2"
|
||||||
>
|
>
|
||||||
{article.title}
|
{article.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
|
<Text className="text-[--gray-11] text-xs md:text-sm line-clamp-2 leading-relaxed">
|
||||||
|
{article.content}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Text
|
<Text
|
||||||
size="1"
|
size="1"
|
||||||
className={`px-2 py-0.5 rounded-full shrink-0 ${article.categoryColor.bg} ${article.categoryColor.text}`}
|
className={`px-2 py-0.5 rounded-full font-medium ${article.categoryColor.bg} ${article.categoryColor.text}`}
|
||||||
>
|
>
|
||||||
{article.category}
|
{article.category}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<Flex gap="2" align="center" className="text-[--gray-11]">
|
||||||
|
<CalendarIcon className="w-3 h-3" />
|
||||||
|
<Text size="1">
|
||||||
|
{article.publishedAt?.toLocaleDateString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<span className="mx-1">·</span>
|
||||||
|
<Text size="1" weight="medium">
|
||||||
|
{article.authorName}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Flex
|
<Flex gap="2" className="flex-wrap">
|
||||||
gap="2"
|
|
||||||
align="center"
|
|
||||||
className="text-[--gray-11] mb-3 flex-wrap"
|
|
||||||
>
|
|
||||||
<CalendarIcon className="w-3 h-3" />
|
|
||||||
<Text size="1">
|
|
||||||
{article.publishedAt?.toLocaleDateString("zh-CN", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
<span className="mx-1">·</span>
|
|
||||||
<Text size="1" weight="medium">
|
|
||||||
{article.authorName}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Text className="text-[--gray-11] text-xs md:text-sm line-clamp-2 md:line-clamp-3 leading-relaxed">
|
|
||||||
{article.content}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Flex gap="2" className="mt-auto pt-3 flex-wrap">
|
|
||||||
{article.tags.map((tag) => (
|
{article.tags.map((tag) => (
|
||||||
<Text
|
<Text
|
||||||
key={tag.name}
|
key={tag.name}
|
||||||
size="1"
|
size="1"
|
||||||
className={`px-2 py-0.5 rounded-full ${tag.color.bg} ${tag.color.text}`}
|
className={`px-2 py-0.5 rounded-full border border-current ${tag.color.text} hover:bg-[--gray-a3] transition-colors`}
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -130,7 +130,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
className="mt-3 p-1 min-w-[180px] rounded-md bg-[--color-panel] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
|
className="mt-3 p-1 min-w-[180px] rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
|
||||||
>
|
>
|
||||||
{loginState ? (
|
{loginState ? (
|
||||||
<>
|
<>
|
||||||
|
336
frontend/themes/echoes/post.tsx
Normal file
336
frontend/themes/echoes/post.tsx
Normal file
@ -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<TocItem[]>([]);
|
||||||
|
const [activeId, setActiveId] = useState<string>('');
|
||||||
|
|
||||||
|
// 解析文章内容生成目录
|
||||||
|
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 (
|
||||||
|
<Container className="py-16 px-4 md:px-8 lg:px-12 bg-[--gray-1]">
|
||||||
|
<Flex gap="20" className="relative max-w-6xl mx-auto">
|
||||||
|
{/* 左侧文章主体 */}
|
||||||
|
<Box className="flex-1 max-w-3xl">
|
||||||
|
{/* 文章头部 - 增加间距和样式 */}
|
||||||
|
<Box className="mb-16">
|
||||||
|
<Heading
|
||||||
|
size="8"
|
||||||
|
className="mb-8 leading-tight text-[--gray-12] font-bold tracking-tight"
|
||||||
|
>
|
||||||
|
{mockPost.title}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Flex gap="4" align="center" className="text-[--gray-11]">
|
||||||
|
<Avatar
|
||||||
|
size="3"
|
||||||
|
fallback={mockPost.authorName[0]}
|
||||||
|
className="border-2 border-[--gray-a5]"
|
||||||
|
/>
|
||||||
|
<Text size="2" weight="medium">{mockPost.authorName}</Text>
|
||||||
|
<Text size="2">·</Text>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<CalendarIcon className="w-4 h-4" />
|
||||||
|
<Text size="2">
|
||||||
|
{mockPost.publishedAt?.toLocaleDateString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 修改封面图片样式 */}
|
||||||
|
{mockPost.coverImage && (
|
||||||
|
<Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg">
|
||||||
|
<img
|
||||||
|
src={mockPost.coverImage}
|
||||||
|
alt={mockPost.title}
|
||||||
|
className="w-full h-full object-cover hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文章内容 - 优化排版和间距 */}
|
||||||
|
<Box className="prose prose-lg dark:prose-invert max-w-none">
|
||||||
|
<ReactMarkdown
|
||||||
|
components={{
|
||||||
|
h1: ({ children }) => {
|
||||||
|
const text = children?.toString() || '';
|
||||||
|
return (
|
||||||
|
<h1 id={text.toLowerCase().replace(/\s+/g, '-')} className="!text-[--gray-12]">
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
h2: ({ children }) => {
|
||||||
|
const text = children?.toString() || '';
|
||||||
|
return (
|
||||||
|
<h2 id={text.toLowerCase().replace(/\s+/g, '-')} className="!text-[--gray-12]">
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
code: ({ inline, className, children }: MarkdownCodeProps) => {
|
||||||
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
|
const lang = match ? match[1] : '';
|
||||||
|
|
||||||
|
return inline ? (
|
||||||
|
<code>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
) : (
|
||||||
|
<div className="group relative my-6">
|
||||||
|
{lang && (
|
||||||
|
<div className="absolute top-3 right-3 px-3 py-1 text-xs font-medium text-[--gray-11] bg-[--gray-3] border border-[--gray-a5] rounded-full">
|
||||||
|
{lang}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language={lang || 'text'}
|
||||||
|
style={{
|
||||||
|
...oneDark,
|
||||||
|
'pre[class*="language-"]': {
|
||||||
|
background: 'transparent',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
border: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
'code[class*="language-"]': {
|
||||||
|
background: 'transparent',
|
||||||
|
textShadow: 'none',
|
||||||
|
border: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
':not(pre) > 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$/, '')}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} as Partial<Components>}
|
||||||
|
>
|
||||||
|
{mockPost.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 优化文章底部标签样式 */}
|
||||||
|
<Flex gap="2" className="mt-16 pt-8 border-t border-[--gray-a5]">
|
||||||
|
{mockPost.metaKeywords.split(',').map((tag) => (
|
||||||
|
<Text
|
||||||
|
key={tag}
|
||||||
|
size="2"
|
||||||
|
className="px-4 py-1.5 rounded-full bg-[--gray-3] border border-[--gray-a5] text-[--gray-11] hover:text-[--gray-12] hover:bg-[--gray-4] transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
{tag.trim()}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 右侧目录 - 优化样式 */}
|
||||||
|
<Box className="w-72 hidden lg:block">
|
||||||
|
<Box className="sticky top-24 p-6 rounded-xl border border-[--gray-a5] bg-[--gray-2]">
|
||||||
|
<Text
|
||||||
|
size="2"
|
||||||
|
weight="medium"
|
||||||
|
className="mb-6 text-[--gray-12] flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<CodeIcon className="w-4 h-4" />
|
||||||
|
目录
|
||||||
|
</Text>
|
||||||
|
<ScrollArea className="h-[calc(100vh-250px)]">
|
||||||
|
<Box className="space-y-3 pr-4">
|
||||||
|
{toc.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.id}
|
||||||
|
href={`#${item.id}`}
|
||||||
|
className={`
|
||||||
|
block text-sm leading-relaxed transition-all
|
||||||
|
${item.level > 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}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用模板包装组件
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
return <PostContent />;
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user