diff --git a/.impeccable/live/config.json b/.impeccable/live/config.json new file mode 100644 index 0000000..34a89c9 --- /dev/null +++ b/.impeccable/live/config.json @@ -0,0 +1,8 @@ +{ + "files": [ + "src/index.html" + ], + "insertBefore": "", + "commentSyntax": "html", + "cspChecked": true +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5859b41 --- /dev/null +++ b/AGENTS.md @@ -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:` / `pnpm build:` (`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 `` pointing at the business H5 (`http://niubsw.com/...?mpCode=`). 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=&formInfo=' })`. + - 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(raw)` for any future base64 route params. + +## Conventions + +- **Page = folder** under `src/pages//` 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`. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..afef593 --- /dev/null +++ b/DESIGN.md @@ -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**(700,56px,1.18):仅用于首屏“门店管理服务”。 +- **Title**(700,32px,1.4):用于“服务行业”“核心服务”和服务名称。 +- **Body**(400,28px,1.65):用于业务说明,单行长度受移动端容器约束。 +- **Label**(600,24px,1.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** 使用渐变文字、侧边粗色条、嵌套卡片或重复的图标卡片网格。 diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..b9c59b0 --- /dev/null +++ b/PRODUCT.md @@ -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,不仅依赖颜色传达信息,并为减少动态效果提供静态降级。 diff --git a/src/app.config.ts b/src/app.config.ts index 47c1273..294ede1 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,6 +1,7 @@ export default defineAppConfig({ pages: [ 'pages/index/index', + 'pages/landing/index', 'pages/pay/index', ], window: { diff --git a/src/assets/images/landing-storefronts.jpg b/src/assets/images/landing-storefronts.jpg new file mode 100644 index 0000000..33c7129 Binary files /dev/null and b/src/assets/images/landing-storefronts.jpg differ diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index e5cdaea..ad09e1e 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -5,7 +5,8 @@ import './index.scss' // xunmeng2,devcon const LINK_PREFIX = '/p/' -const DEFAULT_LINK_CODE = 'Gbym' +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)}`) diff --git a/src/pages/landing/index.config.ts b/src/pages/landing/index.config.ts new file mode 100644 index 0000000..64fd653 --- /dev/null +++ b/src/pages/landing/index.config.ts @@ -0,0 +1,7 @@ +export default definePageConfig({ + navigationBarTitleText: '门店管理服务', + navigationBarBackgroundColor: '#05256e', + navigationBarTextStyle: 'white', + backgroundColor: '#f3f7ff', + backgroundTextStyle: 'light' +}) diff --git a/src/pages/landing/index.scss b/src/pages/landing/index.scss new file mode 100644 index 0000000..582e834 --- /dev/null +++ b/src/pages/landing/index.scss @@ -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; + } +} diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx new file mode 100644 index 0000000..c7208d6 --- /dev/null +++ b/src/pages/landing/index.tsx @@ -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 ( + + + + 门店管理服务 + + 面向百货、超市与便利店,提供公关、推广与市场调查服务。 + + + + + + + + + + + 服务行业 + 覆盖常见线下零售业态 + + + {INDUSTRIES.map((industry) => ( + + + {industry.mark} + + {industry.name} + + ))} + + + + + 核心服务 + 围绕门店现场提供业务支持 + + + {SERVICES.map((service) => ( + + + {service.mark} + + + {service.name} + + {service.description} + + + + ))} + + + + + + + 业务咨询 + {CONTACT_PHONE} + + + + + ) +}