From 6403489954318b59a3e0fffce53d9355714a1fa4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?=
Date: Tue, 21 Apr 2026 07:53:56 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AE=8C=E6=88=90=E9=9D=99?=
=?UTF-8?q?=E6=80=81=E7=BD=91=E7=AB=99=E8=BD=AC=E6=8D=A2=EF=BC=8C=E7=A7=BB?=
=?UTF-8?q?=E9=99=A4=E6=89=80=E6=9C=89=20CMS=20=E5=92=8C=E5=8A=A8=E6=80=81?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 删除数据库相关代码 (src/db/)
- 删除 API 路由 (src/app/api/)
- 删除认证相关代码 (src/lib/auth/, src/providers/)
- 删除监控和安全中间件 (src/lib/security/, src/lib/monitoring/)
- 删除 hooks (use-news, use-products, use-services)
- 更新组件为静态数据源
- 添加 nginx 静态配置和部署脚本
- 添加 static-link 组件
---
.env.example | 11 +-
.gitignore | 3 +
.woodpecker.yml | 401 -------------
Dockerfile | 35 +-
README.md | 558 ++----------------
config/ci/ci.yml | 28 -
config/ci/deploy.yml | 12 -
config/ci/quality-gate.yml | 71 ---
config/ci/test-tiered-simple.yml | 50 --
config/ci/test-tiered.yml | 102 ----
docker-compose.high-perf.yml | 125 ----
docker-compose.prod.yml | 30 -
docker-compose.yml | 31 +-
docs/ADMIN-CREDENTIALS.md | 111 ----
docs/CICD_QUICK_START.md | 357 -----------
docs/CONTACT_CONFIGURATION.md | 164 -----
docs/api-versioning-guide.md | 512 ----------------
docs/api.md | 382 ------------
docs/deployment/quality-gates-ci.md | 104 ----
docs/openapi-guide.md | 466 ---------------
docs/test-tiering-best-practices.md | 450 --------------
docs/testing/README-TIERED-TESTING.md | 295 ---------
drizzle.config.ts | 10 -
drizzle/0000_white_justice.sql | 71 ---
drizzle/0001_clammy_toro.sql | 17 -
drizzle/meta/0000_snapshot.json | 500 ----------------
drizzle/meta/0001_snapshot.json | 502 ----------------
drizzle/meta/_journal.json | 20 -
e2e/admin-frontend-interaction.spec.ts | 332 -----------
e2e/admin-publish-core.spec.ts | 198 -------
e2e/admin-publish.spec.ts | 507 ----------------
ecosystem.config.js | 46 --
forgejo-14.0.3-amd64.tar.gz | Bin 20 -> 0 bytes
forgejo-app.ini | 68 ---
nginx-docker-compose.yml | 22 -
nginx-individual.conf | 270 ---------
nginx-static.conf | 100 ++++
nginx-temp-for-cert.conf | 216 -------
nginx-wildcard.conf | 270 ---------
nginx.conf | 99 ----
novalon-nginx/docker-compose.yml | 21 -
novalon-website/docker-compose.yml | 24 -
package.json | 9 +
playwright-test-production.js | 149 -----
scripts/deploy-static.sh | 84 +++
scripts/deployment/backup.sh | 53 --
scripts/deployment/deploy-production.sh | 32 +-
scripts/deployment/restore.sh | 69 ---
scripts/set-woodpecker-trusted.sh | 62 --
scripts/setup-woodpecker-secrets.sh | 125 ----
scripts/testing/verify-tiered-testing.sh | 164 -----
scripts/utils/validate-woodpecker-config.js | 59 --
sentry.client.config.ts | 21 -
sentry.server.config.ts | 13 -
setup-ssl.sh | 38 --
src/app/(marketing)/cases/[id]/client.tsx | 8 +-
src/app/(marketing)/cases/[id]/page.tsx | 35 +-
src/app/(marketing)/cases/page.test.tsx | 265 ---------
src/app/(marketing)/cases/page.tsx | 96 +--
src/app/(marketing)/contact/actions.ts | 266 ---------
src/app/(marketing)/contact/page.tsx | 90 ++-
src/app/(marketing)/home-content.tsx | 22 +-
src/app/(marketing)/layout.tsx | 2 +-
.../news/[slug]/NewsDetailClient.tsx | 17 +-
src/app/(marketing)/news/page.tsx | 49 +-
src/app/(marketing)/page.tsx | 30 +-
src/app/(marketing)/products/[id]/page.tsx | 10 +-
src/app/(marketing)/products/page.tsx | 51 +-
src/app/(marketing)/services/[id]/client.tsx | 68 +--
src/app/(marketing)/services/page.tsx | 61 +-
src/app/(marketing)/solutions/page.tsx | 10 +-
src/app/admin/content/[id]/page.test.tsx | 83 ---
src/app/admin/content/[id]/page.tsx | 396 -------------
src/app/admin/content/page.test.tsx | 90 ---
src/app/admin/content/page.tsx | 324 ----------
src/app/admin/layout.tsx | 154 -----
src/app/admin/login/page.test.tsx | 100 ----
src/app/admin/login/page.tsx | 123 ----
src/app/admin/page.test.tsx | 108 ----
src/app/admin/page.tsx | 161 -----
src/app/admin/security/page.test.tsx | 148 -----
src/app/admin/security/page.tsx | 271 ---------
src/app/admin/settings/page.test.tsx | 57 --
src/app/admin/settings/page.tsx | 278 ---------
src/app/admin/users/page.test.tsx | 62 --
src/app/admin/users/page.tsx | 422 -------------
src/app/api-docs/page.tsx | 69 ---
src/app/api/admin/config/route.test.ts | 179 ------
src/app/api/admin/config/route.ts | 209 -------
src/app/api/admin/content/[id]/route.test.ts | 188 ------
src/app/api/admin/content/[id]/route.ts | 185 ------
src/app/api/admin/content/route.test.ts | 143 -----
src/app/api/admin/content/route.ts | 296 ----------
src/app/api/admin/security/route.ts | 26 -
src/app/api/admin/upload/route.test.ts | 102 ----
src/app/api/admin/upload/route.ts | 84 ---
src/app/api/admin/users/[id]/route.test.ts | 119 ----
src/app/api/admin/users/[id]/route.ts | 121 ----
src/app/api/admin/users/route.test.ts | 102 ----
src/app/api/admin/users/route.ts | 33 --
src/app/api/auth/[...nextauth]/route.test.ts | 39 --
src/app/api/auth/[...nextauth]/route.ts | 5 -
src/app/api/config/route.ts | 25 -
src/app/api/contact/route.test.ts | 316 ----------
src/app/api/contact/route.ts | 135 -----
src/app/api/content/route.ts | 54 --
src/app/api/docs/route.ts | 188 ------
src/app/api/health/route.test.ts | 83 ---
src/app/api/health/route.ts | 101 ----
src/app/api/v1/config/route.ts | 25 -
src/app/api/v1/health/route.ts | 132 -----
src/app/error.tsx | 20 +-
src/app/not-found.tsx | 28 +-
src/components/admin/RichTextEditor.tsx | 237 --------
src/components/analytics/GoogleAnalytics.tsx | 12 +-
src/components/analytics/analytics.test.tsx | 21 -
src/components/analytics/web-vitals.tsx | 33 --
src/components/layout/breadcrumb.tsx | 10 +-
src/components/layout/footer.tsx | 38 +-
src/components/layout/header.tsx | 33 +-
src/components/layout/mobile-tab-bar.tsx | 6 +-
src/components/sections/about-section.tsx | 6 +-
.../sections/cases-section.test.tsx | 172 ------
src/components/sections/cases-section.tsx | 58 +-
src/components/sections/contact-section.tsx | 172 ++----
.../sections/hero-section-atoms.tsx | 8 +-
src/components/sections/hero-section.tsx | 2 +-
.../news-section.integration.test.tsx | 513 ----------------
src/components/sections/news-section.tsx | 75 +--
.../products-section.integration.test.tsx | 472 ---------------
src/components/sections/products-section.tsx | 67 +--
.../services-section.integration.test.tsx | 347 -----------
src/components/sections/services-section.tsx | 70 +--
src/components/ui/back-button.tsx | 11 +-
src/components/ui/static-link.tsx | 76 +++
src/db/index.ts | 8 -
src/db/mutations.test.ts | 340 -----------
src/db/queries.test.ts | 255 --------
src/db/schema.test.ts | 86 ---
src/db/schema.ts | 86 ---
src/db/seed-test-data.ts | 307 ----------
src/db/seed.ts | 116 ----
src/hooks/use-news.test.ts | 134 -----
src/hooks/use-news.ts | 32 -
src/hooks/use-products.test.ts | 121 ----
src/hooks/use-products.ts | 28 -
src/hooks/use-services.test.ts | 121 ----
src/hooks/use-services.ts | 28 -
src/lib/analytics.test.ts | 95 ---
src/lib/api/client.test.ts | 211 -------
src/lib/api/client.ts | 221 -------
src/lib/api/services.test.ts | 426 -------------
src/lib/api/services.ts | 151 -----
src/lib/api/types.ts | 66 ---
src/lib/audit.test.ts | 106 ----
src/lib/audit.ts | 59 --
src/lib/auth.test.ts | 209 -------
src/lib/auth.ts | 74 ---
src/lib/auth/check-permission.test.ts | 152 -----
src/lib/auth/check-permission.ts | 75 ---
src/lib/auth/permissions.test.ts | 276 ---------
src/lib/auth/permissions.ts | 42 --
src/lib/auth/session.test.ts | 198 -------
src/lib/auth/session.ts | 41 --
src/lib/csrf.test.ts | 66 ---
src/lib/csrf.ts | 29 -
src/lib/email-templates.test.ts | 378 ------------
src/lib/email-templates.ts | 150 -----
src/lib/integration.test.ts | 169 ------
src/lib/monitoring.test.ts | 200 -------
src/lib/monitoring.ts | 75 ---
src/lib/sanitize.ts | 42 --
src/lib/security/captcha.test.ts | 46 --
src/lib/security/captcha.ts | 121 ----
src/lib/security/config.test.ts | 23 -
src/lib/security/config.ts | 77 ---
src/lib/security/logger.test.ts | 49 --
src/lib/security/logger.ts | 143 -----
src/lib/security/middleware.test.ts | 79 ---
src/lib/security/middleware.ts | 129 ----
src/lib/security/rate-limiter.test.ts | 58 --
src/lib/security/rate-limiter.ts | 119 ----
src/lib/security/sanitizer.test.ts | 39 --
src/lib/security/sanitizer.ts | 105 ----
src/lib/sentry.test.ts | 53 --
src/lib/sentry.ts | 15 -
src/lib/upload.test.ts | 445 --------------
src/lib/upload.ts | 190 ------
src/lib/validation.test.ts | 275 ---------
src/lib/validation.ts | 80 ---
src/middleware.test.ts | 132 -----
src/middleware.ts | 53 --
src/providers/session-provider.test.tsx | 30 -
src/providers/session-provider.tsx | 8 -
src/types/next-auth.d.ts | 21 -
tests/security/sql-injection-test.js | 83 ---
tests/security/xss-test.js | 98 ---
197 files changed, 654 insertions(+), 24762 deletions(-)
delete mode 100644 .woodpecker.yml
delete mode 100644 config/ci/ci.yml
delete mode 100644 config/ci/deploy.yml
delete mode 100644 config/ci/quality-gate.yml
delete mode 100644 config/ci/test-tiered-simple.yml
delete mode 100644 config/ci/test-tiered.yml
delete mode 100644 docker-compose.high-perf.yml
delete mode 100644 docker-compose.prod.yml
delete mode 100644 docs/ADMIN-CREDENTIALS.md
delete mode 100644 docs/CICD_QUICK_START.md
delete mode 100644 docs/CONTACT_CONFIGURATION.md
delete mode 100644 docs/api-versioning-guide.md
delete mode 100644 docs/api.md
delete mode 100644 docs/deployment/quality-gates-ci.md
delete mode 100644 docs/openapi-guide.md
delete mode 100644 docs/test-tiering-best-practices.md
delete mode 100644 docs/testing/README-TIERED-TESTING.md
delete mode 100644 drizzle.config.ts
delete mode 100644 drizzle/0000_white_justice.sql
delete mode 100644 drizzle/0001_clammy_toro.sql
delete mode 100644 drizzle/meta/0000_snapshot.json
delete mode 100644 drizzle/meta/0001_snapshot.json
delete mode 100644 drizzle/meta/_journal.json
delete mode 100644 e2e/admin-frontend-interaction.spec.ts
delete mode 100644 e2e/admin-publish-core.spec.ts
delete mode 100644 e2e/admin-publish.spec.ts
delete mode 100644 ecosystem.config.js
delete mode 100644 forgejo-14.0.3-amd64.tar.gz
delete mode 100644 forgejo-app.ini
delete mode 100644 nginx-docker-compose.yml
delete mode 100644 nginx-individual.conf
create mode 100644 nginx-static.conf
delete mode 100644 nginx-temp-for-cert.conf
delete mode 100644 nginx-wildcard.conf
delete mode 100644 nginx.conf
delete mode 100644 novalon-nginx/docker-compose.yml
delete mode 100644 novalon-website/docker-compose.yml
delete mode 100644 playwright-test-production.js
create mode 100755 scripts/deploy-static.sh
delete mode 100755 scripts/deployment/backup.sh
delete mode 100755 scripts/deployment/restore.sh
delete mode 100644 scripts/set-woodpecker-trusted.sh
delete mode 100644 scripts/setup-woodpecker-secrets.sh
delete mode 100755 scripts/testing/verify-tiered-testing.sh
delete mode 100644 scripts/utils/validate-woodpecker-config.js
delete mode 100644 sentry.client.config.ts
delete mode 100644 sentry.server.config.ts
delete mode 100755 setup-ssl.sh
delete mode 100644 src/app/(marketing)/cases/page.test.tsx
delete mode 100644 src/app/(marketing)/contact/actions.ts
delete mode 100644 src/app/admin/content/[id]/page.test.tsx
delete mode 100644 src/app/admin/content/[id]/page.tsx
delete mode 100644 src/app/admin/content/page.test.tsx
delete mode 100644 src/app/admin/content/page.tsx
delete mode 100644 src/app/admin/layout.tsx
delete mode 100644 src/app/admin/login/page.test.tsx
delete mode 100644 src/app/admin/login/page.tsx
delete mode 100644 src/app/admin/page.test.tsx
delete mode 100644 src/app/admin/page.tsx
delete mode 100644 src/app/admin/security/page.test.tsx
delete mode 100644 src/app/admin/security/page.tsx
delete mode 100644 src/app/admin/settings/page.test.tsx
delete mode 100644 src/app/admin/settings/page.tsx
delete mode 100644 src/app/admin/users/page.test.tsx
delete mode 100644 src/app/admin/users/page.tsx
delete mode 100644 src/app/api-docs/page.tsx
delete mode 100644 src/app/api/admin/config/route.test.ts
delete mode 100644 src/app/api/admin/config/route.ts
delete mode 100644 src/app/api/admin/content/[id]/route.test.ts
delete mode 100644 src/app/api/admin/content/[id]/route.ts
delete mode 100644 src/app/api/admin/content/route.test.ts
delete mode 100644 src/app/api/admin/content/route.ts
delete mode 100644 src/app/api/admin/security/route.ts
delete mode 100644 src/app/api/admin/upload/route.test.ts
delete mode 100644 src/app/api/admin/upload/route.ts
delete mode 100644 src/app/api/admin/users/[id]/route.test.ts
delete mode 100644 src/app/api/admin/users/[id]/route.ts
delete mode 100644 src/app/api/admin/users/route.test.ts
delete mode 100644 src/app/api/admin/users/route.ts
delete mode 100644 src/app/api/auth/[...nextauth]/route.test.ts
delete mode 100644 src/app/api/auth/[...nextauth]/route.ts
delete mode 100644 src/app/api/config/route.ts
delete mode 100644 src/app/api/contact/route.test.ts
delete mode 100644 src/app/api/contact/route.ts
delete mode 100644 src/app/api/content/route.ts
delete mode 100644 src/app/api/docs/route.ts
delete mode 100644 src/app/api/health/route.test.ts
delete mode 100644 src/app/api/health/route.ts
delete mode 100644 src/app/api/v1/config/route.ts
delete mode 100644 src/app/api/v1/health/route.ts
delete mode 100644 src/components/admin/RichTextEditor.tsx
delete mode 100644 src/components/analytics/analytics.test.tsx
delete mode 100644 src/components/analytics/web-vitals.tsx
delete mode 100644 src/components/sections/cases-section.test.tsx
delete mode 100644 src/components/sections/news-section.integration.test.tsx
delete mode 100644 src/components/sections/products-section.integration.test.tsx
delete mode 100644 src/components/sections/services-section.integration.test.tsx
create mode 100644 src/components/ui/static-link.tsx
delete mode 100644 src/db/index.ts
delete mode 100644 src/db/mutations.test.ts
delete mode 100644 src/db/queries.test.ts
delete mode 100644 src/db/schema.test.ts
delete mode 100644 src/db/schema.ts
delete mode 100644 src/db/seed-test-data.ts
delete mode 100644 src/db/seed.ts
delete mode 100644 src/hooks/use-news.test.ts
delete mode 100644 src/hooks/use-news.ts
delete mode 100644 src/hooks/use-products.test.ts
delete mode 100644 src/hooks/use-products.ts
delete mode 100644 src/hooks/use-services.test.ts
delete mode 100644 src/hooks/use-services.ts
delete mode 100644 src/lib/analytics.test.ts
delete mode 100644 src/lib/api/client.test.ts
delete mode 100644 src/lib/api/client.ts
delete mode 100644 src/lib/api/services.test.ts
delete mode 100644 src/lib/api/services.ts
delete mode 100644 src/lib/api/types.ts
delete mode 100644 src/lib/audit.test.ts
delete mode 100644 src/lib/audit.ts
delete mode 100644 src/lib/auth.test.ts
delete mode 100644 src/lib/auth.ts
delete mode 100644 src/lib/auth/check-permission.test.ts
delete mode 100644 src/lib/auth/check-permission.ts
delete mode 100644 src/lib/auth/permissions.test.ts
delete mode 100644 src/lib/auth/permissions.ts
delete mode 100644 src/lib/auth/session.test.ts
delete mode 100644 src/lib/auth/session.ts
delete mode 100644 src/lib/csrf.test.ts
delete mode 100644 src/lib/csrf.ts
delete mode 100644 src/lib/email-templates.test.ts
delete mode 100644 src/lib/email-templates.ts
delete mode 100644 src/lib/integration.test.ts
delete mode 100644 src/lib/monitoring.test.ts
delete mode 100644 src/lib/monitoring.ts
delete mode 100644 src/lib/sanitize.ts
delete mode 100644 src/lib/security/captcha.test.ts
delete mode 100644 src/lib/security/captcha.ts
delete mode 100644 src/lib/security/config.test.ts
delete mode 100644 src/lib/security/config.ts
delete mode 100644 src/lib/security/logger.test.ts
delete mode 100644 src/lib/security/logger.ts
delete mode 100644 src/lib/security/middleware.test.ts
delete mode 100644 src/lib/security/middleware.ts
delete mode 100644 src/lib/security/rate-limiter.test.ts
delete mode 100644 src/lib/security/rate-limiter.ts
delete mode 100644 src/lib/security/sanitizer.test.ts
delete mode 100644 src/lib/security/sanitizer.ts
delete mode 100644 src/lib/sentry.test.ts
delete mode 100644 src/lib/sentry.ts
delete mode 100644 src/lib/upload.test.ts
delete mode 100644 src/lib/upload.ts
delete mode 100644 src/lib/validation.test.ts
delete mode 100644 src/lib/validation.ts
delete mode 100644 src/middleware.test.ts
delete mode 100644 src/middleware.ts
delete mode 100644 src/providers/session-provider.test.tsx
delete mode 100644 src/providers/session-provider.tsx
delete mode 100644 src/types/next-auth.d.ts
delete mode 100644 tests/security/sql-injection-test.js
delete mode 100644 tests/security/xss-test.js
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 && (
) : (