first commit
154
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
name: Deploy to Cloudflare Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'deploy-*' # 只在推送 deploy-* 标签时触发
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: swr.cn-south-1.myhuaweicloud.com/bws/node:20.19.6-bookworm-slim-ci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 1. 拉取代码
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# 2. 设置 pnpm(使用 corepack,避免从 GitHub 拉取 action)
|
||||||
|
- name: Setup pnpm
|
||||||
|
run: |
|
||||||
|
corepack enable
|
||||||
|
corepack prepare pnpm@10.23.0 --activate
|
||||||
|
pnpm --version
|
||||||
|
|
||||||
|
- name: Parse tag to env
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# 获取 tag 名称(Gitea Actions 使用 GITHUB_REF_NAME)
|
||||||
|
TAG_NAME="${GITHUB_REF_NAME}"
|
||||||
|
echo "TAG_NAME=$TAG_NAME"
|
||||||
|
|
||||||
|
# Tag 格式: deploy-{project_name}-{deploymentId_no_dashes}
|
||||||
|
# 例如: deploy-b7ea026a-cf09-4e31-9f29-b55d7c652b71-123e4567e89b12d3a456426614174000
|
||||||
|
|
||||||
|
# 去掉 "deploy-" 前缀
|
||||||
|
PREFIX="deploy-"
|
||||||
|
REST="${TAG_NAME#$PREFIX}"
|
||||||
|
|
||||||
|
# deploymentId(无破折号)固定是最后32个字符
|
||||||
|
DEPLOYMENT_ID="${REST: -32}"
|
||||||
|
|
||||||
|
# project_name 是剩余部分(去掉最后的 "-" 和 deploymentId)
|
||||||
|
PROJECT_NAME="${REST%-${DEPLOYMENT_ID}}"
|
||||||
|
|
||||||
|
echo "PROJECT_NAME=$PROJECT_NAME" >> "$GITHUB_ENV"
|
||||||
|
echo "DEPLOYMENT_ID=$DEPLOYMENT_ID" >> "$GITHUB_ENV"
|
||||||
|
#echo "DOMAIN=${PROJECT_NAME}-preview.turingflowai.com" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
# 调试输出
|
||||||
|
echo "Parsed PROJECT_NAME: $PROJECT_NAME"
|
||||||
|
echo "Parsed DEPLOYMENT_ID: $DEPLOYMENT_ID"
|
||||||
|
|
||||||
|
- name: Check toolchain (debug only, 可选)
|
||||||
|
run: |
|
||||||
|
node -v || echo "node not found"
|
||||||
|
pnpm -v || echo "pnpm not found"
|
||||||
|
curl --version || echo "curl not found"
|
||||||
|
|
||||||
|
- name: Use CN npm registry
|
||||||
|
run: |
|
||||||
|
pnpm config set registry http://repo.myhuaweicloud.com/repository/npm/
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
||||||
|
PROJECT_NAME: ${{ env.PROJECT_NAME }}
|
||||||
|
DOMAIN: ${{ env.DOMAIN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[deploy] project: $PROJECT_NAME"
|
||||||
|
#echo "[deploy] domain: $DOMAIN"
|
||||||
|
|
||||||
|
# 部署到 Cloudflare Pages (假定构建产物在 dist/)
|
||||||
|
# 使用项目本地安装的 wrangler
|
||||||
|
npx wrangler pages deploy dist \
|
||||||
|
--project-name "$PROJECT_NAME" \
|
||||||
|
--branch main
|
||||||
|
|
||||||
|
# 绑定自定义域名:<project_name>-preview.turingflowai.com
|
||||||
|
#echo "[deploy] 正在绑定自定义域名..."
|
||||||
|
#DOMAIN_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
|
# "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT_NAME}/domains" \
|
||||||
|
# -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
|
||||||
|
# -H "Content-Type: application/json" \
|
||||||
|
# -d '{"name":"'"${DOMAIN}"'"}')
|
||||||
|
|
||||||
|
#HTTP_CODE=$(echo "$DOMAIN_RESPONSE" | tail -n1)
|
||||||
|
#RESPONSE_BODY=$(echo "$DOMAIN_RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
#if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "409" ]; then
|
||||||
|
# echo "[deploy] 域名绑定成功或已存在 (HTTP $HTTP_CODE)"
|
||||||
|
#else
|
||||||
|
# echo "[deploy] 警告: 域名绑定失败 (HTTP $HTTP_CODE)"
|
||||||
|
# echo "[deploy] 响应: $RESPONSE_BODY"
|
||||||
|
# echo "[deploy] 继续执行,但域名可能未绑定成功"
|
||||||
|
#fi
|
||||||
|
|
||||||
|
- name: Notify Deploy Service (success)
|
||||||
|
if: success()
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DEPLOY_SERVICE_CALLBACK_URL: ${{ secrets.DEPLOY_SERVICE_CALLBACK_URL }}
|
||||||
|
DEPLOY_SERVICE_TOKEN: ${{ secrets.DEPLOY_SERVICE_TOKEN }}
|
||||||
|
DEPLOYMENT_ID: ${{ env.DEPLOYMENT_ID }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 获取当前 commit SHA (Gitea Actions 使用 GITHUB_SHA)
|
||||||
|
COMMIT_SHA="${GITHUB_SHA}"
|
||||||
|
|
||||||
|
curl -X POST "$DEPLOY_SERVICE_CALLBACK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $DEPLOY_SERVICE_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"deploymentId": "'"${DEPLOYMENT_ID}"'",
|
||||||
|
"status": "deployed",
|
||||||
|
"commitSha": "'"${COMMIT_SHA}"'",
|
||||||
|
"cfDeploymentId": "",
|
||||||
|
"errorMessage": null
|
||||||
|
}'
|
||||||
|
|
||||||
|
- name: Notify Deploy Service (failure)
|
||||||
|
if: failure()
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DEPLOY_SERVICE_CALLBACK_URL: ${{ secrets.DEPLOY_SERVICE_CALLBACK_URL }}
|
||||||
|
DEPLOY_SERVICE_TOKEN: ${{ secrets.DEPLOY_SERVICE_TOKEN }}
|
||||||
|
DEPLOYMENT_ID: ${{ env.DEPLOYMENT_ID }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 获取当前 commit SHA
|
||||||
|
COMMIT_SHA="${GITHUB_SHA}"
|
||||||
|
|
||||||
|
curl -X POST "$DEPLOY_SERVICE_CALLBACK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $DEPLOY_SERVICE_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"deploymentId": "'"${DEPLOYMENT_ID}"'",
|
||||||
|
"status": "failed",
|
||||||
|
"commitSha": "'"${COMMIT_SHA}"'",
|
||||||
|
"cfDeploymentId": "",
|
||||||
|
"errorMessage": "see Gitea Actions logs"
|
||||||
|
}'
|
||||||
157
.gitignore
vendored
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Dependencies
|
||||||
|
# ===================
|
||||||
|
node_modules/
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Next.js
|
||||||
|
# ===================
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
/build/
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Production
|
||||||
|
# ===================
|
||||||
|
/dist/
|
||||||
|
*.min.js
|
||||||
|
*.min.css
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Testing
|
||||||
|
# ===================
|
||||||
|
/coverage/
|
||||||
|
.nyc_output
|
||||||
|
*.lcov
|
||||||
|
jest-results.json
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# TypeScript
|
||||||
|
# ===================
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Environment Variables
|
||||||
|
# ===================
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# IDE & Editors
|
||||||
|
# ===================
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# OS Generated Files
|
||||||
|
# ===================
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Logs
|
||||||
|
# ===================
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Cache
|
||||||
|
# ===================
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
*.cache
|
||||||
|
.turbo/
|
||||||
|
.tanstack
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Vercel
|
||||||
|
# ===================
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Debug
|
||||||
|
# ===================
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
*.p12
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Misc
|
||||||
|
# ===================
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
*.orig
|
||||||
|
.temp/
|
||||||
|
.tmp/
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Storybook
|
||||||
|
# ===================
|
||||||
|
storybook-static/
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# PWA
|
||||||
|
# ===================
|
||||||
|
public/sw.js
|
||||||
|
public/workbox-*.js
|
||||||
|
public/sw.js.map
|
||||||
|
public/workbox-*.js.map
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Sentry
|
||||||
|
# ===================
|
||||||
|
.sentryclirc
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Docker
|
||||||
|
# ===================
|
||||||
|
docker-compose.override.yml
|
||||||
358
README.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# turingflow-landing-003
|
||||||
|
|
||||||
|
圣诞节/节日主题单页落地页,适用于促销活动、节日营销、优惠券发放等场景。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- React 19.0.0 (已锁定版本)
|
||||||
|
- Vite 7.2.4
|
||||||
|
- Tailwind CSS 4.1.18
|
||||||
|
- Font Awesome 4.7 (图标)
|
||||||
|
- Google Fonts (Varela, Muli, Great Vibes, Marmelad)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
开发服务器运行在 `http://localhost:3000`
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
turingflow-landing-003/
|
||||||
|
├── public/ # 静态资源
|
||||||
|
│ ├── img/ # 图片资源
|
||||||
|
│ │ ├── iconlogo.png # 导航栏 Logo
|
||||||
|
│ │ ├── homeimg.png # Header 区域主图 (手机展示图)
|
||||||
|
│ │ ├── pic1.jpg # Header 背景图
|
||||||
|
│ │ ├── pic2.jpg # Contact 区域背景图
|
||||||
|
│ │ ├── gift.png # 优惠券礼物图标
|
||||||
|
│ │ ├── sep.png # FAQ 分隔符图片
|
||||||
|
│ │ ├── button-available.png # App Store 按钮
|
||||||
|
│ │ ├── button-googleplay.png # Google Play 按钮
|
||||||
|
│ │ ├── particules_medium.png # 雪花粒子 (中)
|
||||||
|
│ │ └── particules_small.png # 雪花粒子 (小)
|
||||||
|
│ └── audio/
|
||||||
|
│ └── audio.mp3 # 背景音乐
|
||||||
|
├── src/
|
||||||
|
│ ├── App.jsx # 主应用 (包含所有页面组件)
|
||||||
|
│ ├── index.css # 全局样式 + 响应式断点
|
||||||
|
│ └── main.jsx # 入口文件
|
||||||
|
├── index.html # HTML 模板
|
||||||
|
├── vite.config.js # Vite 配置
|
||||||
|
└── package.json # 依赖配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 页面结构
|
||||||
|
|
||||||
|
单页应用,包含以下区域 (从上到下):
|
||||||
|
|
||||||
|
| 区域 | 组件名 | Section ID | 说明 |
|
||||||
|
|------|--------|------------|------|
|
||||||
|
| 雪花动画 | `Illustration` | - | 固定背景,雪花下落动画 |
|
||||||
|
| 导航栏 | `Navbar` | - | 固定顶部,滚动时变色 |
|
||||||
|
| 头部 | `Header` | `#page-top` | Hero 区域,主标题 + 下载按钮 + 社交图标 |
|
||||||
|
| 优惠券 | `CouponSection` | `#coupon` | 促销优惠券展示 |
|
||||||
|
| FAQ | `FAQSection` | `#faq` | 常见问题,两列布局 |
|
||||||
|
| 联系 | `ContactSection` | `#contact` | 联系表单 |
|
||||||
|
| 页脚 | `Footer` | - | 版权信息 + 链接 |
|
||||||
|
|
||||||
|
## 组件说明
|
||||||
|
|
||||||
|
### App.jsx 组件结构
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// 主应用入口
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Illustration /> // 雪花粒子动画背景
|
||||||
|
<Navbar /> // 固定导航栏
|
||||||
|
<Header /> // Hero 区域
|
||||||
|
<CouponSection /> // 优惠券区域
|
||||||
|
<FAQSection /> // FAQ 区域
|
||||||
|
<ContactSection /> // 联系表单
|
||||||
|
<Footer /> // 页脚
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navbar 导航栏
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// 导航菜单项 (位于 Navbar 组件内)
|
||||||
|
const menuItems = [
|
||||||
|
{ text: "Special Offer", href: "#coupon" },
|
||||||
|
{ text: "F.A.Q.", href: "#faq" },
|
||||||
|
{ text: "Contact", href: "#contact" },
|
||||||
|
{ text: "Download", href: "#", highlight: true }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能特性:**
|
||||||
|
|
||||||
|
- 滚动超过 50px 时添加 `affix` 类,背景变白
|
||||||
|
- 平滑滚动到对应区域
|
||||||
|
- 移动端显示汉堡菜单
|
||||||
|
|
||||||
|
### CouponSection 优惠券
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// 优惠券数据 (位于 CouponSection 组件内)
|
||||||
|
const couponData = {
|
||||||
|
title: "December Only",
|
||||||
|
subtitle: "Get your discount, available in stores this month only",
|
||||||
|
discount: "50% Off All Products!",
|
||||||
|
code: "HOLIDAY50",
|
||||||
|
description: "Use this coupon code at checkout...",
|
||||||
|
buttonText: "Shop Now"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FAQSection 常见问题
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// FAQ 数据 (位于 FAQSection 组件内)
|
||||||
|
const faqs = [
|
||||||
|
{ question: "How soon do I get my item?", answer: "..." },
|
||||||
|
{ question: "Is there a free shipping?", answer: "..." },
|
||||||
|
{ question: "Can I get several discounts?", answer: "..." },
|
||||||
|
{ question: "Do you deliver it as a gift?", answer: "..." },
|
||||||
|
{ question: "What payment methods?", answer: "..." },
|
||||||
|
{ question: "I have a different question?", answer: "..." }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### ContactSection 联系表单
|
||||||
|
|
||||||
|
**表单字段:**
|
||||||
|
|
||||||
|
- `name` - 姓名 (必填)
|
||||||
|
- `email` - 邮箱 (必填,格式验证)
|
||||||
|
- `comment` - 留言内容 (必填)
|
||||||
|
|
||||||
|
**状态管理:**
|
||||||
|
|
||||||
|
- `formData` - 表单数据
|
||||||
|
- `errors` - 验证错误
|
||||||
|
- `showSuccess` - 提交成功提示
|
||||||
|
|
||||||
|
## 样式说明
|
||||||
|
|
||||||
|
### CSS 类命名约定
|
||||||
|
|
||||||
|
| 类名 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `.navbar-default` | 导航栏基础样式 |
|
||||||
|
| `.navbar-default.affix` | 导航栏滚动后样式 (白色背景) |
|
||||||
|
| `.nav-link` | 导航链接 |
|
||||||
|
| `.nav-link.active` | 当前激活的导航链接 |
|
||||||
|
| `.menuhighlight` | 高亮按钮 (Download) |
|
||||||
|
| `.header-content-inner` | Header 内容容器 |
|
||||||
|
| `.couponsection` | 优惠券区域 |
|
||||||
|
| `.coupon` | 优惠券卡片 (虚线边框) |
|
||||||
|
| `.btn-buynow` | 购买按钮 |
|
||||||
|
| `.social a` | 社交图标 |
|
||||||
|
|
||||||
|
### 响应式断点
|
||||||
|
|
||||||
|
| 断点 | 尺寸 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| xs | < 480px | 手机竖屏 |
|
||||||
|
| sm | 480-575px | 手机横屏 |
|
||||||
|
| md | 576-767px | 平板竖屏 |
|
||||||
|
| lg | 768-991px | 平板横屏 |
|
||||||
|
| xl | 992-1199px | 小桌面 |
|
||||||
|
| xxl | >= 1200px | 大桌面 |
|
||||||
|
|
||||||
|
### 主题色
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 主要颜色 (在 index.css 中定义) */
|
||||||
|
--primary: #bb0000; /* 品牌红色 */
|
||||||
|
--primary-hover: #aa0000; /* 悬停红色 */
|
||||||
|
--accent: #fdcc52; /* 强调黄色 */
|
||||||
|
--coupon-bg: #37488a; /* 优惠券区域蓝色 */
|
||||||
|
--dark: #111; /* 深色背景 */
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见修改任务
|
||||||
|
|
||||||
|
### 修改网站标题和 Logo
|
||||||
|
|
||||||
|
1. 编辑 `index.html` 中的 `<title>` 标签
|
||||||
|
2. 替换 `public/img/iconlogo.png` 图片
|
||||||
|
3. 修改 `src/App.jsx` 中 `Navbar` 组件的品牌名称:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// src/App.jsx - Navbar 组件内
|
||||||
|
<a className="navbar-brand" ...>
|
||||||
|
<img className="iconlogo" src="/img/iconlogo.png" width="50" alt="Logo" />
|
||||||
|
ZU CHRISTMAS {/* 修改这里 */}
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改导航菜单
|
||||||
|
|
||||||
|
编辑 `src/App.jsx` 中 `Navbar` 组件的菜单列表:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// src/App.jsx - Navbar 组件内
|
||||||
|
<ul className="nav-links">
|
||||||
|
<li><a href="#coupon">Special Offer</a></li> {/* 修改文字和链接 */}
|
||||||
|
<li><a href="#faq">F.A.Q.</a></li>
|
||||||
|
<li><a href="#contact">Contact</a></li>
|
||||||
|
<li><a href="#" className="menuhighlight">Download</a></li>
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改 Header 主标题
|
||||||
|
|
||||||
|
编辑 `src/App.jsx` 中 `Header` 组件:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// src/App.jsx - Header 组件内
|
||||||
|
<h1>Merry Holidays</h1> {/* 主标题 */}
|
||||||
|
<h4>Make your visitors smile...</h4> {/* 副标题 */}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改优惠券信息
|
||||||
|
|
||||||
|
编辑 `src/App.jsx` 中 `CouponSection` 组件:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// src/App.jsx - CouponSection 组件内
|
||||||
|
<h2 className="title section-heading">December Only</h2> {/* 标题 */}
|
||||||
|
<h3>50% Off All Products!</h3> {/* 折扣信息 */}
|
||||||
|
<h4>Coupon Code: <span>HOLIDAY50</span></h4> {/* 优惠码 */}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改 FAQ 内容
|
||||||
|
|
||||||
|
编辑 `src/App.jsx` 中 `FAQSection` 组件的 `faqs` 数组:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// src/App.jsx - FAQSection 组件内
|
||||||
|
const faqs = [
|
||||||
|
{ question: "问题1?", answer: "回答1..." },
|
||||||
|
{ question: "问题2?", answer: "回答2..." },
|
||||||
|
// 添加或修改问题
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改页脚版权信息
|
||||||
|
|
||||||
|
编辑 `src/App.jsx` 中 `Footer` 组件:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// src/App.jsx - Footer 组件内
|
||||||
|
<p>© 2025 Your Company Name. All Rights Reserved.</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改背景图片
|
||||||
|
|
||||||
|
替换 `public/img/` 目录下的图片文件:
|
||||||
|
|
||||||
|
- `pic1.jpg` - Header 背景
|
||||||
|
- `pic2.jpg` - Contact 背景
|
||||||
|
- `homeimg.png` - 手机展示图
|
||||||
|
|
||||||
|
### 修改主题颜色
|
||||||
|
|
||||||
|
编辑 `src/index.css` 中的颜色值:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 品牌红色 */
|
||||||
|
.navbar-brand { color: #bb0000; }
|
||||||
|
.btn-buynow { background: #900d0d; }
|
||||||
|
|
||||||
|
/* 优惠券区域蓝色 */
|
||||||
|
section.couponsection { background: #37488a; }
|
||||||
|
|
||||||
|
/* 强调黄色 */
|
||||||
|
.nav-link:hover { color: #fdcc52; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 禁用背景音乐
|
||||||
|
|
||||||
|
删除或注释 `src/App.jsx` 中 `Header` 组件的 audio 元素:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// src/App.jsx - Header 组件内
|
||||||
|
{/* 删除或注释以下代码 */}
|
||||||
|
<div className="audioembed">
|
||||||
|
<audio ref={audioRef} controls autoPlay src="/audio/audio.mp3" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新的页面区域
|
||||||
|
|
||||||
|
在 `src/App.jsx` 中添加新组件:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// 1. 创建新组件
|
||||||
|
function NewSection() {
|
||||||
|
return (
|
||||||
|
<section id="new-section">
|
||||||
|
<div className="container">
|
||||||
|
{/* 内容 */}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 在 App 组件中使用
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
...
|
||||||
|
<NewSection /> {/* 添加到合适位置 */}
|
||||||
|
...
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 在 Navbar 中添加导航链接
|
||||||
|
<li><a href="#new-section">New Section</a></li>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 图片资源
|
||||||
|
|
||||||
|
| 文件 | 尺寸建议 | 用途 |
|
||||||
|
|------|----------|------|
|
||||||
|
| iconlogo.png | 50x50px | 导航栏 Logo |
|
||||||
|
| homeimg.png | 400x800px | Header 手机展示图 |
|
||||||
|
| pic1.jpg | 1920x1080px | Header 背景 |
|
||||||
|
| pic2.jpg | 1920x1080px | Contact 背景 |
|
||||||
|
| gift.png | 100x100px | 优惠券图标 |
|
||||||
|
| button-*.png | 150x50px | 下载按钮 |
|
||||||
|
|
||||||
|
## 构建部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建生产版本
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
# 预览生产版本
|
||||||
|
pnpm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物位于 `dist/` 目录。
|
||||||
|
|
||||||
|
## 服务器配置
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// vite.config.js
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
allowedHosts: true,
|
||||||
|
host: "0.0.0.0"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
29
eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/img/iconlogo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="ZuChristmas - Free Christmas Landing Page" />
|
||||||
|
<meta name="author" content="ZuThemes" />
|
||||||
|
<title>ZuChristmas - Free Christmas Landing Page</title>
|
||||||
|
</head>
|
||||||
|
<body id="page-top">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "turingflow-landing-003-react",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^4.1.18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/react": "^19.2.5",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"vite": "^7.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
2118
pnpm-lock.yaml
generated
Normal file
BIN
public/audio/audio.mp3
Normal file
BIN
public/img/button-available.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/img/button-googleplay.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
public/img/gift.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/img/homeimg.png
Normal file
|
After Width: | Height: | Size: 273 KiB |
BIN
public/img/iconlogo.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/img/particules_medium.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/img/particules_small.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/img/pic1.jpg
Normal file
|
After Width: | Height: | Size: 439 KiB |
BIN
public/img/pic2.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
public/img/pic3.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/img/sep.png
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
427
src/App.jsx
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
// 导航栏组件
|
||||||
|
function Navbar() {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false)
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
|
const [activeSection, setActiveSection] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 50)
|
||||||
|
|
||||||
|
// 检测当前活跃的 section
|
||||||
|
const sections = ['coupon', 'faq', 'contact']
|
||||||
|
for (const sectionId of sections) {
|
||||||
|
const element = document.getElementById(sectionId)
|
||||||
|
if (element) {
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
if (rect.top <= 100 && rect.bottom >= 100) {
|
||||||
|
setActiveSection(sectionId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollToSection = (e, sectionId) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (sectionId === 'page-top') {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
} else {
|
||||||
|
const element = document.getElementById(sectionId)
|
||||||
|
if (element) {
|
||||||
|
const offset = element.offsetTop - 50
|
||||||
|
window.scrollTo({ top: offset, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsMenuOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={`navbar-default ${isScrolled ? 'affix' : ''}`}>
|
||||||
|
<div className="container">
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<a
|
||||||
|
className="navbar-brand"
|
||||||
|
href="#page-top"
|
||||||
|
onClick={(e) => scrollToSection(e, 'page-top')}
|
||||||
|
>
|
||||||
|
<img className="iconlogo" src="/img/iconlogo.png" width="50" alt="Logo" />
|
||||||
|
ZU CHRISTMAS
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="navbar-toggle"
|
||||||
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
|
>
|
||||||
|
Menu <i className="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul className={`nav-links ${isMenuOpen ? 'open' : ''}`} style={{
|
||||||
|
display: 'flex',
|
||||||
|
listStyle: 'none',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
gap: '20px',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className={`nav-link ${activeSection === 'coupon' ? 'active' : ''}`}
|
||||||
|
href="#coupon"
|
||||||
|
onClick={(e) => scrollToSection(e, 'coupon')}
|
||||||
|
>
|
||||||
|
Special Offer
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className={`nav-link ${activeSection === 'faq' ? 'active' : ''}`}
|
||||||
|
href="#faq"
|
||||||
|
onClick={(e) => scrollToSection(e, 'faq')}
|
||||||
|
>
|
||||||
|
F.A.Q.
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className={`nav-link ${activeSection === 'contact' ? 'active' : ''}`}
|
||||||
|
href="#contact"
|
||||||
|
onClick={(e) => scrollToSection(e, 'contact')}
|
||||||
|
>
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="nav-link menuhighlight"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
<i className="fa fa-cloud-download"></i> Download
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 粒子动画背景
|
||||||
|
function Illustration() {
|
||||||
|
return (
|
||||||
|
<div className="illustration">
|
||||||
|
<div className="i-large"></div>
|
||||||
|
<div className="i-medium"></div>
|
||||||
|
<div className="i-small"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header 区域
|
||||||
|
function Header() {
|
||||||
|
const audioRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.volume = 0.4
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header id="page-top">
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<div className="device-container">
|
||||||
|
<img
|
||||||
|
src="/img/homeimg.png"
|
||||||
|
alt="Home"
|
||||||
|
style={{ maxWidth: '100%', height: 'auto' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<div className="header-content">
|
||||||
|
<div className="header-content-inner">
|
||||||
|
<h1>Merry Holidays</h1>
|
||||||
|
<h4>Make your visitors smile with this special landing page!</h4>
|
||||||
|
<br />
|
||||||
|
<a href="#" className="btnimg">
|
||||||
|
<img src="/img/button-available.png" height="50" alt="App Store" />
|
||||||
|
</a>
|
||||||
|
{' '}
|
||||||
|
<a href="#" className="btnimg">
|
||||||
|
<img src="/img/button-googleplay.png" height="50" alt="Google Play" />
|
||||||
|
</a>
|
||||||
|
<br /><br />
|
||||||
|
<div className="social">
|
||||||
|
<a href="#"><i className="fa fa-facebook"></i></a>
|
||||||
|
<a href="#"><i className="fa fa-twitter"></i></a>
|
||||||
|
<a href="#"><i className="fa fa-google-plus"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="audioembed">
|
||||||
|
<audio
|
||||||
|
ref={audioRef}
|
||||||
|
controls
|
||||||
|
autoPlay
|
||||||
|
src="/audio/audio.mp3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优惠券区域
|
||||||
|
function CouponSection() {
|
||||||
|
return (
|
||||||
|
<section id="coupon" className="couponsection bg-primary text-center">
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-10 col-md-offset-1">
|
||||||
|
<h2 className="title section-heading">December Only</h2>
|
||||||
|
<p>Get your discount, available in stores this month only</p>
|
||||||
|
<div className="coupon">
|
||||||
|
<img src="/img/gift.png" alt="Gift" />
|
||||||
|
<h3>50% Off All Products!</h3>
|
||||||
|
<h4>Coupon Code: <span style={{ borderBottom: '1px dashed' }}>HOLIDAY50</span></h4>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
<i>
|
||||||
|
Use this coupon code at checkout to receive<br />
|
||||||
|
<u>half off</u> your order. Hurry up, limited time offer!
|
||||||
|
</i>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="#" className="btn-buynow">
|
||||||
|
<i className="fa fa-shopping-cart"></i> Shop Now
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAQ 区域
|
||||||
|
function FAQSection() {
|
||||||
|
const faqs = [
|
||||||
|
{
|
||||||
|
question: "How soon do I get my item?",
|
||||||
|
answer: "If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Is there a free shipping?",
|
||||||
|
answer: "If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I get several discounts?",
|
||||||
|
answer: "If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Do you deliver it as a gift?",
|
||||||
|
answer: "If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What payment methods?",
|
||||||
|
answer: "If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "I have a different question?",
|
||||||
|
answer: "If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="faq">
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-12 text-center">
|
||||||
|
<div className="section-heading">
|
||||||
|
<h2 className="title">F.A.Q.</h2>
|
||||||
|
<p className="text-muted">Contact us if you have a different question</p>
|
||||||
|
<img src="/img/sep.png" width="120" alt="Separator" className="sep-img" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-10 col-md-offset-1">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
{faqs.slice(0, 3).map((faq, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<h3>{faq.question}</h3>
|
||||||
|
<p>{faq.answer}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
{faqs.slice(3, 6).map((faq, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<h3>{faq.question}</h3>
|
||||||
|
<p>{faq.answer}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 联系区域
|
||||||
|
function ContactSection() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
comment: ''
|
||||||
|
})
|
||||||
|
const [errors, setErrors] = useState({})
|
||||||
|
const [showSuccess, setShowSuccess] = useState(false)
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}))
|
||||||
|
// 清除错误
|
||||||
|
if (errors[name]) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// 验证
|
||||||
|
const newErrors = {}
|
||||||
|
if (!formData.name.trim()) newErrors.name = true
|
||||||
|
if (!formData.email.trim() || !/\S+@\S+\.\S+/.test(formData.email)) newErrors.email = true
|
||||||
|
if (!formData.comment.trim()) newErrors.comment = true
|
||||||
|
|
||||||
|
if (Object.keys(newErrors).length > 0) {
|
||||||
|
setErrors(newErrors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟提交成功
|
||||||
|
setShowSuccess(true)
|
||||||
|
setFormData({ name: '', email: '', comment: '' })
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowSuccess(false)
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeAlert = () => {
|
||||||
|
setShowSuccess(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="contact">
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-12 text-center">
|
||||||
|
<div className="section-heading">
|
||||||
|
<h2 className="title" style={{ marginBottom: '60px', color: '#fff' }}>Contact Us</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-8 col-lg-offset-2">
|
||||||
|
<div className={`done ${showSuccess ? 'show' : ''}`}>
|
||||||
|
<div className="alert-success">
|
||||||
|
<button type="button" className="close" onClick={closeAlert}>×</button>
|
||||||
|
Your message has been sent. Thank you!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="contactform" onSubmit={handleSubmit}>
|
||||||
|
<div className="form">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder="Name *"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={errors.name ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
placeholder="E-mail *"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={errors.email ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
name="comment"
|
||||||
|
rows="7"
|
||||||
|
placeholder="Message *"
|
||||||
|
value={formData.comment}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={errors.comment ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
<input type="submit" className="clearfix btn" value="Send" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overlay"></div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页脚
|
||||||
|
function Footer() {
|
||||||
|
return (
|
||||||
|
<footer>
|
||||||
|
<div className="container">
|
||||||
|
<p>© 2025 Your Company Name. All Rights Reserved.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#">Privacy</a></li>
|
||||||
|
<li><a href="#">Terms</a></li>
|
||||||
|
<li><a href="#">FAQ</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主应用
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Illustration />
|
||||||
|
<Navbar />
|
||||||
|
<Header />
|
||||||
|
<CouponSection />
|
||||||
|
<FAQSection />
|
||||||
|
<ContactSection />
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
1063
src/index.css
Normal file
10
src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
12
vite.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
allowedHosts: true,
|
||||||
|
host: "0.0.0.0"
|
||||||
|
},
|
||||||
|
})
|
||||||