0cfefaa937
- 移除多个页面的面包屑导航组件 - 添加统一的返回按钮组件替代各页面独立实现 - 优化导航栏滚动检测逻辑和动画效果 - 更新常量类型定义和统计数据 - 调整动态导入的SSR配置为false - 添加FlipClock组件展示公司运营时长 - 优化新闻列表页的类型安全和响应式设计
1421 lines
35 KiB
Markdown
1421 lines
35 KiB
Markdown
# 网站全面优化实施计划
|
||
|
||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||
|
||
**Goal:** 全面优化睿新致远网站,提升导航一致性、测试覆盖率、错误处理、性能、移动端体验和SEO。
|
||
|
||
**Architecture:** 采用渐进式优化策略,按优先级分阶段实施,每个阶段独立可验证和回滚。
|
||
|
||
**Tech Stack:** Next.js 16.1.6, TypeScript, Framer Motion, Playwright, Tailwind CSS
|
||
|
||
---
|
||
|
||
## 阶段一:高优先级修复(1-2天)
|
||
|
||
### Task 1.1: 修复导航一致性问题
|
||
|
||
**Files:**
|
||
- Create: `src/components/layout/breadcrumb.tsx`
|
||
- Modify: `src/components/layout/header.tsx:1-150`
|
||
- Modify: `src/app/(marketing)/about/page.tsx:1-15`
|
||
- Modify: `src/app/(marketing)/cases/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/services/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/products/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/solutions/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/contact/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/news/page.tsx:1-20`
|
||
|
||
**Step 1: 创建面包屑导航组件**
|
||
|
||
```typescript
|
||
// src/components/layout/breadcrumb.tsx
|
||
'use client';
|
||
|
||
import Link from 'next/link';
|
||
import { ChevronRight, Home } from 'lucide-react';
|
||
|
||
interface BreadcrumbItem {
|
||
label: string;
|
||
href: string;
|
||
}
|
||
|
||
interface BreadcrumbProps {
|
||
items: BreadcrumbItem[];
|
||
}
|
||
|
||
export function Breadcrumb({ items }: BreadcrumbProps) {
|
||
return (
|
||
<nav className="flex items-center space-x-2 text-sm text-[#5C5C5C] py-4">
|
||
<Link href="/" className="flex items-center hover:text-[#C41E3A] transition-colors">
|
||
<Home className="w-4 h-4" />
|
||
</Link>
|
||
{items.map((item, index) => (
|
||
<div key={index} className="flex items-center">
|
||
<ChevronRight className="w-4 h-4 text-[#E5E5E5]" />
|
||
<Link
|
||
href={item.href}
|
||
className="ml-2 hover:text-[#C41E3A] transition-colors"
|
||
>
|
||
{item.label}
|
||
</Link>
|
||
</div>
|
||
))}
|
||
</nav>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 2: 运行测试验证组件创建**
|
||
|
||
Run: `npm run build`
|
||
|
||
Expected: BUILD SUCCESS
|
||
|
||
**Step 3: 在各个页面集成面包屑导航**
|
||
|
||
```typescript
|
||
// 示例:src/app/(marketing)/about/page.tsx
|
||
import { Breadcrumb } from '@/components/layout/breadcrumb';
|
||
|
||
export default function AboutPage() {
|
||
return (
|
||
<div className="min-h-screen bg-white">
|
||
<Breadcrumb items={[{ label: '关于我们', href: '/about' }]} />
|
||
{/* 其他内容 */}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 4: 更新 Header 组件以支持统一导航**
|
||
|
||
```typescript
|
||
// src/components/layout/header.tsx
|
||
// 修改导航逻辑,使其在首页使用锚点,在详情页使用路由
|
||
const getNavigationHref = (item: typeof NAVIGATION[number], isHomePage: boolean) => {
|
||
return isHomePage ? item.href : `/${item.id}`;
|
||
};
|
||
```
|
||
|
||
**Step 5: 运行测试验证导航一致性**
|
||
|
||
Run: `npm run lint`
|
||
|
||
Expected: No linting errors
|
||
|
||
**Step 6: 提交代码**
|
||
|
||
```bash
|
||
git add src/components/layout/breadcrumb.tsx src/components/layout/header.tsx src/app/
|
||
git commit -m "feat: implement consistent navigation with breadcrumbs"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.2: 补充关键页面的E2E测试
|
||
|
||
**Files:**
|
||
- Create: `e2e/src/pages/AboutPage.ts`
|
||
- Create: `e2e/src/pages/CasesPage.ts`
|
||
- Create: `e2e/src/pages/ServicesPage.ts`
|
||
- Create: `e2e/src/pages/ProductsPage.ts`
|
||
- Create: `e2e/src/pages/SolutionsPage.ts`
|
||
- Create: `e2e/src/pages/NewsPage.ts`
|
||
- Create: `e2e/src/tests/smoke/about-page.smoke.spec.ts`
|
||
- Create: `e2e/src/tests/smoke/cases-page.smoke.spec.ts`
|
||
- Create: `e2e/src/tests/smoke/services-page.smoke.spec.ts`
|
||
- Create: `e2e/src/tests/smoke/products-page.smoke.spec.ts`
|
||
- Create: `e2e/src/tests/smoke/solutions-page.smoke.spec.ts`
|
||
- Create: `e2e/src/tests/smoke/news-page.smoke.spec.ts`
|
||
|
||
**Step 1: 创建 AboutPage Page Object**
|
||
|
||
```typescript
|
||
// e2e/src/pages/AboutPage.ts
|
||
import { Page, Locator, expect } from '@playwright/test';
|
||
|
||
export class AboutPage {
|
||
readonly page: Page;
|
||
readonly breadcrumb: Locator;
|
||
readonly pageHeader: Locator;
|
||
|
||
constructor(page: Page) {
|
||
this.page = page;
|
||
this.breadcrumb = page.locator('nav');
|
||
this.pageHeader = page.locator('h1');
|
||
}
|
||
|
||
async goto() {
|
||
await this.page.goto('/about');
|
||
await this.page.waitForLoadState('networkidle');
|
||
}
|
||
|
||
async waitForPageLoad() {
|
||
await this.page.waitForLoadState('networkidle');
|
||
await expect(this.pageHeader).toBeVisible();
|
||
}
|
||
|
||
async getBreadcrumbItems() {
|
||
return await this.breadcrumb.allTextContents();
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 创建关于页面冒烟测试**
|
||
|
||
```typescript
|
||
// e2e/src/tests/smoke/about-page.smoke.spec.ts
|
||
import { test, expect } from '../../fixtures/base.fixture';
|
||
|
||
test.describe('关于页面冒烟测试 @smoke', () => {
|
||
test.beforeEach(async ({ aboutPage }) => {
|
||
await aboutPage.goto();
|
||
await aboutPage.waitForPageLoad();
|
||
});
|
||
|
||
test('应该成功加载关于页面', async ({ aboutPage }) => {
|
||
await expect(aboutPage.page).toHaveURL(/\/about/);
|
||
await expect(aboutPage.pageHeader).toBeVisible();
|
||
});
|
||
|
||
test('应该显示面包屑导航', async ({ aboutPage }) => {
|
||
const items = await aboutPage.getBreadcrumbItems();
|
||
expect(items.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
```
|
||
|
||
**Step 3: 运行测试验证失败**
|
||
|
||
Run: `cd e2e && npm test --grep "关于页面冒烟测试"`
|
||
|
||
Expected: FAIL with "aboutPage is not defined"
|
||
|
||
**Step 4: 更新 fixtures 以包含新页面**
|
||
|
||
```typescript
|
||
// e2e/src/fixtures/base.fixture.ts
|
||
import { test as base } from '@playwright/test';
|
||
import { AboutPage } from '../pages/AboutPage';
|
||
import { CasesPage } from '../pages/CasesPage';
|
||
import { ServicesPage } from '../pages/ServicesPage';
|
||
import { ProductsPage } from '../pages/ProductsPage';
|
||
import { SolutionsPage } from '../pages/SolutionsPage';
|
||
import { NewsPage } from '../pages/NewsPage';
|
||
|
||
export const test = base.extend<{
|
||
aboutPage: AboutPage;
|
||
casesPage: CasesPage;
|
||
servicesPage: ServicesPage;
|
||
productsPage: ProductsPage;
|
||
solutionsPage: SolutionsPage;
|
||
newsPage: NewsPage;
|
||
}>({
|
||
aboutPage: async ({ page }, use) => {
|
||
const aboutPage = new AboutPage(page);
|
||
await use(aboutPage);
|
||
},
|
||
casesPage: async ({ page }, use) => {
|
||
const casesPage = new CasesPage(page);
|
||
await use(casesPage);
|
||
},
|
||
servicesPage: async ({ page }, use) => {
|
||
const servicesPage = new ServicesPage(page);
|
||
await use(servicesPage);
|
||
},
|
||
productsPage: async ({ page }, use) => {
|
||
const productsPage = new ProductsPage(page);
|
||
await use(productsPage);
|
||
},
|
||
solutionsPage: async ({ page }, use) => {
|
||
const solutionsPage = new SolutionsPage(page);
|
||
await use(solutionsPage);
|
||
},
|
||
newsPage: async ({ page }, use) => {
|
||
const newsPage = new NewsPage(page);
|
||
await use(newsPage);
|
||
},
|
||
});
|
||
```
|
||
|
||
**Step 5: 运行测试验证通过**
|
||
|
||
Run: `cd e2e && npm test --grep "关于页面冒烟测试"`
|
||
|
||
Expected: PASS
|
||
|
||
**Step 6: 为其他页面创建类似的测试**
|
||
|
||
重复步骤 1-5,为 CasesPage、ServicesPage、ProductsPage、SolutionsPage、NewsPage 创建 Page Objects 和测试。
|
||
|
||
**Step 7: 提交代码**
|
||
|
||
```bash
|
||
git add e2e/src/pages/ e2e/src/tests/smoke/ e2e/src/fixtures/base.fixture.ts
|
||
git commit -m "test: add smoke tests for all major pages"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.3: 完善错误处理
|
||
|
||
**Files:**
|
||
- Create: `src/app/not-found.tsx`
|
||
- Create: `src/app/error.tsx`
|
||
- Create: `src/components/ui/error-boundary.tsx`
|
||
- Modify: `src/app/layout.tsx:1-50`
|
||
|
||
**Step 1: 创建 404 页面**
|
||
|
||
```typescript
|
||
// src/app/not-found.tsx
|
||
import Link from 'next/link';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Home, ArrowRight } from 'lucide-react';
|
||
|
||
export default function NotFound() {
|
||
return (
|
||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||
<div className="text-center">
|
||
<h1 className="text-6xl font-bold text-[#C41E3A] mb-4">404</h1>
|
||
<h2 className="text-2xl font-semibold text-[#1C1C1C] mb-4">
|
||
页面未找到
|
||
</h2>
|
||
<p className="text-[#5C5C5C] mb-8">
|
||
抱歉,您访问的页面不存在。
|
||
</p>
|
||
<div className="flex justify-center gap-4">
|
||
<Button variant="outline" asChild>
|
||
<Link href="/">
|
||
<Home className="w-4 h-4 mr-2" />
|
||
返回首页
|
||
</Link>
|
||
</Button>
|
||
<Button asChild>
|
||
<Link href="/contact">
|
||
联系我们
|
||
<ArrowRight className="w-4 h-4 ml-2" />
|
||
</Link>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 2: 创建全局错误页面**
|
||
|
||
```typescript
|
||
// src/app/error.tsx
|
||
'use client';
|
||
|
||
import { useEffect } from 'react';
|
||
import { Button } from '@/components/ui/button';
|
||
import Link from 'next/link';
|
||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||
|
||
export default function Error({
|
||
error,
|
||
reset,
|
||
}: {
|
||
error: Error & { digest?: string };
|
||
reset: () => void;
|
||
}) {
|
||
useEffect(() => {
|
||
console.error(error);
|
||
}, [error]);
|
||
|
||
return (
|
||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||
<div className="text-center max-w-md">
|
||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||
<AlertCircle className="w-8 h-8 text-[#C41E3A]" />
|
||
</div>
|
||
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-4">
|
||
出错了
|
||
</h2>
|
||
<p className="text-[#5C5C5C] mb-8">
|
||
抱歉,页面加载时发生了错误。请刷新页面或联系我们的技术支持。
|
||
</p>
|
||
<div className="flex justify-center gap-4">
|
||
<Button variant="outline" onClick={reset}>
|
||
<RefreshCw className="w-4 h-4 mr-2" />
|
||
重试
|
||
</Button>
|
||
<Button asChild>
|
||
<Link href="/">
|
||
返回首页
|
||
</Link>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 3: 创建错误边界组件**
|
||
|
||
```typescript
|
||
// src/components/ui/error-boundary.tsx
|
||
'use client';
|
||
|
||
import { Component, ReactNode } from 'react';
|
||
|
||
interface ErrorBoundaryProps {
|
||
children: ReactNode;
|
||
fallback?: ReactNode;
|
||
}
|
||
|
||
interface ErrorBoundaryState {
|
||
hasError: boolean;
|
||
}
|
||
|
||
export class ErrorBoundary extends Component<
|
||
ErrorBoundaryProps,
|
||
ErrorBoundaryState
|
||
> {
|
||
constructor(props: ErrorBoundaryProps) {
|
||
super(props);
|
||
this.state = { hasError: false };
|
||
}
|
||
|
||
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
|
||
return { hasError: true };
|
||
}
|
||
|
||
componentDidCatch(error: Error, errorInfo: any) {
|
||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||
}
|
||
|
||
render() {
|
||
if (this.state.hasError) {
|
||
return this.props.fallback || (
|
||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||
<div className="text-center">
|
||
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-4">
|
||
组件加载失败
|
||
</h2>
|
||
<p className="text-[#5C5C5C] mb-8">
|
||
抱歉,页面部分内容加载失败。请刷新页面重试。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return this.props.children;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 4: 在根布局中集成错误边界**
|
||
|
||
```typescript
|
||
// src/app/layout.tsx
|
||
import { ErrorBoundary } from '@/components/ui/error-boundary';
|
||
|
||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||
return (
|
||
<html lang="zh-CN">
|
||
<body>
|
||
<ErrorBoundary>
|
||
{children}
|
||
</ErrorBoundary>
|
||
</body>
|
||
</html>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 5: 运行测试验证错误处理**
|
||
|
||
Run: `npm run build`
|
||
|
||
Expected: BUILD SUCCESS
|
||
|
||
**Step 6: 添加错误处理的E2E测试**
|
||
|
||
```typescript
|
||
// e2e/src/tests/smoke/error-handling.smoke.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('错误处理冒烟测试 @smoke', () => {
|
||
test('应该显示404页面', async ({ page }) => {
|
||
await page.goto('/non-existent-page');
|
||
await expect(page.locator('h1')).toContainText('404');
|
||
await expect(page.locator('h2')).toContainText('页面未找到');
|
||
});
|
||
|
||
test('404页面应该有返回首页按钮', async ({ page }) => {
|
||
await page.goto('/non-existent-page');
|
||
const homeButton = page.getByRole('link', { name: /返回首页/ });
|
||
await expect(homeButton).toBeVisible();
|
||
await homeButton.click();
|
||
await expect(page).toHaveURL('/');
|
||
});
|
||
});
|
||
```
|
||
|
||
**Step 7: 提交代码**
|
||
|
||
```bash
|
||
git add src/app/not-found.tsx src/app/error.tsx src/components/ui/error-boundary.tsx src/app/layout.tsx e2e/src/tests/smoke/error-handling.smoke.spec.ts
|
||
git commit -m "feat: implement comprehensive error handling with 404 and error pages"
|
||
```
|
||
|
||
---
|
||
|
||
## 阶段二:中优先级优化(1周)
|
||
|
||
### Task 2.1: 优化加载性能
|
||
|
||
**Files:**
|
||
- Create: `src/components/ui/optimized-image.tsx`
|
||
- Modify: `src/components/sections/hero-section.tsx:1-100`
|
||
- Modify: `src/components/sections/services-section.tsx:1-100`
|
||
- Modify: `src/components/sections/products-section.tsx:1-100`
|
||
- Modify: `src/components/sections/cases-section.tsx:1-100`
|
||
- Modify: `src/components/sections/news-section.tsx:1-100`
|
||
- Modify: `next.config.ts:1-50`
|
||
|
||
**Step 1: 创建优化的图片组件**
|
||
|
||
```typescript
|
||
// src/components/ui/optimized-image.tsx
|
||
'use client';
|
||
|
||
import Image from 'next/image';
|
||
import { useState } from 'react';
|
||
|
||
interface OptimizedImageProps {
|
||
src: string;
|
||
alt: string;
|
||
width?: number;
|
||
height?: number;
|
||
className?: string;
|
||
priority?: boolean;
|
||
}
|
||
|
||
export function OptimizedImage({
|
||
src,
|
||
alt,
|
||
width = 800,
|
||
height = 600,
|
||
className,
|
||
priority = false,
|
||
}: OptimizedImageProps) {
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
|
||
return (
|
||
<div className={`relative overflow-hidden ${className || ''}`}>
|
||
{isLoading && (
|
||
<div className="absolute inset-0 bg-[#F5F5F5] animate-pulse" />
|
||
)}
|
||
<Image
|
||
src={src}
|
||
alt={alt}
|
||
width={width}
|
||
height={height}
|
||
priority={priority}
|
||
className={`transition-opacity duration-300 ${
|
||
isLoading ? 'opacity-0' : 'opacity-100'
|
||
}`}
|
||
onLoad={() => setIsLoading(false)}
|
||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 2: 更新 Next.js 配置以支持图片优化**
|
||
|
||
```typescript
|
||
// next.config.ts
|
||
const nextConfig = {
|
||
images: {
|
||
remotePatterns: [
|
||
{
|
||
protocol: 'https',
|
||
hostname: '**',
|
||
},
|
||
],
|
||
formats: ['image/avif', 'image/webp'],
|
||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||
},
|
||
// 其他配置...
|
||
};
|
||
```
|
||
|
||
**Step 3: 在各个 section 中使用优化的图片组件**
|
||
|
||
```typescript
|
||
// 示例:src/components/sections/hero-section.tsx
|
||
import { OptimizedImage } from '@/components/ui/optimized-image';
|
||
|
||
// 替换所有 <img> 标签为 <OptimizedImage>
|
||
<OptimizedImage
|
||
src="/images/hero.jpg"
|
||
alt="Hero section"
|
||
width={1920}
|
||
height={1080}
|
||
priority
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
```
|
||
|
||
**Step 4: 实现代码分割和懒加载**
|
||
|
||
```typescript
|
||
// src/app/(marketing)/page.tsx
|
||
import dynamic from 'next/dynamic';
|
||
|
||
// 使用动态导入和懒加载
|
||
const HeroSection = dynamic(
|
||
() => import('@/components/sections/hero-section').then(mod => ({ default: mod.HeroSection })),
|
||
{ loading: () => <SectionSkeleton />, ssr: true }
|
||
);
|
||
|
||
const ServicesSection = dynamic(
|
||
() => import('@/components/sections/services-section').then(mod => ({ default: mod.ServicesSection })),
|
||
{ loading: () => <SectionSkeleton />, ssr: false }
|
||
);
|
||
```
|
||
|
||
**Step 5: 添加性能测试**
|
||
|
||
```typescript
|
||
// e2e/src/tests/performance/image-loading.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('图片加载性能测试 @performance', () => {
|
||
test('应该使用优化的图片格式', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const images = await page.locator('img').all();
|
||
for (const image of images) {
|
||
const src = await image.getAttribute('src');
|
||
if (src) {
|
||
expect(src).toMatch(/\.(avif|webp|jpg|png)$/);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('应该有合理的图片尺寸', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const images = await page.locator('img').all();
|
||
for (const image of images) {
|
||
const width = await image.getAttribute('width');
|
||
const height = await image.getAttribute('height');
|
||
expect(width).toBeTruthy();
|
||
expect(height).toBeTruthy();
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
**Step 6: 运行性能测试**
|
||
|
||
Run: `cd e2e && npm test --grep "图片加载性能测试"`
|
||
|
||
Expected: PASS
|
||
|
||
**Step 7: 提交代码**
|
||
|
||
```bash
|
||
git add src/components/ui/optimized-image.tsx next.config.ts src/components/sections/ e2e/src/tests/performance/
|
||
git commit -m "perf: implement image optimization and code splitting"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2.2: 改进移动端体验
|
||
|
||
**Files:**
|
||
- Modify: `src/components/layout/mobile-menu.tsx:1-200`
|
||
- Modify: `src/components/layout/mobile-tab-bar.tsx:1-100`
|
||
- Create: `e2e/src/tests/responsive/mobile-ux.spec.ts`
|
||
|
||
**Step 1: 优化移动端菜单交互**
|
||
|
||
```typescript
|
||
// src/components/layout/mobile-menu.tsx
|
||
'use client';
|
||
|
||
import { useState, useEffect, useRef } from 'react';
|
||
import { X } from 'lucide-react';
|
||
|
||
export function MobileMenu({ isOpen, onClose, children }: any) {
|
||
const menuRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
if (isOpen) {
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
document.body.style.overflow = 'hidden';
|
||
} else {
|
||
document.body.style.overflow = 'unset';
|
||
}
|
||
|
||
return () => {
|
||
document.removeEventListener('mousedown', handleClickOutside);
|
||
document.body.style.overflow = 'unset';
|
||
};
|
||
}, [isOpen, onClose]);
|
||
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/50 z-50">
|
||
<div
|
||
ref={menuRef}
|
||
className="fixed right-0 top-0 h-full w-80 bg-white shadow-2xl transform transition-transform duration-300"
|
||
>
|
||
<button
|
||
onClick={onClose}
|
||
className="absolute top-4 right-4 p-2 hover:bg-[#F5F5F5] rounded-lg"
|
||
aria-label="关闭菜单"
|
||
>
|
||
<X className="w-6 h-6" />
|
||
</button>
|
||
<div className="p-6 pt-16">
|
||
{children}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 2: 添加移动端触摸优化**
|
||
|
||
```typescript
|
||
// src/components/ui/touch-button.tsx
|
||
'use client';
|
||
|
||
import { Button } from './button';
|
||
import { forwardRef } from 'react';
|
||
|
||
export const TouchButton = forwardRef<
|
||
HTMLButtonElement,
|
||
React.ComponentProps<typeof Button>
|
||
>((props, ref) => {
|
||
return (
|
||
<Button
|
||
ref={ref}
|
||
{...props}
|
||
className={`${props.className || ''} min-h-11 min-w-11`}
|
||
style={{
|
||
WebkitTapHighlightColor: 'transparent',
|
||
...props.style,
|
||
}}
|
||
/>
|
||
);
|
||
});
|
||
```
|
||
|
||
**Step 3: 添加移动端UX测试**
|
||
|
||
```typescript
|
||
// e2e/src/tests/responsive/mobile-ux.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('移动端UX测试 @responsive', () => {
|
||
test.use({ viewport: { width: 375, height: 667 } });
|
||
|
||
test('应该能够打开和关闭移动端菜单', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const menuButton = page.getByRole('button', { name: /菜单/ });
|
||
await menuButton.tap();
|
||
await expect(page.locator('[role="dialog"]')).toBeVisible();
|
||
|
||
const closeButton = page.getByRole('button', { name: /关闭/ });
|
||
await closeButton.tap();
|
||
await expect(page.locator('[role="dialog"]')).not.toBeVisible();
|
||
});
|
||
|
||
test('应该能够通过触摸导航', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const menuButton = page.getByRole('button', { name: /菜单/ });
|
||
await menuButton.tap();
|
||
|
||
const navItem = page.getByRole('link', { name: /核心业务/ });
|
||
await navItem.tap();
|
||
await expect(page).toHaveURL(/services/);
|
||
});
|
||
|
||
test('触摸目标应该足够大', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const buttons = await page.locator('button, a[role="button"]').all();
|
||
for (const button of buttons) {
|
||
const box = await button.boundingBox();
|
||
expect(box?.width).toBeGreaterThanOrEqual(44);
|
||
expect(box?.height).toBeGreaterThanOrEqual(44);
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
**Step 4: 运行移动端测试**
|
||
|
||
Run: `cd e2e && npm test --grep "移动端UX测试"`
|
||
|
||
Expected: PASS
|
||
|
||
**Step 5: 提交代码**
|
||
|
||
```bash
|
||
git add src/components/layout/mobile-menu.tsx src/components/ui/touch-button.tsx e2e/src/tests/responsive/mobile-ux.spec.ts
|
||
git commit -m "feat: improve mobile experience with optimized menu and touch targets"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2.3: SEO优化
|
||
|
||
**Files:**
|
||
- Create: `src/components/seo/structured-data.tsx`
|
||
- Modify: `src/app/layout.tsx:1-50`
|
||
- Modify: `src/app/(marketing)/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/about/page.tsx:1-15`
|
||
- Modify: `src/app/(marketing)/cases/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/services/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/products/page.tsx:1-20`
|
||
|
||
**Step 1: 创建结构化数据组件**
|
||
|
||
```typescript
|
||
// src/components/seo/structured-data.tsx
|
||
interface StructuredDataProps {
|
||
data: Record<string, any>;
|
||
}
|
||
|
||
export function StructuredData({ data }: StructuredDataProps) {
|
||
return (
|
||
<script
|
||
type="application/ld+json"
|
||
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
|
||
/>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 2: 在根布局中添加基础SEO**
|
||
|
||
```typescript
|
||
// src/app/layout.tsx
|
||
import { StructuredData } from '@/components/seo/structured-data';
|
||
import { COMPANY_INFO } from '@/lib/constants';
|
||
|
||
export const metadata = {
|
||
metadataBase: new URL('http://localhost:3000'),
|
||
title: {
|
||
default: `${COMPANY_INFO.name} - ${COMPANY_INFO.slogan}`,
|
||
template: `%s - ${COMPANY_INFO.name}`,
|
||
},
|
||
description: COMPANY_INFO.description,
|
||
keywords: ['数字化转型', '软件开发', '云服务', '数据分析', '信息安全'],
|
||
openGraph: {
|
||
title: COMPANY_INFO.name,
|
||
description: COMPANY_INFO.description,
|
||
url: 'http://localhost:3000',
|
||
siteName: COMPANY_INFO.name,
|
||
images: [
|
||
{
|
||
url: '/logo.webp',
|
||
width: 1200,
|
||
height: 630,
|
||
},
|
||
],
|
||
},
|
||
};
|
||
|
||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||
const organizationData = {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'Organization',
|
||
name: COMPANY_INFO.name,
|
||
url: 'http://localhost:3000',
|
||
logo: 'http://localhost:3000/logo.webp',
|
||
description: COMPANY_INFO.description,
|
||
address: {
|
||
'@type': 'PostalAddress',
|
||
streetAddress: COMPANY_INFO.address,
|
||
addressLocality: '成都市',
|
||
addressRegion: '四川省',
|
||
addressCountry: 'CN',
|
||
},
|
||
contactPoint: {
|
||
'@type': 'ContactPoint',
|
||
telephone: COMPANY_INFO.phone,
|
||
email: COMPANY_INFO.email,
|
||
},
|
||
};
|
||
|
||
return (
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<StructuredData data={organizationData} />
|
||
</head>
|
||
<body>
|
||
{children}
|
||
</body>
|
||
</html>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 3: 为各个页面添加特定的结构化数据**
|
||
|
||
```typescript
|
||
// 示例:src/app/(marketing)/services/page.tsx
|
||
import { StructuredData } from '@/components/seo/structured-data';
|
||
import { SERVICES } from '@/lib/constants';
|
||
|
||
export const metadata = {
|
||
title: '核心业务 - 睿新致远',
|
||
description: '专业技术团队,为您提供全方位的数字化解决方案',
|
||
};
|
||
|
||
export default function ServicesPage() {
|
||
const servicesData = {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'ItemList',
|
||
itemListElement: SERVICES.map((service) => ({
|
||
'@type': 'Service',
|
||
name: service.title,
|
||
description: service.description,
|
||
})),
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-white">
|
||
<StructuredData data={servicesData} />
|
||
{/* 页面内容 */}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 4: 添加SEO验证测试**
|
||
|
||
```typescript
|
||
// e2e/src/tests/smoke/seo.smoke.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('SEO冒烟测试 @smoke', () => {
|
||
test('应该有正确的页面标题', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const title = await page.title();
|
||
expect(title).toContain('睿新致远');
|
||
});
|
||
|
||
test('应该有meta描述', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const description = await page.locator('meta[name="description"]').getAttribute('content');
|
||
expect(description).toBeTruthy();
|
||
expect(description?.length).toBeGreaterThan(50);
|
||
});
|
||
|
||
test('应该有结构化数据', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const structuredData = await page.locator('script[type="application/ld+json"]').count();
|
||
expect(structuredData).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('应该有Open Graph标签', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const ogTitle = await page.locator('meta[property="og:title"]').getAttribute('content');
|
||
const ogDescription = await page.locator('meta[property="og:description"]').getAttribute('content');
|
||
expect(ogTitle).toBeTruthy();
|
||
expect(ogDescription).toBeTruthy();
|
||
});
|
||
});
|
||
```
|
||
|
||
**Step 5: 运行SEO测试**
|
||
|
||
Run: `cd e2e && npm test --grep "SEO冒烟测试"`
|
||
|
||
Expected: PASS
|
||
|
||
**Step 6: 提交代码**
|
||
|
||
```bash
|
||
git add src/components/seo/structured-data.tsx src/app/layout.tsx src/app/ e2e/src/tests/smoke/seo.smoke.spec.ts
|
||
git commit -m "feat: implement comprehensive SEO optimization with structured data"
|
||
```
|
||
|
||
---
|
||
|
||
## 阶段三:低优先级优化(1个月)
|
||
|
||
### Task 3.1: 代码重构
|
||
|
||
**Files:**
|
||
- Create: `src/components/ui/page-header.tsx`
|
||
- Modify: `src/app/(marketing)/about/page.tsx:1-15`
|
||
- Modify: `src/app/(marketing)/cases/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/services/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/products/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/solutions/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/contact/page.tsx:1-20`
|
||
- Modify: `src/app/(marketing)/news/page.tsx:1-20`
|
||
|
||
**Step 1: 统一页面标题组件**
|
||
|
||
```typescript
|
||
// src/components/ui/page-header.tsx
|
||
import { Badge } from './badge';
|
||
|
||
interface PageHeaderProps {
|
||
badge?: string;
|
||
title: string;
|
||
description: string;
|
||
}
|
||
|
||
export function PageHeader({ badge, title, description }: PageHeaderProps) {
|
||
return (
|
||
<div className="bg-linear-to-br from-[#FFFBF5] to-white py-16">
|
||
<div className="container-wide">
|
||
{badge && (
|
||
<Badge className="mb-4">{badge}</Badge>
|
||
)}
|
||
<h1 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
|
||
{title}
|
||
</h1>
|
||
<p className="text-lg text-[#5C5C5C] max-w-3xl">
|
||
{description}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 2: 提取公共卡片组件**
|
||
|
||
```typescript
|
||
// src/components/ui/content-card.tsx
|
||
import Link from 'next/link';
|
||
import { Card, CardContent } from './card';
|
||
import { Badge } from './badge';
|
||
|
||
interface ContentCardProps {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
category?: string;
|
||
date?: string;
|
||
image?: string;
|
||
href: string;
|
||
}
|
||
|
||
export function ContentCard({
|
||
id,
|
||
title,
|
||
description,
|
||
category,
|
||
date,
|
||
image,
|
||
href,
|
||
}: ContentCardProps) {
|
||
return (
|
||
<Link href={href} className="group">
|
||
<Card className="h-full hover:shadow-xl transition-all duration-300 border-[#E5E5E5] hover:border-[#C41E3A]">
|
||
<CardContent className="p-0">
|
||
{image && (
|
||
<div className="aspect-video bg-linear-to-br from-[#C41E3A]/10 to-[#1C1C1C]/10 flex items-center justify-center">
|
||
<span className="text-4xl">📰</span>
|
||
</div>
|
||
)}
|
||
<div className="p-6">
|
||
{category && (
|
||
<Badge variant="secondary" className="mb-3">
|
||
{category}
|
||
</Badge>
|
||
)}
|
||
{date && (
|
||
<div className="text-sm text-[#5C5C5C] mb-3">
|
||
{date}
|
||
</div>
|
||
)}
|
||
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-3 group-hover:text-[#C41E3A] transition-colors line-clamp-2">
|
||
{title}
|
||
</h3>
|
||
<p className="text-[#5C5C5C] text-sm line-clamp-3">
|
||
{description}
|
||
</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 3: 在各个页面中使用统一组件**
|
||
|
||
```typescript
|
||
// 示例:src/app/(marketing)/cases/page.tsx
|
||
import { PageHeader } from '@/components/ui/page-header';
|
||
import { ContentCard } from '@/components/ui/content-card';
|
||
|
||
export default function CasesPage() {
|
||
return (
|
||
<div className="min-h-screen bg-white">
|
||
<PageHeader
|
||
badge="成功案例"
|
||
title="与谁同行,决定能走多远"
|
||
description="我们与优秀的企业同行,共同成长,共创未来"
|
||
/>
|
||
<div className="container-wide py-16">
|
||
<div className="grid md:grid-cols-2 gap-8">
|
||
{CASES.map((caseItem) => (
|
||
<ContentCard
|
||
key={caseItem.id}
|
||
id={caseItem.id}
|
||
title={caseItem.title}
|
||
description={caseItem.description}
|
||
category={caseItem.industry}
|
||
href={`/cases/${caseItem.id}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 4: 运行测试验证重构**
|
||
|
||
Run: `npm run lint && npm run build`
|
||
|
||
Expected: No linting errors, BUILD SUCCESS
|
||
|
||
**Step 5: 提交代码**
|
||
|
||
```bash
|
||
git add src/components/ui/page-header.tsx src/components/ui/content-card.tsx src/app/
|
||
git commit -m "refactor: extract common components and unify page structure"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3.2: 视觉优化
|
||
|
||
**Files:**
|
||
- Create: `src/components/ui/animated-section.tsx`
|
||
- Modify: `src/components/sections/hero-section.tsx:1-100`
|
||
- Modify: `src/components/sections/services-section.tsx:1-100`
|
||
- Modify: `src/components/sections/products-section.tsx:1-100`
|
||
|
||
**Step 1: 创建优化的动画section组件**
|
||
|
||
```typescript
|
||
// src/components/ui/animated-section.tsx
|
||
'use client';
|
||
|
||
import { useRef, useEffect } from 'react';
|
||
import { motion, useInView } from 'framer-motion';
|
||
|
||
interface AnimatedSectionProps {
|
||
children: React.ReactNode;
|
||
className?: string;
|
||
delay?: number;
|
||
}
|
||
|
||
export function AnimatedSection({
|
||
children,
|
||
className = '',
|
||
delay = 0,
|
||
}: AnimatedSectionProps) {
|
||
const ref = useRef(null);
|
||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||
|
||
return (
|
||
<motion.section
|
||
ref={ref}
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||
transition={{ duration: 0.6, delay }}
|
||
className={className}
|
||
>
|
||
{children}
|
||
</motion.section>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 2: 添加微交互效果**
|
||
|
||
```typescript
|
||
// src/components/ui/interactive-card.tsx
|
||
'use client';
|
||
|
||
import { motion } from 'framer-motion';
|
||
import { Card } from './card';
|
||
|
||
interface InteractiveCardProps {
|
||
children: React.ReactNode;
|
||
className?: string;
|
||
onClick?: () => void;
|
||
}
|
||
|
||
export function InteractiveCard({
|
||
children,
|
||
className = '',
|
||
onClick,
|
||
}: InteractiveCardProps) {
|
||
return (
|
||
<motion.div
|
||
whileHover={{ scale: 1.02, y: -4 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||
className={className}
|
||
onClick={onClick}
|
||
>
|
||
<Card>{children}</Card>
|
||
</motion.div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Step 3: 添加视觉回归测试**
|
||
|
||
```typescript
|
||
// e2e/src/tests/visual/interactions.visual.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('交互视觉回归测试 @visual', () => {
|
||
test('卡片悬停状态应该一致', async ({ page }) => {
|
||
await page.goto('/services');
|
||
|
||
const card = page.locator('.group').first();
|
||
await card.hover();
|
||
await expect(page).toHaveScreenshot('card-hover.png', {
|
||
maxDiffPixels: 100,
|
||
});
|
||
});
|
||
|
||
test('按钮点击状态应该一致', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
const button = page.getByRole('button', { name: /立即咨询/ });
|
||
await button.hover();
|
||
await expect(page).toHaveScreenshot('button-hover.png', {
|
||
maxDiffPixels: 100,
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
**Step 4: 运行视觉测试**
|
||
|
||
Run: `cd e2e && npm test --grep "交互视觉回归测试"`
|
||
|
||
Expected: PASS
|
||
|
||
**Step 5: 提交代码**
|
||
|
||
```bash
|
||
git add src/components/ui/animated-section.tsx src/components/ui/interactive-card.tsx e2e/src/tests/visual/interactions.visual.spec.ts
|
||
git commit -m "feat: add enhanced animations and micro-interactions"
|
||
```
|
||
|
||
---
|
||
|
||
## 测试验证清单
|
||
|
||
在每个阶段完成后,运行以下测试验证:
|
||
|
||
### 阶段一验证
|
||
```bash
|
||
# 导航一致性测试
|
||
cd e2e && npm test --grep "导航"
|
||
|
||
# 页面冒烟测试
|
||
cd e2e && npm test --grep "冒烟测试"
|
||
|
||
# 错误处理测试
|
||
cd e2e && npm test --grep "错误处理"
|
||
```
|
||
|
||
### 阶段二验证
|
||
```bash
|
||
# 性能测试
|
||
cd e2e && npm test --grep "@performance"
|
||
|
||
# 移动端测试
|
||
cd e2e && npm test --grep "@responsive"
|
||
|
||
# SEO测试
|
||
cd e2e && npm test --grep "SEO"
|
||
```
|
||
|
||
### 阶段三验证
|
||
```bash
|
||
# 视觉测试
|
||
cd e2e && npm test --grep "@visual"
|
||
|
||
# 回归测试
|
||
cd e2e && npm test --grep "@regression"
|
||
|
||
# 完整测试套件
|
||
cd e2e && npm test
|
||
```
|
||
|
||
---
|
||
|
||
## 代码质量检查
|
||
|
||
在每个任务完成后,运行以下命令确保代码质量:
|
||
|
||
```bash
|
||
# Lint检查
|
||
npm run lint
|
||
|
||
# 类型检查
|
||
npx tsc --noEmit
|
||
|
||
# 构建检查
|
||
npm run build
|
||
|
||
# 格式化检查
|
||
npx prettier --check "src/**/*.{ts,tsx}"
|
||
```
|
||
|
||
---
|
||
|
||
## 文档更新
|
||
|
||
在完成每个阶段后,更新以下文档:
|
||
|
||
### README.md
|
||
- 更新项目结构
|
||
- 添加新的组件说明
|
||
- 更新测试命令
|
||
|
||
### CHANGELOG.md
|
||
```markdown
|
||
## [版本号] - YYYY-MM-DD
|
||
|
||
### 新增
|
||
- 面包屑导航组件
|
||
- 统一的错误处理页面
|
||
- 优化的图片加载组件
|
||
- 移动端触摸优化
|
||
- SEO结构化数据
|
||
|
||
### 改进
|
||
- 导航一致性
|
||
- 移动端体验
|
||
- 页面加载性能
|
||
- SEO优化
|
||
|
||
### 修复
|
||
- 修复服务详情页Link导入问题
|
||
- 修复导航不一致问题
|
||
```
|
||
|
||
---
|
||
|
||
## 部署检查清单
|
||
|
||
在部署到生产环境前,确保:
|
||
|
||
- [ ] 所有测试通过
|
||
- [ ] Lint检查通过
|
||
- [ ] 类型检查通过
|
||
- [ ] 构建成功
|
||
- [ ] 环境变量配置正确
|
||
- [ ] API端点配置正确
|
||
- [ ] 图片资源优化
|
||
- [ ] 性能指标达标(Lighthouse分数 > 90)
|
||
- [ ] 无障碍检查通过(axe-core)
|
||
- [ ] SEO检查通过(Google Rich Results Test)
|
||
|
||
---
|
||
|
||
## 回滚计划
|
||
|
||
如果某个阶段出现问题,可以按以下步骤回滚:
|
||
|
||
```bash
|
||
# 查看提交历史
|
||
git log --oneline
|
||
|
||
# 回滚到上一个稳定版本
|
||
git revert <commit-hash>
|
||
|
||
# 或者回滚到特定提交
|
||
git reset --hard <commit-hash>
|
||
|
||
# 强制推送(谨慎使用)
|
||
git push --force
|
||
```
|
||
|
||
---
|
||
|
||
## 持续集成配置
|
||
|
||
确保以下CI/CD配置正确:
|
||
|
||
### .woodpecker.yml
|
||
```yaml
|
||
pipeline:
|
||
test:
|
||
image: node:18
|
||
commands:
|
||
- npm ci
|
||
- npm run lint
|
||
- npm run build
|
||
- cd e2e && npm ci
|
||
- cd e2e && npm test
|
||
|
||
deploy:
|
||
image: node:18
|
||
commands:
|
||
- npm run build
|
||
- # 部署命令
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
本实施计划分为三个阶段,每个阶段包含多个bite-sized任务:
|
||
|
||
1. **阶段一(1-2天)**:高优先级修复
|
||
- 导航一致性
|
||
- 测试覆盖
|
||
- 错误处理
|
||
|
||
2. **阶段二(1周)**:中优先级优化
|
||
- 性能优化
|
||
- 移动端体验
|
||
- SEO优化
|
||
|
||
3. **阶段三(1个月)**:低优先级优化
|
||
- 代码重构
|
||
- 视觉优化
|
||
|
||
每个任务都遵循TDD原则,包含完整的测试、实现和验证步骤。建议按阶段顺序执行,每个阶段完成后进行完整的测试验证。
|