diff --git a/src/pages/home/information.wpy b/src/pages/home/information.wpy
index 029e8c4..15f83cb 100644
--- a/src/pages/home/information.wpy
+++ b/src/pages/home/information.wpy
@@ -758,7 +758,7 @@ page {
打个招呼吧
- 完善资料并完成真人认证和学历认证后,每天可免费加好友3次
+ 真人认证、真人头像审核通过、完善资料、学历认证,每完成一项每天可免费加好友1次,最多4次
@@ -771,7 +771,7 @@ page {
取消
发送
- 今日还剩{{friendEligibility.remaining}}次,成为会员将不限制打招呼
+ 已获得每天{{friendEligibility.daily_limit}}次免费机会,今日还剩{{friendEligibility.remaining}}次
@@ -827,7 +827,7 @@ wepy.page({
modalName: '', // 弹框
pairedScore: 0,
userData: {}, // 自己的数据信息
- friendEligibility: {unlimited: false, remaining: 0},
+ friendEligibility: {unlimited: false, remaining: 0, daily_limit: 0},
checkingFriendEligibility: false,
userInfo: {}, // 当前用户详情数据
profile_courtship: {},
@@ -1256,7 +1256,7 @@ wepy.page({
if (code == 0) {
vm.modalName = ''
vm.$showToast('消息已发送')
- // Refresh authoritative quota; the third successful request opens membership checkout.
+ // Refresh quota; guide unfinished tasks before offering membership.
vm.ensureFriendEligibility()
}
vm.throttle = true
@@ -1282,7 +1282,7 @@ wepy.page({
// 联系朋友
async contactFriendFn() {
let vm = this
- // 登录后由服务端依次检查完整资料、认证及剩余次数。
+ // 登录后由服务端计算四项任务奖励及当日剩余次数。
if (!wx.getStorageSync('token')) {
vm.loginShow = true
return
@@ -1297,30 +1297,36 @@ wepy.page({
const {code, data} = await this.$get({url: `${service.host}/friend/request/eligibility`})
if (code !== 0 || !data) return false
this.friendEligibility = data
- if (data.action === 'profile') {
- this.modalName = ''
+ if (data.allowed === true) return true
+ this.modalName = ''
+ const tasks = {
+ real: {name: '真人认证', url: '/pages/users/realName'},
+ photo: {name: '上传真人头像并通过审核', url: '/pages/users/unmarriV2?progressShow=false'},
+ profile: {name: '完善资料', url: '/pages/users/unmarriV2?progressShow=false'},
+ education: {name: '学历认证', url: '/pages/users/degreeCertificate'}
+ }
+ const required = (data.required || []).filter(key => tasks[key])
+ if (required.length) {
wx.showModal({
- title: '请先完善个人资料',
- content: '完善个人资料后,再完成真人认证和学历认证,即可获得每天3次免费加好友机会。',
- confirmText: '去完善',
- success: res => { if (res.confirm) wx.navigateTo({url: '/pages/users/unmarriV2?progressShow=false'}) }
+ title: '领取更多免费机会',
+ content: '当前免费次数已用完。尚未完成:' + required.map(key => tasks[key].name).join('、') + '。每完成一项,每天增加1次免费加好友机会,最多4次/天。',
+ confirmText: '去完成',
+ success: res => {
+ if (!res.confirm) return
+ wx.showActionSheet({
+ itemList: required.map(key => tasks[key].name),
+ success: choice => { const task = tasks[required[choice.tapIndex]]; if (task) wx.navigateTo({url: task.url}) }
+ })
+ }
})
return false
}
if (data.action === 'purchase') {
- this.modalName = ''
- wx.navigateTo({url: '/pages/users/upgradeVIP'})
- return false
- }
- if (data.action === 'verify') {
- this.modalName = ''
- const required = data.required || []
- const real = required.indexOf('real') !== -1
wx.showModal({
- title: '完成认证领取免费机会',
- content: real ? (required.indexOf('education') !== -1 ? '完成真人认证和学历认证后,每天可免费加好友3次。请先完成真人认证。' : '完成真人认证后,每天可免费加好友3次。') : '完成学历认证后,每天可免费加好友3次。',
- confirmText: real ? '真人认证' : '学历认证',
- success: res => { if (res.confirm) wx.navigateTo({url: real ? '/pages/users/realName' : '/pages/users/degreeCertificate'}) }
+ title: '今日免费次数已用完',
+ content: '四项任务已全部完成,今天的4次免费机会已用完。升级会员可无限次加好友。',
+ confirmText: '升级会员',
+ success: res => { if (res.confirm) wx.navigateTo({url: '/pages/users/upgradeVIP'}) }
})
return false
}
diff --git a/src/pages/users/upgradeVIP.wpy b/src/pages/users/upgradeVIP.wpy
index fc9f617..4f06e66 100644
--- a/src/pages/users/upgradeVIP.wpy
+++ b/src/pages/users/upgradeVIP.wpy
@@ -23,14 +23,17 @@
{{ user.deadline_str ? user.deadline_str + ' 到期' : '未激活' }}
-
-
-
-
-
-
-
+ 选择适合你的会员
+ 左右滑动或点击卡片,比较套餐价格
+
+
+ {{cardCur == index ? '✓ 已选套餐' : '点击选择'}}
+
+ {{item.name}}
+ ¥{{item.displayPrice}}
+ {{item.month}}个月
+
+
会员套餐权益:
@@ -75,7 +78,7 @@ background: linear-gradient(270deg, #E6C7AB 0%, #F5E6D8 100%);">
- {{purchaseEnabled ? '点击查看当前套餐价格' : '会员购买暂不可用'}}
+ {{selectedPlan.name || '请选择套餐'}} · {{selectedPlan.displayPrice == '待确认' ? '价格待确认' : '¥' + selectedPlan.displayPrice}}
@@ -166,6 +169,7 @@ import https from '../../mixins/https'
import base from '../../mixins/base'
import {service} from '../../config.js'
import {buyMembership} from '../../utils/virtual-payment'
+import {membershipPlans, planPrice, defaultPlanIndex} from '../../utils/membership-plans'
const app = getApp().$wepy.$options
@@ -179,6 +183,8 @@ wepy.page({
activeV2: false,
swiperList: [],
cardCur: 0,
+ planTouchX: 0,
+ planTouchY: 0,
specialVIP: [],
payData: {
coin: '',
@@ -211,6 +217,7 @@ wepy.page({
loginShow: false
},
computed: {
+ selectedPlan() { return this.rankList[this.cardCur] || {name: '', displayPrice: '待确认'} },
residue() {
return (this.score - (this.price - this.my_rank_price)).toFixed(2)
}
@@ -258,13 +265,39 @@ wepy.page({
watch: {
cardCur() {
- this.payData.title = `超级会员(${this.rankList[this.cardCur].name})`
- this.payData.price = this.rankList[this.cardCur].price
- this.payData.discount_price = this.rankList[this.cardCur].discount_price
+ this.syncSelectedPlan()
}
},
methods: {
+ syncSelectedPlan() {
+ const item = this.rankList[this.cardCur]
+ if (!item) return
+ this.rankList = this.rankList.map((plan, index) => ({...plan,
+ displayPrice: planPrice(plan, this.virtualProducts),
+ position: index === this.cardCur ? 'is-selected' : index === (this.cardCur + this.rankList.length - 1) % this.rankList.length ? 'is-left' : 'is-right'
+ }))
+ this.payData.title = `超级会员(${item.name})`
+ this.payData.price = item.price
+ this.payData.discount_price = this.rankList[this.cardCur].displayPrice
+ this.shareId = item.id
+ },
+ selectPlan(index) {
+ if (this.purchasing || !this.rankList[index]) return
+ this.cardSwiper({$wx: {detail: {current: index}}})
+ this.syncSelectedPlan()
+ },
+ planTouchStart(e) {
+ const touch = (e.$wx || e).touches[0]
+ this.planTouchX = touch.clientX
+ this.planTouchY = touch.clientY
+ },
+ planTouchEnd(e) {
+ const touch = (e.$wx || e).changedTouches[0]
+ const dx = touch.clientX - this.planTouchX, dy = touch.clientY - this.planTouchY
+ if (Math.abs(dx) < 40 || Math.abs(dx) <= Math.abs(dy) || this.rankList.length < 2) return
+ this.selectPlan((this.cardCur + (dx < 0 ? 1 : -1) + this.rankList.length) % this.rankList.length)
+ },
pendingStorageKey() {const u = wx.getStorageSync('userInfo') || {}; return 'virtual-member-order:' + (u.id || u.user_id || 'anonymous')},
async checkRecentOrder() {
const orderNo = wx.getStorageSync(this.pendingStorageKey())
@@ -282,6 +315,7 @@ wepy.page({
async refreshPurchase() {
this.purchaseEnabled = false
try {const status = await this.paymentAPI().get('/rank/purchase/status'); this.virtualProducts = status.products || []; this.purchaseEnabled = status.enabled === true} catch (e) {this.virtualProducts = []}
+ this.syncSelectedPlan()
},
gotoApp(item) {
let URL = `https://love.ufutx.com/h5/#/serviceAgreementV3?title=${item}`
@@ -294,14 +328,10 @@ wepy.page({
vm.user = data.rank_info
let {type, is_subscribe} = wx.getStorageSync('userInfo')
vm.specialVIP = data.ranks
- vm.rankList = []
- data.ranks.sub_ranks.forEach((item) => {
- vm.rankList.push(item)
- })
+ vm.rankList = membershipPlans(data.ranks.sub_ranks)
+ vm.cardCur = defaultPlanIndex(vm.rankList)
if (!vm.rankList.length) {wx.hideLoading();return}
- vm.payData.title = `超级会员(${vm.rankList[0].name})`
- vm.payData.price = `${vm.rankList[0].price}`
- vm.payData.discount_price = `${vm.rankList[0].discount_price}`
+ vm.syncSelectedPlan()
vm.payData.pay_type = data.pay_type
vm.shareId = vm.rankList[vm.cardCur].id
if (wx.getStorageSync('from_user_id')) {
@@ -429,8 +459,10 @@ page {
width: 100%;
background: #171718;
position: relative;
- height: 70vw;
+ min-height: 610rpx;
+ height: auto;
padding-top: 6vw;
+ padding-bottom: 24rpx;
.userinfo-avatar {
width: 110rpx;
@@ -535,13 +567,23 @@ page {
}
}
- .card-swiper {
- width: 100% !important;
-
- .swiper-item {
- border-radius: 22rpx;
- }
- }
+ .plan-heading { clear: both; padding: 34rpx 32rpx 0; color: #fff; font-size: 32rpx; font-weight: 600; }
+ .plan-hint { padding: 10rpx 32rpx 0; color: #cbbdc1; font-size: 23rpx; }
+ .membership-carousel { position: relative; height: 386rpx; margin: 18rpx 20rpx 0; }
+ .membership-plan { position: absolute; box-sizing: border-box; width: 28%; top: 30rpx; height: 316rpx; padding: 18rpx 10rpx 12rpx; border: 2rpx solid #EED3DB; border-radius: 24rpx; background: #FFF9F5; text-align: center; color: #4E4145; transition: left .25s, width .25s, top .25s, height .25s; }
+ .membership-plan.is-left { left: 0; }
+ .membership-plan.is-right { left: 72%; }
+ .membership-plan.is-selected { left: 30%; top: 0; width: 40%; height: 376rpx; border: 4rpx solid #D92554; background: #FFF1F4; box-shadow: 0 12rpx 30rpx rgba(217,37,84,.2); z-index: 2; }
+ .plan-badge { color: #97717D; font-size: 20rpx; height: 30rpx; }
+ .is-selected .plan-badge { color: #D92554; font-weight: 600; }
+ .plan-picture { width: 100%; height: 80rpx; display: block; margin: 6rpx auto 8rpx; }
+ .is-selected .plan-picture { height: 102rpx; }
+ .plan-name { color: #A3294D; font-size: 28rpx; font-weight: 600; }
+ .is-selected .plan-name { font-size: 34rpx; }
+ .plan-price { color: #A3294D; font-size: 42rpx; line-height: 1.4; font-weight: 700; white-space: nowrap; }
+ .is-selected .plan-price { color: #D92554; font-size: 60rpx; }
+ .plan-currency { font-size: 24rpx; margin-right: 2rpx; }
+ .plan-duration { font-size: 22rpx; color: #97717D; }
.iconV2 {
height: 36rpx;
diff --git a/src/utils/membership-plans.js b/src/utils/membership-plans.js
new file mode 100644
index 0000000..dd1e513
--- /dev/null
+++ b/src/utils/membership-plans.js
@@ -0,0 +1,17 @@
+// Only show durations supplied by the API; never manufacture a missing plan or price.
+export function membershipPlans(rows) {
+ const plans = [1, 3, 12].map(month => (rows || []).find(item => Number(item.month) === month)).filter(Boolean)
+ return plans.map(item => ({...item, displayPrice: '待确认', position: ''}))
+}
+
+export function planPrice(item, products) {
+ const product = (products || []).find(p => Number(p.sub_rank_id) === Number(item.id))
+ const raw = product ? Number(product.price_fen) / 100 : (item.discount_price == null ? item.price : item.discount_price)
+ if (raw === '' || raw == null || !Number.isFinite(Number(raw)) || Number(raw) < 0) return '待确认'
+ return Number(raw).toFixed(2).replace(/\.00$/, '')
+}
+
+export function defaultPlanIndex(plans) {
+ const quarter = plans.findIndex(item => Number(item.month) === 3)
+ return quarter < 0 ? 0 : quarter
+}
diff --git a/tools/local-acceptance/friend-eligibility.test.cjs b/tools/local-acceptance/friend-eligibility.test.cjs
index 8b37acb..fce156a 100644
--- a/tools/local-acceptance/friend-eligibility.test.cjs
+++ b/tools/local-acceptance/friend-eligibility.test.cjs
@@ -1,19 +1,20 @@
const fs=require('fs'),vm=require('vm'),assert=require('assert');
const source=fs.readFileSync('ufutx_love_mp/src/pages/home/information.wpy','utf8').match(/