更新最新代码

This commit is contained in:
CodeBuddy
2026-10-26 17:32:47 +08:00
commit 0f5dc01674
315 changed files with 42640 additions and 0 deletions
+529
View File
@@ -0,0 +1,529 @@
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');
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 = 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 = parseInt(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;
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;
var current = this.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 {
// 上传前先预热 session,确保认证状态有效
// 解决 Android 端首次上传时 session 未初始化的问题
try {
wx.showLoading({ title: '准备中...', mask: true });
await api.checkAuth();
wx.hideLoading();
} catch (e) {
wx.hideLoading();
console.warn('Session 预热失败(不影响后续上传)', e);
}
var albumId = this.data.albumId;
if (!albumId) {
wx.showLoading({ title: '创建相册...' });
var album = await api.albumAdd({ title: '默认相册', description: '我们的甜蜜照片' });
albumId = album.id;
this.setData({ albumId: albumId });
wx.hideLoading();
}
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 = uploadRes && (uploadRes.path || uploadRes.url || '');
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"]}));
+8
View File
@@ -0,0 +1,8 @@
{
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"usingComponents": {
"app-nav": "/components/app-nav/app-nav",
"page-state": "/components/page-state/page-state"
}
}
+161
View File
@@ -0,0 +1,161 @@
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
<launch-transition />
<!-- pages/album/album.wxml -->
<app-nav title="相册详情" back="{{true}}" />
<scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd" wx:if="{{!showUploadModal}}">
<view class="container"><view class="j-row"><view><text class="j-eyebrow">MOMENTS WE KEEP</text><text class="j-page-title">相册里的片段</text></view><button class="j-button j-button-small" bindtap="chooseImage">+ 照片</button></view>
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
<view class="moments-list" wx:if="{{!loading && !loadError && batches.length > 0}}">
<view class="moments-item" wx:for="{{batches}}" wx:key="batch_id">
<view class="moments-avatar">
<image wx:if="{{item.creator_avatar}}" src="{{item.creator_avatar}}" mode="aspectFill" class="moments-avatar-img"></image>
<view class="avatar-circle" wx:else>{{item.creator_name}}</view>
</view>
<view class="moments-body">
<view class="moments-name-row">
<text class="moments-name">{{item.creator_name || '情侣'}}</text>
<text class="moments-count">{{item.photos.length}}张</text>
<view class="moments-more" data-batch="{{index}}" catchtap="openPhotoEditor" hover-class="moments-more-pressed" aria-role="button" aria-label="编辑照片信息"><text>•••</text></view>
</view>
<text class="moments-time moments-upload-time">上传于 {{item.created_time}}</text>
<view class="moments-images">
<view class="album-img-wrap" wx:for="{{item.photos}}" wx:key="id" wx:for-index="imgIdx" data-batch="{{index}}" data-index="{{imgIdx}}" bindtap="onPhotoTap" bindlongpress="onPhotoLongPress">
<image src="{{item.url}}" mode="aspectFill" class="album-img" />
</view>
</view>
</view>
</view>
</view>
<view class="empty-state" wx:if="{{!loading && !loadError && !batches.length}}">
<text>📷</text>
<text>还没有照片</text>
<text>点击上方添加照片</text>
</view>
</view>
<view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部照片</text></view>
</scroll-view>
<view class="preview-overlay" wx:if="{{showPreview}}" bindtap="closePreview">
<swiper class="preview-swiper" current="{{previewIndex}}" bindchange="onSwiperChange" circular bindtouchstart="onPreviewTouchStart" bindtouchend="onPreviewTouchEnd">
<swiper-item wx:for="{{previewImages}}" wx:key="*this">
<view class="preview-postcard" bindtap="closePreview" bindlongpress="onPostcardLongPress" data-url="{{item.url}}">
<view class="postcard-photo-frame">
<image src="{{item.url}}" mode="aspectFit" class="postcard-photo" />
<view class="postcard-seal">♥</view>
<view class="postcard-stamp" catchtap="closePreview">
<text class="stamp-heart">❤</text>
<text class="stamp-text">LOVE</text>
</view>
</view>
<view class="postcard-body">
<view class="postcard-rule" wx:if="{{item.taken_at}}">
<text class="rule-icon">📅</text>
<text class="rule-text">{{item.taken_at}}</text>
</view>
<view class="postcard-rule" wx:if="{{item.location}}">
<text class="rule-icon">📍</text>
<text class="rule-text">{{item.location}}</text>
</view>
<view class="postcard-rule" wx:if="{{item.activity}}">
<text class="rule-icon">💌</text>
<text class="rule-text">{{item.activity}}</text>
</view>
<view class="postcard-rule" wx:if="{{!item.taken_at && !item.location && !item.activity}}">
<text class="rule-text postcard-empty">— 我们past —</text>
</view>
<view class="postcard-footer">
<text class="footer-slogan">✦ 让所有的爱都有迹可循 ✦</text>
<text class="footer-hint">长按可保存原图</text>
</view>
</view>
</view>
</swiper-item>
</swiper>
<view class="preview-counter">{{previewIndex + 1}} / {{previewImages.length}}</view>
</view>
<!-- 全屏上传页 -->
<view wx:if="{{showUploadModal}}" class="upload-root" bindtouchstart="onUploadTouchStart" bindtouchend="onUploadTouchEnd">
<view class="upload-nav">
<view class="upload-back" catchtap="cancelUpload">
<image class="back-icon" src="/images/back.svg" mode="aspectFit" />
</view>
</view>
<scroll-view class="upload-body" scroll-y show-scrollbar="{{false}}">
<!-- 照片网格 -->
<view class="photo-grid">
<view class="photo-grid-item {{selectedPhotoIdx == index ? 'selected' : ''}}" wx:for="{{uploadFilePaths}}" wx:key="*this" bindtap="selectPhoto" data-index="{{index}}">
<image src="{{item}}" mode="aspectFill" class="photo-grid-img" />
<view class="photo-has-form" wx:if="{{photoFormList[index].taken_at || photoFormList[index].location || photoFormList[index].activity}}">✓</view>
<view class="photo-del-btn" catchtap="removePhoto" data-index="{{index}}">
<text class="photo-del-x">✕</text>
</view>
</view>
<!-- 添加更多 -->
<view class="photo-grid-item photo-grid-add" catchtap="addMorePhotos" wx:if="{{uploadFilePaths.length < 9}}">
<text class="add-plus">+</text>
<text class="add-label">添加</text>
</view>
</view>
<!-- 选中照片的表单 -->
<view class="upload-form-card" wx:if="{{uploadFilePaths.length > 0}}">
<view class="form-card-head">
<image src="{{uploadFilePaths[selectedPhotoIdx]}}" mode="aspectFill" class="form-preview" />
<text class="form-card-desc">为第 {{selectedPhotoIdx + 1}} 张照片填写信息</text>
</view>
<view class="input-group">
<label>拍摄时间</label>
<picker mode="date" value="{{uploadForm.taken_at}}" bindchange="onUploadInput" data-field="taken_at">
<view class="input">{{uploadForm.taken_at || '选择日期'}}</view>
</picker>
</view>
<view class="input-group">
<label>地点</label>
<input placeholder-class="handwritten-placeholder" class="input" placeholder="{{currentAddress || '留空自动使用当前地址'}}" value="{{uploadForm.location}}" bindinput="onUploadInput" data-field="location" />
</view>
<view class="input-group">
<label>在做什么</label>
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:第一次一起看日落" value="{{uploadForm.activity}}" bindinput="onUploadInput" data-field="activity" />
</view>
</view>
<!-- 空状态提示(无照片时) -->
<view class="upload-empty" wx:if="{{uploadFilePaths.length == 0}}">
<text class="empty-icon">📷</text>
<text class="empty-desc">点击 + 添加照片</text>
</view>
</scroll-view>
<view class="upload-footer">
<button class="btn btn-primary" bindtap="uploadAll" hover-class="none" loading="{{uploading}}" wx:if="{{uploadFilePaths.length > 0}}">上传全部 ({{uploadFilePaths.length}}张)</button>
</view>
</view>
<!-- 照片信息编辑面板 -->
<view class="photo-editor-mask" wx:if="{{showPhotoEditor}}" catchtouchmove="preventEditorMove">
<view class="photo-editor-backdrop" catchtap="closePhotoEditor" />
<view class="photo-editor-sheet">
<view class="photo-editor-grip" />
<view class="photo-editor-head"><view><text class="photo-editor-title">给照片补一段故事</text><text class="photo-editor-caption">{{editPhotos.length > 1 ? '选择照片,分别记录它的时刻' : '留住这一天,也留住当时的心情'}}</text></view><view class="photo-editor-close" catchtap="closePhotoEditor" aria-role="button" aria-label="关闭">×</view></view>
<scroll-view class="photo-editor-body" scroll-y show-scrollbar="{{false}}">
<scroll-view class="photo-editor-picker" scroll-x show-scrollbar="{{false}}" wx:if="{{editPhotos.length > 1}}">
<view class="photo-editor-thumbs"><view wx:for="{{editPhotos}}" wx:key="id" class="photo-editor-thumb {{editPhotoIdx === index ? 'photo-editor-selected' : ''}}" data-index="{{index}}" catchtap="selectEditPhoto"><image src="{{item.url}}" mode="aspectFill" /><text>第 {{index + 1}} 张</text></view></view>
</scroll-view>
<view class="photo-editor-single" wx:else><image src="{{editPhotos[0].url}}" mode="aspectFill" /><text>编辑这张照片的信息</text></view>
<view class="photo-editor-field"><view class="photo-editor-label"><text>拍摄时间</text><text class="photo-editor-clear" wx:if="{{editForm.taken_at}}" catchtap="clearPhotoDate">清除</text></view><picker mode="date" value="{{editForm.taken_at}}" disabled="{{savingPhoto}}" bindchange="onEditPhotoInput" data-field="taken_at"><view class="photo-editor-input {{!editForm.taken_at ? 'photo-editor-placeholder' : ''}}">{{editForm.taken_at || '选择拍摄日期'}}<text>⌄</text></view></picker></view>
<view class="photo-editor-field"><text class="photo-editor-label">地点 · 留空使用当前地址</text><input class="photo-editor-input" placeholder="{{currentAddress || '留空自动使用当前地址'}}" placeholder-class="photo-editor-placeholder handwritten-placeholder" maxlength="200" value="{{editForm.location}}" disabled="{{savingPhoto}}" bindinput="onEditPhotoInput" data-field="location" /></view>
<view class="photo-editor-field"><text class="photo-editor-label">在做什么</text><textarea class="photo-editor-story" placeholder="例如:第一次一起看日落" placeholder-class="photo-editor-placeholder handwritten-placeholder" maxlength="200" value="{{editForm.activity}}" disabled="{{savingPhoto}}" bindinput="onEditPhotoInput" data-field="activity" fixed="{{true}}" adjust-position="{{true}}" /></view>
</scroll-view>
<view class="photo-editor-actions"><button class="photo-editor-cancel" bindtap="closePhotoEditor" disabled="{{savingPhoto}}">取消</button><button class="j-button photo-editor-save" bindtap="savePhotoInfo" loading="{{savingPhoto}}" disabled="{{savingPhoto}}">保存这张照片</button></view>
</view>
</view>
</view>
+402
View File
@@ -0,0 +1,402 @@
/* pages/album/album.wxss - 日记式分组相册 */
@import '../../styles/common.wxss';
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
.upload-root {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
z-index: 1000;
background: #FAFAFA;
display: flex; flex-direction: column;
}
.upload-nav {
display: flex; align-items: center;
padding: 96rpx 32rpx 16rpx;
}
.upload-back {
width: 56rpx; height: 56rpx;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(10rpx);
}
.upload-back .back-icon { width: 28rpx; height: 28rpx; }
.upload-body {
flex: 1; padding: 0 16rpx; overflow-y: auto;
}
/* 三宫格照片 - 精确三等分 */
.photo-grid {
display: flex; flex-wrap: wrap;
gap: 10rpx;
padding: 8rpx 0 20rpx;
}
.photo-grid-item {
width: calc((100% - 20rpx) / 3);
aspect-ratio: 1;
border-radius: 14rpx;
overflow: hidden;
position: relative;
background: #f0f0f0;
box-sizing: border-box;
}
.photo-grid-item.selected {
border: 4rpx solid #B74768;
box-shadow: 0 0 0 2rpx rgba(255,107,157,0.3);
}
.photo-grid-img {
width: 100%;
height: 100%;
display: block;
}
.photo-has-form {
position: absolute;
bottom: 6rpx;
left: 6rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: rgba(255,107,157,0.85);
color: white;
font-size: 22rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.photo-del-btn {
position: absolute;
top: 4rpx;
right: 4rpx;
width: 38rpx;
height: 38rpx;
border-radius: 50%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.photo-del-x {
color: white;
font-size: 22rpx;
font-weight: 700;
line-height: 1;
}
/* 添加更多 */
.photo-grid-add {
background: #fff;
border: 2rpx dashed #ddd;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rpx;
}
.add-plus { font-size: 44rpx; color: #ccc; font-weight: 300; }
.add-label { font-size: 20rpx; color: #bbb; }
/* 表单卡片 */
.upload-form-card {
background: white;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
}
.form-card-head {
display: flex; align-items: center; gap: 16rpx;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #F4E2E3;
}
.form-preview {
width: 80rpx; height: 80rpx;
border-radius: 12rpx;
flex-shrink: 0;
}
.form-card-desc {
font-size: 24rpx; color: #999;
}
.upload-form-card .input-group { margin-bottom: 16rpx; }
.upload-form-card .input-group:last-child { margin-bottom: 0; }
.upload-form-card .input-group label { font-size: 26rpx; font-weight: 500; color: #555; display: block; margin-bottom: 8rpx; }
.upload-form-card .input { padding: 18rpx 22rpx; font-size: 28rpx; background: #FFF5F8; border-radius: 14rpx; border: 1rpx solid #F4E2E3; }
/* 空状态 */
.upload-empty {
text-align: center; padding: 200rpx 40rpx;
}
.upload-empty .empty-icon { font-size: 80rpx; display: block; margin-bottom: 16rpx; }
.upload-empty .empty-desc { font-size: 28rpx; color: #bbb; display: block; }
/* 底部上传按钮 */
.upload-footer {
padding: 12rpx 24rpx;
padding-bottom: calc(12rpx + env(safe-area-inset-bottom));
}
.container { padding-top: 16rpx; padding-bottom: 300rpx; }
/* ===== 留念卡片(复用日记样式) ===== */
.moments-list { display: flex; flex-direction: column; gap: 24rpx; }
.moments-item {
display: flex; gap: 24rpx; background: white; border-radius: 24rpx;
padding: 32rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
}
.moments-avatar { flex-shrink: 0; }
.moments-avatar-img { width: 72rpx; height: 72rpx; border-radius: 50%; }
.avatar-circle {
width: 72rpx; height: 72rpx; border-radius: 50%;
background: linear-gradient(135deg, #B74768, #B388FF);
display: flex; align-items: center; justify-content: center;
font-size: 28rpx; font-weight: 700; color: white;
}
.moments-body { flex: 1; min-width: 0; }
.moments-name-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 24rpx;
margin-bottom: 8rpx;
}
.moments-name { font-size: 28rpx; font-weight: 600; color: #B74768; }
.moments-count { font-size: 22rpx; color: #999; }
.moments-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 12rpx; }
.moments-time { font-size: 22rpx; color: #999; font-weight: 500; flex-shrink: 0; margin-left: auto; }
/* ===== 朋友圈9宫格 ===== */
.moments-images {
display: flex;
flex-wrap: wrap;
margin-top: 40rpx;
}
.album-img-wrap {
width: 33.333%;
height: 0;
padding-bottom: 33.333%;
position: relative;
overflow: hidden;
background: #f0f0f0;
box-sizing: border-box;
}
.album-img-wrap:not(:nth-child(3n)) { padding-right: 4rpx; }
.album-img-wrap:nth-child(n+4):nth-child(-n+6) { padding-top: 4rpx; }
.album-img-wrap:nth-child(n+7) { padding-top: 4rpx; }
.album-img {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
}
/* ===== 全屏明信片预览(导航栏下方整屏) ===== */
.preview-overlay {
position: fixed; top: var(--nav-height,64px); left: 0; right: 0;
height: calc(100vh - var(--nav-height,64px));
background: rgba(0,0,0,0.93); z-index: 9999;
}
.preview-swiper { width: 100%; height: 100%; }
.preview-postcard {
position: relative;
width: 100%;
height: 100%;
min-height: 640rpx;
background: #fffdf7;
background-image:
radial-gradient(circle at 12% 88%, rgba(255,107,157,0.05) 0, rgba(255,107,157,0.05) 60rpx, transparent 61rpx),
radial-gradient(circle at 90% 8%, rgba(255,186,210,0.08) 0, rgba(255,186,210,0.08) 46rpx, transparent 47rpx);
border: 1rpx solid #f5e3d0;
display: flex;
flex-direction: column;
padding: 20rpx 24rpx 18rpx;
box-sizing: border-box;
overflow-y: auto;
box-shadow: inset 0 0 0 6rpx rgba(255,255,255,0.6), 0 20rpx 60rpx rgba(0,0,0,0.35);
}
/* 左上角爱心印章(叠加在照片左上角) */
.postcard-seal {
position: absolute;
top: 20rpx; left: 20rpx;
width: 64rpx; height: 64rpx;
border-radius: 50%;
background: rgba(255,255,255,0.8);
border: 3rpx double rgba(255,107,157,0.6);
color: rgba(255,107,157,0.8);
font-size: 28rpx;
display: flex; align-items: center; justify-content: center;
transform: rotate(-12deg);
z-index: 3;
}
/* 右上角爱心邮票(叠加在照片右上角) */
.postcard-stamp {
position: absolute;
top: 14rpx; right: 14rpx;
width: 84rpx; height: 98rpx;
background: #fff;
border: 3rpx dashed #B74768;
border-radius: 8rpx;
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 3;
transform: rotate(4deg);
box-shadow: 0 4rpx 14rpx rgba(255,107,157,0.25);
}
.stamp-heart {
font-size: 36rpx;
line-height: 1.1;
color: #B74768;
}
.stamp-text {
font-size: 15rpx;
letter-spacing: 2rpx;
color: #B74768;
font-weight: 700;
margin-top: 2rpx;
}
/* 照片白边相框 */
.postcard-photo-frame {
position: relative;
flex: 1;
min-height: 220rpx;
padding: 10rpx;
background: #fff;
border-radius: 10rpx;
box-shadow: 0 10rpx 30rpx rgba(180,120,90,0.18);
box-sizing: border-box;
overflow: hidden;
}
.postcard-photo {
width: 100%;
height: 100%;
border-radius: 6rpx;
background: #f2e8d5;
display: block;
}
/* 底部书写区 */
.postcard-body {
flex-shrink: 0;
margin-top: 14rpx;
padding: 12rpx 4rpx 6rpx;
border-top: 1rpx dashed #e8c9a8;
}
.postcard-rule {
display: flex;
align-items: flex-start;
gap: 10rpx;
padding: 6rpx 0;
line-height: 1.45;
font-size: 24rpx;
color: #6b4a2f;
}
.rule-icon {
font-size: 22rpx;
flex-shrink: 0;
line-height: 1.45;
}
.rule-text {
flex: 1;
word-break: break-all;
line-height: 1.45;
}
.postcard-empty {
color: #c9b294;
font-size: 24rpx;
}
.postcard-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8rpx;
padding-top: 8rpx;
border-top: 1rpx dashed rgba(232,201,168,0.6);
}
.footer-slogan {
font-size: 20rpx;
color: #B74768;
letter-spacing: 2rpx;
}
.footer-hint {
font-size: 18rpx;
color: #c9a98a;
}
.preview-counter {
position: absolute; bottom: 24rpx; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.55); color: white; padding: 8rpx 28rpx;
border-radius: 40rpx; font-size: 26rpx;
z-index: 20;
}
@import '../../styles/design.wxss';
@import '../../styles/journal.wxss';
/* 卡片上的照片故事与编辑入口。 */
.moments-name-row { align-items:center; gap:14rpx; margin-bottom:0; }
.moments-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.moments-count { flex-shrink:0; }
.moments-more { margin-left:auto; flex-shrink:0; width:88rpx; height:88rpx; display:flex; align-items:center; justify-content:center; border-radius:50%; color:#85776D; font-size:28rpx; letter-spacing:3rpx; }
.moments-more-pressed { background:#FFF0F5; }
.moments-upload-time { display:block; margin:0 0 20rpx; font-size:20rpx; color:#A39386; }
.moments-images { margin-top:20rpx; }
.moments-metadata { margin-top:18rpx; padding:16rpx 20rpx; border-radius:20rpx; background:#FFF5F8; }
.moment-note + .moment-note { border-top:1rpx solid #F1DCE4; margin-top:18rpx; padding-top:18rpx; }
.moment-photo-label { display:block; font-size:20rpx; color:#B74768; margin-bottom:12rpx; }
.moment-meta-row { display:flex; align-items:flex-start; gap:18rpx; margin-top:8rpx; font-size:24rpx; line-height:1.6; }
.moment-meta-label { flex-shrink:0; color:#A39386; font-size:21rpx; padding-top:2rpx; }
.moment-meta-value { color:#62504F; overflow-wrap:break-word; min-width:0; white-space:pre-wrap; }
.moment-story { color:#352E2A; }
.moment-add-note { margin-top:22rpx; padding:16rpx 0 2rpx; border-top:1rpx dashed #F1DCE4; color:#B74768; font-size:22rpx; line-height:1.6; }
.photo-editor-mask { position:fixed; inset:0; z-index:1300; display:flex; align-items:flex-end; }
.photo-editor-backdrop { position:absolute; inset:0; background:rgba(53,46,42,.35); }
.photo-editor-sheet { position:relative; width:100%; max-height:90vh; display:flex; flex-direction:column; background:#FFFCFD; border-radius:40rpx 40rpx 0 0; padding:16rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); box-sizing:border-box; }
.photo-editor-grip { width:64rpx; height:7rpx; border-radius:8rpx; background:#EBD5DE; margin:0 auto 26rpx; }
.photo-editor-head { display:flex; align-items:flex-start; gap:20rpx; margin-bottom:24rpx; }
.photo-editor-head > view:first-child { flex:1; min-width:0; }
.photo-editor-title { display:block; font-size:33rpx; font-weight:600; color:#352E2A; }
.photo-editor-caption { display:block; margin-top:10rpx; font-size:23rpx; color:#A39386; }
.photo-editor-close { width:64rpx; height:64rpx; display:flex; align-items:center; justify-content:center; border-radius:50%; background:#FFF0F5; color:#85776D; font-size:40rpx; }
.photo-editor-body { flex:none; height:760rpx; max-height:60vh; }
.photo-editor-picker { width:100%; white-space:nowrap; margin-bottom:28rpx; }
.photo-editor-thumbs { display:flex; gap:16rpx; padding:4rpx; }
.photo-editor-thumb { flex-shrink:0; padding:6rpx; border:2rpx solid transparent; border-radius:20rpx; text-align:center; }
.photo-editor-thumb image { display:block; width:104rpx; height:104rpx; border-radius:14rpx; }
.photo-editor-thumb text { display:block; font-size:20rpx; color:#A39386; margin-top:8rpx; }
.photo-editor-selected { border-color:#B74768; background:#FFF0F5; }.photo-editor-selected text { color:#B74768; }
.photo-editor-single { display:flex; align-items:center; gap:20rpx; margin-bottom:28rpx; font-size:24rpx; color:#85776D; }.photo-editor-single image { width:100rpx; height:100rpx; border-radius:18rpx; }
.photo-editor-field { margin-bottom:24rpx; }
.photo-editor-label { display:flex; align-items:center; justify-content:space-between; color:#62504F; font-size:25rpx; margin-bottom:12rpx; }
.photo-editor-clear { color:#B74768; font-size:22rpx; padding:4rpx 12rpx; }
.photo-editor-input { display:flex; align-items:center; justify-content:space-between; width:100%; height:88rpx; padding:0 24rpx; border:1rpx solid #F1DCE4; border-radius:18rpx; background:#FFF5F8; color:#352E2A; font-size:26rpx; box-sizing:border-box; }
.photo-editor-placeholder { color:#B4A3A6; }
.photo-editor-story { width:100%; height:160rpx; padding:22rpx 24rpx; border:1rpx solid #F1DCE4; border-radius:18rpx; background:#FFF5F8; color:#352E2A; font-size:26rpx; line-height:1.6; box-sizing:border-box; }
.photo-editor-actions { display:flex; align-items:stretch; gap:20rpx; padding-top:20rpx; }
.photo-editor-cancel { flex:1; margin:0; border:1rpx solid #F1DCE4; border-radius:26rpx; background:#FFFCFD; font-size:26rpx; color:#85776D; line-height:88rpx; }
.photo-editor-save { flex:2; }
.photo-editor-actions .photo-editor-save,
.photo-editor-actions .photo-editor-cancel {
display:flex;
align-items:center;
justify-content:center;
height:96rpx;
min-height:96rpx;
padding:0;
margin:0;
line-height:normal;
font-size:27rpx;
box-sizing:border-box;
}
.photo-editor-cancel::after { border:0; }
/* 默认只显示首张照片的两行摘要,完整信息由用户展开。 */
.moment-summary-line { display:flex; align-items:center; gap:12rpx; min-width:0; font-size:22rpx; line-height:1.5; color:#85776D; }
.moment-summary-badge { flex-shrink:0; font-size:19rpx; color:#B74768; }
.moment-summary-date { flex-shrink:0; }
.moment-summary-location { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.moment-summary-story { display:block; margin-top:6rpx; font-size:24rpx; line-height:1.5; color:#62504F; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.moment-metadata-toggle { display:flex; align-items:center; justify-content:space-between; gap:12rpx; min-height:44rpx; padding-top:8rpx; color:#B74768; font-size:21rpx; line-height:1.5; }
.moments-metadata.is-expanded { padding:20rpx; }
.moments-metadata.is-expanded .moment-metadata-toggle { margin-top:10rpx; border-top:1rpx solid #F1DCE4; padding-top:14rpx; }
@import '../../styles/button-theme.wxss';
@import '../../styles/typography.wxss';