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"
|
||||||
|
}'
|
||||||
158
.gitignore
vendored
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 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
|
||||||
|
pnpm-lock.yaml
|
||||||
|
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
|
||||||
297
README.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# turingflow-brand-001
|
||||||
|
|
||||||
|
企业品牌官网模板,使用 React 19 + Vite 7 + Tailwind CSS 4 + TanStack Router 。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 技术 | 版本 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| React | 19.2.3 (锁定) | UI 框架 |
|
||||||
|
| Vite | ^7.0.0 | 构建工具 |
|
||||||
|
| Tailwind CSS | ^4.0.0 | 样式框架 |
|
||||||
|
| TanStack Router | ^1.114.0 | 文件路由 |
|
||||||
|
| Swiper | ^11.2.0 | 轮播组件 |
|
||||||
|
| TypeScript | ^5.7.0 | 类型系统 |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 <http://localhost:3000> 即可预览,支持 HMR 热更新。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
turingflow-brand-001-react/
|
||||||
|
├── index.html # HTML 入口
|
||||||
|
├── package.json # 依赖配置
|
||||||
|
├── vite.config.ts # Vite 配置
|
||||||
|
├── tsconfig.json # TypeScript 配置
|
||||||
|
├── src/
|
||||||
|
│ ├── main.tsx # 应用入口
|
||||||
|
│ ├── index.css # 全局样式 + Tailwind 主题
|
||||||
|
│ ├── routeTree.gen.ts # 自动生成的路由树
|
||||||
|
│ ├── routes/ # 页面路由
|
||||||
|
│ │ ├── __root.tsx # 根布局 (Header + Footer)
|
||||||
|
│ │ ├── index.tsx # 首页 /
|
||||||
|
│ │ ├── about.tsx # 关于 /about
|
||||||
|
│ │ ├── services.tsx # 服务 /services
|
||||||
|
│ │ └── contact.tsx # 联系 /contact
|
||||||
|
│ ├── components/ # 组件目录
|
||||||
|
│ │ ├── layout/ # 布局组件
|
||||||
|
│ │ │ ├── Header.tsx # 导航栏
|
||||||
|
│ │ │ ├── Footer.tsx # 页脚
|
||||||
|
│ │ │ └── ScrollToTop.tsx # 回到顶部
|
||||||
|
│ │ ├── home/ # 首页组件
|
||||||
|
│ │ │ ├── HeroSlider.tsx # 轮播 (Swiper)
|
||||||
|
│ │ │ ├── Features.tsx # 特性展示
|
||||||
|
│ │ │ ├── Services.tsx # 服务卡片
|
||||||
|
│ │ │ ├── CTA.tsx # Call-to-Action
|
||||||
|
│ │ │ ├── Testimonials.tsx # 客户评价
|
||||||
|
│ │ │ ├── Stats.tsx # 统计数据
|
||||||
|
│ │ │ └── LatestNews.tsx # 最新文章
|
||||||
|
│ │ ├── about/ # 关于页组件
|
||||||
|
│ │ │ ├── Mission.tsx # 使命愿景
|
||||||
|
│ │ │ ├── WhyChooseUs.tsx # 为什么选择我们
|
||||||
|
│ │ │ ├── Statistics.tsx # 统计指标
|
||||||
|
│ │ │ └── Team.tsx # 团队成员
|
||||||
|
│ │ ├── services/ # 服务页组件
|
||||||
|
│ │ │ ├── ServiceCards.tsx # 服务卡片
|
||||||
|
│ │ │ ├── ProcessSteps.tsx # 流程步骤
|
||||||
|
│ │ │ └── AdvanceFeatures.tsx # 高级特性
|
||||||
|
│ │ ├── contact/ # 联系页组件
|
||||||
|
│ │ │ ├── ContactForm.tsx # 联系表单
|
||||||
|
│ │ │ └── Map.tsx # 地图
|
||||||
|
│ │ └── shared/ # 共享组件
|
||||||
|
│ │ └── Breadcrumb.tsx # 面包屑
|
||||||
|
│ ├── data/
|
||||||
|
│ │ └── siteData.ts # 静态数据
|
||||||
|
│ └── assets/
|
||||||
|
│ └── images/ # 图片资源
|
||||||
|
└── public/ # 公共资源
|
||||||
|
```
|
||||||
|
|
||||||
|
## 页面路由
|
||||||
|
|
||||||
|
| 路径 | 文件 | 组件 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/` | routes/index.tsx | HeroSlider, Features, Services, CTA, Testimonials, Stats, LatestNews |
|
||||||
|
| `/about` | routes/about.tsx | Breadcrumb, Mission, WhyChooseUs, Statistics, Team |
|
||||||
|
| `/services` | routes/services.tsx | Breadcrumb, ServiceCards, ProcessSteps, AdvanceFeatures |
|
||||||
|
| `/contact` | routes/contact.tsx | Breadcrumb, ContactForm, Map |
|
||||||
|
|
||||||
|
## 主题配置
|
||||||
|
|
||||||
|
主题色和变量在 `src/index.css` 中定义:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@theme {
|
||||||
|
--color-primary: #2e5deb; /* 主题蓝 */
|
||||||
|
--color-secondary: #ff5b83; /* 次要红粉 */
|
||||||
|
--color-text: #585858; /* 正文灰 */
|
||||||
|
--color-title: #1A1D2D; /* 标题深灰 */
|
||||||
|
--color-light-bg: #f6f6f6; /* 浅色背景 */
|
||||||
|
--font-family-sans: 'Poppins', sans-serif;
|
||||||
|
--font-family-body: 'Hind', sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据文件
|
||||||
|
|
||||||
|
所有静态数据集中在 `src/data/siteData.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 导航菜单
|
||||||
|
export const menuItems = [...]
|
||||||
|
|
||||||
|
// 轮播数据
|
||||||
|
export const sliderData = [...]
|
||||||
|
|
||||||
|
// 服务数据
|
||||||
|
export const servicesData = [...]
|
||||||
|
|
||||||
|
// 客户评价
|
||||||
|
export const testimonialsData = [...]
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
export const statsData = [...]
|
||||||
|
|
||||||
|
// 团队成员
|
||||||
|
export const teamData = [...]
|
||||||
|
|
||||||
|
// 联系信息
|
||||||
|
export const contactInfo = {...}
|
||||||
|
|
||||||
|
// 页脚链接
|
||||||
|
export const footerLinks = {...}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件说明
|
||||||
|
|
||||||
|
### 布局组件
|
||||||
|
|
||||||
|
#### Header (src/components/layout/Header.tsx)
|
||||||
|
- 固定导航栏,滚动时背景变色
|
||||||
|
- 响应式汉堡菜单
|
||||||
|
- 搜索弹窗功能
|
||||||
|
|
||||||
|
#### Footer (src/components/layout/Footer.tsx)
|
||||||
|
- 4 列网格布局
|
||||||
|
- 联系信息 + 社交链接
|
||||||
|
- 新闻订阅表单
|
||||||
|
|
||||||
|
#### ScrollToTop (src/components/layout/ScrollToTop.tsx)
|
||||||
|
- 滚动超过 200px 显示
|
||||||
|
- 点击平滑滚动到顶部
|
||||||
|
|
||||||
|
### 首页组件
|
||||||
|
|
||||||
|
#### HeroSlider (src/components/home/HeroSlider.tsx)
|
||||||
|
- 使用 Swiper 实现
|
||||||
|
- 支持自动播放、导航、分页
|
||||||
|
- 淡入淡出效果
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation, Pagination, Autoplay, EffectFade]}
|
||||||
|
navigation
|
||||||
|
pagination={{ clickable: true }}
|
||||||
|
autoplay={{ delay: 5000 }}
|
||||||
|
effect="fade"
|
||||||
|
loop
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stats (src/components/home/Stats.tsx)
|
||||||
|
- 数字计数动画
|
||||||
|
- 使用 IntersectionObserver 触发
|
||||||
|
- 进入视口后开始计数
|
||||||
|
|
||||||
|
### 内页组件
|
||||||
|
|
||||||
|
#### Breadcrumb (src/components/shared/Breadcrumb.tsx)
|
||||||
|
```tsx
|
||||||
|
<Breadcrumb title="About Us" bgImage="/src/assets/images/6.jpg" />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Team (src/components/about/Team.tsx)
|
||||||
|
- 团队成员卡片
|
||||||
|
- Hover 显示社交链接
|
||||||
|
|
||||||
|
## 常见修改任务
|
||||||
|
|
||||||
|
### 修改网站名称/Logo
|
||||||
|
```tsx
|
||||||
|
// src/components/layout/Header.tsx
|
||||||
|
<span className="text-2xl font-bold">
|
||||||
|
Finance <span className="text-secondary">Ideas</span>
|
||||||
|
</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改导航菜单
|
||||||
|
```typescript
|
||||||
|
// src/data/siteData.ts
|
||||||
|
export const menuItems = [
|
||||||
|
{ name: 'Home', href: '/' },
|
||||||
|
{ name: 'About', href: '/about' },
|
||||||
|
// 添加新菜单项...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改主题色
|
||||||
|
```css
|
||||||
|
/* src/index.css */
|
||||||
|
@theme {
|
||||||
|
--color-primary: #新颜色;
|
||||||
|
--color-secondary: #新颜色;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改轮播内容
|
||||||
|
```typescript
|
||||||
|
// src/data/siteData.ts
|
||||||
|
export const sliderData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '新标题',
|
||||||
|
buttonText: '按钮文字',
|
||||||
|
buttonLink: '/services',
|
||||||
|
bgClass: 'bg-slider-1',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改团队成员
|
||||||
|
```typescript
|
||||||
|
// src/data/siteData.ts
|
||||||
|
export const teamData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '姓名',
|
||||||
|
role: '职位',
|
||||||
|
image: '/src/assets/images/team1.jpg',
|
||||||
|
social: { facebook: '#', twitter: '#', linkedin: '#' },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新页面
|
||||||
|
1. 在 `src/routes/` 创建新文件 `newpage.tsx`
|
||||||
|
2. 使用 `createFileRoute` 定义路由
|
||||||
|
3. 在 `menuItems` 添加导航项
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/routes/newpage.tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/newpage')({
|
||||||
|
component: NewPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
function NewPage() {
|
||||||
|
return <div>新页面内容</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vite 配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
TanStackRouterVite({ target: 'react', autoCodeSplitting: true }),
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 图片资源
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| 1-6.jpg | 轮播/横幅背景 |
|
||||||
|
| g1-12.jpg | 内容图片 |
|
||||||
|
| c1-3.jpg | 客户头像 |
|
||||||
|
| team1-4.jpg | 团队成员 |
|
||||||
|
|
||||||
|
## 安全说明
|
||||||
|
|
||||||
|
React 版本已锁定为 19.2.3,修复 CVE-2025-55182 漏洞。请勿使用 `^` 前缀升级。
|
||||||
|
|
||||||
|
## 构建部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build # 构建生产版本
|
||||||
|
pnpm preview # 预览生产版本
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物在 `dist/` 目录。
|
||||||
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Finance Ideas - 企业金融服务</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&family=Hind:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "turingflow-brand-001-react",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "企业品牌官网模板 - React + Vite + Tailwind CSS 4",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3",
|
||||||
|
"@tanstack/react-router": "^1.114.0",
|
||||||
|
"swiper": "^11.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tanstack/router-plugin": "^1.114.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/assets/images/1.jpg
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
src/assets/images/2.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
src/assets/images/3.jpg
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
src/assets/images/4.jpg
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
src/assets/images/5.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
src/assets/images/6.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
src/assets/images/c1.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/c2.jpg
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/images/c3.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/images/g1.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src/assets/images/g10.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/assets/images/g11.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/images/g12.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/assets/images/g2.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src/assets/images/g3.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/assets/images/g4.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/assets/images/g5.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/assets/images/g6.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/assets/images/g7.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/assets/images/g8.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/images/g9.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/assets/images/team1.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/images/team2.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/images/team3.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/team4.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
42
src/components/about/Mission.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { missionVisionData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function Mission() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white" id="features">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex flex-wrap lg:flex-nowrap gap-12">
|
||||||
|
{/* Left - Feature Cards */}
|
||||||
|
<div className="w-full lg:w-2/3 grid sm:grid-cols-2 gap-6 lg:pr-8">
|
||||||
|
{missionVisionData.map((item) => (
|
||||||
|
<div key={item.id} className="feature-gd">
|
||||||
|
<img
|
||||||
|
src={item.image}
|
||||||
|
alt={item.title}
|
||||||
|
className="w-full h-auto object-cover rounded"
|
||||||
|
/>
|
||||||
|
<div className="icon-info mt-3">
|
||||||
|
<h5 className="text-lg font-semibold mt-3 text-title">
|
||||||
|
{item.title}
|
||||||
|
</h5>
|
||||||
|
<p className="text-text mt-2">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Content */}
|
||||||
|
<div className="w-full lg:w-1/3">
|
||||||
|
<h6 className="text-secondary font-semibold mb-3">
|
||||||
|
{commonDescriptions.aboutQuote}
|
||||||
|
</h6>
|
||||||
|
<p className="text-text mt-3 leading-relaxed">
|
||||||
|
{commonDescriptions.aboutDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
src/components/about/Statistics.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import { aboutStatsData } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function Statistics() {
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
const sectionRef = useRef<HTMLElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.3 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (sectionRef.current) {
|
||||||
|
observer.observe(sectionRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
ref={sectionRef}
|
||||||
|
className="py-16"
|
||||||
|
id="stats"
|
||||||
|
style={{ background: 'linear-gradient(100deg, #2e5deb 10%, #5360fd 50%, #ff5b83 100%)' }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4 py-3">
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{aboutStatsData.map((stat) => (
|
||||||
|
<div key={stat.id} className="text-center">
|
||||||
|
<span className={`fa fa-${stat.icon} text-secondary mb-3 block`} style={{ fontSize: '40px' }} />
|
||||||
|
<h3 className="text-[50px] font-bold font-sans mb-1" style={{ color: '#ffffff' }}>
|
||||||
|
<CountUp end={stat.value} isVisible={isVisible} />
|
||||||
|
</h3>
|
||||||
|
<p className="text-white">{stat.label}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CountUp({ end, isVisible }: { end: number; isVisible: boolean }) {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVisible) return
|
||||||
|
|
||||||
|
let startTime: number
|
||||||
|
const duration = 2000
|
||||||
|
|
||||||
|
const step = (timestamp: number) => {
|
||||||
|
if (!startTime) startTime = timestamp
|
||||||
|
const progress = Math.min((timestamp - startTime) / duration, 1)
|
||||||
|
setCount(Math.floor(progress * end))
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(step)
|
||||||
|
}, [end, isVisible])
|
||||||
|
|
||||||
|
return <>{count.toLocaleString()}</>
|
||||||
|
}
|
||||||
63
src/components/about/Team.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { teamData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function Team() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white" id="team">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-12">
|
||||||
|
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
|
||||||
|
{commonDescriptions.teamTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="text-text my-3">
|
||||||
|
{commonDescriptions.sectionDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Team Grid */}
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 pt-5 mt-5">
|
||||||
|
{teamData.map((member) => (
|
||||||
|
<div
|
||||||
|
key={member.id}
|
||||||
|
className="team-info text-center"
|
||||||
|
>
|
||||||
|
<div className="column relative">
|
||||||
|
<a href="#url">
|
||||||
|
<img
|
||||||
|
src={member.image}
|
||||||
|
alt={member.name}
|
||||||
|
className="w-full h-auto object-cover rounded"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="column mt-4">
|
||||||
|
<h3 className="text-lg font-semibold font-sans">
|
||||||
|
<a href="#url" className="text-title hover:text-secondary transition-colors">
|
||||||
|
{member.name}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p className="text-text">{member.role}</p>
|
||||||
|
<div className="social mt-3">
|
||||||
|
<div className="social-left flex justify-center gap-2">
|
||||||
|
<a href={member.social.facebook} className="w-10 h-10 bg-secondary hover:bg-primary rounded flex items-center justify-center text-white transition-colors">
|
||||||
|
<span className="fa fa-facebook" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
<a href={member.social.twitter} className="w-10 h-10 bg-secondary hover:bg-primary rounded flex items-center justify-center text-white transition-colors">
|
||||||
|
<span className="fa fa-twitter" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
<a href={member.social.linkedin} className="w-10 h-10 bg-secondary hover:bg-primary rounded flex items-center justify-center text-white transition-colors">
|
||||||
|
<span className="fa fa-linkedin" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
<a href={member.social.google} className="w-10 h-10 bg-secondary hover:bg-primary rounded flex items-center justify-center text-white transition-colors">
|
||||||
|
<span className="fa fa-google-plus" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
39
src/components/about/WhyChooseUs.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { whyChooseUsData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function WhyChooseUs() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white" id="about">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex flex-wrap lg:flex-nowrap gap-12 items-center">
|
||||||
|
{/* Left - Image */}
|
||||||
|
<div className="w-full lg:w-1/2">
|
||||||
|
<img
|
||||||
|
src="/src/assets/images/g5.jpg"
|
||||||
|
alt="Why Choose Us"
|
||||||
|
className="w-full rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Content */}
|
||||||
|
<div className="w-full lg:w-1/2 lg:pl-8">
|
||||||
|
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
|
||||||
|
{commonDescriptions.whyChooseUsTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="text-text mb-8 leading-relaxed">
|
||||||
|
{commonDescriptions.whyChooseUsDesc}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="cont-4 space-y-3">
|
||||||
|
{whyChooseUsData.map((item, index) => (
|
||||||
|
<li key={index} className="flex items-center gap-3">
|
||||||
|
<span className="fa fa-check text-secondary" />
|
||||||
|
<span className="text-text">{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
120
src/components/contact/ContactForm.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { contactFormInfo } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function ContactForm() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
console.log('Form submitted:', formData)
|
||||||
|
alert('Message sent successfully!')
|
||||||
|
setFormData({ name: '', email: '', subject: '', message: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white" id="contact">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="grid lg:grid-cols-2 gap-12">
|
||||||
|
{/* Contact Info */}
|
||||||
|
<div className="contact-left">
|
||||||
|
<h4 className="text-2xl font-bold text-title mb-4 font-sans">
|
||||||
|
{contactFormInfo.title}
|
||||||
|
</h4>
|
||||||
|
<h6 className="text-text mb-6">
|
||||||
|
{contactFormInfo.subtitle}
|
||||||
|
</h6>
|
||||||
|
<div className="hours space-y-4">
|
||||||
|
<div>
|
||||||
|
<h6 className="font-semibold text-title mt-3">Email:</h6>
|
||||||
|
<p>
|
||||||
|
<a href={`mailto:${contactFormInfo.email}`} className="text-text hover:text-primary transition-colors">
|
||||||
|
{contactFormInfo.email}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 className="font-semibold text-title mt-3">Address:</h6>
|
||||||
|
<p className="text-text">{contactFormInfo.address}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 className="font-semibold text-title mt-3">Contact:</h6>
|
||||||
|
<p>
|
||||||
|
<a href={`tel:${contactFormInfo.phone}`} className="text-text hover:text-primary transition-colors">
|
||||||
|
{contactFormInfo.phone}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Form */}
|
||||||
|
<div className="contact-right">
|
||||||
|
<form onSubmit={handleSubmit} className="signin-form">
|
||||||
|
<div className="input-grids space-y-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="w3lName"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Your Name*"
|
||||||
|
required
|
||||||
|
className="contact-input"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
id="w3lSender"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Your Email*"
|
||||||
|
required
|
||||||
|
className="contact-input"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="subject"
|
||||||
|
id="w3lSubect"
|
||||||
|
value={formData.subject}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Subject*"
|
||||||
|
className="contact-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-input mt-4">
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
id="w3lMessage"
|
||||||
|
value={formData.message}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Type your message here*"
|
||||||
|
rows={5}
|
||||||
|
required
|
||||||
|
className="contact-input resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="mt-6 px-8 py-3 bg-secondary hover:bg-secondary/80 text-white font-semibold rounded transition-colors"
|
||||||
|
>
|
||||||
|
Send Message
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/components/contact/Map.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
export default function Map() {
|
||||||
|
return (
|
||||||
|
<section className="h-96 bg-gray-200 relative overflow-hidden">
|
||||||
|
{/* 静态地图占位图 */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-300">
|
||||||
|
{/* 地图图标 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<svg
|
||||||
|
className="w-24 h-24 mx-auto text-gray-400 mb-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1}
|
||||||
|
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1}
|
||||||
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p className="text-gray-500 text-lg font-medium">Location Map</p>
|
||||||
|
<p className="text-gray-400 text-sm mt-2">
|
||||||
|
Lorem ipsum, #32841 block, #221DRS Estate business building, UK
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 装饰性网格线 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-10"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(0,0,0,0.1) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0,0,0,0.1) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '50px 50px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
src/components/home/CTA.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function CTA() {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="py-16"
|
||||||
|
style={{ background: 'linear-gradient(100deg, #2e5deb 10%, #5360fd 50%, #ff5b83 100%)' }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4 py-3 text-center">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<h3 className="text-2xl md:text-3xl font-bold mb-4 font-sans" style={{ color: '#ffffff' }}>
|
||||||
|
{commonDescriptions.ctaTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="text-white my-3">
|
||||||
|
{commonDescriptions.sectionDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap justify-center gap-4 mt-8">
|
||||||
|
<Link
|
||||||
|
to="/contact"
|
||||||
|
className="px-8 py-3 border-2 border-white text-white hover:bg-secondary hover:border-secondary font-semibold rounded transition-colors"
|
||||||
|
>
|
||||||
|
Contact Us
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/contact"
|
||||||
|
className="px-8 py-3 bg-primary hover:bg-secondary text-white font-semibold rounded transition-colors"
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
51
src/components/home/Features.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { featuresData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function Features() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white" id="about">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex flex-wrap lg:flex-nowrap gap-12">
|
||||||
|
{/* Left Content */}
|
||||||
|
<div className="w-full lg:w-1/3">
|
||||||
|
<h4 className="text-lg font-semibold text-secondary mb-3">
|
||||||
|
{commonDescriptions.featuresSubtitle}
|
||||||
|
</h4>
|
||||||
|
<p className="text-text mt-3 leading-relaxed">
|
||||||
|
{commonDescriptions.featuresDesc}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
to="/services"
|
||||||
|
className="inline-block mt-4 px-6 py-3 bg-secondary hover:bg-secondary/80 text-white font-semibold rounded transition-colors"
|
||||||
|
>
|
||||||
|
See More Services →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Content - Feature Cards */}
|
||||||
|
<div className="w-full lg:w-2/3 grid sm:grid-cols-2 gap-6 lg:pl-8">
|
||||||
|
{featuresData.map((feature) => (
|
||||||
|
<div key={feature.id} className="feature-gd">
|
||||||
|
<img
|
||||||
|
src={feature.image}
|
||||||
|
alt={feature.title}
|
||||||
|
className="w-full h-auto object-cover rounded"
|
||||||
|
/>
|
||||||
|
<div className="icon-info mt-3">
|
||||||
|
<h5 className="text-lg font-semibold mt-3">
|
||||||
|
<a href="#" className="text-title hover:text-secondary transition-colors">
|
||||||
|
{feature.title}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<p className="text-text mt-2">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
src/components/home/HeroSlider.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||||
|
import { Navigation, Pagination, Autoplay, EffectFade } from 'swiper/modules'
|
||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { sliderData } from '../../data/siteData'
|
||||||
|
|
||||||
|
import 'swiper/css'
|
||||||
|
import 'swiper/css/navigation'
|
||||||
|
import 'swiper/css/pagination'
|
||||||
|
import 'swiper/css/effect-fade'
|
||||||
|
|
||||||
|
// 背景图片映射
|
||||||
|
const bgImages: Record<string, string> = {
|
||||||
|
'bg-slider-1': '/src/assets/images/1.jpg',
|
||||||
|
'bg-slider-2': '/src/assets/images/2.jpg',
|
||||||
|
'bg-slider-3': '/src/assets/images/4.jpg',
|
||||||
|
'bg-slider-4': '/src/assets/images/5.jpg',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HeroSlider() {
|
||||||
|
return (
|
||||||
|
<section className="relative" id="home">
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation, Pagination, Autoplay, EffectFade]}
|
||||||
|
navigation
|
||||||
|
pagination={{ clickable: true }}
|
||||||
|
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
||||||
|
effect="fade"
|
||||||
|
loop
|
||||||
|
className="hero-swiper"
|
||||||
|
>
|
||||||
|
{sliderData.map((slide) => (
|
||||||
|
<SwiperSlide key={slide.id}>
|
||||||
|
<div
|
||||||
|
className="relative h-screen min-h-[600px] flex items-center justify-center bg-cover bg-center"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url(${bgImages[slide.bgClass] || '/src/assets/images/1.jpg'})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<h1
|
||||||
|
className="text-3xl md:text-4xl lg:text-5xl font-bold mb-8 font-sans leading-tight"
|
||||||
|
style={{ color: '#ffffff' }}
|
||||||
|
>
|
||||||
|
{slide.title}
|
||||||
|
</h1>
|
||||||
|
<Link
|
||||||
|
to={slide.buttonLink}
|
||||||
|
className="inline-block px-8 py-4 bg-secondary hover:bg-secondary/80 text-white font-semibold rounded transition-colors"
|
||||||
|
>
|
||||||
|
{slide.buttonText}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Scroll Indicator - 原始 HTML 滚动鼠标动画 */}
|
||||||
|
<div className="mt-16">
|
||||||
|
<a href="#about" className="inline-block">
|
||||||
|
<div className="icon-scroll">
|
||||||
|
<div className="wheel" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
65
src/components/home/LatestNews.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { blogData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function LatestNews() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white" id="news">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-8">
|
||||||
|
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
|
||||||
|
{commonDescriptions.newsTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="text-text my-3">
|
||||||
|
{commonDescriptions.sectionDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Blog Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{blogData.map((post, index) => (
|
||||||
|
<div
|
||||||
|
key={post.id}
|
||||||
|
className={`mt-4 ${
|
||||||
|
index === 2 ? 'md:col-start-2 lg:col-start-auto' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="grids5-info bg-white rounded-lg overflow-hidden shadow-card hover:shadow-card-hover transition-shadow">
|
||||||
|
<a href={post.link} className="d-block zoom block overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={post.image}
|
||||||
|
alt={post.title}
|
||||||
|
className="w-full h-48 object-cover transition-transform duration-500 hover:scale-110"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<div className="blog-info p-6">
|
||||||
|
<ul className="flex gap-2 mb-2">
|
||||||
|
{post.categories.map((cat, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<a href="#" className="text-secondary hover:text-primary text-sm">
|
||||||
|
{cat}{i < post.categories.length - 1 ? ',' : ''}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="text-text/60 text-sm mb-2">{post.date}</p>
|
||||||
|
<h4 className="text-lg font-bold mb-3">
|
||||||
|
<a href={post.link} className="text-title hover:text-secondary transition-colors">
|
||||||
|
{post.title}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<p className="text-text mb-4">{post.description}</p>
|
||||||
|
<a
|
||||||
|
href={post.link}
|
||||||
|
className="text-secondary hover:text-primary font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Read More <span className="fa fa-angle-right pl-1" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
51
src/components/home/Services.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { servicesData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function Services() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-services-bg" id="services">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-12">
|
||||||
|
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
|
||||||
|
{commonDescriptions.servicesTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="text-text my-3">
|
||||||
|
{commonDescriptions.sectionDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service Cards */}
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 pt-8 mt-3">
|
||||||
|
{servicesData.map((service) => (
|
||||||
|
<div
|
||||||
|
key={service.id}
|
||||||
|
className="group bg-white text-center px-5 py-10 rounded border-b-[3px] border-secondary hover:bg-secondary transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="icon-holder mb-4">
|
||||||
|
<span
|
||||||
|
className={`fa fa-${service.icon} text-secondary group-hover:text-white transition-colors duration-300`}
|
||||||
|
style={{ fontSize: '36px' }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h4 className="text-xl font-semibold text-title group-hover:text-white mb-3 font-sans transition-colors duration-300">
|
||||||
|
{service.title}
|
||||||
|
</h4>
|
||||||
|
<div className="open-description">
|
||||||
|
<p className="text-text group-hover:text-white/90 mb-5 transition-colors duration-300">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="#read"
|
||||||
|
className="text-secondary group-hover:text-white font-bold transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Read More
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
145
src/components/home/Stats.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { statsData, serviceListData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
function useCountUp(end: number, duration: number = 2000, startCounting: boolean = false) {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!startCounting) return
|
||||||
|
|
||||||
|
let startTime: number
|
||||||
|
let animationFrame: number
|
||||||
|
|
||||||
|
const animate = (currentTime: number) => {
|
||||||
|
if (!startTime) startTime = currentTime
|
||||||
|
const progress = Math.min((currentTime - startTime) / duration, 1)
|
||||||
|
setCount(Math.floor(progress * end))
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
animationFrame = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrame = requestAnimationFrame(animate)
|
||||||
|
return () => cancelAnimationFrame(animationFrame)
|
||||||
|
}, [end, duration, startCounting])
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatItem({ value, label, startCounting }: { value: number; label: string; startCounting: boolean }) {
|
||||||
|
const count = useCountUp(value, 2000, startCounting)
|
||||||
|
return (
|
||||||
|
<div className="stats-1-left">
|
||||||
|
<h4 className="text-[36px] font-semibold mb-1.5 font-sans" style={{ color: '#ffffff' }}>{count}</h4>
|
||||||
|
<h6 className="text-lg" style={{ color: '#ffffff' }}>{label}</h6>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Stats() {
|
||||||
|
const sectionRef = useRef<HTMLElement>(null)
|
||||||
|
const [startCounting, setStartCounting] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
setStartCounting(true)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.3 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (sectionRef.current) {
|
||||||
|
observer.observe(sectionRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
ref={sectionRef}
|
||||||
|
className="py-16 relative"
|
||||||
|
id="stats"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/src/assets/images/2.jpg')",
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Overlay */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
style={{ background: 'rgba(10, 30, 80, 0.92)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-3 relative z-10">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-8">
|
||||||
|
<h3 className="text-3xl md:text-4xl font-bold mb-4 font-sans" style={{ color: '#ffffff' }}>
|
||||||
|
{commonDescriptions.statsTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="my-3" style={{ color: '#d0d0d0' }}>
|
||||||
|
{commonDescriptions.sectionDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap lg:flex-nowrap gap-12 pt-8 mt-3">
|
||||||
|
{/* Left Content */}
|
||||||
|
<div className="w-full lg:w-5/12">
|
||||||
|
<h4 className="text-[38px] leading-[46px] font-semibold mb-4 font-sans" style={{ color: '#ffffff' }}>
|
||||||
|
{commonDescriptions.statsSubtitle}
|
||||||
|
</h4>
|
||||||
|
<p className="mt-2.5 text-base leading-6" style={{ color: '#d0d0d0' }}>
|
||||||
|
{commonDescriptions.statsDesc}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2.5 text-base leading-6" style={{ color: '#d0d0d0' }}>
|
||||||
|
{commonDescriptions.statsDesc2}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-3 gap-2.5 mt-12">
|
||||||
|
{statsData.map((stat) => (
|
||||||
|
<StatItem
|
||||||
|
key={stat.id}
|
||||||
|
value={stat.value}
|
||||||
|
label={stat.label}
|
||||||
|
startCounting={startCounting}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Content - Service List */}
|
||||||
|
<div className="w-full lg:w-7/12 my-8 lg:my-0">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-[30px]">
|
||||||
|
{serviceListData.map((service) => (
|
||||||
|
<div
|
||||||
|
key={service.id}
|
||||||
|
className="group stats-service-card flex gap-4 p-10 rounded"
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<span className={`fa fa-${service.icon} text-[32px] text-secondary transition-colors duration-300`} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 className="font-bold mb-2.5 service-title">
|
||||||
|
<a href="#url" className="text-white group-hover:text-title transition-colors duration-300">
|
||||||
|
{service.title}
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
<p className="text-[15px] leading-[25px] mt-2.5 text-[#d0d0d0] group-hover:text-title transition-colors duration-300">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
84
src/components/home/Testimonials.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { testimonialsData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function Testimonials() {
|
||||||
|
const [activeSlide, setActiveSlide] = useState(0)
|
||||||
|
|
||||||
|
// 创建两组轮播数据(与原始 HTML 一致)
|
||||||
|
const slides = [
|
||||||
|
[testimonialsData[0], testimonialsData[1], testimonialsData[2]],
|
||||||
|
[testimonialsData[1], testimonialsData[2], testimonialsData[0]],
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white" id="testimonials">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-8">
|
||||||
|
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
|
||||||
|
{commonDescriptions.testimonialsTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="text-text my-3">
|
||||||
|
{commonDescriptions.sectionDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Carousel */}
|
||||||
|
<div className="relative pb-12">
|
||||||
|
{/* Slides */}
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="flex transition-transform duration-500 ease-in-out"
|
||||||
|
style={{ transform: `translateX(-${activeSlide * 100}%)` }}
|
||||||
|
>
|
||||||
|
{slides.map((slideGroup, slideIndex) => (
|
||||||
|
<div
|
||||||
|
key={slideIndex}
|
||||||
|
className="w-full flex-shrink-0"
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 py-8 mt-3">
|
||||||
|
{slideGroup.map((testimonial, index) => (
|
||||||
|
<div
|
||||||
|
key={`${slideIndex}-${index}`}
|
||||||
|
className={`bg-white rounded-lg shadow-card p-6 ${
|
||||||
|
index === 2 ? 'md:col-start-2 lg:col-start-auto' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<img
|
||||||
|
src={testimonial.image}
|
||||||
|
alt={testimonial.name}
|
||||||
|
className="w-20 h-20 rounded-full mx-auto object-cover"
|
||||||
|
/>
|
||||||
|
<h3 className="text-xl font-bold text-title mt-2 font-sans">
|
||||||
|
{testimonial.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-secondary mb-3">{testimonial.role}</p>
|
||||||
|
<p className="text-text">
|
||||||
|
{testimonial.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Indicators */}
|
||||||
|
<div className="carousel-indicators">
|
||||||
|
{slides.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setActiveSlide(index)}
|
||||||
|
className={activeSlide === index ? 'active' : ''}
|
||||||
|
aria-label={`Go to slide ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
149
src/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { contactInfo, footerLinks, socialLinks } from '../../data/siteData'
|
||||||
|
|
||||||
|
// 社交链接hover颜色映射
|
||||||
|
const socialHoverColors: Record<string, string> = {
|
||||||
|
facebook: 'hover:bg-[#3b5998]',
|
||||||
|
twitter: 'hover:bg-[#1da1f2]',
|
||||||
|
instagram: 'hover:bg-[#c13584]',
|
||||||
|
'google-plus': 'hover:bg-[#dd4b39]',
|
||||||
|
linkedin: 'hover:bg-[#0077b5]',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer
|
||||||
|
className="relative text-white"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/src/assets/images/5.jpg')",
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Overlay */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
style={{ background: 'rgba(20, 20, 20, 0.94)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-16 relative z-10">
|
||||||
|
{/* Footer Top - 使用原始的 2fr 1fr 2fr 1fr 布局 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-[2fr_1fr_2fr_1fr] gap-10 mb-12">
|
||||||
|
{/* Contact Us */}
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xl font-semibold mb-6 font-sans" style={{ color: '#ffffff' }}>Contact Us</h6>
|
||||||
|
<ul className="space-y-2.5">
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="fa fa-map-marker text-secondary w-5 flex-shrink-0" />
|
||||||
|
<p className="text-white text-base leading-[25px]">{contactInfo.address}</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={`tel:${contactInfo.phone}`} className="flex items-center gap-3 text-white hover:text-secondary transition-colors">
|
||||||
|
<span className="fa fa-phone text-secondary w-5" />
|
||||||
|
<span>{contactInfo.phone}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={`mailto:${contactInfo.email}`} className="flex items-center gap-3 text-white hover:text-secondary transition-colors">
|
||||||
|
<span className="fa fa-envelope-open-o text-secondary w-5" />
|
||||||
|
<span>{contactInfo.email}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{/* Social Links */}
|
||||||
|
<div className="flex gap-2.5 mt-5">
|
||||||
|
{socialLinks.map((social) => (
|
||||||
|
<a
|
||||||
|
key={social.name}
|
||||||
|
href={social.href}
|
||||||
|
className={`w-[35px] h-[35px] rounded-full bg-white/10 flex items-center justify-center transition-colors ${socialHoverColors[social.icon] || 'hover:bg-secondary'}`}
|
||||||
|
aria-label={social.name}
|
||||||
|
>
|
||||||
|
<span className={`fa fa-${social.icon} leading-[35px]`} aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Featured Links */}
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xl font-semibold mb-6 font-sans" style={{ color: '#ffffff' }}>Featured Links</h6>
|
||||||
|
<ul className="space-y-2.5">
|
||||||
|
{footerLinks.featured.map((link) => (
|
||||||
|
<li key={link.name}>
|
||||||
|
<Link
|
||||||
|
to={link.href}
|
||||||
|
className="text-white text-base leading-[25px] hover:text-secondary transition-colors"
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Newsletter */}
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xl font-semibold mb-6 font-sans" style={{ color: '#ffffff' }}>Newsletter</h6>
|
||||||
|
<p className="text-white mb-3">Get in your inbox the latest News and</p>
|
||||||
|
<form className="flex mb-6" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
className="flex-1 bg-white/10 border border-white/15 px-5 py-3 text-white rounded-l outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-secondary px-5 py-3 rounded-r hover:bg-secondary/80 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="fa fa-envelope-o" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<p className="text-white">Subscribe and get our weekly newsletter</p>
|
||||||
|
<p className="text-white">We'll never share your email address</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xl font-semibold mb-6 font-sans" style={{ color: '#ffffff' }}>Quick Links</h6>
|
||||||
|
<ul className="space-y-2.5">
|
||||||
|
{footerLinks.quick.map((link) => (
|
||||||
|
<li key={link.name}>
|
||||||
|
<Link
|
||||||
|
to={link.href}
|
||||||
|
className="text-white text-base leading-[25px] hover:text-secondary transition-colors"
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Bottom */}
|
||||||
|
<div className="border-t border-[#454545] pt-8 mt-8">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<p className="text-white text-base leading-[25px]">
|
||||||
|
© 2025 Finance Ideas. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<ul className="flex gap-4 md:justify-end">
|
||||||
|
<li>
|
||||||
|
<a href="#" className="text-white hover:text-secondary transition-colors">
|
||||||
|
Privacy policy
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="text-white hover:text-secondary transition-colors">
|
||||||
|
Terms of service
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
143
src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Link, useLocation } from '@tanstack/react-router'
|
||||||
|
import { menuItems } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false)
|
||||||
|
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 80)
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 关闭菜单当路由变化时
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMenuOpen(false)
|
||||||
|
}, [location.pathname])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||||
|
isScrolled ? 'bg-white shadow-md' : 'bg-transparent'
|
||||||
|
}`}>
|
||||||
|
<nav className="container mx-auto px-4">
|
||||||
|
<div className="flex items-center justify-between py-4">
|
||||||
|
{/* Logo */}
|
||||||
|
<Link to="/" className="flex items-center">
|
||||||
|
<span className={`text-2xl font-bold font-sans transition-colors ${
|
||||||
|
isScrolled ? 'text-primary' : 'text-white'
|
||||||
|
}`}>
|
||||||
|
Finance <span className="text-secondary">Ideas</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden lg:flex items-center gap-8">
|
||||||
|
<ul className="flex items-center gap-6">
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<li key={item.name}>
|
||||||
|
<Link
|
||||||
|
to={item.href}
|
||||||
|
className={`font-medium transition-colors hover:text-secondary ${
|
||||||
|
isScrolled ? 'text-title' : 'text-white'
|
||||||
|
} ${location.pathname === item.href ? 'text-secondary' : ''}`}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Search Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSearchOpen(true)}
|
||||||
|
className={`p-2 transition-colors hover:text-secondary ${
|
||||||
|
isScrolled ? 'text-title' : 'text-white'
|
||||||
|
}`}
|
||||||
|
aria-label="Search"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
|
className={`lg:hidden p-2 transition-colors ${
|
||||||
|
isScrolled ? 'text-title' : 'text-white'
|
||||||
|
}`}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
{isMenuOpen ? (
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation */}
|
||||||
|
<div className={`lg:hidden overflow-hidden transition-all duration-300 ${
|
||||||
|
isMenuOpen ? 'max-h-64 pb-4' : 'max-h-0'
|
||||||
|
}`}>
|
||||||
|
<ul className="flex flex-col gap-4">
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<li key={item.name}>
|
||||||
|
<Link
|
||||||
|
to={item.href}
|
||||||
|
className={`block font-medium transition-colors hover:text-secondary ${
|
||||||
|
isScrolled ? 'text-title' : 'text-white'
|
||||||
|
} ${location.pathname === item.href ? 'text-secondary' : ''}`}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Search Popup */}
|
||||||
|
{isSearchOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
|
||||||
|
<div className="w-full max-w-2xl px-4">
|
||||||
|
<form className="relative" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search your Keyword"
|
||||||
|
className="w-full h-16 px-6 pr-14 text-lg rounded-lg border-0 focus:ring-2 focus:ring-primary"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-text hover:text-primary"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSearchOpen(false)}
|
||||||
|
className="absolute top-8 right-8 text-white text-4xl hover:text-secondary"
|
||||||
|
aria-label="Close search"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
32
src/components/layout/ScrollToTop.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function ScrollToTop() {
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsVisible(window.scrollY > 200)
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className={`fixed bottom-4 right-4 w-10 h-10 bg-secondary hover:bg-secondary/80 text-white rounded shadow-lg flex items-center justify-center transition-all duration-300 z-50 ${
|
||||||
|
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
aria-label="Back to top"
|
||||||
|
>
|
||||||
|
<span className="fa fa-angle-up" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/components/services/AdvanceFeatures.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { advanceFeaturesData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function AdvanceFeatures() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-light-bg" id="features">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-12">
|
||||||
|
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
|
||||||
|
{commonDescriptions.advanceFeaturesTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="text-text my-3">
|
||||||
|
{commonDescriptions.sectionDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-8 mt-8 pt-3">
|
||||||
|
{advanceFeaturesData.map((feature) => (
|
||||||
|
<div
|
||||||
|
key={feature.id}
|
||||||
|
className="feature-gd grid gap-5"
|
||||||
|
style={{ gridTemplateColumns: 'auto 1fr' }}
|
||||||
|
>
|
||||||
|
<div className="icon flex-shrink-0 w-[55px] h-[55px] bg-secondary rounded text-center leading-[55px]">
|
||||||
|
<span className={`fa fa-${feature.icon} text-[22px] text-white`} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div className="icon-info">
|
||||||
|
<h5 className="text-[20px] leading-[30px] font-bold mb-3 font-sans">
|
||||||
|
<a href="#" className="text-title hover:text-secondary transition-colors">
|
||||||
|
{feature.title}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<p className="text-text text-base leading-6 mb-3 max-w-[450px]">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
<a href="#" className="text-secondary hover:text-primary transition-colors font-bold">
|
||||||
|
Read More <span className="fa fa-angle-right pl-1" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
52
src/components/services/ProcessSteps.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { processStepsData, commonDescriptions } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function ProcessSteps() {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative"
|
||||||
|
id="process"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/src/assets/images/2.jpg')",
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Overlay */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
style={{ background: 'linear-gradient(45deg, #17449e, rgba(0, 0, 0, 0.8))', opacity: 0.9 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-24 relative z-10">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center mx-auto">
|
||||||
|
<h3 className="text-3xl md:text-4xl font-bold mb-4 font-sans" style={{ color: '#ffffff' }}>
|
||||||
|
{commonDescriptions.processTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="text-white my-3">
|
||||||
|
{commonDescriptions.sectionDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Steps */}
|
||||||
|
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-8 mt-5 pt-3 text-center">
|
||||||
|
{processStepsData.map((step) => (
|
||||||
|
<div key={step.id} className="three-grids-columns">
|
||||||
|
<div className="icon mb-4">
|
||||||
|
<span className="inline-flex items-center justify-center w-[50px] h-[50px] rounded-full bg-secondary text-white text-2xl">
|
||||||
|
{step.number}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h4 className="text-[22px] font-semibold mt-5 mb-4 font-sans" style={{ color: '#ffffff' }}>
|
||||||
|
{step.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-white/70 leading-6">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
src/components/services/ServiceCards.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { serviceCardsData } from '../../data/siteData'
|
||||||
|
|
||||||
|
export default function ServiceCards() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white" id="services">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
{serviceCardsData.map((service) => (
|
||||||
|
<div
|
||||||
|
key={service.id}
|
||||||
|
className={`${service.bgClass} min-h-[280px] rounded-lg overflow-hidden`}
|
||||||
|
>
|
||||||
|
<div className="p-8 md:p-12 h-full flex flex-col justify-center text-center">
|
||||||
|
<h4 className="text-xl font-bold mb-4 font-sans">
|
||||||
|
<a href="#url" style={{ color: '#ffffff' }} className="hover:text-secondary transition-colors">
|
||||||
|
{service.title}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<p className="text-white/90">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
src/components/shared/Breadcrumb.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
interface BreadcrumbProps {
|
||||||
|
title: string
|
||||||
|
currentPage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Breadcrumb({ title, currentPage }: BreadcrumbProps) {
|
||||||
|
return (
|
||||||
|
<section className="breadcrum-bg py-20">
|
||||||
|
<div className="container mx-auto px-4 py-5">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-2 font-sans" style={{ color: '#ffffff' }}>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-white">
|
||||||
|
<Link to="/" className="hover:text-secondary transition-colors">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
/
|
||||||
|
{currentPage || title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
381
src/data/siteData.ts
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
// 导航菜单
|
||||||
|
export const menuItems = [
|
||||||
|
{ name: 'Home', href: '/' },
|
||||||
|
{ name: 'About', href: '/about' },
|
||||||
|
{ name: 'Services', href: '/services' },
|
||||||
|
{ name: 'Contact', href: '/contact' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 轮播数据
|
||||||
|
export const sliderData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Check Out Our Latests Tips & Tricks ',
|
||||||
|
buttonText: 'Read More',
|
||||||
|
buttonLink: '/services',
|
||||||
|
bgClass: 'bg-slider-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'The Largest business Expert in Newyork',
|
||||||
|
buttonText: 'Read More',
|
||||||
|
buttonLink: '/services',
|
||||||
|
bgClass: 'bg-slider-2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Check Out Our Latests Tips & Tricks ',
|
||||||
|
buttonText: 'Read More',
|
||||||
|
buttonLink: '/services',
|
||||||
|
bgClass: 'bg-slider-3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'The Largest business Expert in Newyork',
|
||||||
|
buttonText: 'Read More',
|
||||||
|
buttonLink: '/services',
|
||||||
|
bgClass: 'bg-slider-4',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Features 区块数据
|
||||||
|
export const featuresData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Digital Innovation',
|
||||||
|
description: 'Consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices',
|
||||||
|
image: '/src/assets/images/g5.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Branding Strategy',
|
||||||
|
description: 'Consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices',
|
||||||
|
image: '/src/assets/images/g3.jpg',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 服务卡片数据
|
||||||
|
export const servicesData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
icon: 'ravelry',
|
||||||
|
title: 'Consulting',
|
||||||
|
description: 'Maecenas sodales eu commodo ligula eget dolor dolor sit amet ligula',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
icon: 'send-o',
|
||||||
|
title: 'Valuable Ideas',
|
||||||
|
description: 'Maecenas sodales eu commodo ligula eget dolor dolor sit amet ligula',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
icon: 'clock-o',
|
||||||
|
title: 'Excellent Timing',
|
||||||
|
description: 'Maecenas sodales eu commodo ligula eget dolor dolor sit amet ligula',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
icon: 'money',
|
||||||
|
title: 'Budget Friendly',
|
||||||
|
description: 'Maecenas sodales eu commodo ligula eget dolor dolor sit amet ligula',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 客户评价数据
|
||||||
|
export const testimonialsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Henry Nicholas',
|
||||||
|
role: 'Cofounder',
|
||||||
|
image: '/src/assets/images/c1.jpg',
|
||||||
|
content: '"Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Mark Waugh',
|
||||||
|
role: 'Web Designer',
|
||||||
|
image: '/src/assets/images/c2.jpg',
|
||||||
|
content: '"Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Sarina Willams',
|
||||||
|
role: 'Graphic Designer',
|
||||||
|
image: '/src/assets/images/c3.jpg',
|
||||||
|
content: '"Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore"',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 首页统计数据
|
||||||
|
export const statsData = [
|
||||||
|
{ id: 1, value: 2300, label: 'Clients', suffix: '' },
|
||||||
|
{ id: 2, value: 16, label: 'Awards', suffix: '' },
|
||||||
|
{ id: 3, value: 2536, label: 'Projects', suffix: '' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// What We Offer 服务列表
|
||||||
|
export const serviceListData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
icon: 'laptop',
|
||||||
|
title: 'SEO solutions',
|
||||||
|
description: 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Vero, dolore.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
icon: 'database',
|
||||||
|
title: 'Social media',
|
||||||
|
description: 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Vero, dolore.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
icon: 'lock',
|
||||||
|
title: 'Branding Identity',
|
||||||
|
description: 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Vero, dolore.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
icon: 'codepen',
|
||||||
|
title: 'Content Marketing',
|
||||||
|
description: 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Vero, dolore.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 博客文章
|
||||||
|
export const blogData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Plant Trees While Searching The Web',
|
||||||
|
categories: ['Corporate', 'Photography'],
|
||||||
|
date: 'Mar 29, 2020',
|
||||||
|
description: 'Lorem ipsum dolor sit amet nostrum ad minus libero fugiat molestiae ullam ipsam quas unde earum...',
|
||||||
|
image: '/src/assets/images/g7.jpg',
|
||||||
|
link: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Why Are Remarketing Ads Important?',
|
||||||
|
categories: ['Art', 'Photography'],
|
||||||
|
date: 'Sep 28, 2020',
|
||||||
|
description: 'Lorem ipsum dolor sit amet nostrum ad minus libero fugiat molestiae ullam ipsam quas unde earum...',
|
||||||
|
image: '/src/assets/images/g8.jpg',
|
||||||
|
link: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'How to Build a Chatbot from Pitch to Promotion',
|
||||||
|
categories: ['Travel', 'Fashion'],
|
||||||
|
date: 'Dec 28, 2020',
|
||||||
|
description: 'Lorem ipsum dolor sit amet nostrum ad minus libero fugiat molestiae ullam ipsam quas unde earum...',
|
||||||
|
image: '/src/assets/images/g9.jpg',
|
||||||
|
link: '#',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 团队成员
|
||||||
|
export const teamData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Micheal Wagou',
|
||||||
|
role: 'Director',
|
||||||
|
image: '/src/assets/images/team1.jpg',
|
||||||
|
social: {
|
||||||
|
facebook: '#',
|
||||||
|
twitter: '#',
|
||||||
|
linkedin: '#',
|
||||||
|
google: '#',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Alex Mitchell',
|
||||||
|
role: 'Manager',
|
||||||
|
image: '/src/assets/images/team2.jpg',
|
||||||
|
social: {
|
||||||
|
facebook: '#',
|
||||||
|
twitter: '#',
|
||||||
|
linkedin: '#',
|
||||||
|
google: '#',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Scott Colon',
|
||||||
|
role: 'Supervisor',
|
||||||
|
image: '/src/assets/images/team3.jpg',
|
||||||
|
social: {
|
||||||
|
facebook: '#',
|
||||||
|
twitter: '#',
|
||||||
|
linkedin: '#',
|
||||||
|
google: '#',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'John Harris',
|
||||||
|
role: 'Consultant',
|
||||||
|
image: '/src/assets/images/team4.jpg',
|
||||||
|
social: {
|
||||||
|
facebook: '#',
|
||||||
|
twitter: '#',
|
||||||
|
linkedin: '#',
|
||||||
|
google: '#',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 关于页统计
|
||||||
|
export const aboutStatsData = [
|
||||||
|
{ id: 1, value: 7242, label: 'Hours of Works', icon: 'hourglass' },
|
||||||
|
{ id: 2, value: 380, label: 'Projects Done', icon: 'folder' },
|
||||||
|
{ id: 3, value: 220, label: 'Satisfied Customers', icon: 'smile-o' },
|
||||||
|
{ id: 4, value: 24, label: 'Awards Winning', icon: 'trophy' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Why Choose Us 列表
|
||||||
|
export const whyChooseUsData = [
|
||||||
|
'Website Designing and Development',
|
||||||
|
'Android and iOs Apps Development',
|
||||||
|
'Successfully Providing Business Solution from 20 years',
|
||||||
|
'Excellence Records',
|
||||||
|
]
|
||||||
|
|
||||||
|
// 服务页服务卡片
|
||||||
|
export const serviceCardsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Financial Institution',
|
||||||
|
description: 'Lorem ipsum dolor sit amet sed consectetur adipisicing elit. doloret quas saepe autem, dolor set.',
|
||||||
|
bgClass: 'ser-bg1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Concept & Strategy',
|
||||||
|
description: 'Lorem ipsum dolor sit amet sed consectetur adipisicing elit. doloret quas saepe autem, dolor set.',
|
||||||
|
bgClass: 'ser-bg2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Interactive Business',
|
||||||
|
description: 'Lorem ipsum dolor sit amet sed consectetur adipisicing elit. doloret quas saepe autem, dolor set.',
|
||||||
|
bgClass: 'ser-bg3',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 流程步骤
|
||||||
|
export const processStepsData = [
|
||||||
|
{ id: 1, number: '1', title: 'Register', description: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor.' },
|
||||||
|
{ id: 2, number: '2', title: 'App Download', description: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor.' },
|
||||||
|
{ id: 3, number: '3', title: 'Product Testing', description: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor.' },
|
||||||
|
{ id: 4, number: '4', title: 'Pro Support', description: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor.' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 高级特性
|
||||||
|
export const advanceFeaturesData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
icon: 'handshake-o',
|
||||||
|
title: 'User Friendly',
|
||||||
|
description: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices posuere primis in faucibus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
icon: 'mobile',
|
||||||
|
title: 'Responsive Layout',
|
||||||
|
description: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices posuere primis in faucibus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
icon: 'gg',
|
||||||
|
title: 'Easy to customize',
|
||||||
|
description: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices posuere primis in faucibus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
icon: 'eye',
|
||||||
|
title: 'Cross Browser',
|
||||||
|
description: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices posuere primis in faucibus',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 联系信息
|
||||||
|
export const contactInfo = {
|
||||||
|
address: '123 Business Street, Suite 100, New York, NY 10001, USA',
|
||||||
|
phone: '+1 (555) 123-4567',
|
||||||
|
email: 'contact@example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 联系页表单区块信息
|
||||||
|
export const contactFormInfo = {
|
||||||
|
title: 'Leave us a Message',
|
||||||
|
subtitle: 'For more info or inquiry about our products project, and pricing please feel free to get in touch with us.',
|
||||||
|
email: 'info@example.com',
|
||||||
|
address: 'Corporate Office, #32841 block, #221DRS Lorem Street, UK.',
|
||||||
|
phone: '+121-345-6789',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页脚链接
|
||||||
|
export const footerLinks = {
|
||||||
|
featured: [
|
||||||
|
{ name: 'Our People', href: '/contact' },
|
||||||
|
{ name: 'Latest Media', href: '/contact' },
|
||||||
|
{ name: 'Our Branches', href: '/contact' },
|
||||||
|
{ name: 'Organisations', href: '/contact' },
|
||||||
|
{ name: 'Help', href: '/contact' },
|
||||||
|
],
|
||||||
|
quick: [
|
||||||
|
{ name: 'Home', href: '/' },
|
||||||
|
{ name: 'About', href: '/about' },
|
||||||
|
{ name: 'Services', href: '/services' },
|
||||||
|
{ name: 'Blog', href: '#' },
|
||||||
|
{ name: 'Contact', href: '/contact' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 社交链接
|
||||||
|
export const socialLinks = [
|
||||||
|
{ name: 'facebook', href: '#', icon: 'facebook' },
|
||||||
|
{ name: 'twitter', href: '#', icon: 'twitter' },
|
||||||
|
{ name: 'instagram', href: '#', icon: 'instagram' },
|
||||||
|
{ name: 'google-plus', href: '#', icon: 'google-plus' },
|
||||||
|
{ name: 'linkedin', href: '#', icon: 'linkedin' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 通用描述文本
|
||||||
|
export const commonDescriptions = {
|
||||||
|
sectionDesc: 'Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla mollis dapibus nunc, ut rhoncus turpis sodales quis. Integer sit amet mattis quam.',
|
||||||
|
featuresSubtitle: 'More than 10 years experience in the finance industry',
|
||||||
|
featuresDesc: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices posuere primis in faucibus',
|
||||||
|
servicesTitle: 'Designed for you',
|
||||||
|
ctaTitle: 'Developing first class solutions for our clients.',
|
||||||
|
testimonialsTitle: 'What They Says',
|
||||||
|
statsTitle: 'What We Offer',
|
||||||
|
statsSubtitle: "We're Offering Unmatched Services",
|
||||||
|
statsDesc: 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Vero, dolore. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Vero, dolore. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla mollis dapibus nunc Integer sit amet mattis quam.',
|
||||||
|
statsDesc2: 'Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla mollis dapibus nunc, ut rhoncus turpis sodales quis. Integer sit amet mattis quam.',
|
||||||
|
newsTitle: 'Recent posts and updates',
|
||||||
|
teamTitle: 'Our Team',
|
||||||
|
aboutQuote: '"We are always looking ahead to stay on top of the latest design & technology."',
|
||||||
|
aboutDesc: 'Lorem ipsum dolor sit amet, consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices posuere primis in faucibus',
|
||||||
|
whyChooseUsTitle: 'Why choose Us',
|
||||||
|
whyChooseUsDesc: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium dolore mque laudantium, totam rem aperiam, eaque ipsa quae ab illo invent ore veritatis et perspiciatis unde omnis iste natus error sit voluptatemerror sit voluptatem',
|
||||||
|
processTitle: 'Four Steps for Process',
|
||||||
|
advanceFeaturesTitle: 'Advance Features!',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mission/Vision 数据
|
||||||
|
export const missionVisionData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Our Mission',
|
||||||
|
description: 'Consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices',
|
||||||
|
image: '/src/assets/images/g5.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Our Vision',
|
||||||
|
description: 'Consectetur adipisicingelit, sed do eiusmod tempor incididunt primis in faucibus orci luctus et ultrices',
|
||||||
|
image: '/src/assets/images/g3.jpg',
|
||||||
|
},
|
||||||
|
]
|
||||||
241
src/index.css
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
/* 主题色 */
|
||||||
|
--color-primary: #2e5deb;
|
||||||
|
--color-secondary: #ff5b83;
|
||||||
|
--color-text: #585858;
|
||||||
|
--color-title: #1A1D2D;
|
||||||
|
--color-light-bg: #f6f6f6;
|
||||||
|
--color-services-bg: #f2f8ff;
|
||||||
|
|
||||||
|
/* 字体 */
|
||||||
|
--font-family-sans: 'Poppins', sans-serif;
|
||||||
|
--font-family-body: 'Hind', sans-serif;
|
||||||
|
|
||||||
|
/* 阴影 */
|
||||||
|
--shadow-card: 0px 9px 24px 5px rgba(0, 0, 0, 0.04);
|
||||||
|
--shadow-card-hover: 1px 20px 30px rgba(196, 196, 196, 0.2);
|
||||||
|
|
||||||
|
/* 圆角 */
|
||||||
|
--radius-card: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局样式 */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family-body);
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
color: var(--color-title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务页背景图片 */
|
||||||
|
.ser-bg1 {
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/src/assets/images/g1.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ser-bg2 {
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/src/assets/images/g2.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ser-bg3 {
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/src/assets/images/g4.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 面包屑背景 */
|
||||||
|
.breadcrum-bg {
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url('/src/assets/images/6.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动鼠标动画 */
|
||||||
|
.icon-scroll {
|
||||||
|
width: 25px;
|
||||||
|
height: 40px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 15px;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-scroll .wheel {
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
animation: scroll 1.5s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) translateY(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片缩放效果 */
|
||||||
|
.zoom {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom img {
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom:hover img {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.btn-theme2 {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 30px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-theme2:hover {
|
||||||
|
background-color: #e04a70;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式 */
|
||||||
|
.contact-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-family: var(--font-family-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订阅表单 */
|
||||||
|
.subscribe {
|
||||||
|
display: flex;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe button {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计数字动画 */
|
||||||
|
@keyframes countUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Swiper 自定义样式 */
|
||||||
|
.hero-swiper .swiper-button-next,
|
||||||
|
.hero-swiper .swiper-button-prev {
|
||||||
|
color: white;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-swiper .swiper-button-next:after,
|
||||||
|
.hero-swiper .swiper-button-prev:after {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-swiper .swiper-pagination-bullet {
|
||||||
|
background: white;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-swiper .swiper-pagination-bullet-active {
|
||||||
|
opacity: 1;
|
||||||
|
background: #ff5b83;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 轮播指示器 */
|
||||||
|
.carousel-indicators {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-indicators button {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #ddd;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-indicators button.active {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 团队卡片社交图标 */
|
||||||
|
.team-info .social {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务卡片 hover 时标题变色 */
|
||||||
|
.group:hover h4 {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats 区块服务卡片 hover 效果 */
|
||||||
|
.stats-service-card {
|
||||||
|
background: rgba(46, 93, 235, 0.33);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-service-card:hover {
|
||||||
|
background: #ffffff !important;
|
||||||
|
}
|
||||||
19
src/main.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||||
|
import { routeTree } from './routeTree.gen'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
const router = createRouter({ routeTree })
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
113
src/routeTree.gen.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as ServicesRouteImport } from './routes/services'
|
||||||
|
import { Route as ContactRouteImport } from './routes/contact'
|
||||||
|
import { Route as AboutRouteImport } from './routes/about'
|
||||||
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
|
||||||
|
const ServicesRoute = ServicesRouteImport.update({
|
||||||
|
id: '/services',
|
||||||
|
path: '/services',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ContactRoute = ContactRouteImport.update({
|
||||||
|
id: '/contact',
|
||||||
|
path: '/contact',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AboutRoute = AboutRouteImport.update({
|
||||||
|
id: '/about',
|
||||||
|
path: '/about',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const IndexRoute = IndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/about': typeof AboutRoute
|
||||||
|
'/contact': typeof ContactRoute
|
||||||
|
'/services': typeof ServicesRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/about': typeof AboutRoute
|
||||||
|
'/contact': typeof ContactRoute
|
||||||
|
'/services': typeof ServicesRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRouteImport
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/about': typeof AboutRoute
|
||||||
|
'/contact': typeof ContactRoute
|
||||||
|
'/services': typeof ServicesRoute
|
||||||
|
}
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths: '/' | '/about' | '/contact' | '/services'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to: '/' | '/about' | '/contact' | '/services'
|
||||||
|
id: '__root__' | '/' | '/about' | '/contact' | '/services'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
AboutRoute: typeof AboutRoute
|
||||||
|
ContactRoute: typeof ContactRoute
|
||||||
|
ServicesRoute: typeof ServicesRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/services': {
|
||||||
|
id: '/services'
|
||||||
|
path: '/services'
|
||||||
|
fullPath: '/services'
|
||||||
|
preLoaderRoute: typeof ServicesRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/contact': {
|
||||||
|
id: '/contact'
|
||||||
|
path: '/contact'
|
||||||
|
fullPath: '/contact'
|
||||||
|
preLoaderRoute: typeof ContactRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/about': {
|
||||||
|
id: '/about'
|
||||||
|
path: '/about'
|
||||||
|
fullPath: '/about'
|
||||||
|
preLoaderRoute: typeof AboutRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
AboutRoute: AboutRoute,
|
||||||
|
ContactRoute: ContactRoute,
|
||||||
|
ServicesRoute: ServicesRoute,
|
||||||
|
}
|
||||||
|
export const routeTree = rootRouteImport
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
29
src/routes/__root.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { createRootRoute, Outlet, useLocation } from '@tanstack/react-router'
|
||||||
|
import Header from '../components/layout/Header'
|
||||||
|
import Footer from '../components/layout/Footer'
|
||||||
|
import ScrollToTop from '../components/layout/ScrollToTop'
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
component: RootLayout,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RootLayout() {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
|
// 路由切换时滚动到页面顶部
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<ScrollToTop />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
src/routes/about.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import Breadcrumb from '../components/shared/Breadcrumb'
|
||||||
|
import Mission from '../components/about/Mission'
|
||||||
|
import WhyChooseUs from '../components/about/WhyChooseUs'
|
||||||
|
import Statistics from '../components/about/Statistics'
|
||||||
|
import Team from '../components/about/Team'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/about')({
|
||||||
|
component: AboutPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
function AboutPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb title="About Us" currentPage="About" />
|
||||||
|
<Mission />
|
||||||
|
<WhyChooseUs />
|
||||||
|
<Statistics />
|
||||||
|
<Team />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/routes/contact.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import Breadcrumb from '../components/shared/Breadcrumb'
|
||||||
|
import ContactForm from '../components/contact/ContactForm'
|
||||||
|
import Map from '../components/contact/Map'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/contact')({
|
||||||
|
component: ContactPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
function ContactPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb title="Contact Us" currentPage="Contact" />
|
||||||
|
<ContactForm />
|
||||||
|
<Map />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
26
src/routes/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import HeroSlider from '../components/home/HeroSlider'
|
||||||
|
import Features from '../components/home/Features'
|
||||||
|
import Services from '../components/home/Services'
|
||||||
|
import CTA from '../components/home/CTA'
|
||||||
|
import Testimonials from '../components/home/Testimonials'
|
||||||
|
import Stats from '../components/home/Stats'
|
||||||
|
import LatestNews from '../components/home/LatestNews'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/')({
|
||||||
|
component: HomePage,
|
||||||
|
})
|
||||||
|
|
||||||
|
function HomePage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HeroSlider />
|
||||||
|
<Features />
|
||||||
|
<Services />
|
||||||
|
<CTA />
|
||||||
|
<Testimonials />
|
||||||
|
<Stats />
|
||||||
|
<LatestNews />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
src/routes/services.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import Breadcrumb from '../components/shared/Breadcrumb'
|
||||||
|
import ServiceCards from '../components/services/ServiceCards'
|
||||||
|
import ProcessSteps from '../components/services/ProcessSteps'
|
||||||
|
import AdvanceFeatures from '../components/services/AdvanceFeatures'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/services')({
|
||||||
|
component: ServicesPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
function ServicesPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb title="Our Services" currentPage="Services" />
|
||||||
|
<ServiceCards />
|
||||||
|
<ProcessSteps />
|
||||||
|
<AdvanceFeatures />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
tsconfig.app.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
19
tsconfig.node.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
20
vite.config.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
TanStackRouterVite({
|
||||||
|
target: 'react',
|
||||||
|
autoCodeSplitting: true,
|
||||||
|
}),
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
})
|
||||||