fe6e4b1c54
- Remove TestimonialSection from homepage (no customers yet) - Footer: dark theme, NAVIGATION_V2 + MEGA_DROPDOWN_DATA links - Mobile Menu: NAVIGATION_V2 with collapsible dropdown support
1777 lines
53 KiB
Markdown
1777 lines
53 KiB
Markdown
# Atlassian 风格品牌融合重构 实现计划
|
||
|
||
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
||
|
||
**目标:** 借鉴 Atlassian 信息架构,重构 Novalon 首页为"收敛但保留印记"的品牌融合风格,同步沉淀 6 个核心组件作为设计系统基础。
|
||
|
||
**架构:** 保持 Next.js 16 App Router + 静态导出架构不变。首页从单文件长滚动重构为 6 个独立 Section 组件 + 新导航组件。动效从粒子/水墨收敛为微交互。导航从 8 项平铺改为 4 核心 + 2 下拉。
|
||
|
||
**技术栈:** Next.js 16 + React 19 + Tailwind CSS 4 + Framer Motion 12 + Lucide React
|
||
|
||
---
|
||
|
||
## 整体架构图
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph "Phase 1: 首页重构 + 设计系统基础"
|
||
T1[任务1: Design Token 更新] --> T2[任务2: 导航常量重构]
|
||
T2 --> T3[任务3: MegaDropdown 组件]
|
||
T3 --> T4[任务4: Header 重构]
|
||
T4 --> T5[任务5: Hero Section 重构]
|
||
T5 --> T6[任务6: StatsBar 组件]
|
||
T6 --> T7[任务7: 社会证明 Section]
|
||
T7 --> T8[任务8: ProductCard 组件]
|
||
T8 --> T9[任务9: 产品矩阵 Section]
|
||
T9 --> T10[任务10: ChallengeCard 组件]
|
||
T10 --> T11[任务11: 挑战场景 Section]
|
||
T11 --> T12[任务12: TestimonialBlock 组件]
|
||
T12 --> T13[任务13: 客户成果 Section]
|
||
T13 --> T14[任务14: CTASection 组件]
|
||
T14 --> T15[任务15: CTA Section]
|
||
T15 --> T16[任务16: 首页组装]
|
||
T16 --> T17[任务17: 废弃动效清理]
|
||
T17 --> T18[任务18: 集成测试与验证]
|
||
end
|
||
```
|
||
|
||
## 组件依赖图
|
||
|
||
```mermaid
|
||
graph LR
|
||
subgraph "基础层"
|
||
DT[Design Tokens<br/>globals.css]
|
||
NAV_CONST[Navigation Constants<br/>navigation.ts]
|
||
end
|
||
|
||
subgraph "组件层"
|
||
MD[MegaDropdown<br/>mega-dropdown.tsx]
|
||
PC[ProductCard<br/>product-card.tsx]
|
||
CC[ChallengeCard<br/>challenge-card.tsx]
|
||
SB[StatsBar<br/>stats-bar.tsx]
|
||
TB[TestimonialBlock<br/>testimonial-block.tsx]
|
||
CTA[CTASection<br/>cta-section.tsx]
|
||
end
|
||
|
||
subgraph "Section 层"
|
||
HDR[Header<br/>header.tsx]
|
||
HERO[HeroSection<br/>hero-section.tsx]
|
||
SOCIAL[SocialProofSection<br/>social-proof-section.tsx]
|
||
PROD[ProductMatrixSection<br/>product-matrix-section.tsx]
|
||
CHAL[ChallengeSection<br/>challenge-section.tsx]
|
||
TEST[TestimonialSection<br/>testimonial-section.tsx]
|
||
CTAS[CTASectionPage<br/>cta-section-page.tsx]
|
||
end
|
||
|
||
subgraph "页面层"
|
||
HOME[home-content.tsx]
|
||
end
|
||
|
||
DT --> MD & PC & CC & SB & TB & CTA
|
||
NAV_CONST --> MD
|
||
MD --> HDR
|
||
PC --> PROD
|
||
CC --> CHAL
|
||
SB --> SOCIAL
|
||
TB --> TEST
|
||
CTA --> CTAS
|
||
|
||
HDR & HERO & SOCIAL & PROD & CHAL & TEST & CTAS --> HOME
|
||
```
|
||
|
||
## 数据流图
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph "数据源 (src/lib/constants/)"
|
||
NAV[navigation.ts<br/>NAVIGATION_V2]
|
||
PRODS[products.ts<br/>PRODUCTS]
|
||
SOL[solutions.ts<br/>SOLUTIONS]
|
||
CASES[cases.ts<br/>CASES]
|
||
STATS[stats.ts<br/>STATS]
|
||
end
|
||
|
||
NAV -->|导航项+下拉数据| HDR_COMP[Header]
|
||
PRODS -->|4个产品| PROD_SEC[ProductMatrixSection]
|
||
SOL -->|3个挑战场景| CHAL_SEC[ChallengeSection]
|
||
CASES -->|testimonial+results| TEST_SEC[TestimonialSection]
|
||
STATS -->|3个关键数据| SOCIAL_SEC[SocialProofSection]
|
||
```
|
||
|
||
## 文件结构
|
||
|
||
### 新建文件
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `src/components/layout/mega-dropdown.tsx` | 产品/解决方案矩阵下拉导航组件 |
|
||
| `src/components/layout/mega-dropdown.test.tsx` | 下拉导航测试 |
|
||
| `src/components/ui/product-card.tsx` | 朱砂红左边框产品卡片 |
|
||
| `src/components/ui/product-card.test.tsx` | 产品卡片测试 |
|
||
| `src/components/ui/challenge-card.tsx` | 痛点/场景入口卡片 |
|
||
| `src/components/ui/challenge-card.test.tsx` | 场景卡片测试 |
|
||
| `src/components/ui/stats-bar.tsx` | 三列数据统计条 |
|
||
| `src/components/ui/stats-bar.test.tsx` | 统计条测试 |
|
||
| `src/components/ui/testimonial-block.tsx` | 深色背景客户证言 |
|
||
| `src/components/ui/testimonial-block.test.tsx` | 客户证言测试 |
|
||
| `src/components/ui/cta-section.tsx` | 朱砂红渐变行动号召 |
|
||
| `src/components/ui/cta-section.test.tsx` | CTA 测试 |
|
||
| `src/components/sections/hero-section-v2.tsx` | 新 Hero Section |
|
||
| `src/components/sections/hero-section-v2.test.tsx` | 新 Hero 测试 |
|
||
| `src/components/sections/social-proof-section.tsx` | 社会证明 Section |
|
||
| `src/components/sections/social-proof-section.test.tsx` | 社会证明测试 |
|
||
| `src/components/sections/product-matrix-section.tsx` | 产品矩阵 Section |
|
||
| `src/components/sections/product-matrix-section.test.tsx` | 产品矩阵测试 |
|
||
| `src/components/sections/challenge-section.tsx` | 挑战场景 Section |
|
||
| `src/components/sections/challenge-section.test.tsx` | 挑战场景测试 |
|
||
| `src/components/sections/testimonial-section.tsx` | 客户成果 Section |
|
||
| `src/components/sections/testimonial-section.test.tsx` | 客户成果测试 |
|
||
| `src/components/sections/cta-section-page.tsx` | CTA Section |
|
||
| `src/components/sections/cta-section-page.test.tsx` | CTA Section 测试 |
|
||
|
||
### 修改文件
|
||
|
||
| 文件 | 变更内容 |
|
||
|------|---------|
|
||
| `src/app/globals.css` | 新增场景色 Token、废弃动效相关 CSS |
|
||
| `src/lib/constants/navigation.ts` | 新增 NAVIGATION_V2 常量 + MegaDropdown 数据结构 |
|
||
| `src/lib/constants/stats.ts` | 新增 HOME_STATS 三列数据 |
|
||
| `src/components/layout/header.tsx` | 集成 MegaDropdown,使用 NAVIGATION_V2 |
|
||
| `src/app/(marketing)/home-content.tsx` | 替换为新 Section 组件 |
|
||
| `src/components/sections/hero-section.tsx` | 保留旧版,新版本为 hero-section-v2.tsx |
|
||
|
||
---
|
||
|
||
## 任务 1:Design Token 更新
|
||
|
||
**文件:**
|
||
- 修改:`src/app/globals.css`
|
||
|
||
- [ ] **步骤 1:在 `:root` 中新增场景色变量**
|
||
|
||
在 `globals.css` 的 `:root` 块中,在 `/* 状态色 */` 注释之后添加:
|
||
|
||
```css
|
||
/* 场景色 - 挑战卡片 */
|
||
--color-challenge-isolation: #FEF2F4;
|
||
--color-challenge-isolation-hover: #FDE8EC;
|
||
--color-challenge-growth: #FFFBEB;
|
||
--color-challenge-growth-hover: #FEF3C7;
|
||
--color-challenge-compliance: #F0FDF4;
|
||
--color-challenge-compliance-hover: #DCFCE7;
|
||
```
|
||
|
||
- [ ] **步骤 2:验证 CSS 变量可用**
|
||
|
||
运行:`npx tsc --noEmit`
|
||
预期:无类型错误
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add src/app/globals.css
|
||
git commit -m "feat: add challenge scenario color tokens to design system"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 2:导航常量重构
|
||
|
||
**文件:**
|
||
- 修改:`src/lib/constants/navigation.ts`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
在 `src/lib/constants/navigation.test.ts`(新建)中:
|
||
|
||
```typescript
|
||
import { describe, it, expect } from '@jest/globals';
|
||
import { NAVIGATION_V2, MEGA_DROPDOWN_DATA } from './navigation';
|
||
|
||
describe('NAVIGATION_V2', () => {
|
||
it('has exactly 6 navigation items', () => {
|
||
expect(NAVIGATION_V2).toHaveLength(6);
|
||
});
|
||
|
||
it('contains product dropdown item', () => {
|
||
const productItem = NAVIGATION_V2.find(item => item.id === 'products');
|
||
expect(productItem).toBeDefined();
|
||
expect(productItem?.hasDropdown).toBe(true);
|
||
});
|
||
|
||
it('contains solutions dropdown item', () => {
|
||
const solutionsItem = NAVIGATION_V2.find(item => item.id === 'solutions');
|
||
expect(solutionsItem).toBeDefined();
|
||
expect(solutionsItem?.hasDropdown).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('MEGA_DROPDOWN_DATA', () => {
|
||
it('has products dropdown with 4 items', () => {
|
||
expect(MEGA_DROPDOWN_DATA.products).toHaveLength(4);
|
||
});
|
||
|
||
it('has solutions dropdown with 4 items', () => {
|
||
expect(MEGA_DROPDOWN_DATA.solutions).toHaveLength(4);
|
||
});
|
||
|
||
it('each product has id, title, description, href', () => {
|
||
MEGA_DROPDOWN_DATA.products.forEach(product => {
|
||
expect(product).toHaveProperty('id');
|
||
expect(product).toHaveProperty('title');
|
||
expect(product).toHaveProperty('description');
|
||
expect(product).toHaveProperty('href');
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/lib/constants/navigation.test.ts --no-coverage`
|
||
预期:FAIL — `Cannot find module './navigation'` 或 `NAVIGATION_V2 is not exported`
|
||
|
||
- [ ] **步骤 3:实现导航常量**
|
||
|
||
在 `src/lib/constants/navigation.ts` 中追加:
|
||
|
||
```typescript
|
||
export interface NavigationItemV2 {
|
||
id: string;
|
||
label: string;
|
||
href: string;
|
||
hasDropdown?: boolean;
|
||
dropdownKey?: string;
|
||
}
|
||
|
||
export interface MegaDropdownItem {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
href: string;
|
||
}
|
||
|
||
export interface MegaDropdownData {
|
||
[key: string]: MegaDropdownItem[];
|
||
}
|
||
|
||
export const NAVIGATION_V2: NavigationItemV2[] = [
|
||
{ id: 'products', label: '产品', href: '/products', hasDropdown: true, dropdownKey: 'products' },
|
||
{ id: 'solutions', label: '解决方案', href: '/solutions', hasDropdown: true, dropdownKey: 'solutions' },
|
||
{ id: 'services', label: '服务', href: '/services' },
|
||
{ id: 'cases', label: '案例', href: '/cases' },
|
||
{ id: 'about', label: '关于我们', href: '/about' },
|
||
{ id: 'contact', label: '联系我们', href: '/contact' },
|
||
];
|
||
|
||
export const MEGA_DROPDOWN_DATA: MegaDropdownData = {
|
||
products: [
|
||
{ id: 'erp', title: 'ERP 管理系统', description: '财务·采购·销售·库存·生产', href: '/products/erp' },
|
||
{ id: 'crm', title: 'CRM 客户管理', description: '线索·商机·合同·服务', href: '/products/crm' },
|
||
{ id: 'bi', title: 'BI 数据平台', description: '报表·仪表盘·预测·决策', href: '/products/bi' },
|
||
{ id: 'cms', title: 'CMS 内容平台', description: '建站·运营·分发·分析', href: '/products/cms' },
|
||
],
|
||
solutions: [
|
||
{ id: 'manufacturing', title: '制造业', description: '智能制造·MES·质量管控', href: '/solutions/manufacturing' },
|
||
{ id: 'retail', title: '零售业', description: '全渠道·会员·精准营销', href: '/solutions/retail' },
|
||
{ id: 'education', title: '教育行业', description: '招生·教学·学情分析', href: '/solutions/education' },
|
||
{ id: 'healthcare', title: '医疗健康', description: '临床·运营·患者服务', href: '/solutions/healthcare' },
|
||
],
|
||
};
|
||
```
|
||
|
||
- [ ] **步骤 4:更新 index.ts 导出**
|
||
|
||
在 `src/lib/constants/index.ts` 中追加导出:
|
||
|
||
```typescript
|
||
export { NAVIGATION_V2, MEGA_DROPDOWN_DATA } from './navigation';
|
||
export type { NavigationItemV2, MegaDropdownItem, MegaDropdownData } from './navigation';
|
||
```
|
||
|
||
- [ ] **步骤 5:运行测试验证通过**
|
||
|
||
运行:`npx jest src/lib/constants/navigation.test.ts --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 6:Commit**
|
||
|
||
```bash
|
||
git add src/lib/constants/navigation.ts src/lib/constants/navigation.test.ts src/lib/constants/index.ts
|
||
git commit -m "feat: add NAVIGATION_V2 and MEGA_DROPDOWN_DATA for mega dropdown navigation"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 3:MegaDropdown 组件
|
||
|
||
**文件:**
|
||
- 创建:`src/components/layout/mega-dropdown.tsx`
|
||
- 创建:`src/components/layout/mega-dropdown.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
```tsx
|
||
// src/components/layout/mega-dropdown.test.tsx
|
||
import { render, screen, fireEvent } from '@testing-library/react';
|
||
import { describe, it, expect, vi } from 'vitest';
|
||
import { MegaDropdown } from './mega-dropdown';
|
||
import { MEGA_DROPDOWN_DATA } from '@/lib/constants';
|
||
|
||
describe('MegaDropdown', () => {
|
||
it('renders trigger button with label', () => {
|
||
render(
|
||
<MegaDropdown
|
||
label="产品"
|
||
items={MEGA_DROPDOWN_DATA.products}
|
||
isOpen={false}
|
||
onToggle={vi.fn()}
|
||
/>
|
||
);
|
||
expect(screen.getByText('产品')).toBeInTheDocument();
|
||
});
|
||
|
||
it('shows dropdown content when isOpen is true', () => {
|
||
render(
|
||
<MegaDropdown
|
||
label="产品"
|
||
items={MEGA_DROPDOWN_DATA.products}
|
||
isOpen={true}
|
||
onToggle={vi.fn()}
|
||
/>
|
||
);
|
||
expect(screen.getByText('ERP 管理系统')).toBeInTheDocument();
|
||
});
|
||
|
||
it('hides dropdown content when isOpen is false', () => {
|
||
render(
|
||
<MegaDropdown
|
||
label="产品"
|
||
items={MEGA_DROPDOWN_DATA.products}
|
||
isOpen={false}
|
||
onToggle={vi.fn()}
|
||
/>
|
||
);
|
||
expect(screen.queryByText('ERP 管理系统')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('calls onToggle when trigger is clicked', () => {
|
||
const onToggle = vi.fn();
|
||
render(
|
||
<MegaDropdown
|
||
label="产品"
|
||
items={MEGA_DROPDOWN_DATA.products}
|
||
isOpen={false}
|
||
onToggle={onToggle}
|
||
/>
|
||
);
|
||
fireEvent.click(screen.getByText('产品'));
|
||
expect(onToggle).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('renders each item with title, description and link', () => {
|
||
render(
|
||
<MegaDropdown
|
||
label="产品"
|
||
items={MEGA_DROPDOWN_DATA.products}
|
||
isOpen={true}
|
||
onToggle={vi.fn()}
|
||
/>
|
||
);
|
||
MEGA_DROPDOWN_DATA.products.forEach(item => {
|
||
expect(screen.getByText(item.title)).toBeInTheDocument();
|
||
expect(screen.getByText(item.description)).toBeInTheDocument();
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/layout/mega-dropdown.test.ts --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 MegaDropdown 组件**
|
||
|
||
```tsx
|
||
// src/components/layout/mega-dropdown.tsx
|
||
'use client';
|
||
|
||
import { useRef, useEffect } from 'react';
|
||
import { ChevronDown } from 'lucide-react';
|
||
import { AnimatePresence, motion } from 'framer-motion';
|
||
import { StaticLink } from '@/components/ui/static-link';
|
||
import type { MegaDropdownItem } from '@/lib/constants';
|
||
|
||
interface MegaDropdownProps {
|
||
label: string;
|
||
items: MegaDropdownItem[];
|
||
isOpen: boolean;
|
||
onToggle: () => void;
|
||
}
|
||
|
||
export function MegaDropdown({ label, items, isOpen, onToggle }: MegaDropdownProps) {
|
||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
function handleClickOutside(event: MouseEvent) {
|
||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||
if (isOpen) { onToggle(); }
|
||
}
|
||
}
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||
}, [isOpen, onToggle]);
|
||
|
||
return (
|
||
<div ref={dropdownRef} className="relative">
|
||
<button
|
||
onClick={onToggle}
|
||
className="flex items-center gap-1 text-sm font-medium text-[#1C1C1C] hover:text-[#C41E3A] transition-colors duration-200"
|
||
aria-expanded={isOpen}
|
||
aria-haspopup="true"
|
||
>
|
||
{label}
|
||
<ChevronDown
|
||
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||
/>
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -8 }}
|
||
transition={{ duration: 0.15 }}
|
||
className="absolute top-full left-0 mt-2 w-[480px] bg-white rounded-xl border border-[#E5E5E5] shadow-lg p-4 z-50"
|
||
>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{items.map((item) => (
|
||
<StaticLink
|
||
key={item.id}
|
||
href={item.href}
|
||
className="block p-3 rounded-lg border-l-[3px] border-l-[#C41E3A] hover:bg-[#FFFBF5] transition-colors duration-200"
|
||
>
|
||
<div className="text-sm font-semibold text-[#1C1C1C]">{item.title}</div>
|
||
<div className="text-xs text-[#5C5C5C] mt-1">{item.description}</div>
|
||
</StaticLink>
|
||
))}
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/layout/mega-dropdown.test.ts --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/layout/mega-dropdown.tsx src/components/layout/mega-dropdown.test.tsx
|
||
git commit -m "feat: add MegaDropdown component for product/solution matrix navigation"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 4:Header 重构
|
||
|
||
**文件:**
|
||
- 修改:`src/components/layout/header.tsx`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
在 `src/components/layout/header.test.tsx` 中追加测试:
|
||
|
||
```tsx
|
||
it('renders NAVIGATION_V2 items in desktop nav', () => {
|
||
render(<Header />);
|
||
expect(screen.getByText('产品')).toBeInTheDocument();
|
||
expect(screen.getByText('解决方案')).toBeInTheDocument();
|
||
expect(screen.getByText('服务')).toBeInTheDocument();
|
||
expect(screen.getByText('案例')).toBeInTheDocument();
|
||
expect(screen.getByText('关于我们')).toBeInTheDocument();
|
||
expect(screen.getByText('联系我们')).toBeInTheDocument();
|
||
});
|
||
|
||
it('does not render old navigation items (核心业务, 新闻动态)', () => {
|
||
render(<Header />);
|
||
expect(screen.queryByText('核心业务')).not.toBeInTheDocument();
|
||
expect(screen.queryByText('新闻动态')).not.toBeInTheDocument();
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/layout/header.test.tsx --no-coverage`
|
||
预期:FAIL — 新测试用例找不到"产品"等新导航项
|
||
|
||
- [ ] **步骤 3:重构 Header 组件**
|
||
|
||
在 `header.tsx` 中:
|
||
1. 将 `NAVIGATION` import 替换为 `NAVIGATION_V2, MEGA_DROPDOWN_DATA`
|
||
2. 将 `MegaDropdown` import 添加
|
||
3. 桌面导航渲染逻辑改为:遍历 `NAVIGATION_V2`,如果 `hasDropdown` 则渲染 `MegaDropdown`,否则渲染普通链接
|
||
4. 移除旧的 `NAVIGATION` 引用
|
||
5. 添加 `openDropdown` state 管理下拉开关
|
||
|
||
关键代码片段(替换桌面导航部分):
|
||
|
||
```tsx
|
||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||
|
||
// 在导航渲染中:
|
||
{NAVIGATION_V2.map((item) => (
|
||
item.hasDropdown ? (
|
||
<MegaDropdown
|
||
key={item.id}
|
||
label={item.label}
|
||
items={MEGA_DROPDOWN_DATA[item.dropdownKey!]}
|
||
isOpen={openDropdown === item.id}
|
||
onToggle={() => setOpenDropdown(openDropdown === item.id ? null : item.id)}
|
||
/>
|
||
) : (
|
||
<StaticLink
|
||
key={item.id}
|
||
href={item.href}
|
||
className={`text-sm font-medium transition-colors duration-200 ${
|
||
activeSection === item.id ? 'text-[#C41E3A]' : 'text-[#1C1C1C] hover:text-[#C41E3A]'
|
||
}`}
|
||
>
|
||
{item.label}
|
||
</StaticLink>
|
||
)
|
||
))}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/layout/header.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/layout/header.tsx src/components/layout/header.test.tsx
|
||
git commit -m "feat: refactor Header with NAVIGATION_V2 and MegaDropdown integration"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 5:Hero Section 重构
|
||
|
||
**文件:**
|
||
- 创建:`src/components/sections/hero-section-v2.tsx`
|
||
- 创建:`src/components/sections/hero-section-v2.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
```tsx
|
||
// src/components/sections/hero-section-v2.test.tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { HeroSectionV2 } from './hero-section-v2';
|
||
|
||
describe('HeroSectionV2', () => {
|
||
it('renders brand tag NOVALON', () => {
|
||
render(<HeroSectionV2 />);
|
||
expect(screen.getByText(/NOVALON/)).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders main heading with calligraphy', () => {
|
||
render(<HeroSectionV2 />);
|
||
const heading = screen.getByRole('heading', { level: 1 });
|
||
expect(heading).toBeInTheDocument();
|
||
expect(heading.textContent).toContain('智连未来');
|
||
});
|
||
|
||
it('renders two CTA buttons', () => {
|
||
render(<HeroSectionV2 />);
|
||
expect(screen.getByText('预约演示')).toBeInTheDocument();
|
||
expect(screen.getByText('了解方案')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders subtitle text', () => {
|
||
render(<HeroSectionV2 />);
|
||
expect(screen.getByText(/从战略规划到系统落地/)).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/sections/hero-section-v2.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 HeroSectionV2**
|
||
|
||
```tsx
|
||
// src/components/sections/hero-section-v2.tsx
|
||
'use client';
|
||
|
||
import { useEffect, useRef, useState } from 'react';
|
||
import { motion } from 'framer-motion';
|
||
import { StaticLink } from '@/components/ui/static-link';
|
||
|
||
export function HeroSectionV2() {
|
||
const [isVisible, setIsVisible] = useState(false);
|
||
const sectionRef = useRef<HTMLElement>(null);
|
||
|
||
useEffect(() => {
|
||
const observer = new IntersectionObserver(
|
||
([entry]) => {
|
||
if (entry?.isIntersecting) { setIsVisible(true); }
|
||
},
|
||
{ threshold: 0.1 }
|
||
);
|
||
if (sectionRef.current) { observer.observe(sectionRef.current); }
|
||
return () => observer.disconnect();
|
||
}, []);
|
||
|
||
return (
|
||
<section
|
||
id="home"
|
||
ref={sectionRef}
|
||
aria-labelledby="hero-heading"
|
||
className="relative min-h-[85vh] flex items-center overflow-hidden bg-gradient-to-b from-[#FFFBF5] to-white"
|
||
>
|
||
<div
|
||
className="absolute top-12 right-16 w-24 h-24 border border-[rgba(196,30,58,0.08)] rounded-full"
|
||
aria-hidden="true"
|
||
/>
|
||
<div
|
||
className="absolute bottom-16 left-10 w-18 h-18 border border-[rgba(196,30,58,0.06)] rounded-full"
|
||
aria-hidden="true"
|
||
/>
|
||
|
||
<div className="container-wide py-24 md:py-32 lg:py-40 relative z-10">
|
||
<div className="max-w-3xl mx-auto text-center">
|
||
<motion.p
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||
transition={{ duration: 0.6, delay: 0.1 }}
|
||
className="text-xs tracking-[0.2em] text-[#C41E3A] mb-6"
|
||
>
|
||
NOVALON · 睿新致遠
|
||
</motion.p>
|
||
|
||
<motion.h1
|
||
id="hero-heading"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||
transition={{ duration: 0.6, delay: 0.2 }}
|
||
className="font-calligraphy text-4xl md:text-5xl lg:text-6xl text-[#1C1C1C] tracking-[0.05em] mb-6"
|
||
>
|
||
智连未来,成长伙伴
|
||
</motion.h1>
|
||
|
||
<motion.p
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||
transition={{ duration: 0.6, delay: 0.3 }}
|
||
className="text-base md:text-lg text-[#5C5C5C] max-w-xl mx-auto mb-10"
|
||
>
|
||
从战略规划到系统落地,陪伴企业完成数字化转型的每一步
|
||
</motion.p>
|
||
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||
transition={{ duration: 0.6, delay: 0.4 }}
|
||
className="flex flex-col sm:flex-row gap-4 justify-center"
|
||
>
|
||
<StaticLink
|
||
href="/contact"
|
||
className="inline-flex items-center justify-center px-8 py-3 bg-[#C41E3A] text-white text-sm font-medium rounded hover:bg-[#A01830] transition-colors duration-200"
|
||
>
|
||
预约演示
|
||
</StaticLink>
|
||
<StaticLink
|
||
href="/solutions"
|
||
className="inline-flex items-center justify-center px-8 py-3 border border-[#1C1C1C] text-[#1C1C1C] text-sm font-medium rounded hover:bg-[#1C1C1C] hover:text-white transition-colors duration-200"
|
||
>
|
||
了解方案
|
||
</StaticLink>
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/sections/hero-section-v2.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/sections/hero-section-v2.tsx src/components/sections/hero-section-v2.test.tsx
|
||
git commit -m "feat: add HeroSectionV2 with brand fusion style and dual CTAs"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 6:StatsBar 组件
|
||
|
||
**文件:**
|
||
- 创建:`src/components/ui/stats-bar.tsx`
|
||
- 创建:`src/components/ui/stats-bar.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { StatsBar } from './stats-bar';
|
||
|
||
const mockStats = [
|
||
{ value: '200+', label: '企业客户' },
|
||
{ value: '15+', label: '行业覆盖' },
|
||
{ value: '99.9%', label: '系统可用性' },
|
||
];
|
||
|
||
describe('StatsBar', () => {
|
||
it('renders all stat items', () => {
|
||
render(<StatsBar stats={mockStats} />);
|
||
expect(screen.getByText('200+')).toBeInTheDocument();
|
||
expect(screen.getByText('15+')).toBeInTheDocument();
|
||
expect(screen.getByText('99.9%')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders all labels', () => {
|
||
render(<StatsBar stats={mockStats} />);
|
||
expect(screen.getByText('企业客户')).toBeInTheDocument();
|
||
expect(screen.getByText('行业覆盖')).toBeInTheDocument();
|
||
expect(screen.getByText('系统可用性')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/ui/stats-bar.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 StatsBar**
|
||
|
||
```tsx
|
||
// src/components/ui/stats-bar.tsx
|
||
'use client';
|
||
|
||
import { AnimatedNumber } from './animated-number';
|
||
|
||
export interface StatItem {
|
||
value: string;
|
||
label: string;
|
||
}
|
||
|
||
interface StatsBarProps {
|
||
stats: StatItem[];
|
||
}
|
||
|
||
export function StatsBar({ stats }: StatsBarProps) {
|
||
return (
|
||
<div className="grid grid-cols-3 gap-8">
|
||
{stats.map((stat) => (
|
||
<div key={stat.label} className="text-center">
|
||
<div className="text-3xl md:text-4xl font-bold text-[#C41E3A]">
|
||
{stat.value}
|
||
</div>
|
||
<div className="text-sm text-[#5C5C5C] mt-1">{stat.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/ui/stats-bar.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/ui/stats-bar.tsx src/components/ui/stats-bar.test.tsx
|
||
git commit -m "feat: add StatsBar component for social proof data display"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 7:社会证明 Section
|
||
|
||
**文件:**
|
||
- 创建:`src/components/sections/social-proof-section.tsx`
|
||
- 创建:`src/components/sections/social-proof-section.test.tsx`
|
||
- 修改:`src/lib/constants/stats.ts`
|
||
|
||
- [ ] **步骤 1:在 stats.ts 中新增 HOME_STATS**
|
||
|
||
```typescript
|
||
export const HOME_STATS: StatItem[] = [
|
||
{ value: '200+', label: '企业客户' },
|
||
{ value: '15+', label: '行业覆盖' },
|
||
{ value: '99.9%', label: '系统可用性' },
|
||
];
|
||
```
|
||
|
||
- [ ] **步骤 2:编写测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { SocialProofSection } from './social-proof-section';
|
||
|
||
describe('SocialProofSection', () => {
|
||
it('renders section with stats', () => {
|
||
render(<SocialProofSection />);
|
||
expect(screen.getByText('200+')).toBeInTheDocument();
|
||
expect(screen.getByText('企业客户')).toBeInTheDocument();
|
||
});
|
||
|
||
it('has correct background class', () => {
|
||
const { container } = render(<SocialProofSection />);
|
||
const section = container.querySelector('section');
|
||
expect(section?.className).toContain('bg-[#F5F5F5]');
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 3:实现 SocialProofSection**
|
||
|
||
```tsx
|
||
// src/components/sections/social-proof-section.tsx
|
||
'use client';
|
||
|
||
import { StatsBar } from '@/components/ui/stats-bar';
|
||
import { HOME_STATS } from '@/lib/constants/stats';
|
||
|
||
export function SocialProofSection() {
|
||
return (
|
||
<section className="bg-[#F5F5F5] py-16 md:py-20" aria-label="社会证明">
|
||
<div className="container-wide">
|
||
<StatsBar stats={HOME_STATS} />
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/sections/social-proof-section.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/sections/social-proof-section.tsx src/components/sections/social-proof-section.test.tsx src/lib/constants/stats.ts
|
||
git commit -m "feat: add SocialProofSection with HOME_STATS data"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 8:ProductCard 组件
|
||
|
||
**文件:**
|
||
- 创建:`src/components/ui/product-card.tsx`
|
||
- 创建:`src/components/ui/product-card.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { ProductCard } from './product-card';
|
||
|
||
const mockProduct = {
|
||
id: 'erp',
|
||
title: 'ERP 管理系统',
|
||
description: '财务·采购·销售·库存·生产',
|
||
href: '/products/erp',
|
||
};
|
||
|
||
describe('ProductCard', () => {
|
||
it('renders product title', () => {
|
||
render(<ProductCard {...mockProduct} />);
|
||
expect(screen.getByText('ERP 管理系统')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders product description', () => {
|
||
render(<ProductCard {...mockProduct} />);
|
||
expect(screen.getByText('财务·采购·销售·库存·生产')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders link to product page', () => {
|
||
render(<ProductCard {...mockProduct} />);
|
||
const link = screen.getByRole('link');
|
||
expect(link).toHaveAttribute('href', '/products/erp');
|
||
});
|
||
|
||
it('has left border accent', () => {
|
||
const { container } = render(<ProductCard {...mockProduct} />);
|
||
const card = container.firstChild as HTMLElement;
|
||
expect(card.className).toContain('border-l-[3px]');
|
||
expect(card.className).toContain('border-l-[#C41E3A]');
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/ui/product-card.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 ProductCard**
|
||
|
||
```tsx
|
||
// src/components/ui/product-card.tsx
|
||
import { StaticLink } from '@/components/ui/static-link';
|
||
|
||
interface ProductCardProps {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
href: string;
|
||
}
|
||
|
||
export function ProductCard({ title, description, href }: ProductCardProps) {
|
||
return (
|
||
<StaticLink
|
||
href={href}
|
||
className="block p-5 rounded-lg border border-[#E5E5E5] border-l-[3px] border-l-[#C41E3A] bg-white hover:bg-[#FFFBF5] hover:shadow-md hover:-translate-y-0.5 transition-all duration-200"
|
||
>
|
||
<h3 className="text-base font-semibold text-[#1C1C1C]">{title}</h3>
|
||
<p className="text-sm text-[#5C5C5C] mt-1.5">{description}</p>
|
||
</StaticLink>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/ui/product-card.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/ui/product-card.tsx src/components/ui/product-card.test.tsx
|
||
git commit -m "feat: add ProductCard component with brand-red left border"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 9:产品矩阵 Section
|
||
|
||
**文件:**
|
||
- 创建:`src/components/sections/product-matrix-section.tsx`
|
||
- 创建:`src/components/sections/product-matrix-section.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { ProductMatrixSection } from './product-matrix-section';
|
||
|
||
describe('ProductMatrixSection', () => {
|
||
it('renders section heading', () => {
|
||
render(<ProductMatrixSection />);
|
||
expect(screen.getByText('核心产品')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders all 4 products', () => {
|
||
render(<ProductMatrixSection />);
|
||
expect(screen.getByText('ERP 管理系统')).toBeInTheDocument();
|
||
expect(screen.getByText('CRM 客户管理')).toBeInTheDocument();
|
||
expect(screen.getByText('BI 数据平台')).toBeInTheDocument();
|
||
expect(screen.getByText('CMS 内容平台')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/sections/product-matrix-section.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 ProductMatrixSection**
|
||
|
||
```tsx
|
||
// src/components/sections/product-matrix-section.tsx
|
||
'use client';
|
||
|
||
import { ProductCard } from '@/components/ui/product-card';
|
||
import { PRODUCTS } from '@/lib/constants/products';
|
||
|
||
export function ProductMatrixSection() {
|
||
return (
|
||
<section className="bg-white py-20 md:py-24" aria-label="核心产品">
|
||
<div className="container-wide">
|
||
<h2 className="text-2xl md:text-3xl font-bold text-[#1C1C1C] mb-10">核心产品</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{PRODUCTS.map((product) => (
|
||
<ProductCard
|
||
key={product.id}
|
||
id={product.id}
|
||
title={product.title}
|
||
description={product.description.split(',')[0]}
|
||
href={`/products/${product.id}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/sections/product-matrix-section.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/sections/product-matrix-section.tsx src/components/sections/product-matrix-section.test.tsx
|
||
git commit -m "feat: add ProductMatrixSection with 2x2 product card grid"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 10:ChallengeCard 组件
|
||
|
||
**文件:**
|
||
- 创建:`src/components/ui/challenge-card.tsx`
|
||
- 创建:`src/components/ui/challenge-card.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { ChallengeCard } from './challenge-card';
|
||
|
||
const mockChallenge = {
|
||
id: 'isolation',
|
||
icon: '🏭',
|
||
title: '系统孤岛',
|
||
subtitle: '数据不通',
|
||
bgColor: '#FEF2F4',
|
||
hoverBgColor: '#FDE8EC',
|
||
href: '/solutions/manufacturing',
|
||
};
|
||
|
||
describe('ChallengeCard', () => {
|
||
it('renders icon', () => {
|
||
render(<ChallengeCard {...mockChallenge} />);
|
||
expect(screen.getByText('🏭')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders title and subtitle', () => {
|
||
render(<ChallengeCard {...mockChallenge} />);
|
||
expect(screen.getByText('系统孤岛')).toBeInTheDocument();
|
||
expect(screen.getByText('数据不通')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders link to solution page', () => {
|
||
render(<ChallengeCard {...mockChallenge} />);
|
||
const link = screen.getByRole('link');
|
||
expect(link).toHaveAttribute('href', '/solutions/manufacturing');
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/ui/challenge-card.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 ChallengeCard**
|
||
|
||
```tsx
|
||
// src/components/ui/challenge-card.tsx
|
||
import { StaticLink } from '@/components/ui/static-link';
|
||
|
||
interface ChallengeCardProps {
|
||
id: string;
|
||
icon: string;
|
||
title: string;
|
||
subtitle: string;
|
||
bgColor: string;
|
||
hoverBgColor: string;
|
||
href: string;
|
||
}
|
||
|
||
export function ChallengeCard({ icon, title, subtitle, bgColor, hoverBgColor, href }: ChallengeCardProps) {
|
||
return (
|
||
<StaticLink
|
||
href={href}
|
||
className="block p-6 rounded-xl text-center transition-all duration-200 hover:shadow-md hover:-translate-y-0.5"
|
||
style={{ backgroundColor: bgColor }}
|
||
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = hoverBgColor; }}
|
||
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = bgColor; }}
|
||
>
|
||
<div className="text-3xl mb-3">{icon}</div>
|
||
<h3 className="text-sm font-bold text-[#1C1C1C]">{title}</h3>
|
||
<p className="text-xs text-[#C41E3A] mt-1">{subtitle}</p>
|
||
</StaticLink>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/ui/challenge-card.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/ui/challenge-card.tsx src/components/ui/challenge-card.test.tsx
|
||
git commit -m "feat: add ChallengeCard component for scenario entry points"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 11:挑战场景 Section
|
||
|
||
**文件:**
|
||
- 创建:`src/components/sections/challenge-section.tsx`
|
||
- 创建:`src/components/sections/challenge-section.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { ChallengeSection } from './challenge-section';
|
||
|
||
describe('ChallengeSection', () => {
|
||
it('renders section heading', () => {
|
||
render(<ChallengeSection />);
|
||
expect(screen.getByText('您面临什么挑战?')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders three challenge cards', () => {
|
||
render(<ChallengeSection />);
|
||
expect(screen.getByText('系统孤岛')).toBeInTheDocument();
|
||
expect(screen.getByText('增长瓶颈')).toBeInTheDocument();
|
||
expect(screen.getByText('合规风险')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/sections/challenge-section.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 ChallengeSection**
|
||
|
||
```tsx
|
||
// src/components/sections/challenge-section.tsx
|
||
'use client';
|
||
|
||
import { ChallengeCard } from '@/components/ui/challenge-card';
|
||
|
||
const CHALLENGES = [
|
||
{
|
||
id: 'isolation',
|
||
icon: '🏭',
|
||
title: '系统孤岛',
|
||
subtitle: '数据不通',
|
||
bgColor: 'var(--color-challenge-isolation)',
|
||
hoverBgColor: 'var(--color-challenge-isolation-hover)',
|
||
href: '/solutions/manufacturing',
|
||
},
|
||
{
|
||
id: 'growth',
|
||
icon: '📈',
|
||
title: '增长瓶颈',
|
||
subtitle: '效率低下',
|
||
bgColor: 'var(--color-challenge-growth)',
|
||
hoverBgColor: 'var(--color-challenge-growth-hover)',
|
||
href: '/solutions/retail',
|
||
},
|
||
{
|
||
id: 'compliance',
|
||
icon: '🔒',
|
||
title: '合规风险',
|
||
subtitle: '安全隐忧',
|
||
bgColor: 'var(--color-challenge-compliance)',
|
||
hoverBgColor: 'var(--color-challenge-compliance-hover)',
|
||
href: '/solutions/healthcare',
|
||
},
|
||
];
|
||
|
||
export function ChallengeSection() {
|
||
return (
|
||
<section className="bg-[#FFFBF5] py-20 md:py-24" aria-label="挑战与场景">
|
||
<div className="container-wide">
|
||
<h2 className="text-2xl md:text-3xl font-bold text-[#1C1C1C] mb-10">您面临什么挑战?</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
{CHALLENGES.map((challenge) => (
|
||
<ChallengeCard key={challenge.id} {...challenge} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/sections/challenge-section.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/sections/challenge-section.tsx src/components/sections/challenge-section.test.tsx
|
||
git commit -m "feat: add ChallengeSection with three scenario entry cards"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 12:TestimonialBlock 组件
|
||
|
||
**文件:**
|
||
- 创建:`src/components/ui/testimonial-block.tsx`
|
||
- 创建:`src/components/ui/testimonial-block.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { TestimonialBlock } from './testimonial-block';
|
||
|
||
const mockTestimonial = {
|
||
quote: '与睿新合作后,运营效率提升了40%',
|
||
author: '某上市制造企业 CTO',
|
||
results: [
|
||
{ value: '40%', label: '效率提升' },
|
||
{ value: '6月', label: '交付周期' },
|
||
],
|
||
};
|
||
|
||
describe('TestimonialBlock', () => {
|
||
it('renders quote text', () => {
|
||
render(<TestimonialBlock {...mockTestimonial} />);
|
||
expect(screen.getByText(/运营效率提升了40%/)).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders author attribution', () => {
|
||
render(<TestimonialBlock {...mockTestimonial} />);
|
||
expect(screen.getByText(/某上市制造企业 CTO/)).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders result metrics', () => {
|
||
render(<TestimonialBlock {...mockTestimonial} />);
|
||
expect(screen.getByText('40%')).toBeInTheDocument();
|
||
expect(screen.getByText('效率提升')).toBeInTheDocument();
|
||
expect(screen.getByText('6月')).toBeInTheDocument();
|
||
expect(screen.getByText('交付周期')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/ui/testimonial-block.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 TestimonialBlock**
|
||
|
||
```tsx
|
||
// src/components/ui/testimonial-block.tsx
|
||
interface TestimonialResult {
|
||
value: string;
|
||
label: string;
|
||
}
|
||
|
||
interface TestimonialBlockProps {
|
||
quote: string;
|
||
author: string;
|
||
results: TestimonialResult[];
|
||
}
|
||
|
||
export function TestimonialBlock({ quote, author, results }: TestimonialBlockProps) {
|
||
return (
|
||
<div className="bg-[#1C1C1C] rounded-xl p-8 md:p-10">
|
||
<p className="text-xs text-[rgba(255,255,255,0.6)] mb-4">客户成果</p>
|
||
<blockquote className="text-base md:text-lg text-white italic mb-4">
|
||
“{quote}”
|
||
</blockquote>
|
||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||
<p className="text-xs text-[rgba(255,255,255,0.5)]">— {author}</p>
|
||
<div className="flex gap-6">
|
||
{results.map((result) => (
|
||
<div key={result.label} className="text-center">
|
||
<div className="text-lg font-bold text-[#C41E3A]">{result.value}</div>
|
||
<div className="text-[10px] text-[rgba(255,255,255,0.4)]">{result.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/ui/testimonial-block.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/ui/testimonial-block.tsx src/components/ui/testimonial-block.test.tsx
|
||
git commit -m "feat: add TestimonialBlock component with dark background and metrics"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 13:客户成果 Section
|
||
|
||
**文件:**
|
||
- 创建:`src/components/sections/testimonial-section.tsx`
|
||
- 创建:`src/components/sections/testimonial-section.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { TestimonialSection } from './testimonial-section';
|
||
|
||
describe('TestimonialSection', () => {
|
||
it('renders client testimonial quote', () => {
|
||
render(<TestimonialSection />);
|
||
expect(screen.getByText(/客户成果/)).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/sections/testimonial-section.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 TestimonialSection**
|
||
|
||
```tsx
|
||
// src/components/sections/testimonial-section.tsx
|
||
'use client';
|
||
|
||
import { TestimonialBlock } from '@/components/ui/testimonial-block';
|
||
import { CASES } from '@/lib/constants/cases';
|
||
|
||
export function TestimonialSection() {
|
||
const featuredCase = CASES.find(c => c.testimonial) || CASES[0];
|
||
|
||
const testimonial = featuredCase.testimonial
|
||
? {
|
||
quote: featuredCase.testimonial.quote,
|
||
author: `${featuredCase.testimonial.author},${featuredCase.testimonial.role}`,
|
||
results: featuredCase.results.slice(0, 3),
|
||
}
|
||
: {
|
||
quote: featuredCase.description.slice(0, 60) + '...',
|
||
author: featuredCase.client,
|
||
results: featuredCase.results.slice(0, 3),
|
||
};
|
||
|
||
return (
|
||
<section className="bg-[#1C1C1C] py-20 md:py-24" aria-label="客户成果">
|
||
<div className="container-wide">
|
||
<TestimonialBlock {...testimonial} />
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/sections/testimonial-section.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/sections/testimonial-section.tsx src/components/sections/testimonial-section.test.tsx
|
||
git commit -m "feat: add TestimonialSection using CASES data for client stories"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 14:CTASection 组件
|
||
|
||
**文件:**
|
||
- 创建:`src/components/ui/cta-section.tsx`
|
||
- 创建:`src/components/ui/cta-section.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写失败的测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { CTASection } from './cta-section';
|
||
|
||
describe('CTASection', () => {
|
||
it('renders heading', () => {
|
||
render(<CTASection heading="开启数字化转型之旅" />);
|
||
expect(screen.getByText('开启数字化转型之旅')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders subtitle when provided', () => {
|
||
render(<CTASection heading="开启" subtitle="免费咨询 · 专属方案" />);
|
||
expect(screen.getByText('免费咨询 · 专属方案')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders CTA button', () => {
|
||
render(<CTASection heading="开启" buttonText="立即咨询" buttonHref="/contact" />);
|
||
const link = screen.getByText('立即咨询');
|
||
expect(link).toBeInTheDocument();
|
||
expect(link.closest('a')).toHaveAttribute('href', '/contact');
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/ui/cta-section.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 CTASection**
|
||
|
||
```tsx
|
||
// src/components/ui/cta-section.tsx
|
||
import { StaticLink } from '@/components/ui/static-link';
|
||
|
||
interface CTASectionProps {
|
||
heading: string;
|
||
subtitle?: string;
|
||
buttonText?: string;
|
||
buttonHref?: string;
|
||
}
|
||
|
||
export function CTASection({
|
||
heading,
|
||
subtitle,
|
||
buttonText = '立即咨询',
|
||
buttonHref = '/contact',
|
||
}: CTASectionProps) {
|
||
return (
|
||
<div className="bg-gradient-to-br from-[#C41E3A] to-[#A01830] rounded-xl p-10 md:p-14 text-center">
|
||
<h2 className="text-xl md:text-2xl font-bold text-white mb-3">{heading}</h2>
|
||
{subtitle && (
|
||
<p className="text-sm text-[rgba(255,255,255,0.8)] mb-8">{subtitle}</p>
|
||
)}
|
||
<StaticLink
|
||
href={buttonHref}
|
||
className="inline-flex items-center justify-center px-8 py-3 border border-white text-white text-sm font-medium rounded hover:bg-white hover:text-[#C41E3A] transition-colors duration-200"
|
||
>
|
||
{buttonText}
|
||
</StaticLink>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/ui/cta-section.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/ui/cta-section.tsx src/components/ui/cta-section.test.tsx
|
||
git commit -m "feat: add CTASection component with brand-red gradient"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 15:CTA Section 页面级
|
||
|
||
**文件:**
|
||
- 创建:`src/components/sections/cta-section-page.tsx`
|
||
- 创建:`src/components/sections/cta-section-page.test.tsx`
|
||
|
||
- [ ] **步骤 1:编写测试**
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import { CTASectionPage } from './cta-section-page';
|
||
|
||
describe('CTASectionPage', () => {
|
||
it('renders CTA with correct content', () => {
|
||
render(<CTASectionPage />);
|
||
expect(screen.getByText('开启数字化转型之旅')).toBeInTheDocument();
|
||
expect(screen.getByText('免费咨询 · 专属方案 · 30天试用')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/components/sections/cta-section-page.test.tsx --no-coverage`
|
||
预期:FAIL
|
||
|
||
- [ ] **步骤 3:实现 CTASectionPage**
|
||
|
||
```tsx
|
||
// src/components/sections/cta-section-page.tsx
|
||
import { CTASection } from '@/components/ui/cta-section';
|
||
|
||
export function CTASectionPage() {
|
||
return (
|
||
<section className="py-20 md:py-24" aria-label="行动号召">
|
||
<div className="container-wide">
|
||
<CTASection
|
||
heading="开启数字化转型之旅"
|
||
subtitle="免费咨询 · 专属方案 · 30天试用"
|
||
buttonText="立即咨询"
|
||
buttonHref="/contact"
|
||
/>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/components/sections/cta-section-page.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/components/sections/cta-section-page.tsx src/components/sections/cta-section-page.test.tsx
|
||
git commit -m "feat: add CTASectionPage with homepage-specific CTA content"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 16:首页组装
|
||
|
||
**文件:**
|
||
- 修改:`src/app/(marketing)/home-content.tsx`
|
||
|
||
- [ ] **步骤 1:编写测试**
|
||
|
||
在 `src/app/(marketing)/page.test.tsx`(如不存在则新建)中:
|
||
|
||
```tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { describe, it, expect } from 'vitest';
|
||
import HomeContent from './home-content';
|
||
|
||
describe('HomeContent V2', () => {
|
||
it('renders new hero section', () => {
|
||
render(<HomeContent heroStats={null} />);
|
||
expect(screen.getByText('智连未来,成长伙伴')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders product matrix section', () => {
|
||
render(<HomeContent heroStats={null} />);
|
||
expect(screen.getByText('核心产品')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders challenge section', () => {
|
||
render(<HomeContent heroStats={null} />);
|
||
expect(screen.getByText('您面临什么挑战?')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders CTA section', () => {
|
||
render(<HomeContent heroStats={null} />);
|
||
expect(screen.getByText('开启数字化转型之旅')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:运行测试验证失败**
|
||
|
||
运行:`npx jest src/app/\(marketing\)/page.test.tsx --no-coverage`
|
||
预期:FAIL — 找不到新 Section 内容
|
||
|
||
- [ ] **步骤 3:重构 home-content.tsx**
|
||
|
||
替换 `home-content.tsx` 的全部内容:
|
||
|
||
```tsx
|
||
'use client';
|
||
|
||
import dynamic from 'next/dynamic';
|
||
import { HeroSectionV2 } from '@/components/sections/hero-section-v2';
|
||
import { SectionSkeleton } from '@/components/ui/loading-skeleton';
|
||
import type { ReactNode } from 'react';
|
||
|
||
const SocialProofSection = dynamic(
|
||
() => import('@/components/sections/social-proof-section').then(mod => ({ default: mod.SocialProofSection })),
|
||
{ loading: () => <SectionSkeleton />, ssr: false }
|
||
);
|
||
|
||
const ProductMatrixSection = dynamic(
|
||
() => import('@/components/sections/product-matrix-section').then(mod => ({ default: mod.ProductMatrixSection })),
|
||
{ loading: () => <SectionSkeleton />, ssr: false }
|
||
);
|
||
|
||
const ChallengeSection = dynamic(
|
||
() => import('@/components/sections/challenge-section').then(mod => ({ default: mod.ChallengeSection })),
|
||
{ loading: () => <SectionSkeleton />, ssr: false }
|
||
);
|
||
|
||
const TestimonialSection = dynamic(
|
||
() => import('@/components/sections/testimonial-section').then(mod => ({ default: mod.TestimonialSection })),
|
||
{ loading: () => <SectionSkeleton />, ssr: false }
|
||
);
|
||
|
||
const CTASectionPage = dynamic(
|
||
() => import('@/components/sections/cta-section-page').then(mod => ({ default: mod.CTASectionPage })),
|
||
{ loading: () => <SectionSkeleton />, ssr: false }
|
||
);
|
||
|
||
function HomeContent({ heroStats }: { heroStats: ReactNode }) {
|
||
return (
|
||
<>
|
||
<HeroSectionV2 />
|
||
<SocialProofSection />
|
||
<ProductMatrixSection />
|
||
<ChallengeSection />
|
||
<TestimonialSection />
|
||
<CTASectionPage />
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default HomeContent;
|
||
```
|
||
|
||
- [ ] **步骤 4:运行测试验证通过**
|
||
|
||
运行:`npx jest src/app/\(marketing\)/page.test.tsx --no-coverage`
|
||
预期:PASS
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add src/app/\(marketing\)/home-content.tsx src/app/\(marketing\)/page.test.tsx
|
||
git commit -m "feat: assemble homepage with new brand-fusion sections"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 17:废弃动效清理
|
||
|
||
**文件:**
|
||
- 修改:`src/components/effects/index.ts`
|
||
|
||
- [ ] **步骤 1:从 effects/index.ts 移除废弃组件导出**
|
||
|
||
移除以下导出:
|
||
- `InkBackground` / `InkTechFusion`
|
||
- `DataParticleFlow`
|
||
- `ParticleGalaxy`
|
||
- `MouseInteractiveParticles`
|
||
- `FluidWaveBackground`
|
||
- `GeometricAbstract`
|
||
- `GridLines`
|
||
- `AdvancedFloatingEffects`
|
||
|
||
保留以下导出:
|
||
- `SubtleDots`
|
||
- `GradientFlow` / `GradientFlowOptimized`
|
||
- `GradientOrbs`
|
||
- `GlowEffect`
|
||
- `ParallaxEffect`
|
||
- `SealAnimationEnhanced`
|
||
|
||
- [ ] **步骤 2:验证构建无报错**
|
||
|
||
运行:`npx tsc --noEmit`
|
||
预期:无类型错误(如果有引用废弃组件的文件,需更新引用)
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add src/components/effects/index.ts
|
||
git commit -m "chore: remove deprecated particle/ink effect exports from index"
|
||
```
|
||
|
||
---
|
||
|
||
## 任务 18:集成测试与验证
|
||
|
||
- [ ] **步骤 1:运行全量单元测试**
|
||
|
||
运行:`npx jest --no-coverage`
|
||
预期:所有测试 PASS
|
||
|
||
- [ ] **步骤 2:运行类型检查**
|
||
|
||
运行:`npx tsc --noEmit`
|
||
预期:无类型错误
|
||
|
||
- [ ] **步骤 3:运行构建**
|
||
|
||
运行:`npm run build`
|
||
预期:构建成功
|
||
|
||
- [ ] **步骤 4:启动开发服务器,手动验证首页**
|
||
|
||
运行:`npm run dev`
|
||
验证项:
|
||
1. 导航栏显示 6 项,"产品"和"解决方案"有下拉
|
||
2. Hero 白底 + 书法标题 + 双 CTA
|
||
3. 社会证明三列数字
|
||
4. 产品矩阵 2×2 卡片 + 朱砂红左边框
|
||
5. 挑战场景三列卡片 + 场景色
|
||
6. 客户成果深色背景 + 引言
|
||
7. CTA 朱砂红渐变
|
||
8. 无粒子/水墨动效
|
||
|
||
- [ ] **步骤 5:运行 Lighthouse 审计**
|
||
|
||
运行:`npm run lighthouse:mobile`
|
||
预期:Performance ≥ 90, Accessibility ≥ 95
|
||
|
||
- [ ] **步骤 6:最终 Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "feat: complete Phase 1 of Atlassian-style brand fusion homepage redesign"
|
||
```
|
||
|
||
---
|
||
|
||
## 自检清单
|
||
|
||
| 检查项 | 状态 |
|
||
|--------|------|
|
||
| 规格中每个 Section 都有对应任务 | ✅ Hero/SocialProof/ProductMatrix/Challenge/Testimonial/CTA |
|
||
| 规格中每个组件都有对应任务 | ✅ MegaDropdown/ProductCard/ChallengeCard/StatsBar/TestimonialBlock/CTASection |
|
||
| 无占位符(TODO/TBD/待定) | ✅ 所有步骤包含完整代码 |
|
||
| 类型一致性 | ✅ NavigationItemV2/MegaDropdownItem/StatItem 等类型定义明确 |
|
||
| 废弃动效清理有对应任务 | ✅ 任务 17 |
|
||
| 集成验证有对应任务 | ✅ 任务 18 |
|