feat(paid-member): 添加付费会员功能模块

- 新增付费会员API接口层,支持会员查询、下单、记录及报价
- 新增付费会员页面,展示权益、推荐商品及购买流程
- 新增付费会员购买记录页面,支持订单状态查看与继续支付
- 新增 PaidMemberPrice 组件用于商品列表和详情页展示会员价
- 个人中心页添加付费会员入口,支持配置化展示标题与背景
- 商品详情页集成会员价展示,支持秒杀和拼团场景下隐藏
- 商品列表组件集成会员价展示,支持批量请求与防竞态
- 支付页适配付费会员支付流程,支持H5轮询与结果回调
- 订单确认页优惠文案更新为"活动优惠(含会员优惠)"
- pages.json 新增付费会员及购买记录页面路由配置
- README 更新升级计划,按时间倒序补充已完成功能明细
This commit is contained in:
pikachu1995@126.com
2026-11-05 22:37:12 +08:00
parent 06af86af3f
commit cc71d20aa6
12 changed files with 1244 additions and 973 deletions

View File

@@ -20,6 +20,7 @@
}}
</div>
</view>
<PaidMemberPrice :quote="paidQuotes[item.id]" />
<div class="promotion" @click="navigateToDetailPage(item)">
<div v-for="(promotionItem,promotionIndex) in getPromotion(item)" :key="promotionIndex">
<span class="promotion-tag" v-if="promotionItem.indexOf('COUPON') != -1">劵</span>
@@ -80,6 +81,7 @@
<u-icon name="shopping-cart" size="16" :color="mainColor"></u-icon>
</div>
</view>
<PaidMemberPrice v-if="flag('price')" :quote="paidQuotes[item.id]" />
<div class="store-seller-name" v-if="storeName" @click="navigateToStoreDetailPage(item)">
<view class="store-name-row">
<text class="self-tag" v-if="item.selfOperated && flag('selfTag')">自营</text>
@@ -96,7 +98,11 @@
</template>
<script setup lang="ts">
import { computed, watch } from 'vue'
import { computed, watch, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import PaidMemberPrice from '@/components/paid-member-price/index.vue'
import { getPaidMemberQuotes } from '@/api/paid-member'
import { isLogin } from '@/utils/filters'
import { useStore } from '@/store'
import { useGoodsListCommon } from './common'
import { goodsFormatPrice } from '@/utils/filters.js'
@@ -123,6 +129,21 @@ const props = withDefaults(defineProps<{
})
const emit = defineEmits(['addCart'])
const paidQuotes = ref<Record<string, any>>({})
let paidRequest = 0
async function loadPaidQuotes() {
const request = ++paidRequest, memberId = String(isLogin()?.id || '')
paidQuotes.value = {}
if (!memberId) return
const ids = [...new Set((props.res || []).map(item => String(item.id || '')).filter(Boolean))]
try {
const results = await Promise.all(Array.from({length:Math.ceil(ids.length/100)},(_,i)=>getPaidMemberQuotes(ids.slice(i*100,i*100+100))))
if (request !== paidRequest || String(isLogin()?.id || '') !== memberId) return
paidQuotes.value = Object.assign({}, ...results.filter(res=>res.data?.success).map(res=>res.data.result || {}))
} catch {}
}
watch(() => [(props.res || []).map(item=>item.id).join(','), store.state.userInfo?.id, store.state.hasLogin], loadPaidQuotes, {immediate:true})
onShow(loadPaidQuotes)
// 存量字段(促销/销量/自营/价格):showFields 为 null 时维持现状渲染
const flag = (key: string) => props.showFields == null || props.showFields[key] !== false

View File

@@ -0,0 +1,6 @@
<template><view v-if="quote" class="paid-price" @click.stop="open"><image v-if="quote.label" :src="quote.label" mode="aspectFit" /><text>会员价 ¥{{ Number(quote.price).toFixed(2) }}</text><text v-if="!quote.eligible" class="open">开通享 ›</text></view></template>
<script setup>
defineProps({quote:{type:Object,default:null}})
function open(){uni.navigateTo({url:'/pages/mine/paidMember'})}
</script>
<style scoped>.paid-price{display:flex;align-items:center;flex-wrap:wrap;gap:6rpx;margin-top:8rpx;color:#a77c3e;font-size:22rpx;line-height:1.6}.paid-price image{width:28rpx;height:28rpx}.open{font-size:20rpx;opacity:.8}</style>