diff --git a/.env.example b/.env.example
index c56574a..217d70c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,11 +1,2 @@
-DATABASE_URL=postgresql://user:password@localhost:5432/novalon
-NEXTAUTH_SECRET=your-secret-key-here
-NEXTAUTH_URL=https://novalon.cn
-RESEND_API_KEY=your-resend-api-key-here
-OPS_ALERT_EMAIL=ops@novalon.cn
-
+NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
CDN_DOMAIN=https://cdn.novalon.cn
-COS_SECRET_ID=your-tencent-cloud-secret-id
-COS_SECRET_KEY=your-tencent-cloud-secret-key
-COS_BUCKET=novalon-cdn-1250000000
-COS_REGION=ap-chengdu
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index dbd1a4e..3a0719c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -289,3 +289,6 @@ findings.md
# Visual regression snapshots should be committed to version control
# These are in: e2e/src/tests/visual/**/*-snapshots/
# Git will track them because they are not in test-results/ or allure-results/
+
+# AGENTS
+AGENTS.md
\ No newline at end of file
diff --git a/.woodpecker.yml b/.woodpecker.yml
deleted file mode 100644
index fcdfab1..0000000
--- a/.woodpecker.yml
+++ /dev/null
@@ -1,401 +0,0 @@
-# ============================================
-# Novalon Website - 全自动CI/CD工作流
-# ============================================
-# 发布策略:release分支发布 + main分支归档
-#
-# 分支角色:
-# - feature分支:开发新功能
-# - release分支:生产环境代码,合并后自动部署
-# - main分支:稳定代码归档,只读
-#
-# 流水线阶段:
-# 1. 代码质量检查 (lint, type-check, security)
-# 2. 单元测试和集成测试
-# 3. E2E测试 (分层测试)
-# 4. 构建Docker镜像
-# 5. 部署到生产环境 (release分支)
-# 6. 归档到main分支
-# 7. 通知和监控
-# ============================================
-
-# 全局环境变量
-variables:
- - &node_image node:20-alpine
- - &docker_image docker:24-cli
-
-# ============================================
-# 阶段1: 代码质量检查
-# ============================================
-steps:
- # 1.1 Lint检查
- lint:
- image: *node_image
- environment:
- NODE_ENV: development
- commands:
- - npm ci
- - npm run lint
- when:
- event:
- - push
- - pull_request
-
- # 1.2 类型检查
- type-check:
- image: *node_image
- environment:
- NODE_ENV: development
- commands:
- - npm ci
- - npm run type-check
- when:
- event:
- - push
- - pull_request
-
- # 1.3 安全漏洞扫描
- security-scan:
- image: *node_image
- environment:
- NODE_ENV: development
- commands:
- - npm ci
- - npm audit --audit-level=moderate
- when:
- event:
- - push
- - pull_request
- failure: ignore
-
- # ============================================
- # 阶段2: 单元测试和集成测试
- # ============================================
- unit-tests:
- image: *node_image
- environment:
- NODE_ENV: test
- CI: true
- commands:
- - npm ci
- - npm run test:coverage:check
- when:
- event:
- - push
- - pull_request
-
- # ============================================
- # 阶段3: E2E测试 (分层测试)
- # ============================================
- # 3.1 Smoke测试 (PR快速验证)
- e2e-smoke:
- image: mcr.microsoft.com/playwright:v1.48.0-jammy
- environment:
- NODE_ENV: test
- CI: true
- commands:
- - npm ci
- - cd e2e && npm ci
- - npx playwright install chromium --with-deps
- - npm run test:smoke
- when:
- event:
- - pull_request
-
- # 3.2 标准测试 (release分支)
- e2e-standard:
- image: mcr.microsoft.com/playwright:v1.48.0-jammy
- environment:
- NODE_ENV: test
- CI: true
- commands:
- - npm ci
- - cd e2e && npm ci
- - npx playwright install chromium --with-deps
- - npm run test:tier:standard
- when:
- event:
- - push
- branch:
- - release
- - release/**
-
- # 3.3 深度测试 (release分支)
- e2e-deep:
- image: mcr.microsoft.com/playwright:v1.48.0-jammy
- environment:
- NODE_ENV: test
- CI: true
- commands:
- - npm ci
- - cd e2e && npm ci
- - npx playwright install chromium firefox webkit --with-deps
- - npm run test:tier:deep
- when:
- event:
- - push
- branch:
- - release
- - release/**
-
- # 3.4 性能测试 (release分支)
- e2e-performance:
- image: mcr.microsoft.com/playwright:v1.48.0-jammy
- environment:
- NODE_ENV: test
- CI: true
- commands:
- - npm ci
- - cd e2e && npm ci
- - npx playwright install chromium --with-deps
- - npm run test:performance
- when:
- event:
- - push
- branch:
- - release
- - release/**
-
- # 3.5 可访问性测试 (release分支)
- e2e-accessibility:
- image: mcr.microsoft.com/playwright:v1.48.0-jammy
- environment:
- NODE_ENV: test
- CI: true
- commands:
- - npm ci
- - cd e2e && npm ci
- - npx playwright install chromium --with-deps
- - npx playwright test --grep @accessibility
- when:
- event:
- - push
- branch:
- - release
- - release/**
-
- # 3.6 视觉回归测试 (release分支)
- e2e-visual:
- image: mcr.microsoft.com/playwright:v1.48.0-jammy
- environment:
- NODE_ENV: test
- CI: true
- commands:
- - npm ci
- - cd e2e && npm ci
- - npx playwright install chromium --with-deps
- - npx playwright test --grep @visual
- when:
- event:
- - push
- branch:
- - release
- - release/**
-
- # ============================================
- # 阶段4: 构建Docker镜像 (release分支)
- # ============================================
- build-image:
- image: *docker_image
- environment:
- DOCKER_HOST: tcp://docker:2375
- REGISTRY_PASSWORD:
- from_secret: registry_password
- commands:
- - echo "Building Docker image..."
- - docker build -t registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} .
- - docker tag registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} registry.f.novalon.cn/novalon-website:latest
- - docker tag registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} registry.f.novalon.cn/novalon-website:release-${CI_COMMIT_SHA:0:7}
- - echo "Pushing to registry..."
- - echo "$REGISTRY_PASSWORD" | docker login -u novalon-admin --password-stdin registry.f.novalon.cn
- - docker push registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA}
- - docker push registry.f.novalon.cn/novalon-website:latest
- - docker push registry.f.novalon.cn/novalon-website:release-${CI_COMMIT_SHA:0:7}
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock
- when:
- - event: push
- branch:
- - release
- - release/**
-
- # ============================================
- # 阶段5: 部署到生产环境 (release分支)
- # ============================================
- deploy-production:
- image: alpine:latest
- environment:
- DEPLOY_ENV: production
- SSH_PRIVATE_KEY:
- from_secret: ssh_private_key
- REGISTRY_PASSWORD:
- from_secret: registry_password
- commands:
- - echo "Deploying to production environment..."
- - apk add --no-cache openssh-client curl
- - mkdir -p ~/.ssh
- - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- - chmod 600 ~/.ssh/id_rsa
- - ssh-keyscan -H 139.155.109.62 >> ~/.ssh/known_hosts
-
- # 前置检查
- - echo "Pre-deployment checks..."
- - ssh root@139.155.109.62 "echo 'Server connection OK'"
- - ssh root@139.155.109.62 "df -h | grep -E '/$|/home'"
- - ssh root@139.155.109.62 "docker ps | grep novalon-website || echo 'No existing container'"
-
- # 部署
- - |
- ssh root@139.155.109.62 << EOF
- set -e # 任何命令失败立即退出
- cd /home/novalon/docker-app/novalon-website
-
- echo "=== Step 1: Login to Registry ==="
- if ! echo "${REGISTRY_PASSWORD}" | docker login -u novalon-admin --password-stdin registry.f.novalon.cn; then
- echo "❌ Registry login failed!"
- exit 1
- fi
-
- echo "=== Step 2: Backup current version ==="
- BACKUP_TIME=\$(date +%Y%m%d_%H%M%S)
- docker tag registry.f.novalon.cn/novalon-website:latest registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} 2>/dev/null || echo "No existing image to backup"
-
- echo "=== Step 3: Pull new image ==="
- if ! docker-compose pull novalon-website; then
- echo "❌ Image pull failed!"
- exit 1
- fi
-
- echo "=== Step 4: Rolling update ==="
- docker-compose up -d --no-deps novalon-website
-
- echo "=== Step 5: Wait for service startup ==="
- sleep 10
-
- echo "=== Step 6: Database migration ==="
- if ! docker-compose exec -T novalon-website npm run db:migrate; then
- echo "❌ Database migration failed, rolling back..."
- docker tag registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} registry.f.novalon.cn/novalon-website:latest 2>/dev/null || true
- docker-compose pull novalon-website
- docker-compose up -d --no-deps novalon-website
- exit 1
- fi
-
- echo "=== Step 7: Health check ==="
- for i in {1..30}; do
- if curl -f https://novalon.cn/api/health; then
- echo "✅ Health check passed!"
-
- echo "=== Step 8: Cleanup old images ==="
- docker image prune -f
- docker images registry.f.novalon.cn/novalon-website --format "{{.ID}} {{.CreatedAt}}" | tail -n +4 | awk '{print \$1}' | xargs -r docker rmi -f || true
- exit 0
- fi
- echo "Waiting for service to be ready... (\$i/30)"
- sleep 2
- done
-
- echo "❌ Health check failed, rolling back..."
- docker tag registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} registry.f.novalon.cn/novalon-website:latest 2>/dev/null || true
- docker-compose pull novalon-website
- docker-compose up -d --no-deps novalon-website
- sleep 10
-
- # 验证回滚
- if curl -f https://novalon.cn/api/health; then
- echo "✅ Rollback succeeded, but deployment failed"
- else
- echo "❌ Rollback also failed!"
- fi
- exit 1
- EOF
- - echo "✅ Production deployment completed!"
- when:
- event:
- - push
- branch:
- - release
- - release/**
-
- # ============================================
- # 阶段6: 归档到main分支 (release分支)
- # ============================================
- archive-to-main:
- image: alpine:latest
- environment:
- SSH_PRIVATE_KEY:
- from_secret: ssh_private_key
- commands:
- - echo "Archiving to main branch..."
- - apk add --no-cache git openssh-client
- - mkdir -p ~/.ssh
- - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- - chmod 600 ~/.ssh/id_rsa
- - ssh-keyscan -H git.f.novalon.cn >> ~/.ssh/known_hosts
- - |
- set -e
- git config --global user.email "ci@novalon.cn"
- git config --global user.name "Woodpecker CI"
-
- # 使用SSH而不是HTTPS+Token
- git remote set-url origin git@git.f.novalon.cn:novalon/novalon-website.git
-
- # 拉取最新代码
- git fetch origin
- git checkout main
- git pull origin main
-
- # 合并release分支
- git merge release --no-ff -m "chore: 归档release ${CI_COMMIT_SHA:0:7}"
-
- # 创建版本标签
- VERSION_TAG="v$(date +%Y.%m.%d)-${CI_COMMIT_SHA:0:7}"
- git tag -a "$VERSION_TAG" -m "Release $(date +%Y-%m-%d)"
-
- # 推送到远程(带重试)
- for i in {1..3}; do
- if git push origin main && git push origin --tags; then
- echo "✅ Archive succeeded! Version: $VERSION_TAG"
- exit 0
- fi
- echo "Retry $i/3..."
- sleep 5
- done
-
- echo "⚠️ Archive failed, but deployment succeeded"
- echo "Manual archive may be needed"
- exit 0 # 不阻止部署成功
- when:
- event:
- - push
- branch:
- - release
- - release/**
- status:
- - success
-
-# ============================================
-# 服务配置
-# ============================================
-services:
- docker:
- image: docker:24-dind
- privileged: true
- environment:
- DOCKER_TLS_CERTDIR: ""
-
-# ============================================
-# 工作区配置
-# ============================================
-workspace:
- base: /woodpecker
- path: src
-
-# ============================================
-# 克隆配置
-# ============================================
-clone:
- git:
- image: woodpeckerci/plugin-git
- settings:
- depth: 1
- partial: false
diff --git a/Dockerfile b/Dockerfile
index 68b50e3..6289191 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,44 +1,21 @@
-FROM node:20-alpine AS base
+FROM node:20-alpine AS builder
-ARG CDN_DOMAIN
-ENV CDN_DOMAIN=${CDN_DOMAIN}
-
-FROM base AS deps
-RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci && npm cache clean --force
-FROM base AS builder
-WORKDIR /app
-COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
-FROM base AS runner
-WORKDIR /app
+FROM nginx:alpine
-ENV NODE_ENV=production
-ENV NEXT_TELEMETRY_DISABLED=1
+COPY --from=builder /app/dist /usr/share/nginx/html
+COPY nginx-static.conf /etc/nginx/nginx.conf
-RUN addgroup --system --gid 1001 nodejs && \
- adduser --system --uid 1001 nextjs
+EXPOSE 80
-COPY --from=builder /app/dist/standalone ./
-COPY --from=builder /app/dist/static ./dist/static
-COPY --from=builder /app/public ./public
-
-RUN chown -R nextjs:nodejs /app
-
-USER nextjs
-
-EXPOSE 3000
-
-ENV PORT=3000
-ENV HOSTNAME="0.0.0.0"
-
-CMD ["node", "server.js"]
\ No newline at end of file
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/README.md b/README.md
index 2b2f568..6f341e2 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
## 项目概述
-本项目是四川睿新致远科技有限公司的企业官网,采用 Next.js 16 + React 19 + TypeScript 技术栈构建,提供现代化的企业展示、产品服务介绍、案例展示、新闻动态和在线咨询等功能。
+本项目是四川睿新致远科技有限公司的企业官网,采用 Next.js 16 + React 19 + TypeScript 技术栈构建的纯静态网站,提供现代化的企业展示、产品服务介绍、案例展示、新闻动态等功能。
### 核心功能
@@ -13,10 +13,8 @@
- **产品展示** - 产品列表和详情页面
- **案例展示** - 成功案例列表和详情
- **新闻动态** - 公司新闻、产品发布、合作动态、行业资讯
-- **在线咨询** - 联系表单、公司信息展示
- **响应式设计** - 完美适配桌面端、平板和移动设备
- **SEO 优化** - 结构化数据、元信息优化
-- **CMS管理后台** - 内容管理、用户管理、配置中心、审计日志
## 技术栈
@@ -29,14 +27,9 @@
| 组件库 | shadcn/ui (Radix UI) | - |
| 动画 | Framer Motion | 12.x |
| 图标 | Lucide React | 0.563.0 |
-| 邮件服务 | Resend | 6.9.2 |
| 数据验证 | Zod | 4.3.6 |
| 图表 | @antv/g2 | 5.4.8 |
| 3D 效果 | Three.js | 0.183.1 |
-| 数据库 | SQLite | - |
-| ORM | Drizzle ORM | - |
-| 认证 | NextAuth.js | 5.x beta |
-| 富文本编辑 | Tiptap | - |
## 快速开始
@@ -51,37 +44,6 @@
npm install
```
-### 环境变量配置
-
-复制环境变量示例文件:
-
-```bash
-cp .env.example .env.local
-```
-
-配置必要的环境变量:
-
-```env
-# 邮件服务
-RESEND_API_KEY=your_resend_api_key
-COMPANY_EMAIL=contact@novalon.cn
-
-# 数据库
-DATABASE_URL=./data/novalon.db
-
-# NextAuth.js
-NEXTAUTH_SECRET=your_nextauth_secret
-NEXTAUTH_URL=http://localhost:3000
-
-# 文件上传
-UPLOAD_DIR=./uploads
-MAX_FILE_SIZE=10485760
-
-# 管理员账号(首次运行时创建)
-ADMIN_EMAIL=contact@novalon.cn
-ADMIN_PASSWORD=your_secure_password
-```
-
### 开发模式
```bash
@@ -98,10 +60,10 @@ npm run build
输出目录: `dist/`
-### 启动生产服务器
+### 预览生产版本
```bash
-npm start
+npm run preview
```
## 项目结构
@@ -109,7 +71,7 @@ npm start
```
novalon-website/
├── src/ # 源代码
-│ ├── app/ # Next.js App Router
+│ ├── app/ # Next.js App Router
│ │ ├── (marketing)/ # 营销页面路由组
│ │ │ ├── page.tsx # 首页
│ │ │ ├── about/ # 关于我们
@@ -119,22 +81,8 @@ novalon-website/
│ │ │ ├── products/ # 产品服务
│ │ │ ├── services/ # 核心业务
│ │ │ └── solutions/ # 解决方案
-│ │ ├── admin/ # 管理后台
-│ │ │ ├── page.tsx # 仪表盘
-│ │ │ ├── login/ # 登录页面
-│ │ │ ├── content/ # 内容管理
-│ │ │ ├── users/ # 用户管理
-│ │ │ ├── settings/ # 配置中心
-│ │ │ └── logs/ # 审计日志
-│ │ ├── api/ # API 路由
-│ │ │ ├── auth/ # 认证 API
-│ │ │ ├── contact/ # 联系表单 API
-│ │ │ └── admin/ # 管理 API
-│ │ │ ├── content/ # 内容管理
-│ │ │ ├── users/ # 用户管理
-│ │ │ ├── config/ # 配置管理
-│ │ │ ├── upload/ # 文件上传
-│ │ │ └── logs/ # 审计日志
+│ │ ├── privacy/ # 隐私政策
+│ │ ├── terms/ # 服务条款
│ │ ├── layout.tsx # 根布局
│ │ ├── error.tsx # 错误页面
│ │ └── not-found.tsx # 404 页面
@@ -144,76 +92,20 @@ novalon-website/
│ │ ├── sections/ # 页面区块组件
│ │ ├── effects/ # 视觉效果组件
│ │ ├── seo/ # SEO 组件
-│ │ ├── analytics/ # 分析组件
-│ │ └── admin/ # 管理后台组件
-│ ├── lib/ # 工具函数
-│ │ ├── api/ # API 服务
-│ │ ├── auth/ # 认证相关
-│ │ ├── db.ts # 数据库连接
-│ │ ├── audit.ts # 审计日志
-│ │ └── upload.ts # 文件上传
-│ ├── db/ # 数据库相关
-│ │ ├── schema.ts # 数据库 Schema
-│ │ ├── seed.ts # 种子数据
-│ │ └── migrations/ # 迁移文件
+│ │ └── analytics/ # 分析组件
│ ├── hooks/ # 自定义 Hooks
│ └── contexts/ # React Context
-├── e2e/ # E2E 测试(统一测试框架)
-│ ├── src/
-│ │ ├── tests/ # 测试用例
-│ │ │ ├── smoke/ # 冒烟测试
-│ │ │ ├── regression/ # 回归测试
-│ │ │ ├── api/ # API 测试
-│ │ │ ├── accessibility/ # 可访问性测试
-│ │ │ ├── performance/ # 性能测试
-│ │ │ ├── security/ # 安全测试
-│ │ │ └── visual/ # 视觉回归测试
-│ │ ├── pages/ # Page Object
-│ │ ├── fixtures/ # 测试 Fixtures
-│ │ └── config/ # 测试配置
-│ ├── playwright.config.ts
-│ └── MIGRATION.md # 测试框架迁移说明
+├── e2e/ # E2E 测试
+├── tests/ # 测试文件
+│ ├── performance/ # 性能测试
+│ └── styles/ # 样式测试
├── docs/ # 项目文档
-│ ├── architecture/ # 架构文档
-│ ├── development/ # 开发文档
-│ ├── deployment/ # 部署文档
-│ ├── testing/ # 测试文档
-│ ├── api/ # API 文档
-│ ├── guides/ # 使用指南
-│ ├── STRUCTURE_PLAN.md # 目录结构规划
-│ └── OPTIMIZATION_REPORT.md # 优化报告
├── scripts/ # 脚本文件
-│ ├── deployment/ # 部署脚本
-│ ├── monitoring/ # 监控脚本
-│ ├── testing/ # 测试脚本
-│ ├── maintenance/ # 维护脚本
-│ └── utils/ # 工具脚本
├── config/ # 配置文件
-│ ├── ci/ # CI/CD 配置
-│ ├── lint/ # 代码检查配置
-│ └── test/ # 测试配置
-├── reports/ # 测试报告
-│ ├── e2e/ # E2E 测试报告
-│ ├── performance/ # 性能测试报告
-│ └── coverage/ # 代码覆盖率报告
├── public/ # 静态资源
-├── uploads/ # 上传文件存储
-├── data/ # SQLite 数据库文件
└── dist/ # 构建输出
```
-### 项目优化说明
-
-本项目已于 2026-03-24 完成全面的工程化与规范化优化,包括:
-
-1. **测试体系整合** - 统一为 Playwright TypeScript 测试框架
-2. **目录结构规范化** - 建立清晰的目录结构,符合 Next.js 最佳实践
-3. **配置文件优化** - 合并重复配置,统一配置管理
-4. **文档体系完善** - 建立完整的文档体系和导航
-5. **代码质量提升** - 修复所有类型错误,确保构建成功
-
-详细信息请查看 [优化报告](docs/OPTIMIZATION_REPORT.md)
-
## 页面路由
| 路由 | 描述 |
@@ -231,13 +123,6 @@ novalon-website/
| `/contact` | 联系我们 |
| `/privacy` | 隐私政策 |
| `/terms` | 服务条款 |
-| `/admin` | 管理后台仪表盘 |
-| `/admin/login` | 管理员登录 |
-| `/admin/content` | 内容管理 |
-| `/admin/content/[id]` | 内容编辑 |
-| `/admin/users` | 用户管理 |
-| `/admin/settings` | 配置中心 |
-| `/admin/logs` | 审计日志 |
## NPM 脚本
@@ -247,14 +132,11 @@ novalon-website/
| `npm run build` | 构建生产版本 |
| `npm start` | 启动生产服务器 |
| `npm run lint` | 运行 ESLint 检查 |
+| `npm run type-check` | TypeScript 类型检查 |
| `npm run test` | 运行 E2E 测试 |
-| `npm run test:smoke` | 运行冒烟测试 |
-| `npm run check:contrast` | 检查颜色对比度 |
-| `npm run check:headings` | 检查标题层级 |
-| `npm run db:generate` | 生成数据库迁移文件 |
-| `npm run db:migrate` | 执行数据库迁移 |
-| `npm run db:seed` | 填充数据库种子数据 |
-| `npm run db:studio` | 启动 Drizzle Studio |
+| `npm run test:unit` | 运行单元测试 |
+| `npm run test:coverage` | 运行测试覆盖率 |
+| `npm run lighthouse` | 运行 Lighthouse 性能测试 |
## 代码质量门禁
@@ -264,18 +146,12 @@ novalon-website/
- **commitlint**: 提交信息规范
- **Jest**: 代码覆盖率检查
-详细信息请查看 [质量门禁文档](docs/development/quality-gates.md)。
-
### 提交规范
-使用Conventional Commits规范:
+使用 Conventional Commits 规范:
```
():
-
-
-
-
-
+
-
-
+
+
-
+
diff --git a/src/app/(marketing)/contact/actions.ts b/src/app/(marketing)/contact/actions.ts
deleted file mode 100644
index 70153aa..0000000
--- a/src/app/(marketing)/contact/actions.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-'use server';
-
-import { Resend } from 'resend';
-import { z } from 'zod';
-
-const resend = new Resend(process.env.RESEND_API_KEY);
-const companyEmail = process.env.COMPANY_EMAIL || 'contact@novalon.cn';
-
-const contactFormSchema = z.object({
- name: z.string().min(2, '姓名至少需要2个字符'),
- phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号码'),
- email: z.string().email('请输入有效的邮箱地址'),
- subject: z.string().min(2, '主题至少需要2个字符'),
- message: z.string().min(10, '留言内容至少需要10个字符'),
- website: z.string().optional(),
- submitTime: z.string().optional(),
- mathHash: z.string().optional(),
- mathTimestamp: z.string().optional(),
- mathAnswer: z.string().optional(),
-});
-
-export interface ContactFormState {
- success: boolean;
- message?: string;
- error?: string;
- errors?: Record;
-}
-
-export async function submitContactForm(
- _prevState: ContactFormState | null,
- formData: FormData
-): Promise {
- const rawData = {
- name: formData.get('name') as string,
- phone: formData.get('phone') as string,
- email: formData.get('email') as string,
- subject: formData.get('subject') as string,
- message: formData.get('message') as string,
- website: formData.get('website') as string,
- submitTime: formData.get('submitTime') as string,
- mathHash: formData.get('mathHash') as string,
- mathTimestamp: formData.get('mathTimestamp') as string,
- mathAnswer: formData.get('mathAnswer') as string,
- };
-
- const validationResult = contactFormSchema.safeParse(rawData);
-
- if (!validationResult.success) {
- const errors: Record = {};
- validationResult.error.issues.forEach((issue) => {
- const field = issue.path[0] as string;
- errors[field] = issue.message;
- });
- return { success: false, error: '请检查表单字段', errors };
- }
-
- const data = validationResult.data;
-
- if (data.website) {
- console.log('Honeypot field filled, rejecting request');
- return { success: true, message: '消息已发送' };
- }
-
- if (data.submitTime) {
- const timeDiff = Date.now() - parseInt(data.submitTime);
- if (timeDiff < 2000) {
- console.log('Submission too fast:', timeDiff);
- return { success: false, error: '提交过快,请稍后再试' };
- }
- }
-
- if (data.mathHash && data.mathTimestamp && data.mathAnswer !== undefined) {
- const expectedHash = btoa(`${data.mathAnswer}-${data.mathTimestamp}`);
- if (expectedHash !== data.mathHash) {
- console.log('Invalid math captcha');
- return { success: false, error: '验证码错误,请重新计算' };
- }
- }
-
- const emailContent = `
-
-
-
-
-
-
-
-
-
-
-
新消息
-
-
-
-
- ${data.phone ? `
-
- ` : ''}
-
-
-
-
-
咨询内容
-
${data.message}
-
-
-
-
-
-
-
-
-
-
- `;
-
- try {
- const { data: emailData, error } = await resend.emails.send({
- from: '睿新致远官网 ',
- to: [companyEmail],
- subject: `📧 ${data.subject} - ${data.name}`,
- html: emailContent,
- replyTo: data.email,
- });
-
- if (error) {
- console.error('Resend API error:', error);
- return { success: false, error: '邮件发送失败,请稍后重试' };
- }
-
- console.log('Email sent successfully:', emailData);
- return { success: true, message: '消息已发送' };
- } catch (error) {
- console.error('Contact form submission error:', error);
- return { success: false, error: '提交失败,请重试' };
- }
-}
diff --git a/src/app/(marketing)/contact/page.tsx b/src/app/(marketing)/contact/page.tsx
index 89b7bc5..57072c5 100644
--- a/src/app/(marketing)/contact/page.tsx
+++ b/src/app/(marketing)/contact/page.tsx
@@ -1,16 +1,13 @@
'use client';
-import { useState, useEffect, useRef, useActionState } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Toast } from '@/components/ui/toast';
-import { sanitizeInput } from '@/lib/sanitize';
-import { generateCSRFToken, setCSRFTokenToStorage } from '@/lib/csrf';
import { Mail, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2 } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
-import { submitContactForm, ContactFormState } from './actions';
const contactFormSchema = z.object({
name: z.string().min(2, '姓名至少需要2个字符'),
@@ -35,7 +32,8 @@ export default function ContactPage() {
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [toastType, setToastType] = useState<'success' | 'error'>('success');
- const [csrfToken, setCsrfToken] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isSubmitted, setIsSubmitted] = useState(false);
const [formData, setFormData] = useState({
name: '',
phone: '',
@@ -46,47 +44,12 @@ export default function ContactPage() {
const [errors, setErrors] = useState({});
const sectionRef = useRef(null);
- const [state, formAction, isPending] = useActionState(
- submitContactForm,
- null as ContactFormState | null
- );
-
- const isSubmitted = state?.success === true;
- const isSubmitting = isPending;
-
useEffect(() => {
requestAnimationFrame(() => {
setIsVisible(true);
- const token = generateCSRFToken();
- setCsrfToken(token);
- setCSRFTokenToStorage(token);
});
}, []);
- useEffect(() => {
- if (state) {
- requestAnimationFrame(() => {
- if (state.success) {
- setToastMessage(state.message || '表单提交成功!我们会尽快与您联系。');
- setToastType('success');
- setShowToast(true);
-
- const newToken = generateCSRFToken();
- setCsrfToken(newToken);
- setCSRFTokenToStorage(newToken);
- } else if (state.error) {
- setToastMessage(state.error);
- setToastType('error');
- setShowToast(true);
-
- if (state.errors) {
- setErrors(state.errors);
- }
- }
- });
- }
- }, [state]);
-
const validateField = (field: keyof ContactFormData, value: string) => {
try {
contactFormSchema.shape[field].parse(value);
@@ -102,10 +65,9 @@ export default function ContactPage() {
};
const handleChange = (field: keyof ContactFormData, value: string) => {
- const sanitizedValue = sanitizeInput(value);
- setFormData((prev) => ({ ...prev, [field]: sanitizedValue }));
+ setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
- validateField(field, sanitizedValue);
+ validateField(field, value);
}
};
@@ -113,16 +75,9 @@ export default function ContactPage() {
validateField(field, value);
};
- function handleSubmit(e: React.FormEvent) {
+ async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
- if (!csrfToken) {
- setToastMessage('安全验证失败,请刷新页面重试。');
- setToastType('error');
- setShowToast(true);
- return;
- }
-
const result = contactFormSchema.safeParse(formData);
if (!result.success) {
@@ -135,14 +90,36 @@ export default function ContactPage() {
return;
}
- const form = e.currentTarget;
- const formDataObj = new FormData(form);
- formDataObj.set('submitTime', Date.now().toString());
- formAction(formDataObj);
+ setIsSubmitting(true);
+ try {
+ const response = await fetch('https://formspree.io/f/' + process.env.NEXT_PUBLIC_FORMSPREE_ID, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
+ body: JSON.stringify(formData),
+ });
+
+ if (response.ok) {
+ setIsSubmitted(true);
+ setToastMessage('表单提交成功!我们会尽快与您联系。');
+ setToastType('success');
+ setShowToast(true);
+ setFormData({ name: '', phone: '', email: '', subject: '', message: '' });
+ } else {
+ setToastMessage('提交失败,请稍后重试或直接发送邮件联系我们。');
+ setToastType('error');
+ setShowToast(true);
+ }
+ } catch {
+ setToastMessage('网络错误,请稍后重试。');
+ setToastType('error');
+ setShowToast(true);
+ } finally {
+ setIsSubmitting(false);
+ }
}
return (
-
+
{showToast && (
) : (