feat: 存储空间管理、虚拟支付接入与容量守卫
- 新增存储空间管理:会员套餐配置、用户容量/到期管理、后台支付记录查询 - 接入个人主体小程序虚拟支付:下单/发货推送/兜底查单(query_order) - 修复 adminStorageUsers 等接口鉴权、out_trade_no 长度、前端 requestVirtualPayment 参数 - 前端上传前容量预检,超容引导升级无限空间 - 我的页会员卡片展示「无限空间」与已用容量/到期时间 - 后台「存储空间管理」新增支付记录标签页
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
function formatBytes(bytes) {
|
||||
bytes = bytes || 0;
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
Page({
|
||||
onResize: function () { this.setData(getLayout()); },
|
||||
|
||||
data: {
|
||||
loading: true,
|
||||
error: false,
|
||||
usedBytes: 0,
|
||||
usedText: '0 MB',
|
||||
freeMb: 200,
|
||||
freeText: '200 MB',
|
||||
percent: 0,
|
||||
overQuota: false,
|
||||
isVip: false,
|
||||
vipExpireText: '',
|
||||
plans: [],
|
||||
paying: false,
|
||||
},
|
||||
|
||||
onLoad: function () { this.setData(getLayout()); },
|
||||
onShow: function () { this.loadInfo(); },
|
||||
onPullDownRefresh: function () { this.loadInfo().then(function () { wx.stopPullDownRefresh(); }); },
|
||||
|
||||
loadInfo: function () {
|
||||
var self = this;
|
||||
this.setData({ loading: true, error: false });
|
||||
return api.storageInfo().then(function (d) {
|
||||
var freeBytes = d.free_bytes || d.freeBytes || 0;
|
||||
var usedBytes = d.used_bytes || d.usedBytes || 0;
|
||||
var percent = freeBytes > 0 ? Math.min(100, Math.round(usedBytes / freeBytes * 100)) : 0;
|
||||
var overQuota = usedBytes > freeBytes;
|
||||
var plans = (d.plans || []).map(function (p) {
|
||||
var cents = p.price_cents || p.priceCents || 0;
|
||||
return {
|
||||
key: p.key,
|
||||
name: p.name,
|
||||
priceText: (cents / 100).toFixed(cents % 100 === 0 ? 0 : 1),
|
||||
unit: p.key === 'year' ? '年' : '月',
|
||||
productId: p.product_id || p.productId,
|
||||
};
|
||||
});
|
||||
var vipExpireText = '';
|
||||
if (d.is_vip || d.isVip) {
|
||||
var expire = d.vip_expire_at || d.vipExpireAt ? String(d.vip_expire_at || d.vipExpireAt).slice(0, 10) : '';
|
||||
vipExpireText = expire ? ('有效期至 ' + expire) : '已开通无限空间';
|
||||
}
|
||||
self.setData({
|
||||
loading: false,
|
||||
usedBytes: usedBytes,
|
||||
usedText: formatBytes(usedBytes),
|
||||
freeMb: d.free_mb || d.freeMb || 200,
|
||||
freeText: formatBytes(freeBytes),
|
||||
percent: percent,
|
||||
overQuota: overQuota,
|
||||
isVip: !!(d.is_vip || d.isVip),
|
||||
vipExpireText: vipExpireText,
|
||||
plans: plans,
|
||||
});
|
||||
}).catch(function (e) {
|
||||
self.setData({ loading: false, error: true });
|
||||
console.error('加载存储信息失败', e);
|
||||
});
|
||||
},
|
||||
|
||||
buyPlan: function (e) {
|
||||
var key = e.currentTarget.dataset.key;
|
||||
var self = this;
|
||||
if (this.data.paying) return;
|
||||
var plan = null;
|
||||
for (var i = 0; i < this.data.plans.length; i++) {
|
||||
if (this.data.plans[i].key === key) { plan = this.data.plans[i]; break; }
|
||||
}
|
||||
if (!plan) return;
|
||||
|
||||
wx.showLoading({ title: '下单中...', mask: true });
|
||||
this.setData({ paying: true });
|
||||
api.storageCreateOrder({ plan_key: key }).then(function (order) {
|
||||
wx.hideLoading();
|
||||
if (!order || !(order.sign_data || order.signData)) {
|
||||
self.setData({ paying: false });
|
||||
wx.showToast({ title: '下单失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
self.callVirtualPay(order);
|
||||
}).catch(function (err) {
|
||||
wx.hideLoading();
|
||||
self.setData({ paying: false });
|
||||
wx.showToast({ title: (err && err.error) || '下单失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
|
||||
callVirtualPay: function (order) {
|
||||
var self = this;
|
||||
if (!wx.requestVirtualPayment) {
|
||||
this.setData({ paying: false });
|
||||
wx.showModal({
|
||||
title: '版本过低',
|
||||
content: '请升级微信至最新版本后再试。',
|
||||
showCancel: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
// wx.requestVirtualPayment 仅接受 signData / paySig / signature / mode 四个参数;
|
||||
// offerId、env、productId、goodsPrice 等均已编码在 signData JSON 字符串内,不可再平铺为顶层参数。
|
||||
wx.requestVirtualPayment({
|
||||
signData: order.sign_data || order.signData,
|
||||
paySig: order.pay_sig || order.paySig,
|
||||
signature: order.signature,
|
||||
mode: order.mode || 'short_series_goods',
|
||||
success: function () {
|
||||
self.setData({ paying: false });
|
||||
wx.showToast({ title: '支付成功', icon: 'success' });
|
||||
setTimeout(function () { self.loadInfo(); }, 1200);
|
||||
},
|
||||
fail: function (err) {
|
||||
self.setData({ paying: false });
|
||||
var msg = '支付未完成';
|
||||
var m = (err && err.errMsg) || '';
|
||||
if (m.indexOf('cancel') >= 0) msg = '已取消支付';
|
||||
else if (m.indexOf('fail') >= 0) msg = '支付失败,请稍后重试';
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<view class="app-shell app-shell--secondary j-handwritten-page" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/storage/storage.wxml -->
|
||||
<app-nav title="存储空间" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="j-body">
|
||||
<!-- 已用空间 -->
|
||||
<view class="storage-card">
|
||||
<view class="storage-card-head">
|
||||
<view class="storage-title">已用空间</view>
|
||||
<view class="storage-vip" wx:if="{{isVip}}">👑 无限空间</view>
|
||||
</view>
|
||||
<view class="storage-used">
|
||||
<text class="storage-num">{{usedText}}</text>
|
||||
<text class="storage-of" wx:if="{{!isVip}}"> / {{freeText}} 免费</text>
|
||||
</view>
|
||||
<view class="storage-bar" wx:if="{{!isVip}}">
|
||||
<view class="storage-bar-fill" style="width: {{percent}}%;"></view>
|
||||
</view>
|
||||
<view class="storage-hint" wx:if="{{isVip}}">{{vipExpireText}}</view>
|
||||
<view class="storage-hint over" wx:elif="{{overQuota}}">已超出免费空间,升级后即可继续上传</view>
|
||||
<view class="storage-hint" wx:else>已使用 {{percent}}% 免费空间</view>
|
||||
</view>
|
||||
|
||||
<!-- 套餐 -->
|
||||
<view class="section-heading">升级无限空间</view>
|
||||
<view class="plan-list">
|
||||
<view class="plan-card" wx:for="{{plans}}" wx:key="key" bindtap="buyPlan" data-key="{{item.key}}">
|
||||
<view class="plan-info">
|
||||
<view class="plan-name">{{item.name}}</view>
|
||||
<view class="plan-desc">无限存储空间,照片视频想存就存</view>
|
||||
</view>
|
||||
<view class="plan-price">
|
||||
<text class="plan-amount">¥{{item.priceText}}</text>
|
||||
<text class="plan-unit">/{{item.unit}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 说明 -->
|
||||
<view class="storage-note">
|
||||
<view class="note-title">📌 说明</view>
|
||||
<view class="note-item">• 默认提供 {{freeMb}}MB 免费空间</view>
|
||||
<view class="note-item">• 开通会员后,照片、视频等不再限制存储空间</view>
|
||||
<view class="note-item">• 会员到期后,已上传内容仍可正常查看</view>
|
||||
</view>
|
||||
|
||||
<page-state wx:if="{{error}}" kind="error" title="加载失败" action="重试" bind:action="loadInfo" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -0,0 +1,157 @@
|
||||
/* pages/storage/storage.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
/* 已用空间卡片 */
|
||||
.storage-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx 32rpx;
|
||||
margin: 16rpx 0 8rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(183, 71, 104, 0.08);
|
||||
border: 2rpx solid rgba(255, 107, 157, 0.10);
|
||||
}
|
||||
.storage-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.storage-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
.storage-vip {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #B74768;
|
||||
background: #FFE4EC;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.storage-used {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
.storage-num {
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
color: #B74768;
|
||||
line-height: 1;
|
||||
}
|
||||
.storage-of {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
.storage-bar {
|
||||
margin-top: 24rpx;
|
||||
height: 16rpx;
|
||||
background: #F5E9ED;
|
||||
border-radius: 999rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.storage-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #FF9EB5, #B74768);
|
||||
border-radius: 999rpx;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
.storage-hint {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.storage-hint.over {
|
||||
color: #FF4757;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 套餐卡片 */
|
||||
.plan-list {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.plan-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
padding: 30rpx 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 6rpx 24rpx rgba(183, 71, 104, 0.06);
|
||||
border: 2rpx solid rgba(255, 107, 157, 0.10);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.plan-card:active {
|
||||
transform: scale(0.98);
|
||||
border-color: rgba(183, 71, 104, 0.35);
|
||||
background: #FFF8FA;
|
||||
}
|
||||
.plan-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.plan-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
.plan-desc {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
.plan-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.plan-amount {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #B74768;
|
||||
}
|
||||
.plan-unit {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
/* 说明 */
|
||||
.storage-note {
|
||||
background: #FFF9F4;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
margin-top: 12rpx;
|
||||
border: 2rpx solid rgba(255, 107, 157, 0.12);
|
||||
}
|
||||
.note-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #B74768;
|
||||
display: block;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
.note-item {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-family: 'Kaiti SC', 'STKaiti', 'KaiTi', cursive;
|
||||
font-weight: 400;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
@import '../../styles/button-theme.wxss';
|
||||
@import '../../styles/typography.wxss';
|
||||
Reference in New Issue
Block a user