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>
|
||||||
@@ -142,6 +142,12 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tickets/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user