feat: 重构导航和博客系统,支持多级菜单和API数据源

- 重构导航菜单支持多级子菜单结构
- 博客系统改为从API获取数据,移除本地文件存储
- 删除旧的关于页面,准备重构
- 修复博客详情页slug匹配问题
- 默认首页重定向到中文版本
This commit is contained in:
砂糖
2025-11-21 17:49:17 +08:00
parent 0a420f2dbb
commit 216076ff2a
13 changed files with 178 additions and 222 deletions

View File

@@ -1,5 +0,0 @@
export default function Page() {
return (
<div className="container mx-auto px-4 py-12"></div>
);
}

View File

@@ -1,5 +0,0 @@
export default function Page() {
return (
<div className="container mx-auto px-4 py-12"></div>
);
}

View File

@@ -1,5 +0,0 @@
export default function Page() {
return (
<div className="container mx-auto px-4 py-12"></div>
);
}

View File

@@ -1,5 +0,0 @@
export default function Page() {
return (
<div className="container mx-auto px-4 py-12">绿</div>
);
}

View File

@@ -1,5 +0,0 @@
export default function Page() {
return (
<div className="container mx-auto px-4 py-12"></div>
);
}

View File

@@ -1,5 +0,0 @@
export default function Page() {
return (
<div className="container mx-auto px-4 py-12"></div>
);
}

View File

@@ -22,8 +22,9 @@ export async function generateMetadata({
}: MetadataProps): Promise<Metadata> { }: MetadataProps): Promise<Metadata> {
const { locale, slug } = await params; const { locale, slug } = await params;
let { posts }: { posts: BlogPost[] } = await getPosts(locale); let { posts }: { posts: BlogPost[] } = await getPosts(locale);
const post = posts.find((post) => post.slug === "/" + slug); const post = posts.find((post) => post.slug === slug);
console.log(post, posts);
if (!post) { if (!post) {
return constructMetadata({ return constructMetadata({
title: "404", title: "404",
@@ -50,7 +51,7 @@ export default async function BlogPage({ params }: { params: Params }) {
const { locale, slug } = await params; const { locale, slug } = await params;
let { posts }: { posts: BlogPost[] } = await getPosts(locale); let { posts }: { posts: BlogPost[] } = await getPosts(locale);
const post = posts.find((item) => item.slug === "/" + slug); const post = posts.find((item) => item.slug === slug);
if (!post) { if (!post) {
return notFound(); return notFound();

View File

@@ -9,7 +9,7 @@ import { getTranslations } from "next-intl/server";
type Params = Promise<{ locale: string }>; type Params = Promise<{ locale: string }>;
type SearchParams = { page?: string; type?: string }; type SearchParams = { page?: string; category?: string };
type MetadataProps = { type MetadataProps = {
params: Params; params: Params;
@@ -36,34 +36,18 @@ export default async function Page({
searchParams, searchParams,
}: { }: {
params: Params; params: Params;
searchParams?: SearchParams; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const { locale } = await params; const { locale } = await params;
let { posts } = await getPosts(locale); const resolvedSearchParams = await searchParams;
const category = resolvedSearchParams.category as string || "";
let { posts } = await getPosts(locale, category);
const t = await getTranslations("Blog"); const t = await getTranslations("Blog");
// 分类筛选(公司新闻/专家访谈/行业动态/公告) const pageRaw = resolvedSearchParams.page as string || "1";
const type = (searchParams?.type || "").toLowerCase(); const page = Math.max(1, parseInt(pageRaw, 10));
const typeLabelMap: Record<string, string> = {
company: "公司新闻",
experts: "专家访谈",
industry: "行业动态",
notice: "公告",
};
const typeLabel = typeLabelMap[type];
if (type) {
posts = posts.filter((p) => {
const metaType = String((p.metadata as any)?.type || "").toLowerCase();
const tags: string[] = (p.tags as any) || [];
const hitMeta = metaType === type;
const hitTag = Array.isArray(tags) && tags.some((t) => String(t).toLowerCase() === type);
return hitMeta || hitTag;
});
}
const pageRaw = searchParams?.page;
const page = Math.max(1, parseInt(pageRaw || "1", 10));
const pageSize = 10; const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(posts.length / pageSize)); const totalPages = Math.max(1, Math.ceil(posts.length / pageSize));
const start = (page - 1) * pageSize; const start = (page - 1) * pageSize;
@@ -77,14 +61,14 @@ export default async function Page({
{/* 顶部操作区:标签 + 面包屑 */} {/* 顶部操作区:标签 + 面包屑 */}
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mt-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mt-4">
<div className="flex gap-3"> {/* <div className="flex gap-3">
<button className="px-4 py-2 rounded-md border bg-white text-gray-700 shadow-sm hover:bg-gray-50"> <button className="px-4 py-2 rounded-md border bg-white text-gray-700 shadow-sm hover:bg-gray-50">
投资者保护 投资者保护
</button> </button>
<button className="px-4 py-2 rounded-md border bg-white text-gray-700 shadow-sm hover:bg-gray-50"> <button className="px-4 py-2 rounded-md border bg-white text-gray-700 shadow-sm hover:bg-gray-50">
公司公告 公司公告
</button> </button>
</div> </div> */}
<div className="flex items-center gap-2 text-sm text-gray-500"> <div className="flex items-center gap-2 text-sm text-gray-500">
<I18nLink href="/" prefetch={false} className="hover:underline"> <I18nLink href="/" prefetch={false} className="hover:underline">
@@ -93,12 +77,12 @@ export default async function Page({
<I18nLink href="/blog" prefetch={false} className="hover:underline"> <I18nLink href="/blog" prefetch={false} className="hover:underline">
</I18nLink> </I18nLink>
{typeLabel && ( {/* {typeLabel && (
<> <>
<span>/</span> <span>/</span>
<span className="text-gray-700">{typeLabel}</span> <span className="text-gray-700">{typeLabel}</span>
</> </>
)} )} */}
</div> </div>
</div> </div>
@@ -111,7 +95,7 @@ export default async function Page({
return ( return (
<I18nLink <I18nLink
key={post.slug} key={post.slug}
href={`/blog${post.slug}`} href={`/blog/${post.slug}`}
prefetch={false} prefetch={false}
className="block rounded-md mb-4 bg-gray-100 px-6 py-5 transition-all duration-200 shadow hover:bg-gray-200 hover:translate-y-[1px] hover:shadow-inner active:translate-y-[2px] active:shadow-inner focus:outline-none focus:ring-1 focus:ring-gray-300" className="block rounded-md mb-4 bg-gray-100 px-6 py-5 transition-all duration-200 shadow hover:bg-gray-200 hover:translate-y-[1px] hover:shadow-inner active:translate-y-[2px] active:shadow-inner focus:outline-none focus:ring-1 focus:ring-gray-300"
> >
@@ -139,9 +123,8 @@ export default async function Page({
<I18nLink <I18nLink
href={`/blog?page=${Math.max(1, page - 1)}`} href={`/blog?page=${Math.max(1, page - 1)}`}
prefetch={false} prefetch={false}
className={`px-2.5 py-1 border rounded-sm text-sm ${ className={`px-2.5 py-1 border rounded-sm text-sm ${page === 1 ? "bg-gray-100 text-gray-400" : "bg-white hover:bg-gray-50"
page === 1 ? "bg-gray-100 text-gray-400" : "bg-white hover:bg-gray-50" }`}
}`}
> >
{"<"} {"<"}
</I18nLink> </I18nLink>
@@ -167,11 +150,10 @@ export default async function Page({
key={it} key={it}
href={`/blog?page=${it}`} href={`/blog?page=${it}`}
prefetch={false} prefetch={false}
className={`px-2.5 py-1 border rounded-sm text-sm ${ className={`px-2.5 py-1 border rounded-sm text-sm ${isActive
isActive ? "bg-red-600 text-white border-red-600"
? "bg-red-600 text-white border-red-600" : "bg-white hover:bg-gray-50"
: "bg-white hover:bg-gray-50" }`}
}`}
> >
{it} {it}
</I18nLink> </I18nLink>
@@ -181,9 +163,8 @@ export default async function Page({
<I18nLink <I18nLink
href={`/blog?page=${Math.min(totalPages, page + 1)}`} href={`/blog?page=${Math.min(totalPages, page + 1)}`}
prefetch={false} prefetch={false}
className={`px-2.5 py-1 border rounded-sm text-sm ${ className={`px-2.5 py-1 border rounded-sm text-sm ${page === totalPages ? "bg-gray-100 text-gray-400" : "bg-white hover:bg-gray-50"
page === totalPages ? "bg-gray-100 text-gray-400" : "bg-white hover:bg-gray-50" }`}
}`}
> >
{">"} {">"}
</I18nLink> </I18nLink>

6
app/page.tsx Normal file
View File

@@ -0,0 +1,6 @@
// 默认跳转到中文版本
import { redirect } from 'next/navigation';
export default function HomePage() {
redirect('/zh');
}

View File

@@ -3,7 +3,6 @@
import { Link as I18nLink, usePathname, useRouter } from "@/i18n/routing"; import { Link as I18nLink, usePathname, useRouter } from "@/i18n/routing";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { HeaderLink } from "@/types/common"; import { HeaderLink } from "@/types/common";
import { ExternalLink } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
const HeaderLinks = () => { const HeaderLinks = () => {
@@ -17,102 +16,34 @@ const HeaderLinks = () => {
return ( return (
<div className="hidden md:flex flex-row items-center gap-x-2 text-sm font-medium text-muted-500"> <div className="hidden md:flex flex-row items-center gap-x-2 text-sm font-medium text-muted-500">
{headerLinks.map((link) => { {headerLinks.map((link) => {
const isAbout = link.href === "/about";
const isNews = link.href === "/blog";
if (isAbout) {
return (
<div key={link.name} className="relative group">
<I18nLink
href={link.href}
title={link.name}
prefetch={true}
className={cn(
"rounded-xl px-4 py-2 flex items-center gap-x-1 hover:bg-accent-foreground/10 hover:text-accent-foreground",
pathname === link.href && "font-semibold text-accent-foreground"
)}
>
{link.name}
</I18nLink>
{/* 移除触发器与菜单间的间隙,使用 pt-2 保持视觉间距但不中断 hover 区域 */}
<div className="absolute left-0 top-full hidden group-hover:block z-50 pt-2">
<div className="w-56 rounded-md border bg-white shadow-md overflow-hidden">
<I18nLink prefetch={false} href="/about/arch#arch-vision" className="block px-4 py-2 hover:bg-red-600 hover:text-white"></I18nLink>
<I18nLink prefetch={false} href="/about/arch#arch-products" className="block px-4 py-2 hover:bg-red-600 hover:text-white"></I18nLink>
<I18nLink prefetch={false} href="/about/arch#arch-application" className="block px-4 py-2 hover:bg-red-600 hover:text-white"></I18nLink>
<I18nLink prefetch={false} href="/about/arch#arch-contact" className="block px-4 py-2 hover:bg-red-600 hover:text-white"></I18nLink>
</div>
</div>
</div>
);
}
if (isNews) {
return (
<div key={link.name} className="relative group">
<I18nLink
href={link.href}
title={link.name}
prefetch={true}
className={cn(
"rounded-xl px-4 py-2 flex items-center gap-x-1 hover:bg-accent-foreground/10 hover:text-accent-foreground",
pathname === link.href && "font-semibold text-accent-foreground"
)}
>
{link.name}
</I18nLink>
<div className="absolute left-0 top-full hidden group-hover:block z-50 pt-2">
<div className="w-44 rounded-md border bg-white shadow-md overflow-hidden">
{[
{ key: "company", label: "公司新闻" },
{ key: "experts", label: "专家访谈" },
{ key: "industry", label: "行业动态" },
{ key: "notice", label: "公告" },
].map((it) => (
<button
key={it.key}
className="block w-full text-left px-4 py-2 hover:bg-red-600 hover:text-white"
onMouseDown={(e) => {
const url = `${localePrefix}/blog?type=${it.key}`;
try { router.push(url as any); } catch {}
// 兜底,确保跳转
setTimeout(() => { if (typeof window !== 'undefined') window.location.href = url; }, 0);
}}
onClick={(e) => {
const url = `${localePrefix}/blog?type=${it.key}`;
try { router.push(url as any); } catch {}
}}
onTouchStart={() => {
const url = `${localePrefix}/blog?type=${it.key}`;
try { router.push(url as any); } catch {}
}}
>
{it.label}
</button>
))}
</div>
</div>
</div>
);
}
return ( return (
<I18nLink <div key={link.name} className="relative group">
key={link.name} <I18nLink
href={link.href} href={link.href}
title={link.name} title={link.name}
prefetch={link.target && link.target === "_blank" ? false : true} prefetch={true}
target={link.target || "_self"} className={cn(
rel={link.rel || undefined} "rounded-xl px-4 py-2 flex items-center gap-x-1 hover:bg-accent-foreground/10 hover:text-accent-foreground",
className={cn( pathname === link.href && "font-semibold text-accent-foreground"
"rounded-xl px-4 py-2 flex items-center gap-x-1 hover:bg-accent-foreground/10 hover:text-accent-foreground", )}
pathname === link.href && "font-semibold text-accent-foreground" >
)} {link.name}
> </I18nLink>
{link.name} {
{link.target && link.target === "_blank" && ( link?.children && (
<span className="text-xs"> <div className="absolute left-0 top-full hidden group-hover:block z-50 pt-2">
<ExternalLink className="w-4 h-4" /> <div className="w-56 rounded-md border bg-white shadow-md overflow-hidden">
</span> {link.children.map((child) => (
)} <I18nLink key={child.name} prefetch={false} href={child.href} className="block px-4 py-2 hover:bg-red-600 hover:text-white">
</I18nLink> {child.name}
</I18nLink>
))}
</div>
</div>
)
}
</div>
); );
})} })}
</div> </div>

View File

@@ -13,7 +13,33 @@
}, },
{ {
"name": "走进福安德", "name": "走进福安德",
"href": "/about" "href": "/about",
"children": [
{
"name": "公司简介",
"href": "/about?section=company"
},
{
"name": "企业文化",
"href": "/about?section=culture"
},
{
"name": "生产基地",
"href": "/about?section=base"
},
{
"name": "组织架构",
"href": "/about?section=organization"
},
{
"name": "荣誉资质",
"href": "/about?section=awards"
},
{
"name": "发展历程",
"href": "/about?section=history"
}
]
}, },
{ {
"name": "产品中心", "name": "产品中心",
@@ -21,7 +47,21 @@
}, },
{ {
"name": "新闻中心", "name": "新闻中心",
"href": "/blog" "href": "/blog",
"children": [
{
"name": "公告",
"href": "/blog?category=announce"
},
{
"name": "新闻",
"href": "/blog?category=news"
},
{
"name": "活动",
"href": "/blog?category=event"
}
]
} }
] ]
}, },
@@ -35,32 +75,32 @@
"title": "关于我们", "title": "关于我们",
"links": [ "links": [
{ {
"href": "/about?section=company", "href": "/zh/about?section=company",
"name": "公司简介", "name": "公司简介",
"useA": true "useA": true
}, },
{ {
"href": "/about?section=culture", "href": "/zh/about?section=culture",
"name": "企业文化", "name": "企业文化",
"useA": true "useA": true
}, },
{ {
"href": "/about?section=base", "href": "/zh/about?section=base",
"name": "生产基地", "name": "生产基地",
"useA": true "useA": true
}, },
{ {
"href": "/about?section=organization", "href": "/zh/about?section=organization",
"name": "组织架构", "name": "组织架构",
"useA": true "useA": true
}, },
{ {
"href": "/about?section=awards", "href": "/zh/about?section=awards",
"name": "荣誉资质", "name": "荣誉资质",
"useA": true "useA": true
}, },
{ {
"href": "/about?section=history", "href": "/zh/about?section=history",
"name": "发展历程", "name": "发展历程",
"useA": true "useA": true
} }

View File

@@ -1,66 +1,92 @@
import { DEFAULT_LOCALE } from '@/i18n/routing'; import { DEFAULT_LOCALE } from '@/i18n/routing';
import { BlogPost } from '@/types/blog'; import { BlogPost } from '@/types/blog';
import fs from 'fs';
import matter from 'gray-matter';
import path from 'path';
const POSTS_BATCH_SIZE = 10; export async function getPosts(locale: string = DEFAULT_LOCALE, category?: string): Promise<{ posts: BlogPost[], total: number }> {
const Id_Map: Record<string, string> = {
export async function getPosts(locale: string = DEFAULT_LOCALE): Promise<{ posts: BlogPost[] }> { 'announce': '1989174108560080897',
const postsDirectory = path.join(process.cwd(), 'blogs', locale); 'news': '1988814504336605185',
'event': '1989174018231549954',
// is directory exist
if (!fs.existsSync(postsDirectory)) {
return { posts: [] };
} }
let filenames = await fs.promises.readdir(postsDirectory); let url = 'http://49.232.154.205:18081/export/article/list?pageNum=1&pageSize=10&langCode=' + locale;
filenames = filenames.reverse(); if (category) {
url += '&categoryId=' + Id_Map[category || 'announce']
}
let allPosts: BlogPost[] = []; console.log(url);
const response = await fetch(url);
const data = await response.json();
const posts = data.rows.map((item: any) => {
return {
locale, // use locale parameter
title: item.title,
description: item.summary,
image: item.cover || '',
slug: item.articleId,
tags: '',
date: item.publishedTime,
// visible: data.visible || 'published',
pin: false,
content: item.content,
metadata: data,
}
}) || [];
// // is directory exist
// if (!fs.existsSync(postsDirectory)) {
// return { posts: [] };
// }
// let filenames = await fs.promises.readdir(postsDirectory);
// filenames = filenames.reverse();
// let allPosts: BlogPost[] = [];
// read file by batch // read file by batch
for (let i = 0; i < filenames.length; i += POSTS_BATCH_SIZE) { // for (let i = 0; i < filenames.length; i += POSTS_BATCH_SIZE) {
const batchFilenames = filenames.slice(i, i + POSTS_BATCH_SIZE); // const batchFilenames = filenames.slice(i, i + POSTS_BATCH_SIZE);
const batchPosts: BlogPost[] = await Promise.all( // const batchPosts: BlogPost[] = await Promise.all(
batchFilenames.map(async (filename) => { // batchFilenames.map(async (filename) => {
const fullPath = path.join(postsDirectory, filename); // const fullPath = path.join(postsDirectory, filename);
const fileContents = await fs.promises.readFile(fullPath, 'utf8'); // const fileContents = await fs.promises.readFile(fullPath, 'utf8');
const { data, content } = matter(fileContents); // const { data, content } = matter(fileContents);
return { // return {
locale, // use locale parameter // locale, // use locale parameter
title: data.title, // title: data.title,
description: data.description, // description: data.description,
image: data.image || '', // image: data.image || '',
slug: data.slug, // slug: data.slug,
tags: data.tags, // tags: data.tags,
date: data.date, // date: data.date,
visible: data.visible || 'published', // visible: data.visible || 'published',
pin: data.pin || false, // pin: data.pin || false,
content, // content,
metadata: data, // metadata: data,
}; // };
}) // })
); // );
allPosts.push(...batchPosts); // allPosts.push(...batchPosts);
} // }
// filter out non-published articles // filter out non-published articles
allPosts = allPosts.filter(post => post.visible === 'published'); // allPosts = allPosts.filter(post => post.visible === 'published');
// sort posts by pin and date // sort posts by pin and date
allPosts = allPosts.sort((a, b) => { // allPosts = allPosts.sort((a, b) => {
if (a.pin !== b.pin) { // if (a.pin !== b.pin) {
return (b.pin ? 1 : 0) - (a.pin ? 1 : 0); // return (b.pin ? 1 : 0) - (a.pin ? 1 : 0);
} // }
return new Date(b.date).getTime() - new Date(a.date).getTime(); // return new Date(b.date).getTime() - new Date(a.date).getTime();
}); // });
return { return {
posts: allPosts, posts,
total: data.total,
}; };
} }

View File

@@ -4,6 +4,7 @@ export interface HeaderLink {
href: string; href: string;
target?: string; target?: string;
rel?: string; rel?: string;
children?: HeaderLink[];
}; };
export interface FooterLink { export interface FooterLink {