feat: 页面调整

This commit is contained in:
2026-07-30 17:49:55 +08:00
parent a4e58a780a
commit 7081420384
29 changed files with 946 additions and 432 deletions

View File

@@ -12,13 +12,21 @@
<!-- 中间标题区域 -->
<view :class="[
'nav-bar-center flex items-center justify-center',
'nav-bar-center flex min-h-[30px] items-center justify-center',
`nav-bar-center--${titleAlign}`,
]">
<slot name="title">
<text class="text-[17px] font-[500] text-[#000000]" :style="{ color: titleColor }">
{{ title }}
</text>
<view
class="flex w-full min-w-0 flex-col justify-center"
:class="titleContentClass"
>
<text class="max-w-full truncate text-[17px] font-[500] leading-[21px] text-[#000000]" :style="{ color: titleColor }">
{{ title }}
</text>
<text v-if="subtitle" class="mt-[1px] max-w-full truncate text-[11px] leading-[14px]" :style="{ color: subtitleColor }">
{{ subtitle }}
</text>
</view>
</slot>
</view>
@@ -40,6 +48,10 @@ const props = defineProps({
type: String,
default: "",
},
subtitle: {
type: String,
default: "",
},
// 是否固定在顶部
fixed: {
type: Boolean,
@@ -60,6 +72,10 @@ const props = defineProps({
type: String,
default: "#000",
},
subtitleColor: {
type: String,
default: "#667085",
},
// 返回按钮图标颜色
backIconColor: {
type: String,
@@ -141,10 +157,16 @@ const navBarStyle = computed(() => {
const navContentStyle = computed(() => {
return {
height: navBarHeight.value + "px",
height: navBarHeight.value + (props.subtitle ? 12 : 0) + "px",
};
});
const titleContentClass = computed(() =>
props.titleAlign === "left"
? "items-start text-left"
: "items-center text-center"
);
const hasBackListener = computed(() => {
const vnodeProps = instance?.vnode?.props || {};
return Boolean(vnodeProps.onBack);

View File

@@ -20,7 +20,6 @@
.nav-bar-center {
flex: 1;
height: 30px;
padding: 0 20px; // 为左右按钮留出空间
// 居中对齐(默认)

View File

@@ -0,0 +1,72 @@
<template>
<view class="flex h-screen min-w-0 flex-col overflow-hidden bg-slate-100">
<TopNavBar
:title="paid ? '支付结果' : '预订选择'"
:subtitle="paid ? '订单已支付,可查看凭证' : '确认日期、出行人与数量'"
title-align="left"
background="#ffffff"
/>
<PaymentResult
v-if="paid"
class="min-h-0 flex-1"
:order="ticketStore.currentOrder"
@home="goHome"
@orders="goOrders"
@detail="goOrderDetail"
@credentials="goCredentials"
/>
<BookingFlow
v-else
class="min-h-0 flex-1"
:product="ticketStore.currentProduct"
:draft="ticketStore.bookingDraft"
:travelers="ticketStore.travelers"
:available-dates="AVAILABLE_DATES"
:insurance="INSURANCE"
@update-draft="ticketStore.updateBookingDraft"
@open-travelers="openTravelers"
@submit="submitBooking"
/>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import TopNavBar from "@/components/TopNavBar/index.vue";
import BookingFlow from "./components/BookingFlow.vue";
import PaymentResult from "./components/PaymentResult.vue";
import { AVAILABLE_DATES, INSURANCE } from "./data.js";
import { useTicketStore } from "./store.js";
const ticketStore = useTicketStore();
const paid = ref(false);
onLoad((options) => {
ticketStore.startBooking(options?.productId);
});
const submitBooking = () => {
const result = ticketStore.submitBooking();
if (result.error) {
uni.showToast({ title: result.error, icon: "none" });
return;
}
paid.value = true;
};
const openTravelers = () =>
uni.navigateTo({ url: "/pages-service/tickets/travelers?select=1" });
const goHome = () =>
uni.reLaunch({ url: "/pages-service/tickets/index" });
const goOrders = () =>
uni.redirectTo({ url: "/pages-service/tickets/orders" });
const goOrderDetail = () =>
uni.redirectTo({
url: `/pages-service/tickets/order-detail?orderId=${ticketStore.currentOrderId}`,
});
const goCredentials = () =>
uni.redirectTo({
url: `/pages-service/tickets/credentials?orderId=${ticketStore.currentOrderId}`,
});
</script>

View File

@@ -1,12 +1,7 @@
<template>
<view class="flex h-screen flex-col bg-slate-100 text-slate-900">
<view class="flex shrink-0 items-center bg-white px-4 pb-3 pt-10">
<button class="m-0 mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 p-0 text-lg leading-none" @tap="emit('back')"></button>
<text class="text-lg font-semibold">预订选择</text>
</view>
<view class="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">
<view class="space-y-3 px-3 py-3 pb-36">
<view class="box-border w-full min-w-0 space-y-3 px-3 py-3 pb-36">
<view class="flex rounded-2xl bg-white p-3 shadow-sm">
<image class="h-20 w-20 shrink-0 rounded-xl bg-slate-200" :src="product.images?.[0]" mode="aspectFill" />
<view class="ml-3 flex min-w-0 flex-1 flex-col justify-between">
@@ -110,7 +105,7 @@
</view>
</scroll-view>
<view class="flex shrink-0 items-center bg-white px-4 py-3 shadow-[0_-2px_10px_rgba(15,23,42,0.08)]">
<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="min-w-0 flex-1">
<text class="block text-xs text-slate-500">商品¥{{ totals.productAmount }}{{ draft.insuranceSelected ? ` + 保险¥${totals.insuranceAmount}` : '' }}</text>
<text class="mt-1 block text-lg font-semibold text-rose-500">合计 ¥{{ totals.paidAmount }}</text>
@@ -132,7 +127,7 @@ const props = defineProps({
insurance: { type: Object, required: true },
})
const emit = defineEmits(['back', 'update-draft', 'open-travelers', 'submit'])
const emit = defineEmits(['update-draft', 'open-travelers', 'submit'])
const maxQuantity = computed(() => Number(props.product.reservation?.maxQuantity) || 1)
const quantity = computed(() => Math.min(Math.max(Number(props.draft.quantity) || 1, 1), maxQuantity.value))

View File

@@ -1,24 +1,39 @@
<template>
<view class="flex h-screen flex-col bg-[#f8fafc] pt-[env(safe-area-inset-top)]">
<view class="flex items-center bg-white px-[16px] py-[14px]"><view class="flex h-[36px] w-[36px] items-center justify-center" @click="emit('back')"><uni-icons type="left" size="20" color="#171717" /></view><text class="ml-[4px] text-[20px] font-semibold text-[#171717]">我的凭证</text></view>
<scroll-view scroll-y class="min-h-0 flex-1 px-[16px] py-[16px]">
<view class="rounded-[16px] bg-white p-[16px]"><text class="block text-[16px] font-medium text-[#171717]">{{ order.productName || '景区产品' }}</text><text class="mt-[10px] block text-[13px] text-[#667085]">订单号 {{ order.orderNo || order.id || '-' }}</text><text class="mt-[8px] block text-[14px] text-[#475467]">{{ order.visitDate || '-' }} {{ order.timeSlot || '全天' }}</text><text class="mt-[6px] block text-[14px] text-[#475467]">出行人 {{ firstTraveler }}</text></view>
<view class="flex h-full min-w-0 flex-col overflow-hidden bg-[#f8fafc]">
<scroll-view scroll-y class="min-h-0 min-w-0 flex-1">
<view class="box-border w-full min-w-0 px-[16px] py-[16px]">
<view class="box-border w-full min-w-0 overflow-hidden rounded-[16px] bg-white p-[16px]">
<text class="block break-words text-[16px] font-medium text-[#171717]">{{ order.productName || '景区产品' }}</text>
<view class="mt-[10px] flex min-w-0 text-[13px] text-[#667085]"><text class="shrink-0">订单号</text><text class="ml-[8px] min-w-0 flex-1 break-all text-right">{{ order.orderNo || order.id || '-' }}</text></view>
<text class="mt-[8px] block break-words text-[14px] text-[#475467]">{{ order.visitDate || '-' }} {{ order.timeSlot || '全天' }}</text>
<text class="mt-[6px] block break-words text-[14px] text-[#475467]">出行人 {{ firstTraveler }}</text>
</view>
<view v-if="order.gateCredential" class="mt-[12px] overflow-hidden rounded-[18px] bg-[#5b21b6] p-[18px]">
<text class="text-[18px] font-semibold text-white">第三方票务凭证</text><text class="mt-[6px] block text-[13px] text-[#ddd6fe]">请向检票人员出示此二维码</text>
<view class="mt-[16px] flex rounded-[12px] bg-white p-[14px]"><view class="grid h-[162px] w-[162px] grid-cols-9 grid-rows-9 border border-[#5b21b6] p-[4px]"><view v-for="(cell, index) in gateGrid" :key="index" class="h-full w-full" :class="cell ? 'bg-[#5b21b6]' : 'bg-white'" /></view><view class="ml-[16px] flex flex-1 flex-col justify-center"><text class="text-[13px] text-[#667085]">核验状态</text><text class="mt-[5px] text-[16px] font-medium text-[#15803d]">{{ order.gateCredential.status === 'issued' ? '可使用' : '处理中' }}</text></view></view>
<view class="mt-[16px] border-t border-[#7c3aed] pt-[12px]"><text v-for="right in gateRights" :key="right.id" class="mb-[6px] block text-[13px] text-[#ede9fe]">{{ right.name }} × {{ right.quantity || 0 }}</text><text class="mt-[6px] block text-[12px] leading-[18px] text-[#ddd6fe]">请在预约日期和时段内完成核验截图可能无法正常使用</text></view>
</view>
<view v-if="order.gateCredential" class="mt-[12px] box-border w-full min-w-0 overflow-hidden rounded-[18px] bg-[#5b21b6] p-[18px]">
<text class="text-[18px] font-semibold text-white">第三方票务凭证</text>
<text class="mt-[6px] block text-[13px] text-[#ddd6fe]">请向检票人员出示此二维码</text>
<view class="mt-[16px] box-border flex w-full min-w-0 flex-col items-center rounded-[12px] bg-white p-[14px]">
<view class="grid h-[176px] w-[176px] max-w-full grid-cols-9 grid-rows-9 border border-[#5b21b6] p-[4px]"><view v-for="(cell, index) in gateGrid" :key="index" class="h-full w-full" :class="cell ? 'bg-[#5b21b6]' : 'bg-white'" /></view>
<view class="mt-[12px] flex min-w-0 items-center justify-center"><text class="text-[13px] text-[#667085]">核验状态</text><text class="ml-[8px] text-[16px] font-medium text-[#15803d]">{{ order.gateCredential.status === 'issued' ? '可使用' : '处理中' }}</text></view>
</view>
<view class="mt-[16px] min-w-0 border-t border-[#7c3aed] pt-[12px]"><text v-for="right in gateRights" :key="right.id" class="mb-[6px] block break-words text-[13px] text-[#ede9fe]">{{ right.name }} × {{ right.quantity || 0 }}</text><text class="mt-[6px] block text-[12px] leading-[18px] text-[#ddd6fe]">请在预约日期和时段内完成核验截图可能无法正常使用</text></view>
</view>
<view v-if="order.selfCredential" class="mt-[12px] overflow-hidden rounded-[18px] border border-[#bfdbfe] bg-[#eff6ff] p-[18px]">
<text class="text-[18px] font-semibold text-[#1e3a8a]">自营园内权益</text><text class="mt-[6px] block text-[13px] text-[#3b82f6]">请向服务人员出示此二维码</text>
<view class="mt-[16px] flex rounded-[12px] bg-white p-[14px]"><view class="grid h-[162px] w-[162px] grid-cols-9 grid-rows-9 border border-[#1d4ed8] p-[4px]"><view v-for="(cell, index) in selfGrid" :key="index" class="h-full w-full" :class="cell ? 'bg-[#1d4ed8]' : 'bg-white'" /></view><view class="ml-[16px] flex flex-1 flex-col justify-center"><text class="text-[13px] text-[#667085]">核验状态</text><text class="mt-[5px] text-[16px] font-medium text-[#15803d]">{{ order.selfCredential.status === 'issued' ? '可使用' : '处理中' }}</text></view></view>
<view class="mt-[16px] border-t border-[#bfdbfe] pt-[12px]"><text v-for="right in selfRights" :key="right.id" class="mb-[6px] block text-[13px] text-[#1e3a8a]">{{ right.name }} × {{ right.quantity || 0 }}</text><text class="mt-[6px] block text-[12px] leading-[18px] text-[#3b82f6]">请在服务点出示二维码权益核销后不可再次使用</text></view>
<view v-if="order.selfCredential" class="mt-[12px] box-border w-full min-w-0 overflow-hidden rounded-[18px] border border-[#bfdbfe] bg-[#eff6ff] p-[18px]">
<text class="text-[18px] font-semibold text-[#1e3a8a]">自营园内权益</text>
<text class="mt-[6px] block text-[13px] text-[#3b82f6]">请向服务人员出示此二维码</text>
<view class="mt-[16px] box-border flex w-full min-w-0 flex-col items-center rounded-[12px] bg-white p-[14px]">
<view class="grid h-[176px] w-[176px] max-w-full grid-cols-9 grid-rows-9 border border-[#1d4ed8] p-[4px]"><view v-for="(cell, index) in selfGrid" :key="index" class="h-full w-full" :class="cell ? 'bg-[#1d4ed8]' : 'bg-white'" /></view>
<view class="mt-[12px] flex min-w-0 items-center justify-center"><text class="text-[13px] text-[#667085]">核验状态</text><text class="ml-[8px] text-[16px] font-medium text-[#15803d]">{{ order.selfCredential.status === 'issued' ? '可使用' : '处理中' }}</text></view>
</view>
<view class="mt-[16px] min-w-0 border-t border-[#bfdbfe] pt-[12px]"><text v-for="right in selfRights" :key="right.id" class="mb-[6px] block break-words text-[13px] text-[#1e3a8a]">{{ right.name }} × {{ right.quantity || 0 }}</text><text class="mt-[6px] block text-[12px] leading-[18px] text-[#3b82f6]">请在服务点出示二维码权益核销后不可再次使用</text></view>
</view>
<view v-if="!order.gateCredential && !order.selfCredential" class="flex min-h-[260px] min-w-0 flex-col items-center justify-center"><uni-icons type="info" size="34" color="#98a2b3" /><text class="mt-[12px] text-[14px] text-[#98a2b3]">暂无可用凭证</text></view>
<view class="h-[24px]" />
</view>
<view v-if="!order.gateCredential && !order.selfCredential" class="flex min-h-[260px] flex-col items-center justify-center"><uni-icons type="info" size="34" color="#98a2b3" /><text class="mt-[12px] text-[14px] text-[#98a2b3]">暂无可用凭证</text></view>
<view class="h-[80px]" />
</scroll-view>
<view class="bg-white px-[16px] py-[12px] pb-[calc(env(safe-area-inset-bottom)+12px)]"><button class="w-full rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="emit('detail')">查看订单详情</button></view>
<view class="box-border w-full min-w-0 bg-white px-[16px] py-[12px] pb-[calc(env(safe-area-inset-bottom)+12px)]"><button class="box-border w-full min-w-0 rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="emit('detail')">查看订单详情</button></view>
</view>
</template>
@@ -26,7 +41,7 @@
import { computed } from 'vue'
const props = defineProps({ order: { type: Object, required: true } })
const emit = defineEmits(['back', 'detail'])
const emit = defineEmits(['detail'])
const firstTraveler = computed(() => props.order?.travelers?.[0]?.name || '未填写出行人')
const gateRights = computed(() => (props.order?.entitlements || []).filter((right) => right.fulfillment === 'third_party'))
const selfRights = computed(() => (props.order?.entitlements || []).filter((right) => right.fulfillment === 'self'))

View File

@@ -1,62 +1,64 @@
<template>
<view class="flex h-screen flex-col bg-[#f8fafc] pt-[env(safe-area-inset-top)]">
<view class="flex items-center bg-white px-[16px] py-[14px]">
<view class="flex h-[36px] w-[36px] items-center justify-center" @click="emit('back')">
<uni-icons type="left" size="20" color="#171717" />
</view>
<text class="ml-[4px] text-[20px] font-semibold text-[#171717]">订单详情</text>
</view>
<scroll-view scroll-y class="min-h-0 flex-1 px-[16px] py-[16px]">
<view class="rounded-[16px] bg-white p-[16px]">
<view class="flex items-center justify-between">
<text class="text-[16px] font-medium text-[#171717]">{{ order.productName || '景区产品' }}</text>
<text class="rounded-[12px] bg-[#ecfdf3] px-[8px] py-[3px] text-[12px] text-[#15803d]">{{ statusText }}</text>
<view class="flex h-full min-w-0 flex-col overflow-hidden bg-[#f8fafc]">
<scroll-view scroll-y class="min-h-0 min-w-0 flex-1">
<view class="box-border w-full min-w-0 px-[16px] py-[16px]">
<view class="box-border w-full min-w-0 overflow-hidden rounded-[16px] bg-white p-[16px]">
<view class="flex min-w-0 items-start justify-between gap-[10px]">
<text class="min-w-0 flex-1 break-words text-[16px] font-medium leading-[22px] text-[#171717]">{{ order.productName || '景区产品' }}</text>
<text class="shrink-0 rounded-[12px] bg-[#ecfdf3] px-[8px] py-[3px] text-[12px] text-[#15803d]">{{ statusText }}</text>
</view>
<view class="mt-[12px] flex min-w-0 items-start text-[13px] text-[#667085]">
<text class="shrink-0">订单号</text>
<text class="ml-[8px] min-w-0 flex-1 break-all text-right">{{ order.orderNo || order.id || '-' }}</text>
</view>
</view>
<text class="mt-[12px] block text-[13px] text-[#667085]">订单号 {{ order.orderNo || order.id || '-' }}</text>
</view>
<view class="mt-[12px] rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">使用信息</text>
<view class="mt-[14px] flex justify-between text-[14px]"><text class="text-[#667085]">使用日期</text><text class="text-[#344054]">{{ order.visitDate || '-' }}</text></view>
<view class="mt-[12px] flex justify-between text-[14px]"><text class="text-[#667085]">使用时段</text><text class="text-[#344054]">{{ order.timeSlot || '全天' }}</text></view>
</view>
<view class="mt-[12px] rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">出行人</text>
<view v-for="traveler in order.travelers || []" :key="traveler.id || traveler.name" class="mt-[14px] flex justify-between text-[14px]">
<text class="text-[#344054]">{{ traveler.name || '-' }}</text><text class="text-[#667085]">{{ traveler.idType || '游客' }}</text>
<view class="mt-[12px] box-border w-full min-w-0 rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">使用信息</text>
<view class="mt-[14px] flex min-w-0 justify-between gap-[12px] text-[14px]"><text class="shrink-0 text-[#667085]">使用日期</text><text class="min-w-0 break-words text-right text-[#344054]">{{ order.visitDate || '-' }}</text></view>
<view class="mt-[12px] flex min-w-0 justify-between gap-[12px] text-[14px]"><text class="shrink-0 text-[#667085]">使用时段</text><text class="min-w-0 break-words text-right text-[#344054]">{{ order.timeSlot || '全天' }}</text></view>
</view>
<view v-if="!(order.travelers || []).length" class="mt-[14px] text-[14px] text-[#98a2b3]">暂无出行人信息</view>
<view class="mt-[14px] border-t border-[#f2f4f7] pt-[14px] text-[14px]"><text class="text-[#667085]">联系人手机 </text><text class="text-[#344054]">{{ order.phone || '-' }}</text></view>
</view>
<view class="mt-[12px] rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">权益与使用状态</text>
<view v-for="right in order.entitlements || []" :key="right.id" class="mt-[14px] flex items-center justify-between border-b border-[#f2f4f7] pb-[12px] last:border-b-0 last:pb-0">
<view><text class="block text-[14px] text-[#344054]">{{ right.name }}</text><text class="mt-[4px] block text-[12px] text-[#98a2b3]">{{ fulfillmentText(right.fulfillment) }}</text></view>
<text class="text-[13px] text-[#667085]">{{ right.quantity || 0 }} · {{ rightStatus(right.status) }}</text>
<view class="mt-[12px] box-border w-full min-w-0 rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">出行人</text>
<view v-for="traveler in order.travelers || []" :key="traveler.id || traveler.name" class="mt-[14px] flex min-w-0 justify-between gap-[12px] text-[14px]">
<text class="min-w-0 flex-1 break-words text-[#344054]">{{ traveler.name || '-' }}</text>
<text class="shrink-0 text-[#667085]">{{ traveler.idType || '游客' }}</text>
</view>
<view v-if="!(order.travelers || []).length" class="mt-[14px] text-[14px] text-[#98a2b3]">暂无出行人信息</view>
<view class="mt-[14px] flex min-w-0 border-t border-[#f2f4f7] pt-[14px] text-[14px]">
<text class="shrink-0 text-[#667085]">联系人手机</text>
<text class="ml-[12px] min-w-0 flex-1 break-all text-right text-[#344054]">{{ order.phone || '-' }}</text>
</view>
</view>
</view>
<view class="mt-[12px] rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">费用信息</text>
<view class="mt-[14px] flex justify-between text-[14px]"><text class="text-[#667085]">产品金额</text><text class="text-[#344054]">¥{{ money(order.productAmount) }}</text></view>
<view class="mt-[12px] flex justify-between text-[14px]"><text class="text-[#667085]">保险金额</text><text class="text-[#344054]">¥{{ money(order.insuranceAmount) }}</text></view>
<view class="mt-[12px] flex justify-between border-t border-[#f2f4f7] pt-[12px] text-[14px]"><text class="text-[#667085]">实付金额</text><text class="font-semibold text-[#ef4444]">¥{{ money(order.paidAmount) }}</text></view>
<view class="mt-[12px] flex justify-between text-[13px]"><text class="text-[#667085]">支付时间</text><text class="text-[#344054]">{{ order.paidAt || '-' }}</text></view>
</view>
<view class="mt-[12px] box-border w-full min-w-0 rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">权益与使用状态</text>
<view v-for="right in order.entitlements || []" :key="right.id" class="mt-[14px] flex min-w-0 items-center justify-between gap-[12px] border-b border-[#f2f4f7] pb-[12px] last:border-b-0 last:pb-0">
<view class="min-w-0 flex-1"><text class="block break-words text-[14px] text-[#344054]">{{ right.name }}</text><text class="mt-[4px] block text-[12px] text-[#98a2b3]">{{ fulfillmentText(right.fulfillment) }}</text></view>
<text class="shrink-0 text-right text-[13px] text-[#667085]">{{ right.quantity || 0 }} · {{ rightStatus(right.status) }}</text>
</view>
</view>
<view class="mt-[12px] rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">退款规则</text>
<text class="mt-[10px] block text-[13px] leading-[20px] text-[#667085]">未使用的权益可按产品退款规则申请退款已核销权益不支持退款实际退款以审核结果为准</text>
<view class="mt-[12px] box-border w-full min-w-0 rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">费用信息</text>
<view class="mt-[14px] flex justify-between gap-[12px] text-[14px]"><text class="text-[#667085]">产品金额</text><text class="text-right text-[#344054]">¥{{ money(order.productAmount) }}</text></view>
<view class="mt-[12px] flex justify-between gap-[12px] text-[14px]"><text class="text-[#667085]">保险金额</text><text class="text-right text-[#344054]">¥{{ money(order.insuranceAmount) }}</text></view>
<view class="mt-[12px] flex justify-between gap-[12px] border-t border-[#f2f4f7] pt-[12px] text-[14px]"><text class="text-[#667085]">实付金额</text><text class="font-semibold text-[#ef4444]">¥{{ money(order.paidAmount) }}</text></view>
<view class="mt-[12px] flex min-w-0 justify-between gap-[12px] text-[13px]"><text class="shrink-0 text-[#667085]">支付时间</text><text class="min-w-0 break-words text-right text-[#344054]">{{ order.paidAt || '-' }}</text></view>
</view>
<view class="mt-[12px] box-border w-full min-w-0 rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">退款规则</text>
<text class="mt-[10px] block text-[13px] leading-[20px] text-[#667085]">未使用的权益可按产品退款规则申请退款已核销权益不支持退款实际退款以审核结果为准</text>
</view>
<view class="h-[24px]" />
</view>
<view class="h-[92px]" />
</scroll-view>
<view class="flex gap-[12px] bg-white px-[16px] py-[12px] pb-[calc(env(safe-area-inset-bottom)+12px)]">
<button v-if="hasCredential" class="m-0 flex-1 rounded-[24px] border border-[#7c3aed] bg-white p-0 text-[15px] leading-[48px] text-[#6d28d9]" @click="emit('credentials')">查看凭证</button>
<button v-if="canRefund" class="m-0 flex-1 rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="emit('refund')">申请退款</button>
<view class="box-border flex w-full min-w-0 gap-[12px] bg-white px-[16px] py-[12px] pb-[calc(env(safe-area-inset-bottom)+12px)]">
<button v-if="hasCredential" class="m-0 min-w-0 flex-1 rounded-[24px] border border-[#7c3aed] bg-white p-0 text-[15px] leading-[48px] text-[#6d28d9]" @click="emit('credentials')">查看凭证</button>
<button v-if="canRefund" class="m-0 min-w-0 flex-1 rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="emit('refund')">申请退款</button>
</view>
</view>
</template>
@@ -65,7 +67,7 @@
import { computed } from 'vue'
const props = defineProps({ order: { type: Object, required: true } })
const emit = defineEmits(['back', 'credentials', 'refund'])
const emit = defineEmits(['credentials', 'refund'])
const canRefund = computed(() => (props.order?.entitlements || []).some((right) => right.status === 'pending'))
const hasCredential = computed(() => Boolean(props.order?.gateCredential || props.order?.selfCredential))
const money = (value) => Number(value || 0).toFixed(2)

View File

@@ -1,43 +1,35 @@
<template>
<view class="flex h-screen flex-col bg-[#f8fafc] pt-[env(safe-area-inset-top)]">
<view class="flex items-center bg-white px-[16px] py-[14px]">
<view class="flex h-[36px] w-[36px] items-center justify-center" @click="emit('back')">
<uni-icons type="left" size="20" color="#171717" />
</view>
<view class="ml-[4px]">
<text class="block text-[20px] font-semibold text-[#171717]">我的订单</text>
<text class="mt-[2px] block text-[12px] text-[#667085]">购买凭证与退款进度</text>
</view>
</view>
<scroll-view scroll-y class="min-h-0 flex-1 px-[16px] py-[16px]">
<view v-if="orders.length" class="flex flex-col gap-[12px]">
<view v-for="order in orders" :key="order.id || order.orderNo" class="rounded-[16px] bg-white p-[16px]" @click="emit('open', order.id)">
<view class="flex items-start justify-between gap-[12px]">
<text class="flex-1 text-[16px] font-medium leading-[23px] text-[#171717]">{{ order.productName || '景区产品' }}</text>
<view class="flex h-full min-w-0 flex-col overflow-hidden bg-[#f8fafc]">
<scroll-view scroll-y class="min-h-0 min-w-0 flex-1">
<view class="box-border w-full min-w-0 px-[16px] py-[16px]">
<view v-if="orders.length" class="flex min-w-0 flex-col gap-[12px]">
<view v-for="order in orders" :key="order.id || order.orderNo" class="box-border w-full min-w-0 overflow-hidden rounded-[16px] bg-white p-[16px]" @click="emit('open', order.id)">
<view class="flex min-w-0 items-start justify-between gap-[12px]">
<text class="min-w-0 flex-1 break-words text-[16px] font-medium leading-[23px] text-[#171717]">{{ order.productName || '景区产品' }}</text>
<text v-if="orderStatus(order) === 'paid'" class="shrink-0 rounded-[12px] bg-[#ecfdf3] px-[8px] py-[3px] text-[12px] text-[#15803d]">待使用</text>
<text v-else-if="orderStatus(order) === 'refunding'" class="shrink-0 rounded-[12px] bg-[#fff7ed] px-[8px] py-[3px] text-[12px] text-[#c2410c]">退款中</text>
<text v-else-if="orderStatus(order) === 'completed'" class="shrink-0 rounded-[12px] bg-[#eff6ff] px-[8px] py-[3px] text-[12px] text-[#2563eb]">已完成</text>
<text v-else class="shrink-0 rounded-[12px] bg-[#f2f4f7] px-[8px] py-[3px] text-[12px] text-[#667085]">已退款</text>
</view>
<text class="mt-[12px] block text-[14px] text-[#475467]">{{ order.visitDate || '-' }} {{ order.timeSlot || '全天' }}</text>
<view class="mt-[10px] flex items-center gap-[8px] text-[13px] text-[#667085]">
<text>{{ order.quantity || 0 }}</text>
<text>{{ firstTraveler(order) }}</text>
<text v-if="order.insurance" class="rounded-[4px] bg-[#eff6ff] px-[6px] py-[2px] text-[11px] text-[#2563eb]">含保险</text>
</view>
<view class="mt-[14px] flex items-center justify-between border-t border-[#f2f4f7] pt-[12px]">
<text class="max-w-[200px] truncate text-[12px] text-[#98a2b3]">订单号 {{ order.orderNo || order.id || '-' }}</text>
<text class="text-[17px] font-semibold text-[#ef4444]">¥{{ money(order.paidAmount) }}</text>
</view>
<text class="mt-[12px] block text-[14px] text-[#475467]">{{ order.visitDate || '-' }} {{ order.timeSlot || '全天' }}</text>
<view class="mt-[10px] flex min-w-0 flex-wrap items-center gap-[8px] text-[13px] text-[#667085]">
<text>{{ order.quantity || 0 }}</text>
<text>{{ firstTraveler(order) }}</text>
<text v-if="order.insurance" class="rounded-[4px] bg-[#eff6ff] px-[6px] py-[2px] text-[11px] text-[#2563eb]">含保险</text>
</view>
<view class="mt-[14px] flex min-w-0 items-center justify-between gap-[10px] border-t border-[#f2f4f7] pt-[12px]">
<text class="min-w-0 flex-1 truncate text-[12px] text-[#98a2b3]">订单号 {{ order.orderNo || order.id || '-' }}</text>
<text class="shrink-0 text-[17px] font-semibold text-[#ef4444]">¥{{ money(order.paidAmount) }}</text>
</view>
</view>
</view>
</view>
<view v-else class="flex min-h-[420px] flex-col items-center justify-center">
<view class="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-[#eef2ff]">
<uni-icons type="list" size="34" color="#7c3aed" />
<view v-else class="flex min-h-[420px] min-w-0 flex-col items-center justify-center text-center">
<view class="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-[#eef2ff]">
<uni-icons type="list" size="34" color="#7c3aed" />
</view>
<text class="mt-[16px] text-[16px] font-medium text-[#344054]">暂无订单</text>
<text class="mt-[8px] text-[13px] text-[#98a2b3]">购买后可在这里查看凭证和退款进度</text>
</view>
<text class="mt-[16px] text-[16px] font-medium text-[#344054]">暂无订单</text>
<text class="mt-[8px] text-[13px] text-[#98a2b3]">购买后可在这里查看凭证和退款进度</text>
</view>
</scroll-view>
</view>
@@ -45,7 +37,7 @@
<script setup>
defineProps({ orders: { type: Array, default: () => [] } })
const emit = defineEmits(['back', 'open'])
const emit = defineEmits(['open'])
const money = (value) => Number(value || 0).toFixed(2)
const firstTraveler = (order) => order?.travelers?.[0]?.name || '未填写出行人'

View File

@@ -1,5 +1,5 @@
<template>
<view class="flex h-screen flex-col bg-white px-[20px] pt-[calc(env(safe-area-inset-top)+32px)] pb-[calc(env(safe-area-inset-bottom)+20px)]">
<view class="box-border flex h-full min-w-0 flex-col bg-white px-[20px] pb-[calc(env(safe-area-inset-bottom)+20px)] pt-[24px]">
<view class="flex flex-1 flex-col items-center overflow-y-auto">
<view class="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-[#dcfce7]">
<uni-icons type="checkmarkempty" size="46" color="#16a34a" />
@@ -7,7 +7,7 @@
<text class="mt-[16px] text-[24px] font-semibold text-[#171717]">支付成功</text>
<text class="mt-[8px] text-[14px] text-[#667085]">请按预约时间使用您的权益</text>
<view class="mt-[28px] w-full rounded-[16px] bg-[#f8fafc] p-[16px]">
<view class="mt-[28px] box-border w-full min-w-0 rounded-[16px] bg-[#f8fafc] p-[16px]">
<text class="block text-[17px] font-medium text-[#171717]">{{ order.productName || '景区产品' }}</text>
<view class="mt-[16px] flex items-center justify-between text-[14px]">
<text class="text-[#667085]">使用日期</text>
@@ -19,7 +19,7 @@
</view>
<view class="mt-[12px] flex items-center justify-between text-[14px]">
<text class="text-[#667085]">订单号</text>
<text class="max-w-[220px] truncate text-[#344054]">{{ order.orderNo || order.id || '-' }}</text>
<text class="ml-[12px] min-w-0 break-all text-right text-[#344054]">{{ order.orderNo || order.id || '-' }}</text>
</view>
<view class="mt-[16px] flex items-end justify-between border-t border-[#e4e7ec] pt-[16px]">
<text class="text-[14px] text-[#667085]">实付金额</text>
@@ -28,7 +28,7 @@
</view>
</view>
<view class="mt-[16px] grid w-full 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 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 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>

View File

@@ -1,14 +1,12 @@
<template>
<view class="h-screen overflow-hidden bg-slate-50 flex flex-col">
<view class="shrink-0 bg-white px-[16px] pb-[12px] pt-[calc(env(safe-area-inset-top)+12px)]">
<view class="flex h-[32px] items-center justify-between">
<view class="flex h-[32px] w-[32px] items-center justify-center" @click="emit('back')">
<uni-icons type="left" size="22" color="#1f2937" />
</view>
<text class="text-[17px] font-semibold text-slate-800">商品详情</text>
<view class="h-[32px] w-[32px]" />
</view>
</view>
<TopNavBar
title="商品详情"
subtitle="费用说明与购买须知"
title-align="left"
background="#ffffff"
@back="emit('back')"
/>
<scroll-view class="min-h-0 flex-1" scroll-y>
<image class="h-[280px] w-full bg-slate-200" :src="product.images && product.images[0]" mode="aspectFill" />
@@ -66,6 +64,8 @@
</template>
<script setup>
import TopNavBar from "@/components/TopNavBar/index.vue"
defineProps({
product: { type: Object, required: true },
})

View File

@@ -1,33 +1,44 @@
<template>
<view v-if="submittedRefund" class="flex h-screen flex-col items-center justify-center bg-white px-[28px] pb-[env(safe-area-inset-bottom)]">
<view v-if="submittedRefund" class="box-border flex h-full min-w-0 flex-col items-center justify-center bg-white px-[28px] pb-[env(safe-area-inset-bottom)]">
<view class="flex h-[76px] w-[76px] items-center justify-center rounded-full bg-[#dcfce7]"><uni-icons type="checkmarkempty" size="48" color="#16a34a" /></view>
<text class="mt-[18px] text-[23px] font-semibold text-[#171717]">退款申请已提交</text>
<text class="mt-[10px] text-[14px] text-[#667085]">退款金额 ¥{{ money(submittedRefund.amount) }}</text>
<view class="mt-[24px] w-full rounded-[16px] bg-[#f8fafc] p-[16px] text-[14px]">
<view class="flex justify-between"><text class="text-[#667085]">订单号</text><text class="max-w-[210px] truncate text-[#344054]">{{ order.orderNo || order.id || '-' }}</text></view>
<view class="mt-[24px] box-border w-full min-w-0 rounded-[16px] bg-[#f8fafc] p-[16px] text-[14px]">
<view class="flex min-w-0 justify-between gap-[12px]"><text class="shrink-0 text-[#667085]">订单号</text><text class="min-w-0 flex-1 break-all text-right text-[#344054]">{{ order.orderNo || order.id || '-' }}</text></view>
<view class="mt-[12px] flex justify-between"><text class="text-[#667085]">退款状态</text><text class="text-[#d97706]">待审核</text></view>
</view>
<button class="mt-[28px] w-full rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="emit('detail')">查看订单详情</button>
<button class="mt-[12px] w-full rounded-[24px] border border-[#d0d5dd] bg-white p-0 text-[15px] leading-[48px] text-[#344054]" @click="emit('home')">返回景区首页</button>
<button class="mt-[28px] box-border w-full min-w-0 rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="emit('detail')">查看订单详情</button>
<button class="mt-[12px] box-border w-full min-w-0 rounded-[24px] border border-[#d0d5dd] bg-white p-0 text-[15px] leading-[48px] text-[#344054]" @click="emit('home')">返回景区首页</button>
</view>
<view v-else class="flex h-screen flex-col bg-[#f8fafc] pt-[env(safe-area-inset-top)]">
<view class="flex items-center bg-white px-[16px] py-[14px]"><view class="flex h-[36px] w-[36px] items-center justify-center" @click="emit('back')"><uni-icons type="left" size="20" color="#171717" /></view><text class="ml-[4px] text-[20px] font-semibold text-[#171717]">申请退款</text></view>
<scroll-view scroll-y class="min-h-0 flex-1 px-[16px] py-[16px]">
<view class="rounded-[16px] bg-white p-[16px]"><text class="text-[16px] font-medium text-[#171717]">选择退款权益</text>
<view v-for="right in pendingRights" :key="right.id" class="mt-[12px] flex items-center rounded-[12px] border p-[12px]" :class="selectedIds.includes(right.id) ? 'border-[#7c3aed] bg-[#faf5ff]' : 'border-[#eaecf0] bg-white'" @click="toggleRight(right.id)">
<view class="flex h-[20px] w-[20px] items-center justify-center rounded-full border" :class="selectedIds.includes(right.id) ? 'border-[#7c3aed] bg-[#7c3aed]' : 'border-[#d0d5dd] bg-white'"><uni-icons v-if="selectedIds.includes(right.id)" type="checkmarkempty" size="14" color="#ffffff" /></view>
<view class="ml-[10px] flex-1"><text class="block text-[14px] text-[#344054]">{{ right.name }}</text><text class="mt-[3px] block text-[12px] text-[#98a2b3]">{{ right.quantity || 0 }}可退 ¥{{ money((right.refundAllocation || 0) * (right.quantity || 0)) }}</text></view>
<view v-else class="flex h-full min-w-0 flex-col overflow-hidden bg-[#f8fafc]">
<scroll-view scroll-y class="min-h-0 min-w-0 flex-1">
<view class="box-border w-full min-w-0 px-[16px] py-[16px]">
<view class="box-border w-full min-w-0 rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">选择退款权益</text>
<view v-for="right in pendingRights" :key="right.id" class="mt-[12px] box-border flex w-full min-w-0 items-center rounded-[12px] border p-[12px]" :class="selectedIds.includes(right.id) ? 'border-[#7c3aed] bg-[#faf5ff]' : 'border-[#eaecf0] bg-white'" @click="toggleRight(right.id)">
<view class="flex h-[20px] w-[20px] shrink-0 items-center justify-center rounded-full border" :class="selectedIds.includes(right.id) ? 'border-[#7c3aed] bg-[#7c3aed]' : 'border-[#d0d5dd] bg-white'"><uni-icons v-if="selectedIds.includes(right.id)" type="checkmarkempty" size="14" color="#ffffff" /></view>
<view class="ml-[10px] min-w-0 flex-1"><text class="block break-words text-[14px] text-[#344054]">{{ right.name }}</text><text class="mt-[3px] block break-words text-[12px] text-[#98a2b3]">{{ right.quantity || 0 }}可退 ¥{{ money((right.refundAllocation || 0) * (right.quantity || 0)) }}</text></view>
</view>
<text v-if="!pendingRights.length" class="mt-[14px] block text-[14px] text-[#98a2b3]">当前没有可申请退款的权益</text>
</view>
<text v-if="!pendingRights.length" class="mt-[14px] block text-[14px] text-[#98a2b3]">当前没有可申请退款的权益</text>
<view class="mt-[12px] box-border w-full min-w-0 rounded-[16px] bg-white p-[16px]">
<text class="text-[16px] font-medium text-[#171717]">退款原因</text>
<view v-for="reason in reasons" :key="reason" class="mt-[12px] flex min-w-0 items-start" @click="selectedReason = reason">
<view class="mt-[1px] flex h-[20px] w-[20px] shrink-0 items-center justify-center rounded-full border" :class="selectedReason === reason ? 'border-[#7c3aed] bg-[#7c3aed]' : 'border-[#d0d5dd] bg-white'"><view v-if="selectedReason === reason" class="h-[7px] w-[7px] rounded-full bg-white" /></view>
<text class="ml-[10px] min-w-0 flex-1 break-words text-[14px] leading-[21px] text-[#344054]">{{ reason }}</text>
</view>
</view>
<view class="mt-[12px] box-border w-full min-w-0 rounded-[16px] bg-white p-[16px]">
<view class="flex min-w-0 items-center justify-between gap-[12px]"><text class="text-[15px] text-[#667085]">预计退款金额</text><text class="shrink-0 text-[22px] font-semibold text-[#ef4444]">¥{{ money(refundAmount) }}</text></view>
<text class="mt-[8px] block text-[12px] text-[#98a2b3]">实际到账金额以审核结果为准</text>
</view>
<view class="h-[24px]" />
</view>
<view class="mt-[12px] rounded-[16px] bg-white p-[16px]"><text class="text-[16px] font-medium text-[#171717]">退款原因</text>
<view v-for="reason in reasons" :key="reason" class="mt-[12px] flex items-center" @click="selectedReason = reason"><view class="flex h-[20px] w-[20px] items-center justify-center rounded-full border" :class="selectedReason === reason ? 'border-[#7c3aed] bg-[#7c3aed]' : 'border-[#d0d5dd] bg-white'"><view v-if="selectedReason === reason" class="h-[7px] w-[7px] rounded-full bg-white" /></view><text class="ml-[10px] text-[14px] text-[#344054]">{{ reason }}</text></view>
</view>
<view class="mt-[12px] rounded-[16px] bg-white p-[16px]"><view class="flex items-center justify-between"><text class="text-[15px] text-[#667085]">预计退款金额</text><text class="text-[22px] font-semibold text-[#ef4444]">¥{{ money(refundAmount) }}</text></view><text class="mt-[8px] block text-[12px] text-[#98a2b3]">实际到账金额以审核结果为准</text></view>
<view class="h-[80px]" />
</scroll-view>
<view class="bg-white px-[16px] py-[12px] pb-[calc(env(safe-area-inset-bottom)+12px)]"><button class="w-full rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="submitRefund">提交退款申请</button></view>
<view class="box-border w-full min-w-0 bg-white px-[16px] py-[12px] pb-[calc(env(safe-area-inset-bottom)+12px)]"><button class="box-border w-full min-w-0 rounded-[24px] bg-[#7c3aed] p-0 text-[15px] leading-[48px] text-white" @click="submitRefund">提交退款申请</button></view>
</view>
</template>
@@ -40,7 +51,7 @@ const props = defineProps({
reasons: { type: Array, default: () => [] },
submittedRefund: { type: Object, default: null },
})
const emit = defineEmits(['back', 'submit', 'payload', 'detail', 'home'])
const emit = defineEmits(['submit', 'detail', 'home'])
const selectedIds = ref([])
const selectedReason = ref('')
const pendingRights = computed(() => (props.order?.entitlements || []).filter((right) => right.status === 'pending'))

View File

@@ -4,17 +4,21 @@
<image class="absolute inset-0 h-full w-full" :src="scenicSpot.heroImage" mode="aspectFill" />
<view class="absolute inset-0 bg-[linear-gradient(180deg,rgba(6,17,31,0.15)_18%,rgba(6,17,31,0.1)_45%,rgba(6,17,31,0.92)_100%)]" />
<view class="absolute left-[16px] right-[16px] top-[calc(env(safe-area-inset-top)+12px)] flex items-center justify-between">
<view class="flex h-[38px] w-[38px] items-center justify-center rounded-full bg-black/30" @click="emit('back')">
<uni-icons type="left" size="20" color="#FFFFFF" />
<view class="absolute inset-x-0 top-0 z-20">
<TopNavBar
title=""
background="transparent"
title-color="#ffffff"
back-icon-color="#ffffff"
@back="emit('back')"
/>
</view>
<view class="absolute right-[100px] top-[calc(env(safe-area-inset-top)+58px)] z-30 flex items-center gap-[10px]">
<view class="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-black/35 shadow-sm" @tap.stop="emit('open-travelers')">
<uni-icons type="contact" size="21" color="#FFFFFF" />
</view>
<view class="flex items-center gap-[10px]">
<view class="flex h-[38px] w-[38px] items-center justify-center rounded-full bg-black/30" @click="emit('open-travelers')">
<uni-icons type="contact" size="20" color="#FFFFFF" />
</view>
<view class="flex h-[38px] w-[38px] items-center justify-center rounded-full bg-black/30" @click="emit('open-orders')">
<uni-icons type="list" size="20" color="#FFFFFF" />
</view>
<view class="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-black/35 shadow-sm" @tap.stop="emit('open-orders')">
<uni-icons type="list" size="21" color="#FFFFFF" />
</view>
</view>
@@ -40,9 +44,15 @@
<view class="-mt-[16px] min-h-0 flex-1 overflow-hidden rounded-t-[22px] bg-white pt-[14px]">
<view class="flex h-full min-h-0">
<scroll-view class="h-full w-[96px] shrink-0 bg-white" scroll-y>
<scroll-view
class="h-full w-[96px] shrink-0 bg-white"
scroll-y
:scroll-into-view="leftScrollIntoViewId"
scroll-with-animation
>
<view
v-for="group in groups"
:id="`ticket-nav-${group.id}`"
:key="group.id"
class="relative flex min-h-[68px] items-center justify-center px-[10px] py-[12px] text-center"
:class="group.id === activeGroupId ? 'bg-[#ecf8f2] text-[#11795c]' : 'bg-white text-[#5f6875]'"
@@ -53,7 +63,14 @@
</view>
</scroll-view>
<scroll-view class="h-full min-w-0 flex-1 bg-[#f4f6f8]" scroll-y :scroll-into-view="scrollIntoViewId" scroll-with-animation>
<scroll-view
id="ticket-product-scroll"
class="h-full min-w-0 flex-1 bg-[#f4f6f8]"
scroll-y
:scroll-into-view="scrollIntoViewId"
scroll-with-animation
@scroll="handleProductScroll"
>
<view class="px-[12px] pb-[24px]">
<view v-for="group in groups" :id="`ticket-group-${group.id}`" :key="group.id" class="pt-[14px]">
<view class="flex items-center">
@@ -95,7 +112,8 @@
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'
import TopNavBar from "@/components/TopNavBar/index.vue"
const props = defineProps({
scenicSpot: { type: Object, required: true },
@@ -106,6 +124,10 @@ const props = defineProps({
const emit = defineEmits(['back', 'open-travelers', 'open-orders', 'open-detail', 'buy'])
const activeGroupId = ref(props.groups[0]?.id || '')
const scrollIntoViewId = ref('')
const leftScrollIntoViewId = computed(() => `ticket-nav-${activeGroupId.value}`)
const groupOffsets = ref([])
const rightScrollTop = ref(0)
const instance = getCurrentInstance()
const productsById = computed(() => new Map(props.products.map((product) => [product.id, product])))
const productsForGroup = (group) => (group.productIds || [])
@@ -116,9 +138,57 @@ const ageRuleText = (rule) => `${rule.min}—${rule.max}周岁`
const selectGroup = (groupId) => {
activeGroupId.value = groupId
scrollIntoViewId.value = `ticket-group-${groupId}`
scrollIntoViewId.value = ''
nextTick(() => {
scrollIntoViewId.value = `ticket-group-${groupId}`
})
}
const measureGroupOffsets = () => {
if (!props.groups.length) return
nextTick(() => {
const query = uni.createSelectorQuery().in(instance?.proxy)
query.select('#ticket-product-scroll').boundingClientRect()
props.groups.forEach((group) => {
query.select(`#ticket-group-${group.id}`).boundingClientRect()
})
query.exec((rects = []) => {
const containerRect = rects[0]
if (!containerRect) return
groupOffsets.value = props.groups
.map((group, index) => ({
id: group.id,
top: Math.max(
0,
Number(rects[index + 1]?.top || 0) -
Number(containerRect.top || 0) +
rightScrollTop.value
),
}))
.sort((left, right) => left.top - right.top)
})
})
}
const handleProductScroll = (event) => {
rightScrollTop.value = Number(event.detail?.scrollTop || 0)
if (!groupOffsets.value.length) {
measureGroupOffsets()
return
}
const activationLine = rightScrollTop.value + 24
const activeGroup = groupOffsets.value.reduce(
(current, group) => (group.top <= activationLine ? group : current),
groupOffsets.value[0]
)
if (activeGroup?.id) activeGroupId.value = activeGroup.id
}
onMounted(measureGroupOffsets)
watch(() => props.groups.map((group) => group.id).join(','), measureGroupOffsets)
const makePhoneCall = () => {
if (!props.scenicSpot.phone) return
uni.makePhoneCall({ phoneNumber: props.scenicSpot.phone })

View File

@@ -1,12 +1,7 @@
<template>
<view class="flex h-screen flex-col bg-slate-100 text-slate-900">
<view class="flex shrink-0 items-center bg-white px-4 pb-3 pt-10">
<button class="m-0 mr-3 flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 p-0 text-lg leading-none" @tap="emit('back')"></button>
<text class="text-lg font-semibold">常用出行人</text>
</view>
<view class="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">
<view class="space-y-3 px-3 py-3 pb-28">
<view class="box-border w-full min-w-0 space-y-3 px-3 py-3 pb-28">
<view v-if="travelers.length" class="space-y-3">
<button v-for="traveler in travelers" :key="traveler.id" class="m-0 flex w-full items-center rounded-2xl bg-white px-4 py-4 text-left shadow-sm" @tap="selectTraveler(traveler.id)">
<view v-if="selectionMode" class="mr-3 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border" :class="isSelected(traveler.id) ? 'border-emerald-500 bg-emerald-500 text-white' : 'border-slate-300 bg-white text-transparent'"></view>
@@ -39,7 +34,7 @@
</view>
</scroll-view>
<view class="shrink-0 bg-white px-4 py-3">
<view class="box-border w-full min-w-0 shrink-0 bg-white px-4 py-3">
<button class="m-0 w-full rounded-full bg-emerald-600 py-3 text-sm font-semibold text-white" @tap="showForm = true">新增出行人</button>
</view>
</view>
@@ -54,7 +49,7 @@ const props = defineProps({
selectionMode: { type: Boolean, default: false },
})
const emit = defineEmits(['back', 'toggle', 'save'])
const emit = defineEmits(['toggle', 'save'])
const idTypes = ['身份证', '护照']
const showForm = ref(false)
const formError = ref('')

View File

@@ -0,0 +1,32 @@
<template>
<view class="flex h-screen min-w-0 flex-col overflow-hidden bg-[#f8fafc]">
<TopNavBar
title="查看凭证"
subtitle="入园及园内权益核验"
title-align="left"
background="#ffffff"
/>
<CredentialsView
v-if="ticketStore.currentOrder"
class="min-h-0 flex-1"
:order="ticketStore.currentOrder"
@detail="goDetail"
/>
</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import TopNavBar from "@/components/TopNavBar/index.vue";
import CredentialsView from "./components/CredentialsView.vue";
import { useTicketStore } from "./store.js";
const ticketStore = useTicketStore();
onLoad((options) => ticketStore.openOrder(options?.orderId));
const goDetail = () =>
uni.redirectTo({
url: `/pages-service/tickets/order-detail?orderId=${ticketStore.currentOrderId}`,
});
</script>

View File

@@ -1,88 +1,22 @@
<template>
<view class="h-screen overflow-hidden bg-[#f5f7fa] text-[#172033]">
<view class="h-screen min-w-0 overflow-hidden bg-[#f5f7fa] text-[#172033]">
<TicketHome
v-if="currentView === TICKET_VIEWS.HOME"
v-if="!detailProduct"
:scenic-spot="SCENIC_SPOT"
:groups="visibleGroups"
:products="products"
:products="ticketStore.products"
@back="goBack"
@open-travelers="openTravelerManager(false)"
@open-orders="navigate(TICKET_VIEWS.ORDERS)"
@open-travelers="openTravelers"
@open-orders="openOrders"
@open-detail="openProductDetail"
@buy="startBooking"
/>
<ProductDetail
v-else-if="currentView === TICKET_VIEWS.DETAIL"
:product="currentProduct"
@back="goBack"
v-else
:product="detailProduct"
@back="detailProductId = ''"
@buy="startBooking"
/>
<BookingFlow
v-else-if="currentView === TICKET_VIEWS.BOOKING"
:product="currentProduct"
:draft="bookingDraft"
:travelers="travelers"
:available-dates="AVAILABLE_DATES"
:insurance="INSURANCE"
@back="goBack"
@update-draft="updateBookingDraft"
@open-travelers="openTravelerManager(true)"
@submit="submitBooking"
/>
<TravelerManager
v-else-if="currentView === TICKET_VIEWS.TRAVELERS"
:travelers="travelers"
:selected-ids="bookingDraft.travelerIds"
:selection-mode="travelerSelectionMode"
@back="goBack"
@toggle="toggleTraveler"
@save="saveTraveler"
/>
<PaymentResult
v-else-if="currentView === TICKET_VIEWS.PAID"
:order="currentOrder"
@home="returnHome"
@orders="navigate(TICKET_VIEWS.ORDERS)"
@detail="showCurrentOrderDetail"
@credentials="navigate(TICKET_VIEWS.CREDENTIALS)"
/>
<OrderList
v-else-if="currentView === TICKET_VIEWS.ORDERS"
:orders="orders"
@back="goBack"
@open="openOrder"
/>
<OrderDetail
v-else-if="currentView === TICKET_VIEWS.ORDER_DETAIL"
:order="currentOrder"
@back="goBack"
@credentials="navigate(TICKET_VIEWS.CREDENTIALS)"
@refund="openRefund"
/>
<RefundFlow
v-else-if="[TICKET_VIEWS.REFUND, TICKET_VIEWS.REFUND_RESULT].includes(currentView)"
:order="currentOrder"
:reasons="REFUND_REASONS"
:submitted-refund="currentView === TICKET_VIEWS.REFUND_RESULT ? submittedRefund : null"
@back="goBack"
@submit="submitRefund"
@detail="replaceWithOrderDetail"
@home="returnHome"
/>
<CredentialsView
v-else-if="currentView === TICKET_VIEWS.CREDENTIALS"
:order="currentOrder"
@back="goBack"
@detail="replaceWithOrderDetail"
/>
</view>
</template>
@@ -90,203 +24,31 @@
import { computed, ref } from "vue";
import TicketHome from "./components/TicketHome.vue";
import ProductDetail from "./components/ProductDetail.vue";
import BookingFlow from "./components/BookingFlow.vue";
import TravelerManager from "./components/TravelerManager.vue";
import PaymentResult from "./components/PaymentResult.vue";
import OrderList from "./components/OrderList.vue";
import OrderDetail from "./components/OrderDetail.vue";
import RefundFlow from "./components/RefundFlow.vue";
import CredentialsView from "./components/CredentialsView.vue";
import {
AVAILABLE_DATES,
GROUPS,
INITIAL_ORDERS,
INITIAL_TRAVELERS,
INSURANCE,
PRODUCTS,
REFUND_REASONS,
SCENIC_SPOT,
} from "./data.js";
import {
TICKET_VIEWS,
applyRefund,
createBookingDraft,
createPaidOrder,
popViewHistory,
validateBooking,
} from "./model.mjs";
import { GROUPS, SCENIC_SPOT } from "./data.js";
import { useTicketStore } from "./store.js";
const clone = (value) => JSON.parse(JSON.stringify(value));
const currentView = ref(TICKET_VIEWS.HOME);
const viewHistory = ref([]);
const products = ref(clone(PRODUCTS));
const travelers = ref(clone(INITIAL_TRAVELERS));
const orders = ref(clone(INITIAL_ORDERS));
const refunds = ref([]);
const currentProductId = ref(PRODUCTS[0]?.id || "");
const currentOrderId = ref(INITIAL_ORDERS[0]?.id || "");
const bookingDraft = ref(createBookingDraft(PRODUCTS[0]));
const travelerSelectionMode = ref(false);
const submittedRefund = ref(null);
const ticketStore = useTicketStore();
const detailProductId = ref("");
const visibleGroups = computed(() =>
[...GROUPS]
.filter((group) => group.enabled)
.sort((left, right) => left.sortOrder - right.sortOrder)
);
const currentProduct = computed(() =>
products.value.find((product) => product.id === currentProductId.value) ||
products.value[0]
const detailProduct = computed(() =>
ticketStore.products.find((product) => product.id === detailProductId.value)
);
const currentOrder = computed(() =>
orders.value.find((order) => order.id === currentOrderId.value) ||
orders.value[0]
);
const navigate = (view) => {
viewHistory.value = [...viewHistory.value, currentView.value];
currentView.value = view;
};
const replaceView = (view) => {
currentView.value = view;
};
const goBack = () => {
if (!viewHistory.value.length) {
uni.navigateBack();
return;
}
const previous = popViewHistory(viewHistory.value);
viewHistory.value = previous.history;
currentView.value = previous.view;
};
const returnHome = () => {
currentView.value = TICKET_VIEWS.HOME;
viewHistory.value = [];
submittedRefund.value = null;
};
const goBack = () => uni.navigateBack();
const openTravelers = () =>
uni.navigateTo({ url: "/pages-service/tickets/travelers" });
const openOrders = () =>
uni.navigateTo({ url: "/pages-service/tickets/orders" });
const openProductDetail = (productId) => {
currentProductId.value = productId;
navigate(TICKET_VIEWS.DETAIL);
detailProductId.value = productId;
};
const startBooking = (productId) => {
currentProductId.value = productId;
bookingDraft.value = createBookingDraft(currentProduct.value);
navigate(TICKET_VIEWS.BOOKING);
};
const updateBookingDraft = (draft) => {
bookingDraft.value = draft;
};
const openTravelerManager = (selectionMode) => {
travelerSelectionMode.value = selectionMode;
navigate(TICKET_VIEWS.TRAVELERS);
};
const toggleTraveler = (travelerId) => {
const selectedIds = bookingDraft.value.travelerIds || [];
bookingDraft.value = {
...bookingDraft.value,
travelerIds: selectedIds.includes(travelerId)
? selectedIds.filter((id) => id !== travelerId)
: [...selectedIds, travelerId],
};
};
const saveTraveler = (traveler) => {
travelers.value = [...travelers.value, traveler];
if (travelerSelectionMode.value) {
bookingDraft.value = {
...bookingDraft.value,
travelerIds: [...new Set([...bookingDraft.value.travelerIds, traveler.id])],
};
}
uni.showToast({ title: "出行人已保存", icon: "success" });
};
const submitBooking = () => {
const product = currentProduct.value;
const draft = bookingDraft.value;
const error = validateBooking({
product,
draft,
travelers: travelers.value,
const startBooking = (productId) =>
uni.navigateTo({
url: `/pages-service/tickets/booking?productId=${productId}`,
});
if (error) {
uni.showToast({ title: error, icon: "none" });
return;
}
if (product.stock < draft.quantity) {
uni.showToast({ title: "当前库存不足,请减少购买数量", icon: "none" });
return;
}
const order = createPaidOrder({
product,
draft,
travelers: travelers.value,
});
orders.value = [order, ...orders.value];
products.value = products.value.map((item) =>
item.id === product.id
? {
...item,
stock: Math.max(0, item.stock - draft.quantity),
sold: item.sold + draft.quantity,
}
: item
);
currentOrderId.value = order.id;
navigate(TICKET_VIEWS.PAID);
};
const openOrder = (orderId) => {
currentOrderId.value = orderId;
navigate(TICKET_VIEWS.ORDER_DETAIL);
};
const showCurrentOrderDetail = () => {
navigate(TICKET_VIEWS.ORDER_DETAIL);
};
const replaceWithOrderDetail = () => {
replaceView(TICKET_VIEWS.ORDER_DETAIL);
};
const openRefund = () => {
submittedRefund.value = null;
navigate(TICKET_VIEWS.REFUND);
};
const submitRefund = ({ entitlementIds, reason }) => {
const result = applyRefund(
currentOrder.value,
entitlementIds,
reason
);
if (!result.refund) {
uni.showToast({ title: "所选权益暂不可退款", icon: "none" });
return;
}
orders.value = orders.value.map((order) =>
order.id === result.order.id ? result.order : order
);
refunds.value = [result.refund, ...refunds.value];
submittedRefund.value = result.refund;
navigate(TICKET_VIEWS.REFUND_RESULT);
};
</script>

View File

@@ -0,0 +1,37 @@
<template>
<view class="flex h-screen min-w-0 flex-col overflow-hidden bg-[#f8fafc]">
<TopNavBar
title="订单详情"
subtitle="查看使用信息与订单权益"
title-align="left"
background="#ffffff"
/>
<OrderDetail
v-if="ticketStore.currentOrder"
class="min-h-0 flex-1"
:order="ticketStore.currentOrder"
@credentials="goCredentials"
@refund="goRefund"
/>
</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import TopNavBar from "@/components/TopNavBar/index.vue";
import OrderDetail from "./components/OrderDetail.vue";
import { useTicketStore } from "./store.js";
const ticketStore = useTicketStore();
onLoad((options) => ticketStore.openOrder(options?.orderId));
const goCredentials = () =>
uni.navigateTo({
url: `/pages-service/tickets/credentials?orderId=${ticketStore.currentOrderId}`,
});
const goRefund = () =>
uni.navigateTo({
url: `/pages-service/tickets/refund?orderId=${ticketStore.currentOrderId}`,
});
</script>

View File

@@ -0,0 +1,30 @@
<template>
<view class="flex h-screen min-w-0 flex-col overflow-hidden bg-[#f8fafc]">
<TopNavBar
title="我的订单"
subtitle="购买、凭证与退款进度"
title-align="left"
background="#ffffff"
/>
<OrderList
class="min-h-0 flex-1"
:orders="ticketStore.orders"
@open="openOrder"
/>
</view>
</template>
<script setup>
import TopNavBar from "@/components/TopNavBar/index.vue";
import OrderList from "./components/OrderList.vue";
import { useTicketStore } from "./store.js";
const ticketStore = useTicketStore();
const openOrder = (orderId) => {
ticketStore.openOrder(orderId);
uni.navigateTo({
url: `/pages-service/tickets/order-detail?orderId=${orderId}`,
});
};
</script>

View File

@@ -0,0 +1,46 @@
<template>
<view class="flex h-screen min-w-0 flex-col overflow-hidden bg-[#f8fafc]">
<TopNavBar
title="申请退款"
subtitle="选择未使用权益提交申请"
title-align="left"
background="#ffffff"
/>
<RefundFlow
v-if="ticketStore.currentOrder"
class="min-h-0 flex-1"
:order="ticketStore.currentOrder"
:reasons="REFUND_REASONS"
:submitted-refund="ticketStore.submittedRefund"
@submit="submitRefund"
@detail="goDetail"
@home="goHome"
/>
</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import TopNavBar from "@/components/TopNavBar/index.vue";
import RefundFlow from "./components/RefundFlow.vue";
import { REFUND_REASONS } from "./data.js";
import { useTicketStore } from "./store.js";
const ticketStore = useTicketStore();
onLoad((options) => {
ticketStore.openOrder(options?.orderId);
ticketStore.resetRefund();
});
const submitRefund = (payload) => {
const result = ticketStore.submitRefund(payload);
if (result.error) uni.showToast({ title: result.error, icon: "none" });
};
const goDetail = () =>
uni.redirectTo({
url: `/pages-service/tickets/order-detail?orderId=${ticketStore.currentOrderId}`,
});
const goHome = () =>
uni.reLaunch({ url: "/pages-service/tickets/index" });
</script>

View File

@@ -0,0 +1,131 @@
import { defineStore } from "pinia";
import {
INITIAL_ORDERS,
INITIAL_TRAVELERS,
PRODUCTS,
} from "./data.js";
import {
applyRefund,
createBookingDraft,
createPaidOrder,
validateBooking,
} from "./model.mjs";
const clone = (value) => JSON.parse(JSON.stringify(value));
export const useTicketStore = defineStore("ticket-client", {
state: () => ({
products: clone(PRODUCTS),
travelers: clone(INITIAL_TRAVELERS),
orders: clone(INITIAL_ORDERS),
refunds: [],
currentProductId: PRODUCTS[0]?.id || "",
currentOrderId: INITIAL_ORDERS[0]?.id || "",
bookingDraft: createBookingDraft(PRODUCTS[0]),
submittedRefund: null,
}),
getters: {
currentProduct(state) {
return (
state.products.find((product) => product.id === state.currentProductId) ||
state.products[0]
);
},
currentOrder(state) {
return (
state.orders.find((order) => order.id === state.currentOrderId) ||
state.orders[0]
);
},
},
actions: {
startBooking(productId) {
const product =
this.products.find((item) => item.id === productId) || this.products[0];
this.currentProductId = product?.id || "";
this.bookingDraft = createBookingDraft(product);
},
updateBookingDraft(draft) {
this.bookingDraft = draft;
},
toggleTraveler(travelerId) {
const selectedIds = this.bookingDraft.travelerIds || [];
this.bookingDraft = {
...this.bookingDraft,
travelerIds: selectedIds.includes(travelerId)
? selectedIds.filter((id) => id !== travelerId)
: [...selectedIds, travelerId],
};
},
saveTraveler(traveler, selectAfterSave = false) {
this.travelers = [...this.travelers, traveler];
if (selectAfterSave) {
this.bookingDraft = {
...this.bookingDraft,
travelerIds: [
...new Set([...(this.bookingDraft.travelerIds || []), traveler.id]),
],
};
}
},
submitBooking() {
const product = this.currentProduct;
const draft = this.bookingDraft;
const error = validateBooking({
product,
draft,
travelers: this.travelers,
});
if (error) return { error, order: null };
if (product.stock < draft.quantity) {
return { error: "当前库存不足,请减少购买数量", order: null };
}
const order = createPaidOrder({
product,
draft,
travelers: this.travelers,
});
this.orders = [order, ...this.orders];
this.products = this.products.map((item) =>
item.id === product.id
? {
...item,
stock: Math.max(0, item.stock - draft.quantity),
sold: item.sold + draft.quantity,
}
: item
);
this.currentOrderId = order.id;
return { error: "", order };
},
openOrder(orderId) {
if (this.orders.some((order) => order.id === orderId)) {
this.currentOrderId = orderId;
}
},
resetRefund() {
this.submittedRefund = null;
},
submitRefund({ entitlementIds, reason }) {
const order = this.currentOrder;
const result = applyRefund(order, entitlementIds, reason);
if (!result.refund) {
return { error: "当前没有可退款的权益", refund: null };
}
this.orders = this.orders.map((item) =>
item.id === result.order.id ? result.order : item
);
this.refunds = [result.refund, ...this.refunds];
this.submittedRefund = result.refund;
return { error: "", refund: result.refund };
},
},
});

View File

@@ -0,0 +1,38 @@
<template>
<view class="flex h-screen min-w-0 flex-col overflow-hidden bg-slate-100">
<TopNavBar
title="常用出行人"
subtitle="快速管理实名预约信息"
title-align="left"
background="#ffffff"
/>
<TravelerManager
class="min-h-0 flex-1"
:travelers="ticketStore.travelers"
:selected-ids="ticketStore.bookingDraft.travelerIds"
:selection-mode="selectionMode"
@toggle="ticketStore.toggleTraveler"
@save="saveTraveler"
/>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import TopNavBar from "@/components/TopNavBar/index.vue";
import TravelerManager from "./components/TravelerManager.vue";
import { useTicketStore } from "./store.js";
const ticketStore = useTicketStore();
const selectionMode = ref(false);
onLoad((options) => {
selectionMode.value = options?.select === "1";
});
const saveTraveler = (traveler) => {
ticketStore.saveTraveler(traveler, selectionMode.value);
uni.showToast({ title: "出行人已保存", icon: "success" });
};
</script>

View File

@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const ticketRoot = new URL("./", import.meta.url);
const readTicketFile = (path) => readFile(new URL(path, ticketRoot), "utf8");
const pageNames = [
"booking",
"travelers",
"orders",
"order-detail",
"credentials",
"refund",
];
test("registers every visitor ticket flow as an independent page", async () => {
const pagesJson = JSON.parse(
await readFile(new URL("../../pages.json", import.meta.url), "utf8")
);
const ticketPackage = pagesJson.subPackages.find(
(item) => item.root === "pages-service"
);
const paths = ticketPackage.pages.map((item) => item.path);
for (const pageName of pageNames) {
assert.ok(paths.includes(`tickets/${pageName}`), `${pageName} route missing`);
}
});
test("every ticket flow page uses TopNavBar", async () => {
for (const pageName of pageNames) {
const source = await readTicketFile(`${pageName}.vue`);
assert.match(source, /TopNavBar/);
assert.match(source, /subtitle=/);
assert.match(source, /title-align="left"/);
}
});
test("TopNavBar exposes an optional subtitle", async () => {
const source = await readFile(
new URL("../../components/TopNavBar/index.vue", import.meta.url),
"utf8"
);
assert.match(source, /subtitle/);
assert.match(source, /\{\{\s*subtitle\s*\}\}/);
assert.match(source, /titleAlign === "left"/);
assert.match(source, /items-start text-left/);
assert.match(source, /items-center text-center/);
});
test("storefront synchronizes the left category from right-column scrolling", async () => {
const source = await readTicketFile("components/TicketHome.vue");
assert.match(source, /right-\[100px\]/);
assert.match(source, /z-30/);
assert.match(source, /@tap\.stop="emit\('open-travelers'\)"/);
assert.match(source, /@tap\.stop="emit\('open-orders'\)"/);
assert.match(source, /@scroll="handleProductScroll"/);
assert.match(source, /groupOffsets/);
assert.match(source, /scrollTop/);
assert.match(source, /leftScrollIntoViewId/);
assert.match(source, /TopNavBar/);
});
test("order content uses bounded box-model wrappers", async () => {
const componentNames = [
"OrderList",
"OrderDetail",
"RefundFlow",
"CredentialsView",
];
for (const componentName of componentNames) {
const source = await readTicketFile(`components/${componentName}.vue`);
assert.match(source, /box-border/);
assert.match(source, /min-w-0/);
}
});

View File

@@ -148,6 +148,42 @@
"style": {
"navigationStyle": "custom"
}
},
{
"path": "tickets/booking",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "tickets/travelers",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "tickets/orders",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "tickets/order-detail",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "tickets/credentials",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "tickets/refund",
"style": {
"navigationStyle": "custom"
}
}
]
},

View File

@@ -60,7 +60,8 @@ const sendReply = (item) => {
// 快速预定
if (item.type === Command.quickBooking) {
checkToken().then(() => {
uni.navigateTo({ url: "/pages-quick/list" });
// uni.navigateTo({ url: "/pages-quick/list" });
uni.navigateTo({ url: "/pages-service/tickets/index" });
});
return;
}