diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx index 83441af..aa95c77 100644 --- a/app/[locale]/about/page.tsx +++ b/app/[locale]/about/page.tsx @@ -1,4 +1,5 @@ import MDXComponents from "@/components/mdx/MDXComponents"; +import TableOfContents from "@/components/mdx/TableOfContents.client"; import { Locale, LOCALES } from "@/i18n/routing"; import { constructMetadata } from "@/lib/metadata"; import fs from "fs/promises"; @@ -16,7 +17,52 @@ const options = { }, }; -async function getMDXContent(locale: string, section: string) { +interface TableOfContentsItem { + id: string; + text: string; + level: number; +} + +// 解析MDX内容并提取标题 +async function parseMDXContent(content: string): Promise { + if (!content) { + return []; + } + + try { + const headingRegex = /^#{2,4}\s+(.+)$/gm; + const headings: TableOfContentsItem[] = []; + let match; + + while ((match = headingRegex.exec(content)) !== null) { + const fullMatch = match[0]; + const text = match[1]?.trim(); + + if (!text) continue; + + // 确定标题级别 + let level = 2; + if (fullMatch.startsWith("###")) { + level = fullMatch.startsWith("####") ? 4 : 3; + } + + // 生成ID(将文本转换为URL友好的格式) + const id = text + .toLowerCase() + .replace(/[^a-z0-9\u4e00-\u9fa5\s-]/g, "") + .replace(/\s+/g, "-"); + + headings.push({ id, text, level }); + } + + return headings; + } catch (error) { + console.error("Error parsing MDX content for TOC:", error); + return []; + } +} + +async function getMDXContent(locale: string, section: string): Promise { const filePath = path.join( process.cwd(), "content", @@ -69,15 +115,37 @@ export default async function AboutPage({ const section = (resolvedSearchParams.section as string) || "company"; const content = await getMDXContent(locale, section); + const tocItems = await parseMDXContent(content); + + // 获取多语言目录标题 + const t = await getTranslations({ locale, namespace: "Common" }); + const tocTitle = t("tableOfContents") || "目录"; return ( -
- -
+
+ {/* 侧边目录 - 在移动端显示在内容上方 */} +
+ +
+ + {/* 主要内容 */} +
+ {content ? ( + + ) : ( +
+

内容加载中...

+
+ )} +
+
); } diff --git a/app/[locale]/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx index ce36de0..962c472 100644 --- a/app/[locale]/blog/[slug]/page.tsx +++ b/app/[locale]/blog/[slug]/page.tsx @@ -1,13 +1,59 @@ import { Callout } from "@/components/mdx/Callout"; import MDXComponents from "@/components/mdx/MDXComponents"; -import { Locale, LOCALES } from "@/i18n/routing"; -import { getPosts } from "@/lib/getBlogs"; +import TableOfContents from "@/components/mdx/TableOfContents.client"; +import { Locale } from "@/i18n/routing"; +import { getPostDetail } from "@/lib/getBlogDetail"; import { constructMetadata } from "@/lib/metadata"; import { BlogPost } from "@/types/blog"; import { Metadata } from "next"; import { MDXRemote } from "next-mdx-remote-client/rsc"; import { notFound } from "next/navigation"; +interface TableOfContentsItem { + id: string; + text: string; + level: number; +} + +// 解析MDX内容并提取标题 +async function parseMDXContent(content: string): Promise { + if (!content) { + return []; + } + + try { + const headingRegex = /^#{2,4}\s+(.+)$/gm; + const headings: TableOfContentsItem[] = []; + let match; + + while ((match = headingRegex.exec(content)) !== null) { + const fullMatch = match[0]; + const text = match[1]?.trim(); + + if (!text) continue; + + // 确定标题级别 + let level = 2; + if (fullMatch.startsWith("###")) { + level = fullMatch.startsWith("####") ? 4 : 3; + } + + // 生成ID(将文本转换为URL友好的格式) + const id = text + .toLowerCase() + .replace(/[^a-z0-9\u4e00-\u9fa5\s-]/g, "") + .replace(/\s+/g, "-"); + + headings.push({ id, text, level }); + } + + return headings; + } catch (error) { + console.error("Error parsing MDX content for TOC:", error); + return []; + } +} + type Params = Promise<{ locale: string; slug: string; @@ -21,10 +67,10 @@ export async function generateMetadata({ params, }: MetadataProps): Promise { const { locale, slug } = await params; - let { posts }: { posts: BlogPost[] } = await getPosts(locale); - const post = posts.find((post) => post.slug === slug); + let post: BlogPost = await getPostDetail(slug); + + console.log(post); - console.log(post, posts); if (!post) { return constructMetadata({ title: "404", @@ -49,56 +95,73 @@ export async function generateMetadata({ export default async function BlogPage({ params }: { params: Params }) { const { locale, slug } = await params; - let { posts }: { posts: BlogPost[] } = await getPosts(locale); - - const post = posts.find((item) => item.slug === slug); + let post: BlogPost = await getPostDetail(slug); if (!post) { return notFound(); } + console.log(post); + + // 提取博客内容中的标题用于目录 + const tocItems = await parseMDXContent(post.content || ""); + + // 使用默认目录标题 + const tocTitle = "目录"; + return ( -
-

{post.title}

- {post.image && ( - {post.title} - )} - {post.tags && post.tags.split(",").length ? ( -
- {post.tags.split(",").map((tag) => { - return ( -
- {tag.trim()} -
- ); - })} -
- ) : ( - <> - )} - {post.description && {post.description}} - +
+ {/* 侧边目录 - 在移动端显示在内容上方 */} +
+ +
+ + {/* 主要内容 */} +
+

{post.title}

+ {post.image && ( + {post.title} + )} + {post.tags && post.tags.split(",").length ? ( +
+ {post.tags.split(",").map((tag) => { + return ( +
+ {tag.trim()} +
+ ); + })} +
+ ) : ( + <> + )} + {post.description && {post.description}} + +
); } -export async function generateStaticParams() { - let posts = (await getPosts()).posts; +// export async function generateStaticParams() { +// let post = (await getPostDetail()); + +// // Filter out posts without a slug +// posts = posts.filter((post) => post.slug); - // Filter out posts without a slug - posts = posts.filter((post) => post.slug); +// return LOCALES.flatMap((locale) => +// posts.map((post) => { +// const slugPart = post.slug.replace(/^\//, "").replace(/^blog\//, ""); - return LOCALES.flatMap((locale) => - posts.map((post) => { - const slugPart = post.slug.replace(/^\//, "").replace(/^blog\//, ""); - - return { - locale, - slug: slugPart, - }; - }) - ); -} +// return { +// locale, +// slug: slugPart, +// }; +// }) +// ); +// } diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 0ed1e0b..eb99d87 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -6,11 +6,11 @@ export default function Home() { return ; } -export async function generateStaticParams() { - return [ - { locale: 'en' }, - { locale: 'zh' }, - { locale: 'vi' }, - // { locale: 'ja' }, - ] -} +// export async function generateStaticParams() { +// return [ +// { locale: 'en' }, +// { locale: 'zh' }, +// { locale: 'vi' }, +// // { locale: 'ja' }, +// ] +// } diff --git a/components/WebsiteLogo.tsx b/components/WebsiteLogo.tsx index 3cf5858..d8319fe 100644 --- a/components/WebsiteLogo.tsx +++ b/components/WebsiteLogo.tsx @@ -16,13 +16,13 @@ const WebsiteLogo = ({ timeout = 1000, // 1 second }: IProps) => { const domain = getDomain(url); - const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.svg`); + const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.png`); const [fallbackIndex, setFallbackIndex] = useState(0); const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); const fallbackSources = [ - `https://${domain}/logo.svg`, + `https://${domain}/logo.png`, `https://${domain}/logo.png`, `https://${domain}/apple-touch-icon.png`, `https://${domain}/apple-touch-icon-precomposed.png`, @@ -83,9 +83,8 @@ const WebsiteLogo = ({ height={size} onError={handleError} onLoad={handleLoad} - className={`inline-block transition-opacity duration-300 ${ - isLoading ? "opacity-0" : "opacity-100" - }`} + className={`inline-block transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100" + }`} style={{ objectFit: "contain", display: hasError ? "none" : "inline-block", diff --git a/components/header/Header.tsx b/components/header/Header.tsx index d056865..a9817ef 100644 --- a/components/header/Header.tsx +++ b/components/header/Header.tsx @@ -20,7 +20,7 @@ const Header = () => { > {siteConfig.name}(""); + + useEffect(() => { + if (items.length === 0) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: "0% 0% -80% 0%" } + ); + + // 观察所有标题元素 + items.forEach((item) => { + const element = document.getElementById(item.id); + if (element) { + observer.observe(element); + } + }); + + return () => { + items.forEach((item) => { + const element = document.getElementById(item.id); + if (element) { + observer.unobserve(element); + } + }); + }; + }, [items]); + + const handleClick = (e: React.MouseEvent, id: string) => { + e.preventDefault(); + const element = document.getElementById(id); + if (element) { + element.scrollIntoView({ behavior: "smooth" }); + window.history.pushState(null, "", `#${id}`); + setActiveId(id); + } + }; + + if (!items || items.length === 0) { + return null; + } + + return ( + + ); +} \ No newline at end of file diff --git a/lib/getBlogDetail.ts b/lib/getBlogDetail.ts new file mode 100644 index 0000000..523facf --- /dev/null +++ b/lib/getBlogDetail.ts @@ -0,0 +1,23 @@ +import { BlogPost } from '@/types/blog'; + +export async function getPostDetail(articleId: string): Promise { + let url = 'http://49.232.154.205:18081/export/article/' + articleId + + const response = await fetch(url); + const json = await response.json(); + const data = json.data; + const post = { + locale: data.langCode, + title: data.title, + description: data.summary, + image: data.cover || '', + slug: data.articleId, + tags: '', + date: data.publishedTime, + // visible: data.visible || 'published', + pin: false, + content: data.content, + metadata: data, + } + return post; +} \ No newline at end of file diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/public/logo.png b/public/logo.png index cf1be82..2270d8a 100644 Binary files a/public/logo.png and b/public/logo.png differ