feat: 首页的样式的调整

This commit is contained in:
2026-07-30 22:00:37 +08:00
parent 76abf4a9f0
commit 0bb34f8aa7
3 changed files with 82 additions and 70 deletions

View File

@@ -45,7 +45,7 @@
<view class="relative z-10 -mt-[28px] min-h-0 flex-1 overflow-hidden rounded-t-[28px] bg-[#f4f6f8] shadow-[0_-8px_24px_rgba(6,17,31,0.12)]">
<view class="flex h-full min-h-0">
<scroll-view
class="h-full w-[96px] shrink-0 bg-white"
class="h-full w-[80px] shrink-0 bg-white"
scroll-y
:scroll-into-view="leftScrollIntoViewId"
scroll-with-animation
@@ -83,18 +83,23 @@
<view v-for="product in productsForGroup(group)" :key="product.id" class="flex rounded-[14px] bg-white p-[10px]">
<image class="h-[100px] w-[100px] shrink-0 rounded-[10px] bg-[#e8edf0]" :src="product.images && product.images[0]" mode="aspectFill" @click="emit('open-detail', product.id)" />
<view class="ml-[10px] flex min-w-0 flex-1 flex-col">
<text class="line-clamp-2 text-[14px] font-semibold leading-[20px] text-[#242b36]" @click="emit('open-detail', product.id)">{{ product.shortTitle || product.name }}</text>
<view class="flex min-w-0 items-start" @click="emit('open-detail', product.id)">
<text class="line-clamp-2 min-w-0 text-[14px] font-semibold leading-[20px] text-[#242b36]">{{ product.shortTitle || product.name }}</text>
<view class="flex h-[20px] shrink-0 items-center">
<uni-icons type="right" size="14" color="#242b36" />
</view>
</view>
<view class="mt-[5px] flex flex-wrap gap-[4px]">
<text v-if="product.ageRule" class="rounded-[4px] bg-[#e8f2ff] px-[5px] py-[2px] text-[10px] text-[#3474c7]">{{ ageRuleText(product.ageRule) }}</text>
<text v-for="highlight in product.highlights || []" :key="highlight" class="rounded-[4px] bg-[#f2ebff] px-[5px] py-[2px] text-[10px] text-[#7a52b3]">{{ highlight }}</text>
<text v-if="product.ageRule" class="rounded-[4px] bg-[#f2ebff] px-[5px] py-[2px] text-[10px] text-[#7a52b3]">{{ ageRuleText(product.ageRule) }}</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 class="mt-auto flex items-end justify-between pt-[6px]">
<view>
<text class="block text-[10px] text-[#9aa2ad]">已售 {{ product.sold || 0 }}</text>
<text class="text-[12px] font-medium text-[#f26b32]">¥<text class="text-[20px] font-bold">{{ product.salePrice }}</text></text>
<text class="text-[12px] font-medium text-[#f26b32]">¥<text class="text-[20px] font-bold">{{ product.salePrice }}</text></text>
</view>
<view class="rounded-full bg-[#f26b32] px-[13px] py-[6px]" @click="emit('buy', product.id)">
<text class="text-[12px] font-medium text-white">选购</text>
<view class="flex h-[28px] items-center justify-center rounded-full bg-[#f26b32] px-[13px]" @click="emit('buy', product.id)">
<text class="text-[12px] font-medium leading-none text-white">选购</text>
</view>
</view>
</view>
@@ -132,9 +137,13 @@ const productsById = computed(() => new Map(props.products.map((product) => [pro
const productsForGroup = (group) => (group.productIds || [])
.map((productId) => productsById.value.get(productId))
.filter(Boolean)
.filter((product) => product?.status === 'on_sale' && product.clientVisible !== false)
const ageRuleText = (rule) => `${rule.min}${rule.max}周岁`
const ageRuleText = (rule) => {
if (rule.min > 0 && rule.max >= 999) return `${rule.min}周岁及以上`
if (rule.min <= 0 && rule.max < 999) return `${rule.max}周岁及以下`
return `${rule.min}${rule.max}周岁`
}
const selectGroup = (groupId) => {
activeGroupId.value = groupId

View File

@@ -15,16 +15,16 @@ export const SCENIC_SPOT = {
}
export const GROUPS = [
{ id: 'ticket', name: '景点门票', subtitle: '畅游小七孔核心景点', description: '适合自由行游客的景区基础门票', tag: '热门', productIds: ['adult', 'elderly', 'minor', 'free'], sortOrder: 1, enabled: true },
{ id: 'vip', name: 'VIP速通', subtitle: '快速入园省时省心', description: '专属快速通道,减少排队等待', tag: '优选', productIds: ['vip-fast'], sortOrder: 2, enabled: true },
{ id: 'bus', name: '直通车', subtitle: '往返交通一站直达', description: '景区直通车,出行更轻松', tag: '便捷', productIds: ['direct-bus'], sortOrder: 3, enabled: true },
{ id: 'package', name: '门票+观光车', subtitle: '经典组合更划算', description: '门票与观光车联票,覆盖主要游览动线', tag: '套餐', productIds: ['classic'], sortOrder: 4, enabled: true },
{ id: 'tour', name: '一日游', subtitle: '跟团畅游无忧', description: '含行程服务的一日游体验', tag: '省心', productIds: ['day-tour'], sortOrder: 5, enabled: true },
{ id: 'play', name: '玩乐体验', subtitle: '解锁山水更多乐趣', description: '水上与讲解体验任选', tag: '推荐', productIds: ['boat', 'guide'], sortOrder: 6, enabled: true },
{ id: 'ticket', name: '景点门票', subtitle: '累计销量 5000+', description: '一票通玩核销后门票7日内有效', tag: '官方', productIds: ['adult', 'elderly', 'minor', 'free'], sortOrder: 1, enabled: true },
{ id: 'vip', name: 'VIP速通', subtitle: '专属服务', description: '减少排队等待时间,按预约场次使用', tag: '推荐', productIds: ['vip-fast'], sortOrder: 2, enabled: true },
{ id: 'bus', name: '直通车', subtitle: '往返直达', description: '贵阳市区集合,直达小七孔景区', tag: '省心', productIds: ['direct-bus'], sortOrder: 3, enabled: true },
{ id: 'package', name: '门票+观光车', subtitle: '累计销量 1.5万+', description: '门票与观光车由第三方出票,园内项目使用自营权益码', tag: '热卖', productIds: ['classic'], sortOrder: 4, enabled: true },
{ id: 'tour', name: '一日游', subtitle: '精选线路', description: '轻松游览小七孔代表性景观', tag: '品质', productIds: ['day-tour'], sortOrder: 5, enabled: true },
{ id: 'play', name: '玩乐体验', subtitle: '园内自营项目', description: '工作人员扫描园内权益码后选择项目核销', tag: '自营', productIds: ['boat', 'guide'], sortOrder: 6, enabled: true },
]
const TIME_SLOTS = ['08:00—09:30', '09:30—11:30', '11:30—13:30', '13:30—15:00', '15:00—16:00']
const ID_TYPES = ['身份证', '护照', '港澳居民来往内地通行证']
const ID_TYPES = ['身份证', '护照']
const createReservation = ({
requiresDate = true,
@@ -64,6 +64,8 @@ const createProduct = ({
purchaseNotice,
reservation,
entitlements,
status = 'on_sale',
clientVisible = true,
}) => ({
id,
name,
@@ -74,7 +76,8 @@ const createProduct = ({
marketPrice,
stock,
sold,
status: 'on_sale',
status,
clientVisible,
highlights: [...highlights],
ageRule: ageRule ? { ...ageRule } : null,
included: [...included],
@@ -86,58 +89,58 @@ const createProduct = ({
entitlements: entitlements.map((entitlement) => ({ ...entitlement })),
})
const ticketEntitlement = (id, name, fulfillment, refundAllocation) => ({ id, name, fulfillment, quantity: 1, unit: '张', refundAllocation })
const ticketEntitlement = (id, name, fulfillment, refundAllocation, unit = '张') => ({ id, name, fulfillment, quantity: 1, unit, refundAllocation })
export const PRODUCTS = [
createProduct({
id: 'adult', name: '成人票+观光车', shortTitle: '成人门票+观光车', category: '景门票', description: '荔波小七孔景区成人入园门票。', salePrice: 160, marketPrice: 170, stock: 120, sold: 2386,
highlights: ['实名预约入园', '覆盖核心景区'], included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。',
reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('adult-ticket', '成人门票', 'third_party', 120), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)],
id: 'adult', name: '荔波小七孔成人票+观光车', shortTitle: '成人门票+观光车', category: '景门票', description: '成人门票与观光车固定组合,指定日期和场次使用,由第三方票务系统统一出票。', salePrice: 160, marketPrice: 160, stock: 500, sold: 2386,
highlights: ['官方', '可订明日', '随时退', '无需取票'], included: ['荔波小七孔景区成人门票 1 张、观光车票 1 张。'], excluded: ['不含景区保险及其他园内自费项目。'], detail: '门票与观光车为固定销售组合,由第三方票务系统统一出票。出票成功后,可凭本人身份证或入园二维码通过景区闸机。', purchaseNotice: '实名预约,一人一证;请按照预约日期和入园时段使用。',
reservation: { advanceDays: 30 }, entitlements: [ticketEntitlement('adult-ticket', '小七孔成人门票', 'third_party', 120), ticketEntitlement('shuttle', '小七孔观光车票', 'third_party', 40)],
}),
createProduct({
id: 'elderly', name: '老年人票+观光车', shortTitle: '老年人门票+观光车', category: '景门票', description: '适用于60周岁及以上游客的优惠门票。', salePrice: 100, marketPrice: 160, stock: 80, sold: 816,
highlights: ['60周岁及以上', '实名票'], ageRule: { min: 60, max: 120 }, included: ['老年优惠门票', '景区观光车'], excluded: ['其他二次消费'], detail: '入园时须出示本人身份证核验年龄。', purchaseNotice: '年龄不符将无法使用,请按实际年龄购票。',
reservation: { advanceDays: 1, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('elderly-ticket', '老年优惠门票', 'third_party', 60), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)],
id: 'elderly', name: '荔波小七孔老年人票+观光车', shortTitle: '老年人门票+观光车', category: '景门票', description: '老年人门票与观光车固定组合,系统按使用日期和身份证出生日期校验年龄。', salePrice: 100, marketPrice: 100, stock: 1800, sold: 816,
highlights: ['官方', '实名预约', '年龄校验', '第三方出票'], ageRule: { min: 60, max: 999 }, included: ['荔波小七孔景区老年人票 1 张、观光车票 1 张。'], excluded: ['不含景区保险及其他园内项目。'], detail: '系统会根据使用日期和身份证中的出生日期计算年龄,符合条件后提交第三方票务系统出票。', purchaseNotice: '限使用日年满60周岁的游客购买一人一证证件信息须与使用人一致。',
reservation: { advanceDays: 30, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('elderly-ticket', '小七孔老年人票', 'third_party', 60), ticketEntitlement('shuttle', '小七孔观光车票', 'third_party', 40)],
}),
createProduct({
id: 'minor', name: '未成年票+观光车', shortTitle: '未成年门票+观光车', category: '景门票', description: '适用于6至17周岁未成年人。', salePrice: 100, marketPrice: 160, stock: 75, sold: 629,
highlights: ['6至17周岁适用', '实名预约'], ageRule: { min: 6, max: 17 }, included: ['未成年优惠门票', '景区观光车'], excluded: ['其他二次消费'], detail: '未成年人须使用本人有效身份证件预约。', purchaseNotice: '请按使用日年龄选择票种。',
reservation: { advanceDays: 1, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('minor-ticket', '儿童优惠门票', 'third_party', 60), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)],
id: 'minor', name: '荔波小七孔未成年票+观光车', shortTitle: '未成年门票+观光车', category: '景门票', description: '未成年人门票与观光车固定组合,系统按使用日期和身份证出生日期校验年龄。', salePrice: 100, marketPrice: 100, stock: 1800, sold: 629,
highlights: ['官方', '实名预约', '年龄校验', '第三方出票'], ageRule: { min: 6, max: 17 }, included: ['荔波小七孔景区未成年票 1 张、观光车票 1 张。'], excluded: ['不含景区保险及其他园内项目。'], detail: '系统会根据使用日期和身份证中的出生日期计算年龄符合6—17周岁条件后提交第三方票务系统出票。', purchaseNotice: '使用日6—17周岁的游客购买一人一证证件信息须与使用人一致。',
reservation: { advanceDays: 30, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('minor-ticket', '小七孔未成年票', 'third_party', 60), ticketEntitlement('shuttle', '小七孔观光车票', 'third_party', 40)],
}),
createProduct({
id: 'free', name: '免门票+观光车实名预约', shortTitle: '免门票+观光车实名预约', category: '景门票', description: '适用于符合景区优待政策的游客。', salePrice: 40, marketPrice: 80, stock: 60, sold: 346,
highlights: ['优待人群适用', '凭证核验'], included: ['免门票', '景区观光车'], excluded: ['其他二次消费'], detail: '入园时须携带可证明优待资格的有效证件。', purchaseNotice: '不符合优待政策者需现场补差价。',
reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('free-ticket', '免门票', 'third_party', 0), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)],
id: 'free', name: '荔波小七孔免门票+观光车', shortTitle: '免门票+观光车实名预约', category: '景门票', description: '特殊政策期门票免费、观光车正常计费,仍需实名校验并由第三方票务系统出票。', salePrice: 40, marketPrice: 40, stock: 1200, sold: 346,
highlights: ['政策免票', '实名预约', '限期开放', '第三方出票'], ageRule: { min: 0, max: 120 }, included: ['特殊政策期小七孔景区入园预约资格 1 份、观光车票 1 张。'], excluded: ['不含景区保险及所有园内自费项目。'], detail: '免票商品仍需选择日期与场次并填写实名信息。系统校验预约日期处于政策期后,向第三方票务系统请求出票。', purchaseNotice: '仅在后台配置的政策日期内开放;游客还须符合当期景区公告的适用条件。',
reservation: { advanceDays: 30, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('free-ticket', '小七孔特殊时期免门票', 'third_party', 0), ticketEntitlement('shuttle', '小七孔观光车票', 'third_party', 40)],
}),
createProduct({
id: 'vip-fast', name: 'VIP速通票', shortTitle: 'VIP快速通行', category: 'VIP速通', description: '专属通道快速入园,节省排队时间。', salePrice: 98, marketPrice: 128, stock: 50, sold: 126,
highlights: ['专属快速通', '即买即用'], included: ['VIP速通服务'], excluded: ['景区基础门票'], detail: '需与有效景区门票配合使用。', purchaseNotice: '服务一经核销不可退款。',
reservation: { advanceDays: 0, realNameRequired: false, timeSlots: ['08:00—10:00', '10:00—12:00', '12:00—14:00', '14:00—16:00'] }, entitlements: [ticketEntitlement('vip-fast-pass', 'VIP速通服务', 'self', 98)],
id: 'vip-fast', name: '小七孔景区 VIP 快速通行服务', shortTitle: 'VIP 快速通行', category: '园内项目', description: '专属通道与现场引导服务,减少排队等待时间。', salePrice: 98, marketPrice: 98, stock: 200, sold: 126,
highlights: ['专属通道', '快速通', '无需取票'], included: ['小七孔景区 VIP速通服务 1 次。'], excluded: ['不含景区门票、观光车及其他园内项目。'], detail: '购买后生成园内权益码,请按预约时间到指定服务区域联系工作人员。', purchaseNotice: '请提前完成预约并按时到场,具体通行安排以现场工作人员指引为准。',
reservation: { advanceDays: 15, realNameRequired: false, idTypes: ['身份证'], timeSlots: ['08:00—10:00', '10:00—12:00', '12:00—14:00', '14:00—16:00'] }, entitlements: [ticketEntitlement('vip-fast-pass', 'VIP速通服务', 'self', 98, '次')],
}),
createProduct({
id: 'direct-bus', name: '贵阳往返小七孔直通车', shortTitle: '贵阳往返小七孔直通车', category: '直通车', description: '贵阳往返小七孔景区的便捷直通车服务。', salePrice: 68, marketPrice: 88, stock: 40, sold: 92,
highlights: ['往返直达', '固定班次'], included: ['贵阳往返小七孔直通车服务'], excluded: ['景区门票'], detail: '本权益包含固定去程和返程两段车次构成一次完整往返行程请按预约班次提前15分钟候车。', purchaseNotice: '06:30—07:00 为固定去程、18:00—18:30 为固定返程两段均属于同一往返权益不可按单程理解发车前2小时可免费改期一次。',
reservation: { advanceDays: 0, timeSlots: ['06:30—07:00 去程', '18:00—18:30 返程'] }, entitlements: [ticketEntitlement('direct-bus-seat', '景区直通车', 'third_party', 68)],
id: 'direct-bus', name: '贵阳往返小七孔景区直通车', shortTitle: '贵阳往返小七孔直通车', category: '交通票', description: '贵阳市区集合,往返直达小七孔景区,省去中转换乘。', salePrice: 68, marketPrice: 68, stock: 180, sold: 92,
highlights: ['往返直达', '定点发车', '第三方出票'], included: ['贵阳小七孔景区往返直通车乘车权益 1 份。'], excluded: ['不含景区门票、餐饮及其他个人消费。'], detail: '下单时选择出行日期和班次,出票成功后凭乘车凭证按时到达集合地点。', purchaseNotice: '请至少提前20分钟到达集合地点迟到或错过班次按商品退改规则处理。',
reservation: { advanceDays: 30, timeSlots: ['06:30—07:00 去程', '18:00—18:30 返程'] }, entitlements: [ticketEntitlement('direct-bus-seat', '景区直通车', 'third_party', 68, '位')],
}),
createProduct({
id: 'classic', name: '门票+观光车+鸳鸯湖游船', shortTitle: '门票+观光车+鸳鸯湖游船', category: '门票+观光车', description: '景区成人门票与观光车组合套餐。', salePrice: 188, marketPrice: 210, stock: 90, sold: 184,
highlights: ['门票观光车联票', '比单买更优惠'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车', '鸳鸯湖游船'], excluded: ['餐饮及个人消费'], detail: '一次预约即可享受门票观光车与鸳鸯湖游船。', purchaseNotice: '各权益可分别核销,未使用部分可按规则申请退款。',
reservation: { advanceDays: 1, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('classic-ticket-shuttle', '成人门票+观光车', 'third_party', 160), ticketEntitlement('classic-boat', '鸳鸯湖游船', 'self', 28)],
id: 'classic', name: '小七孔经典畅游套餐', shortTitle: '门票 + 观光车 + 鸳鸯湖游船(成人套餐)', category: '组合套餐', description: '门票 + 观光车 + 鸳鸯湖游船,一次购买,两类凭证独立履约。', salePrice: 188, marketPrice: 210, stock: 480, sold: 184,
highlights: ['官方', '实名预约', '整包退', '双凭证'], included: ['成人门票 1 张、观光车票 1 张、鸳鸯湖游船 1 次。'], excluded: ['不含景区保险、餐饮及套餐外个人消费'], detail: '一个订单包含两类凭证:第三方票务凭证包含景区门票观光车票;自营权益码包含鸳鸯湖游船,由工作人员扫码核销。', purchaseNotice: '套餐统一选择使用日期和入园时段。全部权益未使用可整单全额退;任一权益使用后整单不可退,不支持拆项退款。',
reservation: { advanceDays: 30 }, entitlements: [ticketEntitlement('classic-ticket', '小七孔成人门票', 'third_party', 120), ticketEntitlement('classic-shuttle', '观光车', 'third_party', 40), ticketEntitlement('classic-boat', '鸳鸯湖游船', 'self', 28, '次')],
}),
createProduct({
id: 'day-tour', name: '荔波小七孔精品一日游', shortTitle: '荔波小七孔精品一日游', category: '一日游', description: '包含行程组织服务的小七孔一日游。', salePrice: 188, marketPrice: 228, stock: 30, sold: 68,
highlights: ['行程服务', '省心游玩'], included: ['一日游行程服务', '行程讲解'], excluded: ['景区门票及个人消费'], detail: '集合地点与行程安排将于出行前发送。', purchaseNotice: '请在出行前一天18:00前完成预约。',
reservation: { advanceDays: 1, maxQuantity: 10, timeSlots: ['08:00—08:30 集合', '09:00—09:30 集合'] }, entitlements: [ticketEntitlement('day-tour-service', '一日游服务', 'self', 188)],
id: 'day-tour', name: '荔波小七孔精品一日游', shortTitle: '小七孔精品一日游', category: '园内项目', description: '精选游览路线与随行服务,一天轻松游览小七孔代表性景观。', salePrice: 188, marketPrice: 188, stock: 120, sold: 68,
highlights: ['精品小团', '行程讲解', '可订明日'], included: ['小七孔精品一日游随行服务 1 份。'], excluded: ['不含未在商品说明中列明的个人消费'], detail: '覆盖小七孔古桥、拉雅瀑布、水上森林等代表性游览区域,具体线路以当天安排为准。', purchaseNotice: '下单时请选择出行日期和集合场次,并准确填写联系人与出行人信息。',
reservation: { advanceDays: 20, timeSlots: ['08:00—08:30 集合', '09:00—09:30 集合'] }, entitlements: [ticketEntitlement('day-tour-service', '小七孔精品一日游', 'self', 188, '份')],
}),
createProduct({
id: 'boat', name: '鸳鸯湖游船票', shortTitle: '鸳鸯湖游船票', category: '玩乐体验', description: '鸳鸯湖水域游船体验。', salePrice: 50, marketPrice: 60, stock: 45, sold: 326,
highlights: ['湖上游船', '随到随玩'], included: ['鸳鸯湖游船一次'], excluded: ['景区门票'], detail: '请在营业时间内前往鸳鸯湖游船码头核销。', purchaseNotice: '受天气影响时,景区可调整运营安排。',
reservation: { requiresTimeSlot: false, realNameRequired: false, advanceDays: 0 }, entitlements: [ticketEntitlement('boat-experience', '鸳鸯湖游船', 'self', 50)],
id: 'boat', name: '鸳鸯湖游船票', shortTitle: '鸳鸯湖游船票', category: '园内项目', description: '鸳鸯湖游船体验,凭园内权益码由工作人员扫码核销。', salePrice: 50, marketPrice: 60, stock: 480, sold: 326,
highlights: ['官方自营', '当日可订', '未用可退'], included: ['鸳鸯湖游船体验 1 次。'], excluded: ['不含景区门票、观光车及个人消费。'], detail: '乘船游览鸳鸯湖水上森林景观,凭园内权益码由工作人员核销。', purchaseNotice: '受天气和现场运营情况影响,具体以景区通知为准。',
reservation: { requiresTimeSlot: false, realNameRequired: false, advanceDays: 15, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('boat-experience', '鸳鸯湖游船', 'self', 50, '次')],
}),
createProduct({
id: 'guide', name: '小七孔精品讲解服务', shortTitle: '小七孔精品讲解服务', category: '玩乐体验', description: '专业讲解员陪同讲解景区自然与人文风光。', salePrice: 68, marketPrice: 88, stock: 20, sold: 40,
highlights: ['专业讲解', '沉浸式游览'], included: ['60分钟人工讲解服务'], excluded: ['景区门票及交通'], detail: '讲解员将按预约时段在景区入口等候。', purchaseNotice: '请前10分钟到达指定集合点。',
reservation: { advanceDays: 1, realNameRequired: false, maxQuantity: 10, timeSlots: ['09:00—10:30', '13:30—15:00'] }, entitlements: [ticketEntitlement('guide-service', '人工讲解服务', 'self', 68)],
id: 'guide', name: '小七孔精品讲解服务', shortTitle: '小七孔精品讲解服务', category: '园内项目', description: '官方讲解员小团服务,园内权益码核销。', salePrice: 68, marketPrice: 88, stock: 30, sold: 0,
highlights: ['官方讲解', '预约服务'], included: ['精品讲解服务 1 份。'], excluded: ['不含门票、观光车及耳麦以外设备。'], detail: '由官方讲解员提供小团讲解服务,具体集合信息将在订单内展示。', purchaseNotice: '请在预约场次开始前10分钟到达集合点。',
reservation: { advanceDays: 7, realNameRequired: false, idTypes: ['身份证'], maxQuantity: 3, timeSlots: ['09:00—10:30', '13:30—15:00'] }, entitlements: [ticketEntitlement('guide-service', '精品讲解服务', 'self', 68, '份')], status: 'draft', clientVisible: false,
}),
]

View File

@@ -35,17 +35,19 @@ test('provides coherent default visitor ticket data', async () => {
const productsById = new Map(PRODUCTS.map((product) => [product.id, product]))
assert.ok(GROUPS.every((group) => group.productIds.every((productId) => productsById.has(productId))))
assert.deepEqual(GROUPS.flatMap((group) => group.productIds).sort(), PRODUCTS.map(({ id }) => id).sort())
const productKeys = ['id', 'name', 'shortTitle', 'category', 'description', 'salePrice', 'marketPrice', 'stock', 'sold', 'status', 'highlights', 'ageRule', 'included', 'excluded', 'detail', 'purchaseNotice', 'images', 'reservation', 'entitlements']
const productKeys = ['id', 'name', 'shortTitle', 'category', 'description', 'salePrice', 'marketPrice', 'stock', 'sold', 'status', 'clientVisible', 'highlights', 'ageRule', 'included', 'excluded', 'detail', 'purchaseNotice', 'images', 'reservation', 'entitlements']
const reservationKeys = ['requiresDate', 'advanceDays', 'requiresTimeSlot', 'timeSlots', 'realNameRequired', 'idTypes', 'maxQuantity', 'contactPhoneRequired']
const entitlementKeys = ['id', 'name', 'fulfillment', 'quantity', 'unit', 'refundAllocation']
assert.ok(PRODUCTS.every((product) => productKeys.every((key) => key in product) && product.status === 'on_sale' && Number.isFinite(product.salePrice) && Number.isFinite(product.stock) && Number.isFinite(product.sold) && product.images.length === 1 && product.images[0] === PLACEHOLDER_IMAGE && reservationKeys.every((key) => key in product.reservation) && product.entitlements.length && product.entitlements.every((entitlement) => entitlementKeys.every((key) => key in entitlement))))
assert.ok(PRODUCTS.every((product) => productKeys.every((key) => key in product) && ['on_sale', 'draft'].includes(product.status) && Number.isFinite(product.salePrice) && Number.isFinite(product.stock) && Number.isFinite(product.sold) && product.images.length === 1 && product.images[0] === PLACEHOLDER_IMAGE && reservationKeys.every((key) => key in product.reservation) && product.entitlements.length && product.entitlements.every((entitlement) => entitlementKeys.every((key) => key in entitlement))))
assert.deepEqual(PRODUCTS.filter(({ status, clientVisible }) => status !== 'on_sale' || clientVisible === false).map(({ id }) => id), ['guide'])
assert.deepEqual(PRODUCTS.map(({ id, salePrice, marketPrice, sold }) => [id, salePrice, marketPrice, sold]), [
['adult', 160, 170, 2386], ['elderly', 100, 160, 816], ['minor', 100, 160, 629], ['free', 40, 80, 346], ['vip-fast', 98, 128, 126], ['direct-bus', 68, 88, 92], ['classic', 188, 210, 184], ['day-tour', 188, 228, 68], ['boat', 50, 60, 326], ['guide', 68, 88, 40],
['adult', 160, 160, 2386], ['elderly', 100, 100, 816], ['minor', 100, 100, 629], ['free', 40, 40, 346], ['vip-fast', 98, 98, 126], ['direct-bus', 68, 68, 92], ['classic', 188, 210, 184], ['day-tour', 188, 188, 68], ['boat', 50, 60, 326], ['guide', 68, 88, 0],
])
assert.deepEqual(productsById.get('elderly').ageRule, { min: 60, max: 120 })
assert.deepEqual(productsById.get('elderly').ageRule, { min: 60, max: 999 })
assert.deepEqual(productsById.get('minor').ageRule, { min: 6, max: 17 })
assert.deepEqual(productsById.get('free').ageRule, { min: 0, max: 120 })
assert.deepEqual(productsById.get('adult').entitlements.map(({ fulfillment, refundAllocation }) => [fulfillment, refundAllocation]), [['third_party', 120], ['third_party', 40]])
assert.deepEqual(productsById.get('classic').entitlements.map(({ fulfillment, refundAllocation }) => [fulfillment, refundAllocation]), [['third_party', 160], ['self', 28]])
assert.deepEqual(productsById.get('classic').entitlements.map(({ fulfillment, refundAllocation }) => [fulfillment, refundAllocation]), [['third_party', 120], ['third_party', 40], ['self', 28]])
for (const productId of ['vip-fast', 'day-tour', 'boat', 'guide']) assert.ok(productsById.get(productId).entitlements.every(({ fulfillment }) => fulfillment === 'self'))
for (const productId of ['direct-bus', 'free', 'elderly', 'minor']) assert.ok(productsById.get(productId).entitlements.every(({ fulfillment }) => fulfillment === 'third_party'))
const standardSlots = ['08:00—09:30', '09:30—11:30', '11:30—13:30', '13:30—15:00', '15:00—16:00']
@@ -64,17 +66,15 @@ test('provides coherent default visitor ticket data', async () => {
assert.deepEqual(INITIAL_TRAVELERS, [
{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }, { id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020017', phone: '13900139000' },
])
assert.deepEqual(productsById.get('adult'), {
id: 'adult', name: '成人门票+观光车', shortTitle: '成人门票+观光车', category: '景点门票', description: '荔波小七孔景区成人入园门票。', salePrice: 160, marketPrice: 170, stock: 120, sold: 2386, status: 'on_sale', highlights: ['实名预约入园', '覆盖核心景区'], ageRule: null, included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。', images: [PLACEHOLDER_IMAGE], reservation: { requiresDate: true, advanceDays: 1, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照', '港澳居民来往内地通行证'], maxQuantity: 5, contactPhoneRequired: true }, entitlements: [{ id: 'adult-ticket', name: '成人门票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 120 }, { id: 'shuttle', name: '观光车票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 40 }],
})
assert.deepEqual(productsById.get('classic'), {
id: 'classic', name: '门票+观光车+鸳鸯湖游船', shortTitle: '门票+观光车+鸳鸯湖游船', category: '门票+观光车', description: '景区成人门票与观光车组合套餐。', salePrice: 188, marketPrice: 210, stock: 90, sold: 184, status: 'on_sale', highlights: ['门票观光车联票', '比单买更优惠'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车', '鸳鸯湖游船'], excluded: ['餐饮及个人消费'], detail: '一次预约即可享受门票、观光车与鸳鸯湖游船。', purchaseNotice: '各权益可分别核销,未使用部分可按规则申请退款。', images: [PLACEHOLDER_IMAGE], reservation: { requiresDate: true, advanceDays: 1, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证'], maxQuantity: 5, contactPhoneRequired: true }, entitlements: [{ id: 'classic-ticket-shuttle', name: '成人门票+观光车', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 160 }, { id: 'classic-boat', name: '鸳鸯湖游船', fulfillment: 'self', quantity: 1, unit: '张', refundAllocation: 28 }],
})
assert.deepEqual(productsById.get('adult').reservation, { requiresDate: true, advanceDays: 30, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照'], maxQuantity: 5, contactPhoneRequired: true })
assert.deepEqual(productsById.get('adult').highlights, ['官方', '可订明日', '随时退', '无需取票'])
assert.deepEqual(productsById.get('classic').reservation, { requiresDate: true, advanceDays: 30, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照'], maxQuantity: 5, contactPhoneRequired: true })
assert.deepEqual(productsById.get('classic').highlights, ['官方', '实名预约', '整包退', '双凭证'])
assert.deepEqual(INITIAL_TRAVELERS[0], { id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' })
assert.equal(INITIAL_ORDERS.length, 2)
assert.deepEqual(INITIAL_ORDERS.map(({ productId, visitDate, timeSlot, quantity, travelers, insurance, gateCredential, selfCredential, entitlements }) => ({ productId, visitDate, timeSlot, quantity, travelers, insurance, hasGateCredential: Boolean(gateCredential), hasSelfCredential: Boolean(selfCredential), fulfillment: entitlements.map(({ fulfillment }) => fulfillment) })), [
{ productId: 'adult', visitDate: '2026-08-01', timeSlot: '08:00—09:30', quantity: 1, travelers: [{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }], insurance: { name: '出行意外险', price: 5, quantity: 1, status: 'active' }, hasGateCredential: true, hasSelfCredential: false, fulfillment: ['third_party', 'third_party'] },
{ productId: 'classic', visitDate: '2026-08-02', timeSlot: '09:30—11:30', quantity: 1, travelers: [{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }], insurance: null, hasGateCredential: true, hasSelfCredential: true, fulfillment: ['third_party', 'self'] },
{ productId: 'classic', visitDate: '2026-08-02', timeSlot: '09:30—11:30', quantity: 1, travelers: [{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }], insurance: null, hasGateCredential: true, hasSelfCredential: true, fulfillment: ['third_party', 'third_party', 'self'] },
])
for (const order of INITIAL_ORDERS) {
assert.notEqual(order.travelers[0], INITIAL_TRAVELERS[0])
@@ -118,26 +118,26 @@ test('keeps ticket source prototypes, valid traveler IDs, and entitlement alloca
const productsById = new Map(PRODUCTS.map((product) => [product.id, product]))
assert.equal(productsById.get('adult').ageRule, null)
assert.deepEqual(productsById.get('adult').name, '成人票+观光车')
assert.deepEqual(productsById.get('elderly').name, '老年人票+观光车')
assert.deepEqual(productsById.get('minor').name, '未成年票+观光车')
assert.deepEqual(productsById.get('free').name, '免门票+观光车实名预约')
assert.deepEqual(productsById.get('classic').name, '门票+观光车+鸳鸯湖游船')
assert.deepEqual(productsById.get('adult').name, '荔波小七孔成人票+观光车')
assert.deepEqual(productsById.get('elderly').name, '荔波小七孔老年人票+观光车')
assert.deepEqual(productsById.get('minor').name, '荔波小七孔未成年票+观光车')
assert.deepEqual(productsById.get('free').name, '荔波小七孔免门票+观光车')
assert.deepEqual(productsById.get('classic').name, '小七孔经典畅游套餐')
assert.deepEqual(productsById.get('boat').name, '鸳鸯湖游船票')
assert.deepEqual(productsById.get('day-tour').name, '荔波小七孔精品一日游')
assert.deepEqual(productsById.get('guide').name, '小七孔精品讲解服务')
assert.deepEqual(productsById.get('direct-bus').name, '贵阳往返小七孔直通车')
assert.deepEqual(productsById.get('direct-bus').name, '贵阳往返小七孔景区直通车')
assert.deepEqual(productsById.get('elderly').reservation.idTypes, ['身份证'])
assert.deepEqual(productsById.get('minor').reservation.idTypes, ['身份证'])
assert.ok(PRODUCTS.filter((product) => product.ageRule).every((product) => product.reservation.idTypes.length === 1 && product.reservation.idTypes[0] === '身份证'))
assert.ok(productsById.get('direct-bus').included.every((item) => item.includes('往返')))
assert.match(productsById.get('direct-bus').detail, /往返/)
assert.match(productsById.get('direct-bus').purchaseNotice, /往返/)
assert.deepEqual(productsById.get('adult').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['成人门票', 'third_party', 120], ['观光车票', 'third_party', 40]])
assert.match(productsById.get('direct-bus').detail, /乘车凭证/)
assert.match(productsById.get('direct-bus').purchaseNotice, /提前20分钟/)
assert.deepEqual(productsById.get('adult').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['小七孔成人门票', 'third_party', 120], ['小七孔观光车票', 'third_party', 40]])
assert.deepEqual(productsById.get('elderly').entitlements.map(({ refundAllocation }) => refundAllocation), [60, 40])
assert.deepEqual(productsById.get('minor').entitlements.map(({ refundAllocation }) => refundAllocation), [60, 40])
assert.deepEqual(productsById.get('free').entitlements.map(({ refundAllocation }) => refundAllocation), [0, 40])
assert.deepEqual(productsById.get('classic').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['成人门票+观光车', 'third_party', 160], ['鸳鸯湖游船', 'self', 28]])
assert.deepEqual(productsById.get('classic').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['小七孔成人门票', 'third_party', 120], ['观光车', 'third_party', 40], ['鸳鸯湖游船', 'self', 28]])
assert.ok(PRODUCTS.every((product) => product.entitlements.every((entitlement) => Number.isFinite(entitlement.quantity) && entitlement.quantity > 0 && Number.isFinite(entitlement.refundAllocation)) && product.entitlements.reduce((total, entitlement) => total + entitlement.refundAllocation, 0) === product.salePrice))
assert.deepEqual(INITIAL_TRAVELERS[1], { id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020017', phone: '13900139000' })
})