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="container-wide px-4 py-20">
|
||||||
<div className="max-w-2xl mx-auto text-center">
|
<div className="max-w-2xl mx-auto text-center">
|
||||||
<div className="mb-8">
|
<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">
|
<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-primary)]" />
|
<AlertTriangle className="w-12 h-12 text-[var(--color-brand)]" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-3xl font-bold text-[var(--color-text-primary)] mb-4">
|
<h1 className="text-3xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||||
@@ -53,7 +53,7 @@ export default function Error({
|
|||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={reset}
|
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" />
|
<RefreshCw className="w-5 h-5 mr-2" />
|
||||||
重试
|
重试
|
||||||
@@ -81,8 +81,8 @@ export default function Error({
|
|||||||
href="/contact"
|
href="/contact"
|
||||||
className="flex items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
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">
|
<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-primary)]" />
|
<AlertTriangle className="w-5 h-5 text-[var(--color-brand)]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="font-semibold text-[var(--color-text-primary)]">联系我们</div>
|
<div className="font-semibold text-[var(--color-text-primary)]">联系我们</div>
|
||||||
@@ -94,8 +94,8 @@ export default function Error({
|
|||||||
href="/services"
|
href="/services"
|
||||||
className="flex items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
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">
|
<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-primary)]" />
|
<RefreshCw className="w-5 h-5 text-[var(--color-brand)]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="font-semibold text-[var(--color-text-primary)]">服务</div>
|
<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)]">
|
<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>
|
</StaticLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
+803
-514
File diff suppressed because it is too large
Load Diff
+39
-52
@@ -1,9 +1,6 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
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 "./globals.css";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { ThemeProvider } from "@/contexts/theme-context";
|
|
||||||
import { GoogleAnalyticsWrapper } from "@/components/analytics/GoogleAnalyticsWrapper";
|
import { GoogleAnalyticsWrapper } from "@/components/analytics/GoogleAnalyticsWrapper";
|
||||||
import { GlobalErrorTracker } from "@/components/analytics/GlobalErrorTracker";
|
import { GlobalErrorTracker } from "@/components/analytics/GlobalErrorTracker";
|
||||||
import { CookieConsent } from "@/components/analytics/CookieConsent";
|
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 { ScrollProgress } from "@/components/ui/scroll-progress";
|
||||||
import { BackToTop } from "@/components/ui/back-to-top";
|
import { BackToTop } from "@/components/ui/back-to-top";
|
||||||
import { ClientLayout } from "@/components/layout/client-layout";
|
import { ClientLayout } from "@/components/layout/client-layout";
|
||||||
|
import { getPublishedItems } from "@/lib/cms/data-server";
|
||||||
const geistSans = localFont({
|
import { SiteConfigProvider, type SiteConfig, type NavigationItem, type MegaDropdownGroup } from "@/lib/site-config";
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL("https://www.novalon.cn"),
|
metadataBase: new URL("https://www.novalon.cn"),
|
||||||
@@ -109,25 +72,42 @@ export const viewport: Viewport = {
|
|||||||
width: "device-width",
|
width: "device-width",
|
||||||
initialScale: 1,
|
initialScale: 1,
|
||||||
maximumScale: 5,
|
maximumScale: 5,
|
||||||
themeColor: [
|
themeColor: "#FFFFFF",
|
||||||
{ media: "(prefers-color-scheme: light)", color: "#FFFFFF" },
|
|
||||||
{ media: "(prefers-color-scheme: dark)", color: "#0A0A0A" },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
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 (
|
return (
|
||||||
<html lang="zh-CN" className="scroll-smooth" suppressHydrationWarning>
|
<html lang="zh-CN" className="scroll-smooth" suppressHydrationWarning>
|
||||||
<head>
|
<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/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
@@ -142,21 +122,28 @@ export default function RootLayout({
|
|||||||
<LocalBusinessSchema />
|
<LocalBusinessSchema />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<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 />
|
<ScrollProgress />
|
||||||
<GoogleAnalyticsWrapper />
|
<GoogleAnalyticsWrapper />
|
||||||
<GlobalErrorTracker />
|
<GlobalErrorTracker />
|
||||||
<PerformanceTracker />
|
<PerformanceTracker />
|
||||||
<OutboundLinkTracker />
|
<OutboundLinkTracker />
|
||||||
<ScrollDepthTracker />
|
<ScrollDepthTracker />
|
||||||
<ThemeProvider>
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
<SiteConfigProvider config={siteConfig}>
|
||||||
<ClientLayout>
|
<ClientLayout>
|
||||||
{children}
|
{children}
|
||||||
</ClientLayout>
|
</ClientLayout>
|
||||||
|
</SiteConfigProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</ThemeProvider>
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<MobileTabBar />
|
<MobileTabBar />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
+14
-9
@@ -1,16 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { StaticLink } from '@/components/ui/static-link';
|
import { StaticLink } from '@/components/ui/static-link';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Home, ArrowLeft, Search } from 'lucide-react';
|
import { Home, ArrowLeft, Search } from 'lucide-react';
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = '页面未找到 - 睿新致远';
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--color-bg-primary)] flex items-center justify-center">
|
<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="container-wide px-4 py-20">
|
||||||
<div className="max-w-xl mx-auto text-center">
|
<div className="max-w-xl mx-auto text-center">
|
||||||
<div className="mb-8">
|
<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
|
404
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,8 +59,8 @@ export default function NotFound() {
|
|||||||
href="/products"
|
href="/products"
|
||||||
className="flex flex-col items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
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">
|
<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-primary)]" />
|
<Search className="w-4 h-4 text-[var(--color-brand)]" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">产品</span>
|
<span className="font-medium text-sm text-[var(--color-text-primary)]">产品</span>
|
||||||
</StaticLink>
|
</StaticLink>
|
||||||
@@ -64,8 +69,8 @@ export default function NotFound() {
|
|||||||
href="/solutions"
|
href="/solutions"
|
||||||
className="flex flex-col items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
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">
|
<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-primary)]" />
|
<Search className="w-4 h-4 text-[var(--color-brand)]" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">解决方案</span>
|
<span className="font-medium text-sm text-[var(--color-text-primary)]">解决方案</span>
|
||||||
</StaticLink>
|
</StaticLink>
|
||||||
@@ -74,8 +79,8 @@ export default function NotFound() {
|
|||||||
href="/about"
|
href="/about"
|
||||||
className="flex flex-col items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
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">
|
<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-primary)]" />
|
<Search className="w-4 h-4 text-[var(--color-brand)]" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">关于我们</span>
|
<span className="font-medium text-sm text-[var(--color-text-primary)]">关于我们</span>
|
||||||
</StaticLink>
|
</StaticLink>
|
||||||
@@ -84,8 +89,8 @@ export default function NotFound() {
|
|||||||
href="/contact"
|
href="/contact"
|
||||||
className="flex flex-col items-center p-4 bg-[var(--color-bg-primary)] rounded-lg hover:shadow-md transition-shadow group"
|
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">
|
<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-primary)]" />
|
<Search className="w-4 h-4 text-[var(--color-brand)]" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">联系我们</span>
|
<span className="font-medium text-sm text-[var(--color-text-primary)]">联系我们</span>
|
||||||
</StaticLink>
|
</StaticLink>
|
||||||
|
|||||||
@@ -8,35 +8,40 @@ describe('PrivacyPolicyPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('should render privacy policy page', () => {
|
it('should render privacy policy page', async () => {
|
||||||
render(<PrivacyPolicyPage />);
|
const page = await PrivacyPolicyPage();
|
||||||
|
render(page);
|
||||||
const container = screen.getByText('隐私政策').closest('div');
|
const container = screen.getByText('隐私政策').closest('div');
|
||||||
expect(container).toBeInTheDocument();
|
expect(container).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render page title', () => {
|
it('should render page title', async () => {
|
||||||
render(<PrivacyPolicyPage />);
|
const page = await PrivacyPolicyPage();
|
||||||
|
render(page);
|
||||||
const title = screen.getByRole('heading', { level: 1 });
|
const title = screen.getByRole('heading', { level: 1 });
|
||||||
expect(title).toBeInTheDocument();
|
expect(title).toBeInTheDocument();
|
||||||
expect(title).toHaveTextContent('隐私政策');
|
expect(title).toHaveTextContent('隐私政策');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render introduction section', () => {
|
it('should render introduction section', async () => {
|
||||||
render(<PrivacyPolicyPage />);
|
const page = await PrivacyPolicyPage();
|
||||||
|
render(page);
|
||||||
const intro = screen.getByText('引言');
|
const intro = screen.getByText('引言');
|
||||||
expect(intro).toBeInTheDocument();
|
expect(intro).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render information collection section', () => {
|
it('should render information collection section', async () => {
|
||||||
render(<PrivacyPolicyPage />);
|
const page = await PrivacyPolicyPage();
|
||||||
|
render(page);
|
||||||
const section = screen.getByText(/我们如何收集和使用您的个人信息/i);
|
const section = screen.getByText(/我们如何收集和使用您的个人信息/i);
|
||||||
expect(section).toBeInTheDocument();
|
expect(section).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('should have proper heading hierarchy', () => {
|
it('should have proper heading hierarchy', async () => {
|
||||||
render(<PrivacyPolicyPage />);
|
const page = await PrivacyPolicyPage();
|
||||||
|
render(page);
|
||||||
const h1 = screen.getByRole('heading', { level: 1 });
|
const h1 = screen.getByRole('heading', { level: 1 });
|
||||||
expect(h1).toBeInTheDocument();
|
expect(h1).toBeInTheDocument();
|
||||||
|
|
||||||
|
|||||||
+41
-15
@@ -1,4 +1,5 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
import { getPublishedItems } from '@/lib/cms/data-server';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -6,22 +7,8 @@ export const metadata: Metadata = {
|
|||||||
description: `${COMPANY_INFO.name}隐私政策`,
|
description: `${COMPANY_INFO.name}隐私政策`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PrivacyPolicyPage() {
|
function PrivacyContent() {
|
||||||
return (
|
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">
|
<div className="prose prose-lg max-w-none">
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">引言</h2>
|
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">引言</h2>
|
||||||
@@ -232,6 +219,45 @@ export default function PrivacyPolicyPage() {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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>
|
</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', () => {
|
describe('Rendering', () => {
|
||||||
it('should render terms of service page', () => {
|
it('should render terms of service page', async () => {
|
||||||
render(<TermsOfServicePage />);
|
const page = await TermsOfServicePage();
|
||||||
|
render(page);
|
||||||
const container = screen.getByText('服务条款').closest('div');
|
const container = screen.getByText('服务条款').closest('div');
|
||||||
expect(container).toBeInTheDocument();
|
expect(container).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render page title', () => {
|
it('should render page title', async () => {
|
||||||
render(<TermsOfServicePage />);
|
const page = await TermsOfServicePage();
|
||||||
|
render(page);
|
||||||
const title = screen.getByRole('heading', { level: 1 });
|
const title = screen.getByRole('heading', { level: 1 });
|
||||||
expect(title).toBeInTheDocument();
|
expect(title).toBeInTheDocument();
|
||||||
expect(title).toHaveTextContent('服务条款');
|
expect(title).toHaveTextContent('服务条款');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render introduction section', () => {
|
it('should render introduction section', async () => {
|
||||||
render(<TermsOfServicePage />);
|
const page = await TermsOfServicePage();
|
||||||
|
render(page);
|
||||||
const intro = screen.getByText('引言');
|
const intro = screen.getByText('引言');
|
||||||
expect(intro).toBeInTheDocument();
|
expect(intro).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render service content section', () => {
|
it('should render service content section', async () => {
|
||||||
render(<TermsOfServicePage />);
|
const page = await TermsOfServicePage();
|
||||||
|
render(page);
|
||||||
const sections = screen.getAllByRole('heading', { level: 2 });
|
const sections = screen.getAllByRole('heading', { level: 2 });
|
||||||
expect(sections.length).toBeGreaterThan(0);
|
expect(sections.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('should have proper heading hierarchy', () => {
|
it('should have proper heading hierarchy', async () => {
|
||||||
render(<TermsOfServicePage />);
|
const page = await TermsOfServicePage();
|
||||||
|
render(page);
|
||||||
const h1 = screen.getByRole('heading', { level: 1 });
|
const h1 = screen.getByRole('heading', { level: 1 });
|
||||||
expect(h1).toBeInTheDocument();
|
expect(h1).toBeInTheDocument();
|
||||||
|
|
||||||
|
|||||||
+42
-16
@@ -1,4 +1,5 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
import { getPublishedItems } from '@/lib/cms/data-server';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -6,22 +7,8 @@ export const metadata: Metadata = {
|
|||||||
description: `${COMPANY_INFO.name}服务条款`,
|
description: `${COMPANY_INFO.name}服务条款`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TermsOfServicePage() {
|
function TermsContent() {
|
||||||
return (
|
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">
|
<div className="prose prose-lg max-w-none">
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">引言</h2>
|
<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">
|
<section className="bg-[var(--color-warning-bg)] p-6 rounded-lg">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<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>
|
<p className="text-[var(--color-text-primary)] font-medium">最后更新日期</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[var(--color-text-muted)] pl-4">2026年2月26日</p>
|
<p className="text-[var(--color-text-muted)] pl-4">2026年2月26日</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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>
|
</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