Files
jiapuapp/pages/profile/vip.vue
T

359 lines
10 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/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">会员服务</text>
<text class="vip-intro__copy">{{ purchaseSummary }}</text>
</view>
</view>
<view v-if="readState === 'loading'" class="state-card">
<AppLoading text="正在读取 VIP 服务信息" />
</view>
<view v-else-if="readState === 'error'" class="state-card">
<text>暂时无法读取 VIP 服务信息</text>
<text>{{ vipReadError || "请检查网络后重新加载。" }}</text>
<AppButton block type="secondary" label="重新加载" @click="loadVipData" />
</view>
<template v-else>
<view class="vip-section">
<view class="vip-section__heading">
<text>可选套餐</text>
<text>{{ capability.enabled ? "购买资格已开放" : "当前仅供查看" }}</text>
</view>
<view v-if="packages.length" class="vip-package-list">
<view v-for="item in packages" :key="item.key" class="vip-package">
<view class="vip-package__copy">
<text>{{ item.name }}</text>
<text v-if="item.description">{{ item.description }}</text>
</view>
<view class="vip-package__price">
<text>{{ item.price }}</text>
<text v-if="item.originalPrice">原价 {{ item.originalPrice }}</text>
</view>
</view>
</view>
<view v-else class="vip-empty-state"><text>当前没有可展示的套餐</text></view>
</view>
<view class="vip-section">
<view class="vip-section__heading">
<text>订单记录</text>
<text>仅供查看</text>
</view>
<view v-if="orders.length" class="vip-order-list">
<view v-for="item in orders" :key="item.key" class="vip-order">
<view>
<text>{{ item.packageName }}</text>
<text v-if="item.paidAt || item.expiresAt">{{ item.paidAt || item.expiresAt }}</text>
</view>
<view>
<text>{{ item.amount }}</text>
<text>状态{{ item.statusLabel }}</text>
</view>
</view>
</view>
<view v-else class="vip-empty-state"><text>当前没有订单记录</text></view>
</view>
</template>
<AppButton block label="查看购买说明" @click="openServiceNotice" />
</view>
<AppDialog
:visible="serviceNoticeVisible"
:title="capability.enabled ? '购买功能说明' : '当前不能在线购买'"
confirm-text="我知道了"
:close-on-mask="false"
@confirm="closeServiceNotice"
>
<text class="notice-copy">{{ serviceNoticeCopy }}</text>
</AppDialog>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, 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 {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { vipApi } from "@/services/api/vip-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { handleBackPress, runBackGuard } from "@/utils/navigation/gateway.js";
const packages = ref([]);
const orders = ref([]);
const capability = reactive({ enabled: false, disabledReason: "" });
const readState = ref("loading");
const vipReadError = ref("");
const serviceNoticeVisible = ref(false);
const packageController = createRequestController();
const orderController = createRequestController();
const capabilityController = createRequestController();
let active = true;
const purchaseSummary = computed(() =>
capability.enabled
? "当前可以购买会员,支付功能正在完成最后确认。"
: capability.disabledReason || "当前可查看套餐和订单,暂时不能在线购买。",
);
const serviceNoticeCopy = computed(() =>
capability.enabled
? "你的账号目前可以购买会员。为避免重复扣款,支付按钮将在确认完成后开放;现在可以先查看套餐和订单。"
: capability.disabledReason || "当前可查看套餐和已有订单,暂时不能在线购买。",
);
const loadVipData = async () => {
capabilityController.abort();
packageController.abort();
orderController.abort();
readState.value = "loading";
vipReadError.value = "";
try {
const [capabilityResult, packageRows, orderRows] = await Promise.all([
vipApi.getVipCapability({ requestController: capabilityController }),
vipApi.getVipPackages({ requestController: packageController }),
vipApi.getVipOrders({ requestController: orderController }),
]);
if (!active) return;
capability.enabled = capabilityResult.enabled;
capability.disabledReason = capabilityResult.disabledReason;
packages.value = packageRows;
orders.value = orderRows;
readState.value = "ready";
} catch (error) {
if (!active || isRequestCancelled(error)) return;
vipReadError.value = getRequestErrorMessage(error, "请稍后重试。");
readState.value = "error";
}
};
const openServiceNotice = () => {
serviceNoticeVisible.value = true;
};
const closeServiceNotice = () => {
serviceNoticeVisible.value = false;
};
const requestBack = () =>
runBackGuard({
transientOpen: serviceNoticeVisible.value,
"close-transient": closeServiceNotice,
});
onBackPress((event) => handleBackPress(event, requestBack));
onLoad(loadVipData);
onUnload(() => {
active = false;
capabilityController.abort();
packageController.abort();
orderController.abort();
});
</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;
}
.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,
.state-card text,
.vip-package text,
.vip-order text,
.vip-empty-state text,
.notice-copy {
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,
.vip-section {
@include adaptive-profile-content;
}
.state-card {
display: flex;
min-height: 184rpx;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 20rpx;
padding: 34rpx 42rpx;
text-align: center;
}
.state-card text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(19px, 34rpx, 24px);
font-weight: 700;
}
.state-card text:last-child,
.notice-copy {
margin-top: 16rpx;
color: $ink-muted;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.6;
}
.vip-section {
margin-top: 20rpx;
padding: 24rpx;
}
.vip-section__heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16rpx;
}
.vip-section__heading text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(18px, 30rpx, 22px);
font-weight: 700;
}
.vip-section__heading text:last-child {
color: $ink-muted;
font-size: clamp(12px, 20rpx, 15px);
}
.vip-package-list,
.vip-order-list {
display: grid;
gap: 14rpx;
margin-top: 18rpx;
}
.vip-package,
.vip-order {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
padding: 22rpx;
border: 1rpx solid rgba(128, 89, 49, 0.22);
border-radius: 12rpx;
background: rgba(255, 252, 245, 0.68);
}
.vip-package__copy,
.vip-order > view {
min-width: 0;
flex: 1;
}
.vip-package__copy text:first-child,
.vip-order > view:first-child text:first-child {
color: $ink;
font-size: clamp(15px, 25rpx, 19px);
font-weight: 700;
line-height: 1.45;
}
.vip-package__copy text:last-child,
.vip-order > view:first-child text:last-child,
.vip-order > view:last-child text:last-child {
margin-top: 7rpx;
color: $ink-muted;
font-size: clamp(12px, 20rpx, 15px);
line-height: 1.45;
}
.vip-package__price,
.vip-order > view:last-child {
flex: 0 0 auto;
text-align: right;
}
.vip-package__price text:first-child,
.vip-order > view:last-child text:first-child {
color: $brand-red;
font-size: clamp(16px, 28rpx, 21px);
font-weight: 700;
}
.vip-package__price text:last-child {
margin-top: 7rpx;
color: #a49382;
font-size: clamp(11px, 18rpx, 14px);
text-decoration: line-through;
}
.vip-empty-state {
margin-top: 18rpx;
padding: 34rpx 20rpx;
border: 1rpx dashed rgba(128, 89, 49, 0.24);
border-radius: 12rpx;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
text-align: center;
}
.page-content > .app-button {
width: 100%;
margin-top: 26rpx;
}
.notice-copy {
margin: 0;
}
@media (max-width: 340px) {
.page-content {
padding-right: 20rpx;
padding-left: 20rpx;
}
}
</style>