update:优化金币

This commit is contained in:
2026-08-20 16:10:17 +08:00
parent 42cfb5c3d8
commit 45957d04b1
14 changed files with 247 additions and 39 deletions
+11
View File
@@ -752,3 +752,14 @@ export const getNearbyStoresAPI = ({
pageSize, pageSize,
}); });
}; };
// 分页获取指定门店的公开金币规则。
export const getStoreGoldRuleListAPI = ({storeId, page = 1, pageSize = 20} = {}) => {
return request(
"GET",
`/gin/api/v1/super-admin/gold-rule/store/${storeId}/list`,
{page, page_size: pageSize},
API_ROOT_URL,
[0, 200]
);
};
+4 -1
View File
@@ -9,7 +9,10 @@ defineProps({
<template> <template>
<view class="empty-state"> <view class="empty-state">
<image src="/static/coin/empty-coin-record.png" mode="aspectFit" /> <image
src="https://static.shelingxingqiu.com/shootmini/static/coin/empty-coin-record.png"
mode="aspectFit"
/>
<text>{{ text }}</text> <text>{{ text }}</text>
</view> </view>
</template> </template>
@@ -4,7 +4,10 @@ const emit = defineEmits(["authorize"]);
<template> <template>
<view class="location-state"> <view class="location-state">
<image src="/static/coin/location-permission.png" mode="aspectFit" /> <image
src="https://static.shelingxingqiu.com/shootmini/static/coin/location-permission.png"
mode="aspectFit"
/>
<text>授权获取你的定位以便推荐附近门店</text> <text>授权获取你的定位以便推荐附近门店</text>
<button hover-class="none" @click="emit('authorize')">立即授权</button> <button hover-class="none" @click="emit('authorize')">立即授权</button>
</view> </view>
@@ -9,7 +9,10 @@ defineProps({
<template> <template>
<view class="exchange-notice" :class="`exchange-notice--${variant}`"> <view class="exchange-notice" :class="`exchange-notice--${variant}`">
<image src="/static/coin/icon-notice.png" mode="aspectFit" /> <image
src="https://static.shelingxingqiu.com/shootmini/static/coin/icon-notice.png"
mode="aspectFit"
/>
<text>暂不支持线上兑换请前往线下门店进行兑换</text> <text>暂不支持线上兑换请前往线下门店进行兑换</text>
</view> </view>
</template> </template>
@@ -1,7 +1,8 @@
<script setup> <script setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
const DEFAULT_PRODUCT_IMAGE = "/static/coin/product-hero-item.png"; const DEFAULT_PRODUCT_IMAGE =
"https://static.shelingxingqiu.com/shootmini/static/coin/product-hero-item.png";
const props = defineProps({ const props = defineProps({
images: { images: {
type: Array, type: Array,
@@ -29,7 +30,7 @@ const onChange = (event) => {
<view class="hero-swiper__item"> <view class="hero-swiper__item">
<image <image
class="hero-swiper__background" class="hero-swiper__background"
src="/static/coin/product-hero-bg.png" src="https://static.shelingxingqiu.com/shootmini/static/coin/product-hero-bg.png"
mode="scaleToFill" mode="scaleToFill"
/> />
<image class="hero-swiper__product" :src="image" mode="aspectFit" /> <image class="hero-swiper__product" :src="image" mode="aspectFit" />
@@ -1,7 +1,8 @@
<script setup> <script setup>
import { computed } from "vue"; import { computed } from "vue";
const DEFAULT_PRODUCT_IMAGE = "/static/coin/product-card-item.png"; const DEFAULT_PRODUCT_IMAGE =
"https://static.shelingxingqiu.com/shootmini/static/coin/product-hero-bg.png";
const props = defineProps({ const props = defineProps({
product: { product: {
+8 -2
View File
@@ -1,14 +1,20 @@
<script setup> <script setup>
defineProps({ const props = defineProps({
store: { store: {
type: Object, type: Object,
required: true, required: true,
}, },
}); });
const emit = defineEmits(["select"]);
const selectStore = () => {
emit("select", props.store);
};
</script> </script>
<template> <template>
<view class="store-card"> <view class="store-card" hover-class="none" @click="selectStore">
<image class="store-card__photo" :src="store.image" mode="aspectFill" /> <image class="store-card__photo" :src="store.image" mode="aspectFill" />
<text class="store-card__name">{{ store.name }}</text> <text class="store-card__name">{{ store.name }}</text>
<view class="store-card__info"> <view class="store-card__info">
+4 -12
View File
@@ -2,29 +2,21 @@ export const quickMenus = [
{ {
key: "rules", key: "rules",
label: "金币规则", label: "金币规则",
icon: "/static/coin/icon-rules.png", icon: "https://static.shelingxingqiu.com/shootmini/static/coin/icon-rules.png",
}, },
{ {
key: "earningRecords", key: "earningRecords",
label: "获取明细", label: "获取明细",
icon: "/static/coin/icon-earning-records.png", icon: "https://static.shelingxingqiu.com/shootmini/static/coin/icon-earning-records.png",
}, },
{ {
key: "exchangeRecords", key: "exchangeRecords",
label: "兑换记录", label: "兑换记录",
icon: "/static/coin/icon-exchange-records.png", icon: "https://static.shelingxingqiu.com/shootmini/static/coin/icon-exchange-records.png",
}, },
{ {
key: "nearbyStores", key: "nearbyStores",
label: "附近门店", label: "附近门店",
icon: "/static/coin/icon-nearby-store.png", icon: "https://static.shelingxingqiu.com/shootmini/static/coin/icon-nearby-store.png",
}, },
]; ];
export const coinRules = [
"金币获取规则:登录个人账号后,使用射灵智能弓体验个人训练、好友约战、排位赛三种模式,每射出一箭并判为有效,即可根据该箭的实际环数,获得等额金币。如:用户在个人训练中,射出一箭并获得9环,即获得9金币。",
"累计金币:累计金币为用户历史获得的所有金币,不会过期、不受兑换影响。",
"可兑换金币:可兑换金币为用户可用于兑换奖品的金币,用户使用金币兑换相应礼品后,将扣除相应金币。同时,可兑换金币有1年有效期,自金币获得之日起开始计算。",
"射灵星球官方可能会不定期举办各类活动,有机会额外获得更多机会,具体以实际公告为准。",
"金币以每个门店/俱乐部为单位独立计算与兑换核销,暂不支持跨门店、跨俱乐部计算,具体以各门店实际公告为准。",
];
+14 -1
View File
@@ -8,6 +8,7 @@ import CoinEmptyState from "./components/CoinEmptyState.vue";
import { getGoldLogAPI } from "@/apis"; import { getGoldLogAPI } from "@/apis";
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
const storeId = ref("");
const earningRecords = ref([]); const earningRecords = ref([]);
const page = ref(0); const page = ref(0);
const total = ref(0); const total = ref(0);
@@ -61,6 +62,7 @@ const loadRecords = async ({ reset = false } = {}) => {
page: nextPage, page: nextPage,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
type: 1, type: 1,
storeId: storeId.value,
}); });
const list = Array.isArray(result?.list) ? result.list : []; const list = Array.isArray(result?.list) ? result.list : [];
const mappedList = list.map(formatRecord); const mappedList = list.map(formatRecord);
@@ -80,7 +82,18 @@ const loadRecords = async ({ reset = false } = {}) => {
} }
}; };
onLoad(() => loadRecords({ reset: true })); onLoad((options = {}) => {
const currentStoreId = String(options.storeId || "");
if (!/^\d+$/.test(currentStoreId) || Number(currentStoreId) <= 0) {
uni.redirectTo({
url: "/pages/coin/nearby-stores",
});
return;
}
storeId.value = currentStoreId;
loadRecords({ reset: true });
});
</script> </script>
<template> <template>
+14 -1
View File
@@ -8,6 +8,7 @@ import CoinEmptyState from "./components/CoinEmptyState.vue";
import { getGoldLogAPI } from "@/apis"; import { getGoldLogAPI } from "@/apis";
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
const storeId = ref("");
const exchangeRecords = ref([]); const exchangeRecords = ref([]);
const page = ref(0); const page = ref(0);
const total = ref(0); const total = ref(0);
@@ -61,6 +62,7 @@ const loadRecords = async ({ reset = false } = {}) => {
page: nextPage, page: nextPage,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
type: 2, type: 2,
storeId: storeId.value,
}); });
const list = Array.isArray(result?.list) ? result.list : []; const list = Array.isArray(result?.list) ? result.list : [];
const mappedList = list.map(formatRecord); const mappedList = list.map(formatRecord);
@@ -80,7 +82,18 @@ const loadRecords = async ({ reset = false } = {}) => {
} }
}; };
onLoad(() => loadRecords({ reset: true })); onLoad((options = {}) => {
const currentStoreId = String(options.storeId || "");
if (!/^\d+$/.test(currentStoreId) || Number(currentStoreId) <= 0) {
uni.redirectTo({
url: "/pages/coin/nearby-stores",
});
return;
}
storeId.value = currentStoreId;
loadRecords({ reset: true });
});
</script> </script>
<template> <template>
+50 -6
View File
@@ -15,6 +15,8 @@ const coinSummary = ref({
cumulative: 0, cumulative: 0,
available: 0, available: 0,
}); });
const selectedStoreId = ref("");
const selectedStoreName = ref("");
const rewardProducts = ref([]); const rewardProducts = ref([]);
const productPage = ref(0); const productPage = ref(0);
const productTotal = ref(0); const productTotal = ref(0);
@@ -23,7 +25,7 @@ const productNoMore = ref(false);
const loadCoinSummary = async () => { const loadCoinSummary = async () => {
try { try {
const result = await getMyGoldAPI(); const result = await getMyGoldAPI(selectedStoreId.value);
coinSummary.value = { coinSummary.value = {
cumulative: Number(result?.totalGold) || 0, cumulative: Number(result?.totalGold) || 0,
available: Number(result?.usableGold) || 0, available: Number(result?.usableGold) || 0,
@@ -86,10 +88,37 @@ const menuRoutes = {
rules: "/pages/coin/rules", rules: "/pages/coin/rules",
earningRecords: "/pages/coin/earning-records", earningRecords: "/pages/coin/earning-records",
exchangeRecords: "/pages/coin/exchange-records", exchangeRecords: "/pages/coin/exchange-records",
nearbyStores: "/pages/coin/nearby-stores", };
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) => { const onMenuSelect = (menu) => {
if (menu.key === "nearbyStores") {
uni.redirectTo({
url: "/pages/coin/nearby-stores",
});
return;
}
const url = menuRoutes[menu.key]; const url = menuRoutes[menu.key];
if (!url) { if (!url) {
uni.showToast({ uni.showToast({
@@ -98,16 +127,31 @@ const onMenuSelect = (menu) => {
}); });
return; return;
} }
uni.navigateTo({ url }); uni.navigateTo({
url: buildStorePageUrl(url),
});
}; };
const toProductDetail = (product) => { const toProductDetail = (product) => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/coin/product-detail?id=${product.id}`, url: buildStorePageUrl(
"/pages/coin/product-detail",
`id=${encodeURIComponent(product.id)}`
),
}); });
}; };
onLoad(() => { onLoad((options = {}) => {
const storeId = String(options.storeId || "");
if (!/^\d+$/.test(storeId) || Number(storeId) <= 0) {
uni.redirectTo({
url: "/pages/coin/nearby-stores",
});
return;
}
selectedStoreId.value = storeId;
selectedStoreName.value = decodeRouteText(options.storeName);
loadCoinSummary(); loadCoinSummary();
loadProducts({ reset: true }); loadProducts({ reset: true });
}); });
@@ -116,7 +160,7 @@ onLoad(() => {
<template> <template>
<Container :bgType="6" :isHome="true" @scrolltolower="loadProducts"> <Container :bgType="6" :isHome="true" @scrolltolower="loadProducts">
<template #header> <template #header>
<CoinHeader title="我的金币" /> <CoinHeader title="我的金币" :subtitle="selectedStoreName" />
</template> </template>
<view class="coin-page"> <view class="coin-page">
<CoinBalancePanel <CoinBalancePanel
+25 -2
View File
@@ -8,7 +8,8 @@ import StoreCard from "./components/StoreCard.vue";
import { getNearbyStoresAPI } from "@/apis"; import { getNearbyStoresAPI } from "@/apis";
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
const DEFAULT_STORE_IMAGE = "/static/coin/store-photo.png"; const DEFAULT_STORE_IMAGE =
"https://static.shelingxingqiu.com/shootmini/static/coin/store-photo.png";
const showPermissionState = ref(false); const showPermissionState = ref(false);
const stores = ref([]); const stores = ref([]);
const location = ref(null); const location = ref(null);
@@ -100,6 +101,23 @@ const authorizeLocation = () => {
}); });
}; };
// 选择门店后替换当前页面,返回时直接回到进入金币模块前的页面。
const selectStore = (store) => {
const storeId = String(store?.id || "");
if (!/^\d+$/.test(storeId) || Number(storeId) <= 0) {
uni.showToast({
title: "门店信息无效",
icon: "none",
});
return;
}
const storeName = encodeURIComponent(store?.name || "");
uni.redirectTo({
url: `/pages/coin/index?storeId=${encodeURIComponent(storeId)}&storeName=${storeName}`,
});
};
onLoad(locateAndLoadStores); onLoad(locateAndLoadStores);
</script> </script>
@@ -114,7 +132,12 @@ onLoad(locateAndLoadStores);
@authorize="authorizeLocation" @authorize="authorizeLocation"
/> />
<template v-else> <template v-else>
<StoreCard v-for="store in stores" :key="store.id" :store="store" /> <StoreCard
v-for="store in stores"
:key="store.id"
:store="store"
@select="selectStore"
/>
<text v-if="loaded && !stores.length" class="stores-page__more"> <text v-if="loaded && !stores.length" class="stores-page__more">
附近暂无门店~ 附近暂无门店~
</text> </text>
+100 -5
View File
@@ -1,21 +1,104 @@
<script setup> <script setup>
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import CoinHeader from "./components/CoinHeader.vue"; import CoinHeader from "./components/CoinHeader.vue";
import { coinRules } from "./data"; import CoinEmptyState from "./components/CoinEmptyState.vue";
import { getStoreGoldRuleListAPI } from "@/apis";
const PAGE_SIZE = 20;
const storeId = ref("");
const coinRules = ref([]);
const page = ref(0);
const loading = ref(false);
const noMore = ref(false);
const loaded = ref(false);
const showEmpty = computed(() => loaded.value && !coinRules.value.length);
const updatePaginationState = (result, list, nextPage) => {
const currentPage = 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;
page.value = currentPage;
if (Number.isFinite(pageCount) && pageCount >= 0) {
noMore.value = currentPage >= pageCount;
return;
}
if (Number.isFinite(totalCount) && totalCount >= 0) {
noMore.value = coinRules.value.length >= totalCount;
return;
}
noMore.value = list.length < responsePageSize;
};
const loadRules = async ({ reset = false } = {}) => {
if (loading.value || (!reset && noMore.value)) return;
const nextPage = reset ? 1 : page.value + 1;
loading.value = true;
if (reset) noMore.value = false;
try {
const result = await getStoreGoldRuleListAPI({
storeId: storeId.value,
page: nextPage,
pageSize: PAGE_SIZE,
});
const list = Array.isArray(result?.list) ? result.list : [];
const mappedList = list.map((item) => ({
id: item.id,
content: item.content || "",
}));
coinRules.value = reset ? mappedList : coinRules.value.concat(mappedList);
updatePaginationState(result, list, nextPage);
} catch (error) {
if (reset) {
coinRules.value = [];
page.value = 0;
}
console.error("加载金币规则失败", error);
} finally {
loading.value = false;
loaded.value = true;
}
};
onLoad((options = {}) => {
const currentStoreId = String(options.storeId || "");
if (!/^\d+$/.test(currentStoreId) || Number(currentStoreId) <= 0) {
uni.redirectTo({
url: "/pages/coin/nearby-stores",
});
return;
}
storeId.value = currentStoreId;
loadRules({ reset: true });
});
</script> </script>
<template> <template>
<Container :bgType="6" :isHome="true"> <Container :bgType="6" :isHome="true" @scrolltolower="loadRules">
<template #header> <template #header>
<CoinHeader title="金币规则" /> <CoinHeader title="金币规则" />
</template> </template>
<view class="rules-page"> <view class="rules-page">
<CoinEmptyState v-if="showEmpty" text="暂无金币规则。" />
<template v-else>
<view <view
v-for="(rule, index) in coinRules" v-for="rule in coinRules"
:key="index" :key="rule.id"
class="rules-page__item" class="rules-page__item"
> >
<text>{{ index + 1 }}{{ rule }}</text> <rich-text class="rules-page__content" :nodes="rule.content" />
</view>
</template>
<view v-if="loading" class="rules-page__status">
<text>加载中...</text>
</view> </view>
</view> </view>
</Container> </Container>
@@ -35,4 +118,16 @@ import { coinRules } from "./data";
line-height: 52rpx; line-height: 52rpx;
text-align: justify; text-align: justify;
} }
.rules-page__content {
display: block;
}
.rules-page__status {
padding: 12rpx 0 20rpx;
color: rgba(255, 255, 255, 0.6);
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
}
</style> </style>
+1 -1
View File
@@ -26,7 +26,7 @@ const toFristTryPage = async () => {
}; };
const toCoinPage = () => { const toCoinPage = () => {
uni.navigateTo({ uni.navigateTo({
url: "/pages/coin/index", url: "/pages/coin/nearby-stores",
}); });
}; };
const toBeVipPage = () => { const toBeVipPage = () => {