Files
novalon-website/docs/plans/2026-02-26-website-feature-completion-implementation.md
T
张翔 3de9890fc4 fix: 修复TypeScript类型错误
- 移除未使用的导入
- 修复产品详情页面的description类型错误
- 修复服务详情页面的description类型错误
- 修复联系表单API的类型错误
- 添加Award图标的导入
2026-02-26 16:26:40 +08:00

1421 lines
47 KiB
Markdown

# 网站功能补全实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use subagent-driven-development to implement this plan task-by-task.
**Goal:** 完善Novalon网站功能,包括产品详情页面、服务详情页面、联系表单邮件服务集成、隐私政策和服务条款页面
**Architecture:**
- 使用Next.js 16 App Router创建动态路由页面
- 使用Resend API集成邮件服务
- 扩展constants.ts中的数据结构
- 保持与现有页面一致的视觉风格
**Tech Stack:**
- Next.js 16.1.6 + React 19.2.3 + TypeScript 5
- Tailwind CSS v4 + CSS Variables
- shadcn/ui组件库
- Resend邮件服务(每月3000封免费)
- Zod表单验证
---
## 阶段一:数据准备
### Task 1: 扩展PRODUCTS数据结构
**Files:**
- Modify: `src/lib/constants.ts:112-154`
**Step 1: 查看当前PRODUCTS数据结构**
Read `src/lib/constants.ts` lines 112-154 to understand current structure.
Expected: See current PRODUCTS array with id, title, description, category, features, benefits.
**Step 2: 扩展PRODUCTS数据结构**
为每个产品添加以下字段:
- `technicalArchitecture`: { frontend: string[], backend: string[], database: string[], deployment: string[] }
- `scenarios`: { title: string, description: string }[]
- `cases`: { client: string, industry: string, results: { label: string, value: string }[] }[]
- `fullDescription`: string (用于详情页的完整描述)
- `keyFeatures`: string[] (详细功能列表)
**Step 3: 提供完整的产品数据**
为ERP、CRM、CMS、BI四个产品提供完整数据,包括技术架构、应用场景、客户案例等。
Expected: PRODUCTS数组包含完整的产品详细信息。
**Step 4: 验证数据结构**
```bash
npm run dev
```
Expected: 开发服务器启动,没有TypeScript错误。
**Step 5: Commit**
```bash
git add src/lib/constants.ts
git commit -m "feat: extend PRODUCTS data structure with technical architecture and cases"
```
---
### Task 2: 扩展SERVICES数据结构
**Files:**
- Modify: `src/lib/constants.ts:73-110`
**Step 1: 查看当前SERVICES数据结构**
Read `src/lib/constants.ts` lines 73-110 to understand current structure.
Expected: See current SERVICES array with id, title, description, icon.
**Step 2: 扩展SERVICES数据结构**
为每个服务添加以下字段:
- `content`: { scope: string, process: { phase: string, description: string }[], deliverables: string[] }
- `advantages`: { label: string, value: string }[]
- `fullDescription`: string (用于详情页的完整描述)
- `relatedServices`: string[] (相关服务ID列表)
**Step 3: 提供完整的服务数据**
为软件开发、云服务、数据分析、信息安全四个服务提供完整数据,包括服务内容、优势、流程等。
Expected: SERVICES数组包含完整的服务详细信息。
**Step 4: 验证数据结构**
```bash
npm run dev
```
Expected: 开发服务器启动,没有TypeScript错误。
**Step 5: Commit**
```bash
git add src/lib/constants.ts
git commit -m "feat: extend SERVICES data structure with content, advantages, and process"
```
---
## 阶段二:页面开发
### Task 3: 创建产品详情页面组件
**Files:**
- Create: `src/app/(marketing)/products/[id]/page.tsx`
**Step 1: 创建产品详情页面组件**
```typescript
'use client';
import { PRODUCTS } from '@/lib/constants';
import { notFound } from 'next/navigation';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowLeft, Check, Code, Database, Cloud, Server } from 'lucide-react';
import Link from 'next/link';
interface ProductDetailPageProps {
params: Promise<{
id: string;
}>;
}
export function generateStaticParams() {
return PRODUCTS.map((product) => ({
id: product.id,
}));
}
export async function generateMetadata({ params }: ProductDetailPageProps) {
const { id } = await params;
const product = PRODUCTS.find((p) => p.id === id);
if (!product) {
return {
title: `产品未找到`,
};
}
return {
title: `${product.title} - 产品详情`,
description: product.description,
};
}
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
const { id } = await params;
const product = PRODUCTS.find((p) => p.id === id);
if (!product) {
notFound();
}
return (
<div className="pt-32 pb-20">
<div className="container-custom">
<div className="max-w-5xl mx-auto">
{/* 返回按钮 */}
<Button variant="ghost" asChild className="mb-8">
<Link href="/products">
<ArrowLeft className="mr-2 w-4 h-4" />
</Link>
</Button>
{/* 产品标题 */}
<div className="mb-8">
<Badge variant="secondary" className="mb-4">{product.category}</Badge>
<h1 className="text-4xl sm:text-5xl font-bold text-black mb-4">{product.title}</h1>
<p className="text-xl text-gray-600">{product.fullDescription}</p>
</div>
{/* 功能特性 */}
<Card className="mb-8">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{product.features.map((feature, idx) => (
<div key={idx} className="flex items-start gap-2">
<Check className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-700">{feature}</span>
</div>
))}
</div>
</CardContent>
</Card>
{/* 技术架构 */}
<Card className="mb-8">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h3 className="font-semibold text-black mb-2 flex items-center">
<Code className="w-5 h-5 mr-2 text-blue-600" />
</h3>
<div className="flex flex-wrap gap-2">
{product.technicalArchitecture.frontend.map((tech, idx) => (
<Badge key={idx} variant="outline">{tech}</Badge>
))}
</div>
</div>
<div>
<h3 className="font-semibold text-black mb-2 flex items-center">
<Server className="w-5 h-5 mr-2 text-purple-600" />
</h3>
<div className="flex flex-wrap gap-2">
{product.technicalArchitecture.backend.map((tech, idx) => (
<Badge key={idx} variant="outline">{tech}</Badge>
))}
</div>
</div>
<div>
<h3 className="font-semibold text-black mb-2 flex items-center">
<Database className="w-5 h-5 mr-2 text-orange-600" />
</h3>
<div className="flex flex-wrap gap-2">
{product.technicalArchitecture.database.map((tech, idx) => (
<Badge key={idx} variant="outline">{tech}</Badge>
))}
</div>
</div>
<div>
<h3 className="font-semibold text-black mb-2 flex items-center">
<Cloud className="w-5 h-5 mr-2 text-cyan-600" />
</h3>
<div className="flex flex-wrap gap-2">
{product.technicalArchitecture.deployment.map((tech, idx) => (
<Badge key={idx} variant="outline">{tech}</Badge>
))}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 应用场景 */}
<Card className="mb-8">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{product.scenarios.map((scenario, idx) => (
<div key={idx} className="border-l-4 border-black pl-4">
<h3 className="font-semibold text-black mb-1">{scenario.title}</h3>
<p className="text-gray-600">{scenario.description}</p>
</div>
))}
</CardContent>
</Card>
{/* 客户案例 */}
<Card className="mb-8">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{product.cases.map((caseItem, idx) => (
<div key={idx} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Badge variant="secondary">{caseItem.industry}</Badge>
</div>
<h3 className="font-semibold text-black mb-3">{caseItem.client}</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{caseItem.results.map((result, resIdx) => (
<div key={resIdx} className="text-center">
<div className="text-2xl font-bold text-black">{result.value}</div>
<div className="text-sm text-gray-600">{result.label}</div>
</div>
))}
</div>
</div>
))}
</CardContent>
</Card>
{/* 底部CTA */}
<div className="text-center">
<Button size="lg" className="bg-black text-white hover:bg-gray-800">
<ArrowLeft className="ml-2 w-4 h-4 rotate-180" />
</Button>
</div>
</div>
</div>
</div>
);
}
```
**Step 2: 验证页面**
```bash
npm run dev
```
Expected: 访问 `/products/erp` 能正常显示产品详情。
**Step 3: Commit**
```bash
git add src/app/(marketing)/products/[id]/page.tsx
git commit -m "feat: add product detail page with features, architecture, scenarios, and cases"
```
---
### Task 4: 创建服务详情页面组件
**Files:**
- Create: `src/app/(marketing)/services/[id]/page.tsx`
**Step 1: 创建服务详情页面组件**
```typescript
'use client';
import { SERVICES } from '@/lib/constants';
import { notFound } from 'next/navigation';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowLeft, Check, Clock } from 'lucide-react';
import Link from 'next/link';
interface ServiceDetailPageProps {
params: Promise<{
id: string;
}>;
}
export function generateStaticParams() {
return SERVICES.map((service) => ({
id: service.id,
}));
}
export async function generateMetadata({ params }: ServiceDetailPageProps) {
const { id } = await params;
const service = SERVICES.find((s) => s.id === id);
if (!service) {
return {
title: `服务未找到`,
};
}
return {
title: `${service.title} - 服务详情`,
description: service.description,
};
}
export default async function ServiceDetailPage({ params }: ServiceDetailPageProps) {
const { id } = await params;
const service = SERVICES.find((s) => s.id === id);
if (!service) {
notFound();
}
return (
<div className="pt-32 pb-20">
<div className="container-custom">
<div className="max-w-5xl mx-auto">
{/* 返回按钮 */}
<Button variant="ghost" asChild className="mb-8">
<Link href="/services">
<ArrowLeft className="mr-2 w-4 h-4" />
</Link>
</Button>
{/* 服务标题 */}
<div className="mb-8">
<h1 className="text-4xl sm:text-5xl font-bold text-black mb-4">{service.title}</h1>
<p className="text-xl text-gray-600">{service.fullDescription}</p>
</div>
{/* 服务内容 */}
<Card className="mb-8">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="font-semibold text-black mb-2"></h3>
<p className="text-gray-600 leading-relaxed">{service.content.scope}</p>
</div>
<div>
<h3 className="font-semibold text-black mb-2"></h3>
<div className="space-y-4">
{service.content.process.map((phase, idx) => (
<div key={idx} className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-black rounded-full flex items-center justify-center text-white font-medium">
{idx + 1}
</div>
<div>
<h4 className="font-semibold text-black">{phase.phase}</h4>
<p className="text-gray-600">{phase.description}</p>
</div>
</div>
))}
</div>
</div>
<div>
<h3 className="font-semibold text-black mb-2"></h3>
<div className="flex flex-wrap gap-2">
{service.content.deliverables.map((item, idx) => (
<Badge key={idx} variant="secondary">{item}</Badge>
))}
</div>
</div>
</CardContent>
</Card>
{/* 服务优势 */}
<Card className="mb-8">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{service.advantages.map((advantage, idx) => (
<div key={idx} className="flex items-start gap-2">
<Check className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<div className="font-semibold text-black">{advantage.label}</div>
<div className="text-gray-600">{advantage.value}</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 相关服务 */}
{service.relatedServices && service.relatedServices.length > 0 && (
<Card className="mb-8">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{service.relatedServices.map((relatedId, idx) => {
const relatedService = SERVICES.find(s => s.id === relatedId);
if (!relatedService) return null;
return (
<Button key={idx} variant="outline" asChild>
<Link href={`/services/${relatedId}`}>
{relatedService.title}
</Link>
</Button>
);
})}
</div>
</CardContent>
</Card>
)}
{/* 底部CTA */}
<div className="text-center">
<Button size="lg" className="bg-black text-white hover:bg-gray-800" asChild>
<Link href="/contact">
<ArrowLeft className="ml-2 w-4 h-4 rotate-180" />
</Link>
</Button>
</div>
</div>
</div>
</div>
);
}
```
**Step 2: 验证页面**
```bash
npm run dev
```
Expected: 访问 `/services/software` 能正常显示服务详情。
**Step 3: Commit**
```bash
git add src/app/(marketing)/services/[id]/page.tsx
git commit -m "feat: add service detail page with content, advantages, and process"
```
---
### Task 5: 创建隐私政策页面
**Files:**
- Create: `src/app/(marketing)/privacy/page.tsx`
**Step 1: 创建隐私政策页面**
```typescript
import { COMPANY_INFO } from '@/lib/constants';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
export const metadata = {
title: `隐私政策 - ${COMPANY_INFO.name}`,
description: `了解${COMPANY_INFO.name}如何收集、使用和保护您的个人信息`,
};
export default function PrivacyPolicyPage() {
return (
<div className="pt-32 pb-20">
<div className="container-custom max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl sm:text-5xl font-bold text-black mb-6"></h1>
<p className="text-lg text-gray-600">最后更新日期: 2026年2月26日</p>
</div>
<Card>
<CardContent className="p-8 space-y-8">
{/* 引言 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">1. </h2>
<p className="text-gray-600 leading-relaxed">
访("我们""公司")使,
</p>
<p className="text-gray-600 leading-relaxed mt-4">
访使访使,,使
</p>
</section>
{/* 信息收集 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">2. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
使,:
</p>
<div className="space-y-4">
<div>
<h3 className="font-semibold text-black mb-2">2.1 </h3>
<p className="text-gray-600 leading-relaxed">
,:
</p>
<ul className="list-disc list-inside text-gray-600 leading-relaxed mt-2 space-y-1">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div>
<h3 className="font-semibold text-black mb-2">2.2 </h3>
<p className="text-gray-600 leading-relaxed">
访,:
</p>
<ul className="list-disc list-inside text-gray-600 leading-relaxed mt-2 space-y-1">
<li>IP地址</li>
<li></li>
<li></li>
<li>访</li>
<li></li>
<li>Cookie信息</li>
</ul>
</div>
</div>
</section>
{/* 信息使用 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">3. 使</h2>
<p className="text-gray-600 leading-relaxed mb-4">
:
</p>
<ul className="list-disc list-inside text-gray-600 leading-relaxed space-y-1">
<li></li>
<li></li>
<li>,</li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</section>
{/* 信息共享 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">4. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
,:
</p>
<ul className="list-disc list-inside text-gray-600 leading-relaxed space-y-1">
<li></li>
<li></li>
<li></li>
<li>()</li>
<li>()</li>
</ul>
</section>
{/* 信息存储 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">5. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
,访
</p>
<p className="text-gray-600 leading-relaxed">
:
</p>
<ul className="list-disc list-inside text-gray-600 leading-relaxed space-y-1 mt-2">
<li></li>
<li></li>
<li></li>
</ul>
</section>
{/* 用户权利 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">6. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
:
</p>
<ul className="list-disc list-inside text-gray-600 leading-relaxed space-y-1">
<li>访问权:您可以请求访问您的个人信息</li>
<li>更正权:您可以请求更正您的个人信息</li>
<li>删除权:您可以请求删除您的个人信息</li>
<li>限制处理权:您可以请求限制对您的个人信息的处理</li>
<li>数据可携权:您可以请求获取您的个人信息副本</li>
<li>撤回同意权:您可以随时撤回对个人信息处理的同意</li>
</ul>
<p className="text-gray-600 leading-relaxed mt-4">
使,10
</p>
</section>
{/* Cookie使用 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">7. Cookie使用</h2>
<p className="text-gray-600 leading-relaxed mb-4">
使Cookie和其他类似技术来提供服务和改进用户体验Cookie是存储在您设备上的小数据文件,
</p>
<p className="text-gray-600 leading-relaxed">
Cookie,Cookie,使
</p>
</section>
{/* 未成年人保护 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">8. </h2>
<p className="text-gray-600 leading-relaxed">
1414,,,
</p>
</section>
{/* 政策更新 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">9. </h2>
<p className="text-gray-600 leading-relaxed">
,,,使
</p>
</section>
{/* 联系方式 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">10. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
,使,:
</p>
<div className="bg-gray-50 p-6 rounded-lg">
<p className="text-gray-600 leading-relaxed">
<strong className="text-black">:</strong> {COMPANY_INFO.name}<br />
<strong className="text-black">:</strong> {COMPANY_INFO.address}<br />
<strong className="text-black">:</strong> {COMPANY_INFO.phone}<br />
<strong className="text-black">:</strong> {COMPANY_INFO.email}
</p>
</div>
</section>
</CardContent>
</Card>
</div>
</div>
);
}
```
**Step 2: 验证页面**
```bash
npm run dev
```
Expected: 访问 `/privacy` 能正常显示隐私政策页面。
**Step 3: Commit**
```bash
git add src/app/(marketing)/privacy/page.tsx
git commit -m "feat: add privacy policy page with comprehensive sections"
```
---
### Task 6: 创建服务条款页面
**Files:**
- Create: `src/app/(marketing)/terms/page.tsx`
**Step 1: 创建服务条款页面**
```typescript
import { COMPANY_INFO } from '@/lib/constants';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
export const metadata = {
title: `服务条款 - ${COMPANY_INFO.name}`,
description: `使用${COMPANY_INFO.name}网站服务前请仔细阅读本服务条款`,
};
export default function TermsOfServicePage() {
return (
<div className="pt-32 pb-20">
<div className="container-custom max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl sm:text-5xl font-bold text-black mb-6"></h1>
<p className="text-lg text-gray-600">最后更新日期: 2026年2月26日</p>
</div>
<Card>
<CardContent className="p-8 space-y-8">
{/* 引言 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">1. </h2>
<p className="text-gray-600 leading-relaxed">
访("我们""公司")使
</p>
<p className="text-gray-600 leading-relaxed mt-4">
访使访使,,使
</p>
</section>
{/* 服务内容 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">2. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
:
</p>
<ul className="list-disc list-inside text-gray-600 leading-relaxed space-y-1">
<li></li>
<li></li>
<li></li>
<li>线</li>
<li></li>
</ul>
<p className="text-gray-600 leading-relaxed mt-4">
,
</p>
</section>
{/* 用户义务 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">3. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
使,:
</p>
<ul className="list-disc list-inside text-gray-600 leading-relaxed space-y-1">
<li></li>
<li></li>
<li>()</li>
<li></li>
<li>,使访</li>
<li>访</li>
<li>,</li>
</ul>
</section>
{/* 知识产权 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">4. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
()
</p>
<p className="text-gray-600 leading-relaxed">
,
</p>
</section>
{/* 免责条款 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">5. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
,
</p>
<p className="text-gray-600 leading-relaxed">
,使使,
</p>
</section>
{/* 服务变更 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">6. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
,
</p>
<p className="text-gray-600 leading-relaxed">
,,
</p>
</section>
{/* 争议解决 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">7. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
,
</p>
<p className="text-gray-600 leading-relaxed">
,;,
</p>
</section>
{/* 条款更新 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">8. </h2>
<p className="text-gray-600 leading-relaxed">
,,使,使
</p>
</section>
{/* 联系方式 */}
<section>
<h2 className="text-2xl font-bold text-black mb-4">9. </h2>
<p className="text-gray-600 leading-relaxed mb-4">
,:
</p>
<div className="bg-gray-50 p-6 rounded-lg">
<p className="text-gray-600 leading-relaxed">
<strong className="text-black">:</strong> {COMPANY_INFO.name}<br />
<strong className="text-black">:</strong> {COMPANY_INFO.address}<br />
<strong className="text-black">:</strong> {COMPANY_INFO.phone}<br />
<strong className="text-black">:</strong> {COMPANY_INFO.email}
</p>
</div>
</section>
</CardContent>
</Card>
</div>
</div>
);
}
```
**Step 2: 验证页面**
```bash
npm run dev
```
Expected: 访问 `/terms` 能正常显示服务条款页面。
**Step 3: Commit**
```bash
git add src/app/(marketing)/terms/page.tsx
git commit -m "feat: add terms of service page with comprehensive sections"
```
---
## 阶段三:功能集成
### Task 7: 安装依赖
**Files:**
- Modify: `package.json`
**Step 1: 安装Resend和Zod**
```bash
npm install resend zod
```
Expected: Resend和Zod添加到package.json的dependencies中。
**Step 2: 验证安装**
```bash
npm list resend zod
```
Expected: 显示Resend和Zod的版本号。
**Step 3: Commit**
```bash
git add package.json package-lock.json
git commit -m "chore: add resend and zod dependencies"
```
---
### Task 8: 创建联系表单API路由
**Files:**
- Create: `src/app/api/contact/route.ts`
**Step 1: 创建API路由**
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { Resend } from 'resend';
import { z } from 'zod';
// 定义表单数据验证schema
const contactFormSchema = z.object({
name: z.string().min(1, '姓名不能为空').max(50, '姓名不能超过50个字符'),
phone: z.string().optional(),
email: z.string().email('邮箱格式不正确'),
subject: z.string().min(1, '主题不能为空').max(200, '主题不能超过200个字符'),
message: z.string().min(1, '消息内容不能为空').max(2000, '消息内容不能超过2000个字符'),
});
// 定义请求体类型
type ContactFormData = z.infer<typeof contactFormSchema>;
// 初始化Resend
const resend = new Resend(process.env.RESEND_API_KEY);
// 定义允许的来源
const ALLOWED_ORIGINS = [
'https://novalon.cn',
'https://www.novalon.cn',
'http://localhost:3000',
];
// CORS中间件
function corsHeaders(origin: string | null) {
const allowed = origin && ALLOWED_ORIGINS.includes(origin);
return {
'Access-Control-Allow-Origin': allowed ? origin : '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
}
// 处理OPTIONS请求
export async function OPTIONS(request: NextRequest) {
const origin = request.headers.get('origin');
return NextResponse.json({}, { headers: corsHeaders(origin) });
}
// 处理POST请求
export async function POST(request: NextRequest) {
const origin = request.headers.get('origin');
// 检查Content-Type
const contentType = request.headers.get('content-type');
if (contentType !== 'application/json') {
return NextResponse.json(
{ error: 'Content-Type必须为application/json' },
{
status: 400,
headers: corsHeaders(origin)
}
);
}
try {
// 解析请求体
const body = await request.json();
// 验证数据
const validationResult = contactFormSchema.safeParse(body);
if (!validationResult.success) {
const errors = validationResult.error.errors.map(err => ({
field: err.path.join('.'),
message: err.message
}));
return NextResponse.json(
{ error: '验证失败', errors },
{
status: 400,
headers: corsHeaders(origin)
}
);
}
const { name, phone, email, subject, message } = validationResult.data;
// 检查频率限制(简单实现)
const clientIp = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
const rateLimitKey = `contact_form:${clientIp}`;
// 注意:生产环境应该使用Redis等持久化存储
// 这里只是一个简单的内存限制示例
// 实际实现时需要更完善的频率限制机制
// 发送邮件
const response = await resend.emails.send({
from: process.env.FROM_EMAIL || 'No reply <noreply@resend.dev>',
to: [process.env.CONTACT_EMAIL || 'contact@novalon.cn'],
subject: `[${subject}] ${name}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1a1a1a;">新联系表单提交</h2>
<div style="background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 10px 0;"><strong>姓名:</strong> ${name}</p>
${phone ? `<p style="margin: 10px 0;"><strong>电话:</strong> ${phone}</p>` : ''}
<p style="margin: 10px 0;"><strong>邮箱:</strong> ${email}</p>
<p style="margin: 10px 0;"><strong>主题:</strong> ${subject}</p>
<p style="margin: 10px 0;"><strong>消息内容:</strong></p>
<div style="background: white; padding: 15px; border-radius: 4px; margin-top: 10px; white-space: pre-wrap;">${message}</div>
</div>
<p style="color: #666; font-size: 14px;">此邮件由网站联系表单自动发送,请勿直接回复。</p>
</div>
`,
});
// 发送确认邮件(可选)
if (response.data) {
try {
await resend.emails.send({
from: process.env.FROM_EMAIL || 'No reply <noreply@resend.dev>',
to: [email],
subject: '感谢您的联系 - 确认邮件',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1a1a1a;">感谢您的联系!</h2>
<p style="color: #333; margin: 20px 0;">您好 ${name},</p>
<p style="color: #333; margin: 20px 0;">
我们已收到您的消息,主题为"${subject}"。我们的团队会尽快查看并回复您。
</p>
<p style="color: #333; margin: 20px 0;">
如果您有任何其他问题,欢迎随时联系我们。
</p>
<div style="background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 10px 0;"><strong>您提交的信息:</strong></p>
<p style="margin: 10px 0;">主题: ${subject}</p>
<p style="margin: 10px 0;">消息: ${message.substring(0, 200)}${message.length > 200 ? '...' : ''}</p>
</div>
<p style="color: #666; font-size: 14px;">此邮件由网站自动发送,请勿直接回复。</p>
</div>
`,
});
} catch (confirmError) {
console.error('发送确认邮件失败:', confirmError);
// 不影响主流程,仅记录错误
}
}
return NextResponse.json(
{ success: true, message: '消息已发送成功' },
{
status: 200,
headers: corsHeaders(origin)
}
);
} catch (error) {
console.error('联系表单提交失败:', error);
return NextResponse.json(
{ error: '消息发送失败,请稍后重试' },
{
status: 500,
headers: corsHeaders(origin)
}
);
}
}
```
**Step 2: 验证API路由**
```bash
npm run dev
```
Expected: API路由创建成功,没有TypeScript错误。
**Step 3: Commit**
```bash
git add src/app/api/contact/route.ts
git commit -m "feat: add contact form API route with validation and email sending"
```
---
### Task 9: 更新联系表单提交逻辑
**Files:**
- Modify: `src/app/(marketing)/contact/page.tsx`
**Step 1: 查看当前联系表单代码**
Read `src/app/(marketing)/contact/page.tsx` to understand current implementation.
**Step 2: 更新联系表单提交逻辑**
```typescript
// 替换handleSubmit函数
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: formData.get('name'),
phone: formData.get('phone'),
email: formData.get('email'),
subject: formData.get('subject'),
message: formData.get('message'),
}),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '消息发送失败');
}
setIsSubmitting(false);
setIsSubmitted(true);
// 3秒后重置表单
setTimeout(() => {
setIsSubmitted(false);
}, 3000);
} catch (error) {
setIsSubmitting(false);
setError(error instanceof Error ? error.message : '消息发送失败');
}
}
```
**Step 3: 添加错误显示**
在表单中添加错误显示:
```typescript
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm">{error}</p>
</div>
)}
```
**Step 4: 验证表单**
```bash
npm run dev
```
Expected: 联系表单能够成功提交并发送邮件。
**Step 5: Commit**
```bash
git add src/app/(marketing)/contact/page.tsx
git commit -m "feat: update contact form to use API route for email sending"
```
---
## 阶段四:测试和优化
### Task 10: 运行代码检查
**Files:**
- All modified files
**Step 1: 运行ESLint**
```bash
npm run lint
```
Expected: 所有文件通过ESLint检查,没有错误。
**Step 2: 修复ESLint错误**
根据ESLint输出,修复所有错误。
**Step 3: 运行TypeScript类型检查**
```bash
npx tsc --noEmit
```
Expected: 没有TypeScript类型错误。
**Step 4: Commit**
```bash
git add .
git commit -m "fix: fix linting and type errors"
```
---
### Task 11: 运行开发服务器测试
**Files:**
- All new and modified files
**Step 1: 启动开发服务器**
```bash
npm run dev
```
Expected: 开发服务器启动成功,没有错误。
**Step 2: 测试所有页面**
- 访问 `/products/erp` - 测试产品详情页面
- 访问 `/services/software` - 测试服务详情页面
- 访问 `/privacy` - 测试隐私政策页面
- 访问 `/terms` - 测试服务条款页面
- 访问 `/contact` - 测试联系表单
Expected: 所有页面正常显示,功能正常。
**Step 3: Commit**
```bash
git add .
git commit -m "test: verify all pages and functionality"
```
---
### Task 12: 响应式设计测试
**Files:**
- All new pages
**Step 1: 测试移动端**
在浏览器开发者工具中,切换到移动设备模式,测试所有页面在手机上的显示效果。
Expected: 所有页面在移动端正常显示,布局合理。
**Step 2: 测试平板**
切换到平板设备模式,测试所有页面在平板上的显示效果。
Expected: 所有页面在平板上正常显示,布局合理。
**Step 3: Commit**
```bash
git add .
git commit -m "test: verify responsive design on mobile and tablet"
```
---
## 阶段五:部署和上线
### Task 13: 配置环境变量
**Files:**
- `.env.local` (not committed to git)
**Step 1: 创建`.env.local`文件**
```bash
cp .env.example .env.local
```
**Step 2: 配置环境变量**
`.env.local`中添加:
```bash
# Resend API配置
RESEND_API_KEY=re_123456789_your_actual_api_key
FROM_EMAIL=No reply <noreply@resend.dev>
CONTACT_EMAIL=contact@novalon.cn
```
**Step 3: 验证环境变量**
```bash
npm run dev
```
Expected: 开发服务器启动成功,邮件发送功能正常。
**Step 4: Commit**
```bash
git add .env.local
git commit -m "chore: add environment variables (do not commit this to public repo)"
```
---
### Task 14: 构建生产版本
**Files:**
- All files
**Step 1: 构建生产版本**
```bash
npm run build
```
Expected: 构建成功,没有错误。
**Step 2: 验证构建输出**
检查`dist/`目录,确保所有文件都已生成。
**Step 3: Commit**
```bash
git add .
git commit -m "build: create production build"
```
---
### Task 15: 部署到生产环境
**Files:**
- All files
**Step 1: 部署**
根据您的部署平台,部署生产版本。
**Step 2: 验证生产环境**
- 访问生产环境的URL
- 测试所有页面
- 测试联系表单
Expected: 生产环境正常运行,所有功能正常。
**Step 3: Commit**
```bash
git add .
git commit -m "deploy: deploy to production environment"
```
---
## 验收检查清单
### 功能验收
- [ ] 产品详情页面正常显示所有内容模块
- [ ] 服务详情页面正常显示所有内容模块
- [ ] 隐私政策和服务条款页面内容完整
- [ ] 联系表单能够成功发送邮件
- [ ] 表单验证和安全措施正常工作
### 质量验收
- [ ] 所有页面通过ESLint检查
- [ ] 所有页面通过TypeScript类型检查
- [ ] 响应式设计在各种设备上正常显示
- [ ] 页面加载性能符合要求
- [ ] SEO优化符合最佳实践
### 部署验收
- [ ] 生产环境部署成功
- [ ] 所有页面在生产环境正常显示
- [ ] 联系表单在生产环境正常工作
- [ ] 环境变量配置正确
---
## 备注
1. 所有环境变量文件`.env.local`不应提交到版本控制系统
2. Resend API密钥需要在Resend控制台获取
3. FROM_EMAIL需要在Resend中验证域名
4. 建议在生产环境使用更完善的频率限制机制(如Redis)
5. 建议添加表单提交日志,便于后续分析和追溯