fix: 修复TypeScript类型错误
- 移除未使用的导入 - 修复产品详情页面的description类型错误 - 修复服务详情页面的description类型错误 - 修复联系表单API的类型错误 - 添加Award图标的导入
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
# 网站功能补全设计方案
|
||||
|
||||
**日期**: 2026-02-26
|
||||
**状态**: 已确认
|
||||
**优先级**: 高
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了Novalon网站功能补全的完整设计方案,包括产品详情页面、服务详情页面、联系表单邮件服务集成、隐私政策和服务条款页面。
|
||||
|
||||
## 1. 产品详情页面
|
||||
|
||||
### 路由
|
||||
- 路径: `/products/[id]`
|
||||
- 动态路由参数: `id` (产品ID,如 `erp`, `crm`, `cms`, `bi`)
|
||||
|
||||
### 页面结构
|
||||
|
||||
#### 顶部区域
|
||||
- 产品标题
|
||||
- 分类标签
|
||||
- 简短描述
|
||||
- 产品图片或图标
|
||||
|
||||
#### 功能特性模块
|
||||
- 按模块分组展示功能点
|
||||
- 每个功能点配有图标和详细说明
|
||||
- 对于复杂产品,分组展示(如"财务模块"、"采购模块"等)
|
||||
|
||||
#### 技术架构模块
|
||||
- 技术架构图
|
||||
- 技术栈说明
|
||||
- 前端技术、后端技术、数据库、部署方式
|
||||
|
||||
#### 应用场景模块
|
||||
- 典型应用场景
|
||||
- 行业案例
|
||||
- 解决方案说明
|
||||
|
||||
#### 客户案例模块
|
||||
- 成功客户案例
|
||||
- 客户名称、行业
|
||||
- 实施效果(数据化展示)
|
||||
|
||||
#### 底部CTA
|
||||
- "联系我们"按钮
|
||||
- "申请演示"按钮
|
||||
|
||||
### 数据来源
|
||||
- 从 `@/lib/constants` 中的 `PRODUCTS` 数组获取产品数据
|
||||
- 需要扩展 `PRODUCTS` 数据结构,添加详细内容
|
||||
|
||||
## 2. 服务详情页面
|
||||
|
||||
### 路由
|
||||
- 路径: `/services/[id]`
|
||||
- 动态路由参数: `id` (服务ID,如 `software`, `cloud`, `data`, `security`)
|
||||
|
||||
### 页面结构
|
||||
|
||||
#### 顶部区域
|
||||
- 服务标题
|
||||
- 服务图标
|
||||
- 服务概述
|
||||
- 价值主张
|
||||
|
||||
#### 服务内容模块
|
||||
- 服务范围详细说明
|
||||
- 服务流程
|
||||
- 交付标准
|
||||
- 对于软件开发服务,说明需求分析、设计、开发、测试、部署等各阶段
|
||||
|
||||
#### 服务优势模块
|
||||
- 核心优势
|
||||
- 技术实力
|
||||
- 行业经验
|
||||
- 团队规模
|
||||
- 成功案例
|
||||
- 数据化展示(如"10年+行业经验"、"100+成功案例")
|
||||
|
||||
#### 服务流程模块
|
||||
- 时间轴形式展示
|
||||
- 从接触到交付的完整过程
|
||||
|
||||
#### 相关服务模块
|
||||
- 展示其他相关服务
|
||||
- 引导客户了解更多
|
||||
|
||||
#### 底部CTA
|
||||
- "立即咨询"按钮
|
||||
- 跳转到联系页面并预填服务类型
|
||||
|
||||
### 数据来源
|
||||
- 从 `@/lib/constants` 中的 `SERVICES` 数组获取服务数据
|
||||
- 需要扩展 `SERVICES` 数据结构,添加详细内容
|
||||
|
||||
## 3. 联系表单邮件服务集成
|
||||
|
||||
### 技术选型
|
||||
- **主选方案**: Resend
|
||||
- 每月3000封免费邮件
|
||||
- 简洁的API接口
|
||||
- 良好的开发者体验
|
||||
- 支持国内访问
|
||||
|
||||
- **替代方案**:
|
||||
- EmailJS: 前端直接发送,每月200封免费
|
||||
- SendGrid: 每天100封免费
|
||||
- SMTP2GO: 每月1000封免费
|
||||
|
||||
### 实现方式
|
||||
|
||||
#### API路由
|
||||
- 路径: `/api/contact`
|
||||
- 方法: POST
|
||||
- 接收表单数据: name, phone, email, subject, message
|
||||
|
||||
#### 邮件发送流程
|
||||
1. 接收表单数据
|
||||
2. 验证数据(Zod schema)
|
||||
3. 调用Resend SDK发送邮件到公司邮箱
|
||||
4. (可选)发送确认邮件给用户
|
||||
5. 返回成功或失败响应
|
||||
|
||||
### 安全措施
|
||||
- 使用环境变量存储API密钥 (`RESEND_API_KEY`)
|
||||
- 实现CSRF防护
|
||||
- 添加表单验证(Zod schema验证)
|
||||
- 实现频率限制(每IP每小时最多5次)
|
||||
- 使用简单验证码防止机器人提交
|
||||
|
||||
### 用户体验
|
||||
- 提交时显示加载状态
|
||||
- 提交成功后显示成功提示
|
||||
- 提交失败时显示错误信息并允许重试
|
||||
- 保留表单数据,失败时无需重新填写
|
||||
|
||||
### 配置管理
|
||||
- 在 `.env.local` 中配置:
|
||||
- `RESEND_API_KEY`: Resend API密钥
|
||||
- `CONTACT_EMAIL`: 收件人邮箱
|
||||
- `FROM_EMAIL`: 发件人邮箱(需要在Resend中验证域名)
|
||||
|
||||
### 依赖安装
|
||||
```bash
|
||||
npm install resend
|
||||
npm install zod
|
||||
```
|
||||
|
||||
## 4. 隐私政策和服务条款页面
|
||||
|
||||
### 隐私政策页面 (`/privacy`)
|
||||
|
||||
#### 页面结构
|
||||
1. **引言部分**: 适用范围和生效日期
|
||||
2. **信息收集**: 收集的用户信息类型及收集目的
|
||||
3. **信息使用**: 如何使用收集的信息
|
||||
4. **信息共享**: 在何种情况下会共享用户信息
|
||||
5. **信息存储**: 信息的存储方式和存储期限
|
||||
6. **用户权利**: 用户对个人信息享有的权利
|
||||
7. **Cookie使用**: Cookie的目的和管理方式
|
||||
8. **未成年人保护**: 对未成年人的特殊保护措施
|
||||
9. **政策更新**: 隐私政策的更新机制和通知方式
|
||||
10. **联系方式**: 数据保护负责人的联系方式
|
||||
|
||||
#### 法律依据
|
||||
- 《中华人民共和国网络安全法》
|
||||
- 《中华人民共和国个人信息保护法》
|
||||
- 《中华人民共和国数据安全法》
|
||||
|
||||
### 服务条款页面 (`/terms`)
|
||||
|
||||
#### 页面结构
|
||||
1. **引言部分**: 适用范围和生效日期
|
||||
2. **服务内容**: 网站提供的服务内容和范围
|
||||
3. **用户义务**: 用户使用服务时应遵守的规则和限制
|
||||
4. **知识产权**: 网站内容的知识产权归属和使用限制
|
||||
5. **免责条款**: 网站在何种情况下不承担责任
|
||||
6. **服务变更**: 网站变更或终止服务的权利和通知方式
|
||||
7. **争议解决**: 争议解决的方式和适用法律
|
||||
8. **条款更新**: 服务条款的更新机制和通知方式
|
||||
9. **联系方式**: 法律事务联系人的联系方式
|
||||
|
||||
#### 法律依据
|
||||
- 《中华人民共和国民法典》
|
||||
- 《中华人民共和国电子商务法》
|
||||
- 《中华人民共和国网络安全法》
|
||||
|
||||
### 设计特点
|
||||
- 清晰的层级结构,便于阅读
|
||||
- 使用锚点导航,方便用户快速跳转到相关章节
|
||||
- 保持专业、严谨的法律文档风格
|
||||
- 响应式设计,适配各种设备
|
||||
|
||||
## 技术实现要点
|
||||
|
||||
### 通用要求
|
||||
- 所有页面使用Next.js App Router
|
||||
- 组件复用现有的UI组件库(shadcn/ui)
|
||||
- 保持与现有页面一致的视觉风格
|
||||
- 响应式设计,适配各种设备
|
||||
- SEO优化,包含metadata和结构化数据
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
src/
|
||||
├── app/
|
||||
│ ├── (marketing)/
|
||||
│ │ ├── products/
|
||||
│ │ │ └── [id]/
|
||||
│ │ │ └── page.tsx # 产品详情页面
|
||||
│ │ ├── services/
|
||||
│ │ │ └── [id]/
|
||||
│ │ │ └── page.tsx # 服务详情页面
|
||||
│ │ ├── privacy/
|
||||
│ │ │ └── page.tsx # 隐私政策页面
|
||||
│ │ └── terms/
|
||||
│ │ └── page.tsx # 服务条款页面
|
||||
│ └── api/
|
||||
│ └── contact/
|
||||
│ └── route.ts # 联系表单API路由
|
||||
└── lib/
|
||||
└── constants.ts # 扩展PRODUCTS和SERVICES数据
|
||||
```
|
||||
|
||||
### 数据扩展
|
||||
|
||||
#### PRODUCTS数据结构扩展
|
||||
```typescript
|
||||
export const PRODUCTS = [
|
||||
{
|
||||
id: 'erp',
|
||||
title: '睿新ERP管理系统',
|
||||
description: '...',
|
||||
category: '企业软件',
|
||||
features: ['财务管理', '采购管理', ...],
|
||||
benefits: ['提升运营效率30%', ...],
|
||||
// 新增字段
|
||||
technicalArchitecture: {
|
||||
frontend: ['React', 'TypeScript'],
|
||||
backend: ['Node.js', 'Express'],
|
||||
database: ['PostgreSQL', 'Redis'],
|
||||
deployment: ['Docker', 'Kubernetes']
|
||||
},
|
||||
scenarios: [
|
||||
{
|
||||
title: '制造业',
|
||||
description: '实现生产计划、物料需求、库存管理的全流程数字化'
|
||||
},
|
||||
// ...
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
client: '某制造企业',
|
||||
industry: '制造业',
|
||||
results: [
|
||||
{ label: '生产效率', value: '提升40%' },
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// ...
|
||||
]
|
||||
```
|
||||
|
||||
#### SERVICES数据结构扩展
|
||||
```typescript
|
||||
export const SERVICES = [
|
||||
{
|
||||
id: 'software',
|
||||
title: '软件开发',
|
||||
description: '...',
|
||||
icon: 'Code',
|
||||
// 新增字段
|
||||
content: {
|
||||
scope: '提供定制化软件开发服务,包括Web应用、移动应用、企业管理系统等',
|
||||
process: [
|
||||
{ phase: '需求分析', description: '深入了解业务需求,制定详细需求文档' },
|
||||
{ phase: '系统设计', description: '设计系统架构、数据库结构、接口规范' },
|
||||
{ phase: '开发实施', description: '采用敏捷开发方法,快速迭代交付' },
|
||||
{ phase: '测试验收', description: '全面测试,确保系统质量和稳定性' },
|
||||
{ phase: '部署上线', description: '协助部署,提供运维支持' }
|
||||
],
|
||||
deliverables: ['源代码', '技术文档', '用户手册', '培训服务']
|
||||
},
|
||||
advantages: [
|
||||
{ label: '技术实力', value: '10年+行业经验' },
|
||||
{ label: '团队规模', value: '50+专业开发人员' },
|
||||
{ label: '成功案例', value: '100+项目交付' },
|
||||
{ label: '客户满意度', value: '98%' }
|
||||
]
|
||||
},
|
||||
// ...
|
||||
]
|
||||
```
|
||||
|
||||
## 实施计划
|
||||
|
||||
### 阶段一:数据准备
|
||||
- [ ] 扩展 `PRODUCTS` 数据结构,添加详细内容
|
||||
- [ ] 扩展 `SERVICES` 数据结构,添加详细内容
|
||||
- [ ] 创建产品详情页面的静态数据
|
||||
|
||||
### 阶段二:页面开发
|
||||
- [ ] 创建产品详情页面 `/products/[id]`
|
||||
- [ ] 创建服务详情页面 `/services/[id]`
|
||||
- [ ] 创建隐私政策页面 `/privacy`
|
||||
- [ ] 创建服务条款页面 `/terms`
|
||||
|
||||
### 阶段三:功能集成
|
||||
- [ ] 安装Resend SDK和Zod
|
||||
- [ ] 创建联系表单API路由 `/api/contact`
|
||||
- [ ] 实现邮件发送功能
|
||||
- [ ] 添加表单验证和安全措施
|
||||
- [ ] 更新联系表单提交逻辑
|
||||
|
||||
### 阶段四:测试和优化
|
||||
- [ ] 测试所有新页面的功能
|
||||
- [ ] 测试邮件发送功能
|
||||
- [ ] 测试表单验证和安全措施
|
||||
- [ ] 优化页面性能和用户体验
|
||||
- [ ] 检查响应式设计
|
||||
|
||||
### 阶段五:部署和上线
|
||||
- [ ] 配置生产环境的环境变量
|
||||
- [ ] 构建生产版本
|
||||
- [ ] 部署到生产环境
|
||||
- [ ] 验证所有功能正常运行
|
||||
|
||||
## 验收标准
|
||||
|
||||
### 功能验收
|
||||
- [ ] 产品详情页面正常显示所有内容模块
|
||||
- [ ] 服务详情页面正常显示所有内容模块
|
||||
- [ ] 隐私政策和服务条款页面内容完整
|
||||
- [ ] 联系表单能够成功发送邮件
|
||||
- [ ] 表单验证和安全措施正常工作
|
||||
|
||||
### 质量验收
|
||||
- [ ] 所有页面通过ESLint检查
|
||||
- [ ] 所有页面通过TypeScript类型检查
|
||||
- [ ] 响应式设计在各种设备上正常显示
|
||||
- [ ] 页面加载性能符合要求
|
||||
- [ ] SEO优化符合最佳实践
|
||||
|
||||
## 风险和注意事项
|
||||
|
||||
### 技术风险
|
||||
- **邮件服务限制**: Resend免费额度为每月3000封,如超出需要升级或更换服务
|
||||
- **API密钥安全**: 需要妥善保管API密钥,不要提交到代码仓库
|
||||
- **表单滥用**: 需要实施频率限制和验证码,防止恶意提交
|
||||
|
||||
### 法律风险
|
||||
- **隐私政策合规**: 需要确保隐私政策符合最新的法律法规要求
|
||||
- **数据保护**: 需要妥善处理用户提交的个人信息,符合个人信息保护法要求
|
||||
- **条款更新**: 需要定期更新隐私政策和服务条款,确保内容准确
|
||||
|
||||
### 运维风险
|
||||
- **邮件服务稳定性**: 需要监控邮件服务的可用性和发送成功率
|
||||
- **表单数据备份**: 建议保存表单提交记录,便于后续分析和追溯
|
||||
- **用户反馈**: 需要及时处理用户反馈,优化用户体验
|
||||
|
||||
## 后续优化方向
|
||||
|
||||
1. **产品详情页面**
|
||||
- 添加产品视频演示
|
||||
- 添加在线试用功能
|
||||
- 添加产品定价信息
|
||||
- 添加常见问题(FAQ)模块
|
||||
|
||||
2. **服务详情页面**
|
||||
- 添加服务案例展示
|
||||
- 添加服务定价信息
|
||||
- 添加在线预约功能
|
||||
- 添加服务评价和推荐
|
||||
|
||||
3. **联系表单**
|
||||
- 添加文件上传功能
|
||||
- 添加智能客服集成
|
||||
- 添加在线聊天功能
|
||||
- 添加表单数据分析
|
||||
|
||||
4. **法律页面**
|
||||
- 添加Cookie同意管理
|
||||
- 添加数据导出功能
|
||||
- 添加账户删除功能
|
||||
- 添加隐私设置页面
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Next.js App Router文档](https://nextjs.org/docs/app)
|
||||
- [Resend文档](https://resend.com/docs)
|
||||
- [Zod文档](https://zod.dev)
|
||||
- [个人信息保护法](http://www.npc.gov.cn/npc/c30834/202108/a8c4e3672c74491a80b53a172bb753fe.shtml)
|
||||
- [网络安全法](http://www.npc.gov.cn/npc/c30834/201611/6c5a468d8c3f4e8f9d5d5e5e5e5e5e5e.shtml)
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+73
@@ -20,6 +20,7 @@
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"resend": "^6.9.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.183.1",
|
||||
"zod": "^4.3.6"
|
||||
@@ -1761,6 +1762,12 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stablelib/base64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -3039,6 +3046,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
@@ -3846,6 +3859,12 @@
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postal-mime": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz",
|
||||
"integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==",
|
||||
"license": "MIT-0"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -3985,6 +4004,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/resend": {
|
||||
"version": "6.9.2",
|
||||
"resolved": "https://registry.npmjs.org/resend/-/resend-6.9.2.tgz",
|
||||
"integrity": "sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postal-mime": "2.7.3",
|
||||
"svix": "1.84.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-email/render": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@react-email/render": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
@@ -4102,6 +4142,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/base64": "^1.0.0",
|
||||
"fast-sha256": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
@@ -4131,6 +4181,16 @@
|
||||
"integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/svix": {
|
||||
"version": "1.84.1",
|
||||
"resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz",
|
||||
"integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"standardwebhooks": "1.0.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||
@@ -4279,6 +4339,19 @@
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"resend": "^6.9.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.183.1",
|
||||
"zod": "^4.3.6"
|
||||
|
||||
@@ -13,13 +13,36 @@ export default function ContactPage() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
async function handleSubmit(_formData: FormData) {
|
||||
async function handleSubmit(formData: FormData) {
|
||||
setIsSubmitting(true);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitted(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'),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || '发送失败,请稍后再试');
|
||||
}
|
||||
|
||||
setIsSubmitted(true);
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
alert(error instanceof Error ? error.message : '发送失败,请稍后再试');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PRODUCTS, COMPANY_INFO } from '@/lib/constants';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight, Check, TrendingUp, Code, Cloud, Database, Server, Briefcase, Target, Users, Award } from 'lucide-react';
|
||||
import { ArrowRight, Check, TrendingUp, Code, Cloud, Database, Server, Target, Users } from 'lucide-react';
|
||||
|
||||
interface ProductDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -22,7 +22,7 @@ export async function generateMetadata({ params }: ProductDetailPageProps) {
|
||||
|
||||
return {
|
||||
title: `${product.title} - ${COMPANY_INFO.name}`,
|
||||
description: product.fullDescription || product.description,
|
||||
description: product.fullDescription,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SERVICES, COMPANY_INFO } from '@/lib/constants';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight, Check, Target, Users, Award, Briefcase, CheckCircle, FileText, Shield, TrendingUp } from 'lucide-react';
|
||||
import { ArrowRight, CheckCircle, FileText, Award } from 'lucide-react';
|
||||
|
||||
interface ServiceDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -22,7 +22,7 @@ export async function generateMetadata({ params }: ServiceDetailPageProps) {
|
||||
|
||||
return {
|
||||
title: `${service.title} - ${COMPANY_INFO.name}`,
|
||||
description: service.fullDescription || service.description,
|
||||
description: service.fullDescription,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { Resend } from 'resend';
|
||||
import { z } from 'zod';
|
||||
|
||||
const contactSchema = z.object({
|
||||
name: z.string().min(1, '姓名不能为空').max(50, '姓名不能超过50个字符'),
|
||||
phone: z.string().optional(),
|
||||
email: z.string().min(1, '邮箱不能为空').email('邮箱格式不正确').max(100, '邮箱不能超过100个字符'),
|
||||
subject: z.string().min(1, '主题不能为空').max(100, '主题不能超过100个字符'),
|
||||
message: z.string().min(1, '消息内容不能为空').max(1000, '消息内容不能超过1000个字符'),
|
||||
});
|
||||
|
||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1小时
|
||||
const RATE_LIMIT_MAX_REQUESTS = 10;
|
||||
|
||||
interface RateLimitRecord {
|
||||
count: number;
|
||||
resetTime: number;
|
||||
}
|
||||
|
||||
const rateLimitStore: Record<string, RateLimitRecord> = {};
|
||||
|
||||
function checkRateLimit(ip: string): { allowed: boolean; remaining: number } {
|
||||
const now = Date.now();
|
||||
const record = rateLimitStore[ip];
|
||||
|
||||
if (!record || now > record.resetTime) {
|
||||
rateLimitStore[ip] = {
|
||||
count: 1,
|
||||
resetTime: now + RATE_LIMIT_WINDOW,
|
||||
};
|
||||
return { allowed: true, remaining: RATE_LIMIT_MAX_REQUESTS - 1 };
|
||||
}
|
||||
|
||||
record.count += 1;
|
||||
const remaining = Math.max(0, RATE_LIMIT_MAX_REQUESTS - record.count);
|
||||
|
||||
if (record.count > RATE_LIMIT_MAX_REQUESTS) {
|
||||
return { allowed: false, remaining: 0 };
|
||||
}
|
||||
|
||||
return { allowed: true, remaining };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const clientIp = request.headers.get('x-forwarded-for') || 'unknown';
|
||||
|
||||
// 检查速率限制
|
||||
const { allowed, remaining } = checkRateLimit(clientIp);
|
||||
if (!allowed) {
|
||||
return NextResponse.json(
|
||||
{ error: '请求过于频繁,请稍后再试' },
|
||||
{ status: 429, headers: { 'X-RateLimit-Remaining': '0' } }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const validatedData = contactSchema.parse(body);
|
||||
|
||||
const { name, phone, email, subject, message } = validatedData;
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
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: #333;">新消息通知</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: #fff; padding: 15px; border-radius: 4px; margin-top: 10px; white-space: pre-wrap;">${message}</div>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px;">此邮件由系统自动发送,请勿直接回复。</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: true, remaining },
|
||||
{ status: 200, headers: { 'X-RateLimit-Remaining': remaining.toString() } }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: '数据验证失败', details: error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Contact form error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '发送失败,请稍后再试' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json({}, { status: 200 });
|
||||
}
|
||||
Reference in New Issue
Block a user