前端:优化图片加载逻辑,调整文章展示页的排版和样式。

This commit is contained in:
lsy 2024-12-06 15:25:22 +08:00
parent 206f97f0c5
commit 0628d5588f
4 changed files with 384 additions and 217 deletions

View File

@ -718,6 +718,7 @@ export const ImageLoader = ({
const timeoutRef = useRef<NodeJS.Timeout>(); const timeoutRef = useRef<NodeJS.Timeout>();
const loadingRef = useRef(false); const loadingRef = useRef(false);
const imageRef = useRef<HTMLImageElement | null>(null); const imageRef = useRef<HTMLImageElement | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
// 处理图片预加载 // 处理图片预加载
const preloadImage = useCallback(() => { const preloadImage = useCallback(() => {
@ -744,9 +745,51 @@ export const ImageLoader = ({
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
} }
// 在图片加载成功后,立即创建和缓存一个适应容器大小的图片
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; loadingRef.current = false;
// 图片加载成功后,不立即显示,等待粒子动画完成
imageRef.current = img;
setStatus({ setStatus({
isLoading: false, isLoading: false,
hasError: false, hasError: false,
@ -766,7 +809,10 @@ export const ImageLoader = ({
}); });
}; };
img.src = src; // 确保src存在再设置
if (src) {
img.src = src;
}
}, [src]); }, [src]);
useEffect(() => { useEffect(() => {
@ -781,18 +827,23 @@ export const ImageLoader = ({
}, [src, preloadImage]); }, [src, preloadImage]);
return ( 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`}> <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(() => { // 确保图片已经准备好
setShowImage(true); if (imageRef.current) {
}, 800); setTimeout(() => {
setShowImage(true);
}, 800);
}
}} }}
onAnimationComplete={() => { onAnimationComplete={() => {
setShowImage(true); if (imageRef.current) {
setShowImage(true);
}
}} }}
/> />
</div> </div>

View File

@ -54,7 +54,7 @@ export default new Layout(({ children, args }) => {
return ( return (
<Theme <Theme
grayColor="gray" grayColor="gray"
accentColor="gray" accentColor="indigo"
radius="large" radius="large"
panelBackground="solid" panelBackground="solid"
> >
@ -78,9 +78,9 @@ export default new Layout(({ children, args }) => {
<Flex align="center"> <Flex align="center">
<Link <Link
href="/" 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 /> <Echoes />
</Box> </Box>
</Link> </Link>
@ -98,31 +98,33 @@ export default new Layout(({ children, args }) => {
size="2" size="2"
variant="surface" variant="surface"
placeholder="搜索..." 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" id="search"
> >
<TextField.Slot <TextField.Slot
side="right" side="right"
className="p-2" 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.Slot>
</TextField.Root> </TextField.Root>
<Box className="flex items-center gap-6"> <Box className="flex items-center gap-6">
{parse(navString)} <Box className="flex items-center gap-6 [&>a]:text-[--gray-12] [&>a]:transition-colors [&>a:hover]:text-[--accent-9]">
{parse(navString)}
</Box>
</Box> </Box>
<DropdownMenuPrimitive.Root> <DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild> <DropdownMenuPrimitive.Trigger asChild>
<Button <Button
variant="ghost" 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 ? ( {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> </Button>
</DropdownMenuPrimitive.Trigger> </DropdownMenuPrimitive.Trigger>
@ -134,19 +136,19 @@ export default new Layout(({ children, args }) => {
> >
{loginState ? ( {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>
<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>
<DropdownMenuPrimitive.Separator className="h-px bg-[--gray-a5] my-1" /> <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>
</> </>
) : ( ) : (
<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>
)} )}
@ -166,12 +168,12 @@ export default new Layout(({ children, args }) => {
<DropdownMenuPrimitive.Trigger asChild> <DropdownMenuPrimitive.Trigger asChild>
<Button <Button
variant="ghost" 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 ? ( {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> </Button>
</DropdownMenuPrimitive.Trigger> </DropdownMenuPrimitive.Trigger>
@ -187,7 +189,7 @@ export default new Layout(({ children, args }) => {
sideOffset={5} 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" 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)} {parse(navString)}
</Box> </Box>
<Box className="mt-3 pt-3 border-t border-[--gray-a5]"> <Box className="mt-3 pt-3 border-t border-[--gray-a5]">
@ -213,7 +215,7 @@ export default new Layout(({ children, args }) => {
</Box> </Box>
)} )}
{/* 题切换按钮 */} {/* 题切换按钮 */}
<Box className="flex items-center"> <Box className="flex items-center">
<Box className="w-6 h-6 flex items-center justify-center"> <Box className="w-6 h-6 flex items-center justify-center">
<ThemeModeToggle /> <ThemeModeToggle />

View File

@ -1,6 +1,6 @@
import { Template } from "interface/template"; import { Template } from "interface/template";
import ReactMarkdown from 'react-markdown'; 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 { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { import {
Container, Container,
@ -27,19 +27,20 @@ import {
import { Post } from "interface/post"; import { Post } from "interface/post";
import { useMemo, useState, useEffect } from "react"; import { useMemo, useState, useEffect } from "react";
import type { Components } from 'react-markdown'; import type { Components } from 'react-markdown';
import type { MetaFunction } from "@remix-run/node";
// 示例文章数据 // 示例文章数据
const mockPost: Post = { const mockPost: Post = {
id: 1, id: 1,
title: "构建现代化的前端开发工作流", title: "构建现代化的前端开发工作流",
content: ` content: `
# # sssssssssssssssss
## ##
- npmyarn pnpm - npmyarn pnpm
- Vitewebpack Rollup - Vitewebpack Rollup
@ -47,6 +48,7 @@ const mockPost: Post = {
- TypeScript - TypeScript
## ##
###
@ -62,6 +64,11 @@ const mockPost: Post = {
} }
\`\`\` \`\`\`
\`\`\`JavaScript
let a=1
\`\`\`
## ##
GitHub Actions GitHub Actions
@ -72,7 +79,7 @@ const mockPost: Post = {
`, `,
authorName: "张三", authorName: "张三",
publishedAt: new Date("2024-03-15"), publishedAt: new Date("2024-03-15"),
coverImage: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6", coverImage: "",
metaKeywords: "前端开发,工作流,效率", metaKeywords: "前端开发,工作流,效率",
metaDescription: "探讨如何构现代的前端开发工作流,提高开发效率。", metaDescription: "探讨如何构现代的前端开发工作流,提高开发效率。",
status: "published", status: "published",
@ -95,27 +102,71 @@ interface MarkdownCodeProps {
children: React.ReactNode; children: React.ReactNode;
} }
// 创建一个 React 组件 // 添加 meta 函数
function PostContent() { 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 [toc, setToc] = useState<TocItem[]>([]);
const [activeId, setActiveId] = useState<string>(''); const [activeId, setActiveId] = useState<string>('');
// 解析文章内容生成目录 // 解析文章内容生成目录
useEffect(() => { useEffect(() => {
const headings = mockPost.content.split('\n') const parseHeadings = (content: string) => {
.filter(line => line.startsWith('#')) const lines = content.split('\n');
.map(line => { const headings: TocItem[] = [];
const match = line.match(/^#+/); let counts = new Map<number, number>();
const level = match ? match[0].length : 1;
const text = line.replace(/^#+\s+/, ''); lines.forEach((line) => {
const id = text.toLowerCase().replace(/\s+/g, '-'); const match = line.match(/^(#{1,6})\s+(.+)$/);
return { id, text, level }; 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]); }, [mockPost.content]);
// 监听滚动更新当前标题 // 监听滚动更新当前标题
useEffect(() => { useEffect(() => {
const headings = document.querySelectorAll('h1[id], h2[id], h3[id]');
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
@ -124,198 +175,245 @@ 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) => { headings.forEach((heading) => observer.observe(heading));
observer.observe(heading);
});
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
return ( return (
<Container className="py-16 px-4 md:px-8 lg:px-12 bg-[--gray-1]"> <Container size="4" >
<Flex gap="20" className="relative max-w-6xl mx-auto"> <Flex className="relative" gap="8">
{/* 左侧文章主体 */} {/* 文章主体 */}
<Box className="flex-1 max-w-3xl"> <Box className="flex-1">
{/* 文章头部 - 增加间距和样式 */} <Box className="p-0">
<Box className="mb-16"> {/* 章头部 */}
<Heading <Box className="mb-8">
size="8" <Heading
className="mb-8 leading-tight text-[--gray-12] font-bold tracking-tight" size="8"
> className="mb-4 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()} {mockPost.title}
</Text> </Heading>
))}
</Flex> <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="max-w-none">
<ReactMarkdown
components={{
h1: ({ children, node, ...props }: HeadingProps) => {
const text = children?.toString() || '';
const id = `heading-1-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-1`;
return (
<Heading
as="h1"
id={id}
className="text-3xl font-bold mt-12 mb-6 text-[--gray-12] scroll-mt-20"
>
{children}
</Heading>
);
},
h2: ({ children, node, ...props }: HeadingProps) => {
const text = children?.toString() || '';
const id = `heading-2-${text.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-1`;
return (
<Heading
as="h2"
id={id}
className="text-2xl font-semibold mt-10 mb-4 text-[--gray-12] scroll-mt-20"
>
{children}
</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].toLowerCase() : '';
return inline ? (
<code className="px-1.5 py-0.5 rounded bg-[--gray-3] text-[--gray-12] text-[0.9em]">
{children}
</code>
) : (
<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 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,
},
'code[class*="language-"]': {
...oneDark['code[class*="language-"]'],
background: 'transparent',
textShadow: 'none',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
fontWeight: '500',
opacity: '0.95',
}
}}
customStyle={{
margin: 0,
padding: '1.5rem',
background: 'none',
fontSize: '0.95rem',
lineHeight: '1.6',
fontFeatureSettings: '"liga" 0',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
}}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
</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>
</Box>
</Box> </Box>
{/* 右侧目录 - 优化样式 */} {/* 右侧目录 */}
<Box className="w-72 hidden lg:block"> <Box className="hidden lg:block w-36 relative">
<Box className="sticky top-24 p-6 rounded-xl border border-[--gray-a5] bg-[--gray-2]"> <Box className="sticky top-8">
<Text <Text
size="2" size="2"
weight="medium" 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> </Text>
<ScrollArea className="h-[calc(100vh-250px)]"> <ScrollArea className="h-[calc(100vh-250px)]">
<Box className="space-y-3 pr-4"> <Box className="space-y-1.5">
{toc.map((item) => ( {toc.map((item) => (
<a <a
key={item.id} key={item.id}
href={`#${item.id}`} href={`#${item.id}`}
className={` className={`
block text-sm leading-relaxed transition-all block text-xs leading-relaxed transition-all
${item.level > 1 ? `ml-${(item.level - 1) * 4}` : ''} 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 ${activeId === item.id
? 'text-[--accent-11] font-medium translate-x-1' ? 'text-[--accent-11] font-medium border-[--accent-9]'
: 'text-[--gray-11] hover:text-[--gray-12]' : 'text-[--gray-11] hover:text-[--gray-12] hover:border-[--gray-8]'
} }
`} `}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
document.getElementById(item.id)?.scrollIntoView({ const element = document.getElementById(item.id);
behavior: 'smooth', if (element) {
block: 'center' element.scrollIntoView({
}); behavior: 'smooth',
block: 'center'
});
setActiveId(item.id);
}
}} }}
> >
{item.text} {item.text}
@ -328,9 +426,4 @@ function PostContent() {
</Flex> </Flex>
</Container> </Container>
); );
} })
// 使用模板包装组件
export default new Template({}, ({ http, args }) => {
return <PostContent />;
});

View File

@ -4,11 +4,12 @@
#nav a { #nav a {
position: relative; position: relative;
transition: opacity 0.2s ease; transition: all 0.2s ease;
color: var(--gray-11);
} }
#nav a:hover { #nav a:hover {
opacity: 0.8; color: var(--accent-9);
} }
#nav a::after { #nav a::after {
@ -18,7 +19,7 @@
bottom: -3px; bottom: -3px;
width: 100%; width: 100%;
height: 2px; height: 2px;
background-color: var(--gray-a11); background-color: var(--accent-9);
transform: scaleX(0); transform: scaleX(0);
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
@ -27,7 +28,27 @@
transform: scaleX(1); transform: scaleX(1);
} }
#search { #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);
} }