前端:优化图片加载逻辑,调整文章展示页的排版和样式。
This commit is contained in:
parent
206f97f0c5
commit
0628d5588f
@ -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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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
|
||||||
|
|
||||||
在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要。本文将详细介绍如何建一个现代化的前端开发工作流。
|
在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要。本文将详细介绍如何建一个现代化的前端开发工作流。
|
||||||
|
|
||||||
## 工具链选择
|
## 工具链选择
|
||||||
|
|
||||||
选择合适的工具链是构建高效工作流的第一步。我们需要考虑以下几方
|
选择合适的工具链效工作流的第一步。我们需要考虑
|
||||||
|
|
||||||
- 包管理器:npm、yarn 或 pnpm
|
- 包管理器:npm、yarn 或 pnpm
|
||||||
- 构建工具:Vite、webpack 或 Rollup
|
- 构建工具:Vite、webpack 或 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 />;
|
|
||||||
});
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user