This commit is contained in:
zhangjianjun 2026-07-20 15:44:59 +08:00
parent 3214f66fb9
commit 96869761e2
4 changed files with 376 additions and 363 deletions

33
design-qa.md Normal file
View File

@ -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

View File

@ -1,7 +1,7 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '门店企业标注快办', navigationBarTitleText: '门店企业标注快办',
navigationBarBackgroundColor: '#05256e', navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'white', navigationBarTextStyle: 'black',
backgroundColor: '#f3f7ff', backgroundColor: '#f2f7fb',
backgroundTextStyle: 'light' backgroundTextStyle: 'dark'
}) })

View File

@ -1,19 +1,18 @@
/* stylelint-disable selector-class-pattern -- The project requires BEM class names. */ /* stylelint-disable selector-class-pattern -- The project requires BEM class names. */
.landing-page { .landing-page {
--landing-midnight: #05256e; --landing-green: #07c160;
--landing-service-blue: #0b57d0; --landing-green-dark: #06ae56;
--landing-action-blue: #1677ff; --landing-canvas: #f2f7fb;
--landing-signal-cyan: #5fc8ff;
--landing-canvas: #f3f7ff;
--landing-surface: #fff; --landing-surface: #fff;
--landing-ink: #102a56; --landing-ink: #17181a;
--landing-muted: #425f8c; --landing-muted: #606772;
--landing-border: #d7e4ff; --landing-placeholder: #959ba3;
--landing-border: #c7ccd2;
--landing-error: #d94242;
min-height: 100vh; min-height: 100vh;
padding-bottom: calc(132px + constant(safe-area-inset-bottom)); padding: 54px 30px calc(38px + env(safe-area-inset-bottom));
padding-bottom: calc(132px + env(safe-area-inset-bottom));
overflow-x: hidden; overflow-x: hidden;
box-sizing: border-box; box-sizing: border-box;
color: var(--landing-ink); color: var(--landing-ink);
@ -21,295 +20,195 @@
background: var(--landing-canvas); background: var(--landing-canvas);
} }
@supports (color: oklch(0 0 0)) { .landing-page__intro {
.landing-page { padding: 0 10px;
--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 { .landing-page__title {
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; display: block;
color: var(--landing-surface); font-size: 48px;
font-size: 54px;
font-weight: 700; font-weight: 700;
line-height: 1.18; line-height: 1.35;
letter-spacing: -1px; letter-spacing: -1px;
text-wrap: balance;
} }
.landing-page__hero-description { .landing-page__description {
display: block; display: block;
max-width: 620px;
margin-top: 18px; margin-top: 18px;
color: rgb(255 255 255 / 88%); color: var(--landing-muted);
font-size: 28px; font-size: 28px;
line-height: 1.65; line-height: 1.65;
text-wrap: pretty;
} }
.landing-page__hero-visual { .landing-page__notice {
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; 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; 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-size: 28px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
} }
.landing-page__section--services { .landing-page__service-types {
margin-top: 56px; display: flex;
gap: 12px;
} }
.landing-page__services { .landing-page__service-type {
margin-top: 28px; display: flex;
overflow: hidden; height: 72px;
flex: 1;
align-items: center;
justify-content: center;
border: 1px solid var(--landing-border); border: 1px solid var(--landing-border);
border-radius: 24px; border-radius: 10px;
box-sizing: border-box;
font-size: 28px;
background: var(--landing-surface); 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; display: flex;
gap: 24px;
align-items: center; align-items: center;
padding: 30px 26px; gap: 12px;
border-bottom: 1px solid var(--landing-border); }
.landing-page__input,
.landing-page__textarea {
width: 100%;
border: 1px solid var(--landing-border);
border-radius: 10px;
box-sizing: border-box; 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); 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-size: 32px;
font-weight: 700; font-weight: 700;
line-height: 1.4; line-height: 84px;
} background: var(--landing-green);
.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; box-shadow: none;
} }
.landing-page__call-button::after { .landing-page__submit::after {
border: 0; border: 0;
} }
.landing-page__call-button--pressed { .landing-page__submit--pressed {
background: #0849b4; background: var(--landing-green-dark);
transform: scale(0.99);
} }
@keyframes landing-rise { .landing-page__footer {
from { display: flex;
opacity: 0; align-items: center;
transform: translateY(20px); margin-top: 28px;
} flex-direction: column;
color: var(--landing-muted);
to { font-size: 22px;
opacity: 1; line-height: 1.75;
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) { @media (prefers-reduced-motion: reduce) {
.landing-page__hero-copy, .landing-page__input,
.landing-page__hero-visual { .landing-page__textarea,
animation: none; .landing-page__submit {
transition: none;
} }
} }

View File

@ -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 Taro from '@tarojs/taro'
import heroImage from '@/assets/images/landing-storefronts.jpg' import { useState } from 'react'
import './index.scss' import './index.scss'
const CONTACT_PHONE = '4001166311' type ServiceType = 'new' | 'update'
const INDUSTRIES = [ type ApplicationForm = {
{ mark: '百', name: '百货' }, serviceType: ServiceType
{ mark: '超', name: '超市' }, storeName: string
{ mark: '便', name: '便利店' }, storeAddress: string
] contactName: string
contactPhone: string
description: string
}
const SERVICES = [ type FormErrors = Partial<Record<keyof ApplicationForm | 'agreement', string>>
{
mark: '推',
name: '推广',
description: '为门店推广提供支持',
},
{
mark: '曝',
name: '门店曝光',
description: '提高门店曝光量',
},
]
const getErrorMessage = (error: unknown) => { const INITIAL_FORM: ApplicationForm = {
if (error instanceof Error) { serviceType: 'new',
return error.message 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) { if (!form.description.trim()) errors.description = '请描述需要办理的内容'
return String((error as { errMsg?: string }).errMsg) if (!agreed) errors.agreement = '请阅读并同意隐私政策和服务说明'
}
return '' return errors
} }
export default function Landing () { export default function Landing () {
const handleCall = async () => { const [form, setForm] = useState<ApplicationForm>(INITIAL_FORM)
try { const [agreed, setAgreed] = useState(false)
await Taro.makePhoneCall({ phoneNumber: CONTACT_PHONE }) const [errors, setErrors] = useState<FormErrors>({})
} catch (error) {
const errorMessage = getErrorMessage(error)
if (errorMessage.toLowerCase().includes('cancel')) { const updateField = <K extends keyof ApplicationForm>(field: K, value: ApplicationForm[K]) => {
return setForm((current) => ({ ...current, [field]: value }))
} setErrors((current) => ({ ...current, [field]: undefined }))
}
console.error('Failed to make phone call.', error) const showPolicy = (type: 'privacy' | 'service') => {
Taro.showToast({ const isPrivacy = type === 'privacy'
title: '暂时无法拨打,请稍后重试',
icon: 'none' 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 ( return (
<View className='landing-page'> <View className='landing-page'>
<View className='landing-page__hero'> <View className='landing-page__intro'>
<View className='landing-page__hero-copy'> <Text className='landing-page__title'>线</Text>
<Text className='landing-page__hero-title'></Text> <Text className='landing-page__description'>
<Text className='landing-page__hero-description'>
便 </Text>
</Text> <Text className='landing-page__notice'></Text>
</View>
<View className='landing-page__form'>
<View className='landing-page__field'>
<Text className='landing-page__label'></Text>
<RadioGroup
className='landing-page__service-types'
onChange={(event) => updateField('serviceType', event.detail.value as ServiceType)}
>
<View className={`landing-page__service-type ${form.serviceType === 'new' ? 'landing-page__service-type--selected' : ''}`}>
<Radio className='landing-page__radio' value='new' checked={form.serviceType === 'new'} color='#07c160'></Radio>
</View>
<View className={`landing-page__service-type ${form.serviceType === 'update' ? 'landing-page__service-type--selected' : ''}`}>
<Radio className='landing-page__radio' value='update' checked={form.serviceType === 'update'} color='#07c160'></Radio>
</View>
</RadioGroup>
</View> </View>
<View className='landing-page__hero-visual'> <View className='landing-page__field'>
<Image <Text className='landing-page__label'></Text>
className='landing-page__hero-image' <Input
src={heroImage} className={`landing-page__input ${errors.storeName ? 'landing-page__input--error' : ''}`}
mode='widthFix' value={form.storeName}
ariaLabel='百货、超市和便利店门店剪影' maxlength={50}
placeholder='请输入营业执照上的门店名称'
placeholderClass='landing-page__placeholder'
onInput={(event) => updateField('storeName', event.detail.value)}
/> />
{errors.storeName && <Text className='landing-page__error'>{errors.storeName}</Text>}
</View>
<View className='landing-page__field'>
<Text className='landing-page__label'></Text>
<Input
className={`landing-page__input ${errors.storeAddress ? 'landing-page__input--error' : ''}`}
value={form.storeAddress}
maxlength={100}
placeholder='请输入完整经营地址'
placeholderClass='landing-page__placeholder'
onInput={(event) => updateField('storeAddress', event.detail.value)}
/>
{errors.storeAddress && <Text className='landing-page__error'>{errors.storeAddress}</Text>}
</View>
<View className='landing-page__field'>
<Text className='landing-page__label'></Text>
<Input
className={`landing-page__input ${errors.contactName ? 'landing-page__input--error' : ''}`}
value={form.contactName}
maxlength={30}
placeholder='请输入联系人姓名'
placeholderClass='landing-page__placeholder'
onInput={(event) => updateField('contactName', event.detail.value)}
/>
{errors.contactName && <Text className='landing-page__error'>{errors.contactName}</Text>}
</View>
<View className='landing-page__field'>
<Text className='landing-page__label'></Text>
<Input
className={`landing-page__input ${errors.contactPhone ? 'landing-page__input--error' : ''}`}
value={form.contactPhone}
type='number'
maxlength={11}
placeholder='请输入手机号码'
placeholderClass='landing-page__placeholder'
onInput={(event) => updateField('contactPhone', event.detail.value)}
/>
{errors.contactPhone && <Text className='landing-page__error'>{errors.contactPhone}</Text>}
</View>
<View className='landing-page__field'>
<Text className='landing-page__label'></Text>
<Textarea
className={`landing-page__textarea ${errors.description ? 'landing-page__input--error' : ''}`}
value={form.description}
maxlength={300}
placeholder='请描述需要办理的内容'
placeholderClass='landing-page__placeholder'
onInput={(event) => updateField('description', event.detail.value)}
/>
{errors.description && <Text className='landing-page__error'>{errors.description}</Text>}
</View> </View>
</View> </View>
<View className='landing-page__content'> <CheckboxGroup
<View className='landing-page__section'> className='landing-page__agreement'
<Text className='landing-page__section-title'></Text> onChange={(event) => {
<Text className='landing-page__section-note'>线</Text> setAgreed(event.detail.value.includes('agreed'))
setErrors((current) => ({ ...current, agreement: undefined }))
}}
>
<Checkbox className='landing-page__checkbox' value='agreed' checked={agreed} color='#07c160' />
<Text className='landing-page__agreement-text'></Text>
<Text className='landing-page__link' onClick={() => showPolicy('privacy')}></Text>
<Text className='landing-page__agreement-text'></Text>
<Text className='landing-page__link' onClick={() => showPolicy('service')}></Text>
</CheckboxGroup>
{errors.agreement && <Text className='landing-page__agreement-error'>{errors.agreement}</Text>}
<View className='landing-page__industries'> <Button
{INDUSTRIES.map((industry) => ( className='landing-page__submit'
<View className='landing-page__industry' key={industry.name}> hoverClass='landing-page__submit--pressed'
<View className='landing-page__industry-mark'> onClick={handleSubmit}
<Text>{industry.mark}</Text> >
</View>
<Text className='landing-page__industry-name'>{industry.name}</Text> </Button>
</View>
))}
</View>
</View>
<View className='landing-page__section landing-page__section--services'> <View className='landing-page__footer'>
<Text className='landing-page__section-title'></Text> <Text> 9:00-18:00</Text>
<Text className='landing-page__section-note'></Text> <Text>400-800-1234</Text>
<Text></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>
</View> </View>
) )