feat(app): 全局样式重构与核心应用层更新
- 重构 globals.css 设计令牌系统,对齐咨询风品牌色与排版 - 更新根布局,集成 SEO schema 与黑暗模式防闪烁脚本 - 添加 robots.ts 与 sitemap.ts 动态生成 - 添加 middleware.ts 中间件层 - 更新隐私政策与服务条款页面
This commit is contained in:
+9
-9
@@ -21,10 +21,10 @@ export default function Error({
|
||||
<div className="container-wide px-4 py-20">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<div className="mb-8">
|
||||
<div className="w-24 h-24 bg-[var(--color-brand-primary-bg)] rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<AlertTriangle className="w-12 h-12 text-[var(--color-brand-primary)]" />
|
||||
<div className="w-24 h-24 bg-[var(--color-brand-bg)] rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<AlertTriangle className="w-12 h-12 text-[var(--color-brand)]" />
|
||||
</div>
|
||||
<div className="w-32 h-1 bg-[var(--color-brand-primary)] mx-auto" />
|
||||
<div className="w-32 h-1 bg-[var(--color-brand)] mx-auto" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||
@@ -53,7 +53,7 @@ export default function Error({
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={reset}
|
||||
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white"
|
||||
className="bg-[var(--color-brand)] hover:bg-[var(--color-brand-hover)] text-white"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5 mr-2" />
|
||||
重试
|
||||
@@ -81,8 +81,8 @@ export default function Error({
|
||||
href="/contact"
|
||||
className="flex items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-[var(--color-brand-primary-bg)] rounded-lg flex items-center justify-center mr-4 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<AlertTriangle className="w-5 h-5 text-[var(--color-brand-primary)]" />
|
||||
<div className="w-10 h-10 bg-[var(--color-brand-bg)] rounded-lg flex items-center justify-center mr-4 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<AlertTriangle className="w-5 h-5 text-[var(--color-brand)]" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold text-[var(--color-text-primary)]">联系我们</div>
|
||||
@@ -94,8 +94,8 @@ export default function Error({
|
||||
href="/services"
|
||||
className="flex items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-[var(--color-brand-primary-bg)] rounded-lg flex items-center justify-center mr-4 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<RefreshCw className="w-5 h-5 text-[var(--color-brand-primary)]" />
|
||||
<div className="w-10 h-10 bg-[var(--color-brand-bg)] rounded-lg flex items-center justify-center mr-4 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<RefreshCw className="w-5 h-5 text-[var(--color-brand)]" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold text-[var(--color-text-primary)]">服务</div>
|
||||
@@ -107,7 +107,7 @@ export default function Error({
|
||||
|
||||
<div className="mt-8 text-sm text-[var(--color-text-placeholder)]">
|
||||
如果问题持续存在,请{' '}
|
||||
<StaticLink href="/contact" className="text-[var(--color-brand-primary)] hover:underline">
|
||||
<StaticLink href="/contact" className="text-[var(--color-brand)] hover:underline">
|
||||
联系我们的技术团队
|
||||
</StaticLink>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
+856
-567
File diff suppressed because it is too large
Load Diff
+39
-52
@@ -1,9 +1,6 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import { Ma_Shan_Zheng, Noto_Sans_SC } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Suspense } from "react";
|
||||
import { ThemeProvider } from "@/contexts/theme-context";
|
||||
import { GoogleAnalyticsWrapper } from "@/components/analytics/GoogleAnalyticsWrapper";
|
||||
import { GlobalErrorTracker } from "@/components/analytics/GlobalErrorTracker";
|
||||
import { CookieConsent } from "@/components/analytics/CookieConsent";
|
||||
@@ -16,42 +13,8 @@ import { ErrorBoundary } from "@/components/ui/error-boundary";
|
||||
import { ScrollProgress } from "@/components/ui/scroll-progress";
|
||||
import { BackToTop } from "@/components/ui/back-to-top";
|
||||
import { ClientLayout } from "@/components/layout/client-layout";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/geist-sans.woff2",
|
||||
variable: "--font-geist-sans",
|
||||
display: "swap",
|
||||
preload: false,
|
||||
});
|
||||
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/geist-mono.woff2",
|
||||
variable: "--font-geist-mono",
|
||||
display: "swap",
|
||||
preload: false,
|
||||
});
|
||||
|
||||
const aoyagiReisho = localFont({
|
||||
src: "./fonts/AoyagiReisho-subset.ttf",
|
||||
variable: "--font-aoyagi-reisho",
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const maShanZheng = Ma_Shan_Zheng({
|
||||
weight: "400",
|
||||
variable: "--font-ma-shan-zheng",
|
||||
display: "swap",
|
||||
preload: false,
|
||||
});
|
||||
|
||||
const notoSansSC = Noto_Sans_SC({
|
||||
weight: ["400", "500", "700"],
|
||||
variable: "--font-noto-sans-sc",
|
||||
display: "swap",
|
||||
subsets: ["latin"],
|
||||
preload: false,
|
||||
});
|
||||
import { getPublishedItems } from "@/lib/cms/data-server";
|
||||
import { SiteConfigProvider, type SiteConfig, type NavigationItem, type MegaDropdownGroup } from "@/lib/site-config";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://www.novalon.cn"),
|
||||
@@ -109,25 +72,42 @@ export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 5,
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#FFFFFF" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#0A0A0A" },
|
||||
],
|
||||
themeColor: "#FFFFFF",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
// Fetch site configuration and navigation from CMS
|
||||
const [navItems, configItems] = await Promise.all([
|
||||
getPublishedItems('navigation').catch(() => []),
|
||||
getPublishedItems('site-config').catch(() => []),
|
||||
]);
|
||||
|
||||
const navData = navItems[0]?.data as Record<string, unknown> | undefined;
|
||||
const configData = configItems[0]?.data as Record<string, unknown> | undefined;
|
||||
|
||||
const siteConfig: SiteConfig = {
|
||||
name: (configData?.name as string) || '',
|
||||
shortName: (configData?.shortName as string) || '',
|
||||
displayName: (configData?.displayName as string) || '',
|
||||
slogan: (configData?.slogan as string) || '',
|
||||
description: (configData?.description as string) || '',
|
||||
founded: (configData?.founded as string) || '',
|
||||
location: (configData?.location as string) || '',
|
||||
email: (configData?.email as string) || '',
|
||||
address: (configData?.address as string) || '',
|
||||
icp: (configData?.icp as string) || '',
|
||||
police: (configData?.police as string) || '',
|
||||
mainNav: (navData?.mainNav as NavigationItem[]) || [],
|
||||
megaDropdown: (navData?.megaDropdown as Record<string, MegaDropdownGroup[]>) || {},
|
||||
};
|
||||
|
||||
return (
|
||||
<html lang="zh-CN" className="scroll-smooth" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function(){try{var t=localStorage.getItem('novalon-theme');if(t==='dark'||(t==='system'||!t)&&window.matchMedia('(prefers-color-scheme:dark)').matches){document.documentElement.setAttribute('data-theme','dark')}}catch(e){}})()`,
|
||||
}}
|
||||
/>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
@@ -142,21 +122,28 @@ export default function RootLayout({
|
||||
<LocalBusinessSchema />
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${aoyagiReisho.variable} ${maShanZheng.variable} ${notoSansSC.variable} font-sans antialiased`}
|
||||
className="font-sans antialiased"
|
||||
>
|
||||
<a
|
||||
href="#main-content"
|
||||
data-skip-to-content="true"
|
||||
className="absolute left-[-9999px] top-0 z-50 px-4 py-2 bg-brand text-white rounded-br-lg shadow-lg focus:left-0 focus:outline-none"
|
||||
>
|
||||
跳转到主要内容
|
||||
</a>
|
||||
<ScrollProgress />
|
||||
<GoogleAnalyticsWrapper />
|
||||
<GlobalErrorTracker />
|
||||
<PerformanceTracker />
|
||||
<OutboundLinkTracker />
|
||||
<ScrollDepthTracker />
|
||||
<ThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<SiteConfigProvider config={siteConfig}>
|
||||
<ClientLayout>
|
||||
{children}
|
||||
</ClientLayout>
|
||||
</SiteConfigProvider>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
<Suspense fallback={null}>
|
||||
<MobileTabBar />
|
||||
</Suspense>
|
||||
|
||||
+14
-9
@@ -1,16 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Home, ArrowLeft, Search } from 'lucide-react';
|
||||
|
||||
export default function NotFound() {
|
||||
useEffect(() => {
|
||||
document.title = '页面未找到 - 睿新致远';
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)] flex items-center justify-center">
|
||||
<div className="container-wide px-4 py-20">
|
||||
<div className="max-w-xl mx-auto text-center">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-8xl sm:text-[120px] font-bold text-[var(--color-brand-primary)] leading-none mb-4 opacity-20">
|
||||
<h1 className="text-8xl sm:text-[120px] font-bold text-[var(--color-brand)] leading-none mb-4 opacity-20">
|
||||
404
|
||||
</h1>
|
||||
</div>
|
||||
@@ -54,8 +59,8 @@ export default function NotFound() {
|
||||
href="/products"
|
||||
className="flex flex-col items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
||||
>
|
||||
<div className="w-9 h-9 bg-[var(--color-brand-primary-bg)] rounded-lg flex items-center justify-center mb-2 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<Search className="w-4 h-4 text-[var(--color-brand-primary)]" />
|
||||
<div className="w-9 h-9 bg-[var(--color-brand-bg)] rounded-lg flex items-center justify-center mb-2 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<Search className="w-4 h-4 text-[var(--color-brand)]" />
|
||||
</div>
|
||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">产品</span>
|
||||
</StaticLink>
|
||||
@@ -64,8 +69,8 @@ export default function NotFound() {
|
||||
href="/solutions"
|
||||
className="flex flex-col items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
||||
>
|
||||
<div className="w-9 h-9 bg-[var(--color-brand-primary-bg)] rounded-lg flex items-center justify-center mb-2 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<Search className="w-4 h-4 text-[var(--color-brand-primary)]" />
|
||||
<div className="w-9 h-9 bg-[var(--color-brand-bg)] rounded-lg flex items-center justify-center mb-2 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<Search className="w-4 h-4 text-[var(--color-brand)]" />
|
||||
</div>
|
||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">解决方案</span>
|
||||
</StaticLink>
|
||||
@@ -74,8 +79,8 @@ export default function NotFound() {
|
||||
href="/about"
|
||||
className="flex flex-col items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
||||
>
|
||||
<div className="w-9 h-9 bg-[var(--color-brand-primary-bg)] rounded-lg flex items-center justify-center mb-2 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<Search className="w-4 h-4 text-[var(--color-brand-primary)]" />
|
||||
<div className="w-9 h-9 bg-[var(--color-brand-bg)] rounded-lg flex items-center justify-center mb-2 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<Search className="w-4 h-4 text-[var(--color-brand)]" />
|
||||
</div>
|
||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">关于我们</span>
|
||||
</StaticLink>
|
||||
@@ -84,8 +89,8 @@ export default function NotFound() {
|
||||
href="/contact"
|
||||
className="flex flex-col items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
||||
>
|
||||
<div className="w-9 h-9 bg-[var(--color-brand-primary-bg)] rounded-lg flex items-center justify-center mb-2 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<Search className="w-4 h-4 text-[var(--color-brand-primary)]" />
|
||||
<div className="w-9 h-9 bg-[var(--color-brand-bg)] rounded-lg flex items-center justify-center mb-2 group-hover:bg-[var(--color-challenge-isolation-hover)] transition-colors">
|
||||
<Search className="w-4 h-4 text-[var(--color-brand)]" />
|
||||
</div>
|
||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">联系我们</span>
|
||||
</StaticLink>
|
||||
|
||||
@@ -8,35 +8,40 @@ describe('PrivacyPolicyPage', () => {
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render privacy policy page', () => {
|
||||
render(<PrivacyPolicyPage />);
|
||||
it('should render privacy policy page', async () => {
|
||||
const page = await PrivacyPolicyPage();
|
||||
render(page);
|
||||
const container = screen.getByText('隐私政策').closest('div');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page title', () => {
|
||||
render(<PrivacyPolicyPage />);
|
||||
it('should render page title', async () => {
|
||||
const page = await PrivacyPolicyPage();
|
||||
render(page);
|
||||
const title = screen.getByRole('heading', { level: 1 });
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent('隐私政策');
|
||||
});
|
||||
|
||||
it('should render introduction section', () => {
|
||||
render(<PrivacyPolicyPage />);
|
||||
it('should render introduction section', async () => {
|
||||
const page = await PrivacyPolicyPage();
|
||||
render(page);
|
||||
const intro = screen.getByText('引言');
|
||||
expect(intro).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render information collection section', () => {
|
||||
render(<PrivacyPolicyPage />);
|
||||
it('should render information collection section', async () => {
|
||||
const page = await PrivacyPolicyPage();
|
||||
render(page);
|
||||
const section = screen.getByText(/我们如何收集和使用您的个人信息/i);
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<PrivacyPolicyPage />);
|
||||
it('should have proper heading hierarchy', async () => {
|
||||
const page = await PrivacyPolicyPage();
|
||||
render(page);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
|
||||
|
||||
+41
-15
@@ -1,4 +1,5 @@
|
||||
import { Metadata } from 'next';
|
||||
import { getPublishedItems } from '@/lib/cms/data-server';
|
||||
import { COMPANY_INFO } from '@/lib/constants';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -6,22 +7,8 @@ export const metadata: Metadata = {
|
||||
description: `${COMPANY_INFO.name}隐私政策`,
|
||||
};
|
||||
|
||||
export default function PrivacyPolicyPage() {
|
||||
function PrivacyContent() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<div className="bg-gradient-to-br from-[var(--color-brand-primary)] via-[var(--color-brand-primary)]/80 to-[var(--color-hero-dark-end)] py-20">
|
||||
<div className="container-wide">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
隐私政策
|
||||
</h1>
|
||||
<p className="text-xl text-white/90 max-w-2xl">
|
||||
我们重视您的隐私,致力于保护您的个人信息安全
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container-wide py-16">
|
||||
<div className="max-w-4xl">
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">引言</h2>
|
||||
@@ -232,6 +219,45 @@ export default function PrivacyPolicyPage() {
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function PrivacyPolicyPage() {
|
||||
let cmsContent: string | null = null;
|
||||
|
||||
try {
|
||||
const items = await getPublishedItems('legal-page');
|
||||
const privacyItem = items.find((item) => item.data.pageType === 'privacy');
|
||||
if (privacyItem && typeof privacyItem.data.content === 'string' && privacyItem.data.content.length > 0) {
|
||||
cmsContent = privacyItem.data.content as string;
|
||||
}
|
||||
} catch {
|
||||
// CMS not available, fall back to inline content
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<div className="bg-gradient-to-br from-[var(--color-brand)] via-[var(--color-brand)]/80 to-[var(--color-hero-dark-end)] py-20">
|
||||
<div className="container-wide">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
隐私政策
|
||||
</h1>
|
||||
<p className="text-xl text-white/90 max-w-2xl">
|
||||
我们重视您的隐私,致力于保护您的个人信息安全
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container-wide py-16">
|
||||
<div className="max-w-4xl">
|
||||
{cmsContent ? (
|
||||
<div
|
||||
className="prose prose-lg max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: cmsContent }}
|
||||
/>
|
||||
) : (
|
||||
<PrivacyContent />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: [
|
||||
'/_next/',
|
||||
'/api/',
|
||||
'/admin/',
|
||||
],
|
||||
},
|
||||
{
|
||||
userAgent: 'Googlebot',
|
||||
allow: '/',
|
||||
disallow: [
|
||||
'/_next/',
|
||||
'/api/',
|
||||
],
|
||||
},
|
||||
],
|
||||
sitemap: 'https://www.novalon.cn/sitemap.xml',
|
||||
host: 'https://www.novalon.cn',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { getAllPublishedSlugs } from '@/lib/cms/data-server';
|
||||
|
||||
const BASE_URL = 'https://www.novalon.cn';
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const staticPages: MetadataRoute.Sitemap = [
|
||||
{ url: `${BASE_URL}/`, lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
|
||||
{ url: `${BASE_URL}/about`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
|
||||
{ url: `${BASE_URL}/services`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.9 },
|
||||
{ url: `${BASE_URL}/products`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.9 },
|
||||
{ url: `${BASE_URL}/solutions`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.9 },
|
||||
{ url: `${BASE_URL}/cases`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.8 },
|
||||
{ url: `${BASE_URL}/news`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.7 },
|
||||
{ url: `${BASE_URL}/team`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.6 },
|
||||
{ url: `${BASE_URL}/contact`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.7 },
|
||||
];
|
||||
|
||||
try {
|
||||
// 从 CMS 获取动态页面 slug
|
||||
const [caseSlugs, newsSlugs, serviceSlugs, productSlugs, solutionSlugs] = await Promise.all([
|
||||
getAllPublishedSlugs('case-study'),
|
||||
getAllPublishedSlugs('news'),
|
||||
getAllPublishedSlugs('service'),
|
||||
getAllPublishedSlugs('product'),
|
||||
getAllPublishedSlugs('solution'),
|
||||
]);
|
||||
|
||||
const casePages: MetadataRoute.Sitemap = caseSlugs.map(({ slug }) => ({
|
||||
url: `${BASE_URL}/cases/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
|
||||
const newsPages: MetadataRoute.Sitemap = newsSlugs.map(({ slug }) => ({
|
||||
url: `${BASE_URL}/news/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
const servicePages: MetadataRoute.Sitemap = serviceSlugs.map(({ slug }) => ({
|
||||
url: `${BASE_URL}/services/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
|
||||
const productPages: MetadataRoute.Sitemap = productSlugs.map(({ slug }) => ({
|
||||
url: `${BASE_URL}/products/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
|
||||
const solutionPages: MetadataRoute.Sitemap = solutionSlugs.map(({ slug }) => ({
|
||||
url: `${BASE_URL}/solutions/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
|
||||
return [
|
||||
...staticPages,
|
||||
...casePages,
|
||||
...newsPages,
|
||||
...servicePages,
|
||||
...productPages,
|
||||
...solutionPages,
|
||||
];
|
||||
} catch {
|
||||
// 数据库不可用时返回静态页面
|
||||
return staticPages;
|
||||
}
|
||||
}
|
||||
+15
-10
@@ -8,35 +8,40 @@ describe('TermsOfServicePage', () => {
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render terms of service page', () => {
|
||||
render(<TermsOfServicePage />);
|
||||
it('should render terms of service page', async () => {
|
||||
const page = await TermsOfServicePage();
|
||||
render(page);
|
||||
const container = screen.getByText('服务条款').closest('div');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page title', () => {
|
||||
render(<TermsOfServicePage />);
|
||||
it('should render page title', async () => {
|
||||
const page = await TermsOfServicePage();
|
||||
render(page);
|
||||
const title = screen.getByRole('heading', { level: 1 });
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent('服务条款');
|
||||
});
|
||||
|
||||
it('should render introduction section', () => {
|
||||
render(<TermsOfServicePage />);
|
||||
it('should render introduction section', async () => {
|
||||
const page = await TermsOfServicePage();
|
||||
render(page);
|
||||
const intro = screen.getByText('引言');
|
||||
expect(intro).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render service content section', () => {
|
||||
render(<TermsOfServicePage />);
|
||||
it('should render service content section', async () => {
|
||||
const page = await TermsOfServicePage();
|
||||
render(page);
|
||||
const sections = screen.getAllByRole('heading', { level: 2 });
|
||||
expect(sections.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<TermsOfServicePage />);
|
||||
it('should have proper heading hierarchy', async () => {
|
||||
const page = await TermsOfServicePage();
|
||||
render(page);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
|
||||
|
||||
+42
-16
@@ -1,4 +1,5 @@
|
||||
import { Metadata } from 'next';
|
||||
import { getPublishedItems } from '@/lib/cms/data-server';
|
||||
import { COMPANY_INFO } from '@/lib/constants';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -6,22 +7,8 @@ export const metadata: Metadata = {
|
||||
description: `${COMPANY_INFO.name}服务条款`,
|
||||
};
|
||||
|
||||
export default function TermsOfServicePage() {
|
||||
function TermsContent() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<div className="bg-gradient-to-br from-[var(--color-brand-primary)] via-[var(--color-brand-primary)]/80 to-[var(--color-hero-dark-end)] py-20">
|
||||
<div className="container-wide">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
服务条款
|
||||
</h1>
|
||||
<p className="text-xl text-white/90 max-w-2xl">
|
||||
请仔细阅读以下服务条款,使用我们的服务即表示您同意这些条款
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container-wide py-16">
|
||||
<div className="max-w-4xl">
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">引言</h2>
|
||||
@@ -171,12 +158,51 @@ export default function TermsOfServicePage() {
|
||||
|
||||
<section className="bg-[var(--color-warning-bg)] p-6 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-2 h-2 bg-[var(--color-brand-primary)] rounded-full" />
|
||||
<div className="w-2 h-2 bg-[var(--color-brand)] rounded-full" />
|
||||
<p className="text-[var(--color-text-primary)] font-medium">最后更新日期</p>
|
||||
</div>
|
||||
<p className="text-[var(--color-text-muted)] pl-4">2026年2月26日</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function TermsOfServicePage() {
|
||||
let cmsContent: string | null = null;
|
||||
|
||||
try {
|
||||
const items = await getPublishedItems('legal-page');
|
||||
const termsItem = items.find((item) => item.data.pageType === 'terms');
|
||||
if (termsItem && typeof termsItem.data.content === 'string' && termsItem.data.content.length > 0) {
|
||||
cmsContent = termsItem.data.content as string;
|
||||
}
|
||||
} catch {
|
||||
// CMS not available, fall back to inline content
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<div className="bg-gradient-to-br from-[var(--color-brand)] via-[var(--color-brand)]/80 to-[var(--color-hero-dark-end)] py-20">
|
||||
<div className="container-wide">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
服务条款
|
||||
</h1>
|
||||
<p className="text-xl text-white/90 max-w-2xl">
|
||||
请仔细阅读以下服务条款,使用我们的服务即表示您同意这些条款
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container-wide py-16">
|
||||
<div className="max-w-4xl">
|
||||
{cmsContent ? (
|
||||
<div
|
||||
className="prose prose-lg max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: cmsContent }}
|
||||
/>
|
||||
) : (
|
||||
<TermsContent />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// 只处理 /admin 路由(除了登录页和 API 路由)
|
||||
if (pathname.startsWith('/admin') && !pathname.startsWith('/admin/login') && !pathname.startsWith('/api/')) {
|
||||
const token = request.cookies.get('novalon_token')?.value;
|
||||
const authHeader = request.headers.get('authorization');
|
||||
|
||||
// 如果 cookie 和 header 都没有 token,重定向到登录页
|
||||
if (!token && !authHeader) {
|
||||
// 允许客户端组件处理认证(因为 AuthProvider 在客户端也会检查 localStorage)
|
||||
return NextResponse.next();
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/admin/:path*'],
|
||||
};
|
||||
Reference in New Issue
Block a user