From 9cbc80742abc1e697d8d26c05f1acd7df67801f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?=
Date: Sun, 1 Mar 2026 10:56:54 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E8=81=94=E7=B3=BB?=
=?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=B9=B6=E5=A2=9E=E5=BC=BA=E5=AE=89=E5=85=A8?=
=?UTF-8?q?=E6=80=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
refactor: 优化导航和路由逻辑
fix: 修复移动端样式问题
perf: 优化字体加载和性能
test: 添加安全性和可访问性测试
style: 调整按钮和表单样式
chore: 更新依赖版本
ci: 添加安全头配置
build: 优化构建配置
docs: 更新常量信息
---
e2e/package-lock.json | 2 +-
e2e/package.json | 2 +-
e2e/playwright.config.ts | 4 +-
.../tests/accessibility/accessibility.spec.ts | 6 +-
.../accessibility/wcag-compliance.spec.ts | 280 +++++++++
.../tests/security/csrf-protection.spec.ts | 108 ++++
e2e/src/tests/security/xss-protection.spec.ts | 93 +++
next.config.ts | 50 +-
src/app/(marketing)/cases/[id]/client.tsx | 2 +-
src/app/(marketing)/cases/page.tsx | 4 +-
src/app/(marketing)/contact/page.tsx | 554 ++++++++++--------
.../news/[slug]/NewsDetailClient.tsx | 2 +-
src/app/(marketing)/page.tsx | 38 +-
src/app/(marketing)/products/[id]/page.tsx | 4 +-
src/app/(marketing)/solutions/page.tsx | 23 +-
src/app/globals.css | 66 +++
src/app/layout.tsx | 16 +-
src/components/layout/header.tsx | 176 +++---
src/components/layout/mobile-menu.tsx | 18 +-
src/components/layout/mobile-tab-bar.tsx | 6 +-
src/components/sections/hero-section.tsx | 38 +-
src/components/sections/products-section.tsx | 13 +-
src/components/ui/ripple-button.tsx | 6 +-
src/lib/constants.ts | 16 +-
24 files changed, 1087 insertions(+), 440 deletions(-)
create mode 100644 e2e/src/tests/accessibility/wcag-compliance.spec.ts
create mode 100644 e2e/src/tests/security/csrf-protection.spec.ts
create mode 100644 e2e/src/tests/security/xss-protection.spec.ts
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index 5d43343..8d626b6 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"hasInstallScript": true,
"devDependencies": {
- "@axe-core/playwright": "^4.9.0",
+ "@axe-core/playwright": "^4.11.1",
"@playwright/test": "^1.58.2",
"@types/node": "^20.11.0",
"allure-commandline": "^2.37.0",
diff --git a/e2e/package.json b/e2e/package.json
index befad62..e5ca5d7 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -22,7 +22,7 @@
"install": "playwright install --with-deps"
},
"devDependencies": {
- "@axe-core/playwright": "^4.9.0",
+ "@axe-core/playwright": "^4.11.1",
"@playwright/test": "^1.58.2",
"@types/node": "^20.11.0",
"allure-commandline": "^2.37.0",
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
index 618599c..27740fb 100644
--- a/e2e/playwright.config.ts
+++ b/e2e/playwright.config.ts
@@ -61,8 +61,8 @@ export default defineConfig({
},
],
webServer: env.name === 'development' ? {
- command: 'npm run dev',
- url: 'http://localhost:3001',
+ command: 'cd .. && npm run dev',
+ url: 'http://localhost:3000',
timeout: 120000,
reuseExistingServer: !process.env.CI,
} : undefined,
diff --git a/e2e/src/tests/accessibility/accessibility.spec.ts b/e2e/src/tests/accessibility/accessibility.spec.ts
index a07037e..99de65c 100644
--- a/e2e/src/tests/accessibility/accessibility.spec.ts
+++ b/e2e/src/tests/accessibility/accessibility.spec.ts
@@ -37,8 +37,9 @@ test.describe('可访问性测试 @accessibility', () => {
test('表单输入应该有label', async ({ page }) => {
await page.goto('http://localhost:3000/contact');
+ await page.waitForLoadState('networkidle');
- const inputs = page.locator('input, textarea, select');
+ const inputs = page.locator('input:not([type="hidden"]), textarea, select');
const count = await inputs.count();
for (let i = 0; i < count; i++) {
@@ -101,8 +102,9 @@ test.describe('可访问性测试 @accessibility', () => {
test('焦点元素应该可见', async ({ page }) => {
await page.goto('http://localhost:3000');
+ await page.waitForLoadState('networkidle');
- const focusableElements = page.locator('a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])');
+ const focusableElements = page.locator('a[href]:visible, button:visible, input:visible, textarea:visible, select:visible, [tabindex]:not([tabindex="-1"]):visible');
const count = await focusableElements.count();
for (let i = 0; i < Math.min(count, 10); i++) {
diff --git a/e2e/src/tests/accessibility/wcag-compliance.spec.ts b/e2e/src/tests/accessibility/wcag-compliance.spec.ts
new file mode 100644
index 0000000..b2b8f9c
--- /dev/null
+++ b/e2e/src/tests/accessibility/wcag-compliance.spec.ts
@@ -0,0 +1,280 @@
+import { test, expect } from '@playwright/test';
+import AxeBuilder from '@axe-core/playwright';
+
+test.describe('Accessibility Tests (WCAG 2.1 AA)', () => {
+ test('home page should not have accessibility violations', async ({ page }) => {
+ await page.goto('/');
+ await page.waitForLoadState('networkidle');
+
+ const accessibilityScanResults = await new AxeBuilder({ page })
+ .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
+ .analyze();
+
+ expect(accessibilityScanResults.violations).toEqual([]);
+ });
+
+ test('contact page should not have accessibility violations', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const accessibilityScanResults = await new AxeBuilder({ page })
+ .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
+ .analyze();
+
+ expect(accessibilityScanResults.violations).toEqual([]);
+ });
+
+ test('all form inputs should have associated labels', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const inputs = await page.locator('input:not([type="hidden"]), textarea, select').all();
+
+ for (const input of inputs) {
+ const id = await input.getAttribute('id');
+ const ariaLabel = await input.getAttribute('aria-label');
+ const ariaLabelledBy = await input.getAttribute('aria-labelledby');
+
+ if (id) {
+ const label = page.locator(`label[for="${id}"]`);
+ const hasLabel = await label.count() > 0;
+
+ expect(hasLabel || ariaLabel || ariaLabelledBy).toBeTruthy();
+ } else {
+ expect(ariaLabel || ariaLabelledBy).toBeTruthy();
+ }
+ }
+ });
+
+ test('all images should have alt text', async ({ page }) => {
+ await page.goto('/');
+
+ const images = await page.locator('img').all();
+
+ for (const img of images) {
+ const alt = await img.getAttribute('alt');
+ const role = await img.getAttribute('role');
+
+ expect(alt !== null || role === 'presentation').toBeTruthy();
+ }
+ });
+
+ test('all buttons should have accessible names', async ({ page }) => {
+ await page.goto('/');
+
+ const buttons = await page.locator('button').all();
+
+ for (const button of buttons) {
+ const text = await button.textContent();
+ const ariaLabel = await button.getAttribute('aria-label');
+ const ariaLabelledBy = await button.getAttribute('aria-labelledby');
+
+ expect(text?.trim() || ariaLabel || ariaLabelledBy).toBeTruthy();
+ }
+ });
+
+ test('all links should have discernible text', async ({ page }) => {
+ await page.goto('/');
+ await page.waitForLoadState('networkidle');
+
+ const links = await page.locator('a:visible').all();
+
+ expect(links.length).toBeGreaterThan(0);
+
+ let passedCount = 0;
+ for (const link of links) {
+ const text = await link.textContent();
+ const ariaLabel = await link.getAttribute('aria-label');
+ const title = await link.getAttribute('title');
+
+ if ((text && text.trim().length > 0) ||
+ (ariaLabel && ariaLabel.trim().length > 0) ||
+ (title && title.trim().length > 0)) {
+ passedCount++;
+ }
+ }
+
+ const passRate = links.length > 0 ? passedCount / links.length : 1;
+ expect(passRate).toBeGreaterThanOrEqual(0.95);
+ });
+
+ test('page should have proper heading hierarchy', async ({ page }) => {
+ await page.goto('/');
+
+ const h1Count = await page.locator('h1').count();
+ expect(h1Count).toBeGreaterThanOrEqual(1);
+ expect(h1Count).toBeLessThanOrEqual(2);
+
+ const headings = await page.locator('h1, h2, h3, h4, h5, h6').all();
+ let previousLevel = 0;
+
+ for (const heading of headings) {
+ const tagName = await heading.evaluate(el => el.tagName.toLowerCase());
+ const currentLevel = parseInt(tagName.replace('h', ''));
+
+ expect(currentLevel - previousLevel).toBeLessThanOrEqual(1);
+ previousLevel = currentLevel;
+ }
+ });
+
+ test('color contrast should meet WCAG AA standards', async ({ page }) => {
+ await page.goto('/');
+
+ const accessibilityScanResults = await new AxeBuilder({ page })
+ .withRules(['color-contrast'])
+ .analyze();
+
+ const contrastViolations = accessibilityScanResults.violations.filter(
+ v => v.id === 'color-contrast'
+ );
+
+ expect(contrastViolations).toEqual([]);
+ });
+
+ test('touch targets should be at least 44x44 pixels', async ({ page }) => {
+ await page.goto('/');
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.waitForLoadState('networkidle');
+
+ const buttons = await page.locator('button:visible, a:visible, input[type="button"]:visible, input[type="submit"]:visible').all();
+
+ expect(buttons.length).toBeGreaterThanOrEqual(0);
+
+ let passedCount = 0;
+ let totalCount = 0;
+
+ for (const button of buttons) {
+ try {
+ const box = await button.boundingBox();
+
+ if (box && box.width > 0 && box.height > 0) {
+ totalCount++;
+ if (box.width >= 44 && box.height >= 44) {
+ passedCount++;
+ }
+ }
+ } catch (e) {
+ continue;
+ }
+ }
+
+ if (totalCount > 0) {
+ const passRate = passedCount / totalCount;
+ expect(passRate).toBeGreaterThanOrEqual(0.7);
+ } else {
+ expect(true).toBeTruthy();
+ }
+ });
+
+ test('page should be fully navigable via keyboard', async ({ page }) => {
+ await page.goto('/');
+
+ const focusableElements = await page.locator(
+ 'a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
+ ).all();
+
+ for (let i = 0; i < Math.min(focusableElements.length, 20); i++) {
+ await page.keyboard.press('Tab');
+
+ const focusedElement = page.locator(':focus');
+ const isVisible = await focusedElement.isVisible();
+
+ expect(isVisible).toBeTruthy();
+ }
+ });
+
+ test('focus order should be logical', async ({ page }) => {
+ await page.goto('/');
+
+ const focusOrder: string[] = [];
+
+ for (let i = 0; i < 20; i++) {
+ await page.keyboard.press('Tab');
+ const focusedElement = page.locator(':focus');
+ const tagName = await focusedElement.evaluate(el => el.tagName.toLowerCase());
+ const text = await focusedElement.textContent();
+ focusOrder.push(`${tagName}${text ? `: ${text.substring(0, 20)}` : ''}`);
+ }
+
+ expect(focusOrder.length).toBeGreaterThan(0);
+ });
+
+ test('skip link should be present', async ({ page }) => {
+ await page.goto('/');
+
+ const skipLink = page.locator('a[href="#main"], a[href="#content"], a:has-text("Skip"), a:has-text("跳过")');
+ const skipLinkCount = await skipLink.count();
+
+ expect(skipLinkCount).toBeGreaterThanOrEqual(0);
+ });
+
+ test('form error messages should be associated with inputs', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ await page.fill('input[id="email"]', 'invalid-email');
+ await page.locator('input[id="email"]').blur();
+
+ await page.waitForTimeout(500);
+
+ const errorMessages = await page.locator('[role="alert"], .error, .error-message, [data-error], p[id*="error"]').all();
+
+ if (errorMessages.length > 0) {
+ expect(errorMessages.length).toBeGreaterThan(0);
+ } else {
+ const emailInput = page.locator('input[id="email"]');
+ const ariaInvalid = await emailInput.getAttribute('aria-invalid');
+ expect(['true', 'false', null]).toContain(ariaInvalid);
+ }
+ });
+
+ test('modals should trap focus', async ({ page }) => {
+ await page.goto('/');
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.waitForLoadState('networkidle');
+
+ const mobileMenuButton = page.locator('button[aria-label*="菜单"], button[aria-label*="menu"], button[aria-label*="打开菜单"]');
+ const buttonCount = await mobileMenuButton.count();
+
+ if (buttonCount > 0) {
+ const button = mobileMenuButton.first();
+ const isVisible = await button.isVisible();
+
+ if (isVisible) {
+ await button.click();
+ await page.waitForTimeout(500);
+
+ for (let i = 0; i < 10; i++) {
+ await page.keyboard.press('Tab');
+ }
+
+ const focusedElement = page.locator(':focus');
+ const isInModal = await focusedElement.evaluate(el => {
+ let parent = el.parentElement;
+ while (parent) {
+ if (parent.getAttribute('role') === 'dialog' ||
+ parent.getAttribute('aria-modal') === 'true') {
+ return true;
+ }
+ parent = parent.parentElement;
+ }
+ return false;
+ });
+
+ expect(isInModal || await focusedElement.isVisible()).toBeTruthy();
+ }
+ }
+ });
+
+ test('pages should have descriptive titles', async ({ page }) => {
+ await page.goto('/');
+ const homeTitle = await page.title();
+ expect(homeTitle.length).toBeGreaterThan(0);
+ expect(homeTitle).not.toBe('Untitled');
+
+ await page.goto('/contact');
+ const contactTitle = await page.title();
+ expect(contactTitle.length).toBeGreaterThan(0);
+ expect(contactTitle).not.toBe('Untitled');
+ });
+});
diff --git a/e2e/src/tests/security/csrf-protection.spec.ts b/e2e/src/tests/security/csrf-protection.spec.ts
new file mode 100644
index 0000000..f90d93d
--- /dev/null
+++ b/e2e/src/tests/security/csrf-protection.spec.ts
@@ -0,0 +1,108 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('CSRF Protection Security Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+ });
+
+ test('should have CSRF token in contact form', async ({ page }) => {
+ const csrfToken = await page.locator('input[name="_csrf"]').inputValue();
+
+ expect(csrfToken).toBeTruthy();
+ expect(csrfToken.length).toBeGreaterThan(0);
+ });
+
+ test('should have unique CSRF token for each session', async ({ browser }) => {
+ const context1 = await browser.newContext();
+ const page1 = await context1.newPage();
+ await page1.goto('/contact');
+ await page1.waitForLoadState('networkidle');
+ const token1 = await page1.locator('input[name="_csrf"]').inputValue();
+
+ const context2 = await browser.newContext();
+ const page2 = await context2.newPage();
+ await page2.goto('/contact');
+ await page2.waitForLoadState('networkidle');
+ const token2 = await page2.locator('input[name="_csrf"]').inputValue();
+
+ expect(token1).not.toBe(token2);
+
+ await context1.close();
+ await context2.close();
+ });
+
+ test('should reject form submission without CSRF token', async ({ page }) => {
+ await page.evaluate(() => {
+ const csrfInput = document.querySelector('input[name="_csrf"]') as HTMLInputElement;
+ if (csrfInput) {
+ csrfInput.remove();
+ }
+ });
+
+ await page.fill('input[id="name"]', 'Test Name');
+ await page.fill('input[id="email"]', 'test@example.com');
+ await page.fill('input[id="phone"]', '13800138000');
+ await page.fill('input[id="subject"]', 'Test Subject');
+ await page.fill('textarea[id="message"]', 'Test message');
+
+ await page.click('button[type="submit"]');
+
+ await page.waitForTimeout(1000);
+
+ const successMessage = page.locator('text=/提交成功|发送成功/i');
+ await expect(successMessage).not.toBeVisible({ timeout: 2000 });
+ });
+
+ test('should reject form submission with invalid CSRF token', async ({ page }) => {
+ await page.evaluate(() => {
+ const csrfInput = document.querySelector('input[name="_csrf"]') as HTMLInputElement;
+ if (csrfInput) {
+ csrfInput.value = 'invalid-token-12345';
+ }
+ });
+
+ await page.fill('input[id="name"]', 'Test Name');
+ await page.fill('input[id="email"]', 'test@example.com');
+ await page.fill('input[id="phone"]', '13800138000');
+ await page.fill('input[id="subject"]', 'Test Subject');
+ await page.fill('textarea[id="message"]', 'Test message');
+
+ await page.click('button[type="submit"]');
+
+ await page.waitForTimeout(1000);
+
+ const successMessage = page.locator('text=/提交成功|发送成功/i');
+ await expect(successMessage).not.toBeVisible({ timeout: 2000 });
+ });
+
+ test('should regenerate CSRF token after form submission', async ({ page }) => {
+ const initialToken = await page.locator('input[name="_csrf"]').inputValue();
+
+ await page.fill('input[id="name"]', 'Test Name');
+ await page.fill('input[id="email"]', 'test@example.com');
+ await page.fill('input[id="phone"]', '13800138000');
+ await page.fill('input[id="subject"]', 'Test Subject');
+ await page.fill('textarea[id="message"]', 'Test message');
+
+ await page.click('button[type="submit"]');
+
+ await page.waitForTimeout(2000);
+
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+ const newToken = await page.locator('input[name="_csrf"]').inputValue();
+
+ expect(newToken).not.toBe(initialToken);
+ });
+
+ test('should store CSRF token in sessionStorage', async ({ page }) => {
+ await page.waitForLoadState('networkidle');
+
+ const sessionStorage = await page.evaluate(() => {
+ return window.sessionStorage.getItem('csrf_token');
+ });
+
+ expect(sessionStorage).toBeTruthy();
+ });
+});
diff --git a/e2e/src/tests/security/xss-protection.spec.ts b/e2e/src/tests/security/xss-protection.spec.ts
new file mode 100644
index 0000000..3aab3a5
--- /dev/null
+++ b/e2e/src/tests/security/xss-protection.spec.ts
@@ -0,0 +1,93 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('XSS Protection Security Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+ });
+
+ test('should sanitize script tags in contact form', async ({ page }) => {
+ const xssPayload = '';
+
+ await page.fill('input[id="name"]', xssPayload);
+ await page.fill('input[id="email"]', 'test@example.com');
+ await page.fill('input[id="phone"]', '13800138000');
+ await page.fill('input[id="subject"]', 'Test Subject');
+ await page.fill('textarea[id="message"]', 'Test message');
+
+ const nameInput = page.locator('input[id="name"]');
+ const inputValue = await nameInput.inputValue();
+ expect(inputValue).not.toContain('');
+
+ const pageContent = await page.content();
+ expect(pageContent).not.toContain('');
+ });
+
+ test('should sanitize event handlers in contact form', async ({ page }) => {
+ const xssPayload = '
';
+
+ await page.fill('input[id="name"]', xssPayload);
+ await page.fill('input[id="email"]', 'test@example.com');
+ await page.fill('input[id="phone"]', '13800138000');
+ await page.fill('input[id="subject"]', 'Test Subject');
+ await page.fill('textarea[id="message"]', 'Test message');
+
+ const nameInput = page.locator('input[id="name"]');
+ const inputValue = await nameInput.inputValue();
+ expect(inputValue).not.toContain('onerror');
+ expect(inputValue).not.toContain('alert');
+ });
+
+ test('should sanitize javascript protocol in contact form', async ({ page }) => {
+ const xssPayload = 'javascript:alert("XSS")';
+
+ await page.fill('input[id="name"]', 'Test Name');
+ await page.fill('input[id="email"]', 'test@example.com');
+ await page.fill('input[id="phone"]', '13800138000');
+ await page.fill('input[id="subject"]', xssPayload);
+ await page.fill('textarea[id="message"]', 'Test message');
+
+ const subjectInput = page.locator('input[id="subject"]');
+ const inputValue = await subjectInput.inputValue();
+ expect(inputValue.trim().length).toBeGreaterThan(0);
+ });
+
+ test('should sanitize HTML entities in contact form', async ({ page }) => {
+ const htmlPayload = 'Click me
';
+
+ await page.fill('input[id="name"]', htmlPayload);
+ await page.fill('input[id="email"]', 'test@example.com');
+ await page.fill('input[id="phone"]', '13800138000');
+ await page.fill('input[id="subject"]', 'Test Subject');
+ await page.fill('textarea[id="message"]', 'Test message');
+
+ const nameInput = page.locator('input[id="name"]');
+ const inputValue = await nameInput.inputValue();
+ expect(inputValue).not.toContain('onclick');
+ expect(inputValue).not.toContain('alert(1)');
+ });
+
+ test('should handle special characters safely', async ({ page }) => {
+ const specialChars = '<>&"\'';
+
+ await page.fill('input[id="name"]', specialChars);
+ await page.fill('input[id="email"]', 'test@example.com');
+ await page.fill('input[id="phone"]', '13800138000');
+ await page.fill('input[id="subject"]', 'Test Subject');
+ await page.fill('textarea[id="message"]', 'Test message');
+
+ const nameInput = page.locator('input[id="name"]');
+ const inputValue = await nameInput.inputValue();
+ expect(inputValue.trim().length).toBeGreaterThan(0);
+ });
+
+ test('should not execute XSS via URL parameters', async ({ page }) => {
+ const xssUrl = '/contact?name=';
+ await page.goto(xssUrl);
+ await page.waitForLoadState('networkidle');
+
+ const pageContent = await page.content();
+ expect(pageContent).not.toContain('');
+ });
+});
diff --git a/next.config.ts b/next.config.ts
index a226af6..3d37773 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -24,8 +24,9 @@ const nextConfig: NextConfig = {
compress: true,
poweredByHeader: false,
reactStrictMode: true,
+ reactProductionProfiling: !isDev,
experimental: {
- optimizePackageImports: ['lucide-react', 'framer-motion'],
+ optimizePackageImports: ['lucide-react', 'framer-motion', '@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
optimizeCss: true,
},
compiler: {
@@ -33,6 +34,39 @@ const nextConfig: NextConfig = {
},
headers: async () => {
return [
+ {
+ source: '/:path*',
+ headers: [
+ {
+ key: 'X-DNS-Prefetch-Control',
+ value: 'on'
+ },
+ {
+ key: 'Strict-Transport-Security',
+ value: 'max-age=63072000; includeSubDomains; preload'
+ },
+ {
+ key: 'X-XSS-Protection',
+ value: '1; mode=block'
+ },
+ {
+ key: 'X-Frame-Options',
+ value: 'SAMEORIGIN'
+ },
+ {
+ key: 'X-Content-Type-Options',
+ value: 'nosniff'
+ },
+ {
+ key: 'Referrer-Policy',
+ value: 'origin-when-cross-origin'
+ },
+ {
+ key: 'Permissions-Policy',
+ value: 'camera=(), microphone=(), geolocation=()'
+ },
+ ],
+ },
{
source: '/:all*(svg|jpg|jpeg|png|gif|webp|avif)',
locale: false,
@@ -53,6 +87,20 @@ const nextConfig: NextConfig = {
},
],
},
+ {
+ source: '/fonts/:all*',
+ locale: false,
+ headers: [
+ {
+ key: 'Cache-Control',
+ value: 'public, max-age=31536000, immutable',
+ },
+ {
+ key: 'Access-Control-Allow-Origin',
+ value: '*',
+ },
+ ],
+ },
];
},
};
diff --git a/src/app/(marketing)/cases/[id]/client.tsx b/src/app/(marketing)/cases/[id]/client.tsx
index 701c832..1ddc357 100644
--- a/src/app/(marketing)/cases/[id]/client.tsx
+++ b/src/app/(marketing)/cases/[id]/client.tsx
@@ -270,7 +270,7 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
className="w-full bg-white text-[#C41E3A] hover:bg-white/90"
asChild
>
-
+
联系我们
diff --git a/src/app/(marketing)/cases/page.tsx b/src/app/(marketing)/cases/page.tsx
index edb688f..10db760 100644
--- a/src/app/(marketing)/cases/page.tsx
+++ b/src/app/(marketing)/cases/page.tsx
@@ -101,7 +101,7 @@ export default function CasesPage() {
让我们与您同行,共创美好未来
-
+