前端:优化图片加载逻辑,调整文章展示页的排版和样式。
This commit is contained in:
parent
206f97f0c5
commit
0628d5588f
@ -718,6 +718,7 @@ export const ImageLoader = ({
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
const loadingRef = useRef(false);
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 处理图片预加载
|
||||
const preloadImage = useCallback(() => {
|
||||
@ -744,9 +745,51 @@ export const ImageLoader = ({
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
loadingRef.current = false;
|
||||
// 图片加载成功后,不立即显示,等待粒子动画完成
|
||||
// 在图片加载成功后,立即创建和缓存一个适应容器大小的图片
|
||||
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;
|
||||
setStatus({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
@ -766,7 +809,10 @@ export const ImageLoader = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 确保src存在再设置
|
||||
if (src) {
|
||||
img.src = src;
|
||||
}
|
||||
}, [src]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -781,18 +827,23 @@ export const ImageLoader = ({
|
||||
}, [src, preloadImage]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<div ref={containerRef} className="relative w-full h-full overflow-hidden">
|
||||
<div className={`absolute inset-0 ${BG_CONFIG.className} rounded-lg overflow-hidden`}>
|
||||
<ParticleImage
|
||||
src={src}
|
||||
status={status}
|
||||
onLoad={() => {
|
||||
// 确保图片已经准备好
|
||||
if (imageRef.current) {
|
||||
setTimeout(() => {
|
||||
setShowImage(true);
|
||||
}, 800);
|
||||
}
|
||||
}}
|
||||
onAnimationComplete={() => {
|
||||
if (imageRef.current) {
|
||||
setShowImage(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -54,7 +54,7 @@ export default new Layout(({ children, args }) => {
|
||||
return (
|
||||
<Theme
|
||||
grayColor="gray"
|
||||
accentColor="gray"
|
||||
accentColor="indigo"
|
||||
radius="large"
|
||||
panelBackground="solid"
|
||||
>
|
||||
@ -78,9 +78,9 @@ export default new Layout(({ children, args }) => {
|
||||
<Flex align="center">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center hover:opacity-80 transition-all"
|
||||
className="flex items-center group transition-all"
|
||||
>
|
||||
<Box className="w-16 h-16">
|
||||
<Box className="w-16 h-16 [&_path]:transition-all [&_path]:duration-200 group-hover:[&_path]:stroke-[--accent-9]">
|
||||
<Echoes />
|
||||
</Box>
|
||||
</Link>
|
||||
@ -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"
|
||||
>
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
className="p-2"
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-a11]" />
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<Box className="flex items-center gap-6">
|
||||
<Box className="flex items-center gap-6 [&>a]:text-[--gray-12] [&>a]:transition-colors [&>a:hover]:text-[--accent-9]">
|
||||
{parse(navString)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<DropdownMenuPrimitive.Root>
|
||||
<DropdownMenuPrimitive.Trigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-10 h-10 p-0 hover:opacity-70 transition-opacity flex items-center justify-center"
|
||||
className="w-10 h-10 p-0 hover:text-[--accent-9] transition-colors flex items-center justify-center group"
|
||||
>
|
||||
{loginState ? (
|
||||
<AvatarIcon className="w-6 h-6 text-[--gray-a11]" />
|
||||
<AvatarIcon className="w-6 h-6 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||
) : (
|
||||
<PersonIcon className="w-6 h-6 text-[--gray-a11]" />
|
||||
<PersonIcon className="w-6 h-6 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuPrimitive.Trigger>
|
||||
@ -134,19 +136,19 @@ export default new Layout(({ children, args }) => {
|
||||
>
|
||||
{loginState ? (
|
||||
<>
|
||||
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded">
|
||||
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded text-[--gray-12]">
|
||||
个人中心
|
||||
</DropdownMenuPrimitive.Item>
|
||||
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded">
|
||||
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded text-[--gray-12]">
|
||||
设置
|
||||
</DropdownMenuPrimitive.Item>
|
||||
<DropdownMenuPrimitive.Separator className="h-px bg-[--gray-a5] my-1" />
|
||||
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded">
|
||||
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded text-[--gray-12]">
|
||||
退出登录
|
||||
</DropdownMenuPrimitive.Item>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded">
|
||||
<DropdownMenuPrimitive.Item className="py-1.5 px-2 outline-none cursor-pointer hover:bg-[--gray-a3] rounded text-[--gray-12]">
|
||||
登录/注册
|
||||
</DropdownMenuPrimitive.Item>
|
||||
)}
|
||||
@ -166,12 +168,12 @@ export default new Layout(({ children, args }) => {
|
||||
<DropdownMenuPrimitive.Trigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-10 h-10 p-0 hover:opacity-70 transition-opacity flex items-center justify-center"
|
||||
className="w-10 h-10 p-0 hover:text-[--accent-9] transition-colors flex items-center justify-center group"
|
||||
>
|
||||
{moreState ? (
|
||||
<Cross1Icon className="h-5 w-5 text-[--gray-a11]" />
|
||||
<Cross1Icon className="h-5 w-5 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||
) : (
|
||||
<HamburgerMenuIcon className="h-5 w-5 text-[--gray-a11]" />
|
||||
<HamburgerMenuIcon className="h-5 w-5 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuPrimitive.Trigger>
|
||||
@ -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"
|
||||
>
|
||||
<Box className="flex flex-col gap-2">
|
||||
<Box className="flex flex-col gap-2 [&>a]:text-[--gray-12] [&>a]:transition-colors [&>a:hover]:text-[--accent-9]">
|
||||
{parse(navString)}
|
||||
</Box>
|
||||
<Box className="mt-3 pt-3 border-t border-[--gray-a5]">
|
||||
@ -213,7 +215,7 @@ export default new Layout(({ children, args }) => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 主题切换按钮 */}
|
||||
{/* 题切换按钮 */}
|
||||
<Box className="flex items-center">
|
||||
<Box className="w-6 h-6 flex items-center justify-center">
|
||||
<ThemeModeToggle />
|
||||
|
@ -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<HTMLHeadingElement> {
|
||||
node?: any;
|
||||
level?: number;
|
||||
ordered?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// 创建一 React 组件
|
||||
export default new Template({}, ({ http, args }) => {
|
||||
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 };
|
||||
const parseHeadings = (content: string) => {
|
||||
const lines = content.split('\n');
|
||||
const headings: TocItem[] = [];
|
||||
let counts = new Map<number, number>();
|
||||
|
||||
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,26 +175,28 @@ 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 (
|
||||
<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">
|
||||
<Container size="4" >
|
||||
<Flex className="relative" gap="8">
|
||||
{/* 文章主体 */}
|
||||
<Box className="flex-1">
|
||||
<Box className="p-0">
|
||||
{/* 章头部 */}
|
||||
<Box className="mb-8">
|
||||
<Heading
|
||||
size="8"
|
||||
className="mb-8 leading-tight text-[--gray-12] font-bold tracking-tight"
|
||||
className="mb-4 leading-tight text-[--gray-12] font-bold tracking-tight"
|
||||
>
|
||||
{mockPost.title}
|
||||
</Heading>
|
||||
@ -181,141 +234,186 @@ function PostContent() {
|
||||
)}
|
||||
|
||||
{/* 文章内容 - 优化排版和间距 */}
|
||||
<Box className="prose prose-lg dark:prose-invert max-w-none">
|
||||
<Box className="max-w-none">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: ({ children }) => {
|
||||
h1: ({ children, node, ...props }: HeadingProps) => {
|
||||
const text = children?.toString() || '';
|
||||
const id = `heading-1-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-1`;
|
||||
return (
|
||||
<h1 id={text.toLowerCase().replace(/\s+/g, '-')} className="!text-[--gray-12]">
|
||||
<Heading
|
||||
as="h1"
|
||||
id={id}
|
||||
className="text-3xl font-bold mt-12 mb-6 text-[--gray-12] scroll-mt-20"
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
</Heading>
|
||||
);
|
||||
},
|
||||
h2: ({ children }) => {
|
||||
h2: ({ children, node, ...props }: HeadingProps) => {
|
||||
const text = children?.toString() || '';
|
||||
const id = `heading-2-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-1`;
|
||||
return (
|
||||
<h2 id={text.toLowerCase().replace(/\s+/g, '-')} className="!text-[--gray-12]">
|
||||
<Heading
|
||||
as="h2"
|
||||
id={id}
|
||||
className="text-2xl font-semibold mt-10 mb-4 text-[--gray-12] scroll-mt-20"
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
</Heading>
|
||||
);
|
||||
},
|
||||
h3: ({ children, node, ...props }: HeadingProps) => {
|
||||
const text = children?.toString() || '';
|
||||
const id = `heading-3-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-1`;
|
||||
return (
|
||||
<Heading
|
||||
as="h3"
|
||||
id={id}
|
||||
className="text-xl font-medium mt-8 mb-4 text-[--gray-12] scroll-mt-20"
|
||||
>
|
||||
{children}
|
||||
</Heading>
|
||||
);
|
||||
},
|
||||
code: ({ inline, className, children }: MarkdownCodeProps) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const lang = match ? match[1] : '';
|
||||
const lang = match ? match[1].toLowerCase() : '';
|
||||
|
||||
return inline ? (
|
||||
<code>
|
||||
<code className="px-1.5 py-0.5 rounded bg-[--gray-3] text-[--gray-12] text-[0.9em]">
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<div className="group relative my-6">
|
||||
<pre className="relative my-6 rounded-lg border border-[--gray-6] bg-[--gray-2]">
|
||||
{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">
|
||||
<div className="absolute top-3 right-3 px-3 py-1 text-xs text-[--gray-11] bg-[--gray-3] rounded-full">
|
||||
{lang}
|
||||
</div>
|
||||
)}
|
||||
<SyntaxHighlighter
|
||||
language={lang || 'text'}
|
||||
PreTag="div"
|
||||
style={{
|
||||
...oneDark,
|
||||
'pre[class*="language-"]': {
|
||||
...oneDark['pre[class*="language-"]'],
|
||||
background: 'transparent',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
'code[class*="language-"]': {
|
||||
...oneDark['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',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||
fontWeight: '500',
|
||||
opacity: '0.95',
|
||||
}
|
||||
}}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1.5rem',
|
||||
background: 'var(--gray-2)',
|
||||
background: 'none',
|
||||
fontSize: '0.95rem',
|
||||
lineHeight: '1.5',
|
||||
border: '1px solid var(--gray-a5)',
|
||||
borderRadius: '0.75rem',
|
||||
lineHeight: '1.6',
|
||||
fontFeatureSettings: '"liga" 0',
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
MozOsxFontSmoothing: 'grayscale',
|
||||
}}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
},
|
||||
p: ({ children }) => (
|
||||
<Text as="p" className="text-base leading-relaxed mb-6 text-[--gray-11]">
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc pl-6 mb-6 space-y-2 text-[--gray-11]">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal pl-6 mb-6 space-y-2 text-[--gray-11]">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-base leading-relaxed">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-[--gray-6] pl-4 py-1 my-6 text-[--gray-11] italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-[--gray-12]">
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
a: ({ children, href }) => (
|
||||
<a
|
||||
href={href}
|
||||
className="text-[--accent-11] hover:text-[--accent-12] underline-offset-4 hover:underline transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
} 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>
|
||||
|
||||
{/* 右侧目录 - 优化样式 */}
|
||||
<Box className="w-72 hidden lg:block">
|
||||
<Box className="sticky top-24 p-6 rounded-xl border border-[--gray-a5] bg-[--gray-2]">
|
||||
{/* 右侧目录 */}
|
||||
<Box className="hidden lg:block w-36 relative">
|
||||
<Box className="sticky top-8">
|
||||
<Text
|
||||
size="2"
|
||||
weight="medium"
|
||||
className="mb-6 text-[--gray-12] flex items-center gap-2"
|
||||
className="mb-4 text-[--gray-11] flex items-center gap-2 text-sm"
|
||||
>
|
||||
<CodeIcon className="w-4 h-4" />
|
||||
<CodeIcon className="w-3 h-3" />
|
||||
目录
|
||||
</Text>
|
||||
<ScrollArea className="h-[calc(100vh-250px)]">
|
||||
<Box className="space-y-3 pr-4">
|
||||
<Box className="space-y-1.5">
|
||||
{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}` : ''}
|
||||
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({
|
||||
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() {
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
// 使用模板包装组件
|
||||
export default new Template({}, ({ http, args }) => {
|
||||
return <PostContent />;
|
||||
});
|
||||
})
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user