Files
past-go/weapp/pages/profile/profile.js
T
CodeBuddy bf99e52937 feat: 存储空间管理、虚拟支付接入与容量守卫
- 新增存储空间管理:会员套餐配置、用户容量/到期管理、后台支付记录查询
- 接入个人主体小程序虚拟支付:下单/发货推送/兜底查单(query_order)
- 修复 adminStorageUsers 等接口鉴权、out_trade_no 长度、前端 requestVirtualPayment 参数
- 前端上传前容量预检,超容引导升级无限空间
- 我的页会员卡片展示「无限空间」与已用容量/到期时间
- 后台「存储空间管理」新增支付记录标签页
2026-10-30 23:45:39 +08:00

1106 lines
40 KiB
JavaScript

const { itemsOf, totalOf } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout');
// pages/profile/profile.js - 与原网站一致的个人中心
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'},
{key:'diary',label:'日记',icon:'stat-book',count:'—',color:'mint'},
{key:'songs',label:'歌曲',icon:'stat-music',count:'—',color:'blue'},
{key:'checkin',label:'打卡',icon:'stat-check',count:'—',color:'purple'},
{key:'pet',label:'宠物',icon:'stat-pet',count:'—',color:'pink'},
{key:'period',label:'姨姨',icon:'stat-j-flower',count:'',color:'pink'},
{key:'settings',label:'更多',icon:'stat-settings',count:'',color:'gray'}
],
// 我的信息
myNickname: '未登录',
myDisplayId: '',
myPhone: '-',
myGender: '-',
myAge: '-',
myBirthday: '-',
myRegion: '',
myLocation: '',
myStatus: '设置恋爱信息',
myAvatarUrl: '',
myInitial: '?',
// TA的信息
hasPartner: false,
bindingKnown: false, bindingError: false,
partnerName: 'TA',
partnerDisplayId: '',
partnerId: 0,
partnerDetail: '',
partnerAvatarUrl: '',
partnerInitial: '?',
partnerBirthday: '',
partnerGenderRaw: '',
// 恋爱信息
anniversaryDateText: '点击设置你们的纪念日',
loveStoryText: '记录你们的故事',
loveStartDate: '',
// 编辑资料弹窗
showEditProfileModal: false,
editNickname: '',
editGender: 'male',
editGenderText: '男',
editAge: '',
editBirthday: '',
editRegion: '',
editAvatarUrl: '',
editAvatarPath: '',
editNicknameInitial: '?',
// 设置恋爱日期弹窗
showSetDateModal: false,
tempLoveDate: '',
tempLoveStory: '',
// 编辑TA资料弹窗
showEditPartnerModal: false,
partnerSaving: false, partnerAvatarUploading: false,
editPartnerNickname: '',
editPartnerGender: 'male',
editPartnerGenderText: '男',
editPartnerAge: '',
editPartnerBirthday: '',
editPartnerAvatarUrl: '',
editPartnerAvatarPath: '',
editPartnerNameInitial: '?',
genderOptions: ['男', '女'],
// 解除绑定验证
showUnbindVerifyModal: false,
unbindQuestion: '',
unbindAnswer: 0,
unbindUserInput: '',
// 解绑状态
unbindStatus: '',
unbindIsRequester: false,
unbindRequesterName: '',
unbindRemaining: 0,
// TA详情弹窗
showPartnerDetailModal: false,
partnerDetailNickname: '-',
partnerDetailGender: '-',
partnerDetailAge: '-',
partnerDetailBirthday: '-',
partnerDetailAvatarUrl: '',
partnerNameInitial: '?',
// ===== 邀请绑定相关 =====
showInvitePanel: false,
inviteTab: 'received',
receivedInvites: [],
sentInvites: [],
inviteTargetId: '',
inviteMessage: '',
// 分享绑定
showShareBindModal: false,
shareInviterId: '',
shareInviterName: '',
shareDisagree: false
},
// 左滑返回机制
touchStartX: 0, touchStartY: 0,
onTouchStart: function(e) {
this.touchStartX = e.touches[0].clientX;
this.touchStartY = e.touches[0].clientY;
},
onTouchEnd: function(e) {
var dx = this.touchStartX - e.changedTouches[0].clientX;
var dy = Math.abs(this.touchStartY - e.changedTouches[0].clientY);
// 短距离/垂直滑动不处理(交给按钮tap事件)
if (dx < 20 && dy < 20) return;
// 只有水平且左滑超过80px才处理
if (Math.abs(dx) > dy && dx > 80) {
if (this.data.showEditProfileModal) {
this.hideEditProfileModal();
} else if (this.data.showEditPartnerModal) {
this.hideEditPartnerModal();
} else {
wx.navigateBack();
}
}
},
_needRefresh: true,
onScroll: function(e) {
this._scrollTop = e.detail.scrollTop;
saveScrollPosition('profile', e.detail.scrollTop);
},
onLoad: function(options) {
this.setData(getLayout());
this._needRefresh = true;
this._openBindOnLoad = !!(options && options.bind);
this.setData({
userId: wx.getStorageSync('userId'),
coupleId: wx.getStorageSync('coupleId'),
scrollTop: 0
});
this.loadData();
this.loadUnbindStatus();
this.fetchAndSaveLocation();
// 从首页点击头像跳转过来,自动打开编辑
if (options.edit === 'self') {
var self = this;
setTimeout(function() { self.showEditProfileModal(); }, 500);
} else if (options.edit === 'partner') {
var self = this;
setTimeout(function() { self.showEditPartnerModal(); }, 500);
}
// 从分享邀请链接进入
if (options && options.inviter) {
var self = this;
setTimeout(function() { self.handleShareInvite(options.inviter, options.invite_code); }, 800);
}
},
onShow: function() {
resetScrollOnTabShow(this, 'profile');
if (!wx.getStorageSync('coupleId') && !this.data.shareOutgoingCode) api.generateInviteLink().then(res => this.setData({shareOutgoingCode:res.code || ''})).catch(() => {});
const tabBar = this.getTabBar && this.getTabBar();
if (tabBar) tabBar.syncActive();
var uid = wx.getStorageSync('userId');
var cid = wx.getStorageSync('coupleId');
this.setData({ userId: uid, coupleId: cid });
const app = getApp();
if (app.globalData.openProfileBind) {
app.globalData.openProfileBind = false;
this._openBindOnLoad = true;
this._dataLoads = {};
}
this.loadData();
this.loadProfileStats();
this.loadStorageInfo();
if (!cid && this.data.showInvitePanel && !this._pollTimer) this.startPollCoupleStatus();
// 如果已绑定情侣,关闭邀请面板
if (cid && this.data.showInvitePanel) {
this.setData({ showInvitePanel: false });
if (this._pollTimer) {
clearInterval(this._pollTimer);
this._pollTimer = null;
}
}
},
onHide: function() {
if (this._pollTimer) { clearInterval(this._pollTimer); this._pollTimer = null; }
},
onUnload: function() {
if (this._pollTimer) {
clearInterval(this._pollTimer);
this._pollTimer = null;
}
},
onPullDownRefresh: async function() {
await Promise.all([this.loadData(),this.loadProfileStats(),this.loadStorageInfo()]);
wx.stopPullDownRefresh();
},
// 比较新旧值,只返回变化的字段(避免全量 setData 导致闪屏)
_diffUpdate: function(newData) {
var updates = {};
for (var key in newData) {
if (this.data[key] !== newData[key]) {
updates[key] = newData[key];
}
}
if (Object.keys(updates).length > 0) {
this.setData(updates);
}
},
loadData: async function() {
this.setData({bindingError:false});
var requestId = (this._coupleInfoRequestId || 0) + 1;
this._coupleInfoRequestId = requestId;
try {
// 获取情侣信息
const info = await api.coupleInfo();
this.setData({userId: wx.getStorageSync('userId')});
if (requestId !== this._coupleInfoRequestId) return;
// 未绑定情侣时从后端获取用户最新资料
if (info.not_bound) {
wx.removeStorageSync('coupleId');
var myInfo = info.partner1 || {};
var genderText = myInfo.gender === 'male' ? '男' : (myInfo.gender === 'female' ? '女' : '-');
var region = myInfo.region || '';
var location = myInfo.location || '';
if (/^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/.test(location)) location = '';
wx.setStorageSync('region', region);
this._diffUpdate({
bindingKnown: true,
coupleId: null,
hasPartner: false,
partnerName: 'TA',
partnerDisplayId: '',
partnerId: 0,
partnerDetail: '',
partnerAvatarUrl: '',
partnerInitial: '?',
partnerBirthday: '',
partnerGenderRaw: '',
myStatus: '设置恋爱信息',
anniversaryDateText: '点击设置你们的纪念日',
loveStoryText: '记录你们的故事',
loveStartDate: '',
tempLoveDate: '',
tempLoveStory: '',
myNickname: myInfo.nickname || myInfo.username || '未登录',
myDisplayId: myInfo.user_id || wx.getStorageSync('displayId') || '',
myPhone: myInfo.phone || myInfo.username || '-',
myGender: genderText,
myGenderRaw: myInfo.gender || '',
myAge: myInfo.age > 0 ? myInfo.age + '岁' : '-',
myAgeText: myInfo.age > 0 ? myInfo.age : '',
myBirthday: myInfo.birthday || '-',
myBirthdayText: myInfo.birthday || '',
myRegion: region,
myLocation: location,
myAvatarUrl: getImageUrl(myInfo.avatar),
myInitial: (myInfo.nickname || myInfo.username || '?')[0],
editNickname: myInfo.nickname || '',
editAge: myInfo.age > 0 ? myInfo.age : '',
editBirthday: myInfo.birthday || '',
editRegion: region,
editNicknameInitial: (myInfo.nickname || myInfo.username || '?')[0]
});
if (this._openBindOnLoad) {
this._openBindOnLoad = false;
this.showInvitePanel();
}
return;
}
// 同步情侣ID到本地存储
if (info.id) {
wx.setStorageSync('coupleId', info.id);
if (this.data.coupleId !== info.id) {
this.setData({ coupleId: info.id });
}
}
const myUid = this.data.userId;
const p1 = info.partner1 || {};
const p2 = info.partner2 || {};
const me = p1.id == myUid ? p1 : (p2.id == myUid ? p2 : null);
const partner = p1.id == myUid ? p2 : (p2.id == myUid ? p1 : null);
// 我的信息 — 只更新变化的字段
if (me) {
const genderText = me.gender === 'male' ? '男' : (me.gender === 'female' ? '女' : '-');
const storedDisplayId = wx.getStorageSync('displayId') || '';
var region = me.region || '';
var location = me.location || '';
if (/^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/.test(location)) location = '';
wx.setStorageSync('region', region);
this._diffUpdate({
myNickname: me.nickname || me.username || '未登录',
myDisplayId: me.user_id || storedDisplayId,
myPhone: me.phone || me.username || '-',
myGender: genderText,
myGenderRaw: me.gender || '',
myAge: me.age > 0 ? me.age + '岁' : '-',
myAgeText: me.age > 0 ? me.age : '',
myBirthday: me.birthday || '-',
myBirthdayText: me.birthday || '',
myRegion: region,
myLocation: location,
myAvatarUrl: getImageUrl(me.avatar),
myInitial: (me.nickname || me.username || '?')[0],
editNickname: me.nickname || '',
editAge: me.age > 0 ? me.age : '',
editBirthday: me.birthday || '',
editRegion: region,
editNicknameInitial: (me.nickname || me.username || '?')[0]
});
}
// TA的信息 — 只更新变化的字段
if (partner) {
const genderText = partner.gender === 'male' ? '男' : (partner.gender === 'female' ? '女' : '-');
let details = [];
if (genderText !== '-') details.push(genderText);
if (partner.age > 0) details.push(partner.age + '岁');
if (partner.birthday) details.push('🎂 ' + partner.birthday);
this._diffUpdate({
bindingKnown: true,
hasPartner: true,
partnerName: partner.nickname || partner.username || 'TA',
partnerDisplayId: partner.user_id || '',
partnerId: Number(partner.id),
partnerDetail: details.join(' · ') || 'TA的信息',
partnerAvatarUrl: getImageUrl(partner.avatar),
partnerInitial: (partner.nickname || partner.username || '?')[0],
partnerBirthday: partner.birthday || '',
partnerGenderRaw: partner.gender || '',
partnerDetailNickname: partner.nickname || partner.username || '-',
partnerDetailGender: genderText,
partnerDetailAge: partner.age > 0 ? partner.age + '岁' : '-',
partnerDetailBirthday: partner.birthday || '-',
partnerDetailAvatarUrl: getImageUrl(partner.avatar),
partnerNameInitial: (partner.nickname || partner.username || '?')[0]
});
}
// 恋爱天数和状态 — 只更新变化的字段
let daysCount = 0;
if (info.love_start_date) {
const start = new Date(info.love_start_date);
daysCount = Math.floor((Date.now() - start) / 86400000);
const partnerName = partner ? (partner.nickname || partner.username || 'TA') : 'TA';
this._diffUpdate({
anniversaryDateText: info.love_start_date + ' (已设置)',
loveStoryText: info.love_story ? (info.love_story.length > 20 ? info.love_story.substring(0, 20) + '...' : info.love_story) : '记录你们的故事',
myStatus: '和' + partnerName + '在一起 ' + daysCount + ' 天 💕',
loveStartDate: info.love_start_date,
tempLoveDate: info.love_start_date || '',
tempLoveStory: info.love_story || ''
});
} else {
this._diffUpdate({
anniversaryDateText: '点击设置你们的纪念日',
loveStoryText: info.love_story ? (info.love_story.length > 20 ? info.love_story.substring(0, 20) + '...' : info.love_story) : '记录你们的故事',
myStatus: '设置恋爱信息',
loveStartDate: '',
tempLoveDate: '',
tempLoveStory: ''
});
}
} catch (e) {
if (requestId !== this._coupleInfoRequestId) return;
console.error(e);
this.setData({ bindingKnown: false, bindingError:true });
}
},
// 获取定位并保存到数据库(使用模糊定位,getLocation 与 getFuzzyLocation 互斥,统一用模糊定位)
fetchAndSaveLocation: function() {
var self = this;
self._fuzzyLocation();
},
// 模糊定位兜底
_fuzzyLocation: function() {
var self = this;
if (!wx.getFuzzyLocation) return;
wx.getFuzzyLocation({
type: 'gcj02',
success: function(res) {
self._reportLocation(res.latitude, res.longitude);
},
fail: function() {}
});
},
// 上报经纬度到后端并更新城市地址显示
_reportLocation: function(lat, lng) {
var self = this;
if (!lat || !lng) return;
// 保留更多小数位,减少坐标截断误差
var location = lat.toFixed(6) + ',' + lng.toFixed(6);
api.updateLocation(location).then(function(resp) {
// 后端返回的地址;若为空或经纬度(说明逆地理编码失败),一律不采用,绝不显示经纬度
var city = (resp && resp.location) ? resp.location : '';
if (city === '' || /^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/.test(city)) {
return;
}
// 转换成功后展示接口返回的城市地址
if (self.data.myLocation !== city) {
self.setData({ myLocation: city });
}
}).catch(function() {});
},
_syncProfileTabBar: function() {
const tabBar = this.getTabBar && this.getTabBar();
if (tabBar && tabBar.setHidden) tabBar.setHidden(this.data.showEditProfileModal || this.data.showEditPartnerModal);
},
// 编辑资料
showEditProfileModal: function() {
var d = this.data;
var rawGender = d.myGenderRaw || '';
var genderText = rawGender === 'female' ? '女' : '男';
this.setData({
showEditProfileModal: true,
editNickname: d.myNickname === '未设置' ? '' : d.myNickname,
editGender: rawGender || 'male',
editGenderText: genderText,
editAge: d.myAgeText ? String(parseInt(d.myAgeText) || '') : '',
editBirthday: d.myBirthdayText || '',
editRegion: d.myRegion || '',
editAvatarUrl: d.myAvatarUrl || '',
editNicknameInitial: d.myInitial || '?'
});
this._syncProfileTabBar();
},
onEditGenderChange: function(e) {
var idx = e.detail.value;
var texts = ['男', '女'];
var vals = ['male', 'female'];
this.setData({ editGender: vals[idx], editGenderText: texts[idx] });
},
hideEditProfileModal: function() {
this.setData({ showEditProfileModal: false });
this._syncProfileTabBar();
},
onEditNicknameInput: function(e) {
this.setData({ editNickname: e.detail.value });
},
onEditBirthdayInput: function(e) {
var birthday = e.detail.value;
var age = '';
if (birthday) {
var birthDate = new Date(birthday);
var today = new Date();
var calcAge = today.getFullYear() - birthDate.getFullYear();
var m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
calcAge--;
}
age = calcAge > 0 ? calcAge : 1;
}
this.setData({ editBirthday: birthday, editAge: String(age) });
},
chooseAvatar: function() {
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' });
}
}
});
});
},
saveProfile: async function() {
const { editNickname, editGender, editAge, editBirthday, editRegion, editAvatarPath, tempLoveDate, tempLoveStory, coupleId } = this.data;
if (!editNickname) {
wx.showToast({ title: '请输入昵称', icon: 'none' }); return;
}
try {
await api.updateProfile({
nickname: editNickname,
gender: editGender,
age: parseInt(editAge) || 0,
birthday: editBirthday,
region: editRegion,
avatar: editAvatarPath
});
// 更新本地存储的昵称,确保未绑定情侣时也能正确显示
wx.setStorageSync('nickname', editNickname);
if (editAvatarPath) {
wx.setStorageSync('avatar', editAvatarPath);
}
// 如果有恋爱日期且已绑定情侣,也保存
if (tempLoveDate && coupleId) {
await api.updateCoupleInfo({ love_start_date: tempLoveDate, love_story: tempLoveStory || '' });
}
wx.showToast({ title: '资料更新成功 💕', icon: 'success' });
this.hideEditProfileModal();
this.loadData();
} catch (e) {
wx.showToast({ title: e.error || '保存失败', icon: 'none' });
}
},
// 设置恋爱日期
showSetDateModal: function() {
this.setData({ showSetDateModal: true });
},
hideSetDateModal: function() {
this.setData({ showSetDateModal: false });
},
onLoveDateInput: function(e) {
this.setData({ tempLoveDate: e.detail.value });
},
onLoveStoryInput: function(e) {
this.setData({ tempLoveStory: e.detail.value });
},
// 显示恋爱故事
showCoupleStory: function() {
const { tempLoveStory } = this.data;
if (!tempLoveStory) {
wx.showToast({ title: '还没有恋爱故事,先设置纪念日吧 ✨', icon: 'none' });
this.showSetDateModal();
} else {
wx.showModal({
title: '💕 恋爱故事',
content: tempLoveStory,
showCancel: false
});
}
},
saveLoveDate: async function() {
const { tempLoveDate, tempLoveStory } = this.data;
if (!tempLoveDate) {
wx.showToast({ title: '请选择日期', icon: 'none' }); return;
}
try {
await api.updateCoupleInfo({
love_start_date: tempLoveDate,
love_story: tempLoveStory
});
wx.showToast({ title: '纪念日设置成功 💕', icon: 'success' });
this.hideSetDateModal();
this.loadData();
} catch (e) {
wx.showToast({ title: e.error || '保存失败', icon: 'none' });
}
},
// TA详情
showPartnerDetail: function() {
this.setData({ showPartnerDetailModal: true });
},
hidePartnerDetailModal: function() {
this.setData({ showPartnerDetailModal: false });
},
// 编辑TA资料
showEditPartnerModal: function() {
const d = this.data;
this._editPartnerId = d.partnerId;
this._partnerOriginalStory = d.tempLoveStory;
var rawGender = d.partnerGenderRaw || '';
var genderText = rawGender === 'male' ? '男' : (rawGender === 'female' ? '女' : '未设置');
// 从 partnerDetail 中解析年龄(格式:XX岁)
var ageMatch = d.partnerDetail.match(/(\d+)岁/);
var partnerAge = ageMatch ? ageMatch[1] : '';
this.setData({
showEditPartnerModal: true,
editPartnerNickname: d.partnerName === 'TA' ? '' : d.partnerName,
editPartnerGender: rawGender || '',
editPartnerGenderText: genderText,
editPartnerAge: partnerAge,
editPartnerBirthday: d.partnerBirthday || '',
editPartnerAvatarUrl: d.partnerAvatarUrl || '',
editPartnerAvatarPath: '',
editPartnerNameInitial: d.partnerInitial || '?'
});
this._syncProfileTabBar();
},
hideEditPartnerModal: function() {
if (this.data.partnerSaving || this.data.partnerAvatarUploading) return;
this.setData({ showEditPartnerModal: false });
this._syncProfileTabBar();
},
onEditPartnerNicknameInput: function(e) {
if (!this.data.partnerSaving) this.setData({editPartnerNickname:e.detail.value});
},
onEditPartnerGenderChange: function(e) {
if (this.data.partnerSaving) return;
const index = Number(e.detail.value);
this.setData({editPartnerGender:['male','female'][index],editPartnerGenderText:['男','女'][index]});
},
onEditPartnerBirthdayInput: function(e) {
if (this.data.partnerSaving) return;
const birthday = e.detail.value;
const birth = new Date(birthday), today = new Date();
let age = today.getFullYear() - birth.getFullYear();
if (today.getMonth() < birth.getMonth() || (today.getMonth() === birth.getMonth() && today.getDate() < birth.getDate())) age--;
this.setData({editPartnerBirthday:birthday,editPartnerAge:String(Math.max(0,age))});
},
choosePartnerAvatar: function() {
if (this.data.partnerSaving || this.data.partnerAvatarUploading || !this._editPartnerId) return;
const self = this;
const partnerId = this._editPartnerId;
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;
if (d.partnerSaving || d.partnerAvatarUploading) return;
if (!d.hasPartner || !this._editPartnerId || this._editPartnerId !== d.partnerId) {
wx.showToast({title:'绑定信息已变化,请重新打开资料',icon:'none'});return;
}
const nickname = d.editPartnerNickname.trim();
if (!nickname) { wx.showToast({title:'请输入TA的昵称',icon:'none'});return; }
this.setData({partnerSaving:true});
try {
const result = await api.updatePartnerProfile({partner_id:this._editPartnerId,nickname,gender:d.editPartnerGender,age:parseInt(d.editPartnerAge) || 0,birthday:d.editPartnerBirthday,avatar:d.editPartnerAvatarPath || undefined});
if (!result || !result.success) throw result || {};
if (d.coupleId && (d.tempLoveDate !== d.loveStartDate || d.tempLoveStory !== this._partnerOriginalStory)) {
await api.updateCoupleInfo({love_start_date:d.tempLoveDate,love_story:d.tempLoveStory || ''});
}
this.setData({partnerSaving:false});
this.hideEditPartnerModal();
await this.loadData();
wx.showToast({title:'TA的资料已保存',icon:'success'});
} catch(error) { wx.showToast({title:error.error || '保存失败,请重试',icon:'none'}); }
finally { this.setData({partnerSaving:false}); }
},
// 加载解绑状态
loadUnbindStatus: function() {
var self = this;
api.unbindStatus().then(function(res) {
if (res && res.status && res.status !== 'none') {
self.setData({
unbindStatus: res.status,
unbindIsRequester: !!res.is_requester,
unbindRequesterName: res.requester_name || '对方',
unbindRemaining: res.remaining_seconds || 0
});
} else {
self.setData({ unbindStatus: '', unbindIsRequester: false, unbindRequesterName: '' });
}
}).catch(function() {});
},
// 解除绑定 — 根据状态显示不同操作
showUnbindVerify: function() {
var status = this.data.unbindStatus;
var isRequester = this.data.unbindIsRequester;
if (status === 'pending') {
if (isRequester) {
this.doCancelUnbind();
} else {
this.doRespondUnbind();
}
} else if (status === 'cooling') {
wx.showToast({ title: '正在72小时冷静期', icon: 'none' });
wx.navigateTo({ url: '/pages/unbind/unbind' });
} else {
this.doRequestUnbind();
}
},
doRequestUnbind: function() {
var self = this;
wx.showModal({
title: '💔 解除绑定',
content: '对方同意后进入72小时冷静期,期间任一方可取消。到期自动解绑并永久删除共同数据;个人抽烟记录保留。',
cancelText: '取消',
confirmText: '发起解绑',
confirmColor: '#FF4757',
success: function(res) {
if (res.confirm) {
wx.showLoading({ title: '发送请求...' });
api.unbindRequest().then(function() {
wx.hideLoading();
wx.showToast({ title: '解绑请求已发送', icon: 'success' });
self.loadUnbindStatus();
wx.navigateTo({ url: '/pages/unbind/unbind' });
}).catch(function(err) {
wx.hideLoading();
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
});
}
}
});
},
doCancelUnbind: function() {
var self = this;
wx.showModal({
title: '↩️ 撤销解绑',
content: '取消后关系继续保留。确定取消解绑吗?',
cancelText: '取消',
confirmText: '撤销请求',
confirmColor: '#FF6B9D',
success: function(res) {
if (res.confirm) {
wx.showLoading({ title: '撤销中...' });
api.unbindCancel().then(function() {
wx.hideLoading();
wx.showToast({ title: '已撤销解绑请求', icon: 'success' });
self.loadUnbindStatus();
}).catch(function(err) {
wx.hideLoading();
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
});
}
}
});
},
doRespondUnbind: function() {
var self = this;
var name = this.data.unbindRequesterName;
wx.showActionSheet({
itemList: ['✅ 同意解绑', '❌ 拒绝解绑'],
success: function(res) {
if (res.tapIndex === 0) {
wx.showModal({
title: '⚠️ 确认同意',
content: name + ' 发起了解绑请求。同意后进入72小时冷静期,期间可取消;到期自动解绑并永久删除共同数据,个人抽烟记录保留。',
cancelText: '取消',
confirmText: '同意解绑',
confirmColor: '#FF4757',
success: function(r) {
if (r.confirm) {
wx.showLoading({ title: '处理中...' });
api.unbindRespond({ agree: true }).then(function() {
wx.hideLoading();
wx.showToast({ title: '已进入72小时冷静期', icon: 'none' });
self.loadUnbindStatus();
}).catch(function(err) {
wx.hideLoading();
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
});
}
}
});
} else if (res.tapIndex === 1) {
wx.showModal({
title: '拒绝解绑',
content: '确定要拒绝 ' + name + ' 的解绑请求吗?',
cancelText: '取消',
confirmText: '拒绝',
confirmColor: '#FF6B9D',
success: function(r) {
if (r.confirm) {
wx.showLoading({ title: '处理中...' });
api.unbindRespond({ agree: false }).then(function() {
wx.hideLoading();
wx.showToast({ title: '已拒绝解绑请求', icon: 'success' });
self.loadUnbindStatus();
}).catch(function(err) {
wx.hideLoading();
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
});
}
}
});
}
}
});
},
// 空操作(用于 catchtap 阻止冒泡)
noop: function() {},
// 退出登录
logout: function() {
wx.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
api.logout().catch(() => {});
wx.clearStorageSync();
wx.reLaunch({ url: '/pages/index/index' });
}
}
});
},
// 跳转前保存当前滚动位置
_savePosBeforeLeave: function() {
saveScrollPosition('profile', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
},
// ===== 邀请绑定 =====
showInvitePanel: function() {
var displayId = wx.getStorageSync('displayId') || '';
this.setData({
showInvitePanel: true,
inviteTab: 'received',
inviteTargetId: '',
inviteMessage: '',
myDisplayId: displayId
});
this.loadInvites();
this.startPollCoupleStatus();
},
hideInvitePanel: function() {
this.setData({ showInvitePanel: false });
if (this._pollTimer) {
clearInterval(this._pollTimer);
this._pollTimer = null;
}
},
onInviteTargetIdInput: function(e) { this.setData({ inviteTargetId: e.detail.value }); },
onInviteMessageInput: function(e) { this.setData({ inviteMessage: e.detail.value }); },
// 轮询检查情侣关系是否已恢复
startPollCoupleStatus: function() {
var self = this;
if (self._pollTimer) clearInterval(self._pollTimer);
self._pollTimer = setInterval(function() {
api.myInvitations().then(function(data) {
if (data && data.couple_id) {
clearInterval(self._pollTimer);
self._pollTimer = null;
wx.setStorageSync('coupleId', data.couple_id);
wx.showToast({ title: '情侣关系已恢复 💕', icon: 'success' });
setTimeout(function() {
self.setData({ showInvitePanel: false });
self.loadData();
}, 1500);
}
}).catch(function() {});
}, 10000);
},
sendInvite: async function() {
const inviteTargetId = String(this.data.inviteTargetId || '').trim();
const { inviteMessage } = this.data;
if (!inviteTargetId) { wx.showToast({ title: '请输入对方ID', icon: 'none' }); return; }
if (!/^[0-9]{6}$/.test(inviteTargetId)) { wx.showToast({ title: 'ID为6位数字', icon: 'none' }); return; }
// 邀请成功且对方接受时,需要通知邀请人,提前申请一次性订阅授权
requestSubscribe('notify').catch(function() {});
try {
wx.showLoading({ title: '发送中...' });
const res = await api.invitePartner({ target_user_id: inviteTargetId, message: inviteMessage });
wx.hideLoading();
wx.showToast({ title: res.msg || '邀请已发送,等待对方同意 💕', icon: 'success' });
this.setData({ inviteTargetId: '', inviteTab: 'sent' });
this.loadInvites();
} catch (err) {
wx.hideLoading();
wx.showToast({ title: err.error || '发送失败', icon: 'none' });
}
},
loadInvites: async function() {
try {
const data = await api.myInvitations();
const received = (data.received || []).map(function(item) {
item.from_initial = (item.from_nickname || item.from_username || '?')[0];
item.short_date = item.created_at ? item.created_at.substring(0, 16).replace('T', ' ') : '';
return item;
});
const sent = (data.sent || []).map(function(item) {
item.to_initial = (item.to_nickname || item.to_username || '?')[0];
item.short_date = item.created_at ? item.created_at.substring(0, 16).replace('T', ' ') : '';
return item;
});
this.setData({ receivedInvites: received, sentInvites: sent });
if (data.couple_id) {
wx.setStorageSync('coupleId', data.couple_id);
wx.showToast({ title: '情侣关系已恢复 💕', icon: 'success' });
setTimeout(() => {
this.setData({ showInvitePanel: false });
this.loadData();
}, 1500);
}
} catch (e) {
console.error(e);
}
},
switchInviteTab: function(e) {
const tab = e.currentTarget.dataset.tab;
this.setData({ inviteTab: tab });
},
acceptInvite: async function(e) {
const id = e.currentTarget.dataset.id;
try {
wx.showLoading({ title: '处理中...' });
const res = await api.respondInvitation({ invitation_id: id, accept: 1 });
wx.hideLoading();
if (res.couple_id) {
wx.setStorageSync('coupleId', res.couple_id);
wx.showToast({ title: '绑定成功 💕', icon: 'success' });
setTimeout(() => {
this.setData({ showInvitePanel: false });
this.loadData();
}, 1000);
}
} catch (err) {
wx.hideLoading();
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
}
},
rejectInvite: async function(e) {
const id = e.currentTarget.dataset.id;
try {
wx.showLoading({ title: '处理中...' });
await api.respondInvitation({ invitation_id: id, accept: 0 });
wx.hideLoading();
wx.showToast({ title: '已拒绝', icon: 'success' });
this.loadInvites();
} catch (err) {
wx.hideLoading();
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
}
},
// ===== 分享绑定 =====
onShareAppMessage: function() {
if (!this.data.shareOutgoingCode) return {title:'邀请你绑定情侣', path:'/pages/profile/profile'};
var displayId = wx.getStorageSync('displayId') || this.data.myDisplayId;
var nick = wx.getStorageSync('nickname') || '我';
if (!displayId) {
wx.showToast({ title: '请先完善资料', icon: 'none' });
return {};
}
return {
title: nick + ' 邀请你绑定情侣 💕',
path: '/pages/profile/profile?inviter=' + displayId + '&invite_code=' + encodeURIComponent(this.data.shareOutgoingCode || ''),
imageUrl: '/images/share.png'
};
},
// 从分享链接进入时处理邀请绑定
handleShareInvite: function(inviterId, code) {
if (!inviterId) return;
this.setData({ shareInviterId: inviterId, shareInviteCode: code || '', showShareBindModal: true, shareDisagree: false });
},
acceptShareBind: async function() {
var self = this;
try {
wx.showLoading({ title: '处理中...' });
const res = await api.acceptShareInvite({ inviter_id: this.data.shareInviterId, invite_code: this.data.shareInviteCode });
wx.hideLoading();
if (res.couple_id) {
wx.setStorageSync('coupleId', res.couple_id);
wx.showToast({ title: '绑定成功 💕', icon: 'success' });
this.setData({ showShareBindModal: false });
setTimeout(() => { this.loadData(); }, 1000);
}
} catch (err) {
wx.hideLoading();
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
}
},
rejectShareBind: function() {
this.setData({ shareDisagree: true, shareInviterName: 'TA' });
},
hideShareBindModal: function() {
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'];
const results=await Promise.allSettled([
api.timelineList(),api.albumList(),api.diaryList(),api.songsList(),api.checkinList(),
(this.data.coupleId || wx.getStorageSync('coupleId')) ? api.petInfo() : Promise.resolve({has_pet:false})
]);
const counts={};let failed=false;
results.forEach((result,index)=>{
const key=keys[index];
if(result.status !== 'fulfilled') { counts[key]='—';failed=true;return; }
const value=result.value;
if(key==='album') counts[key]=value && value.photo_total != null ? Number(value.photo_total) : itemsOf(value).reduce((sum,album)=>sum+(Number(album.photo_count)||0),0);
else if(key==='diary') counts[key]=Number(value && value.total)||0;
else if(key==='checkin') counts[key]=value && value.checkin_log_total !== undefined ? Number(value.checkin_log_total)||0 : (Array.isArray(value)?value.length:(value && value.items || []).length);
else if(key==='pet') counts[key]=value && value.has_pet && value.pet ? 'Lv.'+(Number(value.pet.level)||0) : '未领养';
else counts[key]=totalOf(value);
});
this.setData({statisticItems:this.data.statisticItems.map(item=>({...item,count:counts[item.key]===undefined?item.count:counts[item.key]})),statsLoading:false,statsError:failed});
},
openStatistic: function(e) {
const routes={timeline:'timeline',album:'albums',diary:'diary',songs:'lovewords',checkin:'punch',pet:'pet',period:'period',settings:'settings'};
const route=routes[e.currentTarget.dataset.key];
if(!route)return;
this._savePosBeforeLeave();
wx.navigateTo({url:'/pages/'+route+'/'+route});
},
// 页面跳转
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"], "loadStorageInfo": ["storageInfo"]}));