feat: migrate app routes and business modules

This commit is contained in:
2026-08-12 18:22:59 +08:00
parent 555aa00043
commit cc706378c2
247 changed files with 28623 additions and 14988 deletions
+358
View File
@@ -0,0 +1,358 @@
<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>