mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-11-06 17:09:01 +08:00
feat(paid-member): 添加付费会员功能模块
- 新增付费会员API接口层,支持会员查询、下单、记录及报价 - 新增付费会员页面,展示权益、推荐商品及购买流程 - 新增付费会员购买记录页面,支持订单状态查看与继续支付 - 新增 PaidMemberPrice 组件用于商品列表和详情页展示会员价 - 个人中心页添加付费会员入口,支持配置化展示标题与背景 - 商品详情页集成会员价展示,支持秒杀和拼团场景下隐藏 - 商品列表组件集成会员价展示,支持批量请求与防竞态 - 支付页适配付费会员支付流程,支持H5轮询与结果回调 - 订单确认页优惠文案更新为"活动优惠(含会员优惠)" - pages.json 新增付费会员及购买记录页面路由配置 - README 更新升级计划,按时间倒序补充已完成功能明细
This commit is contained in:
46
README.md
46
README.md
@@ -160,18 +160,44 @@ lilishop-uniapp
|
|||||||
|
|
||||||
## 8. 升级计划
|
## 8. 升级计划
|
||||||
|
|
||||||
> 更新日期:2026-11-04,依据各端近期提交及当前代码实现同步。
|
> 更新日期:2026-11-05,依据各端提交记录及当前代码实现同步。
|
||||||
|
|
||||||
### ✅ 已完成特性
|
### ✅ 已完成特性
|
||||||
| 功能模块 | 功能模块 |
|
|
||||||
| :--- | :--- |
|
按可核实的实现或完善时间倒序记录;日期表示代码完成或功能更新时间,不代表生产上线时间。
|
||||||
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
|
|
||||||
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
|
| 时间 | 功能模块 | 完成内容 |
|
||||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
| :--- | :--- | :--- |
|
||||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
| 2026-11-05 | ✅ 付费会员 | 单一付费会员、开通与续费、有效期、关联权益、商家与商品折扣、平台补贴及付费记录。 |
|
||||||
| ✅ 卡密商品 | ✅ 二级分销 |
|
| 2026-11-04 | ✅ 客户等级体系 / 客户权益 | 等级配置、权益介绍、经验值任务与明细、权益发放及重试,提供默认等级与权益数据。 |
|
||||||
| ✅ 定时任务管理(数据库配置驱动) | ✅ 预约服务商品 |
|
| 2026-11-04 | ✅ 小程序订单管理 | 增加小程序订单查询入口及微信订单自动上报开关。 |
|
||||||
| ✅ 主题色管理 | ✅ 立减优惠券 |
|
| 2026-11-04 | ✅ 立减优惠券 | 新增立减券,完善优惠券校验、领取与订单优惠计算。 |
|
||||||
|
| 2026-11-04 | ✅ 商品定时上下架 | 完善定时上下架计划管理、独立列表及执行校验。 |
|
||||||
|
| 2026-11-04 | ✅ 微信小程序原生能力 | 接入微信地址导入、头像选择和昵称填写。 |
|
||||||
|
| 2026-11-02 | ✅ 消息模板 | 支持多渠道模板矩阵维护,统一站内信、短信、微信公众号及小程序订阅消息的场景配置和异步发送。 |
|
||||||
|
| 2026-10-30 | ✅ PC 页面装修 | 重构楼层组件,支持自适应高度、可视化热区绘制及配置交互优化。 |
|
||||||
|
| 2026-10-29 | ✅ 定时任务管理 | 重构为数据库配置驱动,支持运营后台维护任务。 |
|
||||||
|
| 2026-10-29 | ✅ 敏感词管理 | 支持按业务场景配置过滤开关,并优化词库加载。 |
|
||||||
|
| 2026-10-29 | ✅ PC 店铺搜索 | 新增店铺列表,支持商品与店铺双模式搜索。 |
|
||||||
|
| 2026-10-22 | ✅ 分类设置 | 支持四种分类页模板选择、商品列表展示字段开关及模板图片预览。 |
|
||||||
|
| 2026-10-20 | ✅ 移动端页面装修 | 完善可视化编辑器,新增首页组合组件及优惠券、店铺、文章等楼层。 |
|
||||||
|
| 2026-10-09 | ✅ 主题色管理 | 完善主题设置,支持自定义模式。 |
|
||||||
|
| 2026-10-02 | ✅ 预约服务商品 | 完善预约商品、预约交易及订单详情功能。 |
|
||||||
|
| 2026-08-05 | ✅ 二级分销 | 由一级分销扩展为二级分销,增加节点配置及分销员中心。 |
|
||||||
|
| 2026-08-03 | ✅ 卡密商品 | 完善电子卡券履约、订单状态及卡池管理。 |
|
||||||
|
| 2026-07-23 | ✅ 素材库管理 | 增加文件分类、目录管理及目录归属校验。 |
|
||||||
|
| 2026-07-20 | ✅ 经营统计报表 | 新增店铺业绩、商品同比环比、销售订单明细及商品销售汇总报表。 |
|
||||||
|
| 2026-07-13 | ✅ 腾讯云直播 | 接入并完善直播、聊天与直播商品相关功能。 |
|
||||||
|
| 2026-06-30 | ✅ 移动端技术升级 | 升级 Vue 3 与 uView Plus,适配组件和页面。 |
|
||||||
|
| 2026-06-29 | ✅ 电子面单发货 | 完善快递鸟电子面单,支持自动携带运单号发货。 |
|
||||||
|
| 2026-06-05 | ✅ 后端与管理端框架升级 | 5 月 25 日至 6 月 5 日完成 Spring Boot 4、管理端 Vue 3 与 Element Plus 升级。 |
|
||||||
|
| 2026-05-22 | ✅ 购物卡 | 新增礼品卡接口,以及选择、绑定、激活和列表管理。 |
|
||||||
|
| 2026-05-20 | ✅ 微信提现 | 完善转账状态查询、确认回调和重试处理,后续补充提现规则说明配置。 |
|
||||||
|
| 2026-04-16 | ✅ 置顶评论 | 支持评论置顶及商品评价管理。 |
|
||||||
|
| 2026-04-13 | ✅ 专用发票(发票上传) | 完善发票信息字段、发票详情及管理展示。 |
|
||||||
|
| 2026-04-01 | ✅ 商品虚拟销量 | 支持按商品规格设置虚拟销量,提供单个及批量设置入口。 |
|
||||||
|
| 2026-03-30 | ✅ 商品分组 | 新增商品分组管理及商品关联功能。 |
|
||||||
|
| 2026-03-24 | ✅ 会员分组 | 新增会员分组管理。 |
|
||||||
|
|
||||||
### 🔜 近期规划中
|
### 🔜 近期规划中
|
||||||
- 🔜 微信小店对接
|
- 🔜 微信小店对接
|
||||||
|
|||||||
7
api/paid-member.js
Normal file
7
api/paid-member.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { http, Method } from '@/utils/request.js'
|
||||||
|
export const getPaidMember = () => http.request({ url: '/member/paid/current', method: Method.GET, needToken: true })
|
||||||
|
export const createPaidMemberOrder = data => http.request({ url: '/member/paid/orders', method: Method.POST, data, needToken: true, header: { 'Content-Type': 'application/json' } })
|
||||||
|
export const getPaidMemberOrders = params => http.request({ url: '/member/paid/orders', method: Method.GET, params, needToken: true })
|
||||||
|
export const getPaidMemberOrder = sn => http.request({ url: `/member/paid/orders/${encodeURIComponent(sn)}`, method: Method.GET, needToken: true })
|
||||||
|
export const cancelPaidMemberOrder = sn => http.request({ url: `/member/paid/orders/${encodeURIComponent(sn)}/cancel`, method: Method.POST, needToken: true })
|
||||||
|
export const getPaidMemberQuotes = skuIds => http.request({ url: '/member/paid/quotes', method: Method.GET, params: { skuIds: skuIds.join(',') }, needToken: true })
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</view>
|
</view>
|
||||||
|
<PaidMemberPrice :quote="paidQuotes[item.id]" />
|
||||||
<div class="promotion" @click="navigateToDetailPage(item)">
|
<div class="promotion" @click="navigateToDetailPage(item)">
|
||||||
<div v-for="(promotionItem,promotionIndex) in getPromotion(item)" :key="promotionIndex">
|
<div v-for="(promotionItem,promotionIndex) in getPromotion(item)" :key="promotionIndex">
|
||||||
<span class="promotion-tag" v-if="promotionItem.indexOf('COUPON') != -1">劵</span>
|
<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>
|
<u-icon name="shopping-cart" size="16" :color="mainColor"></u-icon>
|
||||||
</div>
|
</div>
|
||||||
</view>
|
</view>
|
||||||
|
<PaidMemberPrice v-if="flag('price')" :quote="paidQuotes[item.id]" />
|
||||||
<div class="store-seller-name" v-if="storeName" @click="navigateToStoreDetailPage(item)">
|
<div class="store-seller-name" v-if="storeName" @click="navigateToStoreDetailPage(item)">
|
||||||
<view class="store-name-row">
|
<view class="store-name-row">
|
||||||
<text class="self-tag" v-if="item.selfOperated && flag('selfTag')">自营</text>
|
<text class="self-tag" v-if="item.selfOperated && flag('selfTag')">自营</text>
|
||||||
@@ -96,7 +98,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useStore } from '@/store'
|
||||||
import { useGoodsListCommon } from './common'
|
import { useGoodsListCommon } from './common'
|
||||||
import { goodsFormatPrice } from '@/utils/filters.js'
|
import { goodsFormatPrice } from '@/utils/filters.js'
|
||||||
@@ -123,6 +129,21 @@ const props = withDefaults(defineProps<{
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['addCart'])
|
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 时维持现状渲染
|
// 存量字段(促销/销量/自营/价格):showFields 为 null 时维持现状渲染
|
||||||
const flag = (key: string) => props.showFields == null || props.showFields[key] !== false
|
const flag = (key: string) => props.showFields == null || props.showFields[key] !== false
|
||||||
|
|||||||
6
components/paid-member-price/index.vue
Normal file
6
components/paid-member-price/index.vue
Normal 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>
|
||||||
70
pages.json
70
pages.json
@@ -13,16 +13,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "pages/tabbar/home/index",
|
"path": "pages/tabbar/home/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "首页",
|
"navigationBarTitleText": "首页",
|
||||||
"navigationStyle": "custom", // 隐藏系统导航栏
|
"navigationStyle": "custom", // 隐藏系统导航栏
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"enablePullDownRefresh":true
|
"enablePullDownRefresh":true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path":"pages/tabbar/screen/fullScreen",
|
"path":"pages/tabbar/screen/fullScreen",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -63,13 +63,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "pages/navigation/search/searchPage",
|
"path": "pages/navigation/search/searchPage",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "搜索",
|
"navigationBarTitleText": "搜索",
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
|
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
//app页面不显示滚动条
|
//app页面不显示滚动条
|
||||||
"scrollIndicator": "none",
|
"scrollIndicator": "none",
|
||||||
@@ -89,11 +89,11 @@
|
|||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "pages/tabbar/home/web-view",
|
"path": "pages/tabbar/home/web-view",
|
||||||
"style": {
|
"style": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -174,8 +174,10 @@
|
|||||||
"root": "pages/mine",
|
"root": "pages/mine",
|
||||||
"pages": [
|
"pages": [
|
||||||
{"path":"memberBenefits","style":{"navigationBarTitleText":"会员权益","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
{"path":"memberBenefits","style":{"navigationBarTitleText":"会员权益","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
||||||
|
{"path":"paidMember","style":{"navigationBarTitleText":"付费会员","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
||||||
|
{"path":"paidMemberRecords","style":{"navigationBarTitleText":"会员购买记录","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
||||||
{"path":"experience","style":{"navigationBarTitleText":"我的经验","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
{"path":"experience","style":{"navigationBarTitleText":"我的经验","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "signIn",
|
"path": "signIn",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -381,7 +383,7 @@
|
|||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "deposit/info",
|
"path": "deposit/info",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -434,8 +436,8 @@
|
|||||||
"navigationBarTitleText": "面容登录"
|
"navigationBarTitleText": "面容登录"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "set/securityCenter/editPassword",
|
"path": "set/securityCenter/editPassword",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -445,13 +447,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "set/securityCenter/bindMobile",
|
"path": "set/securityCenter/bindMobile",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "绑定手机号",
|
"navigationBarTitleText": "绑定手机号",
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -463,7 +465,7 @@
|
|||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -473,7 +475,7 @@
|
|||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -621,9 +623,9 @@
|
|||||||
// 禁止页面滚动
|
// 禁止页面滚动
|
||||||
"scrollIndicator": "none",
|
"scrollIndicator": "none",
|
||||||
"safearea": {
|
"safearea": {
|
||||||
"bottom": {
|
"bottom": {
|
||||||
"offset": "none"
|
"offset": "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,14 +681,14 @@
|
|||||||
"animationType": "slide-in-bottom",
|
"animationType": "slide-in-bottom",
|
||||||
"scrollIndicator": "none",
|
"scrollIndicator": "none",
|
||||||
"safearea": {
|
"safearea": {
|
||||||
"bottom": {
|
"bottom": {
|
||||||
"offset": "none"
|
"offset": "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "entry/seller/index",
|
"path": "entry/seller/index",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -742,7 +744,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// #ifndef MP-WEIXIN
|
// #ifndef MP-WEIXIN
|
||||||
{
|
{
|
||||||
"path": "live/room",
|
"path": "live/room",
|
||||||
@@ -804,7 +806,7 @@
|
|||||||
"path": "point/pointList",
|
"path": "point/pointList",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "积分商城"
|
"navigationBarTitleText": "积分商城"
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -820,7 +822,7 @@
|
|||||||
"navigationBarTitleText": "我的优惠券",
|
"navigationBarTitleText": "我的优惠券",
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
"bounce": "coupon/none"
|
"bounce": "coupon/none"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -836,13 +838,13 @@
|
|||||||
"navigationBarTitleText": "优惠券"
|
"navigationBarTitleText": "优惠券"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "coupon/couponCenter",
|
"path": "coupon/couponCenter",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "领券中心",
|
"navigationBarTitleText": "领券中心",
|
||||||
"enablePullDownRefresh": true
|
"enablePullDownRefresh": true
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -899,7 +901,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -930,7 +932,7 @@
|
|||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
"bounce": "none"
|
"bounce": "none"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -971,7 +973,7 @@
|
|||||||
"navigationBarTitleText": "评价详情"
|
"navigationBarTitleText": "评价详情"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "evaluate/releaseEvaluate",
|
"path": "evaluate/releaseEvaluate",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -984,7 +986,7 @@
|
|||||||
"navigationBarTitleText": "我的评价"
|
"navigationBarTitleText": "我的评价"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "afterSales/applyProgress",
|
"path": "afterSales/applyProgress",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -1069,7 +1071,7 @@
|
|||||||
"selectedIconPath": "static/tabbar/category-s.png",
|
"selectedIconPath": "static/tabbar/category-s.png",
|
||||||
"text": "分类"
|
"text": "分类"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"pagePath": "pages/tabbar/cart/cartList",
|
"pagePath": "pages/tabbar/cart/cartList",
|
||||||
"iconPath": "static/tabbar/cart.png",
|
"iconPath": "static/tabbar/cart.png",
|
||||||
@@ -1084,11 +1086,11 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// #todo 为什么要注释condition下代码?
|
// #todo 为什么要注释condition下代码?
|
||||||
// IOS plus.runtime.arguments 添加 condition节点后, 框架会修改 runtime.arguments
|
// IOS plus.runtime.arguments 添加 condition节点后, 框架会修改 runtime.arguments
|
||||||
// 会影响什么功能?
|
// 会影响什么功能?
|
||||||
// -在h5中唤醒app会一直返回默认值 {"name":"","path":"","query":"","id":0}
|
// -在h5中唤醒app会一直返回默认值 {"name":"","path":"","query":"","id":0}
|
||||||
"condition": { //模式配置,仅开发期间生效
|
"condition": { //模式配置,仅开发期间生效
|
||||||
// "current": 0, //当前激活的模式(list 的索引项)
|
// "current": 0, //当前激活的模式(list 的索引项)
|
||||||
// "list": [{
|
// "list": [{
|
||||||
// "name": "", //模式名称
|
// "name": "", //模式名称
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, getCurrentInstance, onMounted, computed } from 'vue'
|
import { ref, getCurrentInstance, onMounted, computed } from 'vue'
|
||||||
import { onLoad, onBackPress } from '@dcloudio/uni-app'
|
import { onLoad, onBackPress, onUnload } from '@dcloudio/uni-app'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import * as API_Trade from '@/api/trade'
|
import * as API_Trade from '@/api/trade'
|
||||||
import { payCallback } from '@/api/members'
|
import { payCallback } from '@/api/members'
|
||||||
@@ -88,6 +88,8 @@ const paymentType = ref('')
|
|||||||
const paymentClient = ref('')
|
const paymentClient = ref('')
|
||||||
const walletValue = ref(0)
|
const walletValue = ref(0)
|
||||||
const autoCancelTime = ref(0)
|
const autoCancelTime = ref(0)
|
||||||
|
let h5PollTimer: ReturnType<typeof setInterval> | undefined
|
||||||
|
onUnload(() => { if (h5PollTimer) clearInterval(h5PollTimer) })
|
||||||
|
|
||||||
onLoad((val) => {
|
onLoad((val) => {
|
||||||
routerVal.value = val || {}
|
routerVal.value = val || {}
|
||||||
@@ -107,7 +109,9 @@ onLoad((val) => {
|
|||||||
|
|
||||||
onBackPress((e) => {
|
onBackPress((e) => {
|
||||||
if (e.from == 'backbutton') {
|
if (e.from == 'backbutton') {
|
||||||
if (routerVal.value.recharge_sn) {
|
if (routerVal.value.paid_member_sn) {
|
||||||
|
uni.redirectTo({ url: '/pages/mine/paidMemberRecords' })
|
||||||
|
} else if (routerVal.value.recharge_sn) {
|
||||||
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||||
} else {
|
} else {
|
||||||
uni.navigateTo({ url: '/pages/order/myOrder?status=0' })
|
uni.navigateTo({ url: '/pages/order/myOrder?status=0' })
|
||||||
@@ -126,6 +130,10 @@ function hideLoadingIfNeeded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function callback(paymentMethod: string) {
|
function callback(paymentMethod: string) {
|
||||||
|
if (orderType.value === 'PAID_MEMBER') {
|
||||||
|
uni.redirectTo({ url: '/pages/mine/paidMember?payment_sn=' + encodeURIComponent(sn.value) })
|
||||||
|
return
|
||||||
|
}
|
||||||
const goSuccess = () => {
|
const goSuccess = () => {
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url:
|
url:
|
||||||
@@ -145,7 +153,10 @@ function callback(paymentMethod: string) {
|
|||||||
function cashierData() {
|
function cashierData() {
|
||||||
const parms: Record<string, string> = {}
|
const parms: Record<string, string> = {}
|
||||||
|
|
||||||
if (routerVal.value.recharge_sn) {
|
if (routerVal.value.paid_member_sn) {
|
||||||
|
sn.value = routerVal.value.paid_member_sn
|
||||||
|
orderType.value = 'PAID_MEMBER'
|
||||||
|
} else if (routerVal.value.recharge_sn) {
|
||||||
sn.value = routerVal.value.recharge_sn
|
sn.value = routerVal.value.recharge_sn
|
||||||
orderType.value = 'RECHARGE'
|
orderType.value = 'RECHARGE'
|
||||||
} else if (routerVal.value.trade_sn) {
|
} else if (routerVal.value.trade_sn) {
|
||||||
@@ -212,6 +223,11 @@ function onPayTimeout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goPayError(message = '支付失败,如果您已支付,请勿反复支付') {
|
function goPayError(message = '支付失败,如果您已支付,请勿反复支付') {
|
||||||
|
if (orderType.value === 'PAID_MEMBER') {
|
||||||
|
uni.showToast({ title: '支付未完成,请在购买记录中查看结果', icon: 'none' })
|
||||||
|
uni.redirectTo({ url: '/pages/mine/paidMemberRecords' })
|
||||||
|
return
|
||||||
|
}
|
||||||
const query = [`orderSn=${encodeURIComponent(sn.value)}`, `message=${encodeURIComponent(message)}`]
|
const query = [`orderSn=${encodeURIComponent(sn.value)}`, `message=${encodeURIComponent(message)}`]
|
||||||
uni.navigateTo({ url: `/pages/cart/payment/error?${query.join('&')}` })
|
uni.navigateTo({ url: `/pages/cart/payment/error?${query.join('&')}` })
|
||||||
}
|
}
|
||||||
@@ -276,15 +292,24 @@ async function pay(payment: string) {
|
|||||||
})
|
})
|
||||||
hideLoadingIfNeeded()
|
hideLoadingIfNeeded()
|
||||||
} else {
|
} else {
|
||||||
window.location.href = JSON.parse(response.result).h5_url
|
const paymentUrl = typeof response.result === 'string' && response.result.startsWith('http')
|
||||||
|
? response.result : JSON.parse(response.result).h5_url
|
||||||
|
const returnUrl = orderType.value === 'PAID_MEMBER'
|
||||||
|
? window.location.href.split('#')[0] + '#/pages/mine/paidMember?payment_sn=' + encodeURIComponent(sn.value)
|
||||||
|
: window.location.href
|
||||||
|
window.location.href = paymentUrl + (paymentUrl.includes('?') ? '&' : '?') + 'redirect_url=' + encodeURIComponent(returnUrl)
|
||||||
const searchParams = { ...params, price: cashierParams.value }
|
const searchParams = { ...params, price: cashierParams.value }
|
||||||
const timer = setInterval(() => {
|
let attempts = 0
|
||||||
|
if (h5PollTimer) clearInterval(h5PollTimer)
|
||||||
|
h5PollTimer = setInterval(() => {
|
||||||
|
if (++attempts > 100) { clearInterval(h5PollTimer); return }
|
||||||
payCallback(searchParams).then((cbRes) => {
|
payCallback(searchParams).then((cbRes) => {
|
||||||
if (cbRes.data.result) {
|
if (cbRes.data.result) {
|
||||||
clearInterval(timer)
|
clearInterval(h5PollTimer)
|
||||||
uni.navigateTo({ url: '/pages/order/myOrder' })
|
if (orderType.value === 'PAID_MEMBER') callback(paymentMethod)
|
||||||
|
else uni.navigateTo({ url: '/pages/order/myOrder' })
|
||||||
}
|
}
|
||||||
})
|
}).catch(() => {})
|
||||||
}, 3000)
|
}, 3000)
|
||||||
hideLoadingIfNeeded()
|
hideLoadingIfNeeded()
|
||||||
}
|
}
|
||||||
|
|||||||
79
pages/mine/paidMember.vue
Normal file
79
pages/mine/paidMember.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<view class="paid-page">
|
||||||
|
<view v-if="loading && !data.config" class="empty">加载中…</view>
|
||||||
|
<view v-else-if="error && !data.config" class="empty" @click="load">加载失败,点击重试</view>
|
||||||
|
<template v-else-if="data.config">
|
||||||
|
<view v-if="error" class="poll-message" @click="load">刷新失败,点击重试</view>
|
||||||
|
<view class="member-card" :style="cardStyle">
|
||||||
|
<image v-if="data.config.backgroundType === 'IMAGE' && data.config.backgroundImage" class="card-bg" :src="data.config.backgroundImage" mode="aspectFill" />
|
||||||
|
<view class="card-content"><view class="card-title"><image v-if="data.config.icon" class="member-icon" :src="data.config.icon" mode="aspectFit" /><text>{{ data.config.name }}</text></view><text class="member-status">{{ data.active ? '会员有效期至 ' + data.endTime : data.status === 'EXPIRED' ? '会员已到期' : '开通会员,享专属权益' }}</text><text v-if="data.status !== 'UNOPENED'" class="saved">累计已优惠 ¥{{ money(data.savedAmount) }}</text></view>
|
||||||
|
</view>
|
||||||
|
<view v-if="pollMessage" class="poll-message" @click="checkPayment">{{ pollMessage }}</view>
|
||||||
|
<view class="purchase-card"><view><text class="price">¥{{ money(data.config.price) }}</text><text class="period"> / {{ data.config.months }}个月</text></view><text class="hint">{{ data.active ? '续费将从原到期时间顺延,续费权益在新周期开始后生效' : '支付成功后开通,按自然月计算有效期' }}</text></view>
|
||||||
|
<view class="section"><view class="section-heading"><text>专属权益</text><text class="record-link" @click="goRecords">购买记录 ›</text></view>
|
||||||
|
<view class="rights-grid"><view v-for="b in displayedBenefits" :key="b.id" class="right-item" @click="detail = b"><image v-if="b.benefitLogo" :src="b.benefitLogo" mode="aspectFit" /><u-icon v-else name="gift" size="28" color="#ae8245" /><text>{{ b.benefitName }}</text></view></view><text v-if="!displayedBenefits.length" class="hint">{{ data.active ? '当前暂无关联权益' : '开通后按会员配置享有权益' }}</text>
|
||||||
|
<view class="discount-note"><text>会员专享商品折扣</text><text>优惠由平台提供,限适用商家及商品。实际优惠以结算页为准。</text></view>
|
||||||
|
</view>
|
||||||
|
<view v-if="data.recommendedGoods?.length" class="section"><view class="section-heading">为你推荐</view><view class="goods-grid"><view v-for="g in data.recommendedGoods" :key="g.id" class="goods-card" @click="goGoods(g)"><image class="goods-cover" :src="g.thumbnail" mode="aspectFit" /><text class="goods-name">{{ g.goodsName }}</text><text class="goods-price">¥{{ money(g.price) }}</text><view v-if="quotes[g.id]" class="member-price"><image v-if="quotes[g.id].label" class="member-price-label" :src="quotes[g.id].label" mode="aspectFit" /><text>会员价 ¥{{ money(quotes[g.id].price) }}</text></view></view></view></view>
|
||||||
|
<view v-if="data.config.description" class="section"><view class="section-heading">购买说明</view><rich-text :nodes="data.config.description" /></view>
|
||||||
|
<view class="purchase-footer"><view class="agreement-row" @click="agreed = !agreed"><u-icon :name="agreed ? 'checkmark-circle-fill' : 'checkmark-circle'" size="17" color="#a78149" /><text>我已阅读并同意</text><text class="agreement-link" @click.stop="showAgreement = true">《付费会员协议》</text></view><button :disabled="!data.config.enabled || buying || !agreed" :loading="buying" class="buy-button" @click="buy">{{ !data.config.enabled ? '暂未开放购买和续费' : data.active ? '立即续费' : '立即开通' }}</button></view>
|
||||||
|
</template>
|
||||||
|
<view v-if="detail || showAgreement" class="overlay"><view class="mask" @click="closeDetail" @touchmove.stop.prevent /><view class="sheet"><text class="sheet-title">{{ detail?.benefitName || '付费会员协议' }}</text><scroll-view scroll-y class="sheet-scroll"><rich-text :nodes="detail ? detail.benefitDesc || '暂无说明' : data.config?.agreement || '请联系平台查看会员协议'" /></scroll-view><button class="close-button" @click="closeDetail">关闭</button></view></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad, onShow, onHide, onUnload, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||||
|
import { getPaidMember, createPaidMemberOrder, getPaidMemberOrder, getPaidMemberQuotes } from '@/api/paid-member'
|
||||||
|
import { isLogin, navigateToLogin } from '@/utils/filters'
|
||||||
|
const data=ref({}), quotes=ref({}), loading=ref(false), error=ref(false), agreed=ref(false), buying=ref(false), detail=ref(null), showAgreement=ref(false), pollMessage=ref('')
|
||||||
|
let request=0, owner='', purchaseRequest='', paymentSn='', timer=null, pollId=0, pollCount=0
|
||||||
|
const money=value=>(Number(value)||0).toFixed(2)
|
||||||
|
const cardStyle=computed(()=>({backgroundColor:data.value.config?.backgroundColor || '#51443b',color:data.value.config?.fontColor || '#fff1d8'}))
|
||||||
|
const displayedBenefits=computed(()=>data.value.active ? data.value.benefits || [] : data.value.offeredBenefits || [])
|
||||||
|
function closeDetail(){detail.value=null;showAgreement.value=false}
|
||||||
|
function goRecords(){uni.navigateTo({url:'/pages/mine/paidMemberRecords'})}
|
||||||
|
function goGoods(g){uni.navigateTo({url:`/pages/product/goods?id=${g.id}&goodsId=${g.goodsId}`})}
|
||||||
|
async function load(){
|
||||||
|
const memberId=String(isLogin()?.id || '')
|
||||||
|
if(!memberId){request++;stopPolling();owner='';data.value={};quotes.value={};loading.value=false;agreed.value=false;purchaseRequest='';paymentSn='';pollMessage.value='';navigateToLogin();uni.stopPullDownRefresh();return}
|
||||||
|
if(owner!==memberId){if(owner){stopPolling();paymentSn='';pollMessage.value=''}owner=memberId;data.value={};quotes.value={};agreed.value=false;purchaseRequest='';detail.value=null}
|
||||||
|
const id=++request;loading.value=true;error.value=false
|
||||||
|
try{
|
||||||
|
const res=await getPaidMember()
|
||||||
|
if(id!==request || String(isLogin()?.id||'')!==memberId)return
|
||||||
|
if(!res.data?.success)throw new Error('load')
|
||||||
|
if(data.value.configVersion !== res.data.result?.configVersion) agreed.value=false
|
||||||
|
data.value=res.data.result||{};quotes.value={}
|
||||||
|
const ids=(data.value.recommendedGoods||[]).map(g=>g.id)
|
||||||
|
const results=await Promise.all(Array.from({length:Math.ceil(ids.length/100)},(_,i)=>getPaidMemberQuotes(ids.slice(i*100,i*100+100))))
|
||||||
|
if(id===request && String(isLogin()?.id||'')===memberId) quotes.value=Object.assign({},...results.filter(q=>q.data?.success).map(q=>q.data.result||{}))
|
||||||
|
}catch{if(id===request)error.value=true}finally{if(id===request)loading.value=false;uni.stopPullDownRefresh()}
|
||||||
|
}
|
||||||
|
async function buy(){
|
||||||
|
if(buying.value || !agreed.value || !data.value.config?.enabled)return
|
||||||
|
buying.value=true
|
||||||
|
const memberId=owner
|
||||||
|
purchaseRequest=purchaseRequest || 'pm_'+Date.now().toString(36)+'_'+Math.random().toString(36).slice(2)
|
||||||
|
try{
|
||||||
|
const res=await createPaidMemberOrder({requestId:purchaseRequest,configVersion:data.value.configVersion,agreed:true})
|
||||||
|
if(String(isLogin()?.id||'')!==memberId)return
|
||||||
|
if(res.data?.success){const o=res.data.result;purchaseRequest='';if(o.status==='PAID')uni.redirectTo({url:'/pages/mine/paidMember?payment_sn='+encodeURIComponent(o.sn)});else uni.navigateTo({url:`/pages/cart/payment/payOrder?paid_member_sn=${o.sn}`})}
|
||||||
|
else{purchaseRequest='';agreed.value=false;await load()}
|
||||||
|
}catch{uni.showToast({title:'购买结果暂未确认,请重试或查看购买记录',icon:'none'})}finally{buying.value=false}
|
||||||
|
}
|
||||||
|
function stopPolling(){pollId++;if(timer)clearTimeout(timer);timer=null}
|
||||||
|
async function checkPayment(){
|
||||||
|
if(!paymentSn)return
|
||||||
|
const id=++pollId;pollMessage.value='正在确认支付结果…'
|
||||||
|
try{const res=await getPaidMemberOrder(paymentSn);if(id!==pollId)return;if(res.data?.success && res.data.result.status==='PAID'){paymentSn='';pollMessage.value='支付已确认,会员资格已更新';await load();return}pollCount++;if(pollCount<10){timer=setTimeout(checkPayment,1500)}else pollMessage.value='支付结果待确认,点击刷新或查看购买记录'}catch{if(id===pollId)pollMessage.value='支付结果待确认,点击重试'}
|
||||||
|
}
|
||||||
|
onLoad(options=>{paymentSn=options?.payment_sn||''})
|
||||||
|
onShow(async()=>{await load();if(paymentSn){pollCount=0;checkPayment()}})
|
||||||
|
onPullDownRefresh(load)
|
||||||
|
onHide(stopPolling)
|
||||||
|
onUnload(()=>{request++;stopPolling()})
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.paid-page{min-height:100vh;box-sizing:border-box;background:#f5f6f9;padding:28rpx 28rpx 240rpx;color:#292f39}.member-card{position:relative;overflow:hidden;border-radius:24rpx;min-height:240rpx}.card-bg{position:absolute;inset:0;width:100%;height:100%}.card-content{position:relative;padding:36rpx}.card-title{display:flex;align-items:center;gap:16rpx;font-size:38rpx;font-weight:600}.member-icon{width:70rpx;height:70rpx}.member-status,.saved{display:block;margin-top:18rpx;font-size:24rpx}.saved{opacity:.8}.purchase-card,.section{margin-top:24rpx;background:#fff;padding:28rpx;border-radius:20rpx}.price{font-size:48rpx;font-weight:600;color:#a67b3d}.period{font-size:25rpx;color:#929aa6}.hint{display:block;margin-top:14rpx;font-size:23rpx;line-height:1.7;color:#929aa6}.section-heading{display:flex;align-items:center;justify-content:space-between;font-size:30rpx;font-weight:600;margin-bottom:24rpx}.record-link{font-size:23rpx;font-weight:400;color:#a78149}.rights-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:28rpx 16rpx}.right-item{display:flex;flex-direction:column;align-items:center;gap:12rpx;text-align:center;font-size:24rpx}.right-item image{width:72rpx;height:72rpx}.discount-note{margin-top:24rpx;padding:20rpx;background:#fbf6ed;border-radius:14rpx;font-size:23rpx;color:#a78149;line-height:1.8}.discount-note text{display:block}.goods-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24rpx}.goods-cover{width:100%;height:230rpx}.goods-name{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:24rpx;line-height:1.6}.goods-price,.member-price{display:block;margin-top:8rpx;font-size:25rpx}.member-price{display:flex;align-items:center;gap:6rpx;color:#9c7139;font-size:22rpx}.member-price-label{width:28rpx;height:28rpx;flex-shrink:0}.purchase-footer{position:fixed;left:0;right:0;bottom:0;background:#fff;padding:22rpx 28rpx calc(22rpx + env(safe-area-inset-bottom));border-top:1rpx solid #eee;z-index:8}.agreement-row{display:flex;justify-content:center;align-items:center;gap:8rpx;font-size:22rpx;color:#929aa6;margin-bottom:18rpx}.agreement-link{color:#a78149}.buy-button,.close-button{background:#ead5b1;color:#705125;border-radius:44rpx;font-size:28rpx;line-height:84rpx}button::after{border:none}.empty{padding:160rpx 20rpx;text-align:center;color:#969da8;font-size:27rpx}.poll-message{padding:20rpx;background:#fff2d9;border-radius:12rpx;margin-top:20rpx;color:#9d753e;font-size:24rpx}.overlay{position:fixed;inset:0;z-index:20;display:flex;align-items:flex-end}.mask{position:absolute;inset:0;background:rgba(0,0,0,.4)}.sheet{position:relative;width:100%;background:#fff;border-radius:28rpx 28rpx 0 0;padding:30rpx;box-sizing:border-box;padding-bottom:calc(30rpx + env(safe-area-inset-bottom))}.sheet-title{display:block;text-align:center;font-size:32rpx;font-weight:600;margin-bottom:24rpx}.sheet-scroll{height:50vh;margin-bottom:24rpx;font-size:26rpx;line-height:1.8}
|
||||||
|
</style>
|
||||||
36
pages/mine/paidMemberRecords.vue
Normal file
36
pages/mine/paidMemberRecords.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<view class="records-page">
|
||||||
|
<view v-for="order in rows" :key="order.id" class="record-card">
|
||||||
|
<view class="heading"><text>{{ memberName(order) }}</text><text class="status">{{ status(order) }}</text></view>
|
||||||
|
<text class="amount">¥{{ Number(order.amount).toFixed(2) }} <text class="months">/ {{ order.months }}个月</text></text>
|
||||||
|
<text class="line">订单号:{{ order.sn }}</text><text class="line">创建时间:{{ order.createTime }}</text>
|
||||||
|
<template v-if="order.status === 'PAID'"><text class="line">支付时间:{{ order.payTime }}</text><text class="line">有效期:{{ order.startTime }} 至 {{ order.endTime }}</text></template>
|
||||||
|
<view v-if="canPay(order)" class="actions"><button class="cancel" @click="cancel(order)">取消订单</button><button class="pay" @click="pay(order)">继续支付</button></view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!rows.length" class="empty">{{ loading ? '加载中…' : error ? '加载失败,点击重试' : '暂无购买记录' }}</view>
|
||||||
|
<view v-if="error" class="load-more" @click="load(retryReset)">点击重新加载</view><view v-else-if="rows.length" class="load-more" @click="load(false)">{{ loading ? '加载中…' : more ? '上拉或点击加载更多' : '已显示全部记录' }}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onShow, onReachBottom, onPullDownRefresh, onUnload } from '@dcloudio/uni-app'
|
||||||
|
import { getPaidMemberOrders, cancelPaidMemberOrder } from '@/api/paid-member'
|
||||||
|
import { isLogin, navigateToLogin } from '@/utils/filters'
|
||||||
|
const rows=ref([]),loading=ref(false),error=ref(false),more=ref(true)
|
||||||
|
let page=1,owner='',request=0,retryReset=true
|
||||||
|
const time=value=>new Date(String(value||'').replace(/-/g,'/')).getTime()
|
||||||
|
function memberName(order){try{return JSON.parse(order.configSnapshot).name}catch{return '付费会员'}}
|
||||||
|
function canPay(order){return order.status==='UNPAID' && time(order.payDeadline)>Date.now()}
|
||||||
|
function status(order){return order.status==='PAID' ? time(order.startTime)>Date.now() ? '待生效' : time(order.endTime)<=Date.now() ? '已到期' : '会员有效' : order.status==='CANCELLED' ? '已取消' : canPay(order) ? '待支付' : '已超时'}
|
||||||
|
function pay(order){uni.navigateTo({url:`/pages/cart/payment/payOrder?paid_member_sn=${order.sn}`})}
|
||||||
|
function cancel(order){uni.showModal({title:'取消会员订单',content:'确定取消这笔未支付订单?',success:async result=>{if(result.confirm){const res=await cancelPaidMemberOrder(order.sn);if(res.data?.success)load(true)}}})}
|
||||||
|
async function load(reset=false){
|
||||||
|
const memberId=String(isLogin()?.id||'');if(!memberId){request++;rows.value=[];owner='';navigateToLogin();uni.stopPullDownRefresh();return}
|
||||||
|
if(!reset && (loading.value || !more.value))return
|
||||||
|
if(owner!==memberId){owner=memberId;rows.value=[];reset=true}
|
||||||
|
const id=++request,next=reset?1:page;loading.value=true;error.value=false;retryReset=reset
|
||||||
|
try{const res=await getPaidMemberOrders({pageNumber:next,pageSize:20});if(id!==request||String(isLogin()?.id||'')!==memberId)return;if(!res.data?.success)throw new Error('load');const data=res.data.result||{};rows.value=reset ? data.records||[] : [...rows.value,...(data.records||[]).filter(row=>!rows.value.some(old=>old.id===row.id))];page=next+1;more.value=next*20<Number(data.total||0);retryReset=false}catch{if(id===request)error.value=true}finally{if(id===request)loading.value=false;uni.stopPullDownRefresh()}
|
||||||
|
}
|
||||||
|
onShow(()=>load(true));onPullDownRefresh(()=>load(true));onReachBottom(()=>{if(!error.value)load(false)});onUnload(()=>request++)
|
||||||
|
</script>
|
||||||
|
<style scoped>.records-page{min-height:100vh;box-sizing:border-box;padding:24rpx 28rpx calc(30rpx + env(safe-area-inset-bottom));background:#f5f6f9}.record-card{padding:28rpx;background:#fff;border-radius:20rpx;margin-bottom:20rpx}.heading{display:flex;align-items:center;justify-content:space-between;font-size:29rpx;font-weight:600}.status{font-size:23rpx;color:#a78149;font-weight:400}.amount{display:block;font-size:40rpx;margin:20rpx 0;color:#9d753e}.months{font-size:23rpx;color:#989faa}.line{display:block;font-size:22rpx;line-height:1.9;color:#929aa6;overflow-wrap:anywhere}.actions{display:flex;justify-content:flex-end;gap:16rpx;margin-top:22rpx}.actions button{margin:0;padding:0 26rpx;font-size:23rpx;line-height:58rpx;border-radius:30rpx}.actions button::after{border:none}.pay{background:#f0dfc2;color:#8d6836}.cancel{background:#f2f3f5;color:#929aa6}.empty,.load-more{padding:40rpx 0;text-align:center;font-size:25rpx;color:#a0a7b1}</style>
|
||||||
@@ -346,7 +346,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<u-row>
|
<u-row>
|
||||||
<u-col :span="6">活动优惠</u-col>
|
<u-col :span="6">活动优惠(含会员优惠)</u-col>
|
||||||
<u-col :span="6" class="tr tipsColor" textAlign="right">
|
<u-col :span="6" class="tr tipsColor" textAlign="right">
|
||||||
<span v-if="orderMessage.priceDetailDTO.discountPrice"
|
<span v-if="orderMessage.priceDetailDTO.discountPrice"
|
||||||
>-¥{{unitPrice(orderMessage.priceDetailDTO.discountPrice)
|
>-¥{{unitPrice(orderMessage.priceDetailDTO.discountPrice)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,136 +1,136 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="wrapper" v-if="res">
|
<view class="wrapper" v-if="res">
|
||||||
<view v-for="prom in promotionKeys" :key="prom">
|
<view v-for="prom in promotionKeys" :key="prom">
|
||||||
<view v-if="getPromType(prom) == 'FULL_DISCOUNT'">
|
<view v-if="getPromType(prom) == 'FULL_DISCOUNT'">
|
||||||
<view class="res_prom_item" v-if="res[prom].fullMinus">
|
<view class="res_prom_item" v-if="res[prom].fullMinus">
|
||||||
<view class="deg_tag">满减</view>
|
<view class="deg_tag">满减</view>
|
||||||
<text class="pro-text">
|
<text class="pro-text">
|
||||||
满{{ res[prom].fullMoney }}元 立减现金
|
满{{ res[prom].fullMoney }}元 立减现金
|
||||||
<text class="price">{{ res[prom].fullMinus }}元</text>
|
<text class="price">{{ res[prom].fullMinus }}元</text>
|
||||||
<text v-if="res[prom].couponFlag"> 赠送优惠券</text>
|
<text v-if="res[prom].couponFlag"> 赠送优惠券</text>
|
||||||
<text v-if="res[prom].pointFlag"> 赠送{{ res[prom].point }}积分</text>
|
<text v-if="res[prom].pointFlag"> 赠送{{ res[prom].point }}积分</text>
|
||||||
<text v-if="res[prom].giftFlag"> 赠送商品</text>
|
<text v-if="res[prom].giftFlag"> 赠送商品</text>
|
||||||
<text v-if="res[prom].freeFreightFlag"> 赠送包邮服务</text>
|
<text v-if="res[prom].freeFreightFlag"> 赠送包邮服务</text>
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="res_prom_item" v-if="res[prom].fullRate && res[prom].fullRateFlag">
|
<view class="res_prom_item" v-if="res[prom].fullRate && res[prom].fullRateFlag">
|
||||||
<view class="deg_tag">打折</view>
|
<view class="deg_tag">打折</view>
|
||||||
<text class="pro-text">
|
<text class="pro-text">
|
||||||
满{{ res[prom].fullMoney }}元,立享
|
满{{ res[prom].fullMoney }}元,立享
|
||||||
<text class="price">{{ res[prom].fullRate }}折</text>
|
<text class="price">{{ res[prom].fullRate }}折</text>
|
||||||
优惠
|
优惠
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="getPromType(prom) == 'PINTUAN'">
|
<view v-if="getPromType(prom) == 'PINTUAN'">
|
||||||
<view class="res_prom_item" v-if="res[prom].requiredNum">
|
<view class="res_prom_item" v-if="res[prom].requiredNum">
|
||||||
<view class="deg_tag">拼团</view>
|
<view class="deg_tag">拼团</view>
|
||||||
<text class="pro-text">
|
<text class="pro-text">
|
||||||
{{ res[prom].requiredNum }}人拼团 限购
|
{{ res[prom].requiredNum }}人拼团 限购
|
||||||
<text class="price">{{ res[prom].limitNum }}件</text>
|
<text class="price">{{ res[prom].limitNum }}件</text>
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="getPromType(prom) == 'SECKILL'">
|
<view v-if="getPromType(prom) == 'SECKILL'">
|
||||||
<view class="res_prom_item">
|
<view class="res_prom_item">
|
||||||
<view class="deg_tag">限时抢购</view>
|
<view class="deg_tag">限时抢购</view>
|
||||||
<text class="pro-text">限时抢购</text>
|
<text class="pro-text">限时抢购</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="getPromType(prom) == 'POINTS_GOODS'">
|
<view v-if="getPromType(prom) == 'POINTS_GOODS'">
|
||||||
<view class="res_prom_item">
|
<view class="res_prom_item">
|
||||||
<view class="deg_tag">积分活动</view>
|
<view class="deg_tag">积分活动</view>
|
||||||
<text class="pro-text">
|
<text class="pro-text">
|
||||||
当前商品参与积分活动。
|
当前商品参与积分活动。
|
||||||
<text @tap="handClickToJoinPromotion(prom)" class="href">点击此处参与活动</text>
|
<text @tap="handClickToJoinPromotion(prom)" class="href">点击此处参与活动</text>
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="getPromType(prom) == 'KANJIA'">
|
<view v-if="getPromType(prom) == 'KANJIA'">
|
||||||
<view class="res_prom_item">
|
<view class="res_prom_item">
|
||||||
<view class="deg_tag">砍价活动</view>
|
<view class="deg_tag">砍价活动</view>
|
||||||
<text class="pro-text">
|
<text class="pro-text">
|
||||||
当前商品参与砍价活动。
|
当前商品参与砍价活动。
|
||||||
<text @tap="handClickToJoinPromotion(prom)" class="href">点击此处参与活动</text>
|
<text @tap="handClickToJoinPromotion(prom)" class="href">点击此处参与活动</text>
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!promotionKeys.length">暂无促销活动</view>
|
<view v-if="!promotionKeys.length">暂无促销活动</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
res?: any
|
res?: any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const promotionKeys = computed(() => {
|
const promotionKeys = computed(() => {
|
||||||
if (!props.res || typeof props.res !== 'object') return []
|
if (!props.res || typeof props.res !== 'object') return []
|
||||||
return Object.keys(props.res).filter((key) =>
|
return Object.keys(props.res).filter((key) =>
|
||||||
['FULL_DISCOUNT', 'PINTUAN', 'SECKILL', 'POINTS_GOODS', 'KANJIA'].includes(getPromType(key))
|
['FULL_DISCOUNT', 'PINTUAN', 'SECKILL', 'POINTS_GOODS', 'KANJIA'].includes(getPromType(key))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function getPromType(key: string) {
|
function getPromType(key: string) {
|
||||||
return key.split('-')[0]
|
return key.split('-')[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
function handClickToJoinPromotion(val: string) {
|
function handClickToJoinPromotion(val: string) {
|
||||||
const type = getPromType(val)
|
const type = getPromType(val)
|
||||||
const promotionMap: Record<string, string> = {
|
const promotionMap: Record<string, string> = {
|
||||||
POINTS_GOODS: `/pages/promotion/point/detail?id=${props.res[val].id}`,
|
POINTS_GOODS: `/pages/promotion/point/detail?id=${props.res[val].id}`,
|
||||||
KANJIA: `/pages/promotion/bargain/detail?id=${props.res[val].id}`,
|
KANJIA: `/pages/promotion/bargain/detail?id=${props.res[val].id}`,
|
||||||
}
|
}
|
||||||
const url = promotionMap[type]
|
const url = promotionMap[type]
|
||||||
if (url) {
|
if (url) {
|
||||||
uni.navigateTo({ url })
|
uni.navigateTo({ url })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.deg_tag {
|
.deg_tag {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: $price-color;
|
color: $price-color;
|
||||||
padding: 4rpx 12rpx;
|
padding: 4rpx 12rpx;
|
||||||
border: 2rpx solid $price-color;
|
border: 2rpx solid $price-color;
|
||||||
border-radius: 6rpx;
|
border-radius: 6rpx;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pro-text {
|
.pro-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.res_prom_item {
|
.res_prom_item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
margin: 20rpx 0;
|
margin: 20rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
color: $price-color;
|
color: $price-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.href {
|
.href {
|
||||||
color: $main-color;
|
color: $main-color;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -69,6 +69,13 @@
|
|||||||
|
|
||||||
<!-- 下方主内容卡片区域 -->
|
<!-- 下方主内容卡片区域 -->
|
||||||
<view class="main-container">
|
<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-box order-box">
|
||||||
<view class="card-title-bar">
|
<view class="card-title-bar">
|
||||||
@@ -132,6 +139,7 @@ import {
|
|||||||
onNavigationBarButtonTap,
|
onNavigationBarButtonTap,
|
||||||
} from '@dcloudio/uni-app'
|
} from '@dcloudio/uni-app'
|
||||||
import tool from '@/pages/tabbar/user/utils/tool.vue'
|
import tool from '@/pages/tabbar/user/utils/tool.vue'
|
||||||
|
import { getPaidMember } from '@/api/paid-member'
|
||||||
import { getCouponsNum, getFootprintNum } from '@/api/members.js'
|
import { getCouponsNum, getFootprintNum } from '@/api/members.js'
|
||||||
import { getUserWallet } from '@/api/members'
|
import { getUserWallet } from '@/api/members'
|
||||||
import configs from '@/config/config'
|
import configs from '@/config/config'
|
||||||
@@ -154,6 +162,22 @@ const aiderLightColor = computed(() => store.getters.aiderLightColor)
|
|||||||
|
|
||||||
const userImage = configs.defaultUserPhoto
|
const userImage = configs.defaultUserPhoto
|
||||||
const userInfo = ref<any>({})
|
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 couponNum = ref<string | number>('')
|
||||||
const footNum = ref<string | number>('')
|
const footNum = ref<string | number>('')
|
||||||
const walletNum = ref<string | number>('')
|
const walletNum = ref<string | number>('')
|
||||||
@@ -189,6 +213,26 @@ function resetMemberGrade() {
|
|||||||
gradeIconFailed.value = false
|
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() {
|
function onGradeBackgroundError() {
|
||||||
gradeBackgroundFailed.value = true
|
gradeBackgroundFailed.value = true
|
||||||
gradeBackgroundLoaded.value = false
|
gradeBackgroundLoaded.value = false
|
||||||
@@ -218,6 +262,8 @@ watch(() => [store.state.userInfo?.id, store.state.hasLogin], () => {
|
|||||||
if (String(account.id || '') !== String(userInfo.value.id || '')) {
|
if (String(account.id || '') !== String(userInfo.value.id || '')) {
|
||||||
userInfo.value = account
|
userInfo.value = account
|
||||||
resetMemberGrade()
|
resetMemberGrade()
|
||||||
|
paidMemberRequestId++
|
||||||
|
paidMemberInfo.value = null
|
||||||
walletNum.value = 0
|
walletNum.value = 0
|
||||||
couponNum.value = 0
|
couponNum.value = 0
|
||||||
footNum.value = 0
|
footNum.value = 0
|
||||||
@@ -256,9 +302,11 @@ async function refreshUserPage() {
|
|||||||
userInfo.value = isLogin() || {}
|
userInfo.value = isLogin() || {}
|
||||||
try {
|
try {
|
||||||
if (isLogin('auth')) {
|
if (isLogin('auth')) {
|
||||||
await Promise.allSettled([loadMemberGrade(), getUserOrderNum()])
|
await Promise.allSettled([loadMemberGrade(), loadPaidMemberInfo(), getUserOrderNum()])
|
||||||
} else {
|
} else {
|
||||||
resetMemberGrade()
|
resetMemberGrade()
|
||||||
|
paidMemberRequestId++
|
||||||
|
paidMemberInfo.value = null
|
||||||
walletNum.value = 0
|
walletNum.value = 0
|
||||||
couponNum.value = 0
|
couponNum.value = 0
|
||||||
footNum.value = 0
|
footNum.value = 0
|
||||||
@@ -313,6 +361,11 @@ async function getUserOrderNum() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<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 {
|
.user-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f5f6f9;
|
background-color: #f5f6f9;
|
||||||
|
|||||||
Reference in New Issue
Block a user