feat: wire ticket client experience
This commit is contained in:
292
src/pages-service/tickets/index.vue
Normal file
292
src/pages-service/tickets/index.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<view class="h-screen overflow-hidden bg-[#f5f7fa] text-[#172033]">
|
||||
<TicketHome
|
||||
v-if="currentView === TICKET_VIEWS.HOME"
|
||||
:scenic-spot="SCENIC_SPOT"
|
||||
:groups="visibleGroups"
|
||||
:products="products"
|
||||
@back="goBack"
|
||||
@open-travelers="openTravelerManager(false)"
|
||||
@open-orders="navigate(TICKET_VIEWS.ORDERS)"
|
||||
@open-detail="openProductDetail"
|
||||
@buy="startBooking"
|
||||
/>
|
||||
|
||||
<ProductDetail
|
||||
v-else-if="currentView === TICKET_VIEWS.DETAIL"
|
||||
:product="currentProduct"
|
||||
@back="goBack"
|
||||
@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>
|
||||
|
||||
<script setup>
|
||||
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";
|
||||
|
||||
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 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 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 openProductDetail = (productId) => {
|
||||
currentProductId.value = productId;
|
||||
navigate(TICKET_VIEWS.DETAIL);
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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>
|
||||
@@ -137,14 +137,20 @@
|
||||
{
|
||||
"root": "pages-service",
|
||||
"pages": [
|
||||
{
|
||||
"path": "order/list",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "order/list",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "tickets/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages-quick",
|
||||
"pages": [
|
||||
|
||||
Reference in New Issue
Block a user