This commit is contained in:
zhangjianjun 2026-07-14 10:42:14 +08:00
parent e67110eafd
commit a170b1372b
10 changed files with 754 additions and 2 deletions

View File

@ -0,0 +1,8 @@
{
"files": [
"src/index.html"
],
"insertBefore": "</body>",
"commentSyntax": "html",
"cspChecked": true
}

47
AGENTS.md Normal file
View File

@ -0,0 +1,47 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
`corp-mp` is a **Taro 4.2** cross-platform app (React 18 + TypeScript + SCSS, compiled with **Vite**). The primary target is the WeChat mini-program (`weapp`); Taro also compiles it to H5, Alipay, Swan, TT, QQ, JD, RN, and Harmony.
## Commands
Package manager is **pnpm** (a `pnpm-lock.yaml` is committed).
```bash
pnpm install # install deps
pnpm dev:weapp # WeChat mini-program dev build with --watch → ./dist
pnpm build:weapp # production mini-program build → ./dist
pnpm dev:h5 / build:h5 # H5 target
```
Other targets follow `pnpm dev:<platform>` / `pnpm build:<platform>` (`alipay`, `swan`, `tt`, `qq`, `jd`, `rn`, `harmony-hybrid`).
**Running the mini-program:** builds output to `./dist`. Open `./dist` in WeChat DevTools (`project.config.json` already points `miniprogramRoot` there; appId `wx6d4f6f29c41aff93`).
**There is no test runner and no lint script in `package.json`.** ESLint (`taro/react`) and Stylelint are configured but run manually. The only enforced gate is a Husky `commit-msg` hook running **commitlint** (Conventional Commits) — non-conforming commit messages are rejected.
## Architecture
The app is a thin native shell around a business H5, with a native payment bridge. Two pages, registered in `src/app.config.ts`:
**1. `pages/index/index.tsx` — auth + WebView host.** On load it calls `Taro.login()` to get the WeChat `code`, then renders a full-screen `<WebView>` pointing at the business H5 (`http://niubsw.com/...?mpCode=<code>`). All business UI lives in that remote H5, not in this repo.
**2. `pages/pay/index.tsx` — native payment bridge.** The H5 cannot invoke WeChat JSAPI pay while running inside the mini-program WebView, so it hands off to this native page. The handoff contract:
- The H5 detects it's inside the mini-program and calls `wxSdk.miniProgram.navigateTo({ url: '/pages/pay/index?order=<b64>&formInfo=<b64>' })`.
- Params are **base64-encoded JSON**, produced H5-side by `utf8ToBase64(v) = btoa(unescape(encodeURIComponent(JSON.stringify(v))))`.
- `order` already contains the complete WeChat JSAPI fields (`timeStamp`/`nonceStr`/`package`/`signType`/`paySign`). The page decodes it, auto-fires `Taro.requestPayment`, then `navigateBack()`s to the H5 on success. No backend call is made here.
- `temp.txt` (repo root) holds the H5-side snippet documenting this contract — keep the two sides in sync.
**`src/utils/base64.ts`** is the strict inverse of the H5 encoder. It hand-rolls a base64 decoder because **weapp has no global `atob`**, and normalizes `+`→space mangling from URL routing. Reuse `decodeOrderParam<T>(raw)` for any future base64 route params.
## Conventions
- **Page = folder** under `src/pages/<name>/` with `index.tsx` + `index.scss` + `index.config.ts`. New pages **must** be added to the `pages` array in `src/app.config.ts` or they won't route.
- **Lifecycle:** use Taro hooks — `useLoad` for page init (reads route params via `Taro.getCurrentInstance().router?.params`), `useLaunch` for app init — not bare `useEffect`.
- **Native API calls must be env-guarded:** wrap `weapp`-only APIs (e.g. `Taro.requestPayment`) in `if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP)` with a fallback branch for other targets.
- **Styling:** SCSS with BEM (`block__element--modifier`); CSS Modules are disabled. Design width is `750`, so author dimensions in `px` — Taro converts to `rpx`/`rem` at compile time.
- **Imports:** `@/*` is aliased to `src/*` (see `tsconfig.json`).
- `strictNullChecks`, `noUnusedLocals`, and `noUnusedParameters` are on; `noImplicitAny` is off.
A more verbose Chinese-language version of these notes lives in `GEMINI.md`.

182
DESIGN.md Normal file
View File

@ -0,0 +1,182 @@
---
name: 门店管理服务
description: 面向线下门店经营负责人的蓝色科技感业务落地页
colors:
midnight-navy: "#05256E"
service-blue: "#0B57D0"
action-blue: "#1677FF"
signal-cyan: "#5FC8FF"
canvas-blue: "#F3F7FF"
pure-white: "#FFFFFF"
trust-ink: "#102A56"
supporting-ink: "#425F8C"
soft-border: "#D7E4FF"
typography:
display:
fontFamily: "-apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif"
fontSize: "56px"
fontWeight: 700
lineHeight: 1.18
letterSpacing: "-1px"
title:
fontFamily: "-apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif"
fontSize: "32px"
fontWeight: 700
lineHeight: 1.4
body:
fontFamily: "-apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif"
fontSize: "28px"
fontWeight: 400
lineHeight: 1.65
label:
fontFamily: "-apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif"
fontSize: "24px"
fontWeight: 600
lineHeight: 1.4
rounded:
sm: "12px"
md: "24px"
lg: "36px"
pill: "999px"
spacing:
xs: "8px"
sm: "16px"
md: "24px"
lg: "32px"
xl: "48px"
section: "72px"
components:
call-button:
backgroundColor: "{colors.action-blue}"
textColor: "{colors.pure-white}"
rounded: "{rounded.pill}"
height: "88px"
padding: "0 36px"
industry-mark:
backgroundColor: "{colors.canvas-blue}"
textColor: "{colors.service-blue}"
rounded: "{rounded.md}"
size: "112px"
service-item:
backgroundColor: "{colors.pure-white}"
textColor: "{colors.trust-ink}"
rounded: "{rounded.md}"
padding: "28px"
---
# Design System: 门店管理服务
## Overview
**Creative North Star: "清晨开店前的蓝色灯箱"**
页面像门店营业前刚亮起的蓝色灯箱:清晰、稳定、有秩序。深蓝主视觉承担品牌识别,纯白内容面保证业务说明快速可读,青蓝只用于光线和细节提示。
整个系统拒绝地图工具、表单后台和政务门户的视觉语言,也拒绝用大量发光、玻璃面板或彩虹图标堆叠科技感。布局应让经营负责人在短时间内认出自己的行业、理解服务内容并完成电话咨询。
**Key Characteristics:**
- 深蓝主视觉与浅色信息面的明确分区,不用面板覆盖主视觉。
- 门店剪影完整呈现,真实业务文案始终由原生组件渲染。
- 行业标识收纳在统一横向信息带中,服务说明舒展,唯一主操作固定在底部。
- 轻量入场动效有静态降级,不阻塞内容显示。
## Colors
以深蓝建立信任,以亮蓝承载行动,以青蓝提供有限的光感;内容区保持接近纯白,正文使用带蓝相的深色文字。
### Primary
- **午夜门楣蓝:** 用于原生导航栏、首屏深色背景与最深层光影。
- **服务蓝:** 用于主视觉过渡、行业标识和服务序号。
- **行动蓝:** 仅用于拨号按钮和关键交互状态。
### Secondary
- **信号青:** 只用于插画轮廓、光带和小面积高光,不承载正文。
### Neutral
- **清晨画布:** 页面内容背景,带极轻蓝相。
- **纯白:** 服务内容与固定电话栏的主要表面。
- **信任墨蓝:** 标题和正文主色。
- **辅助墨蓝:** 次要说明文字,仍需满足 AA 对比度。
- **柔和分隔蓝:** 仅用于完整边框和分隔线。
**The Blue Commitment Rule.** 蓝色覆盖首屏主视觉,但内容区必须回到高对比浅色表面;禁止用第三种高饱和色争夺注意力。
## Typography
**Display Font:** 系统无衬线字体(优先 PingFang SC
**Body Font:** 系统无衬线字体(优先 PingFang SC
**Character:** 单一系统字体通过字号和字重建立层级,加载稳定,适合微信小程序和中文快速扫读。
### Hierarchy
- **Display**70056px1.18):仅用于首屏“门店管理服务”。
- **Title**70032px1.4):用于“服务行业”“核心服务”和服务名称。
- **Body**40028px1.65):用于业务说明,单行长度受移动端容器约束。
- **Label**60024px1.4):用于行业名称、电话提示和辅助标签。
**The Plain Language Rule.** 标题直接命名业务,按钮使用“立即拨打”等动词加对象表达;禁止口号式套话和全大写正文。
## Elevation
系统以色块、完整边框和有限的环境阴影建立层次。内容表面默认平坦,固定电话栏可使用向上的柔和阴影与页面分离,主视觉光晕只存在于蓝色背景内部。
### Shadow Vocabulary
- **内容环境影:** 低透明度、大模糊半径,只用于主内容容器与蓝色主视觉交界处。
- **固定栏上浮影:** 从底部栏向上扩散,说明其固定层级,不模拟厚重卡片。
**The Flat Content Rule.** 服务信息依靠留白与分隔,不给每个条目添加独立厚重阴影。
## Components
### Buttons
- **Shape:** 44px 视觉高度的胶囊按钮88px 设计稿高度)。
- **Primary:** 行动蓝底、纯白粗体文字,左右内边距 36px。
- **Active / Focus:** 按下时轻微降低亮度;焦点通过完整轮廓表达,不移动布局。
### Chips
- **Style:** 三个行业标识共用一个横向信息带,以 1px 分隔线划分;每项由浅蓝圆形字符标识和行业名称组成。
- **State:** 全部为静态信息,不提供选中、悬停或点击状态。
### Cards / Containers
- **Corner Style:** 主内容容器 36px服务条目 24px。
- **Background:** 纯白或清晨画布。
- **Shadow Strategy:** 主容器允许环境影,服务条目使用完整边框或分隔线。
- **Internal Padding:** 24px 至 40px保持移动端舒适密度。
### Navigation
- 原生导航栏使用午夜门楣蓝背景、纯白标题“门店管理服务”,不放 Logo 或自定义返回控件。
### Fixed Consultation Bar
- 白色固定底栏包含“业务咨询”、电话号码和“立即拨打”按钮。
- 底部使用安全区内边距,页面正文预留等高空间,任何屏幕都不得遮挡内容。
## Do's and Don'ts
### Do:
- **Do** 让百货、超市、便利店在首屏直接可见。
- **Do** 使用原生文字承载业务内容,插画加载失败时信息仍完整。
- **Do** 按图片宽度展示门店主视觉,只裁去上方空蓝区域,完整保留门店建筑与地面基线。
- **Do** 保持正文至少 4.5:1 对比度、按钮约 44px 触控高度。
- **Do** 为所有入场动画提供 `prefers-reduced-motion` 静态降级。
- **Do** 使用完整边框、留白和色块建立层级。
### Don't:
- **Don't** 做地图、标注、定位、坐标或地址管理界面。
- **Don't** 做表单页、政务门户或业务办理后台的视觉复刻。
- **Don't** 使用 Logo、彩虹分类图标、夸张赛博霓虹或装饰性玻璃拟态。
- **Don't** 把行业与服务包装成可点击但没有目标的假入口。
- **Don't** 使用负 margin 内容面板覆盖门店图片,或用深色渐变压暗门店底部。
- **Don't** 使用渐变文字、侧边粗色条、嵌套卡片或重复的图标卡片网格。

36
PRODUCT.md Normal file
View File

@ -0,0 +1,36 @@
# Product
## Register
brand
## Users
面向百货、超市与便利店的经营负责人和运营负责人。他们通常在移动端快速了解服务范围,并希望用最短路径联系业务人员。
## Product Purpose
落地页用于清楚介绍门店管理服务覆盖的行业与能力,并将没有 `scene` 的自然访问流量转化为电话咨询。成功标准是访问者能在首屏理解业务对象,在一次滚动内看到完整服务范围,并随时拨打咨询电话。
## Brand Personality
可靠、利落、专业。文案直接说明业务,不夸大结果,不使用营销套话;视觉有明确科技感,但不牺牲可读性和可信度。
## Anti-references
- 不做地图、标注、定位、坐标或地址管理界面。
- 不做表单页、政务门户或业务办理后台的视觉复刻。
- 不使用 Logo、彩虹分类图标、夸张赛博霓虹或装饰性玻璃拟态。
- 不把行业与服务包装成可点击但没有目标的假入口。
## Design Principles
- **先讲清服务对象。** 首屏必须直接出现百货、超市与便利店。
- **唯一行动清晰。** 电话咨询是页面唯一主操作,并始终可见。
- **科技感服务于信任。** 蓝色、光线和门店插画建立专业氛围,不制造地图或数据监控联想。
- **信息先于装饰。** 即使插画未加载,标题、服务范围和联系电话仍完整可用。
- **移动端快速扫读。** 使用短文案、强层级和足够触控尺寸。
## Accessibility & Inclusion
以移动端 WCAG AA 为基线:正文对比度至少 4.5:1大字号至少 3:1交互区域视觉尺寸不小于约 44px不仅依赖颜色传达信息并为减少动态效果提供静态降级。

View File

@ -1,6 +1,7 @@
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/landing/index',
'pages/pay/index',
],
window: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -6,6 +6,7 @@ import './index.scss'
const LINK_PREFIX = '/p/'
const DEFAULT_LINK_CODE = 'Gbym'
const LANDING_PAGE_URL = '/pages/landing/index'
function isValidLinkCode (code: string) {
return Boolean(code) && !/[/?#&=]/.test(code)
@ -31,9 +32,26 @@ function normalizeQrLinkCode (scene?: string) {
export default function Index () {
const [url, setUrl] = useState('')
useLoad(async (options) => {
const scene = typeof options?.scene === 'string' ? options.scene.trim() : ''
if (!scene) {
try {
await Taro.redirectTo({ url: LANDING_PAGE_URL })
} catch (error) {
console.error('Failed to open landing page.', error)
Taro.showToast({
title: '页面加载失败',
icon: 'none'
})
}
return
}
const wxCode = await Taro.login()
const linkCode = normalizeQrLinkCode(options?.scene as string | undefined)
const linkCode = normalizeQrLinkCode(scene)
console.log('Page loaded.', wxCode)
// setUrl(`https://niubsw.com${LINK_PREFIX}${encodeURIComponent(linkCode)}?mpCode=${encodeURIComponent(wxCode.code)}`)

View File

@ -0,0 +1,7 @@
export default definePageConfig({
navigationBarTitleText: '门店管理服务',
navigationBarBackgroundColor: '#05256e',
navigationBarTextStyle: 'white',
backgroundColor: '#f3f7ff',
backgroundTextStyle: 'light'
})

View File

@ -0,0 +1,315 @@
/* stylelint-disable selector-class-pattern -- The project requires BEM class names. */
.landing-page {
--landing-midnight: #05256e;
--landing-service-blue: #0b57d0;
--landing-action-blue: #1677ff;
--landing-signal-cyan: #5fc8ff;
--landing-canvas: #f3f7ff;
--landing-surface: #fff;
--landing-ink: #102a56;
--landing-muted: #425f8c;
--landing-border: #d7e4ff;
min-height: 100vh;
padding-bottom: calc(132px + constant(safe-area-inset-bottom));
padding-bottom: calc(132px + env(safe-area-inset-bottom));
overflow-x: hidden;
box-sizing: border-box;
color: var(--landing-ink);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--landing-canvas);
}
@supports (color: oklch(0 0 0)) {
.landing-page {
--landing-midnight: oklch(29.95% 0.1316 262.9deg);
--landing-service-blue: oklch(49.49% 0.1992 260.6deg);
--landing-action-blue: oklch(59.87% 0.2194 259deg);
--landing-signal-cyan: oklch(79.25% 0.1244 234.3deg);
--landing-canvas: oklch(97.54% 0.0115 264.5deg);
--landing-surface: oklch(100% 0 0deg);
--landing-ink: oklch(29.25% 0.086 260.3deg);
--landing-muted: oklch(48.32% 0.0808 258.9deg);
--landing-border: oklch(91.74% 0.0395 265.1deg);
}
}
.landing-page__hero {
position: relative;
padding: 52px 32px 0;
overflow: hidden;
border-radius: 0 0 36px 36px;
box-sizing: border-box;
background:
radial-gradient(circle at 88% 12%, rgb(95 200 255 / 18%) 0, rgb(95 200 255 / 0%) 30%),
linear-gradient(145deg, var(--landing-midnight) 0%, #073b9e 52%, var(--landing-service-blue) 100%);
}
.landing-page__hero-copy {
position: relative;
z-index: 2;
max-width: 640px;
animation: landing-rise 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.landing-page__hero-title {
display: block;
color: var(--landing-surface);
font-size: 54px;
font-weight: 700;
line-height: 1.18;
letter-spacing: -1px;
text-wrap: balance;
}
.landing-page__hero-description {
display: block;
max-width: 620px;
margin-top: 18px;
color: rgb(255 255 255 / 88%);
font-size: 28px;
line-height: 1.65;
text-wrap: pretty;
}
.landing-page__hero-visual {
position: relative;
height: 340px;
margin: 20px -32px 0;
overflow: hidden;
animation: landing-reveal 760ms 100ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.landing-page__hero-image {
display: block;
width: 100%;
height: auto;
margin-top: -68px;
}
.landing-page__content {
padding: 40px 28px 72px;
box-sizing: border-box;
background: var(--landing-canvas);
}
.landing-page__section-title {
display: block;
color: var(--landing-ink);
font-size: 34px;
font-weight: 700;
line-height: 1.4;
}
.landing-page__section-note {
display: block;
margin-top: 8px;
color: var(--landing-muted);
font-size: 28px;
line-height: 1.55;
}
.landing-page__industries {
display: flex;
margin-top: 32px;
overflow: hidden;
border: 1px solid var(--landing-border);
border-radius: 28px;
background: var(--landing-surface);
}
.landing-page__industry {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
align-items: center;
padding: 28px 8px 24px;
box-sizing: border-box;
}
.landing-page__industry + .landing-page__industry {
border-left: 1px solid var(--landing-border);
}
.landing-page__industry-mark {
display: flex;
width: 88px;
height: 88px;
align-items: center;
justify-content: center;
border: 1px solid rgb(22 119 255 / 18%);
border-radius: 50%;
color: var(--landing-service-blue);
font-size: 34px;
font-weight: 700;
line-height: 1;
background: rgb(22 119 255 / 8%);
}
.landing-page__industry-name {
margin-top: 14px;
color: var(--landing-ink);
font-size: 28px;
font-weight: 600;
line-height: 1.4;
}
.landing-page__section--services {
margin-top: 56px;
}
.landing-page__services {
margin-top: 28px;
overflow: hidden;
border: 1px solid var(--landing-border);
border-radius: 24px;
background: var(--landing-surface);
}
.landing-page__service {
display: flex;
gap: 24px;
align-items: center;
padding: 30px 26px;
border-bottom: 1px solid var(--landing-border);
box-sizing: border-box;
}
.landing-page__service:last-child {
border-bottom: 0;
}
.landing-page__service-mark {
display: flex;
width: 72px;
height: 72px;
flex: 0 0 72px;
align-items: center;
justify-content: center;
border-radius: 50%;
color: var(--landing-surface);
font-size: 26px;
font-weight: 700;
line-height: 1;
background: var(--landing-service-blue);
}
.landing-page__service-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.landing-page__service-name {
color: var(--landing-ink);
font-size: 32px;
font-weight: 700;
line-height: 1.4;
}
.landing-page__service-description {
margin-top: 8px;
color: var(--landing-muted);
font-size: 28px;
line-height: 1.55;
}
.landing-page__consultation {
position: fixed;
z-index: 20;
right: 0;
bottom: 0;
left: 0;
display: flex;
gap: 20px;
align-items: center;
padding: 18px 24px;
padding-bottom: calc(18px + constant(safe-area-inset-bottom));
padding-bottom: calc(18px + env(safe-area-inset-bottom));
border-top: 1px solid var(--landing-border);
box-sizing: border-box;
background: var(--landing-surface);
box-shadow: 0 -12px 36px rgb(16 42 86 / 14%);
}
.landing-page__consultation-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.landing-page__consultation-label {
color: var(--landing-muted);
font-size: 28px;
font-weight: 600;
line-height: 1.35;
}
.landing-page__consultation-phone {
margin-top: 2px;
color: var(--landing-ink);
font-size: 30px;
font-weight: 700;
line-height: 1.35;
letter-spacing: 0.5px;
}
.landing-page__call-button {
min-width: 200px;
height: 88px;
margin: 0;
padding: 0 32px;
border: 0;
border-radius: 999px;
color: var(--landing-surface);
font-size: 28px;
font-weight: 700;
line-height: 88px;
background: var(--landing-action-blue);
box-shadow: none;
}
.landing-page__call-button::after {
border: 0;
}
.landing-page__call-button--pressed {
background: #0849b4;
}
@keyframes landing-rise {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes landing-reveal {
from {
opacity: 0.7;
clip-path: inset(28% 0 0 0);
transform: translateY(18px);
}
to {
opacity: 1;
clip-path: inset(0 0 0 0);
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.landing-page__hero-copy,
.landing-page__hero-visual {
animation: none;
}
}

138
src/pages/landing/index.tsx Normal file
View File

@ -0,0 +1,138 @@
import { Button, Image, Text, View } from '@tarojs/components'
import Taro from '@tarojs/taro'
import heroImage from '@/assets/images/landing-storefronts.jpg'
import './index.scss'
const CONTACT_PHONE = '4001166311'
const INDUSTRIES = [
{ mark: '百', name: '百货' },
{ mark: '超', name: '超市' },
{ mark: '便', name: '便利店' },
]
const SERVICES = [
{
mark: '关',
name: '公关',
description: '协助门店沟通与活动协调',
},
{
mark: '推',
name: '推广',
description: '支持门店推广与现场执行',
},
{
mark: '调',
name: '市场调查',
description: '开展门店走访、信息采集与反馈',
},
]
const getErrorMessage = (error: unknown) => {
if (error instanceof Error) {
return error.message
}
if (typeof error === 'object' && error && 'errMsg' in error) {
return String((error as { errMsg?: string }).errMsg)
}
return ''
}
export default function Landing () {
const handleCall = async () => {
try {
await Taro.makePhoneCall({ phoneNumber: CONTACT_PHONE })
} catch (error) {
const errorMessage = getErrorMessage(error)
if (errorMessage.toLowerCase().includes('cancel')) {
return
}
console.error('Failed to make phone call.', error)
Taro.showToast({
title: '暂时无法拨打,请稍后重试',
icon: 'none'
})
}
}
return (
<View className='landing-page'>
<View className='landing-page__hero'>
<View className='landing-page__hero-copy'>
<Text className='landing-page__hero-title'></Text>
<Text className='landing-page__hero-description'>
便广
</Text>
</View>
<View className='landing-page__hero-visual'>
<Image
className='landing-page__hero-image'
src={heroImage}
mode='widthFix'
ariaLabel='百货、超市和便利店门店剪影'
/>
</View>
</View>
<View className='landing-page__content'>
<View className='landing-page__section'>
<Text className='landing-page__section-title'></Text>
<Text className='landing-page__section-note'>线</Text>
<View className='landing-page__industries'>
{INDUSTRIES.map((industry) => (
<View className='landing-page__industry' key={industry.name}>
<View className='landing-page__industry-mark'>
<Text>{industry.mark}</Text>
</View>
<Text className='landing-page__industry-name'>{industry.name}</Text>
</View>
))}
</View>
</View>
<View className='landing-page__section landing-page__section--services'>
<Text className='landing-page__section-title'></Text>
<Text className='landing-page__section-note'></Text>
<View className='landing-page__services'>
{SERVICES.map((service) => (
<View className='landing-page__service' key={service.name}>
<View className='landing-page__service-mark'>
<Text>{service.mark}</Text>
</View>
<View className='landing-page__service-copy'>
<Text className='landing-page__service-name'>{service.name}</Text>
<Text className='landing-page__service-description'>
{service.description}
</Text>
</View>
</View>
))}
</View>
</View>
</View>
<View className='landing-page__consultation'>
<View className='landing-page__consultation-copy'>
<Text className='landing-page__consultation-label'></Text>
<Text className='landing-page__consultation-phone'>{CONTACT_PHONE}</Text>
</View>
<Button
className='landing-page__call-button'
hoverClass='landing-page__call-button--pressed'
ariaLabel={`拨打业务咨询电话 ${CONTACT_PHONE}`}
onClick={handleCall}
>
</Button>
</View>
</View>
)
}