feat: show trial plan with used/disabled state on subscribe page
- Add GET /api/v1/subscription/plans public endpoint for pricing - Subscription API now returns trial_used field - Subscribe-plans page shows trial card, greyed out with "已使用" tag when trial has been used - Trial activation calls dedicated trial API, not purchase - Prices fetched from server settings, fallback to defaults
这个提交包含在:
@@ -32,33 +32,52 @@ Page({
|
|||||||
loadPlans: function () {
|
loadPlans: function () {
|
||||||
var self = this
|
var self = this
|
||||||
api.getPlans().then(function (data) {
|
api.getPlans().then(function (data) {
|
||||||
var plans = (data.plans || []).filter(function (p) { return p.key !== 'trial' }).map(function (p) {
|
var serverPlans = data.plans || []
|
||||||
|
var monthlyPrice = 99
|
||||||
|
for (var i = 0; i < serverPlans.length; i++) {
|
||||||
|
if (serverPlans[i].key === 'monthly') monthlyPrice = serverPlans[i].price
|
||||||
|
}
|
||||||
|
var plans = serverPlans.map(function (p) {
|
||||||
var daily = p.days > 0 ? (p.price / p.days).toFixed(1) : 0
|
var daily = p.days > 0 ? (p.price / p.days).toFixed(1) : 0
|
||||||
|
var desc = ''
|
||||||
|
if (p.key === 'trial') desc = p.days + '天免费体验'
|
||||||
|
else if (p.key === 'yearly') desc = '省¥' + Math.round(monthlyPrice * 12 - p.price)
|
||||||
|
else desc = '约' + daily + '元/天'
|
||||||
return {
|
return {
|
||||||
key: p.key,
|
key: p.key,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
price: p.price,
|
price: p.price,
|
||||||
priceLabel: '¥' + p.price,
|
priceLabel: p.key === 'trial' ? '免费' : '¥' + p.price,
|
||||||
desc: p.key === 'yearly' ? '省¥' + ((p.price / 12 * 12) > 0 ? Math.round(self._monthlyPrice(data.plans) * 12 - p.price) : '') : '约' + daily + '元/天',
|
desc: desc,
|
||||||
tag: p.key === 'yearly' ? '推荐' : ''
|
tag: p.key === 'yearly' ? '推荐' : '',
|
||||||
|
disabled: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (plans.length > 0) self.setData({ plans: plans })
|
if (plans.length > 0) self.setData({ plans: plans })
|
||||||
|
self.checkTrialUsed()
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
self.setData({
|
self.setData({
|
||||||
plans: [
|
plans: [
|
||||||
|
{ key: 'trial', name: '试用', price: 0, priceLabel: '免费', desc: '7天免费体验', disabled: false },
|
||||||
{ key: 'monthly', name: '月卡', price: 99, priceLabel: '¥99', desc: '约3.3元/天' },
|
{ key: 'monthly', name: '月卡', price: 99, priceLabel: '¥99', desc: '约3.3元/天' },
|
||||||
{ key: 'yearly', name: '年卡', price: 899, priceLabel: '¥899', desc: '省¥289', tag: '推荐' }
|
{ key: 'yearly', name: '年卡', price: 899, priceLabel: '¥899', desc: '省¥289', tag: '推荐' }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
self.checkTrialUsed()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
_monthlyPrice: function (plans) {
|
checkTrialUsed: function () {
|
||||||
for (var i = 0; i < plans.length; i++) {
|
var self = this
|
||||||
if (plans[i].key === 'monthly') return plans[i].price
|
var sub = self.data.subscription
|
||||||
|
if (sub && sub.trial_used) {
|
||||||
|
var plans = self.data.plans.map(function (p) {
|
||||||
|
if (p.key === 'trial') return Object.assign({}, p, { disabled: true, tag: '已使用' })
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
self.setData({ plans: plans })
|
||||||
|
if (self.data.selected === 'trial') self.setData({ selected: 'yearly' })
|
||||||
}
|
}
|
||||||
return 99
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loadSubscription: function () {
|
loadSubscription: function () {
|
||||||
@@ -68,6 +87,7 @@ Page({
|
|||||||
subscription: sub,
|
subscription: sub,
|
||||||
subRemaining: sub.remaining_days || 0
|
subRemaining: sub.remaining_days || 0
|
||||||
})
|
})
|
||||||
|
self.checkTrialUsed()
|
||||||
}).catch(function () {})
|
}).catch(function () {})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -85,7 +105,18 @@ Page({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!plan) return
|
if (!plan || plan.disabled) return
|
||||||
|
|
||||||
|
if (plan.key === 'trial') {
|
||||||
|
wx.showModal({
|
||||||
|
title: '激活试用',
|
||||||
|
content: '免费试用 ' + plan.desc + '?',
|
||||||
|
success: function (res) {
|
||||||
|
if (res.confirm) self.doActivateTrial()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title: '确认支付',
|
title: '确认支付',
|
||||||
@@ -98,6 +129,21 @@ Page({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
doActivateTrial: function () {
|
||||||
|
var self = this
|
||||||
|
self.setData({ purchasing: true })
|
||||||
|
api.activateTrial().then(function () {
|
||||||
|
self.setData({ purchasing: false })
|
||||||
|
wx.showToast({ title: '试用已激活', icon: 'success' })
|
||||||
|
setTimeout(function () {
|
||||||
|
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=trial' })
|
||||||
|
}, 1000)
|
||||||
|
}).catch(function (err) {
|
||||||
|
self.setData({ purchasing: false })
|
||||||
|
wx.showToast({ title: err.message || '激活失败', icon: 'none' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
doPurchase: function (plan) {
|
doPurchase: function (plan) {
|
||||||
var self = this
|
var self = this
|
||||||
self.setData({ purchasing: true })
|
self.setData({ purchasing: true })
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="plans">
|
<view class="plans">
|
||||||
<view class="plan {{selected === item.key ? 'selected' : ''}}" wx:for="{{plans}}" wx:key="key" bindtap="onSelect" data-plan="{{item.key}}">
|
<view class="plan {{selected === item.key ? 'selected' : ''}} {{item.disabled ? 'plan-disabled' : ''}}" wx:for="{{plans}}" wx:key="key" bindtap="{{item.disabled ? '' : 'onSelect'}}" data-plan="{{item.key}}">
|
||||||
<view class="plan-tag {{selected === item.key ? 'plan-tag-active' : ''}}" wx:if="{{item.tag}}">{{item.tag}}</view>
|
<view class="plan-tag {{item.disabled ? 'plan-tag-disabled' : (selected === item.key ? 'plan-tag-active' : '')}}" wx:if="{{item.tag}}">{{item.tag}}</view>
|
||||||
<view class="plan-price">{{item.priceLabel}}</view>
|
<view class="plan-price">{{item.priceLabel}}</view>
|
||||||
<view class="plan-name">{{item.name}}</view>
|
<view class="plan-name">{{item.name}}</view>
|
||||||
<view class="plan-save">{{item.desc}}</view>
|
<view class="plan-save">{{item.desc}}</view>
|
||||||
|
|||||||
@@ -118,3 +118,13 @@
|
|||||||
.btn-primary-gold::after {
|
.btn-primary-gold::after {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plan-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-tag-disabled {
|
||||||
|
background: #ccc;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,13 +27,15 @@ router.get('/subscription/plans', wrap(async (req, res) => {
|
|||||||
|
|
||||||
router.get('/subscription', requireUser, wrap(async (req, res) => {
|
router.get('/subscription', requireUser, wrap(async (req, res) => {
|
||||||
const sub = await subscriptionDao.findActive(req.user.user_id)
|
const sub = await subscriptionDao.findActive(req.user.user_id)
|
||||||
if (!sub) return res.json(ok({ status: 'inactive', plan: 'none', remaining_days: 0 }))
|
const trialUsed = await subscriptionDao.findTrial(req.user.user_id)
|
||||||
|
if (!sub) return res.json(ok({ status: 'inactive', plan: 'none', remaining_days: 0, trial_used: !!trialUsed }))
|
||||||
res.json(ok({
|
res.json(ok({
|
||||||
status: sub.remaining_days > 0 ? 'active' : 'expired',
|
status: sub.remaining_days > 0 ? 'active' : 'expired',
|
||||||
plan: sub.plan,
|
plan: sub.plan,
|
||||||
start_time: sub.start_time,
|
start_time: sub.start_time,
|
||||||
expire_time: sub.expire_time,
|
expire_time: sub.expire_time,
|
||||||
remaining_days: sub.remaining_days
|
remaining_days: sub.remaining_days,
|
||||||
|
trial_used: !!trialUsed
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户