From 96869761e27ded7e171424a217d23bf6544693fd Mon Sep 17 00:00:00 2001 From: zhangjianjun Date: Mon, 20 Jul 2026 15:44:59 +0800 Subject: [PATCH] ud --- design-qa.md | 33 +++ src/pages/landing/index.config.ts | 8 +- src/pages/landing/index.scss | 413 +++++++++++------------------- src/pages/landing/index.tsx | 285 +++++++++++++-------- 4 files changed, 376 insertions(+), 363 deletions(-) create mode 100644 design-qa.md diff --git a/design-qa.md b/design-qa.md new file mode 100644 index 0000000..a71c0ad --- /dev/null +++ b/design-qa.md @@ -0,0 +1,33 @@ +# Design QA + +- source visual truth path: `C:\Users\29169\AppData\Local\Temp\codex-clipboard-5202e314-49da-48b8-a154-05e5d4a96bcc.png` +- implementation screenshot path: unavailable +- viewport: target 750 x 1624 +- state: default form state + +## Full-view comparison evidence + +The source image was opened at its original resolution and used to reproduce the page structure, spacing, typography hierarchy, form controls, green action button, and footer content. A browser-rendered implementation screenshot could not be captured because the repository's existing H5 production build fails in the legacy Babel post-processing stage with an invalid `development` compilation target. + +## Focused region comparison evidence + +Blocked for the same reason. Source details were inspected directly, but a rendered comparison artifact is unavailable. + +## Findings + +- No code-level P0/P1 issue was found by the WeChat production build or stylesheet validation. +- Visual fidelity remains unverified in a browser-rendered 750 x 1624 comparison. + +## Comparison history + +- Initial implementation completed from the supplied reference. +- No visual comparison iteration could be performed because the H5 preview build is blocked by the existing Babel target configuration. + +## Verification + +- Primary interactions implemented: service type selection, all text inputs, agreement selection, policy dialogs, validation feedback, and success dialog. +- WeChat production build: passed. +- Stylesheet validation: passed. +- Browser console check: blocked because no browser-rendered preview was available. + +final result: blocked diff --git a/src/pages/landing/index.config.ts b/src/pages/landing/index.config.ts index cb6951f..0499d4c 100644 --- a/src/pages/landing/index.config.ts +++ b/src/pages/landing/index.config.ts @@ -1,7 +1,7 @@ export default definePageConfig({ navigationBarTitleText: '门店企业标注快办', - navigationBarBackgroundColor: '#05256e', - navigationBarTextStyle: 'white', - backgroundColor: '#f3f7ff', - backgroundTextStyle: 'light' + navigationBarBackgroundColor: '#ffffff', + navigationBarTextStyle: 'black', + backgroundColor: '#f2f7fb', + backgroundTextStyle: 'dark' }) diff --git a/src/pages/landing/index.scss b/src/pages/landing/index.scss index 582e834..9ff113e 100644 --- a/src/pages/landing/index.scss +++ b/src/pages/landing/index.scss @@ -1,19 +1,18 @@ /* 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-green: #07c160; + --landing-green-dark: #06ae56; + --landing-canvas: #f2f7fb; --landing-surface: #fff; - --landing-ink: #102a56; - --landing-muted: #425f8c; - --landing-border: #d7e4ff; + --landing-ink: #17181a; + --landing-muted: #606772; + --landing-placeholder: #959ba3; + --landing-border: #c7ccd2; + --landing-error: #d94242; min-height: 100vh; - padding-bottom: calc(132px + constant(safe-area-inset-bottom)); - padding-bottom: calc(132px + env(safe-area-inset-bottom)); + padding: 54px 30px calc(38px + env(safe-area-inset-bottom)); overflow-x: hidden; box-sizing: border-box; color: var(--landing-ink); @@ -21,295 +20,195 @@ 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__intro { + padding: 0 10px; } -.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 { +.landing-page__title { display: block; - color: var(--landing-surface); - font-size: 54px; + font-size: 48px; font-weight: 700; - line-height: 1.18; + line-height: 1.35; letter-spacing: -1px; - text-wrap: balance; } -.landing-page__hero-description { +.landing-page__description { display: block; - max-width: 620px; margin-top: 18px; - color: rgb(255 255 255 / 88%); + color: var(--landing-muted); 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 { +.landing-page__notice { 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); + color: var(--landing-muted); + font-size: 24px; + line-height: 1.5; +} + +.landing-page__form { + margin-top: 30px; + padding: 28px; + border: 1px solid #d9dde1; + border-radius: 14px; + box-sizing: border-box; + background: var(--landing-surface); + box-shadow: 0 8px 28px rgb(40 65 86 / 5%); +} + +.landing-page__field + .landing-page__field { + margin-top: 26px; +} + +.landing-page__label { + display: block; + margin-bottom: 12px; font-size: 28px; font-weight: 600; line-height: 1.4; } -.landing-page__section--services { - margin-top: 56px; +.landing-page__service-types { + display: flex; + gap: 12px; } -.landing-page__services { - margin-top: 28px; - overflow: hidden; +.landing-page__service-type { + display: flex; + height: 72px; + flex: 1; + align-items: center; + justify-content: center; border: 1px solid var(--landing-border); - border-radius: 24px; + border-radius: 10px; + box-sizing: border-box; + font-size: 28px; background: var(--landing-surface); } -.landing-page__service { +.landing-page__service-type--selected { + border-color: rgb(7 193 96 / 45%); + background: rgb(7 193 96 / 3%); +} + +.landing-page__radio { display: flex; - gap: 24px; align-items: center; - padding: 30px 26px; - border-bottom: 1px solid var(--landing-border); + gap: 12px; +} + +.landing-page__input, +.landing-page__textarea { + width: 100%; + border: 1px solid var(--landing-border); + border-radius: 10px; 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: 28px; + background: var(--landing-surface); + transition: border-color 160ms ease, box-shadow 160ms ease; +} + +.landing-page__input { + height: 72px; + padding: 0 22px; +} + +.landing-page__textarea { + height: 116px; + padding: 18px 22px; + line-height: 1.5; +} + +.landing-page__input:focus, +.landing-page__textarea:focus { + border-color: var(--landing-green); + box-shadow: 0 0 0 3px rgb(7 193 96 / 10%); +} + +.landing-page__input--error { + border-color: var(--landing-error); +} + +.landing-page__placeholder { + color: var(--landing-placeholder); +} + +.landing-page__error, +.landing-page__agreement-error { + display: block; + margin-top: 8px; + color: var(--landing-error); + font-size: 22px; + line-height: 1.4; +} + +.landing-page__agreement { + display: flex; + align-items: center; + margin: 26px 18px 0; + font-size: 24px; + line-height: 1.5; +} + +.landing-page__checkbox { + margin-right: 10px; + transform: scale(0.88); + transform-origin: left center; +} + +.landing-page__agreement-text { + color: var(--landing-ink); +} + +.landing-page__link { + color: var(--landing-green); +} + +.landing-page__agreement-error { + margin-right: 18px; + margin-left: 18px; +} + +.landing-page__submit { + height: 84px; + margin: 24px 10px 0; + padding: 0; + border: 0; + border-radius: 12px; + color: #fff; 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); + line-height: 84px; + background: var(--landing-green); box-shadow: none; } -.landing-page__call-button::after { +.landing-page__submit::after { border: 0; } -.landing-page__call-button--pressed { - background: #0849b4; +.landing-page__submit--pressed { + background: var(--landing-green-dark); + transform: scale(0.99); } -@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); - } +.landing-page__footer { + display: flex; + align-items: center; + margin-top: 28px; + flex-direction: column; + color: var(--landing-muted); + font-size: 22px; + line-height: 1.75; } @media (prefers-reduced-motion: reduce) { - .landing-page__hero-copy, - .landing-page__hero-visual { - animation: none; + .landing-page__input, + .landing-page__textarea, + .landing-page__submit { + transition: none; } } diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index 7ecbd2a..07a41cd 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -1,132 +1,213 @@ -import { Button, Image, Text, View } from '@tarojs/components' +import { Button, Checkbox, CheckboxGroup, Input, Radio, RadioGroup, Text, Textarea, View } from '@tarojs/components' import Taro from '@tarojs/taro' -import heroImage from '@/assets/images/landing-storefronts.jpg' +import { useState } from 'react' import './index.scss' -const CONTACT_PHONE = '4001166311' +type ServiceType = 'new' | 'update' -const INDUSTRIES = [ - { mark: '百', name: '百货' }, - { mark: '超', name: '超市' }, - { mark: '便', name: '便利店' }, -] +type ApplicationForm = { + serviceType: ServiceType + storeName: string + storeAddress: string + contactName: string + contactPhone: string + description: string +} -const SERVICES = [ - { - mark: '推', - name: '推广', - description: '为门店推广提供支持', - }, - { - mark: '曝', - name: '门店曝光', - description: '提高门店曝光量', - }, -] +type FormErrors = Partial> -const getErrorMessage = (error: unknown) => { - if (error instanceof Error) { - return error.message +const INITIAL_FORM: ApplicationForm = { + serviceType: 'new', + storeName: '', + storeAddress: '', + contactName: '', + contactPhone: '', + description: '', +} + +const PHONE_PATTERN = /^1[3-9]\d{9}$/ + +const validateForm = (form: ApplicationForm, agreed: boolean) => { + const errors: FormErrors = {} + + if (!form.storeName.trim()) errors.storeName = '请输入门店名称' + if (!form.storeAddress.trim()) errors.storeAddress = '请输入门店地址' + if (!form.contactName.trim()) errors.contactName = '请输入联系人姓名' + + if (!form.contactPhone.trim()) { + errors.contactPhone = '请输入联系电话' + } else if (!PHONE_PATTERN.test(form.contactPhone.trim())) { + errors.contactPhone = '请输入正确的手机号码' } - if (typeof error === 'object' && error && 'errMsg' in error) { - return String((error as { errMsg?: string }).errMsg) - } + if (!form.description.trim()) errors.description = '请描述需要办理的内容' + if (!agreed) errors.agreement = '请阅读并同意隐私政策和服务说明' - return '' + return errors } export default function Landing () { - const handleCall = async () => { - try { - await Taro.makePhoneCall({ phoneNumber: CONTACT_PHONE }) - } catch (error) { - const errorMessage = getErrorMessage(error) + const [form, setForm] = useState(INITIAL_FORM) + const [agreed, setAgreed] = useState(false) + const [errors, setErrors] = useState({}) - if (errorMessage.toLowerCase().includes('cancel')) { - return - } + const updateField = (field: K, value: ApplicationForm[K]) => { + setForm((current) => ({ ...current, [field]: value })) + setErrors((current) => ({ ...current, [field]: undefined })) + } - console.error('Failed to make phone call.', error) - Taro.showToast({ - title: '暂时无法拨打,请稍后重试', - icon: 'none' - }) + const showPolicy = (type: 'privacy' | 'service') => { + const isPrivacy = type === 'privacy' + + Taro.showModal({ + title: isPrivacy ? '隐私政策' : '服务说明', + content: isPrivacy + ? '您填写的信息仅用于本次门店标注服务的联系与确认,我们将妥善保护您的个人信息。' + : '提交申请后,工作人员将在服务时间内联系您核对资料。具体办理结果以实际审核情况为准。', + showCancel: false, + confirmText: '我知道了', + }) + } + + const handleSubmit = () => { + const validationErrors = validateForm(form, agreed) + const firstError = Object.values(validationErrors)[0] + + setErrors(validationErrors) + + if (firstError) { + Taro.showToast({ title: firstError, icon: 'none' }) + return } + + Taro.showModal({ + title: '提交成功', + content: '您的申请已提交,工作人员将在服务时间内联系您确认。', + showCancel: false, + confirmText: '我知道了', + }) } return ( - - - 门店企业标注快办 - - 面向百货、超市与便利店,提供门店企业标注快办服务。 - + + 门店企业标注,在线提交 + + 提供门店新增标注、信息修改等企业服务,提交资料后由工作人员联系确认。 + + 第三方企业服务,非地图平台官方服务 + + + + + 办理类型 + updateField('serviceType', event.detail.value as ServiceType)} + > + + 新增标注 + + + 信息修改 + + - - + 门店名称 + updateField('storeName', event.detail.value)} /> + {errors.storeName && {errors.storeName}} + + + + 门店地址 + updateField('storeAddress', event.detail.value)} + /> + {errors.storeAddress && {errors.storeAddress}} + + + + 联系人 + updateField('contactName', event.detail.value)} + /> + {errors.contactName && {errors.contactName}} + + + + 联系电话 + updateField('contactPhone', event.detail.value)} + /> + {errors.contactPhone && {errors.contactPhone}} + + + + 补充说明 +