Files
past-go/weapp/pages/profile/profile.js
T
Blizzard 4c9708b562 feat: add WeChat Pay, Redis logging, storage tiers and personal albums
Replace virtual payments with WeChat Pay API v3 and verify callbacks and order polling. Cache WeChat access tokens in Redis and write structured Zap logs. Support configurable capacity plans across the API, admin and mini program, with purchase snapshots and quota enforcement. Allow personal albums before binding and share them with the current partner. Update deployment configuration and domain routing.
2026-11-02 23:40:49 +08:00

1107 lines
41 KiB
JavaScript

const { uploadReference } = require('../../utils/file-media');
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: String(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: uploadReference(data)
});
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:uploadReference(uploaded)});
} 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.quota_bytes != null ? d.quota_bytes : (d.quotaBytes != null ? d.quotaBytes : (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: (d.is_unlimited || d.isUnlimited) ? '无限空间' : formatStorageBytes(freeBytes),
storagePercent: 0,
storageOverQuota: !(d.is_unlimited || d.isUnlimited) && usedBytes>freeBytes,
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"]}));