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 { requestSubscribeAfterAction } = require('../../utils/subscribe'); const { guardStorageSpace } = require('../../utils/storage-guard'); Page(withDataCache(withListPagination({ 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 = []; // ★ 在用户点击手势上下文中提前请求订阅消息(符合微信规范) await requestSubscribeAfterAction('photo'); 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) { 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 + '张失败)'; 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"]}));