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

@@ -69,6 +69,13 @@
<!-- 下方主内容卡片区域 -->
<view class="main-container">
<navigator v-if="showPaidMemberEntry" class="paid-entry" :style="{ color: paidEntryColor }" url="/pages/mine/paidMember" hover-class="none">
<image v-if="paidEntryBackground" class="entry-bg" :src="paidEntryBackground" mode="aspectFill" />
<view class="entry-content">
<view><text class="entry-title">{{ paidEntryTitle }}</text><text class="entry-subtitle">{{ paidEntrySubtitle }}</text></view>
<u-icon name="arrow-right" size="14" :color="paidEntryColor" />
</view>
</navigator>
<!-- 我的订单卡片 -->
<view class="card-box order-box">
<view class="card-title-bar">
@@ -132,6 +139,7 @@ import {
onNavigationBarButtonTap,
} from '@dcloudio/uni-app'
import tool from '@/pages/tabbar/user/utils/tool.vue'
import { getPaidMember } from '@/api/paid-member'
import { getCouponsNum, getFootprintNum } from '@/api/members.js'
import { getUserWallet } from '@/api/members'
import configs from '@/config/config'
@@ -154,6 +162,22 @@ const aiderLightColor = computed(() => store.getters.aiderLightColor)
const userImage = configs.defaultUserPhoto
const userInfo = ref<any>({})
const paidMemberInfo = ref<any>(null)
let paidMemberRequestId = 0
const showPaidMemberEntry = computed(() => !!paidMemberInfo.value?.config
&& !!(paidMemberInfo.value.config.enabled || paidMemberInfo.value.active || paidMemberInfo.value.status === 'EXPIRED'))
const paidEntryTitle = computed(() => paidMemberInfo.value?.active
? paidMemberInfo.value.config?.openedText || '我的付费会员'
: paidMemberInfo.value?.config?.unopenedText || '开通付费会员,享专属权益')
const paidEntryColor = computed(() => (paidMemberInfo.value?.active
? paidMemberInfo.value.config?.openedFontColor
: paidMemberInfo.value?.config?.unopenedFontColor) || '#f4dfb9')
const paidEntryBackground = computed(() => paidMemberInfo.value?.active
? paidMemberInfo.value.config?.openedBackground
: paidMemberInfo.value?.config?.unopenedBackground)
const paidEntrySubtitle = computed(() => paidMemberInfo.value?.active
? '有效期至 ' + paidMemberInfo.value.endTime
: paidMemberInfo.value?.status === 'EXPIRED' ? '会员已到期,续费继续享权益' : '开通享商品优惠与专属权益')
const couponNum = ref<string | number>('')
const footNum = ref<string | number>('')
const walletNum = ref<string | number>('')
@@ -189,6 +213,26 @@ function resetMemberGrade() {
gradeIconFailed.value = false
}
/** 个人中心显示时读取会员配置及资格;退出或切换账号后丢弃旧请求结果。 */
async function loadPaidMemberInfo() {
const requestId = ++paidMemberRequestId
const memberId = String(userInfo.value.id || '')
paidMemberInfo.value = null
if (!memberId) return
try {
const res = await getPaidMember()
if (requestId !== paidMemberRequestId
|| String(userInfo.value.id || '') !== memberId
|| String((isLogin() || {}).id || '') !== memberId) return
if (!res.data?.success) throw new Error(res.data?.message || '会员服务返回异常')
paidMemberInfo.value = res.data.result || null
} catch (error: any) {
if (requestId === paidMemberRequestId) {
console.warn('[付费会员] 入口加载失败', error?.message || '请求失败')
}
}
}
function onGradeBackgroundError() {
gradeBackgroundFailed.value = true
gradeBackgroundLoaded.value = false
@@ -218,6 +262,8 @@ watch(() => [store.state.userInfo?.id, store.state.hasLogin], () => {
if (String(account.id || '') !== String(userInfo.value.id || '')) {
userInfo.value = account
resetMemberGrade()
paidMemberRequestId++
paidMemberInfo.value = null
walletNum.value = 0
couponNum.value = 0
footNum.value = 0
@@ -256,9 +302,11 @@ async function refreshUserPage() {
userInfo.value = isLogin() || {}
try {
if (isLogin('auth')) {
await Promise.allSettled([loadMemberGrade(), getUserOrderNum()])
await Promise.allSettled([loadMemberGrade(), loadPaidMemberInfo(), getUserOrderNum()])
} else {
resetMemberGrade()
paidMemberRequestId++
paidMemberInfo.value = null
walletNum.value = 0
couponNum.value = 0
footNum.value = 0
@@ -313,6 +361,11 @@ async function getUserOrderNum() {
</script>
<style lang="scss" scoped>
.paid-entry { position: relative; overflow: hidden; border-radius: 20rpx; background: linear-gradient(110deg, #423c36, #756149); margin-bottom: 20rpx; }
.entry-bg { position: absolute; inset: 0; width: 100%; height: 100%; }
.entry-content { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 20rpx; padding: 25rpx 28rpx; }
.entry-title { font-size: 27rpx; font-weight: 600; display: block; }
.entry-subtitle { font-size: 21rpx; opacity: .75; display: block; margin-top: 8rpx; }
.user-page {
min-height: 100vh;
background-color: #f5f6f9;