update:新增门店vip相关

This commit is contained in:
2026-10-21 15:27:31 +08:00
parent 5bb9df8a03
commit 00f84fc3e4
23 changed files with 874 additions and 215 deletions
+148 -30
View File
@@ -1,14 +1,53 @@
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ModalDialog from "@/components/ModalDialog.vue";
import CoinHeader from "./components/CoinHeader.vue";
import CoinBalancePanel from "./components/CoinBalancePanel.vue";
import CoinQuickMenu from "./components/CoinQuickMenu.vue";
import OfflineExchangeNotice from "./components/OfflineExchangeNotice.vue";
import RewardProductCard from "./components/RewardProductCard.vue";
import { getGiftListAPI, getMyGoldAPI } from "@/apis";
import { quickMenus } from "./data";
import { quickMenus, storeSections, storeVipBenefits } from "./data";
import StoreSectionTabs from "./components/StoreSectionTabs.vue";
import StoreVipCard from "./components/StoreVipCard.vue";
import StoreBenefitCard from "./components/StoreBenefitCard.vue";
import StoreVipFooter from "./components/StoreVipFooter.vue";
import { getSelectedExperienceStore, rememberExperienceStore, loadStoreMembership } from "@/utils/storeVip";
const activeTab = ref("vip");
const membership = ref(null);
const vipLoading = ref(false);
const vipError = ref("");
const displayedBenefits = computed(() => storeVipBenefits.map((benefit) => ({
...benefit, remaining: membership.value?.remaining[benefit.key] ?? "--",
})));
const loadMembership = async () => {
if (!selectedStoreId.value || vipLoading.value) return;
vipLoading.value = true;
vipError.value = "";
membership.value = null;
try {
const result = await loadStoreMembership(selectedStoreId.value);
membership.value = result;
if (result.name) selectedStoreName.value = result.name;
rememberExperienceStore(selectedStoreId.value, selectedStoreName.value);
} catch (error) {
vipError.value = "会员信息加载失败,请点击重试或切换门店";
console.error("加载门店会员失败", error);
} finally {
vipLoading.value = false;
}
};
const selectStore = () => uni.redirectTo({ url: `/pages/coin/nearby-stores?tab=${activeTab.value}` });
const selectTab = (tab) => {
if (storeSections.some((item) => item.key === tab)) activeTab.value = tab;
};
const showVipIntroduction = () => uni.navigateTo({ url: "/pages/coin/vip-intro" });
const vipOpeningVisible = ref(false);
const showVipOpening = () => { vipOpeningVisible.value = true; };
const closeVipOpening = () => { vipOpeningVisible.value = false; };
const PAGE_SIZE = 20;
const coinSummary = ref({
@@ -19,8 +58,8 @@ const selectedStoreId = ref("");
const selectedStoreName = ref("");
const rewardProducts = ref([]);
const productPage = ref(0);
const productTotal = ref(0);
const productLoading = ref(false);
const productError = ref(false);
const productNoMore = ref(false);
const loadCoinSummary = async () => {
@@ -40,6 +79,7 @@ const loadProducts = async ({ reset = false } = {}) => {
const nextPage = reset ? 1 : productPage.value + 1;
productLoading.value = true;
productError.value = false;
if (reset) productNoMore.value = false;
try {
@@ -65,7 +105,6 @@ const loadProducts = async ({ reset = false } = {}) => {
const totalCount = Number(result?.totalCount ?? result?.total);
const responsePageSize =
Number(result?.pageSize ?? result?.page_size) || PAGE_SIZE;
productTotal.value = Number.isFinite(totalCount) ? totalCount : 0;
if (Number.isFinite(pageCount) && pageCount >= 0) {
productNoMore.value = productPage.value >= pageCount;
@@ -79,6 +118,7 @@ const loadProducts = async ({ reset = false } = {}) => {
rewardProducts.value = [];
productPage.value = 0;
}
productError.value = true;
console.error("加载礼品列表失败", error);
} finally {
productLoading.value = false;
@@ -115,7 +155,7 @@ const decodeRouteText = (value = "") => {
const onMenuSelect = (menu) => {
if (menu.key === "nearbyStores") {
uni.redirectTo({
url: "/pages/coin/nearby-stores",
url: `/pages/coin/nearby-stores?tab=${activeTab.value}`,
});
return;
}
@@ -143,50 +183,104 @@ const toProductDetail = (product) => {
};
onLoad((options = {}) => {
const storeId = String(options.storeId || "");
activeTab.value = options.tab === "coin" ? "coin" : "vip";
const previous = getSelectedExperienceStore();
const storeId = String(options.storeId || previous?.id || "");
if (!/^\d+$/.test(storeId) || Number(storeId) <= 0) {
uni.redirectTo({
url: "/pages/coin/nearby-stores",
});
selectStore();
return;
}
selectedStoreId.value = storeId;
selectedStoreName.value = decodeRouteText(options.storeName);
loadCoinSummary();
selectedStoreName.value = options.storeId
? decodeRouteText(options.storeName)
: previous?.name || "";
rememberExperienceStore(storeId, selectedStoreName.value);
loadProducts({ reset: true });
});
// 从介绍、商品等页面返回时刷新会员和余额。
onShow(() => {
if (!selectedStoreId.value) return;
loadMembership();
loadCoinSummary();
});
</script>
<template>
<Container :bgType="6" :isHome="true" @scrolltolower="loadProducts">
<!-- 标题固定,门店切换复用原有附近门店页面。 -->
<Container :bgType="6" bgColor="#22222e" :isHome="true" @scrolltolower="activeTab === 'coin' && loadProducts()">
<template #header>
<CoinHeader title="我的金币" :subtitle="selectedStoreName" />
<CoinHeader title="线下体验中心" :subtitle="selectedStoreName || '选择门店'" switchable @select-store="selectStore" />
</template>
<view class="coin-page">
<CoinBalancePanel
:cumulative="coinSummary.cumulative"
:available="coinSummary.available"
<StoreSectionTabs
:tabs="storeSections"
:active="activeTab"
@select="selectTab"
/>
<CoinQuickMenu :menus="quickMenus" @select="onMenuSelect" />
<OfflineExchangeNotice />
<view class="reward-section">
<view class="reward-grid">
<view
v-for="product in rewardProducts"
:key="product.id"
class="reward-grid__item"
@click="toProductDetail(product)"
>
<RewardProductCard :product="product" />
<!-- 卡片与权益列表共用当前门店的会员数据。 -->
<view v-if="activeTab === 'vip'" class="vip-section">
<view v-if="vipLoading" class="page-status">会员信息加载中...</view>
<view v-else-if="vipError" class="page-status" @click="loadMembership">{{ vipError }}</view>
<StoreVipCard
v-else-if="membership"
:membership="membership"
:benefits="storeVipBenefits"
@introduction="showVipIntroduction"
/>
<view class="vip-section__content">
<text class="vip-section__description">
门店体验中心VIP是为热爱射箭运动的用户专属打造的VIP服务,开通后即可享受以下三大专属权益:
</text>
<view class="vip-section__benefits">
<StoreBenefitCard
v-for="benefit in displayedBenefits"
:key="benefit.key"
:benefit="benefit"
/>
</view>
</view>
<StoreVipFooter v-if="membership" :active="membership.active" @open="showVipOpening" />
</view>
<!-- 保留原金币接口、分页与详情链路。 -->
<view v-else class="coin-section">
<CoinBalancePanel
:cumulative="coinSummary.cumulative"
:available="coinSummary.available"
/>
<CoinQuickMenu :menus="quickMenus" @select="onMenuSelect" />
<OfflineExchangeNotice />
<view class="reward-section">
<view class="reward-grid">
<view
v-for="product in rewardProducts"
:key="product.id"
class="reward-grid__item"
@click="toProductDetail(product)"
>
<RewardProductCard :product="product" />
</view>
</view>
</view>
<view v-if="productLoading" class="page-status">加载中...</view>
<view v-else-if="productError" class="page-status" @click="loadProducts()">加载失败,点击重试</view>
<view v-else-if="productNoMore" class="page-status">{{ rewardProducts.length ? '没有更多商品了~' : '暂无兑换商品~' }}</view>
</view>
</view>
</Container>
<ModalDialog
:show="vipOpeningVisible"
content="请前往线下门店体验中心,联系工作人员开通VIP。"
:show-cancel="false"
confirm-text="我已知晓"
:on-confirm="closeVipOpening"
/>
</template>
<style scoped>
<style scoped lang="scss">
.page-status { padding: 30rpx; color: #cccccc; text-align: center; font-size: 26rpx; }
.coin-page {
width: 100%;
min-height: 100%;
@@ -194,6 +288,30 @@ onLoad((options = {}) => {
box-sizing: border-box;
}
.vip-section {
padding-top: 30rpx;
}
.vip-section__content {
padding: 0 28rpx;
}
.vip-section__description {
display: block;
margin-top: 50rpx;
color: #ffffff;
font-size: 24rpx;
line-height: 34rpx;
}
.vip-section__benefits {
margin-top: 30rpx;
}
.coin-section {
padding-top: 20rpx;
}
.reward-section {
padding: 20rpx 28rpx 40rpx;
box-sizing: border-box;