feat: 接入后端全局Snippet(导航菜单/站点设置),补充团队/证言/合作伙伴 service 层
- Header 改为异步 Server Component,接入 NavigationMenu(name=main), 接口不可用或未配置时回退静态导航项 - Footer 改为异步 Server Component,接入 SiteSettings, 公司名/ICP备案号/备案链接优先取后台配置,缺省回退硬编码值 - types/wagtail.ts 新增 TeamMember/Testimonial/Partner/NavigationMenu/SiteSettings 类型 - 新增 services: team/testimonial/partner/navigation/site-settings.service.ts - lint、build 均已验证通过
This commit is contained in:
@@ -1,28 +1,41 @@
|
|||||||
export default function Footer() {
|
import { getSiteSettings } from "@/services/site-settings.service";
|
||||||
|
|
||||||
|
// 合规要求:中国大陆网站需展示 ICP 备案号并链接至工信部备案查询。
|
||||||
|
// 详见 documents/设计方案分析与完善版.md §2.15。
|
||||||
|
const FALLBACK_ICP_NUMBER = "冀ICP备2025130506号-1";
|
||||||
|
const FALLBACK_ICP_URL = "https://beian.miit.gov.cn/";
|
||||||
|
|
||||||
|
export default async function Footer() {
|
||||||
|
// 优先使用后台配置的站点设置(apps.core.SiteSettings),
|
||||||
|
// 接口不可用时回退到硬编码的备案信息,保证构建/渲染始终可用。
|
||||||
|
const siteSettings = await getSiteSettings();
|
||||||
|
const icpNumber = siteSettings?.icp_number || FALLBACK_ICP_NUMBER;
|
||||||
|
const icpUrl = siteSettings?.icp_url || FALLBACK_ICP_URL;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="mt-auto border-t border-slate-100 bg-slate-950 py-10 text-slate-400">
|
<footer className="mt-auto border-t border-slate-100 bg-slate-950 py-10 text-slate-400">
|
||||||
<div className="mx-auto max-w-7xl px-6 text-center text-sm">
|
<div className="mx-auto max-w-7xl px-6 text-center text-sm">
|
||||||
<p>© {new Date().getFullYear()} 企业官网. All rights reserved.</p>
|
<p>
|
||||||
|
© {new Date().getFullYear()}{" "}
|
||||||
|
{siteSettings?.company_name || "企业官网"}. All rights reserved.
|
||||||
|
</p>
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
<a href="/privacy-policy" className="hover:text-white">
|
<a href="/privacy-policy" className="hover:text-white">
|
||||||
隐私政策
|
隐私政策
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{/*
|
|
||||||
合规要求:中国大陆网站需展示 ICP 备案号并链接至工信部备案查询。
|
|
||||||
详见 documents/设计方案分析与完善版.md §2.15。
|
|
||||||
*/}
|
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
<a
|
<a
|
||||||
href="https://beian.miit.gov.cn/"
|
href={icpUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="hover:text-white"
|
className="hover:text-white"
|
||||||
>
|
>
|
||||||
冀ICP备2025130506号-1
|
{icpNumber}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { getNavigationMenu } from "@/services/navigation.service";
|
||||||
|
|
||||||
const navItems = [
|
const fallbackNavItems = [
|
||||||
{ title: "首页", href: "/" },
|
{ title: "首页", href: "/" },
|
||||||
{ title: "产品", href: "/products" },
|
{ title: "产品", href: "/products" },
|
||||||
{ title: "解决方案", href: "/solutions" },
|
{ title: "解决方案", href: "/solutions" },
|
||||||
@@ -8,7 +9,18 @@ const navItems = [
|
|||||||
{ title: "博客", href: "/blog" },
|
{ title: "博客", href: "/blog" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Header() {
|
export default async function Header() {
|
||||||
|
// 优先使用后台配置的主导航(apps.core.NavigationMenu,name="main"),
|
||||||
|
// 未配置或接口不可用时回退到静态导航项,保证构建/渲染始终可用。
|
||||||
|
const menu = await getNavigationMenu("main");
|
||||||
|
const navItems = menu?.items.length
|
||||||
|
? menu.items.map((item) => ({
|
||||||
|
title: item.label,
|
||||||
|
href: item.url,
|
||||||
|
newTab: item.open_in_new_tab,
|
||||||
|
}))
|
||||||
|
: fallbackNavItems.map((item) => ({ ...item, newTab: false }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-slate-100 bg-white/80 backdrop-blur">
|
<header className="sticky top-0 z-50 border-b border-slate-100 bg-white/80 backdrop-blur">
|
||||||
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-6">
|
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-6">
|
||||||
@@ -17,7 +29,13 @@ export default function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
<nav className="flex gap-8">
|
<nav className="flex gap-8">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<Link key={item.href} href={item.href} className="text-slate-700">
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className="text-slate-700"
|
||||||
|
target={item.newTab ? "_blank" : undefined}
|
||||||
|
rel={item.newTab ? "noreferrer" : undefined}
|
||||||
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -26,3 +44,4 @@ export default function Header() {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { fetchAPI } from "./api-client";
|
||||||
|
import type { NavigationMenuValue } from "@/types/wagtail";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 name(如 main / footer)获取导航菜单(apps.core.NavigationMenu Snippet)。
|
||||||
|
* 未在后台配置对应菜单时返回 null,调用方应回退到静态导航项。
|
||||||
|
*/
|
||||||
|
export async function getNavigationMenu(
|
||||||
|
name: string,
|
||||||
|
): Promise<NavigationMenuValue | null> {
|
||||||
|
try {
|
||||||
|
return await fetchAPI<NavigationMenuValue>(
|
||||||
|
`/api/v1/custom/core/navigation/?name=${encodeURIComponent(name)}`,
|
||||||
|
{ tags: [`navigation:${name}`] },
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { fetchAPI } from "./api-client";
|
||||||
|
import type { PartnerValue } from "@/types/wagtail";
|
||||||
|
|
||||||
|
/** 获取合作伙伴/客户 Logo 列表(apps.core.Partner Snippet)。 */
|
||||||
|
export async function getPartners(): Promise<PartnerValue[]> {
|
||||||
|
const res = await fetchAPI<{ items: PartnerValue[] }>(
|
||||||
|
"/api/v1/custom/core/partners/",
|
||||||
|
{ tags: ["partners"] },
|
||||||
|
);
|
||||||
|
return res.items;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { fetchAPI } from "./api-client";
|
||||||
|
import type { SiteSettingsValue } from "@/types/wagtail";
|
||||||
|
|
||||||
|
/** 获取站点全局配置(apps.core.SiteSettings,wagtail.contrib.settings)。 */
|
||||||
|
export async function getSiteSettings(): Promise<SiteSettingsValue | null> {
|
||||||
|
try {
|
||||||
|
return await fetchAPI<SiteSettingsValue>(
|
||||||
|
"/api/v1/custom/core/site-settings/",
|
||||||
|
{ tags: ["site-settings"] },
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { fetchAPI } from "./api-client";
|
||||||
|
import type { TeamMemberValue } from "@/types/wagtail";
|
||||||
|
|
||||||
|
/** 获取团队成员列表(apps.core.TeamMember Snippet)。 */
|
||||||
|
export async function getTeamMembers(): Promise<TeamMemberValue[]> {
|
||||||
|
const res = await fetchAPI<{ items: TeamMemberValue[] }>(
|
||||||
|
"/api/v1/custom/core/team/",
|
||||||
|
{ tags: ["team"] },
|
||||||
|
);
|
||||||
|
return res.items;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { fetchAPI } from "./api-client";
|
||||||
|
import type { TestimonialValue } from "@/types/wagtail";
|
||||||
|
|
||||||
|
/** 获取客户证言列表(apps.core.Testimonial Snippet)。 */
|
||||||
|
export async function getTestimonials(): Promise<TestimonialValue[]> {
|
||||||
|
const res = await fetchAPI<{ items: TestimonialValue[] }>(
|
||||||
|
"/api/v1/custom/core/testimonials/",
|
||||||
|
{ tags: ["testimonials"] },
|
||||||
|
);
|
||||||
|
return res.items;
|
||||||
|
}
|
||||||
@@ -160,3 +160,53 @@ export interface SolutionPageDetail extends SolutionPageSummary {
|
|||||||
export interface SimpleContentPageDetail extends WagtailPageSummary {
|
export interface SimpleContentPageDetail extends WagtailPageSummary {
|
||||||
body: string; // richtext HTML
|
body: string; // richtext HTML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局 Snippet 类型(apps/core/models.py)。
|
||||||
|
* 通过 /api/v1/custom/core/... 只读接口获取,非 Wagtail Page API。
|
||||||
|
*/
|
||||||
|
export interface TeamMemberValue {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
bio: string;
|
||||||
|
photo: { url: string; title: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestimonialValue {
|
||||||
|
id: number;
|
||||||
|
quote: string;
|
||||||
|
author_name: string;
|
||||||
|
author_title: string;
|
||||||
|
author_photo: { url: string; title: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartnerValue {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
website_url: string;
|
||||||
|
logo: { url: string; title: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavigationMenuItemValue {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
open_in_new_tab: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavigationMenuValue {
|
||||||
|
name: string;
|
||||||
|
items: NavigationMenuItemValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteSettingsValue {
|
||||||
|
company_name: string;
|
||||||
|
contact_phone: string;
|
||||||
|
contact_email: string;
|
||||||
|
address: string;
|
||||||
|
icp_number: string;
|
||||||
|
icp_url: string;
|
||||||
|
wechat_account: string;
|
||||||
|
weibo_url: string;
|
||||||
|
wechat_qrcode: { url: string; title: string } | null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user