Files
jiapuapp/pages/profile/m09-vip-orders.vue
T

615 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="orders-page">
<ModulePageBackground module="profile" />
<view class="page-layer"
><PageHeader
class="orders-header"
title="VIP 与订单"
custom-back
@back="requestBack"
/></view>
<view class="page-content page-layer">
<view class="vip-intro">
<image
class="vip-intro__background"
src="/static/assets/modules/profile/opaque/m09-vip-hero-landscape.png"
mode="aspectFill"
aria-hidden="true"
/>
<view class="vip-intro__content">
<text class="vip-intro__eyebrow">家谱传承服务</text>
<text class="vip-intro__title">开通 VIP守护家谱长久传承</text>
<text class="vip-intro__copy">选择适合家谱规模的服务套餐</text>
</view>
</view>
<view v-if="state === 'loading'" class="state-card"
><AppLoading text="正在读取服务套餐"
/></view>
<view v-else-if="state === 'error'" class="state-card"
><text>暂时无法读取套餐或订单</text
><AppButton block type="secondary" label="重新加载" @click="loadVip"
/></view>
<template v-else>
<view v-if="packages.length" class="package-list"
><view
v-for="item in packages"
:key="item.id"
class="package-card"
><image
class="package-courtyard"
src="/static/assets/modules/profile/opaque/m09-featured-courtyard.png"
mode="aspectFill"
aria-hidden="true"
/>
<view class="package-heading">
<view>
<text>{{ item.name }}</text>
<text v-if="item.description">{{ item.description }}</text>
</view>
</view>
<view class="package-meta"
><text v-if="item.price" class="package-price">¥{{ item.price }}</text
><text class="package-duration">{{ item.duration || "服务期限以订单为准" }}</text
><button
class="purchase-button"
:disabled="creating"
@click="openPurchase(item)"
>立即开通</button
></view
>
</view
></view
>
<view v-else class="state-card"
><text>当前没有可展示的服务套餐</text></view
>
<view class="section-heading"
><text>订单记录</text><text>{{ orders.length }} </text></view
>
<text v-if="orderNotice" class="order-notice">{{ orderNotice }}</text>
<view v-if="orders.length" class="order-list"
><view v-for="item in orders" :key="item.id" class="order-card"
><view><text>{{ item.title }}</text><text>订单编号已由服务端生成</text></view
><text>{{ item.status || "状态待服务端确认" }}</text></view
></view
>
<view v-else class="order-empty"><text>暂无订单记录</text></view>
</template>
</view>
<AppDialog
class="payment-sheet"
:visible="Boolean(selectedPackage)"
title="确认下单"
:confirm-text="creating ? '正在创建订单' : '创建订单'"
cancel-text="暂不购买"
show-cancel
:close-on-mask="!creating"
@confirm="createOrder"
@cancel="closePurchase"
>
<view class="sheet-order-summary"
><view
><text>套餐</text><text>{{ selectedPackage?.name }}</text></view
><view v-if="selectedPackage?.duration"
><text>有效期</text><text>{{ selectedPackage.duration }}</text></view
><view><text>应付金额</text><text>¥{{ selectedPackage?.price }}</text></view></view
>
<view class="payment-method"><text>支付方式</text><text>微信支付</text></view>
<text class="payment-copy">将创建微信支付订单支付完成后请以订单状态为准</text>
<text v-if="purchaseError" class="purchase-error">{{ purchaseError }}</text>
</AppDialog>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onBackPress, onLoad, onReady, onShow, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
const packages = ref([]);
const orders = ref([]);
const state = ref("loading");
const selectedPackage = ref(null);
const creating = ref(false);
const purchaseError = ref("");
const orderNotice = ref("");
const controller = createRequestController();
let active = true;
let statusBarTimer = null;
const formatPrice = (value) =>
Number.isFinite(Number(value)) ? Number(value).toFixed(2) : "";
const durationUnits = Object.freeze({ year: "年", month: "个月", day: "天" });
const formatDuration = (value, unit) => {
if (value === undefined || value === null || value === "") return "";
if (Number.isFinite(Number(value)) && Number(value) <= 0) return "永久有效";
const normalizedUnit = String(unit || "")
.trim()
.toLowerCase();
return durationUnits[normalizedUnit]
? `${value} ${durationUnits[normalizedUnit]}`
: String(value);
};
const loadVip = async () => {
controller.abort();
state.value = "loading";
try {
const packageRows = await appApi.getVipPackages({
requestController: controller,
});
const orderRows = await appApi.getVipOrders({
requestController: controller,
});
if (!active) return;
packages.value = packageRows.map((item) => ({
id: String(item.packageId),
name: String(item.packageName || "未命名套餐"),
description: String(item.packageDesc || ""),
price: formatPrice(item.price),
duration: formatDuration(item.durationValue, item.durationUnit),
}));
orders.value = orderRows
.map((item) => ({
id: String(item.orderId || item.id || ""),
title: String(item.packageName || item.orderNo || "VIP 订单"),
status: String(item.orderStatus || item.status || ""),
}))
.filter((item) => item.id);
state.value = "ready";
} catch (error) {
if (!active || isRequestCancelled(error)) return;
state.value = "error";
}
};
const openPurchase = (item) => {
if (creating.value || !item?.id) return;
purchaseError.value = "";
selectedPackage.value = item;
};
const closePurchase = () => {
if (creating.value) return;
selectedPackage.value = null;
purchaseError.value = "";
};
const createOrder = async () => {
const item = selectedPackage.value;
if (!item?.id || creating.value) return;
creating.value = true;
purchaseError.value = "";
try {
await appApi.createVipOrder(
{ packageId: item.id, payType: "wechat" },
{ requestController: controller },
);
if (!active) return;
selectedPackage.value = null;
orderNotice.value = `“${item.name}”订单已创建,并已从服务端重新读取订单状态。`;
await loadVip();
} catch (error) {
if (!active || isRequestCancelled(error)) return;
purchaseError.value = error?.message || "订单创建失败,请稍后重试。";
} finally {
creating.value = false;
}
};
const requestBack = () => runBackGuard({});
const setVipStatusBar = (style, color) => {
const navigator = globalThis.plus?.navigator;
if (!navigator) return;
const apply = () => {
navigator.setStatusBarStyle(style);
navigator.setStatusBarBackground(color);
};
apply();
if (statusBarTimer) clearTimeout(statusBarTimer);
statusBarTimer = setTimeout(apply, 80);
};
onLoad(() => {
setVipStatusBar("dark", "#f4ebde");
loadVip();
});
onReady(() => setVipStatusBar("dark", "#f4ebde"));
onShow(() => {
setVipStatusBar("dark", "#f4ebde");
if (state.value !== "loading") loadVip();
});
onUnload(() => {
active = false;
controller.abort();
if (statusBarTimer) clearTimeout(statusBarTimer);
const navigator = globalThis.plus?.navigator;
if (navigator) {
navigator.setStatusBarStyle("light");
navigator.setStatusBarBackground("#b52e22");
}
});
onBackPress((event) => handleBackPress(event, requestBack));
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.orders-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: #f7f2e9;
}
.page-layer {
z-index: 1;
}
.page-content {
flex: 1;
padding: 20rpx 30rpx 72rpx;
}
.state-card,
.order-empty {
@include adaptive-profile-content;
}
.vip-intro {
position: relative;
min-height: 284rpx;
overflow: hidden;
margin: -20rpx -30rpx 0;
padding: 36rpx 48rpx 30rpx;
text-align: center;
}
.vip-intro__background {
position: absolute;
z-index: 0;
inset: 0;
width: 100%;
height: 100%;
opacity: 0.78;
pointer-events: none;
}
.vip-intro__content {
position: relative;
z-index: 1;
}
.vip-intro text {
display: block;
}
.vip-intro__eyebrow {
color: $brand-red;
font-size: clamp(13px, 20rpx, 16px);
font-weight: 700;
letter-spacing: 5rpx;
}
.vip-intro__title {
margin-top: 16rpx;
color: #33261d;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(20px, 38rpx, 26px);
font-weight: 700;
letter-spacing: 1rpx;
}
.vip-intro__copy {
margin-top: 14rpx;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
line-height: 1.55;
}
.state-card {
display: flex;
min-height: 190rpx;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 18rpx;
padding: 36rpx;
text-align: center;
color: $ink;
font-size: clamp(16px, 26rpx, 20px);
}
.state-card .app-button {
width: 100%;
margin-top: 22rpx;
}
.package-list,
.order-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.package-card {
position: relative;
overflow: hidden;
padding: 38rpx 36rpx 32rpx;
border: 1rpx solid rgba(159, 23, 15, 0.52);
border-radius: 26rpx;
background: rgba(255, 253, 248, 0.95);
box-shadow: 0 16rpx 32rpx rgba(102, 48, 33, 0.12);
}
.package-courtyard {
position: absolute;
right: 0;
bottom: 0;
z-index: 0;
width: 100%;
height: 100%;
opacity: 0.38;
pointer-events: none;
}
.package-card > :not(.package-courtyard) {
position: relative;
z-index: 1;
}
.package-heading > view {
min-width: 0;
}
.package-heading > view text {
display: block;
}
.package-heading > view text:first-child {
color: #302319;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(19px, 36rpx, 24px);
font-weight: 700;
}
.package-heading > view text:last-child {
margin-top: 10rpx;
color: #77675a;
font-size: clamp(14px, 23rpx, 17px);
line-height: 1.55;
}
.package-price {
color: $brand-red;
font-family: Georgia, STKaiti, serif;
font-size: clamp(20px, 38rpx, 26px);
font-weight: 700;
line-height: 1.1;
}
.package-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-top: 30rpx;
padding-top: 22rpx;
border-top: 1rpx solid rgba(117, 84, 52, 0.14);
}
.package-duration {
color: #837164;
font-size: clamp(14px, 23rpx, 17px);
}
.purchase-button {
display: inline-flex;
min-width: 184rpx;
min-height: 70rpx;
align-items: center;
justify-content: center;
box-sizing: border-box;
margin: 0;
padding: 0 28rpx;
border: 0;
border-radius: 999rpx;
background: $brand-red;
color: #fffaf1;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 700;
letter-spacing: 1rpx;
line-height: normal;
}
.purchase-button::after {
border: 0;
}
.purchase-button[disabled] {
opacity: 0.5;
}
.section-heading {
display: flex;
justify-content: space-between;
margin: 44rpx 6rpx 16rpx;
color: #38281d;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(17px, 32rpx, 22px);
font-weight: 700;
}
.section-heading text:last-child {
color: #8a796b;
font-size: clamp(13px, 21rpx, 16px);
font-weight: 400;
}
.order-notice {
display: block;
margin: 0 6rpx 14rpx;
color: #786556;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.5;
}
.order-card {
display: flex;
justify-content: space-between;
gap: 18rpx;
padding: 26rpx 4rpx;
border-bottom: 1rpx solid rgba(117, 84, 52, 0.14);
color: #48372a;
font-size: clamp(15px, 24rpx, 18px);
}
.order-card > view text {
display: block;
}
.order-card > view text:first-child {
font-weight: 700;
}
.order-card > view text:last-child {
margin-top: 8rpx;
color: #8a796b;
font-size: clamp(13px, 20rpx, 16px);
}
.order-card text:last-child {
flex: 0 0 auto;
color: #7f6959;
font-size: clamp(13px, 21rpx, 16px);
}
.order-empty {
padding: 38rpx;
text-align: center;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
}
:deep(.orders-header .page-header) {
height: calc(104rpx + var(--status-bar-height, 0px));
border-bottom: 1rpx solid rgba(117, 84, 52, 0.08);
background: #f4ebde;
}
:deep(.orders-header.page-header-slot) {
height: calc(104rpx + var(--status-bar-height, 0px));
}
:deep(.orders-header .header-title) {
color: #302319;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(20px, 37rpx, 26px);
}
:deep(.orders-header .header-back__icon) {
filter: none;
opacity: 0.78;
}
:deep(.payment-sheet.app-dialog-layer) {
align-items: flex-end;
padding: 0;
background: rgba(37, 26, 18, 0.38);
}
:deep(.payment-sheet .app-dialog) {
width: 100%;
max-width: none;
max-height: 76vh;
border: 0;
border-radius: 38rpx 38rpx 0 0;
background: #fffdf9;
box-shadow: 0 -12rpx 38rpx rgba(50, 33, 20, 0.16);
}
:deep(.payment-sheet .app-dialog__content) {
align-items: stretch;
padding: 34rpx 42rpx calc(30rpx + env(safe-area-inset-bottom));
text-align: left;
}
:deep(.payment-sheet .app-dialog__content::before) {
display: block;
width: 64rpx;
height: 8rpx;
align-self: center;
margin-bottom: 24rpx;
border-radius: 999rpx;
background: rgba(76, 58, 45, 0.22);
content: "";
}
:deep(.payment-sheet .app-dialog__copy) {
align-items: flex-start;
}
:deep(.payment-sheet .app-dialog__title) {
color: #302319;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(22px, 42rpx, 28px);
letter-spacing: 1rpx;
text-align: left;
}
:deep(.payment-sheet .app-dialog__actions) {
display: flex;
flex-direction: column-reverse;
gap: 4rpx;
margin-top: 28rpx;
}
:deep(.payment-sheet .app-button) {
width: 100%;
min-height: 88rpx;
}
:deep(.payment-sheet .app-button__skin) {
display: none;
}
:deep(.payment-sheet .app-button--primary) {
border-radius: 999rpx;
background: $brand-red;
}
:deep(.payment-sheet .app-button--secondary) {
min-height: 60rpx;
background: transparent;
}
:deep(.payment-sheet .app-button--secondary .app-button__label) {
color: #8a796b;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 400;
}
.sheet-order-summary {
margin-top: 24rpx;
overflow: hidden;
border: 1rpx solid rgba(117, 84, 52, 0.12);
border-radius: 20rpx;
background: #fffaf4;
}
.sheet-order-summary > view,
.payment-method {
display: flex;
min-height: 82rpx;
align-items: center;
justify-content: space-between;
gap: 24rpx;
padding: 0 24rpx;
border-bottom: 1rpx solid rgba(117, 84, 52, 0.1);
color: #5a4432;
font-size: clamp(15px, 24rpx, 18px);
}
.sheet-order-summary > view:last-child {
border-bottom: 0;
}
.sheet-order-summary > view text:last-child,
.payment-method text:last-child {
color: #302319;
font-weight: 700;
text-align: right;
}
.sheet-order-summary > view:last-child text:last-child {
color: $brand-red;
font-family: Georgia, STKaiti, serif;
font-size: clamp(19px, 34rpx, 24px);
}
.payment-method {
width: 100%;
min-height: 88rpx;
box-sizing: border-box;
margin-top: 16rpx;
border: 1rpx solid rgba(117, 84, 52, 0.12);
border-radius: 20rpx;
background: #fffaf4;
}
.payment-method text:last-child {
color: $brand-red;
}
.payment-copy,
.purchase-error {
display: block;
width: 100%;
margin-top: 16rpx;
color: #8a796b;
font-size: clamp(13px, 20rpx, 16px);
line-height: 1.55;
}
.purchase-error {
color: $brand-red;
}
@media (max-width: 340px) {
.page-content {
padding-right: 22rpx;
padding-left: 22rpx;
}
.package-card {
padding-right: 28rpx;
padding-left: 28rpx;
}
.purchase-button {
min-width: 166rpx;
padding-right: 20rpx;
padding-left: 20rpx;
}
}
</style>