前端:更新依赖项以支持Tailwind CSS排版插件,更新配色,文章展示页排版,新增文章
This commit is contained in:
parent
caa23c6ac5
commit
9eada9af3b
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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: '<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);
|
||||
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,
|
||||
|
@ -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);
|
||||
|
||||
// 只有当尺寸变化超过阈值时才重生成粒子
|
||||
// 只有当尺寸变化超<EFBFBD><EFBFBD><EFBFBD>阈值时才重生成粒子
|
||||
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 = ({
|
||||
};
|
||||
}
|
||||
|
||||
// 建错误动<EFBFBD><EFBFBD>函数
|
||||
// 建错误动函数
|
||||
const showErrorAnimation = () => {
|
||||
if (!scene) return;
|
||||
|
||||
@ -451,7 +464,7 @@ export const ParticleImage = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 加载图<EFBFBD><EFBFBD><EFBFBD>
|
||||
// 加载图
|
||||
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 (
|
||||
<div className="relative w-[140px] md:w-[180px] h-[140px] md:h-[180px] 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={`relative ${BG_CONFIG.size.container} shrink-0 overflow-hidden`}>
|
||||
<div className={`absolute inset-0 ${BG_CONFIG.className} rounded-lg overflow-hidden`}>
|
||||
<ParticleImage
|
||||
src={src}
|
||||
status={status}
|
||||
onLoad={() => {
|
||||
// 粒子动画完成后,延迟显示图片
|
||||
setTimeout(() => {
|
||||
setShowImage(true);
|
||||
}, 800); // 延迟时间可以根据需要调整
|
||||
}, 800);
|
||||
}}
|
||||
onAnimationComplete={() => {
|
||||
setShowImage(true);
|
||||
|
@ -5,7 +5,7 @@ export interface Post {
|
||||
title?: string; // 标题
|
||||
metaKeywords: string; // 元关键词
|
||||
metaDescription: string; // 元描述
|
||||
content: string; // 内容
|
||||
content: string; // Markdown 格式的内容
|
||||
status: string; // 状态
|
||||
isEditor: boolean; // 是否为编辑器
|
||||
draftContent?: string; // 草稿内容
|
||||
|
@ -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": {
|
||||
|
@ -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;
|
||||
|
@ -149,40 +149,44 @@ export default new Template({}, ({ http, args }) => {
|
||||
|
||||
return (
|
||||
<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) => (
|
||||
<Card
|
||||
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"
|
||||
>
|
||||
<div className={`p-5 relative flex gap-4`}>
|
||||
<div className="p-4 relative flex flex-col gap-4">
|
||||
<div className="flex gap-4">
|
||||
<ImageLoader
|
||||
src={article.coverImage}
|
||||
alt={article.title || ""}
|
||||
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"
|
||||
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 items-start justify-between gap-3 mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<Heading
|
||||
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}
|
||||
</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
|
||||
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}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
align="center"
|
||||
className="text-[--gray-11] mb-3 flex-wrap"
|
||||
>
|
||||
<Flex gap="2" align="center" className="text-[--gray-11]">
|
||||
<CalendarIcon className="w-3 h-3" />
|
||||
<Text size="1">
|
||||
{article.publishedAt?.toLocaleDateString("zh-CN", {
|
||||
@ -196,17 +200,14 @@ export default new Template({}, ({ http, args }) => {
|
||||
{article.authorName}
|
||||
</Text>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Flex gap="2" className="flex-wrap">
|
||||
{article.tags.map((tag) => (
|
||||
<Text
|
||||
key={tag.name}
|
||||
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}
|
||||
</Text>
|
||||
|
@ -130,7 +130,7 @@ export default new Layout(({ children, args }) => {
|
||||
<DropdownMenuPrimitive.Content
|
||||
align="end"
|
||||
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 ? (
|
||||
<>
|
||||
|
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