Files
shoot-miniprograms/src/pages/coin/index.vue
T

335 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.
<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>