Files
past-go/weapp/pages/album/album.js
T
Blizzard 8be6d9b1ab feat: add account deletion, announcements and subscription renewal
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.
2026-11-03 22:06:47 +08:00

538 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { tryLoginRenewal } = require('../../utils/subscribe');
const { uploadReference } = require('../../utils/file-media');
const { itemsOf, startList, current, mergeUnique, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
const { currentUserLocation } = require('../../utils/user-location');
const { photoForm, metadataRows } = require('../../utils/photo-metadata');
const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout');
// pages/album/album.js - 日记式分组相册
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
const { guardStorageSpace } = require('../../utils/storage-guard');
Page(withDataCache(withListPagination({
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
onResize: function() { this.setData(getLayout()); },
data: { loading: true, loadError: false,
batches: [],
showPhotoEditor: false, editPhotos: [], editPhotoIdx: 0, editBatchId: '',
editForm: { taken_at: '', location: '', activity: '' }, savingPhoto: false, currentAddress: '',
albumId: null,
showUploadModal: false,
uploadFilePaths: [],
photoFormList: [], // 每张照片独立的表单数据
selectedPhotoIdx: 0, // 当前选中编辑的照片索引
uploadForm: { taken_at: '', location: '', activity: '' },
uploading: false,
showPreview: false,
previewImages: [],
previewIndex: 0,
},
onLoad: function(options) {
this.setData(getLayout());var id = options && options.album_id;
if (id) { this.setData({ albumId: id }); }
this._pendingPhotoPicker = !!(id && options.upload === '1');
this.loadData();
},
onShow: function() { this._visible = true; this.loadData(); },
onHide: function() { this._visible = false; },
openPhotoEditor: function(e) {
const batch = this.data.batches[Number(e.currentTarget.dataset.batch)];
if (!batch || !batch.photos.length) return;
this.setData({ showPhotoEditor: true, editBatchId: batch.batch_id, editPhotos: batch.photos, editPhotoIdx: 0, editForm: photoForm(batch.photos[0]) });
this._loadCurrentAddress();
},
_loadCurrentAddress: function() {
return currentUserLocation().then(location => { this.setData({ currentAddress: location.location }); return location; });
},
onEditPhotoInput: function(e) {
const field = e.currentTarget.dataset.field;
if (['taken_at', 'location', 'activity'].indexOf(field) < 0 || this.data.savingPhoto) return;
this.setData({ editForm: { ...this.data.editForm, [field]: e.detail.value } });
},
clearPhotoDate: function() {
if (!this.data.savingPhoto) this.setData({ editForm: { ...this.data.editForm, taken_at: '' } });
},
_confirmPhotoDiscard: function(next) {
if (this.data.savingPhoto) return;
const original = photoForm(this.data.editPhotos[this.data.editPhotoIdx] || {});
if (JSON.stringify(original) === JSON.stringify(this.data.editForm)) { next(); return; }
wx.showModal({ title: '放弃本次修改?', content: '当前照片的信息还没有保存。', confirmText: '放弃修改', success: res => { if (res.confirm && !this.data.savingPhoto) next(); } });
},
selectEditPhoto: function(e) {
const index = Number(e.currentTarget.dataset.index);
if (index === this.data.editPhotoIdx || !this.data.editPhotos[index]) return;
this._confirmPhotoDiscard(() => this.setData({ editPhotoIdx: index, editForm: photoForm(this.data.editPhotos[index]) }));
},
closePhotoEditor: function() {
this._confirmPhotoDiscard(() => this.setData({ showPhotoEditor: false }));
},
toggleMetadata: function(e) {
const index = Number(e.currentTarget.dataset.batch);
if (!this.data.batches[index]) return;
this.setData({ ['batches[' + index + '].metadataExpanded']: !this.data.batches[index].metadataExpanded });
},
preventEditorMove: function() {},
savePhotoInfo: async function() {
if (this.data.savingPhoto) return;
const photo = this.data.editPhotos[this.data.editPhotoIdx];
if (!photo || !photo.id) return;
const form = { taken_at: this.data.editForm.taken_at, location: this.data.editForm.location.trim(), activity: this.data.editForm.activity.trim() };
this.setData({ savingPhoto: true });
try {
if (!form.location) Object.assign(form, await currentUserLocation());
const result = await api.photoUpdate({ id: photo.id, lat: 0, lng: 0, ...form });
if (result && result.success && result.location !== undefined) form.location = result.location;
if (!result || !result.success) throw result || { error: '保存失败' };
// 就地更新卡片和预览,不重载列表,不改变滚动位置。
const batches = this.data.batches.map(batch => {
const photos = batch.photos.map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item);
return { ...batch, photos, metadata: metadataRows(photos) };
});
const previewImages = this.data.previewImages.map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item);
this._photoRows = (this._photoRows || []).map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item);
this.setData({ batches, photoItems: this._photoRows, previewImages, showPhotoEditor: false });
wx.showToast({ title: '照片信息已保存', icon: 'success' });
} catch (error) {
wx.showToast({ title: (error && (error.error || error.message)) || '保存失败,请重试', icon: 'none' });
} finally { this.setData({ savingPhoto: false }); }
},
// 左滑返回相册列表
touchStartX: 0, touchStartY: 0,
onTouchStart: function(e) {
if (this.data.showPreview) return;
this.touchStartX = e.touches[0].clientX;
this.touchStartY = e.touches[0].clientY;
},
onTouchEnd: function(e) {
if (this.data.showPreview) return;
var dx = this.touchStartX - e.changedTouches[0].clientX;
var dy = Math.abs(this.touchStartY - e.changedTouches[0].clientY);
if (dx < 20 && dy < 20) return;
if (Math.abs(dx) > dy && dx > 80) this.goToAlbums();
},
onPullDownRefresh: async function() {
await this.loadData();
wx.stopPullDownRefresh();
},
// 分享到好友
onShareAppMessage: function() {
return {
title: '让所有的爱有迹可循',
path: '/pages/album/album?album_id=' + (this.data.albumId || ''),
imageUrl: '/images/share.png'
};
},
// 分享到朋友圈
onShareTimeline: function() {
return {
title: '让所有的爱有迹可循',
imageUrl: '/images/share.png'
};
},
loadData: async function(page) {
const ticket = startList(this,page);
if (!ticket) return;
try {
var self = this;
var albumId = /^-\d+$/.test(String(this.data.albumId)) ? null : this.data.albumId;
if (!albumId) {
var albums = itemsOf(await api.albumList({page:1,page_size:1}));
if (albums && albums.length > 0) {
albumId = albums[0].id;
this.setData({ albumId: albumId });
}
}
if (albumId) {
// 共享内存缓存;写操作成功后自动失效。
var result = await api.photos(albumId, {page:ticket.page,page_size:20});
if (!current(this,ticket)) return;
var photos = ticket.page === 1 ? itemsOf(result) : mergeUnique(this._photoRows || [], itemsOf(result));
// 根据 batch_id 分组
var batchMap = {};
var myUid = wx.getStorageSync('userId');
var info = await api.coupleInfo();
if (!current(this,ticket)) return;
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
var nameMap = {};
var avatarMap = {};
nameMap[p1.id] = p1.nickname || p1.username || 'TA';
nameMap[p2.id] = p2.nickname || p2.username || 'TA';
avatarMap[p1.id] = getImageUrl(p1.avatar);
avatarMap[p2.id] = getImageUrl(p2.avatar);
var defaultName = nameMap[p1.id] || nameMap[p2.id] || '情侣';
var defaultAvatar = avatarMap[p1.id] || avatarMap[p2.id] || '';
(photos || []).forEach(function(p) {
var batchId = p.batch_id || p.created_at || 'default';
if (!batchMap[batchId]) {
var cid = String(p.created_by) || 0;
batchMap[batchId] = {
batch_id: batchId,
creator_name: nameMap[cid] || defaultName,
creator_avatar: avatarMap[cid] || defaultAvatar,
created_at: p.created_at || '',
created_time: p.created_at ? p.created_at.substring(0, 16).replace('T', ' ') : '',
photos: []
};
}
batchMap[batchId].photos.push({
...p,
url: getImageUrl(p.path)
});
});
// 按时间排序(最新的在最前)
var batches = Object.values(batchMap);
batches.sort(function(a, b) { return b.created_at > a.created_at ? 1 : -1; });
batches.forEach(function(batch) { batch.metadata = metadataRows(batch.photos); });
this._photoRows = photos;
applyList(this,ticket,result,itemsOf(result),'photoItems',{batches:batches});
} else { applyList(this,ticket,{items:[],total:0,has_more:false},[],'photoItems',{batches:[]}); }
} catch (e) {
failList(this,ticket); console.error(e); }
finally {
finishList(this,ticket);
if (this._visible && this._pendingPhotoPicker && !this.data.loadError && this.data.albumId) {
this._pendingPhotoPicker = false;
this.chooseImage();
}
}
},
chooseImage: function() {
var self = this;
guardStorageSpace(0).then(function(ok) {
if (!ok) return;
wx.chooseImage({
count: 9,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: function(res) {
// 确保文件路径是字符串数组
var paths = (res.tempFilePaths || []).map(function(p) { return String(p); });
if (paths.length === 0) {
wx.showToast({ title: '请选择照片', icon: 'none' });
return;
}
self._loadCurrentAddress();
var formList = paths.map(function() { return { taken_at: '', location: '', activity: '' }; });
self.setData({
showUploadModal: true,
uploadFilePaths: paths,
photoFormList: formList,
selectedPhotoIdx: 0,
uploadForm: { taken_at: '', location: '', activity: '' }
});
},
fail: function(err) {
// 微信选图器关闭时也会触发 fail,主动取消无需提示错误。
if (/cancel/i.test(String(err && err.errMsg || ''))) return;
console.error('Choose image failed:', err);
wx.showToast({ title: '选择照片失败', icon: 'none' });
}
});
});
},
onUploadInput: function(e) {
var field = e.currentTarget.dataset.field;
var v = e.detail.value;
// 同步更新当前显示的表单和 photoFormList
var formList = this.data.photoFormList.slice();
formList[this.data.selectedPhotoIdx] = Object.assign({}, formList[this.data.selectedPhotoIdx], { [field]: v });
this.setData({
['uploadForm.' + field]: v,
photoFormList: formList
});
},
// ===== 选中照片编辑 =====
selectPhoto: function(e) {
var idx = e.currentTarget.dataset.index;
var form = this.data.photoFormList[idx] || { taken_at: '', location: '', activity: '' };
this.setData({
selectedPhotoIdx: idx,
uploadForm: { taken_at: form.taken_at || '', location: form.location || '', activity: form.activity || '' }
});
},
removePhoto: function(e) {
var idx = e.currentTarget.dataset.index;
var paths = this.data.uploadFilePaths.filter(function(_, i) { return i !== idx; });
var formList = this.data.photoFormList.filter(function(_, i) { return i !== idx; });
if (paths.length === 0) {
this.cancelUpload();
} else {
var newIdx = Math.min(this.data.selectedPhotoIdx, paths.length - 1);
var f = formList[newIdx] || { taken_at: '', location: '', activity: '' };
this.setData({
uploadFilePaths: paths,
photoFormList: formList,
selectedPhotoIdx: newIdx,
uploadForm: { taken_at: f.taken_at || '', location: f.location || '', activity: f.activity || '' }
});
}
},
addMorePhotos: function() {
var self = this;
guardStorageSpace(0).then(function(ok) {
if (!ok) return;
var current = self.data.uploadFilePaths.length;
wx.chooseImage({
count: 9 - current,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: function(res) {
var newPaths = (res.tempFilePaths || []).map(function(p) { return String(p); });
var newForms = newPaths.map(function() { return { taken_at: '', location: '', activity: '' }; });
self.setData({
uploadFilePaths: self.data.uploadFilePaths.concat(newPaths),
photoFormList: self.data.photoFormList.concat(newForms)
});
}
});
});
},
// ===== 批量上传全部照片(每张使用各自的表单)=====
uploadAll: async function() {
if (this.data.uploading) return;
var paths = this.data.uploadFilePaths;
if (paths.length === 0) { wx.showToast({ title: '没有照片', icon: 'none' }); return; }
// 先保存当前编辑中的表单到 photoFormList
var formList = this.data.photoFormList.slice();
var curIdx = this.data.selectedPhotoIdx;
formList[curIdx] = {
taken_at: this.data.uploadForm.taken_at || '',
location: this.data.uploadForm.location || '',
activity: this.data.uploadForm.activity || ''
};
this.setData({ uploading: true });
var self = this;
var successCount = 0;
var failCount = 0;
var failReasons = [];
var quotaReached = false;
try {
var albumId = /^-\d+$/.test(String(this.data.albumId)) ? null : this.data.albumId;
if (!albumId) {
const albums = itemsOf(await api.albumList({ page: 1, page_size: 1 }));
albumId = albums.length ? albums[0].id : null;
}
if (!albumId) {
wx.showLoading({ title: '创建相册...' });
var album = await api.albumAdd({ title: '默认相册', description: '我们的甜蜜照片' });
albumId = album.id;
this.setData({ albumId: albumId });
wx.hideLoading();
}
this.setData({ albumId: albumId });
var batchId = Date.now().toString();
// 自动定位:为未填写位置的照片自动获取地理位置(发布后自动显示)
var autoLoc = null;
if (formList.some(function(x) { return !String(x.location || '').trim(); })) autoLoc = await currentUserLocation();
for (var i = 0; i < paths.length; i++) {
wx.showLoading({ title: '上传中 ' + (i + 1) + '/' + paths.length, mask: true });
var f = formList[i] || { taken_at: '', location: '', activity: '' };
try {
// 按照日记上传逻辑:先上传到通用 upload 接口获取路径,再调用 API 保存到相册
var uploadRes = await uploadFileCompat({
url: API_BASE + '/upload',
filePath: paths[i],
name: 'file',
formData: { sub_dir: 'photos' }
});
// 兼容不同响应格式:{path:'...'} 或 {success:true, path:'...'} 或 {url:'...'}
var uploadedPath = uploadReference(uploadRes);
if (uploadedPath) {
await api.photoAdd({
album_id: String(albumId),
path: uploadedPath,
taken_at: f.taken_at || '',
location: String(f.location || '').trim() || (autoLoc ? autoLoc.location : ''),
activity: f.activity || '',
batch_id: batchId,
// 未手动填写位置时,自动定位并记录经纬度(发布后自动显示地址)
lat: (f.location && f.location.trim()) ? 0 : (autoLoc ? autoLoc.lat : 0),
lng: (f.location && f.location.trim()) ? 0 : (autoLoc ? autoLoc.lng : 0)
});
successCount++;
} else {
failCount++;
failReasons.push('第' + (i+1) + '张: 上传返回无路径 - ' + JSON.stringify(uploadRes));
}
} catch (e) {
if (e && Number(e.code) === 10001) {
quotaReached = true;
failCount += paths.length - i;
wx.hideLoading();
break;
}
failCount++;
failReasons.push('第' + (i+1) + '张: ' + (e.error || e.errMsg || e.message || JSON.stringify(e)));
// 内容安全违规:明确提示用户
if (e && e.error) {
wx.showToast({ title: e.error, icon: 'none', duration: 2000 });
}
}
wx.hideLoading();
}
} catch (e) {
wx.hideLoading();
failReasons.push('整体异常: ' + (e.error || e.errMsg || e.message || JSON.stringify(e)));
}
this.setData({ uploading: false });
var msg = '上传完成 ' + successCount + '/' + paths.length;
if (failCount > 0) msg += '(' + failCount + '张失败)';
if (!quotaReached) wx.showToast({ title: msg, icon: successCount > 0 ? 'success' : 'none', duration: 2000 });
// 如果有失败,输出详细原因到控制台
if (failCount > 0 && failReasons.length > 0) {
console.error('相册上传失败详情:', failReasons.join(' | '));
}
if (successCount > 0) {
this.setData({ showUploadModal: false, uploadFilePaths: [], photoFormList: [], uploadForm: { taken_at: '', location: '', activity: '' } });
this.loadData();
}
},
cancelUpload: function() {
if (this.data.uploading) return;
this.setData({ showUploadModal: false, uploadFilePaths: [], photoFormList: [], uploadForm: { taken_at: '', location: '', activity: '' } });
},
// 上传页左滑返回
_upTouchX: 0, _upTouchY: 0,
onUploadTouchStart: function(e) {
this._upTouchX = e.touches[0].clientX;
this._upTouchY = e.touches[0].clientY;
},
onUploadTouchEnd: function(e) {
var dx = this._upTouchX - e.changedTouches[0].clientX;
var dy = Math.abs(this._upTouchY - e.changedTouches[0].clientY);
if (dx < 20 && dy < 20) return;
if (Math.abs(dx) > dy && dx > 80) {
this.cancelUpload();
}
},
closePreview: function() { this.setData({ showPreview: false }); },
onSwiperChange: function(e) { this.setData({ previewIndex: e.detail.current }); },
// 长按明信片 - 保存原图到相册
onPostcardLongPress: function(e) {
var self = this;
var url = e.currentTarget.dataset.url;
if (!url) return;
wx.showActionSheet({
itemList: ['保存原图到相册'],
success: function(res) {
if (res.tapIndex === 0) self.savePreviewImage(url);
}
});
},
// 保存原图:先通过 getImageInfo 获取本地临时路径再保存
savePreviewImage: function(url) {
var self = this;
wx.showLoading({ title: '保存中...' });
wx.getImageInfo({
src: url,
success: function(info) {
wx.hideLoading();
self._saveToAlbum(info.path);
},
fail: function() {
wx.hideLoading();
wx.showToast({ title: '获取图片失败', icon: 'none' });
}
});
},
_saveToAlbum: function(filePath) {
wx.saveImageToPhotosAlbum({
filePath: filePath,
success: function() {
wx.showToast({ title: '已保存到相册', icon: 'success' });
},
fail: function(err) {
var msg = (err && err.errMsg) || '';
if (msg.indexOf('auth') >= 0 || msg.indexOf('authorize') >= 0) {
wx.showModal({
title: '需要相册权限',
content: '请在设置中开启「保存到相册」权限,才能保存原图哦',
confirmText: '去设置',
success: function(res) {
if (res.confirm) wx.openSetting();
}
});
} else {
wx.showToast({ title: '保存失败', icon: 'none' });
}
}
});
},
// 预览时仅支持点击遮罩关闭
onPreviewTouchStart: function() {},
onPreviewTouchEnd: function() {},
// 点击单张照片 - 进入全屏明信片预览
onPhotoTap: function(e) {
var batchIdx = e.currentTarget.dataset.batch;
var imgIdx = e.currentTarget.dataset.index;
var batch = this.data.batches[batchIdx];
if (!batch || !batch.photos || !batch.photos[imgIdx]) return;
var allImages = [];
batch.photos.forEach(function(p) {
allImages.push({ id: p.id, url: p.url, taken_at: p.taken_at, location: p.location || '', activity: p.activity });
});
this.setData({ showPreview: true, previewImages: allImages, previewIndex: imgIdx });
},
// 长按照片 - 删除照片
onPhotoLongPress: function(e) {
var self = this;
var batchIdx = e.currentTarget.dataset.batch;
var imgIdx = e.currentTarget.dataset.index;
var batch = this.data.batches[batchIdx];
if (!batch || !batch.photos || !batch.photos[imgIdx]) return;
var photo = batch.photos[imgIdx];
wx.showModal({
title: '删除照片',
content: '确定要删除这张照片吗?',
success: function(res) {
if (res.confirm) {
wx.showLoading({ title: '删除中...' });
api.photoDelete({ id: photo.id }).then(function() {
wx.hideLoading();
wx.showToast({ title: '已删除', icon: 'success' });
self.loadData();
}).catch(function(err) {
wx.hideLoading();
wx.showToast({ title: err.error || '删除失败', icon: 'none' });
});
}
}
});
},
goToAlbums: function() { wx.navigateBack(); },
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
}, 'loadData'), {"loadData": ["photos", "albumList"]}));