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.
227 lines
9.6 KiB
JavaScript
227 lines
9.6 KiB
JavaScript
const { tryLoginRenewal } = require('../../utils/subscribe');
|
|
const { uploadReference } = require('../../utils/file-media');
|
|
const { withDataCache } = require('../../utils/page-cache');
|
|
const { getLayout } = require('../../utils/layout');
|
|
// pages/checkin-detail/checkin-detail.js
|
|
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
|
|
const { createSwipeBack } = require('../../utils/util');
|
|
const { guardStorageSpace } = require('../../utils/storage-guard');
|
|
const COOLDOWN = 5000;
|
|
Page(withDataCache({
|
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
|
onResize: function() { this.setData(getLayout()); },
|
|
data: { loading: true, loadError: false, item: null, logs: [], logsPage: 1, logsTotal: 0, logsHasMore: false, logsLoading: false, logsError: false, freqLabel: '', typeLabel: '', myStreak: 0, partnerStreak: 0, showCheckModal: false, checkPhoto: '', checkNote: '', lastActionTime: 0, detailId: 0, myAvatar: '', partnerAvatar: '', myName: '我', partnerName: 'TA' },
|
|
|
|
onLoad: function(options) {
|
|
this.setData(getLayout());
|
|
var id = options.id;
|
|
if (id) {
|
|
this.setData({ detailId: String(id) });
|
|
this.loadCoupleInfo();
|
|
this.loadDetail(String(id));
|
|
}
|
|
var self = this;
|
|
var swipe = createSwipeBack(function() { wx.navigateBack(); });
|
|
this.onTouchStart = swipe.onTouchStart;
|
|
this.onTouchEnd = swipe.onTouchEnd;
|
|
},
|
|
|
|
noop: function() {},
|
|
retryDetail: function() { this.loadDetail(this.data.detailId); },
|
|
onShow: function() {
|
|
if (this.data.detailId) {
|
|
this.loadCoupleInfo();
|
|
this.loadDetail(this.data.detailId);
|
|
}
|
|
},
|
|
|
|
// 从接口加载情侣头像和名称
|
|
loadCoupleInfo: function() {
|
|
var self = this;
|
|
return api.coupleInfo().then(function(info) {
|
|
self._applyCoupleInfo(info);
|
|
}).catch(function(e) { console.error(e); });
|
|
},
|
|
|
|
_applyCoupleInfo: function(info) {
|
|
if (!info) return;
|
|
var myUid = wx.getStorageSync('userId');
|
|
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
|
|
var me = (p1.id == myUid) ? p1 : p2;
|
|
var partner = (p1.id == myUid) ? p2 : p1;
|
|
this.setData({
|
|
myAvatar: getImageUrl(me.avatar || ''),
|
|
partnerAvatar: getImageUrl(partner.avatar || ''),
|
|
myName: me.nickname || '我',
|
|
partnerName: partner.nickname || 'TA'
|
|
});
|
|
},
|
|
|
|
loadDetail: async function(id) {
|
|
if (this._pageLoading) return;
|
|
this._pageLoading = true;
|
|
this._logsGeneration = (this._logsGeneration || 0) + 1;
|
|
this.setData({ logsLoading: false, logsError: false });
|
|
this.setData({loading: !this._hasCachedData, loadError:false});
|
|
try {
|
|
var data = await api.checkinDetail(id, { page: 1, page_size: 20 });
|
|
var item = data.item || {};
|
|
var myUid = wx.getStorageSync('userId');
|
|
var freqLabels = { daily: '每天', weekly: '每周', monthly: '每月', once: '一次', unlimited: '不限' };
|
|
var typeLabels = { both: '双方', first: '第一视角', her: '她打卡' };
|
|
var freqLabel = freqLabels[item.frequency] || '每天';
|
|
var typeLabel = typeLabels[item.type] || '双方';
|
|
// 创建人头像URL
|
|
item.creator_avatar_url = item.creator_avatar ? getImageUrl(item.creator_avatar) : '';
|
|
// 使用本次页面加载的情侣信息
|
|
var myAvatar = this.data.myAvatar;
|
|
var partnerAvatar = this.data.partnerAvatar;
|
|
var myName = this.data.myName;
|
|
var partnerName = this.data.partnerName;
|
|
var logs = this.formatLogs(data.logs || []);
|
|
// 根据打卡权限类型映射统计数据
|
|
var isCreator = (myUid == item.created_by);
|
|
var showMyStats = (item.type == 'both') || (item.type == 'first');
|
|
var showPartnerStats = (item.type == 'both') || (item.type == 'her');
|
|
var checkinerIsMe = (item.type == 'first' && isCreator) || (item.type == 'her' && !isCreator);
|
|
var displayAvatar = (item.type == 'both' || checkinerIsMe) ? myAvatar : partnerAvatar;
|
|
var displayName = (item.type == 'both' || checkinerIsMe) ? myName : partnerName;
|
|
var checkinToday = checkinerIsMe ? (data.my_today || 0) : (data.partner_today || 0);
|
|
var checkinWeek = checkinerIsMe ? (data.my_week || 0) : (data.partner_week || 0);
|
|
var checkinMonth = checkinerIsMe ? (data.my_month || 0) : (data.partner_month || 0);
|
|
var checkinYear = checkinerIsMe ? (data.my_year || 0) : (data.partner_year || 0);
|
|
var checkinStreak = checkinerIsMe ? (item.my_streak || 0) : (item.partner_streak || 0);
|
|
var pageData = {
|
|
item: item,
|
|
logs: logs,
|
|
logsPage: 1, logsTotal: Number(data.total) || logs.length,
|
|
logsHasMore: data.has_more === true, logsError: false,
|
|
freqLabel: freqLabel,
|
|
typeLabel: typeLabel,
|
|
myStreak: (item.type == 'both') ? (item.my_streak || 0) : checkinStreak,
|
|
partnerStreak: (item.type == 'both') ? (item.partner_streak || 0) : 0,
|
|
showMyStats: showMyStats,
|
|
showPartnerStats: showPartnerStats,
|
|
myToday: (item.type == 'both') ? (data.my_today || 0) : checkinToday,
|
|
myWeek: (item.type == 'both') ? (data.my_week || 0) : checkinWeek,
|
|
myMonth: (item.type == 'both') ? (data.my_month || 0) : checkinMonth,
|
|
myYear: (item.type == 'both') ? (data.my_year || 0) : checkinYear,
|
|
partnerToday: (item.type == 'both') ? (data.partner_today || 0) : 0,
|
|
partnerWeek: (item.type == 'both') ? (data.partner_week || 0) : 0,
|
|
partnerMonth: (item.type == 'both') ? (data.partner_month || 0) : 0,
|
|
partnerYear: (item.type == 'both') ? (data.partner_year || 0) : 0,
|
|
myAvatar: displayAvatar,
|
|
myName: displayName,
|
|
partnerAvatar: partnerAvatar,
|
|
partnerName: partnerName
|
|
};
|
|
this.setData(pageData);
|
|
} catch(e) {
|
|
this.setData({loadError:true}); console.error(e); }
|
|
finally { this._pageLoading = false; this.setData({loading:false}); }
|
|
},
|
|
|
|
formatLogs: function(logs) {
|
|
return logs.map(function(log) {
|
|
return Object.assign({}, log, {
|
|
time: log.created_at ? log.created_at.substring(0, 16).replace('T', ' ') : '',
|
|
photoUrl: log.photo ? getImageUrl(log.photo) : ''
|
|
});
|
|
});
|
|
},
|
|
loadMoreLogs: async function() {
|
|
if (this._pageLoading || this.data.loading || this.data.loadError || this.data.logsLoading || !this.data.logsHasMore) return;
|
|
var generation = this._logsGeneration;
|
|
var page = this.data.logsPage + 1;
|
|
this.setData({ logsLoading: true, logsError: false });
|
|
try {
|
|
var data = await api.checkinDetail(this.data.detailId, { page: page, page_size: 20 });
|
|
if (generation !== this._logsGeneration) return;
|
|
var seen = new Set(this.data.logs.map(function(log) { return String(log.id); }));
|
|
var added = this.formatLogs(data.logs || []).filter(function(log) {
|
|
var id = String(log.id);
|
|
if (seen.has(id)) return false;
|
|
seen.add(id); return true;
|
|
});
|
|
this.setData({ logs: this.data.logs.concat(added), logsPage: page,
|
|
logsTotal: Number(data.total) || 0, logsHasMore: data.has_more === true });
|
|
} catch (e) {
|
|
if (generation === this._logsGeneration) this.setData({ logsError: true });
|
|
} finally {
|
|
if (generation === this._logsGeneration) this.setData({ logsLoading: false });
|
|
}
|
|
},
|
|
|
|
// ===== 打卡操作 =====
|
|
showCheckinModal: function() {
|
|
var now = Date.now();
|
|
if (now - this.data.lastActionTime < COOLDOWN) {
|
|
wx.showToast({ title: '操作太快,请稍后', icon: 'none' }); return;
|
|
}
|
|
this.data.lastActionTime = now;
|
|
this.setData({ showCheckModal: true, checkPhoto: '', checkNote: '' });
|
|
},
|
|
|
|
onCheckNoteInput: function(e) { this.setData({ checkNote: e.detail.value }); },
|
|
|
|
chooseCheckPhoto: function() {
|
|
var self = this;
|
|
guardStorageSpace(0).then(function(ok) {
|
|
if (!ok) return;
|
|
wx.chooseImage({
|
|
count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'],
|
|
success: function(res) { self.setData({ checkPhoto: res.tempFilePaths[0] }); }
|
|
});
|
|
});
|
|
},
|
|
|
|
submitCheck: async function() {
|
|
var item = this.data.item;
|
|
if (!item || this._submittingCheck) return;
|
|
this._submittingCheck = true;
|
|
|
|
try {
|
|
var photoPath = '';
|
|
if (this.data.checkPhoto) {
|
|
wx.showLoading({ title: '上传中...' });
|
|
try {
|
|
var upResult = await uploadFileCompat({
|
|
url: API_BASE + '/upload',
|
|
filePath: this.data.checkPhoto,
|
|
name: 'file',
|
|
formData: { sub_dir: 'checkins' }
|
|
});
|
|
photoPath = uploadReference(upResult);
|
|
} catch(e) {
|
|
throw e;
|
|
}
|
|
wx.hideLoading();
|
|
}
|
|
var res = await api.checkinDo({ id: item.id, photo: photoPath, note: this.data.checkNote });
|
|
this.setData({ showCheckModal: false });
|
|
var toast = '打卡成功 ✅';
|
|
if (item.type === 'both' && item.partner_done_today !== true && !res.both_done) {
|
|
toast = '已打卡,等待对方';
|
|
}
|
|
wx.showToast({ title: toast, icon: 'success' });
|
|
// 刷新详情数据
|
|
this.loadDetail(item.id);
|
|
} catch(e) { wx.hideLoading(); wx.showToast({ title: e.error || '打卡失败', icon: 'none' }); }
|
|
finally { this._submittingCheck = false; }
|
|
},
|
|
|
|
hideCheckModal: function() { this.setData({ showCheckModal: false }); },
|
|
|
|
// ===== 图片预览 =====
|
|
previewPhoto: function(e) {
|
|
var url = e.currentTarget.dataset.url;
|
|
if (url) {
|
|
wx.previewImage({ urls: [url], current: url });
|
|
}
|
|
},
|
|
|
|
goBack: function() { wx.navigateBack(); },
|
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
|
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
|
}, {"loadDetail": ["checkinDetail", "coupleInfo"]}));
|