feat:商品预订的调整

This commit is contained in:
2026-07-31 11:22:53 +08:00
parent ec1ff6ded0
commit eecc79f31a
4 changed files with 231 additions and 85 deletions

View File

@@ -2,7 +2,7 @@
<view class="flex h-screen min-w-0 flex-col overflow-hidden bg-slate-100"> <view class="flex h-screen min-w-0 flex-col overflow-hidden bg-slate-100">
<TopNavBar <TopNavBar
:title="paid ? '支付结果' : '预订选择'" :title="paid ? '支付结果' : '预订选择'"
:subtitle="paid ? '订单已支付,可查看凭证' : '确认日期、出行人与数量'" :subtitle="paid ? '订单已支付,可查看凭证' : '可选日期'"
title-align="left" title-align="left"
background="#ffffff" background="#ffffff"
/> />

View File

@@ -1,116 +1,171 @@
<template> <template>
<view class="flex h-full min-w-0 flex-col overflow-hidden bg-slate-100 text-slate-900"> <view class="relative flex h-full min-w-0 flex-col overflow-hidden bg-slate-100 text-slate-900">
<scroll-view scroll-y class="min-h-0 flex-1"> <scroll-view scroll-y class="relative z-0 min-h-0 flex-1">
<view class="box-border w-full min-w-0 space-y-3 px-3 py-3 pb-36"> <view class="box-border w-full min-w-0 pb-32">
<view class="flex rounded-2xl bg-white p-3 shadow-sm"> <view class="box-border w-full min-w-0 p-3">
<image class="h-20 w-20 shrink-0 rounded-xl bg-slate-200" :src="product.images?.[0]" mode="aspectFill" /> <view class="flex rounded-2xl bg-white p-3 shadow-sm">
<view class="ml-3 flex min-w-0 flex-1 flex-col justify-between"> <image class="h-20 w-20 shrink-0 rounded-xl bg-slate-200" :src="product.images?.[0]" mode="aspectFill" />
<view> <view class="ml-3 flex min-w-0 flex-1 flex-col justify-between">
<text class="block truncate text-base font-semibold">{{ product.name }}</text> <view>
<view class="mt-2 flex flex-wrap gap-1"> <text class="block truncate text-base font-semibold">{{ product.name }}</text>
<text v-for="highlight in product.highlights || []" :key="highlight" class="rounded bg-orange-50 px-1.5 py-0.5 text-xs text-orange-600">{{ highlight }}</text> <view class="mt-[5px] flex flex-wrap gap-[4px]">
<text v-if="product.ageRule" class="rounded-[4px] bg-[#f2ebff] px-[5px] py-[2px] text-[10px] text-[#7a52b3]">{{ ageRuleText }}</text>
<text v-for="highlight in product.highlights || []" :key="highlight" class="rounded-[4px] bg-[#e8f2ff] px-[5px] py-[2px] text-[10px] text-[#3474c7]">{{ highlight }}</text>
</view>
</view> </view>
<text class="text-sm font-semibold text-rose-500">¥{{ product.salePrice }}</text>
</view> </view>
<text class="text-sm font-semibold text-rose-500">¥{{ product.salePrice }} </text>
</view> </view>
</view> </view>
<view class="rounded-2xl bg-white px-4 py-4"> <view class="border-b border-orange-100 bg-orange-50 p-4 text-xs leading-5 text-orange-600">
<text class="text-base font-semibold">使用日期</text> <text class="font-semibold text-blue-600">{{ bookingHighlights }}</text>
<text class="block">请先选择使用日期和场次再填写游客信息</text>
</view>
<view v-if="product.ageRule" class="border-b border-blue-100 bg-blue-50 p-4">
<view class="flex min-w-0 items-start gap-3">
<uni-icons class="mt-0.5 shrink-0" type="info" size="18" color="#2563eb" />
<view class="min-w-0 flex-1">
<text class="block text-sm font-bold text-blue-900">年龄限制{{ ageRuleText }}</text>
<text class="mt-1 block text-xs leading-5 text-blue-700">提交订单时仅校验门票游客观光车和园内项目不校验年龄</text>
</view>
</view>
</view>
<view v-if="product.reservation?.requiresDate !== false" class="bg-white p-4">
<text class="font-bold">使用日期</text>
<scroll-view scroll-x class="mt-3 whitespace-nowrap"> <scroll-view scroll-x class="mt-3 whitespace-nowrap">
<view class="flex gap-2"> <view class="flex gap-2 pb-1">
<button <button
v-for="item in availableDates" v-for="(item, index) in availableDates"
:key="item.date || item.value" :key="item.date || item.value"
class="m-0 flex min-w-20 flex-col items-center rounded-xl border px-3 py-2 text-left" class="booking-button relative m-0 min-w-[90px] rounded-xl border px-3 pb-3 pt-6 text-center"
:class="dateClass(item)" :class="dateClass(item)"
:disabled="isSoldOut(item)" :disabled="isSoldOut(item)"
@tap="selectDate(item)" @tap="selectDate(item)"
> >
<text class="text-xs">{{ item.week || '可预约' }}</text> <view v-if="index < 2" class="absolute left-0 top-0 flex h-5 items-center justify-center rounded-br-lg rounded-tl-xl bg-orange-500 px-2">
<text class="mt-1 text-sm font-medium">{{ item.label || item.date || item.value }}</text> <text class="text-[10px] leading-none text-white">{{ index === 0 ? '明天' : '后天' }}</text>
<text v-if="isSoldOut(item)" class="mt-1 text-xs">已售罄</text> </view>
<text v-else class="mt-1 text-xs">{{ item.stock }}</text> <text class="block text-xs">{{ item.week || '可预约' }}</text>
<text class="mt-1 block text-sm font-semibold">{{ item.label || item.date || item.value }}</text>
<text class="mt-1 block text-sm text-orange-600">¥{{ datePrice(item) }}</text>
<text v-if="isSoldOut(item)" class="mt-1 block text-[10px] text-slate-400">已售罄</text>
<text v-else class="mt-1 block text-[10px] text-slate-400"> {{ item.stock }}</text>
</button> </button>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<view v-if="product.reservation?.requiresTimeSlot" class="rounded-2xl bg-white px-4 py-4"> <view v-if="product.reservation?.requiresTimeSlot" class="mt-2 bg-white p-4">
<text class="text-base font-semibold">使用场次</text> <text class="font-bold">{{ slotTitle }}</text>
<text v-if="!draft.visitDate" class="mt-2 block text-xs text-orange-600">请先选择使用日期再查看各场次剩余库存</text>
<view class="mt-3 grid grid-cols-2 gap-2"> <view class="mt-3 grid grid-cols-2 gap-2">
<button <button
v-for="slot in product.reservation.timeSlots || []" v-for="slot in product.reservation.timeSlots || []"
:key="slot" :key="slot"
class="m-0 rounded-xl border px-2 py-2 text-sm" class="booking-button m-0 rounded-xl border px-2 py-3 text-sm font-semibold"
:class="draft.timeSlot === slot ? 'border-emerald-500 bg-emerald-50 text-emerald-700' : 'border-slate-200 bg-white text-slate-700'" :class="slotClass(slot)"
:disabled="!draft.visitDate"
@tap="patchDraft({ timeSlot: slot })" @tap="patchDraft({ timeSlot: slot })"
>{{ slot }}</button> >
</view> <text class="block">{{ slot }}{{ isScenicAdmission ? '入园' : '' }}</text>
</view> <text class="mt-1 block text-[10px] font-normal" :class="draft.visitDate ? 'text-slate-400' : 'text-slate-300'">{{ draft.visitDate ? '库存充足' : '待选日期' }}</text>
<view class="flex items-center justify-between rounded-2xl bg-white px-4 py-4">
<view>
<text class="block text-base font-semibold">购买数量</text>
<text class="mt-1 block text-xs text-slate-500">每单最多{{ maxQuantity }}</text>
</view>
<view class="flex items-center overflow-hidden rounded-lg border border-slate-200">
<button class="m-0 h-8 w-8 rounded-none bg-slate-50 p-0 text-lg text-slate-600" :disabled="quantity <= 1" @tap="changeQuantity(-1)"></button>
<text class="flex h-8 w-9 items-center justify-center text-center text-sm font-semibold">{{ quantity }}</text>
<button class="m-0 h-8 w-8 rounded-none bg-slate-50 p-0 text-lg text-slate-600" :disabled="quantity >= maxQuantity" @tap="changeQuantity(1)"></button>
</view>
</view>
<view v-if="product.reservation?.realNameRequired" class="rounded-2xl bg-white px-4 py-4">
<view class="flex items-center justify-between">
<view>
<text class="block text-base font-semibold">出行人</text>
<text class="mt-1 block text-xs text-slate-500">实名制门票需填写出行人信息</text>
</view>
<text class="text-sm text-emerald-600">已选{{ selectedTravelers.length }}/{{ quantity }}</text>
</view>
<view v-if="selectedTravelers.length" class="mt-3 space-y-2">
<button v-for="traveler in selectedTravelers" :key="traveler.id" class="m-0 flex w-full items-center justify-between rounded-xl bg-slate-50 px-3 py-2 text-left" @tap="toggleTraveler(traveler.id)">
<view><text class="text-sm font-medium">{{ traveler.name }}</text><text class="ml-2 text-xs text-slate-500">{{ traveler.idType }}</text></view>
<text class="text-xs text-emerald-600">已选择</text>
</button> </button>
</view> </view>
<button class="mt-3 flex w-full items-center justify-center rounded-xl border border-dashed border-emerald-300 bg-emerald-50 px-3 py-2 text-sm text-emerald-700" @tap="emit('open-travelers')">管理常用出行人</button>
</view> </view>
<view v-if="product.reservation?.contactPhoneRequired" class="rounded-2xl bg-white px-4 py-4"> <view class="mt-2 bg-white p-4">
<text class="text-base font-semibold">联系人</text> <view class="flex items-center justify-between">
<view class="mt-3 flex items-center border-b border-slate-100 py-2"> <view>
<text class="w-20 text-sm text-slate-700">姓名</text> <text class="block font-bold">购买数量</text>
<input class="min-w-0 flex-1 text-right text-sm" :value="draft.contactName || ''" placeholder="请输入联系人姓名" @input="updateContactName" /> <text class="mt-1 block text-xs text-slate-400">最多购买 {{ maxQuantity }} </text>
</view> </view>
<view class="flex items-center py-2"> <view class="flex items-center gap-4 rounded-full border border-slate-200 p-1">
<text class="w-20 text-sm text-slate-700">手机号</text> <button class="booking-button m-0 flex h-7 w-7 items-center justify-center rounded-full bg-slate-100 p-0 text-base text-slate-500" :disabled="quantity <= 1" @tap="changeQuantity(-1)"></button>
<input class="min-w-0 flex-1 text-right text-sm" type="number" maxlength="11" :value="draft.phone || ''" placeholder="用于接收订单通知" @input="updatePhone" /> <text class="min-w-4 text-center font-bold">{{ quantity }}</text>
<button class="booking-button m-0 flex h-7 w-7 items-center justify-center rounded-full bg-blue-600 p-0 text-base text-white" :disabled="quantity >= maxQuantity" @tap="changeQuantity(1)"></button>
</view>
</view> </view>
</view> </view>
<view class="flex items-center rounded-2xl bg-white px-4 py-4" @tap="toggleInsurance"> <view v-if="product.reservation?.realNameRequired" class="mt-2 bg-white p-4">
<view class="flex min-w-0 items-center justify-between gap-3">
<text class="font-bold">需填 <text class="text-orange-500">{{ quantity }}</text> 位游客</text>
<button class="booking-button m-0 flex shrink-0 items-center gap-1 bg-transparent p-0 text-xs font-semibold text-blue-600" @tap="emit('open-travelers')">
选择/新增
<uni-icons type="right" size="15" color="#2563eb" />
</button>
</view>
<text class="mt-1 block text-xs text-slate-400">可直接选择常用出行人也可以进入常用出行人页面新增</text>
<scroll-view scroll-x class="mt-3 whitespace-nowrap">
<view class="flex gap-2 pb-1">
<button
v-for="traveler in travelers"
:key="traveler.id"
class="booking-button relative m-0 min-w-[92px] rounded-xl border px-3 py-3 text-center text-sm font-semibold"
:class="travelerClass(traveler)"
@tap="toggleTraveler(traveler.id)"
>
{{ traveler.name }}
<view v-if="isTravelerSelected(traveler.id)" class="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-[4px] bg-blue-600">
<uni-icons type="checkmarkempty" size="12" color="#ffffff" />
</view>
</button>
<button class="booking-button m-0 flex min-w-[92px] items-center justify-center gap-1 rounded-xl border border-dashed border-blue-200 bg-blue-50 px-3 py-3 text-xs font-semibold text-blue-600" @tap="emit('open-travelers')">
选择/新增
<uni-icons type="right" size="14" color="#2563eb" />
</button>
</view>
</scroll-view>
<view v-if="selectedTravelers.length" class="mt-4 divide-y divide-slate-100 border-t border-slate-100">
<view v-for="(traveler, index) in selectedTravelers" :key="traveler.id" class="flex min-w-0 items-center gap-3 py-3">
<button class="booking-button m-0 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-slate-400 bg-white p-0 text-base leading-none text-slate-500" @tap="toggleTraveler(traveler.id)"></button>
<view class="min-w-0 flex-1">
<text class="block text-xs font-bold text-slate-900">游客 {{ index + 1 }}</text>
<text class="mt-1 block truncate text-sm text-slate-500">{{ traveler.name }} · {{ traveler.idType }} {{ traveler.idNumber }}</text>
</view>
</view>
</view>
<view v-if="product.reservation?.contactPhoneRequired" class="flex min-w-0 items-center gap-3 border-t border-slate-100 pt-4">
<text class="shrink-0 text-sm font-bold">联系手机<text class="text-rose-500">*</text></text>
<input class="h-10 min-w-0 flex-1 border-b border-slate-200 px-1 text-sm" type="number" maxlength="11" :value="draft.phone || ''" placeholder="用于接收出票通知" @input="updatePhone" />
<uni-icons class="shrink-0" type="phone" size="18" color="#2563eb" />
</view>
</view>
<view v-else-if="product.reservation?.contactPhoneRequired" class="mt-2 bg-white p-4">
<text class="font-bold">联系人</text>
<input class="mt-3 h-11 w-full rounded-xl border border-slate-200 px-3 text-sm" type="number" maxlength="11" :value="draft.phone || ''" placeholder="手机号,用于接收出票通知" @input="updatePhone" />
</view>
<view class="mt-2 flex items-start bg-white p-4" @tap="toggleInsurance">
<view class="min-w-0 flex-1"> <view class="min-w-0 flex-1">
<text class="block text-base font-semibold">{{ insurance.name }}</text> <view class="flex min-w-0 items-center justify-between gap-3">
<text class="mt-1 block truncate text-xs text-slate-500">{{ insurance.description }}</text> <text class="font-bold text-slate-900">{{ insurance.name }}</text>
<text class="mt-1 block text-xs text-rose-500">¥{{ insurancePrice }}/</text> <text class="shrink-0 font-bold text-orange-600">¥{{ insurancePrice }}/</text>
</view>
<text class="mt-1 block text-xs leading-5 text-slate-500">默认购买按游客人数计算如不需要可取消勾选</text>
<text class="mt-1 block text-xs text-slate-400">{{ insurance.description }}</text>
<text class="mt-2 block text-xs font-semibold text-blue-600">{{ draft.insuranceSelected ? `已选择 ${quantity} 份,共 ¥${totals.insuranceAmount}` : '未购买保险' }}</text>
</view> </view>
<view class="ml-3 flex h-6 w-11 items-center rounded-full p-0.5" :class="draft.insuranceSelected ? 'bg-emerald-500' : 'bg-slate-300'"> <view class="ml-3 flex h-6 w-11 items-center rounded-full p-0.5" :class="draft.insuranceSelected ? 'bg-emerald-500' : 'bg-slate-300'">
<view class="h-5 w-5 rounded-full bg-white shadow transition-transform" :class="draft.insuranceSelected ? 'translate-x-5' : 'translate-x-0'"></view> <view class="h-5 w-5 rounded-full bg-white shadow transition-transform" :class="draft.insuranceSelected ? 'translate-x-5' : 'translate-x-0'"></view>
</view> </view>
</view> </view>
<view class="rounded-2xl bg-amber-50 px-4 py-3 text-xs leading-5 text-amber-700">提交订单前将校验实名制门票所需的出行人信息请确认姓名及证件号码与有效证件一致</view>
</view> </view>
</scroll-view> </scroll-view>
<view class="box-border flex w-full min-w-0 shrink-0 items-center bg-white px-4 py-3 shadow-[0_-2px_10px_rgba(15,23,42,0.08)]"> <view class="absolute bottom-0 left-0 right-0 z-20 box-border flex w-full min-w-0 items-center gap-3 border-t border-slate-200 bg-white p-4 pb-[calc(env(safe-area-inset-bottom)+8px)]">
<view class="min-w-0 flex-1"> <view class="shrink-0">
<text class="block text-xs text-slate-500">商品¥{{ totals.productAmount }}{{ draft.insuranceSelected ? ` + 保险¥${totals.insuranceAmount}` : '' }}</text> <text class="block text-[10px] text-slate-400">商品 ¥{{ totals.productAmount }}{{ draft.insuranceSelected ? ` + 保险 ¥${totals.insuranceAmount}` : '' }}</text>
<text class="mt-1 block text-lg font-semibold text-rose-500">合计 ¥{{ totals.paidAmount }}</text> <text class="block text-xl font-bold text-orange-600">¥{{ totals.paidAmount }}</text>
</view> </view>
<button class="m-0 rounded-full bg-emerald-600 px-4 py-3 text-sm font-semibold text-white" @tap="emit('submit')">提交订单并模拟支付</button> <button class="booking-button m-0 ml-auto h-12 w-[270rpx] flex-none rounded-full bg-orange-500 px-3 text-sm font-bold leading-[48px] text-white" @tap="emit('submit')">提交订单并支付</button>
</view> </view>
</view> </view>
</template> </template>
@@ -137,29 +192,69 @@ const selectedTravelers = computed(() => {
const selectedIds = new Set(props.draft.travelerIds || []) const selectedIds = new Set(props.draft.travelerIds || [])
return props.travelers.filter((traveler) => selectedIds.has(traveler.id)) return props.travelers.filter((traveler) => selectedIds.has(traveler.id))
}) })
const bookingHighlights = computed(() => {
const highlights = (props.product.highlights || []).filter((item) => item !== '官方')
return highlights.slice(0, 3).join(' ') || '官方预订 便捷入园'
})
const ageRuleText = computed(() => {
const { min, max } = props.product.ageRule || {}
if (Number(max) >= 999) return `${min}周岁及以上`
return `${min}${max}周岁`
})
const isScenicAdmission = computed(() => ['景区门票', '组合套餐'].includes(props.product.category))
const slotTitle = computed(() => isScenicAdmission.value ? '入园场次' : '预约场次')
const patchDraft = (patch) => emit('update-draft', { ...props.draft, ...patch }) const patchDraft = (patch) => emit('update-draft', { ...props.draft, ...patch })
const isSoldOut = (item) => Number(item.stock) === 0 const isSoldOut = (item) => Number(item.stock) === 0
const dateValue = (item) => item.date || item.value || '' const dateValue = (item) => item.date || item.value || ''
const datePrice = (item) => Number(item.price ?? props.product.salePrice ?? 0).toLocaleString('zh-CN')
const dateClass = (item) => { const dateClass = (item) => {
if (isSoldOut(item)) return 'border-slate-100 bg-slate-50 text-slate-400' if (isSoldOut(item)) return 'border-slate-100 bg-slate-50 text-slate-300'
return props.draft.visitDate === dateValue(item) return props.draft.visitDate === dateValue(item)
? 'border-emerald-500 bg-emerald-50 text-emerald-700' ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-100 text-blue-700'
: 'border-slate-200 bg-white text-slate-700' : 'border-slate-200 bg-slate-50 text-slate-700'
}
const slotClass = (slot) => {
if (props.draft.timeSlot === slot) return 'border-blue-500 bg-blue-50 ring-2 ring-blue-100 text-blue-700'
if (!props.draft.visitDate) return 'border-slate-100 bg-slate-50 text-slate-300'
return 'border-slate-200 bg-slate-50 text-slate-700'
} }
const selectDate = (item) => { const selectDate = (item) => {
if (!isSoldOut(item)) patchDraft({ visitDate: dateValue(item), timeSlot: '' }) if (!isSoldOut(item)) patchDraft({ visitDate: dateValue(item), timeSlot: '' })
} }
const changeQuantity = (offset) => { const changeQuantity = (offset) => {
const nextQuantity = Math.min(Math.max(quantity.value + offset, 1), maxQuantity.value) const nextQuantity = Math.min(Math.max(quantity.value + offset, 1), maxQuantity.value)
patchDraft({ quantity: nextQuantity }) const travelerIds = (props.draft.travelerIds || []).slice(0, nextQuantity)
patchDraft({ quantity: nextQuantity, travelerIds })
} }
const isTravelerSelected = (id) => (props.draft.travelerIds || []).includes(id)
const travelerClass = (traveler) => isTravelerSelected(traveler.id)
? 'border-blue-500 bg-blue-50 text-blue-700 ring-1 ring-blue-100'
: 'border-slate-200 bg-slate-50 text-slate-700'
const toggleTraveler = (id) => { const toggleTraveler = (id) => {
const ids = new Set(props.draft.travelerIds || []) const ids = [...new Set(props.draft.travelerIds || [])]
ids.has(id) ? ids.delete(id) : ids.add(id) const isSelected = ids.includes(id)
patchDraft({ travelerIds: [...ids] }) if (!isSelected && ids.length >= quantity.value) {
uni.showToast({ title: `最多选择${quantity.value}位游客`, icon: 'none' })
return
}
const travelerIds = isSelected ? ids.filter((value) => value !== id) : [...ids, id]
const firstTraveler = props.travelers.find((traveler) => travelerIds.includes(traveler.id))
patchDraft({ travelerIds, contactName: firstTraveler?.name || props.draft.contactName || '' })
}
const updatePhone = (event) => {
const firstTraveler = selectedTravelers.value[0]
patchDraft({
phone: event.detail.value,
contactName: props.draft.contactName || firstTraveler?.name || '游客',
})
} }
const updateContactName = (event) => patchDraft({ contactName: event.detail.value })
const updatePhone = (event) => patchDraft({ phone: event.detail.value })
const toggleInsurance = () => patchDraft({ insuranceSelected: !props.draft.insuranceSelected, insurancePrice: insurancePrice.value }) const toggleInsurance = () => patchDraft({ insuranceSelected: !props.draft.insuranceSelected, insurancePrice: insurancePrice.value })
</script> </script>
<style scoped>
.booking-button::after {
border: 0;
}
</style>

View File

@@ -29,8 +29,8 @@
</view> </view>
<view class="mt-[16px] grid w-full min-w-0 grid-cols-2 gap-[12px]"> <view class="mt-[16px] grid w-full min-w-0 grid-cols-2 gap-[12px]">
<button class="m-0 flex h-[48px] items-center justify-center rounded-[24px] border border-[#7c3aed] bg-white p-0 text-[15px] leading-[48px] text-[#6d28d9]" @click="emit('detail')">查看订单详情</button> <button class="m-0 flex h-[48px] items-center justify-center rounded-[24px] border border-[#2563eb] bg-white p-0 text-[15px] leading-[48px] text-[#2563eb]" @click="emit('detail')">查看订单详情</button>
<button v-if="hasCredential" class="m-0 flex h-[48px] items-center justify-center rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="emit('credentials')">查看凭证</button> <button v-if="hasCredential" class="m-0 flex h-[48px] items-center justify-center rounded-[24px] bg-[#2563eb] p-0 text-[15px] leading-[48px] text-white" @click="emit('credentials')">查看凭证</button>
<button class="m-0 flex h-[48px] items-center justify-center rounded-[24px] border border-[#d0d5dd] bg-white p-0 text-[15px] leading-[48px] text-[#344054]" @click="emit('orders')">我的订单</button> <button class="m-0 flex h-[48px] items-center justify-center rounded-[24px] border border-[#d0d5dd] bg-white p-0 text-[15px] leading-[48px] text-[#344054]" @click="emit('orders')">我的订单</button>
<button class="m-0 flex h-[48px] items-center justify-center rounded-[24px] border border-[#d0d5dd] bg-white p-0 text-[15px] leading-[48px] text-[#344054]" @click="emit('home')">返回景区首页</button> <button class="m-0 flex h-[48px] items-center justify-center rounded-[24px] border border-[#d0d5dd] bg-white p-0 text-[15px] leading-[48px] text-[#344054]" @click="emit('home')">返回景区首页</button>
</view> </view>

View File

@@ -62,6 +62,57 @@ test("storefront synchronizes the left category from right-column scrolling", as
assert.match(source, /TopNavBar/); assert.match(source, /TopNavBar/);
}); });
test("booking flow matches the visitor prototype while preserving requested controls", async () => {
const component = await readTicketFile("components/BookingFlow.vue");
const page = await readTicketFile("booking.vue");
assert.match(component, /flex rounded-2xl bg-white p-3 shadow-sm/);
assert.match(component, /product\.images\?\.\[0\]/);
assert.match(component, /mt-\[5px\] flex flex-wrap gap-\[4px\]/);
assert.match(component, /rounded-\[4px\] bg-\[#f2ebff\] px-\[5px\] py-\[2px\] text-\[10px\] text-\[#7a52b3\]/);
assert.match(component, /rounded-\[4px\] bg-\[#e8f2ff\] px-\[5px\] py-\[2px\] text-\[10px\] text-\[#3474c7\]/);
assert.doesNotMatch(component, /rounded bg-orange-50 px-1\.5 py-0\.5 text-xs text-orange-600/);
assert.match(component, /border-b border-orange-100 bg-orange-50 p-4/);
assert.match(component, /请先选择使用日期和场次,再填写游客信息。/);
assert.match(component, /product\.ageRule/);
assert.match(component, /年龄限制/);
assert.match(component, /border-blue-500 bg-blue-50 ring-2 ring-blue-100/);
assert.match(component, /min-w-\[90px\][^"]*pb-3 pt-6 text-center/);
assert.match(component, /<view v-if="index < 2" class="absolute left-0 top-0 flex h-5 items-center justify-center rounded-br-lg rounded-tl-xl bg-orange-500 px-2">/);
assert.match(component, /<text class="text-\[10px\] leading-none text-white">/);
assert.doesNotMatch(component, /v-if="index < 2"[^>]*-top-/);
assert.match(component, /datePrice/);
assert.match(component, /余 \{\{ item\.stock \}\}/);
assert.match(component, /slotTitle/);
assert.match(component, /购买数量/);
assert.match(component, /rounded-full border border-slate-200 p-1/);
assert.match(component, /bg-blue-600[^"]*text-white/);
assert.match(component, /需填/);
assert.match(component, /选择\/新增/);
assert.match(component, /h-5 w-5 items-center justify-center rounded-\[4px\] bg-blue-600/);
assert.doesNotMatch(component, /h-5 w-5 items-center justify-center rounded-full bg-blue-600/);
assert.match(component, /联系手机/);
assert.match(component, /用于接收出票通知/);
assert.match(component, /<scroll-view scroll-y class="relative z-0 min-h-0 flex-1">/);
assert.match(component, /默认购买,按游客人数计算;如不需要可取消勾选。/);
assert.match(component, /ml-3 flex h-6 w-11 items-center rounded-full p-0\.5/);
assert.match(component, /draft\.insuranceSelected \? 'bg-emerald-500' : 'bg-slate-300'/);
assert.match(component, /absolute bottom-0 left-0 right-0 z-20/);
assert.match(component, /ml-auto h-12 w-\[\d+rpx\] flex-none rounded-full/);
assert.doesNotMatch(component, /ml-auto h-12 min-w-0 flex-1 rounded-full/);
assert.match(component, /bg-orange-500/);
assert.match(component, /text-orange-600/);
assert.match(page, /paid \? '订单已支付,可查看凭证' : '可选日期'/);
});
test("payment result uses blue primary actions", async () => {
const component = await readTicketFile("components/PaymentResult.vue");
assert.match(component, /border border-\[#2563eb\][^"]*text-\[#2563eb\]" @click="emit\('detail'\)">查看订单详情/);
assert.match(component, /bg-\[#2563eb\][^"]*text-white" @click="emit\('credentials'\)">查看凭证/);
assert.doesNotMatch(component, /#7c3aed|#6d28d9/);
});
test("order content uses bounded box-model wrappers", async () => { test("order content uses bounded box-model wrappers", async () => {
const componentNames = [ const componentNames = [
"OrderList", "OrderList",