feat: 存储空间管理、虚拟支付接入与容量守卫
- 新增存储空间管理:会员套餐配置、用户容量/到期管理、后台支付记录查询 - 接入个人主体小程序虚拟支付:下单/发货推送/兜底查单(query_order) - 修复 adminStorageUsers 等接口鉴权、out_trade_no 长度、前端 requestVirtualPayment 参数 - 前端上传前容量预检,超容引导升级无限空间 - 我的页会员卡片展示「无限空间」与已用容量/到期时间 - 后台「存储空间管理」新增支付记录标签页
This commit is contained in:
@@ -5,12 +5,21 @@ const { getLayout } = require('../../utils/layout');
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||
const { requestSubscribe } = require('../../utils/subscribe');
|
||||
const { guardStorageSpace } = require('../../utils/storage-guard');
|
||||
|
||||
function formatStorageBytes(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(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
scrollTop: 0,
|
||||
statsLoading:false, statsError:false,
|
||||
storageText:'—', storagePercent:0, storageOverQuota:false, storageVip:false, storageExpireText:'',
|
||||
statisticItems:[
|
||||
{key:'timeline',label:'记忆日',icon:'stat-timeline',count:'—',color:'pink'},
|
||||
{key:'album',label:'相册',icon:'stat-album',count:'—',color:'yellow'},
|
||||
@@ -191,6 +200,7 @@ Page(withDataCache({
|
||||
}
|
||||
this.loadData();
|
||||
this.loadProfileStats();
|
||||
this.loadStorageInfo();
|
||||
if (!cid && this.data.showInvitePanel && !this._pollTimer) this.startPollCoupleStatus();
|
||||
// 如果已绑定情侣,关闭邀请面板
|
||||
if (cid && this.data.showInvitePanel) {
|
||||
@@ -214,7 +224,7 @@ Page(withDataCache({
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await Promise.all([this.loadData(),this.loadProfileStats()]);
|
||||
await Promise.all([this.loadData(),this.loadProfileStats(),this.loadStorageInfo()]);
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
@@ -486,25 +496,29 @@ Page(withDataCache({
|
||||
},
|
||||
|
||||
chooseAvatar: function() {
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const filePath = res.tempFiles[0].path;
|
||||
this.setData({ editAvatarUrl: filePath });
|
||||
// 上传头像
|
||||
try {
|
||||
const data = await api.uploadAvatar(filePath);
|
||||
this.setData({
|
||||
editAvatarUrl: getImageUrl(data.path),
|
||||
editAvatarPath: data.path
|
||||
});
|
||||
wx.showToast({ title: '头像上传成功 ✨', icon: 'success' });
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '上传失败', icon: 'none' });
|
||||
const self = this;
|
||||
guardStorageSpace(0).then(function(ok) {
|
||||
if (!ok) return;
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const filePath = res.tempFiles[0].path;
|
||||
self.setData({ editAvatarUrl: filePath });
|
||||
// 上传头像
|
||||
try {
|
||||
const data = await api.uploadAvatar(filePath);
|
||||
self.setData({
|
||||
editAvatarUrl: getImageUrl(data.path),
|
||||
editAvatarPath: data.path
|
||||
});
|
||||
wx.showToast({ title: '头像上传成功 ✨', icon: 'success' });
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '上传失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -640,16 +654,20 @@ Page(withDataCache({
|
||||
},
|
||||
choosePartnerAvatar: function() {
|
||||
if (this.data.partnerSaving || this.data.partnerAvatarUploading || !this._editPartnerId) return;
|
||||
const self = this;
|
||||
const partnerId = this._editPartnerId;
|
||||
wx.chooseImage({count:1,sizeType:['compressed'],sourceType:['album','camera'],success:async res=>{
|
||||
if (!this.data.showEditPartnerModal || partnerId !== this._editPartnerId) return;
|
||||
this.setData({partnerAvatarUploading:true});
|
||||
try {
|
||||
const uploaded = await api.uploadPartnerAvatar(res.tempFilePaths[0],partnerId);
|
||||
this.setData({editPartnerAvatarUrl:getImageUrl(uploaded.path),editPartnerAvatarPath:uploaded.path});
|
||||
} catch(error) { wx.showToast({title:error.error || '头像上传失败',icon:'none'}); }
|
||||
finally { this.setData({partnerAvatarUploading:false}); }
|
||||
}});
|
||||
guardStorageSpace(0).then(function(ok) {
|
||||
if (!ok) return;
|
||||
wx.chooseImage({count:1,sizeType:['compressed'],sourceType:['album','camera'],success:async res=>{
|
||||
if (!self.data.showEditPartnerModal || partnerId !== self._editPartnerId) return;
|
||||
self.setData({partnerAvatarUploading:true});
|
||||
try {
|
||||
const uploaded = await api.uploadPartnerAvatar(res.tempFilePaths[0],partnerId);
|
||||
self.setData({editPartnerAvatarUrl:getImageUrl(uploaded.path),editPartnerAvatarPath:uploaded.path});
|
||||
} catch(error) { wx.showToast({title:error.error || '头像上传失败',icon:'none'}); }
|
||||
finally { self.setData({partnerAvatarUploading:false}); }
|
||||
}});
|
||||
});
|
||||
},
|
||||
savePartnerProfile: async function() {
|
||||
const d = this.data;
|
||||
@@ -1014,6 +1032,44 @@ Page(withDataCache({
|
||||
this.setData({ showShareBindModal: false });
|
||||
},
|
||||
|
||||
loadStorageInfo: async function() {
|
||||
try {
|
||||
const d = await api.storageInfo();
|
||||
const freeBytes = d.free_bytes || d.freeBytes || 0;
|
||||
const usedBytes = d.used_bytes || d.usedBytes || 0;
|
||||
const isVip = !!(d.is_vip || d.isVip);
|
||||
const usedText = formatStorageBytes(usedBytes);
|
||||
if (isVip) {
|
||||
// 会员:展示「无限空间」+ 已用容量 + 到期时间,不再提示空间已满。
|
||||
const expire = d.vip_expire_at || d.vipExpireAt;
|
||||
this.setData({
|
||||
storageVip: true,
|
||||
storageText: '无限空间',
|
||||
storagePercent: 0,
|
||||
storageOverQuota: false,
|
||||
storageUsedText: usedText,
|
||||
storageExpireText: expire ? ('有效期至 ' + String(expire).slice(0, 10)) : '已开通无限空间'
|
||||
});
|
||||
} else {
|
||||
const percent = freeBytes > 0 ? Math.min(100, Math.round(usedBytes / freeBytes * 100)) : 0;
|
||||
this.setData({
|
||||
storageVip: false,
|
||||
storageText: usedText,
|
||||
storagePercent: percent,
|
||||
storageOverQuota: usedBytes > freeBytes,
|
||||
storageUsedText: usedText,
|
||||
storageExpireText: ''
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载存储信息失败', e);
|
||||
}
|
||||
},
|
||||
goStorage: function() {
|
||||
this._savePosBeforeLeave();
|
||||
wx.navigateTo({ url: '/pages/storage/storage' });
|
||||
},
|
||||
|
||||
loadProfileStats: async function() {
|
||||
this.setData({statsLoading:true,statsError:false});
|
||||
const keys=['timeline','album','diary','songs','checkin','pet'];
|
||||
@@ -1046,4 +1102,4 @@ Page(withDataCache({
|
||||
manageRelationship: function() { wx.navigateTo({ url: '/pages/unbind/unbind' }); },
|
||||
goToSettings: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/settings/settings' }); },
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); }
|
||||
}, {"loadData": ["coupleInfo"], "loadProfileStats": ["timelineList","albumList","diaryList","songsList","checkinList","petInfo"]}));
|
||||
}, {"loadData": ["coupleInfo"], "loadProfileStats": ["timelineList","albumList","diaryList","songsList","checkinList","petInfo"], "loadStorageInfo": ["storageInfo"]}));
|
||||
|
||||
@@ -10,6 +10,27 @@
|
||||
<view wx:if="{{bindingKnown && hasPartner}}" class="j-relation-card" bindtap="showEditPartnerModal"><view class="j-row"><view><text class="j-small-title">{{partnerName}}</text><text class="j-caption" wx:if="{{partnerDisplayId}}">ID {{partnerDisplayId}}</text><text class="j-caption">{{partnerDetail}}</text></view><view class="j-avatar"><image wx:if="{{partnerAvatarUrl}}" src="{{partnerAvatarUrl}}" mode="aspectFill" /><text wx:else>{{partnerInitial}}</text></view></view></view>
|
||||
<view wx:elif="{{bindingKnown}}" class="j-invite" bindtap="showInvitePanel"><view><text class="j-small-title">另一半还没有加入</text><text class="j-caption">邀请TA,共写这本生活手账</text></view><text class="j-outline-label">绑定情侣</text></view>
|
||||
<page-state wx:elif="{{bindingError}}" kind="error" title="绑定状态加载失败" action="重新加载" bind:action="loadData" /><page-state wx:else kind="loading" title="正在获取绑定状态" />
|
||||
<view wx:if="{{storageVip}}" class="j-storage-card j-storage-vip" bindtap="goStorage">
|
||||
<view class="j-storage-top">
|
||||
<text class="j-storage-label">存储空间</text>
|
||||
<text class="j-storage-val j-vip">👑 无限空间</text>
|
||||
</view>
|
||||
<view class="j-storage-bottom">
|
||||
<text class="j-storage-hint">已用 {{storageUsedText}}{{storageExpireText ? ' · ' + storageExpireText : ''}}</text>
|
||||
<text class="j-storage-cta">查看 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="j-storage-card" bindtap="goStorage">
|
||||
<view class="j-storage-top">
|
||||
<text class="j-storage-label">存储空间</text>
|
||||
<text class="j-storage-val">{{storageText}}</text>
|
||||
</view>
|
||||
<view class="j-storage-bar"><view class="j-storage-fill" style="width:{{storagePercent}}%;"></view></view>
|
||||
<view class="j-storage-bottom">
|
||||
<text class="j-storage-hint">{{storageOverQuota ? '已超出免费空间,升级后可继续上传' : '已用 ' + storagePercent + '% 免费空间'}}</text>
|
||||
<text class="j-storage-cta">{{storageOverQuota ? '升级无限空间 ›' : '查看 ›'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-statistics">
|
||||
<view class="profile-stat-grid">
|
||||
<view class="profile-stat-item" wx:for="{{statisticItems}}" wx:key="key" data-key="{{item.key}}" bindtap="openStatistic" hover-class="profile-stat-pressed">
|
||||
|
||||
@@ -324,6 +324,24 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
.profile-stat-pressed { opacity:.65; }
|
||||
.profile-stats-retry { text-align:center; color:#B74768; font-size:22rpx; padding:24rpx 0 0; }
|
||||
|
||||
/* 存储空间入口卡片。 */
|
||||
.j-storage-card { margin-top:32rpx; padding:26rpx 30rpx; background:#FFFDF8; border:1rpx solid #F1DCE4; border-radius:32rpx; }
|
||||
.j-storage-top { display:flex; justify-content:space-between; align-items:baseline; }
|
||||
.j-storage-label { font-family:'Kaiti SC','STKaiti','KaiTi',cursive; font-size:28rpx; color:#64424D; }
|
||||
.j-storage-val { font-size:26rpx; font-weight:600; color:#B74768; }
|
||||
.j-storage-bar { margin-top:18rpx; height:16rpx; background:#F5E9ED; border-radius:999rpx; overflow:hidden; }
|
||||
.j-storage-fill { height:100%; background:linear-gradient(90deg,#FF9EB5,#B74768); border-radius:999rpx; transition:width .4s ease; }
|
||||
.j-storage-bottom { margin-top:14rpx; display:flex; justify-content:space-between; align-items:center; }
|
||||
.j-storage-hint { font-size:22rpx; color:#A39386; }
|
||||
.j-storage-cta { font-size:24rpx; color:#B74768; font-weight:500; }
|
||||
|
||||
/* 会员(无限空间)卡片:玫红渐变,突出「无限空间」与到期时间。 */
|
||||
.j-storage-vip { background:linear-gradient(135deg,#FF9EB5,#B74768); border:1rpx solid rgba(255,255,255,.4); border-radius:32rpx; }
|
||||
.j-storage-vip .j-storage-label { color:#FFF; }
|
||||
.j-storage-vip .j-storage-val.j-vip { color:#FFF; font-size:28rpx; }
|
||||
.j-storage-vip .j-storage-hint { color:rgba(255,255,255,.9); }
|
||||
.j-storage-vip .j-storage-cta { color:#FFF; }
|
||||
|
||||
/* 本人与对方资料卡共用完整样式,整卡点击编辑。 */
|
||||
.j-body > .j-profile-summary,
|
||||
.j-body > .j-relation-card {
|
||||
|
||||
Reference in New Issue
Block a user