Merge branch 'feat/production-ready'
这个提交包含在:
@@ -96,7 +96,7 @@ Page({
|
||||
onMockScanDone: function () {
|
||||
var self = this
|
||||
if (self._progressTimer) clearInterval(self._progressTimer)
|
||||
var mask = parseInt(self.options.regions) || 0x7F
|
||||
var mask = parseInt(self.data.regions) || 0x7F
|
||||
wx.redirectTo({
|
||||
url: '/pages/treating/treating?regions=' + mask +
|
||||
'&wavelength=2' +
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "护理记录",
|
||||
"navigationBarBackgroundColor": "#E6508C",
|
||||
"navigationBarTextStyle": "white"
|
||||
"navigationBarTextStyle": "white",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
|
||||
+23
-10
@@ -14,11 +14,15 @@ Page({
|
||||
bleState: 'disconnected',
|
||||
hasDevice: false,
|
||||
deviceInfo: null,
|
||||
devMode: false
|
||||
devMode: false,
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
this.setData({ devMode: config.__DEV__ || false })
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.setData({ devMode: config.__DEV__ || false })
|
||||
this.checkState()
|
||||
this._onStatus = this.onBleStatus.bind(this)
|
||||
this._onBattery = this.onBleBattery.bind(this)
|
||||
@@ -53,9 +57,10 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
var isFirstLoad = self.data.loading
|
||||
self.setData({ connected: ble.isConnected() })
|
||||
|
||||
api.getDevices().then(function (data) {
|
||||
var p1 = api.getDevices().then(function (data) {
|
||||
var devices = data.devices || []
|
||||
self.setData({ hasDevice: devices.length > 0 })
|
||||
if (devices.length > 0) {
|
||||
@@ -65,14 +70,22 @@ Page({
|
||||
deviceInfo: devices[0]
|
||||
})
|
||||
}
|
||||
}).catch(function () {})
|
||||
}).catch(function (err) {
|
||||
console.error('getDevices failed', err)
|
||||
})
|
||||
|
||||
api.getSubscription().then(function (sub) {
|
||||
var p2 = api.getSubscription().then(function (sub) {
|
||||
self.setData({
|
||||
subscription: sub,
|
||||
subRemaining: sub.remaining_days || 0
|
||||
})
|
||||
}).catch(function () {})
|
||||
}).catch(function (err) {
|
||||
console.error('getSubscription failed', err)
|
||||
})
|
||||
|
||||
Promise.all([p1, p2]).then(function () {
|
||||
if (isFirstLoad) self.setData({ loading: false })
|
||||
})
|
||||
},
|
||||
|
||||
onBleStatus: function (status) {
|
||||
@@ -98,7 +111,9 @@ Page({
|
||||
},
|
||||
onConnected: function () {
|
||||
self.setData({ connected: true, bleState: 'connected' })
|
||||
ble.queryStatus().catch(function () {})
|
||||
ble.queryStatus().catch(function (err) {
|
||||
console.error('queryStatus failed', err)
|
||||
})
|
||||
},
|
||||
onError: function (err) {
|
||||
self.setData({ connected: false, bleState: 'error' })
|
||||
@@ -179,8 +194,6 @@ Page({
|
||||
},
|
||||
|
||||
onViewSubscription: function () {
|
||||
if (!this.data.subscription || this.data.subscription.status !== 'active') {
|
||||
wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' })
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<view class="page">
|
||||
<view class="page-content" wx:if="{{!hasDevice}}">
|
||||
<view class="loading-container" wx:if="{{loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content" wx:if="{{!loading && !hasDevice}}">
|
||||
<view class="empty-device">
|
||||
<text class="empty-icon">📱</text>
|
||||
<view class="empty-text">还没有绑定设备</view>
|
||||
@@ -11,16 +16,16 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content" wx:if="{{hasDevice}}">
|
||||
<view class="page-content" wx:if="{{!loading && hasDevice}}">
|
||||
<view class="device-card">
|
||||
<view class="device-icon">💆</view>
|
||||
<view class="device-name">{{deviceName || '我的光面膜'}}</view>
|
||||
<text class="status-badge status-badge-green" wx:if="{{connected}}">已连接</text>
|
||||
<text class="status-badge" style="background:#ccc;" wx:else>未连接</text>
|
||||
<view class="device-battery" wx:if="{{connected}}">
|
||||
<text>🔋</text>
|
||||
<text>电量 {{battery}}%</text>
|
||||
<view class="device-card-icon">💆</view>
|
||||
<view class="device-card-name">{{deviceName || '光子美容仪'}}</view>
|
||||
<view class="device-card-status">
|
||||
<text class="status-badge status-badge-green" wx:if="{{connected}}">已连接</text>
|
||||
<text class="status-badge" style="background:#ccc;" wx:else>未连接</text>
|
||||
<text class="device-card-battery" wx:if="{{connected}}">🔋 {{battery}}%</text>
|
||||
</view>
|
||||
<button class="btn-reconnect" bindtap="onConnectBle" wx:if="{{!connected}}">重新连接</button>
|
||||
</view>
|
||||
|
||||
<view class="sub-info" bindtap="onViewSubscription">
|
||||
@@ -32,7 +37,9 @@
|
||||
<text class="sub-info-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary" bindtap="onStartTreatment">开始护理</button>
|
||||
<button class="btn-secondary" bindtap="onManageDevice">设备管理</button>
|
||||
<view class="action-buttons">
|
||||
<button class="btn-primary" bindtap="onStartTreatment">开始护理</button>
|
||||
<button class="btn-secondary" bindtap="onManageDevice">设备管理</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -19,6 +19,62 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 36rpx 28rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.device-card-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.device-card-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.device-card-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.device-card-battery {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-reconnect {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 24rpx;
|
||||
padding: 20rpx;
|
||||
background: #f5f5f5;
|
||||
color: #E6508C;
|
||||
border: 2rpx solid #E6508C;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-reconnect::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -28,6 +84,7 @@
|
||||
padding: 24rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.sub-info-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -35,10 +92,39 @@
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sub-info-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.sub-info-arrow {
|
||||
color: #999;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 6rpx solid #f0f0f0;
|
||||
border-top-color: #E6508C;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var api = require('../../utils/api')
|
||||
var config = require('../../config/env')
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
@@ -6,7 +7,12 @@ Page({
|
||||
userInfo: null,
|
||||
subscription: null,
|
||||
deviceCount: 0,
|
||||
subRemaining: 0
|
||||
subRemaining: 0,
|
||||
loading: true,
|
||||
editing: false,
|
||||
editNickname: '',
|
||||
editAvatarUrl: '',
|
||||
saving: false
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
@@ -15,19 +21,29 @@ Page({
|
||||
|
||||
loadProfile: function () {
|
||||
var self = this
|
||||
api.getProfile().then(function (profile) {
|
||||
self.setData({ loading: true })
|
||||
|
||||
var p1 = api.getProfile().then(function (profile) {
|
||||
self.setData({
|
||||
userInfo: profile,
|
||||
deviceCount: profile.device_count || 0
|
||||
})
|
||||
}).catch(function () {})
|
||||
}).catch(function (err) {
|
||||
console.error('getProfile failed', err)
|
||||
})
|
||||
|
||||
api.getSubscription().then(function (sub) {
|
||||
var p2 = api.getSubscription().then(function (sub) {
|
||||
self.setData({
|
||||
subscription: sub,
|
||||
subRemaining: sub.remaining_days || 0
|
||||
})
|
||||
}).catch(function () {})
|
||||
}).catch(function (err) {
|
||||
console.error('getSubscription failed', err)
|
||||
})
|
||||
|
||||
Promise.all([p1, p2]).then(function () {
|
||||
self.setData({ loading: false })
|
||||
})
|
||||
},
|
||||
|
||||
onManageDevice: function () {
|
||||
@@ -75,6 +91,93 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/contact/contact' })
|
||||
},
|
||||
|
||||
onEditProfile: function () {
|
||||
var info = this.data.userInfo || {}
|
||||
this.setData({
|
||||
editing: true,
|
||||
editNickname: info.nickname || '',
|
||||
editAvatarUrl: info.avatar || ''
|
||||
})
|
||||
},
|
||||
|
||||
onCancelEdit: function () {
|
||||
this.setData({ editing: false })
|
||||
},
|
||||
|
||||
onEditNicknameInput: function (e) {
|
||||
this.setData({ editNickname: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onPickAvatar: function () {
|
||||
var self = this
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function (res) {
|
||||
if (res.tempFiles && res.tempFiles[0]) {
|
||||
self.setData({ editAvatarUrl: res.tempFiles[0].tempFilePath })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
uploadAvatar: function (tempPath) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var token = wx.getStorageSync('token')
|
||||
wx.uploadFile({
|
||||
url: config.API_BASE + '/api/v1/user/avatar',
|
||||
filePath: tempPath,
|
||||
name: 'file',
|
||||
header: { Authorization: 'Bearer ' + token },
|
||||
success: function (res) {
|
||||
try {
|
||||
var data = JSON.parse(res.data)
|
||||
if (data.code === 0 && data.data && data.data.avatar) {
|
||||
resolve(data.data.avatar)
|
||||
} else {
|
||||
reject(new Error(data.message || '上传失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
},
|
||||
fail: function () { reject(new Error('上传失败')) }
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
onSaveProfile: function () {
|
||||
var self = this
|
||||
var nickname = (self.data.editNickname || '').trim()
|
||||
if (!nickname) {
|
||||
wx.showToast({ title: '昵称不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
self.setData({ saving: true })
|
||||
|
||||
var avatarUrl = self.data.editAvatarUrl
|
||||
var isLocalFile = avatarUrl && (avatarUrl.indexOf('wxfile://') === 0 || avatarUrl.indexOf('http://tmp') === 0)
|
||||
var avatarPromise = isLocalFile
|
||||
? self.uploadAvatar(avatarUrl)
|
||||
: Promise.resolve(avatarUrl || '')
|
||||
|
||||
avatarPromise.then(function (permanentUrl) {
|
||||
return api.updateProfile({
|
||||
nickname: nickname,
|
||||
avatar: permanentUrl || ''
|
||||
})
|
||||
}).then(function () {
|
||||
self.setData({ saving: false, editing: false })
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
self.loadProfile()
|
||||
}).catch(function (err) {
|
||||
self.setData({ saving: false })
|
||||
wx.showToast({ title: err.message || '保存失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onLogout: function () {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<view class="page">
|
||||
<view class="page-content">
|
||||
<view class="loading-container" wx:if="{{loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content" wx:if="{{!loading}}">
|
||||
<view class="user-row">
|
||||
<view class="user-avatar">👤</view>
|
||||
<image class="user-avatar-img" wx:if="{{userInfo.avatar}}" src="{{userInfo.avatar}}" mode="aspectFill"></image>
|
||||
<view class="user-avatar" wx:else>👤</view>
|
||||
<view class="user-info">
|
||||
<view class="user-name">{{userInfo.nickname || '未登录'}}</view>
|
||||
<view class="user-phone">{{userInfo.phone || ''}}</view>
|
||||
</view>
|
||||
<view class="edit-profile-btn" bindtap="onEditProfile">编辑资料</view>
|
||||
</view>
|
||||
|
||||
<view class="sub-card" wx:if="{{subscription && subscription.status === 'active' && subRemaining > 0}}">
|
||||
@@ -55,4 +62,23 @@
|
||||
|
||||
<view class="logout-btn" bindtap="onLogout">退出登录</view>
|
||||
</view>
|
||||
|
||||
<!-- Edit profile modal -->
|
||||
<view class="edit-mask" wx:if="{{editing}}" bindtap="onCancelEdit"></view>
|
||||
<view class="edit-modal" wx:if="{{editing}}">
|
||||
<view class="edit-modal-title">编辑资料</view>
|
||||
<view class="edit-avatar-row" bindtap="onPickAvatar">
|
||||
<image class="edit-avatar-img" wx:if="{{editAvatarUrl}}" src="{{editAvatarUrl}}" mode="aspectFill"></image>
|
||||
<view class="edit-avatar-placeholder" wx:else>👤</view>
|
||||
<text class="edit-avatar-hint">点击更换头像</text>
|
||||
</view>
|
||||
<view class="edit-field">
|
||||
<text class="edit-label">昵称</text>
|
||||
<input class="edit-input" value="{{editNickname}}" bindinput="onEditNicknameInput" placeholder="请输入昵称" maxlength="20" />
|
||||
</view>
|
||||
<view class="edit-actions">
|
||||
<button class="btn-secondary edit-btn" bindtap="onCancelEdit">取消</button>
|
||||
<button class="btn-primary edit-btn" bindtap="onSaveProfile" disabled="{{saving}}" loading="{{saving}}">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -107,11 +107,144 @@
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 6rpx solid #f0f0f0;
|
||||
border-top-color: #E6508C;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sub-card-inactive {
|
||||
background: #f5f5f5;
|
||||
border: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
.user-avatar-img {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-profile-btn {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
border-radius: 24rpx;
|
||||
padding: 6rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.edit-modal {
|
||||
position: fixed;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.edit-modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.edit-avatar-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.edit-avatar-img {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.edit-avatar-placeholder {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 56rpx;
|
||||
}
|
||||
|
||||
.edit-avatar-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.edit-field {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.edit-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.edit-input {
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
flex: 1;
|
||||
margin: 0 !important;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
|
||||
@@ -8,7 +8,8 @@ Page({
|
||||
selected: 'yearly',
|
||||
purchasing: false,
|
||||
subscription: null,
|
||||
subRemaining: 0
|
||||
subRemaining: 0,
|
||||
loadingPlans: true
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
@@ -31,6 +32,7 @@ Page({
|
||||
|
||||
loadPlans: function () {
|
||||
var self = this
|
||||
self.setData({ loadingPlans: true })
|
||||
api.getPlans().then(function (data) {
|
||||
var serverPlans = data.plans || []
|
||||
var monthlyPrice = 99
|
||||
@@ -54,9 +56,11 @@ Page({
|
||||
}
|
||||
})
|
||||
if (plans.length > 0) self.setData({ plans: plans })
|
||||
self.setData({ loadingPlans: false })
|
||||
self.checkTrialUsed()
|
||||
}).catch(function () {
|
||||
self.setData({
|
||||
loadingPlans: false,
|
||||
plans: [
|
||||
{ key: 'trial', name: '试用', price: 0, priceLabel: '免费', desc: '7天免费体验', disabled: false },
|
||||
{ key: 'monthly', name: '月卡', price: 99, priceLabel: '¥99', desc: '约3.3元/天' },
|
||||
@@ -126,8 +130,8 @@ Page({
|
||||
var planDays = plan.key === 'yearly' ? 365 : 30
|
||||
var title = isRenew ? '确认续费' : '确认支付'
|
||||
var content = isRenew
|
||||
? '在现有订阅基础上延长' + planDays + '天,模拟支付 ¥' + plan.price + '?(测试环境)'
|
||||
: '模拟支付 ¥' + plan.price + '?(测试环境)'
|
||||
? '在现有订阅基础上延长' + planDays + '天,支付 ¥' + plan.price
|
||||
: '支付 ¥' + plan.price
|
||||
|
||||
wx.showModal({
|
||||
title: title,
|
||||
@@ -158,15 +162,72 @@ Page({
|
||||
doPurchase: function (plan) {
|
||||
var self = this
|
||||
self.setData({ purchasing: true })
|
||||
api.mockPurchase(plan.key).then(function (order) {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + plan.key })
|
||||
}, 1000)
|
||||
api.purchase(plan.key).then(function (data) {
|
||||
if (data.mock || !data.payment_params) {
|
||||
// Dev fallback: use mock purchase
|
||||
return api.mockPurchase(plan.key).then(function () {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + plan.key })
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
// Real payment
|
||||
var params = data.payment_params
|
||||
self._currentOrderId = data.order_id
|
||||
wx.requestPayment({
|
||||
timeStamp: params.timeStamp,
|
||||
nonceStr: params.nonceStr,
|
||||
package: params.package,
|
||||
signType: params.signType,
|
||||
paySign: params.paySign,
|
||||
success: function () {
|
||||
// Payment dialog succeeded, sync order to confirm
|
||||
self.syncAndRedirect(data.order_id, plan.key)
|
||||
},
|
||||
fail: function (err) {
|
||||
self.setData({ purchasing: false })
|
||||
var msg = (err.errMsg || '').indexOf('cancel') > -1 ? '已取消支付' : '支付失败'
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: err.message || '支付失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || '创建订单失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
syncAndRedirect: function (orderId, planKey) {
|
||||
var self = this
|
||||
var retryCount = 0
|
||||
var maxRetries = 3
|
||||
|
||||
function pollSync() {
|
||||
api.syncPaymentOrder(orderId).then(function (result) {
|
||||
if (result.status === 'paid') {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + planKey })
|
||||
}, 1000)
|
||||
} else if (retryCount < maxRetries) {
|
||||
retryCount++
|
||||
setTimeout(pollSync, 2000)
|
||||
} else {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付处理中,请稍后在订阅页查看', icon: 'none' })
|
||||
}
|
||||
}).catch(function () {
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++
|
||||
setTimeout(pollSync, 2000)
|
||||
} else {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付处理中,请稍后在订阅页查看', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
pollSync()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
|
||||
<view class="page-title">选择订阅套餐</view>
|
||||
|
||||
<view class="service-card">
|
||||
<view class="loading-plans" wx:if="{{loadingPlans}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载套餐中...</text>
|
||||
</view>
|
||||
|
||||
<view class="service-card" wx:if="{{!loadingPlans}}">
|
||||
<view class="service-icon">✨</view>
|
||||
<view class="service-info">
|
||||
<view class="service-name">智能模式</view>
|
||||
@@ -28,7 +33,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="plans">
|
||||
<view class="plans" wx:if="{{!loadingPlans}}">
|
||||
<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 {{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>
|
||||
@@ -37,9 +42,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary btn-primary-gold" bindtap="onPurchase" disabled="{{purchasing}}" loading="{{purchasing}}">
|
||||
{{subscription && subscription.status === 'active' && subRemaining > 0 ? '续费' : '确认支付'}}
|
||||
</button>
|
||||
<view class="agreement" bindtap="onAgreement">支付即表示同意《订阅协议》</view>
|
||||
<block wx:if="{{!loadingPlans}}">
|
||||
<button class="btn-primary btn-primary-gold" bindtap="onPurchase" disabled="{{purchasing}}" loading="{{purchasing}}">
|
||||
{{subscription && subscription.status === 'active' && subRemaining > 0 ? '续费' : '确认支付'}}
|
||||
</button>
|
||||
<view class="agreement" bindtap="onAgreement">支付即表示同意《订阅协议》</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -128,3 +128,30 @@
|
||||
background: #ccc;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.loading-plans {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 6rpx solid #f0f0f0;
|
||||
border-top-color: #DCB982;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户