Support personal records and persistent smoking visibility. Add upload quota preflight, file cleanup, privacy entry, image caching and revision-based page refresh across the backend, admin and mini program.
427 lines
15 KiB
JavaScript
427 lines
15 KiB
JavaScript
const { getLayout } = require('../../utils/layout');
|
||
// pages/settings/settings.js
|
||
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
|
||
const { requestSubscribe, getSubscribeStatus, isTemplateConfigured, subscribeErrorMessage } = require('../../utils/subscribe');
|
||
const { guardStorageSpace } = require('../../utils/storage-guard');
|
||
Page({
|
||
onResize: function() { this.setData(getLayout()); },
|
||
data: {
|
||
showBgModal: false,
|
||
bgPreview: '',
|
||
bgFilePath: '',
|
||
|
||
|
||
hasPartner: false,
|
||
bindingKnown: false, bindingError:false,
|
||
|
||
// 解绑状态
|
||
unbindStatus: '', // '' / 'pending' / 'agreed' / 'rejected' / 'cancelled' / 'expired'
|
||
unbindIsRequester: false, // 当前用户是否是发起方
|
||
unbindRequesterName: '', // 发起方昵称
|
||
unbindRemaining: 0, // 剩余秒数
|
||
|
||
// 消息通知设置
|
||
showSubscribeModal: false,
|
||
subscribeStatus: {},
|
||
notifyAvailable: typeof isTemplateConfigured === 'function' && isTemplateConfigured('notify'),
|
||
subscribeItems: [],
|
||
subscribeLoading: false,
|
||
|
||
// 我们past
|
||
showAboutModal: false,
|
||
// 联系我们
|
||
showContactModal: false,
|
||
},
|
||
|
||
onLoad: function() {
|
||
this.setData(getLayout());},
|
||
|
||
onPullDownRefresh: function() {
|
||
wx.stopPullDownRefresh();
|
||
},
|
||
|
||
onShow: async function() {
|
||
this.setData({ bindingKnown: false, bindingError:false });
|
||
try {
|
||
var info = await api.coupleInfo();
|
||
var hasPartner = !!(info && !info.not_bound && info.id);
|
||
this.setData({ hasPartner: hasPartner, bindingKnown: true });
|
||
if (hasPartner) {
|
||
wx.setStorageSync('coupleId', info.id);
|
||
this.loadUnbindStatus();
|
||
} else {
|
||
wx.removeStorageSync('coupleId');
|
||
this.setData({ unbindStatus: '', unbindIsRequester: false, unbindRequesterName: '' });
|
||
}
|
||
} catch (e) {
|
||
this.setData({bindingError:true});
|
||
console.error('获取绑定状态失败', e);
|
||
}
|
||
},
|
||
|
||
goBind: function() {
|
||
getApp().globalData.openProfileBind = true;
|
||
wx.switchTab({ url: '/pages/profile/profile' });
|
||
},
|
||
|
||
deleteAccount: async function() {
|
||
if(this._deletingAccount)return;
|
||
this._deletingAccount=true;
|
||
try {
|
||
const preview=await api.accountDeletionPreview();
|
||
const counts=preview.summary.counts;
|
||
const text='已上传文件 '+counts.files+' 个,相册 '+counts.albums+' 个,照片 '+counts.photos+' 张,日记 '+counts.diaries+' 篇,记忆日 '+counts.timeline+' 条,打卡 '+counts.checkins+' 条,抽烟记录 '+counts.smoking+' 条,生理期日记录 '+counts.period+' 条。';
|
||
const labels={past_song:'歌曲',past_diary_comment:'评论',past_period_record:'生理期周期',past_period_sex_record:'亲密记录',past_storage_order:'订单',past_subscribe_template:'消息订阅'};
|
||
const more=Object.keys(labels).map(key=>labels[key]+' '+((preview.summary.record_counts||{})[key]||0)+' 条').join(',');
|
||
const confirm=options=>new Promise(resolve=>wx.showModal({...options,success:r=>resolve(r.confirm),fail:()=>resolve(false)}));
|
||
if(!await confirm({title:'注销账号前,请确认',content:text+' 其他数据:'+more+'。注销后,账号、所有内容及文件关联会被删除,无法恢复;剩余空间权益将失效。',confirmText:'继续注销',confirmColor:'#B74768'}))return;
|
||
if(!await confirm({title:'最后一次确认',content:'确定永久注销并删除全部数据?独占文件将从存储中清理;与其他用户重复的物理文件只移除你的关联,不会删除他人的内容。',confirmText:'确认注销',confirmColor:'#B74768'}))return;
|
||
wx.showLoading({title:'正在注销',mask:true});
|
||
await api.accountDelete({confirmed:true,confirmation_text:'注销并删除全部数据',confirmation_token:preview.confirmation_token});
|
||
getApp().finishAccountDeletion();
|
||
wx.reLaunch({url:'/pages/index/index'});
|
||
} catch(error) {wx.showToast({title:error.error||'注销失败,请稍后重试',icon:'none',duration:3000});}
|
||
finally {wx.hideLoading();this._deletingAccount=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() {});
|
||
},
|
||
|
||
noop: function() {},
|
||
|
||
// ===== 消息通知管理(按场景独立授权) =====
|
||
showSubscribeModal: async function() {
|
||
this.setData({ showSubscribeModal: true, subscribeLoading: true });
|
||
try {
|
||
// forceRefresh=true 确保从后端获取最新状态
|
||
const status = await getSubscribeStatus(true);
|
||
this.setData({
|
||
subscribeStatus: status,
|
||
subscribeChecked: !!status['notify'],
|
||
subscribeLoading: false
|
||
});
|
||
} catch (e) {
|
||
this.setData({ subscribeLoading: false });
|
||
}
|
||
},
|
||
|
||
hideSubscribeModal: function() {
|
||
this.setData({ showSubscribeModal: false });
|
||
},
|
||
|
||
// ===== 我们past =====
|
||
showAboutModal: function() {
|
||
this.setData({ showAboutModal: true });
|
||
},
|
||
hideAboutModal: function() {
|
||
this.setData({ showAboutModal: false });
|
||
},
|
||
|
||
openPrivacyContract: function() {
|
||
if (typeof wx.openPrivacyContract !== 'function') {
|
||
wx.showToast({ title: '请升级微信后查看隐私协议', icon: 'none' });
|
||
return;
|
||
}
|
||
wx.openPrivacyContract({
|
||
fail: function() {
|
||
wx.showToast({ title: '隐私协议暂时无法打开,请稍后重试', icon: 'none' });
|
||
}
|
||
});
|
||
},
|
||
|
||
// ===== 联系我们 =====
|
||
showContactModal: function() {
|
||
this.setData({ showContactModal: true });
|
||
},
|
||
hideContactModal: function() {
|
||
this.setData({ showContactModal: false });
|
||
},
|
||
|
||
toggleSubscribe: async function(e) {
|
||
const type = 'notify';
|
||
if (typeof isTemplateConfigured === 'function' && !isTemplateConfigured(type)) {
|
||
wx.showToast({title:'该提醒暂未配置模板',icon:'none'});
|
||
return;
|
||
}
|
||
const field = 'subscribeChecked';
|
||
const current = this.data[field];
|
||
|
||
// 防止重复点击
|
||
if (this._subscribing) return;
|
||
this._subscribing = true;
|
||
|
||
if (current) {
|
||
// 取消订阅
|
||
try {
|
||
await api.unsubscribeTemplate({ template_type: type });
|
||
this.setData({ [field]: false });
|
||
wx.setStorageSync('subscribed_' + type, false);
|
||
wx.showToast({ title: '已关闭通知', icon: 'success' });
|
||
} catch (e) {
|
||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||
}
|
||
} else {
|
||
// 请求订阅授权
|
||
try {
|
||
const results = await requestSubscribe(type);
|
||
if (results[type]) {
|
||
this.setData({ [field]: true });
|
||
wx.setStorageSync('subscribed_' + type, true);
|
||
wx.showToast({ title: '已开启通知', icon: 'success' });
|
||
} else {
|
||
wx.showToast({ title: '订阅被拒绝,可在设置中重新开启', icon: 'none' });
|
||
}
|
||
} catch (e) {
|
||
wx.showModal({title:'无法订阅提醒',content:subscribeErrorMessage(e),showCancel:false});
|
||
}
|
||
}
|
||
|
||
this._subscribing = false;
|
||
},
|
||
|
||
renewSubscribe: async function(e) {
|
||
const type = 'notify';
|
||
const field = 'subscribeChecked';
|
||
if (this._subscribing) return;
|
||
this._subscribing = true;
|
||
try {
|
||
const results = await requestSubscribe(type);
|
||
if (results[type]) {
|
||
this.setData({ [field]: true });
|
||
wx.showToast({ title: '已订阅一次提醒', icon: 'success' });
|
||
} else {
|
||
wx.showToast({ title: '本次未授权提醒', icon: 'none' });
|
||
}
|
||
} catch (e) {
|
||
wx.showModal({title:'无法订阅提醒',content:subscribeErrorMessage(e),showCancel:false});
|
||
} finally {
|
||
this._subscribing = false;
|
||
}
|
||
},
|
||
|
||
// ===== 背景图 =====
|
||
showBgModal: async function() {
|
||
this.setData({ showBgModal: true, bgPreview: '', bgFilePath: '' });
|
||
try {
|
||
var cfg = await api.getBgConfig();
|
||
if (!this.data.bgFilePath) this.setData({ bgPreview: cfg && cfg.image ? getImageUrl(cfg.image) : '' });
|
||
} catch (e) { console.error(e); }
|
||
},
|
||
hideBgModal: function() { this.setData({ showBgModal: false }); },
|
||
|
||
// 背景编辑页左滑返回设置页面
|
||
bgTouchStartX: 0, bgTouchStartY: 0,
|
||
onBgTouchStart: function(e) {
|
||
this.bgTouchStartX = e.touches[0].clientX;
|
||
this.bgTouchStartY = e.touches[0].clientY;
|
||
},
|
||
onBgTouchEnd: function(e) {
|
||
var dx = this.bgTouchStartX - e.changedTouches[0].clientX;
|
||
var dy = Math.abs(this.bgTouchStartY - e.changedTouches[0].clientY);
|
||
if (dx < 20 && dy < 20) return;
|
||
if (Math.abs(dx) > dy && dx > 80) this.hideBgModal();
|
||
},
|
||
chooseBgImage: function() {
|
||
var self = this;
|
||
guardStorageSpace(0).then(function(ok) {
|
||
if (!ok) return;
|
||
wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album'],
|
||
success: function(res) {
|
||
self.setData({ bgPreview: res.tempFilePaths[0], bgFilePath: res.tempFilePaths[0] });
|
||
}
|
||
});
|
||
});
|
||
},
|
||
saveBgImage: async function() {
|
||
var fp = this.data.bgFilePath;
|
||
if (!fp) {
|
||
if (this.data.bgPreview) {
|
||
wx.showToast({ title: '背景已设置', icon: 'success' });
|
||
this.hideBgModal();
|
||
return;
|
||
}
|
||
wx.showToast({ title: '请先选择图片', icon: 'none' }); return;
|
||
}
|
||
var self = this;
|
||
wx.showLoading({ title: '上传中...' });
|
||
try {
|
||
var data = await uploadFileCompat({
|
||
url: API_BASE + '/setBgImage',
|
||
filePath: fp,
|
||
name: 'bg',
|
||
formData: {}
|
||
});
|
||
wx.hideLoading();
|
||
if (data && data.path) {
|
||
var imgUrl = getImageUrl(data.path);
|
||
wx.showToast({ title: '背景已保存 ✨', icon: 'success' });
|
||
self.setData({ bgPreview: imgUrl, bgFilePath: '' });
|
||
self.hideBgModal();
|
||
} else {
|
||
var errMsg = (data && data.error) || '上传失败';
|
||
if (data && data.code) errMsg += ' (错误码: ' + data.code + ')';
|
||
wx.showToast({ title: errMsg, icon: 'none' });
|
||
}
|
||
} catch(e) {
|
||
wx.hideLoading();
|
||
var errMsg = (e && e.error) || '上传失败';
|
||
if (e && e.code) errMsg += ' (错误码: ' + e.code + ')';
|
||
wx.showToast({ title: errMsg, icon: 'none' });
|
||
}
|
||
},
|
||
removeBgImage: async function() {
|
||
try { await api.deleteBgImage(); } catch(e) {}
|
||
wx.showToast({ title: '已恢复默认', icon: 'success' });
|
||
this.setData({ bgPreview: '', bgFilePath: '' });
|
||
this.hideBgModal();
|
||
},
|
||
|
||
// ===== 解除绑定(根据状态显示不同操作)=====
|
||
showUnbind: 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();
|
||
wx.navigateTo({ url: '/pages/unbind/unbind' });
|
||
}).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' });
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 导航
|
||
goToHistory: function() { wx.navigateTo({ url: '/pages/history/history' }); },
|
||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||
goToProfile: function() { wx.navigateBack(); },
|
||
});
|