335 lines
10 KiB
Vue
335 lines
10 KiB
Vue
<script setup>
|
||
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, 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({
|
||
cumulative: 0,
|
||
available: 0,
|
||
});
|
||
const selectedStoreId = ref("");
|
||
const selectedStoreName = ref("");
|
||
const rewardProducts = ref([]);
|
||
const productPage = ref(0);
|
||
const productLoading = ref(false);
|
||
const productError = ref(false);
|
||
const productNoMore = ref(false);
|
||
|
||
const loadCoinSummary = async () => {
|
||
try {
|
||
const result = await getMyGoldAPI(selectedStoreId.value);
|
||
coinSummary.value = {
|
||
cumulative: Number(result?.totalGold) || 0,
|
||
available: Number(result?.usableGold) || 0,
|
||
};
|
||
} catch (error) {
|
||
console.error("加载金币统计失败", error);
|
||
}
|
||
};
|
||
|
||
const loadProducts = async ({ reset = false } = {}) => {
|
||
if (productLoading.value || (!reset && productNoMore.value)) return;
|
||
|
||
const nextPage = reset ? 1 : productPage.value + 1;
|
||
productLoading.value = true;
|
||
productError.value = false;
|
||
if (reset) productNoMore.value = false;
|
||
|
||
try {
|
||
const result = await getGiftListAPI({
|
||
page: nextPage,
|
||
pageSize: PAGE_SIZE,
|
||
storeId: selectedStoreId.value,
|
||
});
|
||
const list = Array.isArray(result?.list) ? result.list : [];
|
||
const mappedList = list.map((item) => ({
|
||
id: item.id,
|
||
name: item.name || "",
|
||
cost: Number(item.coin_price) || 0,
|
||
stock: Number(item.stock) || 0,
|
||
image: item.cover_image || "",
|
||
}));
|
||
|
||
rewardProducts.value = reset
|
||
? mappedList
|
||
: rewardProducts.value.concat(mappedList);
|
||
productPage.value = Number(result?.page) || nextPage;
|
||
const pageCount = Number(result?.pageCount ?? result?.page_count);
|
||
const totalCount = Number(result?.totalCount ?? result?.total);
|
||
const responsePageSize =
|
||
Number(result?.pageSize ?? result?.page_size) || PAGE_SIZE;
|
||
|
||
if (Number.isFinite(pageCount) && pageCount >= 0) {
|
||
productNoMore.value = productPage.value >= pageCount;
|
||
} else if (Number.isFinite(totalCount) && totalCount >= 0) {
|
||
productNoMore.value = rewardProducts.value.length >= totalCount;
|
||
} else {
|
||
productNoMore.value = list.length < responsePageSize;
|
||
}
|
||
} catch (error) {
|
||
if (reset) {
|
||
rewardProducts.value = [];
|
||
productPage.value = 0;
|
||
}
|
||
productError.value = true;
|
||
console.error("加载礼品列表失败", error);
|
||
} finally {
|
||
productLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const menuRoutes = {
|
||
rules: "/pages/coin/rules",
|
||
earningRecords: "/pages/coin/earning-records",
|
||
exchangeRecords: "/pages/coin/exchange-records",
|
||
};
|
||
|
||
const buildStorePageUrl = (path, extraQuery = "") => {
|
||
const query = [
|
||
`storeId=${encodeURIComponent(selectedStoreId.value)}`,
|
||
`storeName=${encodeURIComponent(selectedStoreName.value)}`,
|
||
extraQuery,
|
||
]
|
||
.filter(Boolean)
|
||
.join("&");
|
||
return `${path}?${query}`;
|
||
};
|
||
|
||
const decodeRouteText = (value = "") => {
|
||
const text = String(value);
|
||
try {
|
||
return decodeURIComponent(text.replace(/\+/g, " "));
|
||
} catch (error) {
|
||
console.error("门店名称解码失败", error);
|
||
return text;
|
||
}
|
||
};
|
||
|
||
const onMenuSelect = (menu) => {
|
||
if (menu.key === "nearbyStores") {
|
||
uni.redirectTo({
|
||
url: `/pages/coin/nearby-stores?tab=${activeTab.value}`,
|
||
});
|
||
return;
|
||
}
|
||
|
||
const url = menuRoutes[menu.key];
|
||
if (!url) {
|
||
uni.showToast({
|
||
title: "功能开发中",
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
uni.navigateTo({
|
||
url: buildStorePageUrl(url),
|
||
});
|
||
};
|
||
|
||
const toProductDetail = (product) => {
|
||
uni.navigateTo({
|
||
url: buildStorePageUrl(
|
||
"/pages/coin/product-detail",
|
||
`id=${encodeURIComponent(product.id)}`
|
||
),
|
||
});
|
||
};
|
||
|
||
onLoad((options = {}) => {
|
||
activeTab.value = options.tab === "coin" ? "coin" : "vip";
|
||
const previous = getSelectedExperienceStore();
|
||
const storeId = String(options.storeId || previous?.id || "");
|
||
if (!/^\d+$/.test(storeId) || Number(storeId) <= 0) {
|
||
selectStore();
|
||
return;
|
||
}
|
||
selectedStoreId.value = storeId;
|
||
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" bgColor="#22222e" :isHome="true" @scrolltolower="activeTab === 'coin' && loadProducts()">
|
||
<template #header>
|
||
<CoinHeader title="线下体验中心" :subtitle="selectedStoreName || '选择门店'" switchable @select-store="selectStore" />
|
||
</template>
|
||
<view class="coin-page">
|
||
<StoreSectionTabs
|
||
:tabs="storeSections"
|
||
:active="activeTab"
|
||
@select="selectTab"
|
||
/>
|
||
|
||
<!-- 卡片与权益列表共用当前门店的会员数据。 -->
|
||
<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 lang="scss">
|
||
.page-status { padding: 30rpx; color: #cccccc; text-align: center; font-size: 26rpx; }
|
||
.coin-page {
|
||
width: 100%;
|
||
min-height: 100%;
|
||
padding-bottom: 40rpx;
|
||
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;
|
||
}
|
||
|
||
.reward-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.reward-grid__item {
|
||
width: 336rpx;
|
||
margin-right: 22rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.reward-grid__item:nth-child(2n) {
|
||
margin-right: 0;
|
||
}
|
||
</style>
|