更新最新代码
This commit is contained in:
@@ -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"]}));
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
@@ -0,0 +1,178 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/albums/albums.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
albums: [], uploadMode:false,
|
||||
showMoreModal: false,
|
||||
showEditModal: false,
|
||||
currentAlbum: null,
|
||||
editForm: { title: '', desc: '' }
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
this.setData({uploadMode: !!(options && options.upload === '1')});
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
this.loadAlbums();
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadAlbums();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
// 分享到好友
|
||||
onShareAppMessage: function() {
|
||||
return {
|
||||
title: '让所有的爱有迹可循',
|
||||
path: '/pages/albums/albums',
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
},
|
||||
// 分享到朋友圈
|
||||
onShareTimeline: function() {
|
||||
return {
|
||||
title: '让所有的爱有迹可循',
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
|
||||
noop: function() {},
|
||||
|
||||
loadAlbums: async function(page) {
|
||||
const ticket = startList(this, page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
var list = await api.albumList({page:ticket.page,page_size:20});
|
||||
var result = list;
|
||||
list = itemsOf(list).map(function(a) {
|
||||
a.cover = getImageUrl(a.cover);
|
||||
a.photo_count = a.photo_count || 0;
|
||||
// 地理位置:优先显示地址名称,其次显示经纬度
|
||||
a.location_text = a.location || ((a.lat && a.lng) ? (a.lat + ', ' + a.lng) : '');
|
||||
return a;
|
||||
});
|
||||
applyList(this, ticket, result, list, 'albums');
|
||||
} catch(e) { console.error(e); failList(this,ticket); } finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
openAlbum: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
wx.navigateTo({ url: '/pages/album/album?album_id=' + id + (this.data.uploadMode ? '&upload=1' : '') });
|
||||
},
|
||||
|
||||
// 显示更多菜单
|
||||
showMoreMenu: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
var title = e.currentTarget.dataset.title || '默认相册';
|
||||
var desc = e.currentTarget.dataset.desc || '';
|
||||
var cover = e.currentTarget.dataset.cover || '';
|
||||
this.setData({
|
||||
showMoreModal: true,
|
||||
currentAlbum: { id: id, title: title, description: desc, cover: cover }
|
||||
});
|
||||
},
|
||||
|
||||
hideMoreMenu: function() {
|
||||
this.setData({ showMoreModal: false });
|
||||
},
|
||||
|
||||
// 编辑相册
|
||||
editAlbum: function() {
|
||||
var album = this.data.currentAlbum;
|
||||
if (!album) return;
|
||||
this.setData({
|
||||
showMoreModal: false,
|
||||
showEditModal: true,
|
||||
editForm: { title: album.title, desc: album.description || '' }
|
||||
});
|
||||
},
|
||||
|
||||
hideEditModal: function() {
|
||||
this.setData({ showEditModal: false });
|
||||
},
|
||||
|
||||
onEditTitleInput: function(e) { this.setData({ 'editForm.title': e.detail.value }); },
|
||||
onEditDescInput: function(e) { this.setData({ 'editForm.desc': e.detail.value }); },
|
||||
|
||||
saveAlbumEdit: async function() {
|
||||
var self = this;
|
||||
var album = this.data.currentAlbum;
|
||||
if (!album) return;
|
||||
var title = this.data.editForm.title;
|
||||
if (!title) { wx.showToast({ title: '请输入相册标题', icon: 'none' }); return; }
|
||||
try {
|
||||
wx.showLoading({ title: '保存中...' });
|
||||
await api.albumUpdate({
|
||||
id: album.id,
|
||||
title: title,
|
||||
description: this.data.editForm.desc || '',
|
||||
// 保留原有地理位置
|
||||
lat: album.lat || 0,
|
||||
lng: album.lng || 0,
|
||||
location: album.location || ''
|
||||
});
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已保存 ✨', icon: 'success' });
|
||||
this.hideEditModal();
|
||||
this.loadAlbums();
|
||||
} catch(e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: e.error || '保存失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 删除相册
|
||||
deleteAlbum: function() {
|
||||
var self = this;
|
||||
var album = this.data.currentAlbum;
|
||||
if (!album) return;
|
||||
// 从相册列表中查找照片数量
|
||||
var albums = this.data.albums;
|
||||
var targetAlbum = albums.find(function(a) { return String(a.id) === String(album.id); });
|
||||
var count = targetAlbum ? (targetAlbum.photo_count || 0) : 0;
|
||||
var title = album.title || '该相册';
|
||||
|
||||
this.setData({ showMoreModal: false });
|
||||
|
||||
if (count > 0) {
|
||||
wx.showModal({
|
||||
title: '无法删除',
|
||||
content: '「' + title + '」中还有 ' + count + ' 张照片,请先删除里面的全部照片后再删除相册',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
});
|
||||
return;
|
||||
}
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除「' + title + '」吗?',
|
||||
success: function(res) {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: '删除中...' });
|
||||
api.albumDelete({ id: album.id }).then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
self.loadAlbums();
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '删除失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, goToPublish: function() { wx.navigateTo({ url: '/pages/publish/publish?type=album' }); },
|
||||
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, 'loadAlbums'), {"loadAlbums": ["albumList"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/albums/albums.wxml - 相册列表 -->
|
||||
<app-nav title="{{uploadMode ? '选择相册' : '相册'}}" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="j-body"><view class="j-row"><view><text class="j-eyebrow">MOMENTS WE KEEP</text><text class="j-page-title">{{uploadMode ? '选择上传照片的相册' : '照片里的日常'}}</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 建相册</button></view>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在打开相册" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadAlbums" />
|
||||
<view class="j-album-list" wx:else><view class="j-album-print" wx:for="{{albums}}" wx:key="id" bindtap="openAlbum" data-id="{{item.id}}"><image wx:if="{{item.cover}}" src="{{item.cover}}" mode="aspectFill" /><view wx:else class="j-album-placeholder"><image src="/images/album.svg" /><text>还没有照片</text></view><view class="j-row"><view><text class="j-story-title">{{item.title || '我们的相册'}}</text><text class="j-caption">{{item.photo_count || 0}} 张照片</text></view><view class="j-link j-album-menu" catchtap="showMoreMenu" data-id="{{item.id}}" data-title="{{item.title}}" data-desc="{{item.description}}" data-cover="{{item.cover}}">•••</view></view><text wx:if="{{item.description}}" class="j-caption">{{item.description}}</text></view></view>
|
||||
<page-state wx:if="{{!loading && !loadError && !albums.length}}" title="相册还空着" description="为旅行、周末或日常,留一本相册" action="创建相册" bind:action="goToPublish" />
|
||||
</view>
|
||||
<!-- 操作菜单弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showMoreModal}}" bindtap="hideMoreMenu">
|
||||
<view class="action-sheet" catchtap="noop">
|
||||
<view class="action-item" bindtap="editAlbum">
|
||||
<image src="/images/edit.svg" mode="aspectFit" class="action-icon" />
|
||||
<text>编辑相册信息</text>
|
||||
</view>
|
||||
<view class="action-item danger" bindtap="deleteAlbum">
|
||||
<image src="/images/delete.svg" mode="aspectFit" class="action-icon" />
|
||||
<text>删除相册</text>
|
||||
</view>
|
||||
<view class="action-cancel" bindtap="hideMoreMenu">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 编辑相册弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showEditModal}}" bindtap="hideEditModal">
|
||||
<view class="modal" catchtap="noop" style="width:90%;max-width:560rpx;">
|
||||
<button class="modal-close" bindtap="hideEditModal"><image src="/images/close.svg" mode="aspectFit" /></button>
|
||||
<text class="title">编辑相册</text>
|
||||
<view class="input-group">
|
||||
<label>相册标题</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:旅行记录" value="{{editForm.title}}" bindinput="onEditTitleInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>描述(选填)</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="简单描述这个相册" value="{{editForm.desc}}" bindinput="onEditDescInput" />
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="saveAlbumEdit" hover-class="none">保存</button>
|
||||
</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>
|
||||
@@ -0,0 +1,132 @@
|
||||
/* pages/albums/albums.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; }
|
||||
|
||||
|
||||
.album-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16rpx 4rpx 300rpx;
|
||||
}
|
||||
.album-cell {
|
||||
width: calc(50% - 8rpx);
|
||||
margin: 0 4rpx 16rpx 4rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.album-cover {
|
||||
width: 100%;
|
||||
height: 260rpx;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.album-info {
|
||||
padding: 20rpx 16rpx;
|
||||
padding-right: 60rpx;
|
||||
}
|
||||
.album-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.album-desc {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.album-count {
|
||||
font-size: 22rpx;
|
||||
color: var(--pink);
|
||||
font-weight: 500;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
/* 三个点菜单按钮 */
|
||||
.album-more-btn {
|
||||
position: absolute;
|
||||
right: 12rpx;
|
||||
bottom: 12rpx;
|
||||
width: 56rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.85);
|
||||
border-radius: 24rpx;
|
||||
z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.album-more-btn:active {
|
||||
background: rgba(255,255,255,0.6);
|
||||
}
|
||||
.more-dots {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #999;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 操作菜单弹窗 */
|
||||
.action-sheet {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 16rpx 0;
|
||||
padding-bottom: calc(48rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
z-index: 1000;
|
||||
animation: slideUp 0.25s ease;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 40rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
.action-item .action-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.action-item.danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.action-item.danger .action-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-cancel {
|
||||
margin: 12rpx 30rpx 0;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
background: #f5f5f5;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,226 @@
|
||||
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 { requestSubscribeAfterAction } = require('../../utils/subscribe');
|
||||
const COOLDOWN = 5000;
|
||||
Page(withDataCache({
|
||||
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: parseInt(id) });
|
||||
this.loadCoupleInfo();
|
||||
this.loadDetail(parseInt(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;
|
||||
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 {
|
||||
// 为本人接收TA的提醒补充授权。
|
||||
await requestSubscribeAfterAction('checkin');
|
||||
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 = upResult.path || '';
|
||||
} catch(e) {
|
||||
// 内容安全违规:抛出错误中断打卡并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
photoPath = '';
|
||||
}
|
||||
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"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/checkin-detail/checkin-detail.wxml - 打卡详情 -->
|
||||
<app-nav title="打卡详情" back="{{true}}" />
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" bindscrolltolower="loadMoreLogs" lower-threshold="120" style="height:100vh;" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<view class="c-body">
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载详情" /><page-state wx:elif="{{loadError}}" kind="error" title="详情加载失败" action="重试" bind:action="retryDetail" />
|
||||
<block wx:else>
|
||||
|
||||
<!-- 打卡信息卡片(与列表卡片一致) -->
|
||||
<view class="detail-card" wx:if="{{item}}">
|
||||
<!-- 主题 -->
|
||||
<view class="card-row card-theme">
|
||||
<text class="card-icon" wx:if="{{item.icon && item.icon !== '✅'}}">{{item.icon}}</text><image wx:else class="card-icon" src="/images/check.svg" mode="aspectFit" />
|
||||
<text class="card-title">{{item.title}}</text>
|
||||
</view>
|
||||
<!-- 打卡频率 -->
|
||||
<view class="card-row">
|
||||
<text class="card-label">打卡频率</text>
|
||||
<text class="card-value">{{freqLabel}}</text>
|
||||
</view>
|
||||
<!-- 参与人(双人:双方昵称+头像 / 单人:对应昵称+头像) -->
|
||||
<view class="card-row" wx:if="{{item.type == 'both'}}">
|
||||
<text class="card-label">参与人</text>
|
||||
<view class="card-users">
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{myAvatar}}" class="chip-avatar" src="{{myAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{myName ? myName.charAt(0) : '我'}}</text>
|
||||
<text class="chip-name">{{myName}}</text>
|
||||
</view>
|
||||
<text class="chip-sep">·</text>
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{partnerAvatar}}" class="chip-avatar" src="{{partnerAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{partnerName ? partnerName.charAt(0) : 'TA'}}</text>
|
||||
<text class="chip-name">{{partnerName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-row" wx:elif="{{item.type == 'first' || item.type == 'her'}}">
|
||||
<text class="card-label">参与人</text>
|
||||
<view class="card-users">
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{item.type == 'first' && myAvatar}}" class="chip-avatar" src="{{myAvatar}}" mode="aspectFill" />
|
||||
<image wx:elif="{{item.type == 'her' && partnerAvatar}}" class="chip-avatar" src="{{partnerAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{item.type == 'first' ? (myName ? myName.charAt(0) : '我') : (partnerName ? partnerName.charAt(0) : 'TA')}}</text>
|
||||
<text class="chip-name">{{item.type == 'first' ? myName : partnerName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 创建人 -->
|
||||
<view class="card-row">
|
||||
<text class="card-label">创建人</text>
|
||||
<view class="card-creator-info">
|
||||
<image wx:if="{{item.creator_avatar_url}}" class="creator-avatar" src="{{item.creator_avatar_url}}" mode="aspectFill" />
|
||||
<text wx:else class="creator-avatar-fallback">{{item.creator_name ? item.creator_name.charAt(0) : '?'}}</text>
|
||||
<text class="creator-name">{{item.creator_name || '未知'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 连续统计卡片 -->
|
||||
<view class="streak-section">
|
||||
<!-- 我的连续(根据权限控制显示) -->
|
||||
<view class="streak-card" wx:if="{{showMyStats}}">
|
||||
<view class="streak-header">
|
||||
<image wx:if="{{myAvatar}}" class="streak-avatar" src="{{myAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="streak-avatar-fallback">{{myName ? myName.charAt(0) : '我'}}</text>
|
||||
<text class="streak-user">{{myName}}</text>
|
||||
</view>
|
||||
<view class="streak-grid">
|
||||
<view class="streak-item"><text class="streak-num">{{myToday}}</text><text class="streak-label">当天</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{myWeek}}</text><text class="streak-label">本周</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{myMonth}}</text><text class="streak-label">本月</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{myYear}}</text><text class="streak-label">今年</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- TA连续(根据权限控制显示) -->
|
||||
<view class="streak-card" wx:if="{{showPartnerStats}}">
|
||||
<view class="streak-header">
|
||||
<image wx:if="{{partnerAvatar}}" class="streak-avatar" src="{{partnerAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="streak-avatar-fallback">{{partnerName ? partnerName.charAt(0) : 'TA'}}</text>
|
||||
<text class="streak-user">{{partnerName}}</text>
|
||||
</view>
|
||||
<view class="streak-grid">
|
||||
<view class="streak-item"><text class="streak-num">{{partnerToday}}</text><text class="streak-label">当天</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{partnerWeek}}</text><text class="streak-label">本周</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{partnerMonth}}</text><text class="streak-label">本月</text></view>
|
||||
<view class="streak-item"><text class="streak-num">{{partnerYear}}</text><text class="streak-label">今年</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 打卡按钮(独立居中,匹配全局UI) -->
|
||||
<button class="detail-checkin-btn" wx:if="{{item.can_checkin}}" bindtap="showCheckinModal" hover-class="none">记录这一次</button>
|
||||
|
||||
<!-- 打卡记录列表 -->
|
||||
<text class="section-title">打卡记录 · {{logsTotal}} 条</text>
|
||||
<view class="log-list" wx:if="{{logs.length > 0}}">
|
||||
<view class="log-item" wx:for="{{logs}}" wx:key="id">
|
||||
<view class="log-dot" style="background:{{item.avatar ? '#B74768' : '#9C27B0'}};"></view>
|
||||
<view class="log-card">
|
||||
<view class="log-row">
|
||||
<text class="log-user">{{item.nickname || '我们'}}</text>
|
||||
<text class="log-time">{{item.time}}</text>
|
||||
</view>
|
||||
<text wx:if="{{item.note}}" class="log-note">{{item.note}}</text>
|
||||
<view wx:if="{{item.photo && item.photoUrl}}" class="log-photo-wrap" bindtap="previewPhoto" data-url="{{item.photoUrl}}">
|
||||
<image src="{{item.photoUrl}}" mode="aspectFill" class="log-photo" />
|
||||
<view class="log-photo-polaroid">
|
||||
<text class="polaroid-date">{{item.time}}</text>
|
||||
<text class="polaroid-icon">📸</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" wx:else>
|
||||
<text>📭</text>
|
||||
<text>暂无打卡记录</text>
|
||||
</view>
|
||||
<view class="logs-footer" wx:if="{{logs.length > 0}}">
|
||||
<text wx:if="{{logsLoading}}">正在加载…</text>
|
||||
<text wx:elif="{{logsError}}" bindtap="loadMoreLogs">加载失败,点击重试</text>
|
||||
<text wx:elif="{{logsHasMore}}" bindtap="loadMoreLogs">加载更多</text>
|
||||
<text wx:else>已加载全部记录</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<!-- 打卡弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showCheckModal}}" bindtap="hideCheckModal">
|
||||
<view class="modal" catchtap="noop" style="max-width:600rpx;">
|
||||
<text class="title">{{item.icon || '✅'}} {{item.title}}</text>
|
||||
<text class="modal-subtitle">记录这一刻</text>
|
||||
<view class="input-group"><label>备注</label><textarea placeholder-class="handwritten-placeholder" class="input" style="min-height:100rpx;" placeholder="一句话..." value="{{checkNote}}" bindinput="onCheckNoteInput" maxlength="60" /></view>
|
||||
<view class="input-group"><label>照片</label>
|
||||
<view class="photo-upload-box" bindtap="chooseCheckPhoto">
|
||||
<image wx:if="{{checkPhoto}}" src="{{checkPhoto}}" mode="aspectFill" class="photo-upload-img" />
|
||||
<view wx:else class="photo-upload-placeholder"><text>+</text><text>添加照片</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<button class="btn btn-outline" bindtap="hideCheckModal" hover-class="none">取消</button>
|
||||
<button class="btn btn-primary" bindtap="submitCheck" hover-class="none">确认打卡</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,26 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
@import '../../styles/companion.wxss';
|
||||
.streak-section { margin-top:28rpx; display:flex; flex-direction:column; gap:20rpx; }
|
||||
.streak-card { background:#FAF5EE; border-top:1rpx solid #E9DDD0; padding:24rpx 20rpx; border-radius:12rpx; }
|
||||
.streak-header { display:flex; align-items:center; gap:12rpx; margin-bottom:20rpx; }
|
||||
.streak-avatar,.streak-avatar-fallback { width:38rpx; height:38rpx; border-radius:50%; }.streak-avatar-fallback { background:#F4E2E3; color:#943C58; display:flex; align-items:center; justify-content:center; font-size:21rpx; }
|
||||
.streak-user { font-size:25rpx; font-weight:500; color:#352E2A; }.streak-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:12rpx; }.streak-item { display:flex; flex-direction:column; align-items:center; gap:8rpx; }
|
||||
.streak-num { font-size:36rpx; font-weight:500; color:#943C58; }.streak-label { font-size:21rpx; color:#85776D; }
|
||||
.detail-checkin-btn { width:100%; margin:24rpx 0 40rpx; padding:0; height:84rpx; line-height:84rpx; border-radius:18rpx; background:#B74768; color:#FFFDF8; font-size:28rpx; font-weight:500; box-shadow:none; }.detail-checkin-btn::after { border:0; }
|
||||
.section-title { display:block; font-size:29rpx; font-weight:600; color:#352E2A; margin:36rpx 0 24rpx; }
|
||||
.log-list { border-left:1rpx solid #DFD2C3; margin-left:8rpx; padding-left:28rpx; }.log-item { position:relative; margin-bottom:24rpx; }
|
||||
.log-dot { position:absolute; left:-36rpx; top:24rpx; width:14rpx; height:14rpx; border:4rpx solid #FAF5EE; border-radius:50%; }
|
||||
.log-card { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:12rpx; padding:24rpx; }.log-row { display:flex; align-items:center; justify-content:space-between; gap:14rpx; flex-wrap:wrap; }.log-user { color:#352E2A; font-size:25rpx; font-weight:500; }.log-time { color:#A39386; font-size:20rpx; }
|
||||
.log-note { display:block; font-size:26rpx; line-height:1.7; color:#65594F; margin-top:14rpx; }
|
||||
.log-photo-wrap { padding:10rpx; margin-top:20rpx; background:#FAF5EE; border:1rpx solid #E9DDD0; }.log-photo { width:100%; height:280rpx; display:block; }.log-photo-polaroid { display:none; }
|
||||
.empty-state { padding:64rpx 20rpx; font-size:25rpx; color:#85776D; text-align:center; }
|
||||
.modal-overlay { z-index:1200; }.modal .title { display:block; color:#352E2A; text-align:left; }.modal-subtitle { text-align:left; }.modal-actions { display:flex; gap:20rpx; margin-top:28rpx; }.modal-actions .btn { flex:1; min-width:0; padding:22rpx 12rpx; }
|
||||
.photo-upload-box { width:160rpx; height:160rpx; border:1rpx dashed #D5C5B3; border-radius:16rpx; overflow:hidden; background:#FAF5EE; }.photo-upload-img { width:100%; height:100%; }.photo-upload-placeholder { height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8rpx; color:#85776D; font-size:22rpx; }.photo-upload-placeholder text:first-child { font-size:44rpx; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
.logs-footer { padding:28rpx 0; text-align:center; color:#85776D; font-size:23rpx; }
|
||||
@@ -0,0 +1,167 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// Diary journal with server pagination.
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const { diaryEntry } = require('../../utils/journal');
|
||||
const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos');
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
scrollTop: 0,
|
||||
tabType: 'all' // 'all' 全部 | 'favorite' 收藏
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
this.setData({ scrollTop: getScrollPosition('diary') });
|
||||
},
|
||||
onShow: function() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// 收藏 tab 切换已移除
|
||||
|
||||
onScroll: function(e) {
|
||||
this._scrollTop = e.detail.scrollTop;
|
||||
saveScrollPosition('diary', e.detail.scrollTop);
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
this.setData({ page: 1, list: [], hasMore: true });
|
||||
await this.loadData();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
onScrollToLower: function() {
|
||||
if (this.data.loadingMore || !this.data.hasMore) return;
|
||||
this.loadMore();
|
||||
},
|
||||
|
||||
onShareAppMessage: function() {
|
||||
return { title: '让所有的爱有迹可循', path: '/pages/diary/diary', 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 {
|
||||
const data=await api.diaryList({page:ticket.page,page_size:20});
|
||||
applyList(this,ticket,data,this._formatItems(itemsOf(data)),'list');
|
||||
this._loadPartnerInfo();
|
||||
} catch(e) { failList(this,ticket); }
|
||||
finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
_formatItems: function(items) {
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var partnerNames = {};
|
||||
var partnerAvatars = {};
|
||||
return items.map(function(item) {
|
||||
item.creator_name = partnerNames[item.created_by] || '我';
|
||||
item.creator_avatar = partnerAvatars[item.created_by] || '';
|
||||
item.created_time = item.created_at ? item.created_at.substring(0, 16).replace('T', ' ') : '';
|
||||
var imgs = item.images || [];
|
||||
if (typeof imgs === 'string') {
|
||||
try { imgs = JSON.parse(imgs); } catch(e) { imgs = []; }
|
||||
}
|
||||
if (!Array.isArray(imgs)) imgs = [];
|
||||
item.imageUrls = imgs.map(function(p) { return getImageUrl(p); });
|
||||
// 生成摘要文本(截断内容,用于无图卡片展示)
|
||||
var summaryText = item.content || item.title || '';
|
||||
summaryText = summaryText.replace(/\s+/g, ' ');
|
||||
if (summaryText.length > 80) summaryText = summaryText.substring(0, 80) + '...';
|
||||
item.summary = summaryText;
|
||||
// 地理位置:优先显示地址名称,其次显示经纬度
|
||||
item.location_text = item.location || ((item.lat && item.lng) ? (item.lat + ', ' + item.lng) : '');
|
||||
return Object.assign(item, diaryEntry(item));
|
||||
});
|
||||
},
|
||||
|
||||
_loadPartnerInfo: async function() {
|
||||
try {
|
||||
var info = await api.coupleInfo();
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
|
||||
var names = {}, avatars = {};
|
||||
names[p1.id] = p1.nickname || p1.username || 'TA';
|
||||
names[p2.id] = p2.nickname || p2.username || 'TA';
|
||||
avatars[p1.id] = getImageUrl(p1.avatar);
|
||||
avatars[p2.id] = getImageUrl(p2.avatar);
|
||||
var oldList = this.data.list;
|
||||
var changed = false;
|
||||
var updates = {};
|
||||
for (var i = 0; i < oldList.length; i++) {
|
||||
var item = oldList[i];
|
||||
var newName = names[item.created_by] || '我';
|
||||
var newAvatar = avatars[item.created_by] || '';
|
||||
if (item.creator_name !== newName || item.creator_avatar !== newAvatar) {
|
||||
updates['list[' + i + '].creator_name'] = newName;
|
||||
updates['list[' + i + '].author'] = newName;
|
||||
updates['list[' + i + '].creator_avatar'] = newAvatar;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.setData(updates);
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
_refreshPartnerInfo: function() {
|
||||
var self = this;
|
||||
api.coupleInfo().then(function(info) {
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
|
||||
var names = {}, avatars = {};
|
||||
names[p1.id] = p1.nickname || p1.username || 'TA';
|
||||
names[p2.id] = p2.nickname || p2.username || 'TA';
|
||||
avatars[p1.id] = getImageUrl(p1.avatar);
|
||||
avatars[p2.id] = getImageUrl(p2.avatar);
|
||||
var oldList = self.data.list;
|
||||
var changed = false;
|
||||
var updates = {};
|
||||
for (var i = 0; i < oldList.length; i++) {
|
||||
var item = oldList[i];
|
||||
var newName = names[item.created_by] || '我';
|
||||
var newAvatar = avatars[item.created_by] || '';
|
||||
if (item.creator_name !== newName || item.creator_avatar !== newAvatar) {
|
||||
updates['list[' + i + '].creator_name'] = newName;
|
||||
updates['list[' + i + '].author'] = newName;
|
||||
updates['list[' + i + '].creator_avatar'] = newAvatar;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
self.setData(updates);
|
||||
}
|
||||
}).catch(function() {});
|
||||
},
|
||||
|
||||
// 点击进入日记详情
|
||||
goToDetail: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
if (!id) return;
|
||||
saveScrollPosition('diary', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
wx.navigateTo({ url: '/pages/diary_detail/diary_detail?id=' + id });
|
||||
},
|
||||
|
||||
_savePosBeforeLeave: function() {
|
||||
saveScrollPosition('diary', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
},
|
||||
|
||||
openJournalEntry: function(e) { this.goToDetail(e); },
|
||||
goToPublish: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/publish/publish?type=diary' }); },
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, 'loadData'), {"loadData": ["diaryList", "coupleInfo"]}));
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<import src="../../templates/journal.wxml" />
|
||||
<launch-transition />
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}"><app-nav title="日记" back="{{true}}" />
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bindscrolltolower="onScrollToLower" lower-threshold="100" bindscroll="onScroll" scroll-top="{{scrollTop}}">
|
||||
<view class="j-body"><view class="j-row"><view><text class="j-eyebrow">LIFE IN WORDS</text><text class="j-page-title">生活里的每一页</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 写日记</button></view>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在翻开日记" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadData" />
|
||||
<block wx:else><block wx:for="{{list}}" wx:key="id"><template is="journal-entry" data="{{entry:item}}" /></block><page-state wx:if="{{!list.length}}" title="故事,从今天开始" description="写下第一篇,记录一个平常又特别的瞬间" action="写日记" bind:action="goToPublish" /></block>
|
||||
<view class="j-caption j-center" wx:if="{{loadingMore}}">正在加载更多…</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>
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,279 @@
|
||||
const { itemsOf, totalOf, mergeUnique } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page(withDataCache({
|
||||
noop: function() {},
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loadError: false,
|
||||
diary: null,
|
||||
loading: true,
|
||||
comments: [],
|
||||
commentPage: 1, commentsLoadingMore: false, commentsMoreError: false, commentsTotal: 0,
|
||||
hasMoreComments: true,
|
||||
showCommentInput: false,
|
||||
commentText: '',
|
||||
commentInitialText: '',
|
||||
commentLength: 0,
|
||||
commentFocus: false,
|
||||
commentKeyboardHeight: 0,
|
||||
commentEditorHeight: 56,
|
||||
commentSending: false,
|
||||
replyTo: null,
|
||||
coverSwiperIndex: 0,
|
||||
previewImages: [],
|
||||
previewIndex: 0,
|
||||
showPreview: false,
|
||||
myUid: 0
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
var diaryId = options.id;
|
||||
if (!diaryId) { wx.showToast({ title: '参数错误', icon: 'none' }); return; }
|
||||
this.setData({ diaryId: parseInt(diaryId), myUid: wx.getStorageSync('userId') || 0 });
|
||||
this.loadDiary();
|
||||
this.loadComments();
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
if (this.data.diaryId) { this.loadDiary(); this.loadComments(); }
|
||||
},
|
||||
|
||||
loadDiary: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
var data = await api.diaryDetail(this.data.diaryId);
|
||||
data = this._formatDiary(data);
|
||||
this.setData({ diary: data, myUid: wx.getStorageSync('userId') || 0, loading: false });
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
_formatDiary: function(data) {
|
||||
var myUid = wx.getStorageSync('userId') || 0;
|
||||
data.creator_name = data.creator_name || (data.created_by == myUid ? '我' : 'TA');
|
||||
data.creator_avatar = getImageUrl(data.creator_avatar);
|
||||
data.created_time = data.created_at ? data.created_at.substring(0, 16).replace('T', ' ') : '';
|
||||
var imgs = data.images || [];
|
||||
if (typeof imgs === 'string') {
|
||||
try { imgs = JSON.parse(imgs); } catch(e) { imgs = []; }
|
||||
}
|
||||
if (!Array.isArray(imgs)) imgs = [];
|
||||
data.imageUrls = imgs.map(function(p) { return getImageUrl(p); });
|
||||
data.comment_count = data.comment_count || 0;
|
||||
// 地理位置:优先显示地址名称,其次显示经纬度(经纬度为0的不显示)
|
||||
data.location_text = '';
|
||||
var hasCoord = data.lat && data.lng && parseFloat(data.lat) !== 0 && parseFloat(data.lng) !== 0;
|
||||
if (data.location) {
|
||||
data.location_text = data.location;
|
||||
} else if (hasCoord) {
|
||||
data.location_text = data.lat + ', ' + data.lng;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
loadComments: function() { return this._loadCommentsPage(1); },
|
||||
_loadCommentsPage: async function(page) {
|
||||
page = Number(page) || 1;
|
||||
if (this._commentsFirst || (page > 1 && (this.data.commentsLoadingMore || !this.data.hasMoreComments))) return;
|
||||
if (page === 1) { this._commentsFirst = true; this._commentsGeneration = (this._commentsGeneration || 0) + 1; }
|
||||
var generation = this._commentsGeneration;
|
||||
this.setData(page === 1 ? {commentsError:false,commentsMoreError:false,commentsLoadingMore:false} : {commentsLoadingMore:true,commentsMoreError:false});
|
||||
try {
|
||||
var result = await api.diaryCommentList({diary_id:this.data.diaryId,page:page,page_size:20});
|
||||
if (generation !== this._commentsGeneration) return;
|
||||
var added = itemsOf(result).map(function(comment) {
|
||||
return Object.assign({}, comment, {_time:String(comment.created_at || comment.create_time || '').slice(0,16).replace('T',' ')});
|
||||
});
|
||||
this.setData({comments:page===1?added:mergeUnique(this.data.comments,added),commentPage:page,commentsTotal:totalOf(result),hasMoreComments:!Array.isArray(result)&&(result.has_more != null?!!result.has_more:page*20<totalOf(result))});
|
||||
} catch(e) {
|
||||
if (generation === this._commentsGeneration) this.setData(page===1?{commentsError:true}:{commentsMoreError:true});
|
||||
} finally {
|
||||
if (generation === this._commentsGeneration) { this._commentsFirst=false;this.setData({commentsLoadingMore:false}); }
|
||||
}
|
||||
},
|
||||
loadMoreComments: function() { return this._loadCommentsPage(this.data.commentPage+1); },
|
||||
|
||||
|
||||
onShareTap: function() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['分享给微信好友', '分享到朋友圈', '生成分享海报'],
|
||||
success: function(res) {
|
||||
if (res.tapIndex === 0) {
|
||||
wx.shareAppMessage ? wx.shareAppMessage() : wx.showToast({ title: '请点击右上角分享', icon: 'none' });
|
||||
} else if (res.tapIndex === 1) {
|
||||
wx.showToast({ title: '请点击右上角分享到朋友圈', icon: 'none' });
|
||||
} else {
|
||||
wx.showToast({ title: '功能开发中', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onCoverSwiperChange: function(e) {
|
||||
this.setData({ coverSwiperIndex: e.detail.current });
|
||||
},
|
||||
|
||||
onImageTap: function(e) {
|
||||
var idx = this.data.coverSwiperIndex;
|
||||
var imgs = this.data.diary.imageUrls || [];
|
||||
this.setData({ showPreview: true, previewIndex: idx, previewImages: imgs });
|
||||
},
|
||||
closePreview: function() { this.setData({ showPreview: false }); },
|
||||
onPreviewSwiperChange: function(e) { this.setData({ previewIndex: e.detail.current }); },
|
||||
|
||||
onCommentTap: function() {
|
||||
this.openComment(null);
|
||||
},
|
||||
onReplyTap: function(e) {
|
||||
var item = e.currentTarget.dataset.item;
|
||||
this.openComment({ id: item.id, user_id: item.user_id, nickname: item.nickname });
|
||||
},
|
||||
openComment: function(replyTo) {
|
||||
var sameTarget = (!replyTo && !this.data.replyTo) ||
|
||||
(replyTo && this.data.replyTo && replyTo.id === this.data.replyTo.id);
|
||||
var text = sameTarget ? this.data.commentText : '';
|
||||
this.setData({ showCommentInput: true, replyTo: replyTo, commentText: text,
|
||||
commentInitialText: text, commentLength: text.length, commentFocus: true,
|
||||
commentKeyboardHeight: 0, commentEditorHeight: 56 });
|
||||
},
|
||||
onCommentInput: function(e) {
|
||||
// 不把正文反复写回原生输入框,避免中文输入时光标跳动。
|
||||
this.data.commentText = e.detail.value || '';
|
||||
this.setData({ commentLength: this.data.commentText.length });
|
||||
},
|
||||
onCommentKeyboardHeight: function(e) {
|
||||
var height = Math.max(0, Number(e.detail.height) || 0);
|
||||
if (this.data.showCommentInput) this.setData({ commentKeyboardHeight: height });
|
||||
},
|
||||
onCommentLineChange: function(e) {
|
||||
this.setData({ commentEditorHeight: Math.min(160, Math.max(56, Number(e.detail.height) || 56)) });
|
||||
},
|
||||
onCommentBlur: function() {
|
||||
this.setData({ commentFocus: false });
|
||||
},
|
||||
cancelComment: function() {
|
||||
if (this.data.commentSending) return;
|
||||
this.setData({ showCommentInput: false, commentFocus: false, commentKeyboardHeight: 0 });
|
||||
wx.hideKeyboard();
|
||||
},
|
||||
onHide: function() {
|
||||
this.setData({ showCommentInput: false, commentFocus: false, commentKeyboardHeight: 0 });
|
||||
},
|
||||
|
||||
submitComment: async function() {
|
||||
if (this.data.commentSending) return;
|
||||
var text = this.data.commentText.trim();
|
||||
if (!text) { wx.showToast({ title: '请输入评论内容', icon: 'none' }); return; }
|
||||
this.setData({ commentSending: true });
|
||||
try {
|
||||
var params = { diary_id: this.data.diaryId, content: text };
|
||||
if (this.data.replyTo) {
|
||||
params.parent_id = this.data.replyTo.id;
|
||||
params.reply_to_user_id = this.data.replyTo.user_id;
|
||||
params.reply_to_username = this.data.replyTo.nickname;
|
||||
}
|
||||
var res = await api.diaryCommentAdd(params);
|
||||
if (res.success) {
|
||||
this._commentsGeneration = (this._commentsGeneration || 0) + 1;
|
||||
this._commentsFirst = false;
|
||||
this.setData({commentsLoadingMore:false});
|
||||
var newComment = res.comment || {};
|
||||
if (newComment.created_at) {
|
||||
newComment._time = newComment.created_at.substring(0, 16).replace('T', ' ');
|
||||
} else if (newComment.create_time) {
|
||||
newComment._time = newComment.create_time.substring(0, 16).replace('T', ' ');
|
||||
} else {
|
||||
var now = new Date();
|
||||
newComment._time = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + ' ' + String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
|
||||
}
|
||||
var newComments = this.data.comments.slice();
|
||||
newComments.unshift(newComment);
|
||||
this.setData({
|
||||
comments: newComments,
|
||||
commentsTotal: this.data.commentsTotal + 1,
|
||||
showCommentInput: false,
|
||||
replyTo: null,
|
||||
commentText: '',
|
||||
commentInitialText: '',
|
||||
commentLength: 0,
|
||||
commentFocus: false,
|
||||
commentKeyboardHeight: 0,
|
||||
'diary.comment_count': (this.data.diary.comment_count || 0) + 1
|
||||
});
|
||||
wx.hideKeyboard();
|
||||
wx.showToast({ title: '评论成功', icon: 'success' });
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '评论失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ commentSending: false });
|
||||
}
|
||||
},
|
||||
|
||||
onCommentLongPress: function(e) {
|
||||
var item = e.currentTarget.dataset.item;
|
||||
if (item.user_id != this.data.myUid) return;
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '删除评论',
|
||||
content: '确定要删除这条评论吗?',
|
||||
success: async function(res) {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await api.diaryCommentDelete({ id: item.id });
|
||||
var list = self.data.comments.filter(function(c) { return c.id != item.id; });
|
||||
self.setData({
|
||||
comments: list,
|
||||
'diary.comment_count': Math.max(0, (self.data.diary.comment_count || 0) - 1)
|
||||
});
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
} catch (e) { wx.showToast({ title: '删除失败', icon: 'none' }); }
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onMoreTap: function() {
|
||||
var self = this;
|
||||
wx.showActionSheet({
|
||||
itemList: ['编辑', '删除'],
|
||||
success: function(res) {
|
||||
if (res.tapIndex === 0) {
|
||||
wx.navigateTo({ url: '/pages/diary_edit/diary_edit?id=' + self.data.diaryId });
|
||||
} else if (res.tapIndex === 1) {
|
||||
self._deleteDiary();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_deleteDiary: function() {
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '删除日记',
|
||||
content: '确定要删除这篇日记吗?',
|
||||
success: async function(res) {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await api.diaryDelete({ id: self.data.diaryId });
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(function() { wx.navigateBack(); }, 1500);
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '删除失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }
|
||||
}, {"loadDiary": ["diaryDetail"], "loadComments": ["diaryCommentList"]}));
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<app-nav title="日记详情" back="{{true}}" /><view class="page-bg">
|
||||
|
||||
<view class="read-state" wx:if="{{loading || loadError}}"><page-state wx:if="{{loading}}" kind="loading" title="正在加载日记" /><page-state wx:else kind="error" title="日记加载失败" action="重试" bind:action="loadDiary" /></view>
|
||||
<scroll-view scroll-y bindscrolltolower="loadMoreComments" lower-threshold="120" class="read-scroll" wx:if="{{!loading && !loadError && diary}}" show-scrollbar="{{false}}" enhanced>
|
||||
<view class="read-page">
|
||||
<view class="read-author"><image class="read-avatar" src="{{diary.creator_avatar || '/images/default-avatar.svg'}}" mode="aspectFill" /><view class="read-author-info"><text class="read-name">{{diary.creator_name || 'TA'}}</text><text class="read-date">{{diary.created_time}}</text><text class="read-location" wx:if="{{diary.location_text}}">{{diary.location_text}}</text></view><view class="read-menu" catchtap="onMoreTap" aria-label="日记操作"><image src="/images/more.svg" mode="aspectFit" /></view></view>
|
||||
<view class="read-paper">
|
||||
<!-- ===== 图片区:有图轮播,无图用背景替代 ===== -->
|
||||
<view wx:if="{{diary.imageUrls && diary.imageUrls.length > 0}}">
|
||||
<view class="img-box">
|
||||
<swiper class="img-swiper" circular="{{false}}" indicator-dots="{{false}}" bindchange="onCoverSwiperChange" current="{{coverSwiperIndex}}">
|
||||
<swiper-item wx:for="{{diary.imageUrls}}" wx:key="*this" class="img-slide">
|
||||
<image src="{{item}}" mode="aspectFill" class="img-item" catchtap="onImageTap" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="img-count" wx:if="{{diary.imageUrls.length > 1}}">
|
||||
<text>{{coverSwiperIndex + 1}}/{{diary.imageUrls.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- ===== 内容区:白色卡片 ===== -->
|
||||
<view class="content-card">
|
||||
|
||||
<!-- 标题 -->
|
||||
<text class="title-text" wx:if="{{diary.title}}">{{diary.title}}</text>
|
||||
|
||||
<!-- 正文 -->
|
||||
<text class="content-text" wx:if="{{diary.content}}">{{diary.content}}</text>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<!-- ===== 评论区 ===== -->
|
||||
<view class="comment-box">
|
||||
<view class="comment-head">
|
||||
<text class="comment-head-text">留言 {{diary.comment_count || 0}}</text>
|
||||
</view>
|
||||
|
||||
<view class="comment-empty" wx:if="{{comments.length === 0}}">
|
||||
<text>还没有留言,写下你读到这里的心情吧</text>
|
||||
</view>
|
||||
|
||||
<view class="comment-item" wx:for="{{comments}}" wx:key="id" wx:for-item="c"
|
||||
data-item="{{c}}" catchlongpress="onCommentLongPress" catchtap="onReplyTap">
|
||||
<image class="comment-avatar" src="{{c.avatar_url || '/images/default-avatar.svg'}}" mode="aspectFill" />
|
||||
<view class="comment-body">
|
||||
<view class="comment-top">
|
||||
<text class="comment-name">{{c.nickname || '匿名'}}</text>
|
||||
<text class="comment-date">{{c._time || ''}}</text>
|
||||
</view>
|
||||
<text class="comment-content" wx:if="{{!c.reply_to_username}}">{{c.content}}</text>
|
||||
<text class="comment-content" wx:else>
|
||||
<text>回复 </text>
|
||||
<text class="comment-at">@{{c.reply_to_username}}</text>
|
||||
<text>:{{c.content}}</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="j-pagination" wx:if="{{commentsError}}" bindtap="loadComments">留言加载失败,点击重试</view>
|
||||
<view class="j-pagination" wx:elif="{{comments.length}}"><text wx:if="{{commentsLoadingMore}}">正在加载…</text><text wx:elif="{{commentsMoreError}}" bindtap="loadMoreComments">加载失败,点击重试</text><text wx:elif="{{hasMoreComments}}" bindtap="loadMoreComments">加载更多留言</text><text wx:else>已加载全部留言</text></view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入栏 -->
|
||||
<view class="bottom-bar" wx:if="{{!loading && !loadError && diary}}">
|
||||
<view class="bottom-input" catchtap="onCommentTap">
|
||||
<image class="bottom-input-icon" src="/images/comment.svg" mode="aspectFit" />
|
||||
<text class="bottom-input-text">说点什么...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评论面板自行跟随键盘,禁用原生页面上推,避免双重位移。 -->
|
||||
<view class="comment-compose-mask" wx:if="{{showCommentInput}}" catchtap="cancelComment" catchtouchmove="noop">
|
||||
<view class="comment-compose {{commentKeyboardHeight > 0 ? 'comment-compose--keyboard' : ''}}" style="bottom:{{commentKeyboardHeight}}px;" catchtap="noop">
|
||||
<view class="comment-compose-heading"><text>{{replyTo ? '回复 @' + replyTo.nickname : '写下你的留言'}}</text><text class="comment-compose-close" catchtap="cancelComment">收起</text></view>
|
||||
<view class="comment-compose-field">
|
||||
<textarea placeholder-class="handwritten-placeholder" class="comment-compose-input" style="height:{{commentEditorHeight}}px;" placeholder="说点什么,让 TA 知道你的心情…" value="{{commentInitialText}}" bindinput="onCommentInput" bindlinechange="onCommentLineChange" bindkeyboardheightchange="onCommentKeyboardHeight" bindblur="onCommentBlur" focus="{{commentFocus}}" fixed="{{true}}" adjust-position="{{false}}" hold-keyboard="{{true}}" show-confirm-bar="{{false}}" cursor-spacing="0" maxlength="500" disabled="{{commentSending}}" />
|
||||
</view>
|
||||
<view class="comment-compose-toolbar"><text class="comment-compose-count">{{commentLength}}/500</text><button class="comment-compose-send" catchtap="submitComment" disabled="{{commentLength === 0 || commentSending}}" loading="{{commentSending}}" hover-class="none">发送</button></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<view class="preview-mask" wx:if="{{showPreview}}" catchtap="closePreview">
|
||||
<swiper class="preview-swiper" current="{{previewIndex}}" bindchange="onPreviewSwiperChange" circular>
|
||||
<swiper-item wx:for="{{previewImages}}" wx:key="*this" class="preview-slide">
|
||||
<image src="{{item}}" mode="widthFix" class="preview-img" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="preview-num">{{previewIndex + 1}} / {{previewImages.length}}</view>
|
||||
<view class="preview-close" catchtap="closePreview">
|
||||
<image class="preview-close-icon" src="/images/close.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,33 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
.read-scroll { position:fixed; top:var(--nav-height,64px); left:0; right:0; height:calc(100vh - var(--nav-height,64px) - 100rpx - env(safe-area-inset-bottom)); }
|
||||
.read-state { padding:28rpx 44rpx; }.read-page { padding:32rpx 44rpx 40rpx; }
|
||||
.read-author { display:flex; align-items:center; gap:16rpx; margin-bottom:28rpx; }.read-avatar { width:64rpx; height:64rpx; border-radius:50%; flex-shrink:0; }.read-author-info { flex:1; min-width:0; }.read-name { display:block; font-size:26rpx; font-weight:500; color:#352E2A; }.read-date { display:block; margin-top:6rpx; font-size:21rpx; color:#85776D; }.read-location { display:block; font-size:21rpx; color:#85776D; margin-top:6rpx; line-height:1.5; }
|
||||
.read-menu { width:64rpx; height:64rpx; display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-right:-12rpx; }.read-menu image { width:36rpx; height:36rpx; }
|
||||
.read-paper { padding:12rpx; border:1rpx solid #E9DDD0; border-radius:6rpx 28rpx 6rpx 6rpx; background:#FFFDF8; }
|
||||
.img-box { width:100%; height:500rpx; position:relative; background:#F1EBE2; overflow:hidden; border-radius:3rpx 16rpx 3rpx 3rpx; }.img-swiper { width:100%; height:100%; }.img-slide { display:flex; align-items:center; justify-content:center; }.img-item { width:100%; height:100%; display:block; }
|
||||
.img-count { position:absolute; right:16rpx; bottom:16rpx; background:rgba(53,46,42,.6); color:#FFFDF8; padding:6rpx 14rpx; border-radius:10rpx; font-size:21rpx; }
|
||||
.content-card { padding:32rpx 24rpx 28rpx; }.title-text { display:block; font-size:34rpx; line-height:1.5; font-weight:600; color:#352E2A; margin-bottom:20rpx; }.content-text { display:block; font-size:29rpx; line-height:1.9; color:#4D423A; white-space:pre-wrap; overflow-wrap:break-word; }
|
||||
.comment-box { margin-top:38rpx; }.comment-head { padding-bottom:20rpx; border-bottom:1rpx solid #E9DDD0; }.comment-head-text { color:#352E2A; font-size:28rpx; font-weight:600; }.comment-empty { padding:44rpx 8rpx; text-align:center; font-size:24rpx; line-height:1.7; color:#85776D; }
|
||||
.comment-item { display:flex; align-items:flex-start; gap:14rpx; padding:24rpx 0; border-bottom:1rpx solid #E9DDD0; }.comment-item:last-child { border:0; }.comment-avatar { width:48rpx; height:48rpx; border-radius:50%; flex-shrink:0; }.comment-body { flex:1; min-width:0; }.comment-top { display:flex; justify-content:space-between; gap:12rpx; flex-wrap:wrap; margin-bottom:12rpx; }.comment-name { font-size:24rpx; color:#352E2A; font-weight:500; }.comment-date { font-size:20rpx; color:#A39386; }.comment-content { display:block; font-size:26rpx; line-height:1.65; color:#65594F; overflow-wrap:break-word; }.comment-at { color:#B74768; }
|
||||
.bottom-bar { position:fixed; left:0; right:0; bottom:0; z-index:20; background:#FFFDF8; border-top:1rpx solid #E9DDD0; padding:16rpx 44rpx calc(16rpx + env(safe-area-inset-bottom)); height:calc(100rpx + env(safe-area-inset-bottom)); box-sizing:border-box; }.bottom-input { display:flex; align-items:center; gap:14rpx; height:66rpx; border:1rpx solid #E9DDD0; background:#FAF5EE; padding:0 20rpx; border-radius:16rpx; }.bottom-input-icon { width:30rpx; height:30rpx; }.bottom-input-text { color:#85776D; font-size:25rpx; }
|
||||
.comment-compose-mask { position:fixed; inset:0; z-index:1200; background:rgba(53,46,42,.24); }
|
||||
.comment-compose { position:fixed; left:0; right:0; padding:22rpx 28rpx calc(20rpx + env(safe-area-inset-bottom)); box-sizing:border-box; border-radius:28rpx 28rpx 0 0; background:#FFFDF8; }
|
||||
.comment-compose--keyboard { padding-bottom:16rpx; }
|
||||
.comment-compose-heading { display:flex; align-items:center; justify-content:space-between; gap:20rpx; margin-bottom:16rpx; font-size:23rpx; line-height:1.5; color:#85776D; }
|
||||
.comment-compose-heading > text:first-child { flex:1; min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
||||
.comment-compose-close { flex-shrink:0; padding:8rpx 4rpx; color:#B74768; }
|
||||
.comment-compose-field { background:#FFF0F5; border-radius:22rpx; padding:20rpx 24rpx; }
|
||||
.comment-compose-input { display:block; width:100%; font-size:28rpx; line-height:1.6; color:#352E2A; }
|
||||
.comment-compose-toolbar { display:flex; align-items:center; justify-content:space-between; margin-top:14rpx; }
|
||||
.comment-compose-count { font-size:21rpx; color:#A39386; }
|
||||
.comment-compose-send { margin:0; min-width:124rpx; height:64rpx; padding:0 28rpx; border-radius:32rpx; background:#F47FA5; color:#FFF; font-size:25rpx; line-height:64rpx; }
|
||||
.comment-compose-send::after { border:0; }
|
||||
.comment-compose-send[disabled] { background:#F4E6EB; color:#A39386; }
|
||||
.preview-mask { position:fixed; top:0; left:0; right:0; bottom:0; z-index:9999; background:rgba(0,0,0,.95); }.preview-swiper { width:100%; height:100%; }.preview-slide { position:relative; }.preview-img { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; display:block; }.preview-num { position:fixed; bottom:calc(44rpx + env(safe-area-inset-bottom)); left:0; right:0; text-align:center; color:#FFFDF8; font-size:25rpx; }.preview-close { position:fixed; top:var(--nav-height,64px); right:24rpx; width:72rpx; height:72rpx; display:flex; align-items:center; justify-content:center; }.preview-close-icon { width:34rpx; height:34rpx; filter:brightness(0) invert(1); }
|
||||
@media (max-width:350px) { .read-page { padding-left:32rpx; padding-right:32rpx; }.bottom-bar { padding-left:32rpx; padding-right:32rpx; } }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,187 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
const { api, API_BASE, getImageUrl, uploadFileCompat } = require('../../utils/request');
|
||||
|
||||
var MOOD_OPTIONS = ['😊', '🥰', '😢', '😤', '😴', '🤩', '🥺', '😎', '🤔', '😭', '😋', '😌'];
|
||||
var WEATHER_OPTIONS = ['☀️', '⛅', '☁️', '🌧️', '⛈️', '❄️', '🌤️', '🌥️', '🌈', '🌪️', '🌫️', '🌙'];
|
||||
|
||||
Page({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
editorContent: '', loading: true, loadError: false,
|
||||
diaryId: 0,
|
||||
form: { title: '', content: '' },
|
||||
tempImages: [],
|
||||
existingImages: [],
|
||||
submitting: false,
|
||||
contentLength: 0
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
var id = options.id;
|
||||
if (!id) {
|
||||
wx.showToast({ title: '参数错误', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ diaryId: parseInt(id) });
|
||||
this.loadDiary();
|
||||
},
|
||||
|
||||
loadDiary: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
try {
|
||||
var data = await api.diaryDetail(this.data.diaryId);
|
||||
var title = data.title || '';
|
||||
var content = data.content || '';
|
||||
var imgs = data.images || [];
|
||||
if (typeof imgs === 'string') {
|
||||
try { imgs = JSON.parse(imgs); } catch(e) { imgs = []; }
|
||||
}
|
||||
if (!Array.isArray(imgs)) imgs = [];
|
||||
var tempImages = imgs.map(function(p) { return getImageUrl(p); });
|
||||
this.setData({
|
||||
'form.title': title,
|
||||
'form.content': content,
|
||||
editorContent: content,
|
||||
'form.lat': data.lat || 0,
|
||||
'form.lng': data.lng || 0,
|
||||
'form.location': data.location || '',
|
||||
tempImages: tempImages,
|
||||
existingImages: imgs,
|
||||
contentLength: content.length
|
||||
});
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
wx.showToast({ title: '加载日记失败', icon: 'none' });
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
onTitleInput: function(e) {
|
||||
this.setData({ 'form.title': e.detail.value });
|
||||
},
|
||||
onContentInput: function(e) {
|
||||
var val = e.detail.value || '';
|
||||
// 正文只存于逻辑层,避免每次敲字回传长文本影响原生输入。
|
||||
this.data.form.content = val;
|
||||
this.setData({ contentLength: val.length });
|
||||
},
|
||||
|
||||
chooseImages: function() {
|
||||
var self = this;
|
||||
wx.chooseImage({
|
||||
count: 9 - this.data.tempImages.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function(res) {
|
||||
self.setData({ tempImages: self.data.tempImages.concat(res.tempFilePaths) });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 删除单张图片 =====
|
||||
removeImage: function(e) {
|
||||
var index = parseInt(e.currentTarget.dataset.index);
|
||||
var images = this.data.tempImages;
|
||||
var existing = this.data.existingImages;
|
||||
images.splice(index, 1);
|
||||
existing.splice(index, 1);
|
||||
this.setData({ tempImages: images, existingImages: existing });
|
||||
},
|
||||
|
||||
submit: async function() {
|
||||
if (this.data.loading || this.data.loadError) return;
|
||||
if (this.data.submitting) return;
|
||||
var content = this.data.form.content;
|
||||
if (!content || !content.trim()) {
|
||||
wx.showToast({ title: '请填写日记内容', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ submitting: true });
|
||||
|
||||
try {
|
||||
var uploadPaths = [];
|
||||
var existing = this.data.existingImages || [];
|
||||
var temps = this.data.tempImages || [];
|
||||
|
||||
// 判断哪些是新上传的图片(tempFilePaths 以 http 开头的是已有图片,否则是新选的)
|
||||
for (var i = 0; i < temps.length; i++) {
|
||||
var p = temps[i];
|
||||
if (p.indexOf('http') === 0) {
|
||||
// 已有图片,用原始路径
|
||||
uploadPaths.push(existing[i] || '');
|
||||
} else {
|
||||
// 新图片,需要上传
|
||||
wx.showLoading({ title: '上传图片中...' });
|
||||
var res = await this._uploadImage(p);
|
||||
wx.hideLoading();
|
||||
if (res) uploadPaths.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
await api.diaryUpdate({
|
||||
id: this.data.diaryId,
|
||||
title: this.data.form.title || '',
|
||||
content: content.trim(),
|
||||
images: uploadPaths,
|
||||
// 保留原有地理位置(编辑时位置不变)
|
||||
lat: this.data.form.lat || 0,
|
||||
lng: this.data.form.lng || 0,
|
||||
location: this.data.form.location || ''
|
||||
});
|
||||
|
||||
// 清除详情页缓存
|
||||
|
||||
wx.showToast({ title: '修改成功 ✨', icon: 'success' });
|
||||
setTimeout(function() { wx.navigateBack(); }, 1500);
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: (e && (e.error || e.message || e.errMsg)) || '修改失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
},
|
||||
|
||||
_uploadImage: async function(filePath) {
|
||||
try {
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
var path = data && (data.path || data.url || '');
|
||||
if (!path) {
|
||||
// 内容安全检测失败:抛出错误提示用户
|
||||
if (data && data.error) {
|
||||
throw data;
|
||||
}
|
||||
try {
|
||||
var retryData = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
path = retryData && (retryData.path || retryData.url || '');
|
||||
} catch(retryErr) {
|
||||
console.error('图片上传重试失败', retryErr);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
} catch(e) {
|
||||
// 内容安全违规错误:直接抛出,中断发布并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
console.error('图片上传失败', e);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
goBack: function() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"disableScroll": true,
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<app-nav title="编辑日记" back="{{true}}" />
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" style="height:100vh;">
|
||||
<view class="container"><page-state wx:if="{{loading}}" kind="loading" title="正在加载日记" /><page-state wx:elif="{{loadError}}" kind="error" title="日记加载失败" action="重试" bind:action="loadDiary" /><block wx:else>
|
||||
<!-- 标题 -->
|
||||
<view class="input-group">
|
||||
<label>标题(选填)</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input diary-title-input" placeholder="给日记起个标题..." value="{{form.title}}" bindinput="onTitleInput" maxlength="100" />
|
||||
</view>
|
||||
|
||||
<!-- 内容编辑器(支持换行) -->
|
||||
<view class="input-group editor-group">
|
||||
<view class="editor-header">
|
||||
<label>内容</label>
|
||||
<text class="char-count {{contentLength > 5000 ? 'char-warn' : ''}}">{{contentLength}}/5000</text>
|
||||
</view>
|
||||
<textarea placeholder-class="handwritten-placeholder" class="input textarea diary-editor" placeholder="想写些什么..." value="{{editorContent}}" bindinput="onContentInput" maxlength="5000" auto-height="{{true}}" cursor-spacing="24" />
|
||||
</view>
|
||||
|
||||
<!-- 图片(可删除) -->
|
||||
<view class="input-group">
|
||||
<view class="editor-header">
|
||||
<label>图片</label>
|
||||
<text class="img-count">{{tempImages.length}}/9</text>
|
||||
</view>
|
||||
<view class="img-picker-row">
|
||||
<view wx:for="{{tempImages}}" wx:key="*this" class="img-picker-wrap">
|
||||
<image class="img-picker-thumb" src="{{item}}" mode="aspectFill" />
|
||||
<view class="img-picker-del" bindtap="removeImage" data-index="{{index}}" hover-class="del-hover">
|
||||
<text>✕</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="img-picker-add" bindtap="chooseImages" wx:if="{{tempImages.length < 9}}">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn btn-primary submit-btn" bindtap="submit" hover-class="none" loading="{{submitting}}">保存修改</button>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,149 @@
|
||||
@import '../../styles/common.wxss';
|
||||
|
||||
.container { padding: 84rpx 32rpx 300rpx; }
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
|
||||
/* ===== 输入框美化 ===== */
|
||||
.input-group { margin-bottom: 36rpx; }
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 28rpx 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.input:focus {
|
||||
border-color: #FF6B81;
|
||||
box-shadow: 0 0 0 6rpx rgba(255,107,129,0.08);
|
||||
}
|
||||
.textarea {
|
||||
min-height: 200rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ===== 日记标题输入框 ===== */
|
||||
.diary-title-input {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 编辑器头部(标签+计数) ===== */
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.editor-header label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.char-count {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
.char-warn {
|
||||
color: #ff6b6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.img-count {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* ===== 日记编辑器 ===== */
|
||||
.diary-editor {
|
||||
min-height: 300rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.7;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 图片选择(可删除) ===== */
|
||||
.img-picker-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.img-picker-wrap {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: visible;
|
||||
}
|
||||
.img-picker-thumb {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #eee;
|
||||
}
|
||||
.img-picker-del {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
right: -12rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,71,87,0.9);
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255,71,87,0.3);
|
||||
z-index: 2;
|
||||
}
|
||||
.del-hover {
|
||||
transform: scale(1.2);
|
||||
background: #ff4757;
|
||||
}
|
||||
.img-picker-add {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
border: 2rpx dashed #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.img-picker-add .add-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ccc;
|
||||
line-height: 1;
|
||||
}
|
||||
.img-picker-add .add-text {
|
||||
font-size: 20rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* ===== 提交按钮 ===== */
|
||||
.submit-btn {
|
||||
margin-top: 40rpx;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* 正文随输入增高,长文由外层页面滚动,避免嵌套原生输入框滚动。 */
|
||||
textarea.diary-editor { min-height:420rpx; box-sizing:border-box; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,101 @@
|
||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/discover/discover.js
|
||||
const { api } = require('../../utils/request');
|
||||
const { diaryEntry, albumEntry, memoryEntry } = require('../../utils/journal');
|
||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
stats: null, loading: true, loadError: false, refreshing: false, publishOpen: false,
|
||||
activeFilter: 'diary', filters: [{value:'diary',label:'日记',icon:'stat-book'}, {value:'album',label:'相册',icon:'stat-album'}, {value:'memory',label:'记忆日',icon:'stat-timeline'}],
|
||||
entries: [], albums: [], memories: [],
|
||||
scrollTop: 0
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.setData({ scrollTop: 0 });
|
||||
},
|
||||
onShow: function() {
|
||||
resetScrollOnTabShow(this, 'discover');
|
||||
const tabBar = this.getTabBar && this.getTabBar();
|
||||
if (tabBar) tabBar.syncActive();
|
||||
this._syncPublishTabBar();
|
||||
this.loadStats();
|
||||
},
|
||||
|
||||
// 保存滚动位置
|
||||
onScroll: function(e) {
|
||||
this._scrollTop = e.detail.scrollTop;
|
||||
saveScrollPosition('discover', e.detail.scrollTop);
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadStats(true);
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
loadStats: async function() {
|
||||
if (this._loading) return;
|
||||
this._loading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
|
||||
try {
|
||||
const [timeline, album, diary] = await Promise.all([
|
||||
api.timelineList(), api.albumList(), api.diaryList()
|
||||
]);
|
||||
var photoCount = album && album.photo_total != null ? Number(album.photo_total) : itemsOf(album).reduce((sum,item)=>sum+(Number(item.photo_count)||0),0);
|
||||
var stats = {
|
||||
timeline: totalOf(timeline),
|
||||
album: photoCount,
|
||||
diary: (diary && diary.total) ? diary.total : 0,
|
||||
};
|
||||
this.setData({ stats: stats, entries: ((diary && diary.items) || []).map(diaryEntry), albums: itemsOf(album).map(albumEntry), memories: itemsOf(timeline).map(memoryEntry) });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setData({loadError:true});
|
||||
} finally { this._loading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
onRefresh: async function() { this.setData({refreshing:true}); try { await this.loadStats(); } finally { this.setData({refreshing:false}); } },
|
||||
selectFilter: function(e) { this.setData({activeFilter:e.currentTarget.dataset.filter}); },
|
||||
_syncPublishTabBar: function() {
|
||||
const tabBar = this.getTabBar && this.getTabBar();
|
||||
if (tabBar && tabBar.setHidden) tabBar.setHidden(this.data.publishOpen);
|
||||
},
|
||||
showPublish: function() { this.setData({publishOpen:true}); this._syncPublishTabBar(); },
|
||||
hidePublish: function() { this.setData({publishOpen:false}); this._syncPublishTabBar(); },
|
||||
onHide: function() { this.hidePublish(); },
|
||||
noop: function() {}, preventMove: function() {},
|
||||
_publish: function(type) { this.hidePublish(); this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/publish/publish?type=' + type}); },
|
||||
writeDiary: function() { this._publish('diary'); },
|
||||
uploadPhotos: function() { this.hidePublish(); this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/albums/albums?upload=1'}); },
|
||||
createAlbum: function() { this._publish('album'); },
|
||||
createMemory: function() { this._publish('timeline'); },
|
||||
openJournalEntry: function(e) { this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/diary_detail/diary_detail?id=' + e.currentTarget.dataset.id}); },
|
||||
openAlbum: function(e) { this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/album/album?album_id=' + e.currentTarget.dataset.id}); },
|
||||
// 跳转前保存当前滚动位置
|
||||
_savePosBeforeLeave: function() {
|
||||
saveScrollPosition('discover', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
},
|
||||
|
||||
// Tab 导航(替换,不留栈)
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goToDiaryFromDiscover: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/diary/diary?from=discover' }); },
|
||||
goToProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); },
|
||||
|
||||
// 功能入口(推入栈,支持左滑返回)
|
||||
goToTimeline: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/timeline/timeline' }); },
|
||||
goToAlbum: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/albums/albums' }); },
|
||||
goToSongs: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/lovewords/lovewords' }); },
|
||||
goToCheckin: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/punch/punch' }); },
|
||||
goToPet: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/pet/pet' }); },
|
||||
goToPeriod: function() {
|
||||
this.hidePublish();
|
||||
this._savePosBeforeLeave();
|
||||
wx.navigateTo({ url: '/pages/period/period' });
|
||||
}
|
||||
}, {"loadStats": ["timelineList", "albumList", "diaryList"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<import src="../../templates/journal.wxml" />
|
||||
<launch-transition />
|
||||
<view class="app-shell j-handwritten-page" style="{{layoutStyle}}"><view class="j-fixed-header"><view class="j-fixed-heading" style="height:{{barHeight}}px;padding-right:{{navSide}}px;"><text class="j-eyebrow">PAGES OF US</text><text class="j-page-title">我们的生活手账</text></view></view>
|
||||
<scroll-view class="page-bg j-main-scroll" scroll-y show-scrollbar="{{false}}" bindscroll="onScroll" scroll-top="{{scrollTop}}" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh">
|
||||
<view class="j-body"><view class="records-action-row"><text class="j-small-title">一起的日常</text><button class="j-button j-button-small" bindtap="showPublish">+ 记录</button></view>
|
||||
<view class="j-tools records-tools"><view class="j-tool-grid"><view bindtap="goToSongs"><image src="/images/stat-music.svg" /><text>歌曲</text></view><view bindtap="goToCheckin"><image src="/images/stat-check.svg" /><text>打卡</text></view><view bindtap="goToPet"><image src="/images/stat-pet.svg" /><text>宠物</text></view><view bindtap="goToPeriod"><image src="/images/stat-j-flower.svg" /><text>姨姨</text></view></view></view>
|
||||
<view class="j-filters"><view wx:for="{{filters}}" wx:key="value" class="j-filter {{activeFilter === item.value ? 'is-active' : ''}}" data-filter="{{item.value}}" bindtap="selectFilter"><image src="/images/{{item.icon}}.svg" mode="aspectFit" /><text>{{item.label}}</text></view></view>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在翻开手账" /><page-state wx:elif="{{loadError}}" kind="error" title="手账暂时没有加载成功" action="重新加载" bind:action="loadStats" />
|
||||
<block wx:else>
|
||||
<block wx:if="{{activeFilter === 'diary'}}"><block wx:for="{{entries}}" wx:key="id"><template is="journal-entry" data="{{entry:item}}" /></block><page-state wx:if="{{!entries.length}}" title="故事,从今天开始" description="慢慢积攒属于你们的生活片段" action="写日记" bind:action="writeDiary" /><view class="j-link j-center" wx:else bindtap="goToDiaryFromDiscover">查看全部日记 →</view></block>
|
||||
<block wx:elif="{{activeFilter === 'album'}}"><view class="j-album-list"><view class="j-album-print" wx:for="{{albums}}" wx:key="id" bindtap="openAlbum" data-id="{{item.id}}"><image wx:if="{{item.cover}}" src="{{item.cover}}" mode="aspectFill" /><view wx:else class="j-album-placeholder"><image src="/images/stat-album.svg" /><text>还没有照片</text></view><text class="j-story-title">{{item.title}}</text><text class="j-caption">{{item.photo_count || 0}} 张照片</text></view></view><page-state wx:if="{{!albums.length}}" title="相册还空着" description="为旅行、周末或日常,留一本相册" action="创建相册" bind:action="createAlbum" /><view class="j-link j-center" wx:else bindtap="goToAlbum">管理全部相册 →</view></block>
|
||||
<block wx:else><view class="j-memory j-memory-list" wx:for="{{memories}}" wx:key="id" bindtap="goToTimeline"><view class="j-stamp"><text>{{item.dateMonth}}月</text><text>{{item.dateDay}}</text></view><view class="j-memory-info"><text class="j-small-title">{{item.title}}</text><text class="j-caption">{{item.date}}</text></view><text class="j-link">›</text></view><page-state wx:if="{{!memories.length}}" title="记住一个特别的日子" description="第一次见面、旅行,还有每个值得期待的日子" action="添加记忆日" bind:action="createMemory" /><view class="j-link j-center" wx:else bindtap="goToTimeline">查看全部记忆日 →</view></block>
|
||||
</block>
|
||||
|
||||
</view></scroll-view>
|
||||
<view wx:if="{{publishOpen}}" class="j-sheet-mask" bindtap="hidePublish" catchtouchmove="preventMove"><view class="j-sheet" catchtap="noop"><view class="j-sheet-grip" /><view class="j-section-head"><text>留下一页日常</text><view class="j-link" bindtap="hidePublish">关闭</view></view><view class="j-publish-grid"><view bindtap="writeDiary"><image src="/images/stat-book.svg" /><text>写日记</text></view><view bindtap="uploadPhotos"><image src="/images/stat-album.svg" /><text>上传照片</text></view><view bindtap="createMemory"><image src="/images/stat-timeline.svg" /><text>记忆日</text></view></view></view></view>
|
||||
</view>
|
||||
@@ -0,0 +1,24 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
.records-tools { padding-top:0; margin:0 0 32rpx; border-top:0; }
|
||||
.records-tools .j-tool-grid { margin-top:12rpx; border-bottom:1rpx solid #E9DDD0; padding-bottom:12rpx; gap:12rpx; }
|
||||
.records-tools .j-tool-grid > view { flex-direction:row; gap:8rpx; background:transparent; border:0; border-radius:0; padding:20rpx 0; color:#352E2A; min-height:80rpx; }
|
||||
.records-tools .j-tool-grid image { width:42rpx; height:42rpx; flex-shrink:0; }
|
||||
|
||||
.records-action-row { display:flex; align-items:center; justify-content:space-between; gap:24rpx; margin-bottom:24rpx; }
|
||||
.records-action-row .j-button { margin:0; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
/* 发布抽屉使用同一套手账图标。 */
|
||||
.j-publish-grid > view > image { width:48rpx; height:48rpx; }
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
/* 分类标签配上同款图标,并保证窄屏仍能放下三个入口。 */
|
||||
.j-filters { gap:24rpx; }
|
||||
.j-filters .j-filter { display:flex; align-items:center; gap:8rpx; flex-shrink:0; }
|
||||
.j-filters .j-filter image { width:34rpx; height:34rpx; flex-shrink:0; }
|
||||
@media (max-width:350px) { .j-filters { gap:16rpx; }.j-filters .j-filter { gap:6rpx; font-size:25rpx; } }
|
||||
@@ -0,0 +1,106 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/history/history.js
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
// 操作类型对应的图标和颜色
|
||||
var ACTION_CONFIG = {
|
||||
login: { icon: '🔓', color: '#82B1FF', label: '登录' },
|
||||
timeline_create: { icon: '📅', color: '#FF8A65', label: '记忆日' },
|
||||
timeline_update: { icon: '✏️', color: '#FF8A65', label: '记忆日' },
|
||||
album_create: { icon: '📷', color: '#66BB6A', label: '相册' },
|
||||
photo_upload: { icon: '🖼️', color: '#66BB6A', label: '照片' },
|
||||
diary_create: { icon: '📝', color: '#FF6B9D', label: '日记' },
|
||||
diary_update: { icon: '✏️', color: '#FF6B9D', label: '日记' },
|
||||
song_add: { icon: '🎵', color: '#AB47BC', label: '歌曲' },
|
||||
checkin_do: { icon: '✅', color: '#42A5F5', label: '打卡' },
|
||||
profile_edit: { icon: '👤', color: '#FFA726', label: '资料' },
|
||||
partner_edit: { icon: '💑', color: '#FFA726', label: '资料' },
|
||||
couple_edit: { icon: '💕', color: '#EC407A', label: '纪念日' },
|
||||
unbind_request: { icon: '💔', color: '#EF5350', label: '解绑' },
|
||||
unbind_agreed: { icon: '💔', color: '#EF5350', label: '解绑' },
|
||||
unbind_rejected: { icon: '💖', color: '#EF5350', label: '解绑' },
|
||||
unbind_cancelled: { icon: '↩️', color: '#EF5350', label: '解绑' },
|
||||
};
|
||||
|
||||
Page({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loadError: false, list: [], page: 1, totalPages: 0, pages: [], loading: true },
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout()); this.loadLogs(); },
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
this.setData({ page: 1, list: [] });
|
||||
await this.loadLogs();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
loadLogs: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
var cp = this.data.page;
|
||||
this.setData({ loading: true });
|
||||
|
||||
try {
|
||||
const res = await api.activityLogList({ page: cp, page_size: 20 });
|
||||
var items = (res.items || []).map(function(item) {
|
||||
item.created_at = item.created_at ? item.created_at.substring(0, 16).replace('T', ' ') : '';
|
||||
var cfg = ACTION_CONFIG[item.action_type] || { icon: '📌', color: '#999', label: '' };
|
||||
item.action_icon = cfg.icon;
|
||||
item.action_color = cfg.color;
|
||||
item.action_label = cfg.label;
|
||||
return item;
|
||||
});
|
||||
var tp = res.total_pages || 1;
|
||||
var pages = [];
|
||||
if (tp <= 7) {
|
||||
for (var i = 1; i <= tp; i++) { pages.push({ num: i, dot: false }); }
|
||||
} else {
|
||||
pages.push({ num: 1, dot: false });
|
||||
pages.push({ num: 2, dot: false });
|
||||
pages.push({ num: 3, dot: false });
|
||||
if (cp > 4) pages.push({ num: 0, dot: true });
|
||||
pages.push({ num: tp - 2, dot: false });
|
||||
pages.push({ num: tp - 1, dot: false });
|
||||
pages.push({ num: tp, dot: false });
|
||||
}
|
||||
var pageData = {
|
||||
list: items,
|
||||
page: cp,
|
||||
totalPages: tp,
|
||||
pages: pages,
|
||||
loading: false
|
||||
};
|
||||
this.setData(pageData);
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
this.setData({ loading: false });
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
goPage: function(e) {
|
||||
var p = parseInt(e.currentTarget.dataset.page);
|
||||
if (p === this.data.page) return;
|
||||
this.setData({ page: p });
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
goPrev: function() {
|
||||
if (this.data.page <= 1) return;
|
||||
this.setData({ page: this.data.page - 1 });
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
goNext: function() {
|
||||
if (this.data.page >= this.data.totalPages) return;
|
||||
this.setData({ page: this.data.page + 1 });
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
goToSettings: function() { wx.navigateTo({ url: '/pages/settings/settings' }); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"disableScroll": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/history/history.wxml - 情侣操作记录(优化版) -->
|
||||
<app-nav title="操作记录" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" style="height:100vh;">
|
||||
<view class="container">
|
||||
<!-- 加载状态 -->
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载操作记录" /><page-state wx:elif="{{loadError}}" kind="error" title="操作记录加载失败" action="重试" bind:action="loadLogs" />
|
||||
<block wx:elif="{{list.length > 0}}">
|
||||
<view class="log-list">
|
||||
<view class="log-item" wx:for="{{list}}" wx:key="id">
|
||||
<view class="log-icon-wrap" style="background:{{item.action_color}}20;">
|
||||
<text class="log-icon">{{item.action_icon}}</text>
|
||||
</view>
|
||||
<view class="log-card">
|
||||
<view class="log-header">
|
||||
<view class="log-user-row">
|
||||
<text class="log-label" style="color:{{item.action_color}};">{{item.action_label}}</text>
|
||||
<text class="log-user" style="color:{{item.gender == 'male' ? '#82B1FF' : '#FF8A80'}};">{{item.nickname}}</text>
|
||||
</view>
|
||||
<text class="log-time">{{item.created_at}}</text>
|
||||
</view>
|
||||
<view class="log-body">{{item.description}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pagination" wx:if="{{totalPages > 1}}">
|
||||
<text class="page-btn {{page <= 1 ? 'disabled' : ''}}" bindtap="goPrev">‹</text>
|
||||
<block wx:for="{{pages}}" wx:key="num">
|
||||
<text class="page-dot" wx:if="{{item.dot}}">…</text>
|
||||
<text class="page-num {{page == item.num ? 'active' : ''}}" wx:else bindtap="goPage" data-page="{{item.num}}">{{item.num}}</text>
|
||||
</block>
|
||||
<text class="page-btn {{page >= totalPages ? 'disabled' : ''}}" bindtap="goNext">›</text>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="empty-state">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-text">暂无操作记录</text>
|
||||
<text class="empty-desc">双方的操作都会记录在这里 ✨</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
</scroll-view>
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,81 @@
|
||||
/* pages/history/history.wxss - 操作记录优化版 */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
.container { padding-top: 16rpx; padding-bottom: 300rpx; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
/* ===== 加载状态 ===== */
|
||||
.loading-state { text-align:center; padding:160rpx 40rpx; }
|
||||
.loading-icon { font-size:60rpx; display:block; margin-bottom:16rpx; }
|
||||
.loading-text { font-size:26rpx; color:#bbb; }
|
||||
|
||||
/* ===== 操作列表(卡片式) ===== */
|
||||
.log-list { padding: 0 8rpx; }
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.log-icon-wrap {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.log-icon { font-size: 28rpx; }
|
||||
|
||||
.log-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.log-user-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.log-label {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.log-user { font-size: 24rpx; font-weight: 600; }
|
||||
.log-time { font-size: 20rpx; color: #bbb; flex-shrink: 0; }
|
||||
.log-body { font-size: 26rpx; color: #555; line-height: 1.6; }
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination { display:flex; justify-content:center; align-items:center; gap:8rpx; padding:32rpx 0; flex-wrap:wrap; }
|
||||
.page-btn { width:56rpx; height:56rpx; line-height:56rpx; text-align:center; border-radius:50%; background:#fff; color:#FF6B81; font-size:28rpx; font-weight:700; }
|
||||
.page-btn.disabled { color:#ddd; }
|
||||
.page-num { width:56rpx; height:56rpx; line-height:56rpx; text-align:center; border-radius:50%; background:#fff; color:#888; font-size:24rpx; }
|
||||
.page-num.active { background:linear-gradient(135deg,#B74768,#B388FF); color:#fff; font-weight:700; }
|
||||
.page-dot { width:56rpx; height:56rpx; line-height:56rpx; text-align:center; color:#ccc; font-size:24rpx; }
|
||||
|
||||
/* ===== 空状态 ===== */
|
||||
.empty-state { text-align:center; padding:120rpx 40rpx; }
|
||||
.empty-icon { font-size:80rpx; display:block; margin-bottom:24rpx; }
|
||||
.empty-text { font-size:30rpx; color:#888; display:block; margin-bottom:8rpx; }
|
||||
.empty-desc { font-size:24rpx; color:#bbb; }
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,345 @@
|
||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||
let lastLocationReport = 0;
|
||||
let locationReportUser = null;
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/index/index.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const { diaryEntry } = require('../../utils/journal');
|
||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
userId: null,
|
||||
coupleId: null,
|
||||
|
||||
partnerName: '和TA',
|
||||
loveStartDate: '',
|
||||
daysCount: '?',
|
||||
daysLabel: '已注册',
|
||||
datePrefix: '注册于',
|
||||
partner1AvatarUrl: '',
|
||||
partner2AvatarUrl: '',
|
||||
partner1Initial: '',
|
||||
partner2Initial: '',
|
||||
partner1Birthday: '',
|
||||
partner2Birthday: '',
|
||||
partner1Name: '?',
|
||||
partner2Name: '?',
|
||||
partner1Location: '',
|
||||
partner2Location: '',
|
||||
coupleDistance: '',
|
||||
|
||||
stats: {
|
||||
timeline: 0, album: 0, diary: 0,
|
||||
songs: 0, checkin: 0, pet: 0
|
||||
},
|
||||
bgImage: '',
|
||||
scrollTop: 0,
|
||||
refreshing: false,
|
||||
relationshipKnown: false, homeError: false, statsLoading: true, statsError: false, recentDiary: [], recentAlbums: [], upcomingMemory: null
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.setData({
|
||||
userId: wx.getStorageSync('userId'),
|
||||
coupleId: wx.getStorageSync('coupleId'),
|
||||
scrollTop: 0
|
||||
});
|
||||
this.loadBg();
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
resetScrollOnTabShow(this, 'index');
|
||||
const tabBar = this.getTabBar && this.getTabBar();
|
||||
if (tabBar) tabBar.syncActive();
|
||||
var prevCoupleId = this.data.coupleId;
|
||||
var curCoupleId = wx.getStorageSync('coupleId');
|
||||
this.setData({
|
||||
userId: wx.getStorageSync('userId'),
|
||||
coupleId: curCoupleId
|
||||
});
|
||||
// 检查是否已被解绑(之前有 coupleId 但被清除了)
|
||||
if (prevCoupleId && !curCoupleId) {
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
return;
|
||||
}
|
||||
this.loadBg();
|
||||
this.loadData();
|
||||
this.fetchIpLocation();
|
||||
},
|
||||
|
||||
// 保存滚动位置
|
||||
onScroll: function(e) {
|
||||
this._scrollTop = e.detail.scrollTop;
|
||||
saveScrollPosition('index', e.detail.scrollTop);
|
||||
},
|
||||
|
||||
_loadingData: false,
|
||||
_daysSince: function(dateStr) {
|
||||
if (!dateStr) return '?';
|
||||
var parts = dateStr.substring(0, 10).split('-').map(Number);
|
||||
if (parts.length !== 3 || parts.some(isNaN) || parts[0] < 1970 ||
|
||||
parts[1] < 1 || parts[1] > 12 || parts[2] < 1 || parts[2] > 31) return '?';
|
||||
var start = new Date(parts[0], parts[1] - 1, parts[2]);
|
||||
if (start.getFullYear() !== parts[0] || start.getMonth() !== parts[1] - 1 ||
|
||||
start.getDate() !== parts[2]) return '?';
|
||||
var now = new Date();
|
||||
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
return Math.max(0, Math.round((today - start) / 86400000));
|
||||
},
|
||||
loadData: async function() {
|
||||
var self = this;
|
||||
// 防止并发重复加载
|
||||
if (this._loadingData) return;
|
||||
this._loadingData = true;
|
||||
this.setData({ homeError: false });
|
||||
try {
|
||||
const info = await api.coupleInfo();
|
||||
this.setData({userId: wx.getStorageSync('userId')});
|
||||
this.setData({ relationshipKnown: true });
|
||||
// 未绑定情侣时显示用户注册时间和默认信息
|
||||
if (info.not_bound) {
|
||||
wx.removeStorageSync('coupleId');
|
||||
var p1 = info.partner1 || {};
|
||||
let daysCount = '?';
|
||||
let loveStartDate = '';
|
||||
if (info.love_start_date) {
|
||||
daysCount = this._daysSince(info.love_start_date);
|
||||
var parts = info.love_start_date.split('-');
|
||||
loveStartDate = parseInt(parts[0]) + '年' + parseInt(parts[1]) + '月' + parseInt(parts[2]) + '日';
|
||||
}
|
||||
this.setData({
|
||||
coupleId: null,
|
||||
partner1AvatarUrl: getImageUrl(p1.avatar),
|
||||
partner1Name: p1.nickname || '未命名',
|
||||
partner1Initial: (p1.nickname || '未命名')[0],
|
||||
partner1Birthday: '',
|
||||
partner1Location: p1.location || '',
|
||||
partner2AvatarUrl: '',
|
||||
partner2Name: '未绑定',
|
||||
partner2Initial: '?',
|
||||
partner2Birthday: '',
|
||||
partner2Location: '',
|
||||
daysCount: daysCount,
|
||||
daysLabel: '已注册',
|
||||
datePrefix: '注册于',
|
||||
loveStartDate: loveStartDate,
|
||||
coupleDistance: '',
|
||||
stats: { timeline: 0, album: 0, diary: 0, songs: 0, checkin: 0, pet: 0 }
|
||||
});
|
||||
await this.loadStats();
|
||||
return;
|
||||
}
|
||||
// 同步情侣ID:红包/邀请创建后分享人刷新即可识别
|
||||
if (info.id) {
|
||||
var prevCid = wx.getStorageSync('coupleId');
|
||||
if (prevCid != info.id) {
|
||||
wx.setStorageSync('coupleId', info.id);
|
||||
}
|
||||
}
|
||||
const myUid = this.data.userId;
|
||||
var p1 = info.partner1 || {};
|
||||
var p2 = info.partner2 || {};
|
||||
|
||||
if (p2.id == myUid) {
|
||||
var tmp = p1; p1 = p2; p2 = tmp;
|
||||
}
|
||||
|
||||
let daysCount = '?';
|
||||
let loveStartDate = '';
|
||||
var togetherStart = info.love_start_date || (info.created_at || '').substring(0, 10);
|
||||
if (togetherStart) {
|
||||
daysCount = this._daysSince(togetherStart);
|
||||
var parts = togetherStart.split('-');
|
||||
loveStartDate = parseInt(parts[0]) + '年' + parseInt(parts[1]) + '月' + parseInt(parts[2]) + '日';
|
||||
}
|
||||
|
||||
function getBirthdayText(birthday) {
|
||||
if (!birthday) return '';
|
||||
var t = new Date(); t.setHours(0,0,0,0);
|
||||
var p = birthday.split('-');
|
||||
if (p.length !== 3) return '';
|
||||
var nb = new Date(t.getFullYear(), parseInt(p[1])-1, parseInt(p[2]));
|
||||
if (nb < t) nb.setFullYear(nb.getFullYear()+1);
|
||||
var c = Math.ceil((nb - t) / 86400000);
|
||||
if (c === 0) return '今天!';
|
||||
if (c === 1) return '1天';
|
||||
if (!Number.isFinite(c)) return '';
|
||||
return c + '天';
|
||||
}
|
||||
|
||||
// 双方相距距离(后端基于双方经纬度用 Haversine 公式计算,单位:公里)
|
||||
var coupleDistance = '';
|
||||
if (info.distance !== null && info.distance !== undefined && info.distance !== '') {
|
||||
var d = parseFloat(info.distance);
|
||||
if (!isNaN(d)) {
|
||||
coupleDistance = d < 1
|
||||
? '相距 ' + Math.round(d * 1000) + ' 米'
|
||||
: '相距 ' + d.toFixed(1) + ' 公里';
|
||||
}
|
||||
}
|
||||
|
||||
var dn = {
|
||||
coupleId: info.id,
|
||||
daysCount: daysCount,
|
||||
daysLabel: '我们在一起',
|
||||
datePrefix: '始于',
|
||||
loveStartDate: loveStartDate,
|
||||
partner1AvatarUrl: getImageUrl(p1.avatar),
|
||||
partner2AvatarUrl: getImageUrl(p2.avatar),
|
||||
partner1Name: p1.nickname || p1.username || '?',
|
||||
partner2Name: p2.nickname || p2.username || '?',
|
||||
partner1Initial: (p1.nickname || p1.username || '?')[0],
|
||||
partner2Initial: (p2.nickname || p2.username || '?')[0],
|
||||
partner1Birthday: getBirthdayText(p1.birthday),
|
||||
partner2Birthday: getBirthdayText(p2.birthday),
|
||||
partner1Location: (p1.location && !/^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/.test(p1.location)) ? p1.location : '',
|
||||
partner2Location: (p2.location && !/^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/.test(p2.location)) ? p2.location : '',
|
||||
coupleDistance: coupleDistance
|
||||
};
|
||||
|
||||
this.setData(dn);
|
||||
await this.loadStats();
|
||||
|
||||
} catch (e) {
|
||||
this.setData({ homeError: true, relationshipKnown: !!this._hasCachedData, statsLoading: false, statsError: true });
|
||||
console.error(e);
|
||||
|
||||
} finally {
|
||||
this._loadingData = false;
|
||||
}
|
||||
},
|
||||
|
||||
loadStats: async function() {
|
||||
this.setData({ statsLoading: !this._hasCachedData, statsError: false });
|
||||
try {
|
||||
const [timeline, album, diary, songs, checkin, pet] = await Promise.all([
|
||||
api.timelineList(), api.albumList(), api.diaryList(),
|
||||
api.songsList(), api.checkinList(),
|
||||
this.data.coupleId ? api.petInfo() : Promise.resolve({ has_pet: false })
|
||||
]);
|
||||
var photoCount = album && album.photo_total != null ? Number(album.photo_total) : itemsOf(album).reduce((sum,item)=>sum+(Number(item.photo_count)||0),0);
|
||||
var petLevel = 0;
|
||||
if (pet && pet.has_pet && pet.pet) {
|
||||
petLevel = parseInt(pet.pet.level || 0);
|
||||
}
|
||||
var recentDiary = ((diary && diary.items) || []).slice(0, 1).map(diaryEntry);
|
||||
var today = new Date(); today.setHours(0,0,0,0);
|
||||
var future = itemsOf(timeline).filter(function(item) { return item.event_date && new Date(item.event_date.replace(/-/g, '/')) >= today; }).sort(function(a,b) { return a.event_date.localeCompare(b.event_date); });
|
||||
var next = timeline && !Array.isArray(timeline) && 'upcoming' in timeline ? timeline.upcoming : future[0];
|
||||
this.setData({
|
||||
recentDiary: recentDiary,
|
||||
recentAlbums: itemsOf(album).filter(function(item) { return !!item.cover; }).slice(0,3).map(function(item) { return { id:item.id, title:item.title || '我们的相册', cover:getImageUrl(item.cover) }; }),
|
||||
upcomingMemory: next ? { title: next.title || '下一个记忆日', date: next.event_date, dateMonth: next.event_date.slice(5,7), dateDay: next.event_date.slice(8,10), days: Math.ceil((new Date(next.event_date.replace(/-/g, '/')) - today) / 86400000) } : null,
|
||||
stats: {
|
||||
timeline: totalOf(timeline),
|
||||
album: photoCount,
|
||||
diary: (diary && diary.total) ? diary.total : 0,
|
||||
songs: totalOf(songs),
|
||||
checkin: (checkin && checkin.checkin_log_total !== undefined) ? checkin.checkin_log_total : ((checkin && checkin.items) ? checkin.items.length : (Array.isArray(checkin) ? checkin.length : 0)),
|
||||
pet: petLevel
|
||||
}
|
||||
});
|
||||
} catch (e) { console.error(e); this.setData({ statsError: true }); } finally { this.setData({ statsLoading: false }); }
|
||||
},
|
||||
|
||||
// 获取本机经纬度并上报后端解析地址(后端按距离判断是否重新解析,超过5km才更新),并刷新首页双方地址显示
|
||||
fetchIpLocation: function() {
|
||||
const locationUser = wx.getStorageSync('userId');
|
||||
if (locationReportUser === locationUser && Date.now() - lastLocationReport < 300000) return;
|
||||
locationReportUser = locationUser;
|
||||
lastLocationReport = Date.now();
|
||||
var self = this;
|
||||
var report = function(lat, lng) {
|
||||
var data = {};
|
||||
if (lat && lng) { data.lat = lat; data.lng = lng; }
|
||||
api.updateIpLocation(data).then(function(resp) {
|
||||
if (resp && resp.location) {
|
||||
self.loadData();
|
||||
}
|
||||
}).catch(function() { lastLocationReport = 0; });
|
||||
};
|
||||
// 优先用模糊定位获取经纬度(getFuzzyLocation 与 getLocation 互斥,统一用模糊定位)
|
||||
if (wx.getFuzzyLocation) {
|
||||
wx.getFuzzyLocation({
|
||||
type: 'gcj02',
|
||||
success: function(res) {
|
||||
report(res.latitude, res.longitude);
|
||||
},
|
||||
fail: function() {
|
||||
report(0, 0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
report(0, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// 背景图以接口返回为准
|
||||
loadBg: function() {
|
||||
var self = this;
|
||||
return api.getBgConfig().then(function(cfg) {
|
||||
self.setData({ bgImage: cfg && cfg.image ? getImageUrl(cfg.image) : '' });
|
||||
}).catch(function(e){ console.warn('loadBg error:', e && (e.error || e._statusCode || e)); });
|
||||
},
|
||||
|
||||
// 跳转前保存当前滚动位置
|
||||
_savePosBeforeLeave: function() {
|
||||
saveScrollPosition('index', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
},
|
||||
|
||||
// 页面跳转
|
||||
goToDiary: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/diary/diary' }); },
|
||||
goToProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); },
|
||||
goToTimeline: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/timeline/timeline' }); },
|
||||
goToAlbum: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/albums/albums' }); },
|
||||
goToSongs: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/lovewords/lovewords' }); },
|
||||
goToCheckin: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/punch/punch' }); },
|
||||
goToPet: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/pet/pet' }); },
|
||||
goToPeriod: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/period/period' }); },
|
||||
bindPartner: function() { getApp().globalData.openProfileBind = true; wx.switchTab({ url: '/pages/profile/profile' }); },
|
||||
openJournalEntry: function(e) { wx.navigateTo({ url: '/pages/diary_detail/diary_detail?id=' + e.currentTarget.dataset.id }); },
|
||||
writeDiary: function() { wx.navigateTo({ url: '/pages/publish/publish?type=diary' }); },
|
||||
openAlbumPreview: function(e) { wx.navigateTo({ url: '/pages/album/album?album_id=' + e.currentTarget.dataset.id }); },
|
||||
openRecentDiary: function(e) { wx.navigateTo({ url: '/pages/diary_detail/diary_detail?id=' + e.currentTarget.dataset.id }); },
|
||||
showMore: function() {
|
||||
wx.switchTab({ url: '/pages/discover/discover' });
|
||||
},
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadData();
|
||||
this.loadBg();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
onRefresh: async function() {
|
||||
this.setData({ refreshing: true });
|
||||
try {
|
||||
await this.loadData();
|
||||
this.loadBg();
|
||||
} catch (e) {
|
||||
console.error('下拉刷新失败', e);
|
||||
} finally {
|
||||
this.setData({ refreshing: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 分享到好友
|
||||
onShareAppMessage: function() {
|
||||
return {
|
||||
title: '让所有的爱有迹可循',
|
||||
path: '/pages/index/index',
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
},
|
||||
// 分享到朋友圈
|
||||
onShareTimeline: function() {
|
||||
return {
|
||||
title: '让所有的爱有迹可循',
|
||||
query: '',
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
}
|
||||
}, {"loadData": ["coupleInfo", "timelineList", "albumList", "diaryList", "songsList", "checkinList", "petInfo", "getBgConfig"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<import src="../../templates/journal.wxml" />
|
||||
<launch-transition />
|
||||
<view class="app-shell {{bgImage ? 'home-has-background' : ''}}" style="{{layoutStyle}}">
|
||||
<view wx:if="{{bgImage}}" class="j-background"><image src="{{bgImage}}" mode="aspectFill" /><view class="j-background-wash" /></view>
|
||||
<view class="home-fixed-header"><view class="home-fixed-heading" style="height:{{barHeight}}px;padding-right:{{navSide}}px;"><text class="j-eyebrow">OUR LITTLE WORLD</text><view class="home-handwritten-title"><text class="j-page-title">让所有的爱有迹可循</text></view></view></view>
|
||||
<scroll-view class="page-bg home-scroll" scroll-y show-scrollbar="{{false}}" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh" bindscroll="onScroll" scroll-top="{{scrollTop}}">
|
||||
<view class="j-body home-body">
|
||||
<page-state wx:if="{{homeError}}" kind="error" title="首页加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
|
||||
<page-state wx:elif="{{!relationshipKnown}}" kind="loading" title="正在打开你的空间" />
|
||||
<view wx:else class="j-cover">
|
||||
<view class="j-tape" /><view class="j-row"><text class="j-eyebrow">{{daysLabel}}</text><image class="j-cover-heart" src="/images/home-cover-heart.svg" /></view>
|
||||
<view class="j-days"><text class="j-days-number">{{daysCount}}</text><text class="j-days-unit">天</text></view><text class="j-cover-date" wx:if="{{loveStartDate}}">{{datePrefix}} {{loveStartDate}}</text>
|
||||
<view class="home-couple-panel">
|
||||
<view class="home-couple-identity">
|
||||
<view class="home-couple-avatars">
|
||||
<view class="j-avatar"><image wx:if="{{partner1AvatarUrl}}" src="{{partner1AvatarUrl}}" mode="aspectFill" /><text wx:else>{{partner1Initial}}</text></view>
|
||||
<view class="j-avatar j-avatar-partner" wx:if="{{coupleId}}"><image wx:if="{{partner2AvatarUrl}}" src="{{partner2AvatarUrl}}" mode="aspectFill" /><text wx:else>{{partner2Initial}}</text></view>
|
||||
</view>
|
||||
<text class="home-couple-names">{{partner1Name}}{{coupleId ? ' & ' + partner2Name : ''}}</text>
|
||||
<text wx:if="{{coupleDistance}}" class="home-distance-pill">{{coupleDistance}}</text>
|
||||
</view>
|
||||
<view class="home-couple-locations" wx:if="{{partner1Location || partner2Location}}">
|
||||
<view class="home-location-row" wx:if="{{partner1Location}}"><text class="home-location-label">我</text><text class="home-location-text">{{partner1Location}}</text></view>
|
||||
<view class="home-location-row" wx:if="{{partner2Location && coupleId}}"><text class="home-location-label home-location-partner">TA</text><text class="home-location-text">{{partner2Location}}</text></view>
|
||||
</view>
|
||||
<view class="home-birthday-row" wx:if="{{partner1Birthday || partner2Birthday}}">
|
||||
<text class="home-birthday-item" wx:if="{{partner1Birthday}}">🎂 我的生日{{partner1Birthday === '今天!' ? '今天' : partner1Birthday}}</text>
|
||||
<text class="home-birthday-item" wx:if="{{partner2Birthday}}">🎂 TA的生日{{partner2Birthday === '今天!' ? '今天' : partner2Birthday}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="j-invite" wx:if="{{relationshipKnown && !coupleId}}" bindtap="bindPartner"><view><text class="j-small-title">邀请另一半,共写这本手账</text><text class="j-caption">绑定后,一起收藏日常</text></view><text class="j-outline-label">邀请</text></view>
|
||||
<view class="j-memory" wx:if="{{upcomingMemory && !homeError}}" bindtap="goToTimeline"><view class="j-stamp"><text>{{upcomingMemory.dateMonth}}月</text><text>{{upcomingMemory.dateDay}}</text></view><view class="j-memory-info"><text class="j-small-title">下一次值得期待</text><text class="j-caption">{{upcomingMemory.title}}</text></view><view class="j-countdown"><text>{{upcomingMemory.days}}</text><text>{{upcomingMemory.days === 0 ? '今天' : '天后'}}</text></view></view>
|
||||
<view class="j-section"><view class="j-section-head"><text>最近的一页</text><view class="j-link" bindtap="showMore">翻翻手账 →</view></view>
|
||||
<page-state wx:if="{{statsLoading}}" kind="loading" title="正在加载记录" /><page-state wx:elif="{{statsError}}" kind="error" title="记录暂时没有加载成功" action="重新加载" bind:action="loadStats" />
|
||||
<block wx:elif="{{recentDiary.length}}"><block wx:for="{{recentDiary}}" wx:key="id"><template is="journal-entry" data="{{entry:item}}" /></block></block>
|
||||
<page-state wx:else title="故事,从今天开始" description="一句心情、一张照片,都可以成为第一页" action="留下第一篇" bind:action="writeDiary" />
|
||||
</view>
|
||||
<view class="j-section" wx:if="{{recentAlbums.length && !statsError}}"><view class="j-section-head"><text>相册里的片段</text><view class="j-link" bindtap="goToAlbum">全部相册 →</view></view><view class="j-photo-strip"><view class="j-photo-print" wx:for="{{recentAlbums}}" wx:key="id" data-id="{{item.id}}" bindtap="openAlbumPreview"><image src="{{item.cover}}" mode="aspectFill" /><text>{{item.title}}</text></view></view></view>
|
||||
</view></scroll-view>
|
||||
</view>
|
||||
@@ -0,0 +1,57 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* 首页标题与系统胶囊位于同一行,状态栏和右侧空间按设备计算。 */
|
||||
.home-fixed-header { position:fixed; top:0; left:0; right:0; z-index:900; padding:var(--status-height,20px) 0 0; margin:0; text-align:left; box-sizing:border-box; }
|
||||
.home-scroll { position:fixed; top:var(--nav-height,64px); left:0; right:0; height:calc(100vh - var(--nav-height,64px)); padding-top:0!important; }
|
||||
.home-body { padding-top:48rpx; }
|
||||
.home-fixed-heading { display:flex; flex-direction:column; align-items:flex-start; justify-content:center; box-sizing:border-box; padding-left:44rpx; }
|
||||
.home-fixed-heading .j-eyebrow { font-size:20rpx; line-height:1.2; letter-spacing:2rpx; white-space:nowrap; }
|
||||
/* 用楷体的自然笔画呈现手写感,不对文字做倾斜或旋转。 */
|
||||
.home-handwritten-title { position:relative; flex-shrink:0; margin-top:5rpx; }
|
||||
.home-handwritten-title::before { content:''; position:absolute; left:-4rpx; right:-5rpx; bottom:2rpx; height:12rpx; background:rgba(225,133,159,.22); border-radius:8rpx 3rpx 10rpx 5rpx; }
|
||||
.home-fixed-heading .home-handwritten-title .j-page-title { position:relative; font-family:'Kaiti SC','STKaiti','KaiTi',cursive; font-size:34rpx; font-weight:400; font-style:normal; line-height:1.35; letter-spacing:1rpx; margin:0; white-space:nowrap; color:#64424D; }
|
||||
|
||||
@media (max-width:350px) { .home-fixed-heading { padding-left:32rpx; } }
|
||||
|
||||
/* 在一起天数使用参考图中的亮粉色。 */
|
||||
.home-body .j-days-number { color:#F47FA5; }
|
||||
|
||||
/* 身份、地址、距离与生日分层呈现,长地址允许完整换行。 */
|
||||
.home-couple-panel { margin-top:40rpx; padding-top:28rpx; border-top:1rpx dashed #E9DDD0; }
|
||||
.home-couple-identity { display:flex; align-items:center; gap:12rpx; }
|
||||
.home-couple-avatars { display:flex; align-items:center; flex-shrink:0; }
|
||||
.home-couple-avatars .j-avatar { width:64rpx; height:64rpx; }
|
||||
.home-couple-avatars .j-avatar-partner { margin-left:-16rpx; }
|
||||
.home-couple-names { flex:1; min-width:0; font-size:26rpx; line-height:1.5; color:#64424D; overflow-wrap:break-word; }
|
||||
.home-couple-locations { margin-top:22rpx; }
|
||||
.home-location-row { display:flex; align-items:flex-start; gap:14rpx; margin-top:12rpx; }
|
||||
.home-location-label { flex-shrink:0; width:42rpx; padding:3rpx 0; border-radius:10rpx; text-align:center; font-size:19rpx; line-height:1.5; color:#B74768; background:#F8E6ED; }
|
||||
.home-location-partner { color:#526B57; background:#E4EBE3; }
|
||||
.home-location-text { flex:1; min-width:0; padding-top:2rpx; font-size:23rpx; line-height:1.6; color:#85776D; overflow-wrap:break-word; word-break:break-all; }
|
||||
.home-distance-pill { display:inline-block; flex-shrink:0; white-space:nowrap; padding:8rpx 14rpx; border-radius:24rpx; background:#FBE4ED; color:#B74768; font-size:22rpx; line-height:1.5; }
|
||||
.home-birthday-row { display:flex; flex-wrap:wrap; align-items:center; gap:8rpx 28rpx; margin-top:18rpx; color:#A65A73; font-size:22rpx; line-height:1.6; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
/* 自定义照片铺满首页,降低洗白遮罩;卡片透出照片且保留阅读底色。 */
|
||||
.home-has-background .j-background { z-index:0; pointer-events:none; }
|
||||
.home-has-background .j-background > image { display:block; }
|
||||
.home-has-background .j-background-wash {
|
||||
background:linear-gradient(180deg,rgba(253,228,236,.24) 0%,rgba(255,240,245,.32) 48%,rgba(255,247,250,.42) 100%);
|
||||
}
|
||||
.home-has-background .home-scroll { z-index:1; }
|
||||
.home-has-background .home-fixed-header { background:rgba(253,228,236,.72); }
|
||||
.home-has-background .j-cover {
|
||||
background:rgba(255,253,248,.78);
|
||||
border-color:rgba(255,253,248,.7);
|
||||
}
|
||||
.home-has-background .j-story,
|
||||
.home-has-background .j-memory,
|
||||
.home-has-background .j-invite { background:rgba(255,253,248,.9); }
|
||||
.home-has-background .j-section-head { text-shadow:0 1rpx 5rpx rgba(255,253,248,.85); }
|
||||
|
||||
.home-has-background .home-couple-panel { padding:24rpx; border:1rpx solid rgba(233,221,208,.65); border-radius:24rpx; background:rgba(255,253,248,.82); }
|
||||
@@ -0,0 +1,127 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/lovewords/lovewords.js - 一起听过的歌
|
||||
const { api } = require('../../utils/request');
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
list: [],
|
||||
showModal: false,
|
||||
isEdit: false,
|
||||
editId: '',
|
||||
form: { song_name: '', artist: '', note: '' }
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.loadData();
|
||||
},
|
||||
onShow: function() { this.loadData(); },
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadData();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
onShareAppMessage: function() {
|
||||
return { title: '让所有的爱有迹可循', path: '/pages/lovewords/lovewords', 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 {
|
||||
const list = await api.songsList({page:ticket.page,page_size:20});
|
||||
const items = itemsOf(list).map(function(item) {
|
||||
if (item.created_at) {
|
||||
var d = new Date(item.created_at);
|
||||
item.date_month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
item.date_day = String(d.getDate()).padStart(2, '0');
|
||||
}
|
||||
return item;
|
||||
});
|
||||
applyList(this, ticket, list, items, 'list');
|
||||
} catch (e) {
|
||||
failList(this,ticket);
|
||||
console.error(e);
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
}
|
||||
finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
noop: function() {},
|
||||
onSongMenu: function(e) {
|
||||
wx.showActionSheet({ itemList:['编辑歌曲', '删除歌曲'], success:(res) => {
|
||||
if (res.tapIndex === 0) this.editSong(e);
|
||||
else if (res.tapIndex === 1) this.deleteSong(e);
|
||||
}});
|
||||
},
|
||||
deleteSong: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这首歌吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await api.songsDelete({ id: id });
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
this.loadData();
|
||||
} catch (err) { wx.showToast({ title: err.error || '删除失败', icon: 'none' }); }
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 编辑歌曲:打开弹窗并带入原数据
|
||||
editSong: function(e) {
|
||||
var ds = e.currentTarget.dataset;
|
||||
this.setData({
|
||||
showModal: true,
|
||||
isEdit: true,
|
||||
editId: ds.id,
|
||||
form: { song_name: ds.name || '', artist: ds.artist || '', note: ds.note || '' }
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 添加/编辑弹窗 =====
|
||||
showModal: function() {
|
||||
this.setData({ showModal: true, isEdit: false, editId: '', form: { song_name: '', artist: '', note: '' } });
|
||||
},
|
||||
hideModal: function() { this.setData({ showModal: false, isEdit: false, editId: '' }); },
|
||||
onSongNameInput: function(e) { this.setData({ 'form.song_name': e.detail.value }); },
|
||||
onArtistInput: function(e) { this.setData({ 'form.artist': e.detail.value }); },
|
||||
onNoteInput: function(e) { this.setData({ 'form.note': e.detail.value }); },
|
||||
|
||||
submit: async function() {
|
||||
var form = this.data.form;
|
||||
if (!form.song_name || !form.artist) {
|
||||
wx.showToast({ title: '请填写歌曲名和歌手名', icon: 'none' }); return;
|
||||
}
|
||||
try {
|
||||
if (this.data.isEdit && this.data.editId) {
|
||||
await api.songsUpdate({ id: this.data.editId, song_name: form.song_name, artist: form.artist, note: form.note || '' });
|
||||
wx.showToast({ title: '已修改 ✏️', icon: 'success' });
|
||||
} else {
|
||||
await api.songsAdd(form);
|
||||
wx.showToast({ title: '收藏成功 🎵', icon: 'success' });
|
||||
}
|
||||
this.hideModal();
|
||||
this.loadData();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '保存失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
goToPublish: function() {
|
||||
this.showModal();
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, 'loadData'), {"loadData": ["songsList"]}));
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/lovewords/lovewords.wxml - 一起听过的歌 -->
|
||||
<app-nav title="一起听的歌" back="{{true}}" />
|
||||
|
||||
|
||||
|
||||
<scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="c-body"><view class="c-heading j-row"><view><text class="j-eyebrow">OUR SOUNDTRACK</text><text class="j-page-title">一起听过的旋律</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 添加</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="song-list" wx:if="{{!loading && !loadError && list.length}}">
|
||||
<view class="song-item" wx:for="{{list}}" wx:key="id">
|
||||
<view class="song-number">{{index < 9 ? '0' + (index + 1) : index + 1}}</view>
|
||||
<view class="song-content">
|
||||
<text class="song-name">{{item.song_name}}</text><text class="song-artist">{{item.artist}}</text>
|
||||
<text class="song-note" wx:if="{{item.note}}">{{item.note}}</text>
|
||||
<view class="song-footer"><text>{{item.nickname || item.author || '我们'}}</text><text wx:if="{{item.date_month}}">{{item.date_month}}/{{item.date_day}}</text></view>
|
||||
</view>
|
||||
<view class="song-menu" bindtap="onSongMenu" data-id="{{item.id}}" data-name="{{item.song_name}}" data-artist="{{item.artist}}" data-note="{{item.note}}" aria-label="歌曲操作"><image src="/images/more.svg" mode="aspectFit" /></view>
|
||||
</view>
|
||||
</view>
|
||||
<page-state wx:if="{{!loading && !loadError && !list.length}}" title="收藏我们的第一首歌" description="让某一段旋律,成为两个人的记忆" action="添加歌曲" bind:action="goToPublish" />
|
||||
</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="modal-overlay" wx:if="{{showModal}}" bindtap="hideModal">
|
||||
<view class="modal" catchtap="noop">
|
||||
<button class="modal-close" bindtap="hideModal"><image src="/images/close.svg" mode="aspectFit" /></button>
|
||||
<text class="title">{{isEdit ? '编辑歌曲' : '收藏歌曲'}}</text>
|
||||
<text class="modal-subtitle">记录我们一起听过的歌</text>
|
||||
<view class="input-group">
|
||||
<label>歌曲名</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="输入歌曲名" value="{{form.song_name}}" bindinput="onSongNameInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>歌手</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="输入歌手名" value="{{form.artist}}" bindinput="onArtistInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>备注(选填)</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="一句话,限60字" maxlength="60" value="{{form.note}}" bindinput="onNoteInput" />
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="submit" hover-class="none">{{isEdit ? '保存修改' : '保存'}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,17 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
@import '../../styles/companion.wxss';
|
||||
.song-list { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:12rpx 32rpx 12rpx 12rpx; padding:0 26rpx; }
|
||||
.song-item { display:flex; align-items:flex-start; gap:18rpx; padding:30rpx 0; border-bottom:1rpx solid #E9DDD0; }.song-item:last-child { border-bottom:0; }
|
||||
.song-number { flex-shrink:0; width:32rpx; padding-top:5rpx; font-size:22rpx; color:#A39386; font-variant-numeric:tabular-nums; }
|
||||
.song-content { min-width:0; flex:1; }.song-name { display:block; font-size:29rpx; line-height:1.5; font-weight:600; color:#352E2A; overflow-wrap:break-word; }
|
||||
.song-artist { display:block; font-size:24rpx; color:#85776D; margin-top:4rpx; line-height:1.5; }
|
||||
.song-note { display:block; margin-top:14rpx; font-size:24rpx; line-height:1.6; color:#65594F; }
|
||||
.song-footer { display:flex; gap:16rpx; flex-wrap:wrap; margin-top:16rpx; font-size:21rpx; color:#A39386; }
|
||||
.song-menu { width:64rpx; height:64rpx; margin-right:-12rpx; display:flex; align-items:center; justify-content:center; flex-shrink:0; }.song-menu image { width:36rpx; height:36rpx; }
|
||||
.modal-overlay { z-index:1200; }.modal-close { background:transparent; }.modal .title { display:block; text-align:left; font-size:32rpx; color:#352E2A; }.modal-subtitle { text-align:left; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/period/period.wxml - 姨姨(美柚风格日历模式) -->
|
||||
<app-nav title="姨姨" back="{{true}}" /><view class="period-loading" wx:if="{{accessLoading}}"><page-state kind="loading" title="正在加载姨姨记录" /></view>
|
||||
|
||||
<view class="j-denied" wx:if="{{!accessLoading && accessDenied}}"><view class="j-denied-card"><image style="width:56rpx;height:56rpx" src="/images/j-flower.svg" /><text class="j-page-title">对方暂时关闭了可见</text><text class="j-caption">这份生理记录目前不可查看,需要对方重新开启后才能访问。</text><button class="j-button" bindtap="loadData">刷新权限</button></view><view class="j-permission" wx:if="{{hasCouple}}"><view><text class="j-small-title">对方可见</text><text class="j-caption">你的开关只决定对方能否查看</text></view><switch checked="{{periodVisible}}" disabled="{{visibilitySaving}}" bindchange="onPeriodVisibleChange" color="#B74768" /></view></view>
|
||||
<view class="j-denied" wx:if="{{!accessLoading && selectionRequired}}"><view class="j-denied-card"><text class="j-page-title">选择保留的生理记录</text><text class="j-caption">绑定前双方都有记录,请选择一份作为当前记录。另一份将删除。</text><button class="j-button" bindtap="choosePeriodSource">选择当前记录</button></view></view>
|
||||
|
||||
<scroll-view class="page-bg" wx:if="{{!accessLoading && !accessDenied && !selectionRequired}}" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="j-body">
|
||||
|
||||
<view class="j-period-summary"><view class="j-row"><text class="j-eyebrow">BODY JOURNAL</text><view class="j-link" bindtap="showPeriodSettings">周期设置</view></view><text class="j-period-title">{{cycles.length ? '周期第 ' + cycleDay + ' 天' : '慢慢了解自己的节奏'}}</text><text class="j-period-description">{{cycles.length ? '周期长度 ' + avgCycle + ' 天 · 距下次预计 ' + predictedDays + ' 天' : '从记录一次生理期开始'}}</text><text wx:if="{{cycles.length}}" class="j-caption">{{periodStatusText}}</text></view>
|
||||
<view wx:if="{{!cycles.length}}" class="j-denied-card" style="margin-bottom:36rpx"><text class="j-small-title">还没有生理期记录</text><text class="j-caption">填写最近一次生理期,开始记录身体状态。</text><button class="j-button" bindtap="showPeriodSettings">设置生理期</button></view>
|
||||
|
||||
<!-- ===== 日历卡片 ===== -->
|
||||
<view class="calendar-card">
|
||||
<!-- 日历导航 -->
|
||||
<view class="calendar-nav">
|
||||
<text class="nav-btn" bindtap="prevMonth">‹</text>
|
||||
<text class="nav-title">{{currentYear}}年{{currentMonth}}月</text>
|
||||
<text class="nav-btn" bindtap="nextMonth">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 日历星期头 -->
|
||||
<view class="calendar-weekdays">
|
||||
<text wx:for="{{weekdays}}" wx:key="index" class="weekday {{item === '日' || item === '六' ? 'weekend' : ''}}">{{item}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 日历格子 -->
|
||||
<view class="calendar-grid">
|
||||
<view wx:for="{{calendarDays}}" wx:key="index" class="calendar-cell {{item.className}}" catchtap="{{(item.isFuture || !item.dateStr) ? '' : 'onDayTap'}}" bindlongpress="{{(item.isFuture || !item.dateStr) ? '' : 'onDayLongPress'}}" data-date="{{item.dateStr}}" data-index="{{index}}">
|
||||
<!-- 爱爱图标(日期数字上方) -->
|
||||
<view wx:if="{{item.hasSex}}" class="sex-icon-top">
|
||||
<text class="sex-icon-top-text">💕</text>
|
||||
<text wx:if="{{item.sexCount > 1}}" class="sex-icon-top-count">{{item.sexCount}}</text>
|
||||
</view>
|
||||
<text class="cell-day">{{item.isToday ? '今' : item.day}}</text>
|
||||
<view class="cell-dots-row">
|
||||
<!-- 经期指示 -->
|
||||
<view wx:if="{{item.periodLevel > 0}}" class="cell-dot period-dot level-{{item.periodLevel}}"></view>
|
||||
<!-- 有记录指示(无经期时显示) -->
|
||||
<view wx:if="{{item.hasRecord && item.periodLevel === 0}}" class="cell-dot record-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历图例(卡片内最下方) -->
|
||||
<view class="calendar-legend">
|
||||
<view class="legend-item"><view class="legend-dot legend-period"></view><text>经期</text></view>
|
||||
<view class="legend-item"><view class="legend-dot legend-ovulation-day"></view><text>排卵日</text></view>
|
||||
<view class="legend-item"><view class="legend-dot legend-ovulation"></view><text>排卵期</text></view>
|
||||
<view class="legend-item"><view class="legend-dot legend-predicted"></view><text>预测经期</text></view>
|
||||
<view class="legend-item"><view class="legend-dot legend-safe"></view><text>安全期</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 快捷记录卡片(8个按钮,日历下方) ===== -->
|
||||
<page-state wx:if="{{!selectedDate}}" title="选择一天,留意身体的感受" description="点选上方日历中的日期,查看或填写当天记录" />
|
||||
<view class="quick-events-card" wx:if="{{selectedDate}}"><view class="section-heading"><text>{{selectedDateLabel}}</text><text class="section-link">{{selectedWeekday}}</text></view>
|
||||
<view class="quick-events-grid">
|
||||
<view class="quick-event-btn {{dailyRecord.period > 0 ? 'active period' : ''}}" catchtap="onPeriodTap">
|
||||
<image class="qe-icon" src="/images/j-flower.svg" />
|
||||
<text class="qe-label">月经</text>
|
||||
<text wx:if="{{dailyRecord.period > 0}}" class="qe-check">✓</text>
|
||||
</view>
|
||||
<view class="quick-event-btn {{sexRecords.length > 0 ? 'active sex' : ''}}" catchtap="showSexRecords">
|
||||
<image class="qe-icon" src="/images/heart.svg" />
|
||||
<text class="qe-label">爱爱</text>
|
||||
<text wx:if="{{sexRecords.length > 0}}" class="sex-count-badge">{{sexRecords.length}}</text>
|
||||
</view>
|
||||
<view class="quick-event-btn {{dailyRecord.symptoms ? 'active' : ''}}" catchtap="showSymptomPicker">
|
||||
<image class="qe-icon" src="/images/j-activity.svg" />
|
||||
<text class="qe-label">症状</text>
|
||||
<text wx:if="{{dailyRecord.symptoms}}" class="qe-check">✓</text>
|
||||
</view>
|
||||
<view class="quick-event-btn {{dailyRecord.mood ? 'active' : ''}}" catchtap="showMoodPicker">
|
||||
<image class="qe-icon" src="/images/j-smile.svg" />
|
||||
<text class="qe-label">心情</text>
|
||||
<text wx:if="{{dailyRecord.mood}}" class="qe-check">✓</text>
|
||||
</view>
|
||||
<view class="quick-event-btn {{dailyRecord.discharge ? 'active' : ''}}" catchtap="onDischargeTap">
|
||||
<image class="qe-icon" src="/images/j-droplet.svg" />
|
||||
<text class="qe-label">白带</text>
|
||||
<text wx:if="{{dailyRecord.discharge}}" class="qe-check">✓</text>
|
||||
</view>
|
||||
<view class="quick-event-btn {{dailyRecord.temperature ? 'active' : ''}}" catchtap="onTempTap">
|
||||
<image class="qe-icon" src="/images/j-thermometer.svg" />
|
||||
<text class="qe-label">体温</text>
|
||||
<text wx:if="{{dailyRecord.temperature}}" class="qe-check">✓</text>
|
||||
</view>
|
||||
<view class="quick-event-btn {{dailyRecord.weight ? 'active' : ''}}" catchtap="onWeightTap">
|
||||
<image class="qe-icon" src="/images/j-scale.svg" />
|
||||
<text class="qe-label">体重</text>
|
||||
<text wx:if="{{dailyRecord.weight}}" class="qe-check">✓</text>
|
||||
</view>
|
||||
<view class="quick-event-btn {{dailyRecord.note ? 'active' : ''}}" catchtap="showNoteInput">
|
||||
<image class="qe-icon" src="/images/book.svg" />
|
||||
<text class="qe-label">备注</text>
|
||||
<text wx:if="{{dailyRecord.note}}" class="qe-check">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="j-permission" wx:if="{{hasCouple}}"><view><text class="j-small-title">对方可见</text><text class="j-caption">关闭后,对方无法查看这份生理记录</text></view><switch checked="{{periodVisible}}" disabled="{{visibilitySaving}}" bindchange="onPeriodVisibleChange" color="#B74768" /></view>
|
||||
<!-- ===== 历史周期列表 ===== -->
|
||||
<view class="section-title">历史周期<text class="section-hint">长按可删除</text></view>
|
||||
<view class="cycle-list" wx:if="{{cycles.length > 0}}">
|
||||
<view class="cycle-card" wx:for="{{cycles}}" wx:key="start_date" bindlongpress="onCycleLongPress" data-id="{{item.id}}" data-start="{{item.start_date}}" data-end="{{item.end_date}}">
|
||||
<view class="cycle-header">
|
||||
<image class="cycle-icon" src="/images/j-flower.svg" style="width:28rpx;height:28rpx" />
|
||||
<text class="cycle-date">{{item.start_date}} ~ {{item.end_date}}</text>
|
||||
</view>
|
||||
<view class="cycle-details">
|
||||
<text class="cycle-detail">周期 {{item.cycle_length}}天</text>
|
||||
<text class="cycle-detail">持续 {{item.period_length}}天</text>
|
||||
<text wx:if="{{item.symptoms}}" class="cycle-detail">{{item.symptoms}}</text>
|
||||
</view>
|
||||
<view wx:if="{{item.note}}" class="cycle-note">{{item.note}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" wx:else>
|
||||
<text class="empty-icon">🌸</text>
|
||||
<text class="empty-title">还没有周期记录</text>
|
||||
<text class="empty-desc">点击上方周期设置,记录最近一次生理期</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<!-- 浮动按钮已移至日历卡片内 -->
|
||||
|
||||
<!-- ===== 姨姨设置弹窗 ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showCycleModal}}" catchtouchmove="preventMove">
|
||||
<scroll-view scroll-y class="modal period-modal settings-modal" show-scrollbar="{{false}}">
|
||||
|
||||
<text class="modal-title">姨姨设置</text>
|
||||
<text class="modal-subtitle">填写上次姨妈信息,帮你推算下次</text>
|
||||
<view class="modal-form">
|
||||
<!-- 持续天数 -->
|
||||
<view class="form-row">
|
||||
<text class="form-label">持续天数</text>
|
||||
<view class="form-stepper">
|
||||
<text class="stepper-btn" catchtap="adjustPeriod" data-dir="-1">−</text>
|
||||
<text class="stepper-value">{{cycleForm.period_length}}天</text>
|
||||
<text class="stepper-btn" catchtap="adjustPeriod" data-dir="1">+</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 上一次来时 -->
|
||||
<view class="form-row">
|
||||
<text class="form-label">上一次来时</text>
|
||||
<picker mode="date" value="{{cycleForm.start_date}}" bindchange="onCycleStartChange">
|
||||
<view class="form-picker">{{cycleForm.start_date}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<!-- 结束时 -->
|
||||
<view class="form-row">
|
||||
<text class="form-label">结束时</text>
|
||||
<picker mode="date" value="{{cycleForm.end_date}}" bindchange="onCycleEndChange">
|
||||
<view class="form-picker">{{cycleForm.end_date}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<!-- 周期长度(用于推算) -->
|
||||
<view class="form-row">
|
||||
<text class="form-label">周期长度</text>
|
||||
<view class="form-stepper">
|
||||
<text class="stepper-btn" catchtap="adjustCycle" data-dir="-1">−</text>
|
||||
<text class="stepper-value">{{cycleForm.cycle_length}}天</text>
|
||||
<text class="stepper-btn" catchtap="adjustCycle" data-dir="1">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推算结果 -->
|
||||
<view class="prediction-box">
|
||||
<text class="prediction-title">📅 推算结果</text>
|
||||
<view class="prediction-row">
|
||||
<text class="prediction-label">下次姨妈</text>
|
||||
<text class="prediction-value">{{cycleForm.predicted_start}} ~ {{cycleForm.predicted_end}}</text>
|
||||
</view>
|
||||
<view class="prediction-row">
|
||||
<text class="prediction-label">预计排卵日</text>
|
||||
<text class="prediction-value ovulation">{{cycleForm.predicted_ovulation_day}}</text>
|
||||
</view>
|
||||
<view class="prediction-row">
|
||||
<text class="prediction-label">排卵期</text>
|
||||
<text class="prediction-value ovulation-range">{{cycleForm.predicted_ovulation_start}} ~ {{cycleForm.predicted_ovulation_end}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 推算公式说明 -->
|
||||
<view class="formula-toggle" bindtap="toggleFormula">
|
||||
<text class="formula-toggle-text">{{showFormula ? '收起' : '查看'}}推算公式</text>
|
||||
<text class="formula-toggle-arrow">{{showFormula ? '▲' : '▼'}}</text>
|
||||
</view>
|
||||
<view class="formula-detail" wx:if="{{showFormula}}">
|
||||
<view class="formula-item">
|
||||
<text class="formula-label">下次姨妈:</text>
|
||||
<text class="formula-text">上次来时 + 周期长度({{cycleForm.cycle_length}}天)</text>
|
||||
</view>
|
||||
<view class="formula-item">
|
||||
<text class="formula-label">下次结束:</text>
|
||||
<text class="formula-text">下次来时 + 持续天数({{cycleForm.period_length}}天) − 1</text>
|
||||
</view>
|
||||
<view class="formula-item">
|
||||
<text class="formula-label">排卵日:</text>
|
||||
<text class="formula-text">下次姨妈开始日 − 14天</text>
|
||||
</view>
|
||||
<view class="formula-item">
|
||||
<text class="formula-label">排卵期:</text>
|
||||
<text class="formula-text">排卵日 ± 3天(共7天)</text>
|
||||
</view>
|
||||
<view class="formula-item">
|
||||
<text class="formula-label">安全期:</text>
|
||||
<text class="formula-text">经期 & 排卵期之外的日期</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 免责声明 -->
|
||||
<view class="disclaimer-box">
|
||||
<text class="disclaimer-icon">⚠️</text>
|
||||
<text class="disclaimer-text">以上推算基于标准周期公式,<text class="disclaimer-bold">仅供参考</text>。实际周期受个人身体状态、作息规律、情绪压力、饮食运动等多种因素影响,可能存在偏差。如有疑问请咨询专业医生。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closeCycleModal">取消</view>
|
||||
<view class="btn btn-primary" bindtap="saveCycle">保存设置</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 经期详情弹窗(量、颜色、疼痛) ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showPeriodModal}}" catchtouchmove="preventMove">
|
||||
<view class="modal period-modal">
|
||||
<view class="modal-heart">🌸</view>
|
||||
<text class="modal-title">月经详情</text>
|
||||
<text class="modal-subtitle">{{selectedDateLabel}} {{selectedWeekday}}</text>
|
||||
|
||||
<!-- 量 -->
|
||||
<view class="period-section">
|
||||
<text class="period-section-label">量</text>
|
||||
<view class="period-chip-group">
|
||||
<view wx:for="{{periodAmountOptions}}" wx:key="value" class="period-chip {{periodForm.amount === item.value ? 'active' : ''}}" catchtap="onPeriodAmount" data-value="{{item.value}}">{{item.label}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 颜色 -->
|
||||
<view class="period-section">
|
||||
<text class="period-section-label">颜色</text>
|
||||
<view class="period-chip-group">
|
||||
<view wx:for="{{periodColorOptions}}" wx:key="value" class="period-chip {{periodForm.color === item.value ? 'active' : ''}}" catchtap="onPeriodColor" data-value="{{item.value}}">
|
||||
<view class="color-dot" style="background:{{item.dot}};"></view>
|
||||
<text>{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 疼痛 -->
|
||||
<view class="period-section">
|
||||
<text class="period-section-label">疼痛</text>
|
||||
<view class="period-chip-group">
|
||||
<view wx:for="{{periodPainOptions}}" wx:key="value" class="period-chip {{periodForm.pain === item.value ? 'active' : ''}}" catchtap="onPeriodPain" data-value="{{item.value}}">{{item.label}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closePeriodModal">取消</view>
|
||||
<view class="btn btn-primary" bindtap="confirmPeriodDetail">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 症状选择弹窗 ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showSymptomModal}}" catchtouchmove="preventMove">
|
||||
<view class="modal period-modal">
|
||||
<view class="modal-heart">🤒</view>
|
||||
<text class="modal-title">选择症状</text>
|
||||
<view class="symptom-grid">
|
||||
<text wx:for="{{symptomOptions}}" wx:key="*this" class="symptom-tag-lg {{symptomMap[item] ? 'active' : ''}}" catchtap="toggleSymptom" data-symptom="{{item}}">{{item}}</text>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closeSymptomModal">取消</view>
|
||||
<view class="btn btn-primary" bindtap="confirmSymptoms">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 心情选择弹窗 ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showMoodModal}}" catchtouchmove="preventMove">
|
||||
<view class="modal period-modal">
|
||||
<view class="modal-heart">😊</view>
|
||||
<text class="modal-title">选择心情</text>
|
||||
<view class="mood-grid">
|
||||
<view wx:for="{{moodOptions}}" wx:key="*this" class="mood-item {{dailyRecord.mood === item.label ? 'active' : ''}}" catchtap="selectMood" data-mood="{{item.label}}">
|
||||
<text class="mood-emoji">{{item.emoji}}</text>
|
||||
<text class="mood-label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closeMoodModal">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 白带详情弹窗(状态、流量、异常) ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showDischargeModal}}" catchtouchmove="preventMove">
|
||||
<view class="modal period-modal">
|
||||
<view class="modal-heart">💧</view>
|
||||
<text class="modal-title">白带详情</text>
|
||||
<text class="modal-subtitle">{{selectedDateLabel}} {{selectedWeekday}}</text>
|
||||
|
||||
<!-- 状态 -->
|
||||
<view class="period-section">
|
||||
<text class="period-section-label">状态</text>
|
||||
<view class="period-chip-group">
|
||||
<view wx:for="{{dischargeStatusOptions}}" wx:key="value" class="period-chip {{dischargeForm.status === item.value ? 'active' : ''}}" catchtap="onDischargeStatus" data-value="{{item.value}}">{{item.label}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 流量 -->
|
||||
<view class="period-section">
|
||||
<text class="period-section-label">流量</text>
|
||||
<view class="period-chip-group">
|
||||
<view wx:for="{{dischargeFlowOptions}}" wx:key="value" class="period-chip {{dischargeForm.flow === item.value ? 'active' : ''}}" catchtap="onDischargeFlow" data-value="{{item.value}}">{{item.label}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 异常(多选) -->
|
||||
<view class="period-section">
|
||||
<text class="period-section-label">异常(可多选)</text>
|
||||
<view class="period-chip-group">
|
||||
<view wx:for="{{dischargeAbnormalOptions}}" wx:key="value" class="period-chip {{dischargeAbnormalMap[item.value] ? 'active' : ''}}" catchtap="onDischargeAbnormal" data-value="{{item.value}}">{{item.label}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closeDischargeModal">取消</view>
|
||||
<view class="btn btn-primary" bindtap="confirmDischargeDetail">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 体温输入弹窗 ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showTempModal}}" catchtouchmove="preventMove">
|
||||
<view class="modal period-modal">
|
||||
<view class="modal-heart">🌡️</view>
|
||||
<text class="modal-title">记录体温</text>
|
||||
<view class="temp-input-row">
|
||||
<input placeholder-class="handwritten-placeholder" class="temp-input" type="digit" placeholder="36.5" value="{{tempValue}}" bindinput="onTempInput" />
|
||||
<text class="temp-unit">°C</text>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closeTempModal">取消</view>
|
||||
<view class="btn btn-primary" bindtap="confirmTemp">保存</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 体重输入弹窗 ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showWeightModal}}" catchtouchmove="preventMove">
|
||||
<view class="modal period-modal">
|
||||
<view class="modal-heart">⚖️</view>
|
||||
<text class="modal-title">记录体重</text>
|
||||
<view class="temp-input-row">
|
||||
<input placeholder-class="handwritten-placeholder" class="temp-input" type="digit" placeholder="50.0" value="{{weightValue}}" bindinput="onWeightInput" />
|
||||
<text class="temp-unit">kg</text>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closeWeightModal">取消</view>
|
||||
<view class="btn btn-primary" bindtap="confirmWeight">保存</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 备注输入弹窗 ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showNoteModal}}" catchtouchmove="preventMove">
|
||||
<view class="modal period-modal">
|
||||
<view class="modal-heart">📝</view>
|
||||
<text class="modal-title">添加备注</text>
|
||||
<textarea placeholder-class="handwritten-placeholder" class="note-textarea" placeholder="记录一下今天的状态..." value="{{noteValue}}" bindinput="onNoteInput" maxlength="500" />
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closeNoteModal">取消</view>
|
||||
<view class="btn btn-primary" bindtap="confirmNote">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 爱爱记录弹窗 ===== -->
|
||||
<view class="modal-overlay" wx:if="{{showSexModal}}" catchtouchmove="preventMove">
|
||||
<view class="modal period-modal sex-modal">
|
||||
<view class="modal-heart">💕</view>
|
||||
<text class="modal-title">爱爱记录</text>
|
||||
<text class="modal-subtitle">{{selectedDateLabel}} {{selectedWeekday}}</text>
|
||||
|
||||
<!-- 已记录列表 -->
|
||||
<scroll-view class="sex-record-list" wx:if="{{sexRecords.length > 0}}" scroll-y show-scrollbar="{{false}}">
|
||||
<view class="sex-record-item" wx:for="{{sexRecords}}" wx:key="id">
|
||||
<view class="sex-record-info">
|
||||
<text class="sex-time">{{item.record_time ? item.record_time.substring(0, 5) : '--:--'}}</text>
|
||||
<text class="sex-measure">{{item.measure || '未知'}}</text>
|
||||
<text wx:if="{{item.remark}}" class="sex-remark">{{item.remark}}</text>
|
||||
</view>
|
||||
<view class="sex-record-time">{{item.record_time || '--:--'}}</view>
|
||||
<view class="sex-record-del" catchtap="deleteSexRecord" data-id="{{item.id}}">
|
||||
<text class="sex-del-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 添加新记录 -->
|
||||
<view class="sex-add-section">
|
||||
<text class="sex-add-title">添加记录</text>
|
||||
|
||||
<!-- 时间选择 -->
|
||||
<view class="sex-form-row">
|
||||
<text class="sex-form-label">时间</text>
|
||||
<picker mode="time" value="{{sexForm.record_time}}" bindchange="onSexTimeChange">
|
||||
<view class="sex-form-picker">{{sexForm.record_time || '选择时间'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 措施选择 -->
|
||||
<text class="sex-form-label" style="margin-top:16rpx;display:block;">措施</text>
|
||||
<view class="sex-measure-grid">
|
||||
<view wx:for="{{measureOptions}}" wx:key="value" class="sex-measure-item {{sexForm.measure === item.value ? 'active' : ''}}" catchtap="onSexMeasureSelect" data-measure="{{item.value}}">
|
||||
<text class="sex-measure-icon">{{item.icon}}</text>
|
||||
<text class="sex-measure-label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="sex-form-row">
|
||||
<text class="sex-form-label">备注</text>
|
||||
<input placeholder-class="handwritten-placeholder" class="sex-form-input" placeholder="选填" value="{{sexForm.remark}}" bindinput="onSexRemarkInput" />
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
<view class="btn btn-secondary" bindtap="closeSexModal">关闭</view>
|
||||
<view class="btn btn-primary" bindtap="confirmSexAdd">添加记录</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/pet/achievements.js
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
achievements: [],
|
||||
unlockedCount: 0,
|
||||
totalCount: 0,
|
||||
progressPercent: 0
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.loadAchievements();
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
this.loadAchievements();
|
||||
},
|
||||
|
||||
loadAchievements: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
const res = await api.petAchievements();
|
||||
let list = res.achievements || [];
|
||||
// 排序:已解锁的按解锁时间倒序(最新获得的排最前),未解锁的排后面
|
||||
list = list.sort(function(a, b) {
|
||||
var aUnlocked = a.unlocked ? 1 : 0;
|
||||
var bUnlocked = b.unlocked ? 1 : 0;
|
||||
if (aUnlocked !== bUnlocked) {
|
||||
return bUnlocked - aUnlocked;
|
||||
}
|
||||
if (aUnlocked && bUnlocked) {
|
||||
var ta = (a.unlocked_at || '').replace(/-/g, '').replace(/[: ]/g, '');
|
||||
var tb = (b.unlocked_at || '').replace(/-/g, '').replace(/[: ]/g, '');
|
||||
return tb.localeCompare(ta);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const unlocked = list.filter(a => a.unlocked);
|
||||
const total = list.length;
|
||||
var pageData = {
|
||||
achievements: list,
|
||||
unlockedCount: unlocked.length,
|
||||
totalCount: total,
|
||||
progressPercent: total > 0 ? Math.round(unlocked.length / total * 100) : 0
|
||||
};
|
||||
this.setData(pageData);
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
console.error(e);
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
goBack: function() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
}, {"loadAchievements": ["petAchievements"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "宠物成就",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/pet/achievements.wxml -->
|
||||
<app-nav title="宠物成就" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="container">
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadAchievements" /><block wx:else>
|
||||
<!-- 成就概览 -->
|
||||
<view class="ach-summary">
|
||||
<view class="ach-deco ach-deco-1">🌟</view>
|
||||
<view class="ach-deco ach-deco-2">🏅</view>
|
||||
<text class="ach-count">{{unlockedCount}}<text class="ach-total">/{{totalCount}}</text></text>
|
||||
<text class="ach-progress-text">已解锁成就</text>
|
||||
<view class="ach-progress-track">
|
||||
<view class="ach-progress-fill" style="width:{{progressPercent}}%;"></view>
|
||||
</view>
|
||||
<text class="ach-percent">{{progressPercent}}%</text>
|
||||
</view>
|
||||
|
||||
<!-- 成就列表 -->
|
||||
<view class="ach-list">
|
||||
<view wx:for="{{achievements}}" wx:key="key" class="ach-card {{item.unlocked?'':'locked'}}">
|
||||
<view class="ach-card-icon">
|
||||
<text>{{item.unlocked ? item.icon : '🔒'}}</text>
|
||||
</view>
|
||||
<view class="ach-card-info">
|
||||
<view class="ach-card-name-row">
|
||||
<text class="ach-card-name">{{item.name}}</text>
|
||||
<text class="ach-card-status {{item.unlocked?'unlocked':'locked-badge'}}">{{item.unlocked ? '已解锁' : '未解锁'}}</text>
|
||||
</view>
|
||||
<text class="ach-card-desc">{{item.desc}}</text>
|
||||
<text wx:if="{{item.unlocked && item.unlocked_at}}" class="ach-card-time">🏅 {{item.unlocked_at}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,187 @@
|
||||
/* pages/pet/achievements.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;
|
||||
padding-top: 196rpx;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
|
||||
/* ===== 顶部导航(与其他页面一致,fixed 定位与微信胶囊对齐) ===== */
|
||||
.top-nav-card {
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #C96A85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 右侧占位,保持标题真正居中 */
|
||||
.nav-spacer {
|
||||
width: 56rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 成就概览 ===== */
|
||||
.ach-summary {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #FFD3E0, #FFB6C6 55%, #FF9AB0);
|
||||
border-radius: 32rpx;
|
||||
padding: 40rpx 40rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
color: #fff;
|
||||
box-shadow: 0 12rpx 30rpx rgba(255, 140, 170, 0.22);
|
||||
}
|
||||
.ach-deco {
|
||||
position: absolute;
|
||||
font-size: 60rpx;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.ach-deco-1 {
|
||||
top: 24rpx;
|
||||
left: 32rpx;
|
||||
}
|
||||
.ach-deco-2 {
|
||||
bottom: 24rpx;
|
||||
right: 32rpx;
|
||||
}
|
||||
.ach-count {
|
||||
font-size: 88rpx;
|
||||
font-weight: 800;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
.ach-total {
|
||||
font-size: 40rpx;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.ach-progress-text {
|
||||
font-size: 26rpx;
|
||||
opacity: 0.85;
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.ach-progress-track {
|
||||
height: 14rpx;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-radius: 999rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ach-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #FFE066, #FFD700);
|
||||
border-radius: 999rpx;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
.ach-percent {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ===== 成就列表 ===== */
|
||||
.ach-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.ach-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(160deg, #FFF8FA 0%, #FFF0F4 100%);
|
||||
border: 2rpx solid rgba(255,190,210,0.3);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 140, 170, 0.08);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.ach-card.locked {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.ach-card-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #FFD3E0, #FFB6C6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
flex-shrink: 0;
|
||||
box-shadow: inset 0 0 0 2rpx rgba(255,107,129,0.15);
|
||||
}
|
||||
.ach-card.locked .ach-card-icon {
|
||||
background: #F5F5F5;
|
||||
box-shadow: inset 0 0 0 2rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.ach-card-info {
|
||||
flex: 1;
|
||||
}
|
||||
.ach-card-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.ach-card-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #5A4A4E;
|
||||
}
|
||||
.ach-card-desc {
|
||||
font-size: 24rpx;
|
||||
color: #B08A8A;
|
||||
display: block;
|
||||
}
|
||||
.ach-card-time {
|
||||
font-size: 20rpx;
|
||||
color: #C9A8A8;
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.ach-card-status {
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ach-card-status.unlocked {
|
||||
background: #E8F5E9;
|
||||
color: #66BB6A;
|
||||
}
|
||||
.ach-card-status.locked-badge {
|
||||
background: #F5F5F5;
|
||||
color: #C9A8A8;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
.adopt-card,.stat-card,.action-card,.dynamics-card,.ach-card { background:#FFFDF8; border:1rpx solid #E9DDD0; box-shadow:none; }.adopt-title,.ach-card-name { color:#352E2A; }.adopt-desc,.ach-card-desc { color:#85776D; }.ach-summary { background:#F4E2E3; color:#943C58; box-shadow:none; }.ach-deco { display:none; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,325 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/pet/pet.js
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
const BREED_NAMES = {
|
||||
'golden': '金毛',
|
||||
'husky': '哈士奇',
|
||||
'corgi': '柯基',
|
||||
'shiba': '柴犬'
|
||||
};
|
||||
|
||||
const BREED_EMOJIS = {
|
||||
'golden': '🐕',
|
||||
'husky': '🐺',
|
||||
'corgi': '🦊',
|
||||
'shiba': '🐶'
|
||||
};
|
||||
|
||||
const STAGE_NAMES = ['', '幼犬期', '少年期', '青年期', '成年期', '终极期'];
|
||||
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false, needsBinding: false,
|
||||
hasPet: false,
|
||||
pet: null,
|
||||
daily: null,
|
||||
achievements: [],
|
||||
dynamics: [],
|
||||
displayDynamics: [],
|
||||
equipped: [],
|
||||
|
||||
// 领养表单
|
||||
breedSelect: 'golden',
|
||||
genderSelect: 1,
|
||||
petName: '',
|
||||
adopting: false,
|
||||
|
||||
// 宠物状态
|
||||
dailyStatus: {},
|
||||
partnerDaily: {},
|
||||
allDone: false,
|
||||
expProgress: 0,
|
||||
expForNextLevel: 0,
|
||||
breedName: '',
|
||||
petEmoji: '',
|
||||
stageName: '',
|
||||
equippedCollar: false,
|
||||
|
||||
// 最近成就
|
||||
recentAchievements: [],
|
||||
achievementCount: 0,
|
||||
|
||||
// 改名
|
||||
showRename: false,
|
||||
renameValue: '',
|
||||
renaming: false,
|
||||
// 动态折叠(默认收起,收起时显示一条)
|
||||
showAllDynamics: false
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.loadPetInfo();
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
this.loadPetInfo();
|
||||
},
|
||||
|
||||
loadPetInfo: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
const relationship = await api.coupleInfo();
|
||||
if (relationship.not_bound) {
|
||||
this.setData({needsBinding:true,hasPet:false,pet:null,daily:null,achievements:[],dynamics:[],displayDynamics:[],showRename:false});
|
||||
return;
|
||||
}
|
||||
this.setData({needsBinding:false});
|
||||
const res = await api.petInfo();
|
||||
if (res.has_pet && res.pet) {
|
||||
const pet = res.pet;
|
||||
const dailyStatus = {};
|
||||
const partnerDaily = {};
|
||||
if (res.daily) {
|
||||
const d = res.daily;
|
||||
const suffix = res.is_user1 ? 'u1' : 'u2';
|
||||
const partnerSuffix = res.is_user1 ? 'u2' : 'u1';
|
||||
['feed', 'water', 'walk', 'bath', 'sleep', 'play'].forEach(a => {
|
||||
dailyStatus[a] = d[a + '_' + suffix] == 1;
|
||||
partnerDaily[a] = d[a + '_' + partnerSuffix] == 1;
|
||||
});
|
||||
}
|
||||
// 获取已装备物品
|
||||
const equipped = res.equipped || [];
|
||||
const equippedCollar = equipped.some(e => e.item_type === 'collar');
|
||||
// 计算最近成就
|
||||
const allAch = this.getAllAchievementDefs();
|
||||
const recentAch = allAch.filter(a => res.achievements.indexOf(a.key) >= 0).slice(0, 5);
|
||||
|
||||
// 计算领养天数
|
||||
var adoptDays = 0;
|
||||
if (pet.adopted_at) {
|
||||
var adoptDate = new Date(pet.adopted_at.replace(/-/g, '/'));
|
||||
if (!isNaN(adoptDate.getTime())) {
|
||||
adoptDays = Math.floor((Date.now() - adoptDate.getTime()) / 86400000);
|
||||
}
|
||||
}
|
||||
var pageData = {
|
||||
hasPet: true,
|
||||
pet: pet,
|
||||
daily: res.daily,
|
||||
achievements: res.achievements,
|
||||
dynamics: this._formatDynamics(res.dynamics || []),
|
||||
showAllDynamics: false,
|
||||
equipped: equipped,
|
||||
dailyStatus: dailyStatus,
|
||||
partnerDaily: partnerDaily,
|
||||
allDone: res.daily ? res.daily.all_done == 1 : false,
|
||||
expProgress: res.exp_progress || 0,
|
||||
expForNextLevel: res.exp_for_next_level || 0,
|
||||
breedName: BREED_NAMES[pet.breed] || '金毛',
|
||||
petEmoji: BREED_EMOJIS[pet.breed] || '🐕',
|
||||
stageName: STAGE_NAMES[pet.stage] || '幼犬期',
|
||||
equippedCollar: equippedCollar,
|
||||
recentAchievements: recentAch,
|
||||
achievementCount: (res.achievements || []).length,
|
||||
adoptDays: adoptDays
|
||||
};
|
||||
|
||||
this.setData(pageData);
|
||||
this._buildDisplayDynamics();
|
||||
|
||||
} else {
|
||||
this.setData({ hasPet: false, pet:null, daily:null, achievements:[], dynamics:[], displayDynamics:[] });
|
||||
}
|
||||
} catch (e) {
|
||||
if (e && e.error === '请先绑定情侣') {
|
||||
this.setData({needsBinding:true,hasPet:false,pet:null,showRename:false});
|
||||
} else {
|
||||
this.setData({loadError:true});
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
// 格式化动态数据:时间转为友好显示,昵称取首字
|
||||
_formatDynamics: function(list) {
|
||||
var now = Date.now();
|
||||
return list.map(function(d) {
|
||||
var timeText = '';
|
||||
if (d.created_at) {
|
||||
var t = new Date(String(d.created_at).replace(/-/g, '/').replace('T', ' '));
|
||||
if (!isNaN(t.getTime())) {
|
||||
var diff = Math.floor((now - t.getTime()) / 1000);
|
||||
if (diff < 60) timeText = '刚刚';
|
||||
else if (diff < 3600) timeText = Math.floor(diff / 60) + '分钟前';
|
||||
else if (diff < 86400) timeText = Math.floor(diff / 3600) + '小时前';
|
||||
else if (diff < 172800) timeText = '昨天';
|
||||
else timeText = (t.getMonth() + 1) + '月' + t.getDate() + '日';
|
||||
}
|
||||
}
|
||||
var initial = d.user_id == 0 ? '🐾' : (d.nickname ? String(d.nickname).trim()[0] : '?');
|
||||
return {
|
||||
user_id: d.user_id,
|
||||
nickname: d.nickname,
|
||||
avatar: d.avatar || '',
|
||||
action: d.action,
|
||||
message: d.message,
|
||||
time_text: timeText,
|
||||
initial: initial
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getAllAchievementDefs: function() {
|
||||
return [
|
||||
{ key: 'first_adopt', name: '初次相遇', icon: '🐣' },
|
||||
{ key: 'level_5', name: '成长之路', icon: '⭐' },
|
||||
{ key: 'level_10', name: '小有成就', icon: '🌟' },
|
||||
{ key: 'level_20', name: '资深铲屎官', icon: '👑' },
|
||||
{ key: 'level_30', name: '终极陪伴', icon: '💎' },
|
||||
{ key: 'stage_2', name: '少年初长成', icon: '🐕' },
|
||||
{ key: 'stage_3', name: '青春活力', icon: '🦮' },
|
||||
{ key: 'stage_4', name: '成熟稳重', icon: '🐩' },
|
||||
{ key: 'stage_5', name: '终极形态', icon: '🦁' },
|
||||
{ key: 'all_done_7', name: '一周全勤', icon: '📅' },
|
||||
{ key: 'all_done_30', name: '月度之星', icon: '🏆' },
|
||||
{ key: 'feed_100', name: '大胃王', icon: '🍖' },
|
||||
{ key: 'walk_50', name: '散步达人', icon: '🚶' },
|
||||
{ key: 'bath_30', name: '爱干净', icon: '🛁' },
|
||||
{ key: 'play_50', name: '玩伴', icon: '🎾' },
|
||||
{ key: 'happiness_100', name: '快乐满分', icon: '😊' },
|
||||
{ key: 'rename_first', name: '取名大师', icon: '✏️' },
|
||||
{ key: 'adopt_7days', name: '一周陪伴', icon: '📆' },
|
||||
{ key: 'adopt_30days', name: '一月相伴', icon: '🗓️' },
|
||||
{ key: 'adopt_100days', name: '百日纪念', icon: '💯' }
|
||||
];
|
||||
},
|
||||
|
||||
// 选择品种
|
||||
selectBreed: function(e) {
|
||||
this.setData({ breedSelect: e.currentTarget.dataset.breed });
|
||||
},
|
||||
|
||||
// 选择性别
|
||||
selectGender: function(e) {
|
||||
this.setData({ genderSelect: parseInt(e.currentTarget.dataset.gender) });
|
||||
},
|
||||
|
||||
// 输入名字
|
||||
onNameInput: function(e) {
|
||||
this.setData({ petName: e.detail.value });
|
||||
},
|
||||
|
||||
// 领养
|
||||
doAdopt: async function() {
|
||||
var name = this.data.petName.trim();
|
||||
if (!name) {
|
||||
wx.showToast({ title: '请给宠物取个名字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ adopting: true });
|
||||
try {
|
||||
const res = await api.petAdopt({
|
||||
name: name,
|
||||
breed: this.data.breedSelect,
|
||||
gender: this.data.genderSelect
|
||||
});
|
||||
wx.showToast({ title: '🎉 领养成功!', icon: 'success' });
|
||||
this.loadPetInfo();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '领养失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ adopting: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 执行日常任务
|
||||
doDailyAction: async function(e) {
|
||||
var action = e.currentTarget.dataset.action;
|
||||
if (this.data.dailyStatus[action]) {
|
||||
wx.showToast({ title: '已完成', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.petDailyAction({ action: action });
|
||||
wx.showToast({ title: '✅ +' + (res.exp_gain || 0) + '经验', icon: 'success', duration: 1500 });
|
||||
this.loadPetInfo();
|
||||
} catch (e) {
|
||||
if (e.already_done) {
|
||||
wx.showToast({ title: '今天已完成啦', icon: 'none' });
|
||||
this.loadPetInfo();
|
||||
} else {
|
||||
wx.showToast({ title: e.error || '操作失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 改名
|
||||
showRenameModal: function() {
|
||||
this.setData({
|
||||
showRename: true,
|
||||
renameValue: this.data.pet ? this.data.pet.name : ''
|
||||
});
|
||||
},
|
||||
hideRenameModal: function() {
|
||||
this.setData({ showRename: false });
|
||||
},
|
||||
onRenameInput: function(e) {
|
||||
this.setData({ renameValue: e.detail.value });
|
||||
},
|
||||
doRename: async function() {
|
||||
var name = this.data.renameValue.trim();
|
||||
if (!name) {
|
||||
wx.showToast({ title: '请输入名字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ renaming: true });
|
||||
try {
|
||||
await api.petRename({ name: name });
|
||||
wx.showToast({ title: '改名成功 ✏️', icon: 'success' });
|
||||
this.setData({ showRename: false });
|
||||
this.loadPetInfo();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '改名失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ renaming: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 展开/收起动态
|
||||
toggleDynamics: function() {
|
||||
this.setData({ showAllDynamics: !this.data.showAllDynamics }, () => {
|
||||
this._buildDisplayDynamics();
|
||||
});
|
||||
},
|
||||
|
||||
// 根据折叠状态计算要展示的动态列表(收起时只显示一条)
|
||||
_buildDisplayDynamics: function() {
|
||||
var list = this.data.dynamics || [];
|
||||
var show = this.data.showAllDynamics;
|
||||
this.setData({ displayDynamics: show ? list : list.slice(0, 1) });
|
||||
},
|
||||
|
||||
// 跳转到成就页
|
||||
goToAchievements: function() {
|
||||
wx.navigateTo({ url: '/pages/pet/achievements' });
|
||||
},
|
||||
|
||||
bindPartner: function() {
|
||||
getApp().globalData.openProfileBind = true;
|
||||
wx.switchTab({url:'/pages/profile/profile'});
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack: function() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
stopPropagation: function() {}
|
||||
}, {"loadPetInfo": ["coupleInfo", "petInfo"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "宠物养成",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/pet/pet.wxml -->
|
||||
<app-nav title="宠物养成" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="c-body">
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{needsBinding}}" title="绑定另一半后一起养宠物" description="邀请TA加入,一起领养和照顾你们的伙伴" action="去绑定" bind:action="bindPartner" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadPetInfo" /><block wx:else>
|
||||
|
||||
<!-- ===== 未领养:领养引导 ===== -->
|
||||
<view wx:if="{{!hasPet}}" class="adopt-section">
|
||||
<view class="adopt-hero">
|
||||
<text class="adopt-hero-emoji">🐾</text>
|
||||
</view>
|
||||
<view class="adopt-card">
|
||||
<text class="adopt-title">领养一只属于我们的宠物</text>
|
||||
<text class="adopt-desc">一起照顾它,见证它成长</text>
|
||||
<view class="breed-select">
|
||||
<view class="breed-option {{breedSelect=='golden'?'active':''}}" bindtap="selectBreed" data-breed="golden">
|
||||
<text class="breed-emoji">🐕</text>
|
||||
<text class="breed-name">金毛</text>
|
||||
</view>
|
||||
<view class="breed-option {{breedSelect=='husky'?'active':''}}" bindtap="selectBreed" data-breed="husky">
|
||||
<text class="breed-emoji">🐺</text>
|
||||
<text class="breed-name">哈士奇</text>
|
||||
</view>
|
||||
<view class="breed-option {{breedSelect=='corgi'?'active':''}}" bindtap="selectBreed" data-breed="corgi">
|
||||
<text class="breed-emoji">🦊</text>
|
||||
<text class="breed-name">柯基</text>
|
||||
</view>
|
||||
<view class="breed-option {{breedSelect=='shiba'?'active':''}}" bindtap="selectBreed" data-breed="shiba">
|
||||
<text class="breed-emoji">🐶</text>
|
||||
<text class="breed-name">柴犬</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="input-group" style="margin-top:32rpx;">
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="给宠物取个名字吧" value="{{petName}}" bindinput="onNameInput" maxlength="20" />
|
||||
</view>
|
||||
<view class="gender-select">
|
||||
<view class="gender-option {{genderSelect==1?'active':''}}" bindtap="selectGender" data-gender="1">
|
||||
<text>♂ 男孩</text>
|
||||
</view>
|
||||
<view class="gender-option {{genderSelect==2?'active':''}}" bindtap="selectGender" data-gender="2">
|
||||
<text>♀ 女孩</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn btn-primary adopt-btn" bindtap="doAdopt" loading="{{adopting}}">领养我们的伙伴</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 已领养:宠物主界面 ===== -->
|
||||
<view wx:else class="pet-main">
|
||||
|
||||
<view class="pet-profile">
|
||||
<view class="pet-profile-heading"><text class="j-eyebrow">OUR LITTLE COMPANION</text><view class="pet-profile-actions"><view bindtap="showRenameModal">改名</view><view bindtap="goToAchievements">成就</view></view></view>
|
||||
<view class="pet-profile-main"><view class="pet-avatar-wrap"><view class="pet-avatar-ring"><text class="pet-avatar">{{petEmoji}}</text></view><view class="pet-equip" wx:if="{{equippedCollar}}"><text>🎀</text></view></view><view class="pet-profile-info"><view class="pet-name-row"><text class="pet-name">{{pet.name}}</text><text class="pet-gender-text">{{pet.gender==1?'♂':'♀'}}</text></view><text class="pet-description">{{breedName}} · {{stageName}}</text><text class="pet-adopt-days">一起照顾了 {{adoptDays || 0}} 天</text></view></view>
|
||||
<view class="pet-level-bar"><text class="level-label">Lv.{{pet.level}}</text><view class="exp-track"><view class="exp-fill" style="width:{{expProgress}}%;" /></view><text class="exp-text">{{pet.exp}} / {{expForNextLevel}}</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 属性卡片 -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card stat-happiness">
|
||||
<view class="stat-head">
|
||||
<text class="stat-icon">😊</text>
|
||||
<text class="stat-label">心情</text>
|
||||
<text class="stat-value">{{pet.happiness}}</text>
|
||||
</view>
|
||||
<view class="stat-bar"><view class="stat-fill" style="width:{{pet.happiness}}%;"></view></view>
|
||||
</view>
|
||||
<view class="stat-card stat-hunger">
|
||||
<view class="stat-head">
|
||||
<text class="stat-icon">🍖</text>
|
||||
<text class="stat-label">饱腹</text>
|
||||
<text class="stat-value">{{pet.hunger}}</text>
|
||||
</view>
|
||||
<view class="stat-bar"><view class="stat-fill" style="width:{{pet.hunger}}%;"></view></view>
|
||||
</view>
|
||||
<view class="stat-card stat-energy">
|
||||
<view class="stat-head">
|
||||
<text class="stat-icon">⚡</text>
|
||||
<text class="stat-label">精力</text>
|
||||
<text class="stat-value">{{pet.energy}}</text>
|
||||
</view>
|
||||
<view class="stat-bar"><view class="stat-fill" style="width:{{pet.energy}}%;"></view></view>
|
||||
</view>
|
||||
<view class="stat-card stat-cleanliness">
|
||||
<view class="stat-head">
|
||||
<text class="stat-icon">✨</text>
|
||||
<text class="stat-label">清洁</text>
|
||||
<text class="stat-value">{{pet.cleanliness}}</text>
|
||||
</view>
|
||||
<view class="stat-bar"><view class="stat-fill" style="width:{{pet.cleanliness}}%;"></view></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日任务 -->
|
||||
<view class="section-title">
|
||||
<text>今日照顾</text>
|
||||
<text class="section-subtitle">一起完成</text>
|
||||
</view>
|
||||
<view class="daily-grid">
|
||||
<view class="daily-item {{dailyStatus.feed?'done':''}}" bindtap="doDailyAction" data-action="feed">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">🍖</text>
|
||||
<view class="daily-check {{dailyStatus.feed?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.feed}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">喂食</text>
|
||||
<text class="daily-partner {{partnerDaily.feed?'partner-done':''}}">{{partnerDaily.feed ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.water?'done':''}}" bindtap="doDailyAction" data-action="water">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">💧</text>
|
||||
<view class="daily-check {{dailyStatus.water?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.water}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">喂水</text>
|
||||
<text class="daily-partner {{partnerDaily.water?'partner-done':''}}">{{partnerDaily.water ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.walk?'done':''}}" bindtap="doDailyAction" data-action="walk">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">🐾</text>
|
||||
<view class="daily-check {{dailyStatus.walk?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.walk}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">遛狗</text>
|
||||
<text class="daily-partner {{partnerDaily.walk?'partner-done':''}}">{{partnerDaily.walk ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.bath?'done':''}}" bindtap="doDailyAction" data-action="bath">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">🛁</text>
|
||||
<view class="daily-check {{dailyStatus.bath?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.bath}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">洗澡</text>
|
||||
<text class="daily-partner {{partnerDaily.bath?'partner-done':''}}">{{partnerDaily.bath ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.sleep?'done':''}}" bindtap="doDailyAction" data-action="sleep">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">😴</text>
|
||||
<view class="daily-check {{dailyStatus.sleep?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.sleep}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">睡觉</text>
|
||||
<text class="daily-partner {{partnerDaily.sleep?'partner-done':''}}">{{partnerDaily.sleep ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.play?'done':''}}" bindtap="doDailyAction" data-action="play">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">🎾</text>
|
||||
<view class="daily-check {{dailyStatus.play?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.play}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">玩耍</text>
|
||||
<text class="daily-partner {{partnerDaily.play?'partner-done':''}}">{{partnerDaily.play ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 全部完成庆祝 -->
|
||||
<view wx:if="{{allDone}}" class="all-done-banner">
|
||||
<text>今天的照顾已完成,辛苦啦</text>
|
||||
</view>
|
||||
|
||||
<!-- 成就入口 -->
|
||||
<view class="section-title">
|
||||
<text>成长纪念</text>
|
||||
<text class="section-subtitle" bindtap="goToAchievements">查看全部 →</text>
|
||||
</view>
|
||||
<view class="achievement-strip">
|
||||
<view wx:for="{{recentAchievements}}" wx:key="key" class="ach-badge">
|
||||
<text class="ach-icon">{{item.icon}}</text>
|
||||
<text class="ach-name">{{item.name}}</text>
|
||||
</view>
|
||||
<view wx:if="{{achievementCount==0}}" class="ach-empty">
|
||||
<text>完成日常任务解锁成就</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态 -->
|
||||
<view class="section-title">
|
||||
<text>最近的陪伴</text>
|
||||
<text class="section-subtitle" wx:if="{{dynamics.length>1}}" bindtap="toggleDynamics">{{showAllDynamics?'收起':'查看全部'}} ({{dynamics.length}})</text>
|
||||
</view>
|
||||
<view class="dynamics-list">
|
||||
<view wx:for="{{displayDynamics}}" wx:key="id" class="dynamic-item" wx:for-item="d">
|
||||
<view class="dynamic-avatar">
|
||||
<image wx:if="{{d.avatar}}" class="dynamic-avatar-img" src="{{d.avatar}}" mode="aspectFill" />
|
||||
<text wx:else>{{d.initial}}</text>
|
||||
</view>
|
||||
<view class="dynamic-content">
|
||||
<text class="dynamic-text">{{d.message}}</text>
|
||||
<text class="dynamic-time">{{d.time_text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{dynamics.length==0}}" class="empty-state">
|
||||
<text>🐾</text>
|
||||
<text>还没有动态,快去照顾宠物吧</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<view style="height:60rpx;"></view>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 改名弹窗 -->
|
||||
<view wx:if="{{showRename}}" class="modal-overlay" bindtap="hideRenameModal">
|
||||
<view class="rename-modal" catchtap="stopPropagation">
|
||||
<text class="rename-modal-title">给宠物改名</text>
|
||||
<input class="rename-input" placeholder="输入新名字" placeholder-class="rename-placeholder handwritten-placeholder" value="{{renameValue}}" bindinput="onRenameInput" maxlength="20" />
|
||||
<view class="rename-actions">
|
||||
<button class="btn btn-secondary rename-btn" bindtap="hideRenameModal">取消</button>
|
||||
<button class="btn btn-primary rename-btn" bindtap="doRename" loading="{{renaming}}">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,23 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
@import '../../styles/companion.wxss';
|
||||
.pet-profile { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:12rpx 36rpx 12rpx 12rpx; padding:28rpx; }
|
||||
.pet-profile-heading { display:flex; align-items:center; justify-content:space-between; gap:16rpx; }.pet-profile-heading .j-eyebrow { font-size:17rpx; letter-spacing:1rpx; }
|
||||
.pet-profile-actions { display:flex; align-items:center; gap:18rpx; }.pet-profile-actions > view { font-size:22rpx; color:#B74768; min-height:52rpx; display:flex; align-items:center; }
|
||||
.pet-profile-main { display:flex; align-items:center; gap:26rpx; margin:22rpx 0 30rpx; }.pet-avatar-wrap { position:relative; flex-shrink:0; }.pet-avatar-ring { width:134rpx; height:134rpx; background:#F1EBE2; border:1rpx solid #E9DDD0; border-radius:50%; display:flex; align-items:center; justify-content:center; }.pet-avatar { font-size:76rpx; }.pet-equip { position:absolute; right:-6rpx; top:0; font-size:28rpx; }
|
||||
.pet-profile-info { flex:1; min-width:0; }.pet-name-row { display:flex; align-items:center; gap:12rpx; }.pet-name { font-size:36rpx; line-height:1.4; font-weight:600; color:#352E2A; overflow-wrap:break-word; }.pet-gender-text { font-size:24rpx; color:#85776D; }.pet-description { display:block; font-size:24rpx; color:#65594F; margin-top:10rpx; }.pet-adopt-days { display:block; font-size:23rpx; color:#85776D; margin-top:12rpx; }
|
||||
.pet-level-bar { display:flex; align-items:center; gap:16rpx; }.level-label { font-size:22rpx; color:#943C58; flex-shrink:0; }.exp-track { height:10rpx; flex:1; background:#EDE3D7; border-radius:10rpx; overflow:hidden; }.exp-fill { height:100%; background:#B78F68; border-radius:10rpx; }.exp-text { font-size:20rpx; color:#85776D; flex-shrink:0; }
|
||||
.stats-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:16rpx; margin-top:20rpx; padding:0!important; border:0!important; background:transparent!important; box-shadow:none!important; }
|
||||
.stat-card { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:14rpx; padding:20rpx; }.stat-head { display:flex; align-items:center; gap:10rpx; margin-bottom:16rpx; }.stat-icon { font-size:27rpx; }.stat-label { flex:1; font-size:23rpx; color:#85776D; }.stat-value { font-size:28rpx; font-weight:500; color:#352E2A; }.stat-bar { height:7rpx; background:#EDE3D7; border-radius:8rpx; overflow:hidden; }.stat-fill { height:100%; border-radius:8rpx; }.stat-happiness .stat-fill { background:#C6A06F; }.stat-hunger .stat-fill { background:#CA8E9E; }.stat-energy .stat-fill { background:#91A487; }.stat-cleanliness .stat-fill { background:#87A3B0; }
|
||||
.section-title { display:flex; align-items:center; justify-content:space-between; gap:16rpx; margin:34rpx 0 20rpx; font-size:29rpx; font-weight:600; color:#352E2A; }.section-subtitle { font-size:22rpx; font-weight:400; color:#85776D; padding:10rpx 0; }
|
||||
.daily-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:16rpx; }.daily-item { display:flex; flex-direction:column; align-items:center; gap:12rpx; padding:22rpx 8rpx; background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:14rpx; }.daily-item.done { background:#F0F3EB; border-color:#C7D0BA; }.daily-icon-wrap { position:relative; }.daily-icon { font-size:34rpx; }.daily-check { position:absolute; right:-16rpx; top:-4rpx; width:24rpx; height:24rpx; border:1rpx solid #CBBDAE; background:#FFFDF8; border-radius:50%; }.daily-check.checked { display:flex; align-items:center; justify-content:center; color:#FFFDF8; border-color:#8B9D7E; background:#8B9D7E; font-size:18rpx; }.daily-label { color:#352E2A; font-size:25rpx; }.daily-partner { font-size:18rpx; color:#A39386; }.daily-partner.partner-done { color:#677B59; }.all-done-banner { margin-top:20rpx; border-radius:14rpx; background:#F0F3EB; padding:22rpx; text-align:center; font-size:24rpx; color:#677B59; }
|
||||
.achievement-strip { display:flex; gap:14rpx; overflow-x:auto; padding-bottom:8rpx; }.ach-badge { display:flex; flex-direction:column; align-items:center; gap:12rpx; min-width:116rpx; padding:18rpx 12rpx; background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:14rpx; flex-shrink:0; }.ach-icon { font-size:34rpx; }.ach-name { color:#65594F; font-size:20rpx; }.ach-empty { width:100%; text-align:center; font-size:23rpx; color:#85776D; padding:30rpx 0; }
|
||||
.dynamics-list { border-top:1rpx solid #E9DDD0; }.dynamic-item { display:flex; align-items:flex-start; gap:16rpx; padding:24rpx 0; border-bottom:1rpx solid #E9DDD0; }.dynamic-avatar { width:50rpx; height:50rpx; border-radius:50%; background:#F4E2E3; color:#943C58; display:flex; align-items:center; justify-content:center; font-size:23rpx; flex-shrink:0; overflow:hidden; }.dynamic-avatar-img { width:100%; height:100%; }.dynamic-content { flex:1; min-width:0; }.dynamic-text { display:block; font-size:25rpx; color:#65594F; line-height:1.6; }.dynamic-time { display:block; font-size:21rpx; color:#A39386; margin-top:8rpx; }
|
||||
.adopt-hero { text-align:center; margin:12rpx 0 28rpx; }.adopt-hero-emoji { font-size:64rpx; }.adopt-card { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:16rpx; padding:28rpx; }.adopt-title { display:block; font-size:31rpx; font-weight:600; color:#352E2A; text-align:center; }.adopt-desc { display:block; font-size:24rpx; line-height:1.6; color:#85776D; margin-top:14rpx; text-align:center; }
|
||||
.breed-select { display:grid; grid-template-columns:repeat(4,1fr); gap:12rpx; margin-top:28rpx; }.breed-option { display:flex; flex-direction:column; align-items:center; gap:10rpx; padding:18rpx 4rpx; border:1rpx solid #E9DDD0; border-radius:12rpx; }.breed-option.active { background:#F4E2E3; border-color:#D5A6AE; }.breed-emoji { font-size:35rpx; }.breed-name { font-size:22rpx; color:#65594F; }.gender-select { display:flex; gap:16rpx; margin:24rpx 0; }.gender-option { display:flex; flex:1; justify-content:center; padding:20rpx 8rpx; border:1rpx solid #E9DDD0; border-radius:12rpx; color:#85776D; font-size:25rpx; }.gender-option.active { background:#F4E2E3; border-color:#D5A6AE; color:#943C58; }.adopt-btn { width:100%; font-size:26rpx; border-radius:18rpx; padding:24rpx 12rpx; }
|
||||
.modal-overlay { z-index:1200; }.rename-modal { width:100%; max-width:600rpx; padding:36rpx; background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:24rpx; box-sizing:border-box; }.rename-modal-title { display:block; color:#352E2A; font-weight:600; font-size:31rpx; margin-bottom:26rpx; }.rename-input { width:100%; height:84rpx; padding:0 20rpx; background:#FAF5EE; border:1rpx solid #E9DDD0; border-radius:14rpx; box-sizing:border-box; font-size:27rpx; }.rename-actions { display:flex; gap:20rpx; margin-top:28rpx; }.rename-btn { flex:1; min-width:0; padding:24rpx 10rpx; font-size:26rpx; }.rename-placeholder { color:#A39386; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<view class="app-shell j-handwritten-page" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/profile/profile.wxml -->
|
||||
<view wx:if="{{!showEditProfileModal && !showEditPartnerModal}}" class="j-fixed-header"><view class="j-fixed-heading" style="height:{{barHeight}}px;padding-right:{{navSide}}px;"><text class="j-eyebrow">MY CORNER</text><text class="j-page-title">我的小角落</text></view></view>
|
||||
<view wx:if="{{!showEditProfileModal && !showEditPartnerModal}}" class="page-root" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<scroll-view class="page-bg j-main-scroll" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" bindscroll="onScroll" scroll-top="{{scrollTop}}">
|
||||
<view class="j-body">
|
||||
<view class="j-profile-summary" bindtap="showEditProfileModal"><view class="j-profile-info"><text class="j-profile-name">{{myNickname}}</text><text class="j-caption">ID {{myDisplayId}}</text><text class="j-caption">{{myGender}} {{myAge}} {{myBirthday}}</text></view><view class="j-avatar"><image wx:if="{{myAvatarUrl}}" src="{{myAvatarUrl}}" mode="aspectFill" /><text wx:else>{{myInitial}}</text></view></view>
|
||||
<view class="j-section-head"><text>我们的关系</text></view>
|
||||
<view wx:if="{{bindingKnown && hasPartner}}" class="j-relation-card" bindtap="showEditPartnerModal"><view class="j-row"><view><text class="j-small-title">{{partnerName}}</text><text class="j-caption" wx:if="{{partnerDisplayId}}">ID {{partnerDisplayId}}</text><text class="j-caption">{{partnerDetail}}</text></view><view class="j-avatar"><image wx:if="{{partnerAvatarUrl}}" src="{{partnerAvatarUrl}}" mode="aspectFill" /><text wx:else>{{partnerInitial}}</text></view></view></view>
|
||||
<view wx:elif="{{bindingKnown}}" class="j-invite" bindtap="showInvitePanel"><view><text class="j-small-title">另一半还没有加入</text><text class="j-caption">邀请TA,共写这本生活手账</text></view><text class="j-outline-label">绑定情侣</text></view>
|
||||
<page-state wx:elif="{{bindingError}}" kind="error" title="绑定状态加载失败" action="重新加载" bind:action="loadData" /><page-state wx:else kind="loading" title="正在获取绑定状态" />
|
||||
<view class="profile-statistics">
|
||||
<view class="profile-stat-grid">
|
||||
<view class="profile-stat-item" wx:for="{{statisticItems}}" wx:key="key" data-key="{{item.key}}" bindtap="openStatistic" hover-class="profile-stat-pressed">
|
||||
<view class="profile-stat-icon {{item.color}}"><image src="/images/{{item.icon}}.svg" mode="aspectFit" /></view>
|
||||
<view class="profile-stat-label"><text>{{item.label}}</text><text wx:if="{{item.count !== ''}}" class="profile-stat-count">{{item.count}}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{statsError}}" class="profile-stats-retry" bindtap="loadProfileStats">部分统计未加载,点击重试</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view wx:if="{{showEditProfileModal}}" class="edit-root" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<view class="edit-nav"><view class="back-btn" bindtap="hideEditProfileModal"><image class="back-icon" src="/images/back.svg" mode="aspectFit" /></view></view>
|
||||
<scroll-view class="edit-body" scroll-y show-scrollbar="{{false}}" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<view class="edit-avatar-wrap" bindtap="chooseAvatar"><image wx:if="{{editAvatarUrl}}" src="{{editAvatarUrl}}" mode="aspectFill" class="edit-avatar-img"></image><text wx:else class="edit-avatar-placeholder">{{editNicknameInitial || '?'}}</text><text class="edit-avatar-hint">点击更换头像</text></view>
|
||||
<text class="edit-id">我的ID: {{myDisplayId}}</text>
|
||||
<view class="input-group"><label>昵称</label><input placeholder-class="handwritten-placeholder" class="input" placeholder="你的昵称" value="{{editNickname}}" bindinput="onEditNicknameInput" /></view>
|
||||
<view class="input-group"><label>性别</label><picker mode="selector" range="{{genderOptions}}" bindchange="onEditGenderChange"><view class="input">{{editGenderText || '请选择'}}</view></picker></view>
|
||||
<view class="input-group"><label>生日</label><picker mode="date" value="{{editBirthday}}" bindchange="onEditBirthdayInput"><view class="input">{{editBirthday || '请选择日期'}}</view></picker><text style="font-size:24rpx;color:#B74768;margin-top:8rpx;display:block;" wx:if="{{editAge}}">自动计算:{{editAge}}岁</text></view>
|
||||
<view class="input-group" wx:if="{{hasPartner}}"><label>恋爱纪念日</label><picker mode="date" value="{{tempLoveDate}}" bindchange="onLoveDateInput"><view class="input">{{tempLoveDate || '设置你们在一起的日期'}}</view></picker></view>
|
||||
<view class="input-group" wx:if="{{hasPartner}}"><label>恋爱故事</label><textarea placeholder-class="handwritten-placeholder" class="input" style="min-height:120rpx;" placeholder="记录你们的故事..." value="{{tempLoveStory}}" bindinput="onLoveStoryInput"></textarea></view>
|
||||
</scroll-view>
|
||||
<view class="edit-footer"><button class="btn btn-primary" bindtap="saveProfile" hover-class="none">保存</button></view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{showEditPartnerModal}}" class="edit-root partner-edit" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<view class="edit-nav"><view class="back-btn" bindtap="hideEditPartnerModal"><image class="back-icon" src="/images/back.svg" mode="aspectFit" /></view><text class="partner-nav-title">编辑TA的资料</text></view>
|
||||
<scroll-view class="edit-body" scroll-y show-scrollbar="{{false}}">
|
||||
<view class="edit-avatar-wrap" bindtap="choosePartnerAvatar"><image wx:if="{{editPartnerAvatarUrl}}" src="{{editPartnerAvatarUrl}}" mode="aspectFill" class="edit-avatar-img" /><text wx:else class="edit-avatar-placeholder">{{editPartnerNameInitial || '?'}}</text><text class="edit-avatar-hint">{{partnerAvatarUploading ? '正在上传头像…' : '点击更换头像'}}</text></view>
|
||||
<text class="edit-id" wx:if="{{partnerDisplayId}}">TA的ID: {{partnerDisplayId}}</text>
|
||||
<view class="input-group"><label>昵称</label><input placeholder-class="handwritten-placeholder" class="input" value="{{editPartnerNickname}}" placeholder="TA的昵称" bindinput="onEditPartnerNicknameInput" disabled="{{partnerSaving}}" /></view>
|
||||
<view class="input-group"><label>性别</label><picker mode="selector" range="{{genderOptions}}" bindchange="onEditPartnerGenderChange" disabled="{{partnerSaving}}"><view class="input">{{editPartnerGenderText || '请选择'}}</view></picker></view>
|
||||
<view class="input-group"><label>生日</label><picker mode="date" value="{{editPartnerBirthday}}" bindchange="onEditPartnerBirthdayInput" disabled="{{partnerSaving}}"><view class="input">{{editPartnerBirthday || '请选择日期'}}</view></picker><text class="edit-avatar-hint" wx:if="{{editPartnerAge}}">自动计算:{{editPartnerAge}}岁</text></view>
|
||||
<view class="input-group"><label>恋爱纪念日</label><picker mode="date" value="{{tempLoveDate}}" bindchange="onLoveDateInput" disabled="{{partnerSaving}}"><view class="input">{{tempLoveDate || '设置你们在一起的日期'}}</view></picker></view>
|
||||
<view class="input-group"><label>恋爱故事</label><textarea placeholder-class="handwritten-placeholder" class="input" style="min-height:120rpx;" value="{{tempLoveStory}}" placeholder="记录你们的故事…" bindinput="onLoveStoryInput" disabled="{{partnerSaving}}" /></view>
|
||||
</scroll-view>
|
||||
<view class="edit-footer"><button class="btn btn-primary" bindtap="savePartnerProfile" loading="{{partnerSaving}}" disabled="{{partnerSaving || partnerAvatarUploading}}" hover-class="none">保存</button></view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 邀请绑定面板 ===== -->
|
||||
<view wx:if="{{showInvitePanel}}" class="invite-root">
|
||||
<view class="invite-nav">
|
||||
<view class="back-btn" bindtap="hideInvitePanel"><image class="back-icon" src="/images/back.svg" mode="aspectFit" /></view>
|
||||
<text class="invite-nav-title">💕 绑定情侣</text>
|
||||
<view style="width:56rpx;"></view>
|
||||
</view>
|
||||
<scroll-view class="invite-body" scroll-y show-scrollbar="{{false}}">
|
||||
<view class="container" style="padding-top:200rpx;">
|
||||
<view class="card" style="padding:32rpx;text-align:center;margin-bottom:32rpx;">
|
||||
<text style="font-size:26rpx;color:#888;display:block;">你的ID</text>
|
||||
<text style="font-size:64rpx;font-weight:700;color:#B74768;display:block;margin-top:8rpx;" wx:if="{{myDisplayId}}">{{myDisplayId}}</text>
|
||||
<text style="font-size:64rpx;font-weight:700;color:#B74768;display:block;margin-top:8rpx;" wx:else>---</text>
|
||||
<text style="font-size:22rpx;color:#888;display:block;margin-top:8rpx;">将此ID告诉TA</text>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<label>TA的ID</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" type="tel" placeholder="输入对方的6位ID" value="{{inviteTargetId}}" bindinput="onInviteTargetIdInput" maxlength="6" />
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="sendInvite" hover-class="none">
|
||||
<text>💕</text> 发送邀请
|
||||
</button>
|
||||
|
||||
<view style="margin:32rpx 0;display:flex;align-items:center;gap:16rpx;">
|
||||
<view style="flex:1;height:2rpx;background:#FFE4EC;"></view>
|
||||
<text style="font-size:26rpx;color:#888;">或</text>
|
||||
<view style="flex:1;height:2rpx;background:#FFE4EC;"></view>
|
||||
</view>
|
||||
|
||||
<button class="btn btn-secondary" open-type="share" hover-class="none">
|
||||
<text>💌</text> 分享给微信好友
|
||||
</button>
|
||||
|
||||
<view class="invite-tabs" style="margin-top:32px;">
|
||||
<view class="invite-tab {{inviteTab == 'received' ? 'active' : ''}}" bindtap="switchInviteTab" data-tab="received">收到的邀请</view>
|
||||
<view class="invite-tab {{inviteTab == 'sent' ? 'active' : ''}}" bindtap="switchInviteTab" data-tab="sent">发出的邀请</view>
|
||||
</view>
|
||||
|
||||
<view class="invite-list" wx:if="{{inviteTab == 'received'}}">
|
||||
<block wx:for="{{receivedInvites}}" wx:key="id">
|
||||
<view class="invite-card">
|
||||
<view class="invite-card-header">
|
||||
<view class="invite-avatar">{{item.from_initial}}</view>
|
||||
<view class="invite-info">
|
||||
<text class="invite-name">{{item.from_nickname || item.from_username}}</text>
|
||||
<text class="invite-time">{{item.short_date}}</text>
|
||||
</view>
|
||||
<text class="invite-status {{item.status == 'accepted' ? 'accepted' : 'pending'}}">{{item.status == 'accepted' ? '已接受' : '待处理'}}</text>
|
||||
</view>
|
||||
<view class="invite-message" wx:if="{{item.message}}">💌 {{item.message}}</view>
|
||||
<view class="invite-actions" wx:if="{{item.status == 'pending'}}">
|
||||
<button class="btn btn-primary btn-sm" bindtap="acceptInvite" data-id="{{item.id}}" hover-class="none">✅ 接受</button>
|
||||
<button class="btn btn-outline btn-sm" bindtap="rejectInvite" data-id="{{item.id}}" hover-class="none">❌ 拒绝</button>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="empty-tip" wx:if="{{receivedInvites.length == 0}}">
|
||||
<text>暂无收到的邀请 💭</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="invite-list" wx:if="{{inviteTab == 'sent'}}">
|
||||
<block wx:for="{{sentInvites}}" wx:key="id">
|
||||
<view class="invite-card">
|
||||
<view class="invite-card-header">
|
||||
<view class="invite-avatar" style="background:linear-gradient(135deg,#82B1FF,#B388FF);">{{item.to_initial}}</view>
|
||||
<view class="invite-info">
|
||||
<text class="invite-name">{{item.to_nickname || item.to_username || item.to_phone}}</text>
|
||||
<text class="invite-time">{{item.short_date}}</text>
|
||||
</view>
|
||||
<text class="invite-status {{item.status == 'accepted' ? 'accepted' : 'pending'}}">{{item.status == 'accepted' ? '已接受' : '待处理'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="empty-tip" wx:if="{{sentInvites.length == 0}}">
|
||||
<text>暂无发出的邀请 💭</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 分享绑定弹窗 ===== -->
|
||||
<view wx:if="{{showShareBindModal}}" class="modal-overlay" bindtap="hideShareBindModal">
|
||||
<view class="modal" catchtap="noop" style="text-align:center;">
|
||||
<view class="auth-header">
|
||||
<text class="auth-icon" style="font-size:128rpx;">💌</text>
|
||||
<text class="auth-title">情侣邀请</text>
|
||||
<text class="auth-desc" wx:if="{{!shareDisagree}}">有人想和你绑定为情侣</text>
|
||||
<text class="auth-desc" wx:if="{{shareDisagree}}" style="color:#FF4757;">需要对方同意绑定后才能使用</text>
|
||||
</view>
|
||||
<view wx:if="{{!shareDisagree}}">
|
||||
<view class="card" style="padding:48rpx 32rpx;margin-bottom:40rpx;">
|
||||
<text style="font-size:36rpx;font-weight:600;display:block;color:#4A4A4A;">ID: {{shareInviterId}}</text>
|
||||
<text style="font-size:26rpx;color:#888;display:block;margin-top:8rpx;">想要和你绑定为情侣 💕</text>
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="acceptShareBind" hover-class="none">
|
||||
<text>💕</text> 同意绑定
|
||||
</button>
|
||||
<button class="btn btn-outline" style="margin-top:16rpx;" bindtap="rejectShareBind" hover-class="none">
|
||||
不同意
|
||||
</button>
|
||||
</view>
|
||||
<view wx:if="{{shareDisagree}}">
|
||||
<view style="padding:80rpx 0;">
|
||||
<text style="font-size:80rpx;display:block;">😅</text>
|
||||
<text style="font-size:30rpx;color:#888;display:block;margin-top:32rpx;">需要对方同意绑定后才能使用本应用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,367 @@
|
||||
/* pages/profile/profile.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
.container { padding-top: 16rpx; padding-bottom: 300rpx; }
|
||||
.page-root { height: 100vh; overflow: hidden; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
/* 编辑卡全屏 */
|
||||
.edit-root {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 1000;
|
||||
background: linear-gradient(180deg, #FFF0F5 0%, #F4E2E3 40%, #FAFAFA 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 100rpx 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.edit-body {
|
||||
flex: 1;
|
||||
padding: 0 56rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.edit-avatar-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.edit-avatar-img,
|
||||
.edit-avatar-placeholder {
|
||||
width: 120rpx; height: 120rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #FFB4C8;
|
||||
}
|
||||
.edit-avatar-placeholder {
|
||||
background: linear-gradient(135deg, #FF6B81, #B388FF);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 40rpx; font-weight: 700; color: white;
|
||||
}
|
||||
.edit-avatar-hint {
|
||||
font-size: 22rpx; color: #B74768; margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.edit-id {
|
||||
font-size: 24rpx; color: #bbb;
|
||||
text-align: center; display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.input-group { margin-bottom: 20rpx; }
|
||||
.input-group label {
|
||||
font-size: 28rpx; font-weight: 500; color: #555;
|
||||
display: block; margin-bottom: 8rpx;
|
||||
}
|
||||
.input {
|
||||
padding: 20rpx 28rpx; font-size: 30rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid #F0E6FF;
|
||||
}
|
||||
.input-group.readonly .input.readonly-text {
|
||||
background: #F8F4FF;
|
||||
color: #666;
|
||||
border-color: #E8DCF5;
|
||||
padding: 20rpx 28rpx;
|
||||
font-size: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid #E8DCF5;
|
||||
line-height: 1.5;
|
||||
min-height: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.edit-footer {
|
||||
padding: 20rpx 56rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* ===== 邀请绑定面板 ===== */
|
||||
.invite-root {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 1000;
|
||||
background: #FFF5F5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.invite-nav {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
z-index: 1001;
|
||||
height: 176rpx;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 16rpx;
|
||||
background: linear-gradient(to bottom, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 100%);
|
||||
}
|
||||
|
||||
.invite-nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.invite-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 邀请页标签 */
|
||||
.invite-tabs {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-radius: 100rpx;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 48rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.invite-tab {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
border-radius: 100rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.invite-tab.active {
|
||||
background: linear-gradient(135deg, #B74768, #B388FF);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.invite-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.invite-card {
|
||||
background: white;
|
||||
border-radius: 48rpx;
|
||||
padding: 40rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.invite-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.invite-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #B388FF, #82B1FF);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.invite-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.invite-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.invite-time {
|
||||
font-size: 26rpx;
|
||||
color: #888;
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.invite-status {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.invite-status.accepted {
|
||||
background: #E8F5E9;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.invite-status.pending {
|
||||
background: #FFF3E0;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.invite-message {
|
||||
font-size: 28rpx;
|
||||
color: #4A4A4A;
|
||||
padding: 24rpx;
|
||||
background: #FFF8E7;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.invite-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.invite-actions .btn {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 80rpx 0;
|
||||
color: #888;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 分享绑定弹窗 */
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
font-size: 112rpx;
|
||||
margin-bottom: 32rpx;
|
||||
display: block;
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
color: #4A4A4A;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
background: linear-gradient(135deg, #B74768, #B388FF);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.auth-desc {
|
||||
color: #888;
|
||||
font-size: 28rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 24rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: #B74768;
|
||||
border: 4rpx solid #B74768;
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: #F4E2E3;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #B74768;
|
||||
border: 4rpx solid #FFB4C8;
|
||||
}
|
||||
|
||||
.btn-outline:active {
|
||||
background: #F4E2E3;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* TA 资料编辑页的标题与滚动区域。 */
|
||||
.partner-edit .edit-body { min-height: 0; padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); }
|
||||
|
||||
.partner-nav-title { position: absolute; left: 120rpx; right: 120rpx; text-align: center; font-size: 32rpx; font-weight: 600; color: #342F2B; pointer-events: none; }
|
||||
.partner-edit .readonly-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
|
||||
/* 编辑内容独立滚动,底部保存区始终保留空间。 */
|
||||
.edit-root > .edit-body { flex:1; height:0; min-height:0; box-sizing:border-box; padding-bottom:24rpx; }
|
||||
.edit-root > .edit-footer { flex-shrink:0; box-sizing:border-box; }
|
||||
.edit-footer > .btn { width:100%; margin:0; }
|
||||
|
||||
/* 个人页八宫格统计入口。 */
|
||||
.profile-statistics { margin-top:32rpx; padding:28rpx 16rpx; background:#FFFDF8; border:1rpx solid #F1DCE4; border-radius:32rpx; }
|
||||
.profile-stat-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:28rpx 6rpx; }
|
||||
.profile-stat-item { min-width:0; display:flex; flex-direction:column; align-items:center; padding:8rpx 0; }
|
||||
.profile-stat-icon { width:94rpx; height:94rpx; border-radius:30rpx; display:flex; align-items:center; justify-content:center; }
|
||||
.profile-stat-icon image { width:58rpx; height:58rpx; }
|
||||
.profile-stat-icon.pink { background:#FCE5ED; }.profile-stat-icon.yellow { background:#FFF3DC; }.profile-stat-icon.mint { background:#E5F5ED; }.profile-stat-icon.blue { background:#EAF0FD; }.profile-stat-icon.purple { background:#F1EAFB; }.profile-stat-icon.gray { background:#F0EFF2; }
|
||||
.profile-stat-label { display:flex; flex-wrap:wrap; justify-content:center; align-items:baseline; gap:6rpx; margin-top:14rpx; font-family:'Kaiti SC','STKaiti','KaiTi',cursive; font-size:25rpx; line-height:1.5; color:#64424D; }
|
||||
.profile-stat-count { font-family:-apple-system,BlinkMacSystemFont,sans-serif; font-size:19rpx; color:#A39386; }
|
||||
.profile-stat-pressed { opacity:.65; }
|
||||
.profile-stats-retry { text-align:center; color:#B74768; font-size:22rpx; padding:24rpx 0 0; }
|
||||
|
||||
/* 本人与对方资料卡共用完整样式,整卡点击编辑。 */
|
||||
.j-body > .j-profile-summary,
|
||||
.j-body > .j-relation-card {
|
||||
height:216rpx;
|
||||
box-sizing:border-box;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
padding:24rpx 30rpx;
|
||||
margin:20rpx 0 24rpx;
|
||||
background:#FFFDF8;
|
||||
border:1rpx solid #E9DDD0;
|
||||
border-radius:12rpx 40rpx 12rpx 40rpx;
|
||||
box-shadow:none;
|
||||
}
|
||||
.j-relation-card > .j-row { width:100%; gap:30rpx; }
|
||||
.j-profile-summary .j-profile-name,
|
||||
.j-relation-card .j-small-title {
|
||||
display:block;
|
||||
font-family:'Kaiti SC','STKaiti','KaiTi',cursive;
|
||||
font-size:30rpx;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
line-height:1.6;
|
||||
color:#64424D;
|
||||
overflow:hidden;
|
||||
white-space:nowrap;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.j-profile-summary .j-avatar,
|
||||
.j-relation-card .j-avatar {
|
||||
width:96rpx;
|
||||
height:96rpx;
|
||||
flex-shrink:0;
|
||||
box-sizing:border-box;
|
||||
border:6rpx solid #FFFDF8;
|
||||
font-size:32rpx;
|
||||
}
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,553 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/publish/publish.js - 统一发布页(增强版编辑器)
|
||||
const { api, API_BASE, uploadFileCompat } = require('../../utils/request');
|
||||
const { createSwipeBack } = require('../../utils/util');
|
||||
const { requestSubscribeAfterAction } = require('../../utils/subscribe');
|
||||
|
||||
var MOOD_OPTIONS = ['😊', '🥰', '😢', '😤', '😴', '🤩', '🥺', '😎', '🤔', '😭', '😋', '😌'];
|
||||
var WEATHER_OPTIONS = ['☀️', '⛅', '☁️', '🌧️', '⛈️', '❄️', '🌤️', '🌥️', '🌈', '🌪️', '🌫️', '🌙'];
|
||||
|
||||
var TYPE_CONFIG = {
|
||||
timeline: { title: '添加记忆日', btn: '保存记忆日', api: 'timelineAdd' },
|
||||
diary: { title: '写日记', btn: '发布日记', api: 'diaryAdd' },
|
||||
album: { title: '创建相册', btn: '创建相册', api: 'albumAdd' },
|
||||
song: { title: '收藏歌曲', btn: '保存歌曲', api: 'songsAdd' },
|
||||
checkin: { title: '添加打卡', btn: '保存打卡', api: 'checkinAdd' }
|
||||
};
|
||||
|
||||
Page({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
type: '',
|
||||
titleText: '',
|
||||
btnText: '',
|
||||
form: {},
|
||||
editorContent: '',
|
||||
tempImages: [],
|
||||
coverPreview: '',
|
||||
coverPath: '',
|
||||
submitting: false, editLoading:false, editLoadError:false,
|
||||
checkinTypes: ['双方打卡', '第一视角(我)', '她打卡'],
|
||||
checkinTypeIdx: 0,
|
||||
checkinTypeMap: ['both', 'first', 'her'],
|
||||
checkinFreqs: ['每天打卡', '每周打卡', '每月打卡', '不限次数'],
|
||||
checkinFreqIdx: 0,
|
||||
checkinFreqMap: ['daily', 'weekly', 'monthly', 'unlimited'],
|
||||
repeatOptions: ['无重复', '每周重复', '每月重复', '每年重复'],
|
||||
repeatOptionIdx: 0,
|
||||
repeatOptionMap: ['', 'week', 'month', 'year'],
|
||||
editId: 0,
|
||||
// 背景图
|
||||
bgImageUrl: '',
|
||||
bgImagePath: '',
|
||||
// 心情/天气
|
||||
moodOptions: MOOD_OPTIONS,
|
||||
weatherOptions: WEATHER_OPTIONS,
|
||||
contentLength: 0
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
var type = options.type || 'timeline';
|
||||
var editId = options.edit ? parseInt(options.edit) : 0;
|
||||
var cfg = TYPE_CONFIG[type] || TYPE_CONFIG['timeline'];
|
||||
var titleText = editId ? '编辑事件' : cfg.title;
|
||||
var btnText = editId ? '保存修改' : cfg.btn;
|
||||
this.setData({ type: type, titleText: titleText, btnText: btnText, editId: editId });
|
||||
|
||||
var self = this;
|
||||
var swipe = createSwipeBack(function() { self.goBack(); });
|
||||
this.onTouchStart = swipe.onTouchStart;
|
||||
this.onTouchEnd = swipe.onTouchEnd;
|
||||
|
||||
// 初始化表单
|
||||
this.resetForm(type);
|
||||
|
||||
// 非编辑模式:优先恢复本地草稿(防误退出丢失内容)
|
||||
if (!editId) {
|
||||
this.loadDraft(type);
|
||||
}
|
||||
|
||||
// 如果是编辑模式,加载已有数据
|
||||
if (editId && type === 'timeline') {
|
||||
this.loadEditData(editId);
|
||||
}
|
||||
|
||||
// 如果是打卡页,加载情侣昵称
|
||||
if (type === 'checkin') {
|
||||
this.loadCoupleNames();
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 草稿缓存 =====
|
||||
// 读取本地草稿并填充表单
|
||||
loadDraft: function(type) {
|
||||
try {
|
||||
var draftKey = 'publishDraft_' + type;
|
||||
var draft = wx.getStorageSync(draftKey);
|
||||
if (!draft) return;
|
||||
var data = JSON.parse(draft);
|
||||
var patch = {};
|
||||
if (data.form) {
|
||||
var form = this.data.form;
|
||||
for (var k in data.form) {
|
||||
if (data.form.hasOwnProperty(k) && form.hasOwnProperty(k)) {
|
||||
form[k] = data.form[k];
|
||||
}
|
||||
}
|
||||
patch.form = form;
|
||||
patch.editorContent = form.content || '';
|
||||
patch.contentLength = (form.content || '').length;
|
||||
}
|
||||
if (typeof data.repeatOptionIdx === 'number') patch.repeatOptionIdx = data.repeatOptionIdx;
|
||||
if (typeof data.checkinTypeIdx === 'number') patch.checkinTypeIdx = data.checkinTypeIdx;
|
||||
if (typeof data.checkinFreqIdx === 'number') patch.checkinFreqIdx = data.checkinFreqIdx;
|
||||
if (data.bgImageUrl) patch.bgImageUrl = data.bgImageUrl;
|
||||
if (data.coverPreview) {
|
||||
patch.coverPreview = data.coverPreview;
|
||||
patch.coverPath = data.coverPath || data.coverPreview;
|
||||
}
|
||||
this.setData(patch);
|
||||
} catch(e) {}
|
||||
},
|
||||
|
||||
// 连续输入只保留一个草稿写入任务,避免每个按键同步写存储。
|
||||
_saveDraft: function() {
|
||||
this._draftCleared = false;
|
||||
clearTimeout(this._draftTimer);
|
||||
this._draftTimer = setTimeout(() => {
|
||||
this._draftTimer = null;
|
||||
this._persistDraft();
|
||||
}, 500);
|
||||
},
|
||||
_flushDraft: function() {
|
||||
clearTimeout(this._draftTimer);
|
||||
this._draftTimer = null;
|
||||
if (!this._draftCleared) this._persistDraft();
|
||||
},
|
||||
onHide: function() { this._flushDraft(); },
|
||||
onUnload: function() { this._flushDraft(); },
|
||||
_persistDraft: function() {
|
||||
try {
|
||||
var type = this.data.type;
|
||||
if (this.data.editId) return; // 编辑模式不保存草稿
|
||||
var draftKey = 'publishDraft_' + type;
|
||||
var data = {
|
||||
form: this.data.form,
|
||||
repeatOptionIdx: this.data.repeatOptionIdx,
|
||||
checkinTypeIdx: this.data.checkinTypeIdx,
|
||||
checkinFreqIdx: this.data.checkinFreqIdx,
|
||||
bgImageUrl: this.data.bgImageUrl,
|
||||
coverPreview: this.data.coverPreview,
|
||||
coverPath: this.data.coverPath,
|
||||
time: Date.now()
|
||||
};
|
||||
wx.setStorageSync(draftKey, JSON.stringify(data));
|
||||
} catch(e) {}
|
||||
},
|
||||
|
||||
// 发布成功后清除草稿
|
||||
_clearDraft: function() {
|
||||
clearTimeout(this._draftTimer);
|
||||
this._draftTimer = null;
|
||||
this._draftCleared = true;
|
||||
try {
|
||||
var type = this.data.type;
|
||||
wx.removeStorageSync('publishDraft_' + type);
|
||||
} catch(e) {}
|
||||
},
|
||||
|
||||
retryEdit: function() { this.loadEditData(this.data.editId); },
|
||||
loadEditData: async function(id) {
|
||||
this.setData({editLoading:true,editLoadError:false});
|
||||
try {
|
||||
var item = await api.timelineDetail(id);
|
||||
if (item) {
|
||||
var form = {
|
||||
title: item.title || '',
|
||||
date: item.event_date || '',
|
||||
location: item.location || '',
|
||||
content: item.content || '',
|
||||
repeat_interval: parseInt(item.repeat_interval) || 0,
|
||||
repeat_unit: item.repeat_unit || ''
|
||||
};
|
||||
// 加载背景图
|
||||
var bgPath = item.bg_image || '';
|
||||
var bgUrl = bgPath ? require('../../utils/request').getImageUrl(bgPath) : '';
|
||||
var repeatOptionIdx = 0;
|
||||
if (form.repeat_interval > 0 && form.repeat_unit) {
|
||||
var opts = this.data.repeatOptionMap;
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
if (opts[i] === form.repeat_unit) {
|
||||
repeatOptionIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
form: form,
|
||||
editorContent: form.content || '',
|
||||
repeatOptionIdx: repeatOptionIdx,
|
||||
bgImageUrl: bgUrl,
|
||||
bgImagePath: bgPath
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('加载编辑数据失败', e);
|
||||
this.setData({editLoadError:true});
|
||||
} finally { this.setData({editLoading:false}); }
|
||||
},
|
||||
|
||||
loadCoupleNames: async function() {
|
||||
try {
|
||||
var info = await api.coupleInfo();
|
||||
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;
|
||||
var myName = me.nickname || '我';
|
||||
var partnerName = partner.nickname || 'TA';
|
||||
this.setData({
|
||||
checkinTypes: ['双方打卡', myName, partnerName]
|
||||
});
|
||||
} catch(e) {
|
||||
console.error('获取情侣信息失败', e);
|
||||
}
|
||||
},
|
||||
|
||||
resetForm: function(type) {
|
||||
var form = {};
|
||||
if (type === 'diary') {
|
||||
form = { title: '', content: '', mood: '😊', weather: '☀️', lat: 0, lng: 0, location: '' };
|
||||
} else if (type === 'song') {
|
||||
form = { song_name: '', artist: '', note: '' };
|
||||
} else if (type === 'album') {
|
||||
form = { title: '', desc: '', lat: 0, lng: 0, location: '' };
|
||||
this.setData({ coverPreview: '', coverPath: '' });
|
||||
} else if (type === 'checkin') {
|
||||
form = { title: '', type: 'both', frequency: 'daily' };
|
||||
this.setData({ checkinTypeIdx: 0, checkinFreqIdx: 0 });
|
||||
} else {
|
||||
form = { title: '', date: '', location: '', content: '', repeat_interval: 0, repeat_unit: '' };
|
||||
}
|
||||
this.setData({ form: form, editorContent: form.content || '', tempImages: [], repeatOptionIdx: 0, contentLength: (form.content || '').length });
|
||||
},
|
||||
|
||||
// ===== 自动定位(日记 / 相册发布时自动获取经纬度)=====
|
||||
_autoLocate: function() {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
// 已有位置则直接使用,避免重复定位
|
||||
if (self.data.form && self.data.form.lat && self.data.form.lng) {
|
||||
resolve({ lat: self.data.form.lat, lng: self.data.form.lng, location: self.data.form.location || '' });
|
||||
return;
|
||||
}
|
||||
wx.getFuzzyLocation({
|
||||
success: function(res) {
|
||||
resolve({ lat: res.latitude, lng: res.longitude, location: '' });
|
||||
},
|
||||
fail: function() {
|
||||
// 定位失败不阻塞发布,经纬度置 0
|
||||
resolve({ lat: 0, lng: 0, location: '' });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 通用输入(自动保存草稿)=====
|
||||
onTitleInput: function(e) { this.setData({ 'form.title': e.detail.value }); this._saveDraft(); },
|
||||
onDateInput: function(e) { this.setData({ 'form.date': e.detail.value }); this._saveDraft(); },
|
||||
onContentInput: function(e){
|
||||
var val = e.detail.value || '';
|
||||
// 正文只存于逻辑层,避免每次敲字回传长文本影响原生输入。
|
||||
this.data.form.content = val;
|
||||
this.setData({ contentLength: val.length });
|
||||
this._saveDraft();
|
||||
},
|
||||
onLocationInput: function(e){ this.setData({ 'form.location': e.detail.value }); this._saveDraft(); },
|
||||
onDescInput: function(e) { this.setData({ 'form.desc': e.detail.value }); this._saveDraft(); },
|
||||
onArtistInput: function(e) { this.setData({ 'form.artist': e.detail.value }); this._saveDraft(); },
|
||||
onNoteInput: function(e) { this.setData({ 'form.note': e.detail.value }); this._saveDraft(); },
|
||||
|
||||
onCheckinTypeChange: function(e) {
|
||||
var idx = parseInt(e.detail.value);
|
||||
this.setData({ checkinTypeIdx: idx, 'form.type': this.data.checkinTypeMap[idx] });
|
||||
this._saveDraft();
|
||||
},
|
||||
onCheckinFreqChange: function(e) {
|
||||
var idx = parseInt(e.detail.value);
|
||||
this.setData({ checkinFreqIdx: idx, 'form.frequency': this.data.checkinFreqMap[idx] });
|
||||
this._saveDraft();
|
||||
},
|
||||
onRepeatChange: function(e) {
|
||||
var idx = parseInt(e.detail.value);
|
||||
var unit = this.data.repeatOptionMap[idx];
|
||||
this.setData({
|
||||
repeatOptionIdx: idx,
|
||||
'form.repeat_interval': unit ? 1 : 0,
|
||||
'form.repeat_unit': unit || ''
|
||||
});
|
||||
this._saveDraft();
|
||||
},
|
||||
|
||||
// ===== 封面图片选择(相册)=====
|
||||
chooseCover: function() {
|
||||
var self = this;
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function(res) {
|
||||
self.setData({ coverPreview: res.tempFilePaths[0], coverPath: res.tempFilePaths[0] });
|
||||
self._saveDraft();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 图片选择(日记)=====
|
||||
chooseImages: function() {
|
||||
var self = this;
|
||||
wx.chooseImage({
|
||||
count: 9 - this.data.tempImages.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function(res) {
|
||||
self.setData({ tempImages: self.data.tempImages.concat(res.tempFilePaths) });
|
||||
self._saveDraft();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 删除单张图片 =====
|
||||
removeImage: function(e) {
|
||||
var index = parseInt(e.currentTarget.dataset.index);
|
||||
var images = this.data.tempImages;
|
||||
images.splice(index, 1);
|
||||
this.setData({ tempImages: images });
|
||||
this._saveDraft();
|
||||
},
|
||||
|
||||
// ===== 提交 =====
|
||||
submit: async function() {
|
||||
if (this.data.editLoading || this.data.editLoadError) return;
|
||||
if (this.data.submitting) return;
|
||||
this.setData({ submitting: true });
|
||||
var editId = this.data.editId;
|
||||
|
||||
try {
|
||||
var type = this.data.type;
|
||||
var form = this.data.form;
|
||||
|
||||
// 先校验表单
|
||||
if (type === 'timeline') {
|
||||
if (!form.title) { throw new Error('请填写事件内容'); }
|
||||
if (!form.date) { throw new Error('请填写事件日期'); }
|
||||
} else if (type === 'diary') {
|
||||
if (!form.content || !form.content.trim()) { throw new Error('请填写日记内容'); }
|
||||
} else if (type === 'album') {
|
||||
if (!form.title) { throw new Error('请填写相册名称'); }
|
||||
} else if (type === 'song') {
|
||||
if (!form.song_name || !form.artist) { throw new Error('请填写歌曲名和歌手名'); }
|
||||
} else if (type === 'checkin') {
|
||||
if (!form.title) { throw new Error('请填写打卡标题'); }
|
||||
}
|
||||
|
||||
// ★ 在用户点击手势上下文中提前请求订阅消息(符合微信规范)
|
||||
// 为本人接收TA的提醒补充授权,拒绝或保存失败也不影响发布
|
||||
var sceneMap = {
|
||||
diary: 'diary', album: 'photo', checkin: 'checkin',
|
||||
timeline: 'timeline', song: 'song'
|
||||
};
|
||||
var scene = sceneMap[type];
|
||||
if (scene) {
|
||||
await requestSubscribeAfterAction(scene);
|
||||
}
|
||||
|
||||
// 再执行异步操作(上传/API调用)
|
||||
if (type === 'timeline') {
|
||||
var editId = this.data.editId;
|
||||
// 上传背景图
|
||||
var bgImagePath = this.data.bgImagePath;
|
||||
if (this.data.bgImageUrl && !bgImagePath) {
|
||||
wx.showLoading({ title: '上传背景图...' });
|
||||
try {
|
||||
var uploadRes = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: this.data.bgImageUrl,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'timeline_bg' }
|
||||
});
|
||||
bgImagePath = uploadRes && (uploadRes.path || uploadRes.url || '');
|
||||
} catch(e) {}
|
||||
wx.hideLoading();
|
||||
}
|
||||
var result;
|
||||
if (editId) {
|
||||
result = await api.timelineUpdate({ id: editId, title: form.title, event_date: form.date, location: form.location || '', content: form.content || '', repeat_interval: form.repeat_interval || 0, repeat_unit: form.repeat_unit || '', bg_image: bgImagePath || '' });
|
||||
} else {
|
||||
result = await api.timelineAdd({ title: form.title, event_date: form.date, location: form.location || '', content: form.content || '', repeat_interval: form.repeat_interval || 0, repeat_unit: form.repeat_unit || '', bg_image: bgImagePath || '' });
|
||||
}
|
||||
} else if (type === 'diary') {
|
||||
var today = new Date();
|
||||
var ds = today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0') + '-' + String(today.getDate()).padStart(2,'0');
|
||||
var uploadPaths = [];
|
||||
if (this.data.tempImages.length > 0) {
|
||||
wx.showLoading({ title: '上传图片中...' });
|
||||
for (var i = 0; i < this.data.tempImages.length; i++) {
|
||||
var res = await this._uploadDiaryImage(this.data.tempImages[i]);
|
||||
if (res) uploadPaths.push(res);
|
||||
}
|
||||
wx.hideLoading();
|
||||
}
|
||||
// 从缓存获取用户地区
|
||||
var region = wx.getStorageSync('region') || '';
|
||||
// 自动定位获取经纬度(发布后自动显示地理位置)
|
||||
var loc = await this._autoLocate();
|
||||
await api.diaryAdd({
|
||||
title: form.title || '',
|
||||
diary_date: ds,
|
||||
content: form.content,
|
||||
images: uploadPaths,
|
||||
region: region,
|
||||
lat: loc.lat,
|
||||
lng: loc.lng,
|
||||
location: loc.location
|
||||
});
|
||||
} else if (type === 'album') {
|
||||
var coverUrl = '';
|
||||
if (this.data.coverPath) {
|
||||
wx.showLoading({ title: '上传封面...' });
|
||||
coverUrl = await this._uploadAlbumCover(this.data.coverPath);
|
||||
wx.hideLoading();
|
||||
}
|
||||
// 自动定位获取经纬度(发布后自动显示地理位置)
|
||||
var albumLoc = await this._autoLocate();
|
||||
await api.albumAdd({ title: form.title, description: form.desc || '', cover: coverUrl, lat: albumLoc.lat, lng: albumLoc.lng, location: albumLoc.location });
|
||||
} else if (type === 'song') {
|
||||
await api.songsAdd({ song_name: form.song_name, artist: form.artist, note: form.note || '' });
|
||||
} else if (type === 'checkin') {
|
||||
await api.checkinAdd({ title: form.title, type: form.type || 'dual', frequency: form.frequency || 'daily' });
|
||||
}
|
||||
|
||||
// 发布/修改成功后清除草稿
|
||||
this._clearDraft();
|
||||
|
||||
wx.showToast({ title: editId ? '修改成功 ✨' : '发布成功 ✨', icon: 'success' });
|
||||
// 延迟返回页面,给订阅弹窗留出时间
|
||||
setTimeout(function() { wx.navigateBack(); }, 1500);
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: (e && (e.error || e.message || e.errMsg)) || '发布失败', icon: 'none', duration: 2000 });
|
||||
} finally {
|
||||
this.setData({ submitting: false });
|
||||
}
|
||||
},
|
||||
|
||||
_uploadAlbumCover: async function(filePath) {
|
||||
try {
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'albums' }
|
||||
});
|
||||
// 兼容不同响应格式:{path:'...'} 或 {success:true, path:'...'} 或 {url:'...'}
|
||||
var path = data && (data.path || data.url || '');
|
||||
if (!path) {
|
||||
// 内容安全检测失败:抛出错误提示用户
|
||||
if (data && data.error) {
|
||||
throw data;
|
||||
}
|
||||
console.error('封面上传返回无路径', JSON.stringify(data), '错误码:', data && data.code);
|
||||
// 尝试重新上传一次
|
||||
try {
|
||||
var retryData = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'albums' }
|
||||
});
|
||||
var retryPath = retryData && (retryData.path || retryData.url || '');
|
||||
if (retryPath) {
|
||||
console.log('封面上传重试成功');
|
||||
return retryPath;
|
||||
}
|
||||
} catch(retryErr) {
|
||||
console.error('封面上传重试也失败', retryErr, '错误码:', retryErr && retryErr.code);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
} catch(e) {
|
||||
// 内容安全违规错误:直接抛出,中断发布并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
console.error('封面上传失败', e, '错误码:', e && e.code);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
_uploadDiaryImage: async function(filePath) {
|
||||
try {
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
// 兼容不同响应格式:{path:'...'} 或 {success:true, path:'...'} 或 {url:'...'}
|
||||
var path = data && (data.path || data.url || '');
|
||||
if (!path) {
|
||||
// 内容安全检测失败:抛出错误提示用户
|
||||
if (data && data.error) {
|
||||
throw data;
|
||||
}
|
||||
console.error('日记图片上传返回无路径', JSON.stringify(data), '错误码:', data && data.code);
|
||||
// 尝试重新上传一次
|
||||
try {
|
||||
var retryData = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
var retryPath = retryData && (retryData.path || retryData.url || '');
|
||||
if (retryPath) {
|
||||
console.log('日记图片重试上传成功');
|
||||
return retryPath;
|
||||
}
|
||||
} catch(retryErr) {
|
||||
console.error('日记图片重试上传也失败', retryErr, '错误码:', retryErr && retryErr.code);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
} catch(e) {
|
||||
// 内容安全违规错误:直接抛出,中断发布并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
console.error('日记图片上传失败', e, '错误码:', e && e.code);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 背景图 =====
|
||||
chooseBgImage: function() {
|
||||
var self = this;
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function(res) {
|
||||
var filePath = res.tempFilePaths[0];
|
||||
self.setData({ bgImageUrl: filePath, bgImagePath: '' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeBgImage: function() {
|
||||
this.setData({ bgImageUrl: '', bgImagePath: '' });
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); }
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"disableScroll": true,
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/publish/publish.wxml - 统一发布页(增强版编辑器) -->
|
||||
<app-nav title="{{titleText}}" back="{{true}}" />
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" style="height:100vh;" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<view class="container"><page-state wx:if="{{editLoading}}" kind="loading" title="正在加载事件" /><page-state wx:elif="{{editLoadError}}" kind="error" title="事件加载失败" action="重试" bind:action="retryEdit" /><block wx:else>
|
||||
|
||||
<!-- ===== 时间线 / 打卡 / 相册 ===== -->
|
||||
<block wx:if="{{type == 'timeline' || type == 'checkin' || type == 'album'}}">
|
||||
<view class="input-group">
|
||||
<label>{{type == 'album' ? '相册名称' : (type == 'timeline' ? '事件内容' : '标题')}}</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="{{type == 'album' ? '例如:我们的旅行' : (type == 'checkin' ? '例如:吃饭、睡觉、运动' : '例如:第一次约会')}}" value="{{form.title}}" bindinput="onTitleInput" />
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ===== 打卡(类型 + 频率)===== -->
|
||||
<block wx:if="{{type == 'checkin'}}">
|
||||
<view class="input-group">
|
||||
<label>打卡权限</label>
|
||||
<picker mode="selector" range="{{checkinTypes}}" value="{{checkinTypeIdx}}" bindchange="onCheckinTypeChange">
|
||||
<view class="input">{{checkinTypes[checkinTypeIdx] || '请选择'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>打卡频率</label>
|
||||
<picker mode="selector" range="{{checkinFreqs}}" value="{{checkinFreqIdx}}" bindchange="onCheckinFreqChange">
|
||||
<view class="input">{{checkinFreqs[checkinFreqIdx] || '请选择'}}</view>
|
||||
</picker>
|
||||
<text style="font-size:22rpx;color:#999;margin-top:8rpx;display:block;">每天/每周/每月:周期内打卡一次即完成 | 不限:有记录即算完成</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ===== 时间线(日期)===== -->
|
||||
<block wx:if="{{type == 'timeline'}}">
|
||||
<view class="input-group">
|
||||
<label>事件日期</label>
|
||||
<picker mode="date" value="{{form.date}}" bindchange="onDateInput">
|
||||
<view class="input">{{form.date || '请选择事件日期'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ===== 时间线(地点 + 备注 + 重复 + 背景图)===== -->
|
||||
<block wx:if="{{type == 'timeline'}}">
|
||||
<view class="input-group">
|
||||
<label>事件地点(选填)</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:西湖边" value="{{form.location}}" bindinput="onLocationInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>备注(选填)</label>
|
||||
<textarea placeholder-class="handwritten-placeholder" class="input textarea" placeholder="记录一些想说的话..." value="{{editorContent}}" bindinput="onContentInput" auto-height maxlength="500" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>重复选项(选填)</label>
|
||||
<picker mode="selector" range="{{repeatOptions}}" value="{{repeatOptionIdx}}" bindchange="onRepeatChange">
|
||||
<view class="input">{{repeatOptions[repeatOptionIdx]}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>背景图片(选填)</label>
|
||||
<view class="bg-pick-row">
|
||||
<image wx:if="{{bgImageUrl}}" class="bg-preview" src="{{bgImageUrl}}" mode="aspectFill" />
|
||||
<view class="bg-pick-btn" bindtap="chooseBgImage">
|
||||
<text wx:if="{{!bgImageUrl}}">+ 添加背景</text>
|
||||
<text wx:else>更换背景</text>
|
||||
</view>
|
||||
<view class="bg-remove-btn" wx:if="{{bgImageUrl}}" bindtap="removeBgImage">✕ 移除</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ===== 相册(描述 + 封面)===== -->
|
||||
<view class="input-group" wx:if="{{type == 'album'}}">
|
||||
<label>描述</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="简单描述一下这个相册" value="{{form.desc}}" bindinput="onDescInput" />
|
||||
</view>
|
||||
<view class="input-group" wx:if="{{type == 'album'}}">
|
||||
<label>封面图片</label>
|
||||
<view class="cover-pick-row">
|
||||
<image wx:if="{{coverPreview}}" class="cover-preview" src="{{coverPreview}}" mode="aspectFill" />
|
||||
<view class="cover-pick-btn" bindtap="chooseCover" wx:if="{{!coverPreview}}">
|
||||
<text>+</text>
|
||||
</view>
|
||||
<view class="cover-pick-btn" bindtap="chooseCover" wx:else style="background:#f0f0f0;">
|
||||
<text style="font-size:28rpx;">换</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 日记 ===== -->
|
||||
<block wx:if="{{type == 'diary'}}">
|
||||
<!-- 标题 -->
|
||||
<view class="input-group">
|
||||
<label>标题(选填)</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input diary-title-input" placeholder="给日记起个标题..." value="{{form.title}}" bindinput="onTitleInput" maxlength="100" />
|
||||
</view>
|
||||
|
||||
<!-- 内容编辑器(支持换行) -->
|
||||
<view class="input-group editor-group">
|
||||
<view class="editor-header">
|
||||
<label>内容</label>
|
||||
<text class="char-count {{contentLength > 5000 ? 'char-warn' : ''}}">{{contentLength}}/5000</text>
|
||||
</view>
|
||||
<textarea placeholder-class="handwritten-placeholder" class="input textarea diary-editor" placeholder="今天发生了什么有趣的事呢?写下你的心情和感受吧…" value="{{editorContent}}" bindinput="onContentInput" maxlength="5000" auto-height="{{true}}" cursor-spacing="24" />
|
||||
</view>
|
||||
|
||||
<!-- 图片选择(可删除) -->
|
||||
<view class="input-group">
|
||||
<view class="editor-header">
|
||||
<label>图片</label>
|
||||
<text class="img-count">{{tempImages.length}}/9</text>
|
||||
</view>
|
||||
<view class="img-picker-row">
|
||||
<view wx:for="{{tempImages}}" wx:key="*this" class="img-picker-wrap">
|
||||
<image class="img-picker-thumb" src="{{item}}" mode="aspectFill" />
|
||||
<view class="img-picker-del" bindtap="removeImage" data-index="{{index}}" hover-class="del-hover">
|
||||
<text>✕</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="img-picker-add" bindtap="chooseImages" wx:if="{{tempImages.length < 9}}">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ===== 收藏歌曲 ===== -->
|
||||
<block wx:if="{{type == 'song'}}">
|
||||
<view class="input-group">
|
||||
<label>歌曲名</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:小幸运" value="{{form.song_name}}" bindinput="onTitleInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>歌手</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:田馥甄" value="{{form.artist}}" bindinput="onArtistInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>备注(选填)</label>
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="一句话,限60字" maxlength="60" value="{{form.note}}" bindinput="onNoteInput" />
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 提交 -->
|
||||
<button class="btn btn-primary submit-btn" bindtap="submit" hover-class="none" loading="{{submitting}}">{{btnText}}</button>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,215 @@
|
||||
/* pages/publish/publish.wxss - 增强版编辑器样式 */
|
||||
@import '../../styles/common.wxss';
|
||||
|
||||
.container { padding: 84rpx 32rpx 300rpx; }
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
|
||||
/* ===== 输入框美化 ===== */
|
||||
.input-group { margin-bottom: 36rpx; }
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 28rpx 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.input:focus {
|
||||
border-color: #FF6B81;
|
||||
box-shadow: 0 0 0 6rpx rgba(255,107,129,0.08);
|
||||
}
|
||||
.textarea {
|
||||
min-height: 200rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ===== 日记标题输入框 ===== */
|
||||
.diary-title-input {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 编辑器头部(标签+计数) ===== */
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.editor-header label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.char-count {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
.char-warn {
|
||||
color: #ff6b6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.img-count {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* ===== 日记编辑器 ===== */
|
||||
.diary-editor {
|
||||
min-height: 300rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.7;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 图片选择(可删除) ===== */
|
||||
.img-picker-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.img-picker-wrap {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: visible;
|
||||
}
|
||||
.img-picker-thumb {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #eee;
|
||||
}
|
||||
.img-picker-del {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
right: -12rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,71,87,0.9);
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255,71,87,0.3);
|
||||
z-index: 2;
|
||||
}
|
||||
.del-hover {
|
||||
transform: scale(1.2);
|
||||
background: #ff4757;
|
||||
}
|
||||
.img-picker-add {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
border: 2rpx dashed #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.img-picker-add .add-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ccc;
|
||||
line-height: 1;
|
||||
}
|
||||
.img-picker-add .add-text {
|
||||
font-size: 20rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* ===== 提交按钮 ===== */
|
||||
.submit-btn {
|
||||
margin-top: 40rpx;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
/* ===== 封面图片选择 ===== */
|
||||
.cover-pick-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.cover-preview {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #eee;
|
||||
}
|
||||
.cover-pick-btn {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
border: 2rpx dashed #ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 56rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* ===== 背景图选择 ===== */
|
||||
.bg-pick-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.bg-preview {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #eee;
|
||||
}
|
||||
.bg-pick-btn {
|
||||
padding: 20rpx 32rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
border: 2rpx dashed #ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
min-height: 80rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bg-remove-btn {
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
border: 2rpx solid #ff6b6b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
color: #ff6b6b;
|
||||
min-height: 80rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* 正文随输入增高,长文由外层页面滚动,避免嵌套原生输入框滚动。 */
|
||||
textarea.diary-editor { min-height:420rpx; box-sizing:border-box; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,131 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/punch/punch.js - 情侣打卡
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loadError: false,
|
||||
checkins: [],
|
||||
coupleInfo: null,
|
||||
myAvatar: '', partnerAvatar: '',
|
||||
myName: '我', partnerName: 'TA',
|
||||
weekBoth: 0, weekMy: 0, weekPartner: 0,
|
||||
todayMy: 0, todayPartner: 0,
|
||||
smokingVisible: true,
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.refreshSmokingVisible();
|
||||
this.loadCoupleInfo();
|
||||
this.loadData();
|
||||
},
|
||||
onShow: function() { this.refreshSmokingVisible(); this.loadCoupleInfo(); this.loadData(); },
|
||||
|
||||
// 读取本地设置:抽烟打卡默认事件是否显示(默认显示)
|
||||
refreshSmokingVisible: function() {
|
||||
var v = wx.getStorageSync('smokingVisible');
|
||||
this.setData({ smokingVisible: (v === '' || v == null) ? true : !!v });
|
||||
},
|
||||
|
||||
onSmokingUpdated: function() { this.loadData(); },
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
const smoking = this.selectComponent && this.selectComponent("#smoking");
|
||||
if (smoking) await smoking.load(true);
|
||||
await this.loadCoupleInfo();
|
||||
await this.loadData();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
// 从接口加载情侣头像和名称
|
||||
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({
|
||||
coupleInfo: info,
|
||||
myAvatar: getImageUrl(me.avatar || ''),
|
||||
partnerAvatar: getImageUrl(partner.avatar || ''),
|
||||
myName: me.nickname || '我',
|
||||
partnerName: partner.nickname || 'TA'
|
||||
});
|
||||
},
|
||||
|
||||
loadData: async function(page) {
|
||||
const ticket = startList(this, page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
const [res, stats] = await Promise.all([
|
||||
api.checkinList({page:ticket.page,page_size:20}),
|
||||
api.checkinStats().catch(() => ({}))
|
||||
]);
|
||||
const list = itemsOf(res).map(function(item) {
|
||||
item.type = item.type || 'both';
|
||||
var typeLabels = { both: '双方', first: '第一视角', her: '她打卡' };
|
||||
item.type_label = typeLabels[item.type] || '双方';
|
||||
item.frequency = item.frequency || 'daily';
|
||||
var freqLabels = { daily: '每天', weekly: '每周', monthly: '每月', once: '一次', unlimited: '不限' };
|
||||
item.freq_label = freqLabels[item.frequency] || '每天';
|
||||
var periodLabels = { daily: '今日', weekly: '本周', monthly: '本月', once: '本次', unlimited: '' };
|
||||
item.period_label = periodLabels[item.frequency] || '今日';
|
||||
item.my_photo_url = item.my_photo ? getImageUrl(item.my_photo) : '';
|
||||
item.partner_photo_url = item.partner_photo ? getImageUrl(item.partner_photo) : '';
|
||||
// 打卡人头像
|
||||
item.creator_avatar_url = item.creator_avatar ? getImageUrl(item.creator_avatar) : '';
|
||||
item.my_checkin_avatar_url = item.my_checkin_avatar ? getImageUrl(item.my_checkin_avatar) : '';
|
||||
item.partner_checkin_avatar_url = item.partner_checkin_avatar ? getImageUrl(item.partner_checkin_avatar) : '';
|
||||
return item;
|
||||
});
|
||||
var data = {
|
||||
checkins: list,
|
||||
weekBoth: stats.week_both_count || 0,
|
||||
weekMy: stats.week_my_count || 0,
|
||||
weekPartner: stats.week_partner_count || 0,
|
||||
todayMy: stats.today_my_count || 0,
|
||||
todayPartner: stats.today_partner_count || 0,
|
||||
loading: false
|
||||
};
|
||||
delete data.checkins;
|
||||
applyList(this, ticket, res, list, 'checkins', data);
|
||||
} catch(e) {
|
||||
failList(this,ticket); console.error(e); this.setData({ loading: false }); }
|
||||
finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
// ===== 点击卡片 → 详情弹窗 =====
|
||||
openDetail: function(e) {
|
||||
var idx = e.currentTarget.dataset.index;
|
||||
var item = this.data.checkins[idx];
|
||||
wx.navigateTo({ url: '/pages/checkin-detail/checkin-detail?id=' + item.id });
|
||||
},
|
||||
|
||||
// ===== 跳转发布页 =====
|
||||
showAddModal: function() { wx.navigateTo({ url: '/pages/publish/publish?type=checkin' }); },
|
||||
|
||||
// ===== 删除 =====
|
||||
deleteCheckin: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
wx.showModal({
|
||||
title: '确认删除', content: '确定要删除这个打卡项吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) { try { await api.checkinDelete({ id: id }); wx.showToast({ title: '已删除', icon: 'success' }); this.loadData(); } catch(err) {} }
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, 'loadData'), {"loadData": ["checkinList", "checkinStats", "coupleInfo"]}));
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state",
|
||||
"smoking-card": "/components/smoking-card/smoking-card"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/punch/punch.wxml - 情侣打卡 -->
|
||||
<app-nav title="打卡" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="c-body"><view class="c-heading j-row"><view><text class="j-eyebrow">LITTLE PROMISES</text><text class="j-page-title">把约定变成习惯</text></view><button class="j-button j-button-small" bindtap="showAddModal">+ 添加</button></view>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
|
||||
<!-- 顶部 Banner - 卡片式 -->
|
||||
<view class="punch-banner-card" wx:if="{{!loading && !loadError}}">
|
||||
<view class="promise-summary-head"><text class="c-small-title">一起坚持的小约定</text><view class="promise-avatars"><view class="banner-avatar-circle"><image wx:if="{{myAvatar}}" src="{{myAvatar}}" mode="aspectFill" /><text wx:else>我</text></view><view class="banner-avatar-circle"><image wx:if="{{partnerAvatar}}" src="{{partnerAvatar}}" mode="aspectFill" /><text wx:else>TA</text></view></view></view>
|
||||
<view class="banner-stat-grid">
|
||||
<view class="banner-stat-card">
|
||||
<text class="stat-num">{{weekBoth}}</text>
|
||||
<text class="stat-label">本周共同打卡</text>
|
||||
</view>
|
||||
<view class="banner-stat-card">
|
||||
<text class="stat-num">{{todayMy}}</text>
|
||||
<text class="stat-label">我今天</text>
|
||||
</view>
|
||||
<view class="banner-stat-card">
|
||||
<text class="stat-num">{{todayPartner}}</text>
|
||||
<text class="stat-label">TA今天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<smoking-card id="smoking" wx:if="{{smokingVisible}}" bind:updated="onSmokingUpdated" />
|
||||
|
||||
<!-- 打卡列表 -->
|
||||
<view class="checkin-cards" wx:if="{{!loading && !loadError && checkins.length > 0}}">
|
||||
<view class="checkin-card" wx:for="{{checkins}}" wx:key="id" bindtap="openDetail" data-index="{{index}}" bindlongpress="deleteCheckin" data-id="{{item.id}}">
|
||||
<!-- 主题 -->
|
||||
<view class="card-row card-theme">
|
||||
<text class="card-icon" wx:if="{{item.icon && item.icon !== '✅'}}">{{item.icon}}</text><image wx:else class="card-icon" src="/images/check.svg" mode="aspectFit" />
|
||||
<text class="card-title">{{item.title}}</text>
|
||||
</view>
|
||||
<!-- 打卡频率 -->
|
||||
<view class="card-row">
|
||||
<text class="card-label">打卡频率</text>
|
||||
<text class="card-value">{{item.freq_label}}</text>
|
||||
</view>
|
||||
<!-- 参与人(双人:双方昵称+头像 / 单人:对应昵称+头像) -->
|
||||
<view class="card-row" wx:if="{{item.type == 'both'}}">
|
||||
<text class="card-label">参与人</text>
|
||||
<view class="card-users">
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{myAvatar}}" class="chip-avatar" src="{{myAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{myName ? myName.charAt(0) : '我'}}</text>
|
||||
<text class="chip-name">{{myName}}</text>
|
||||
</view>
|
||||
<text class="chip-sep">·</text>
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{partnerAvatar}}" class="chip-avatar" src="{{partnerAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{partnerName ? partnerName.charAt(0) : 'TA'}}</text>
|
||||
<text class="chip-name">{{partnerName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-row" wx:elif="{{item.type == 'first' || item.type == 'her'}}">
|
||||
<text class="card-label">参与人</text>
|
||||
<view class="card-users">
|
||||
<view class="user-chip">
|
||||
<image wx:if="{{item.type == 'first' && myAvatar}}" class="chip-avatar" src="{{myAvatar}}" mode="aspectFill" />
|
||||
<image wx:elif="{{item.type == 'her' && partnerAvatar}}" class="chip-avatar" src="{{partnerAvatar}}" mode="aspectFill" />
|
||||
<text wx:else class="chip-avatar-fallback">{{item.type == 'first' ? (myName ? myName.charAt(0) : '我') : (partnerName ? partnerName.charAt(0) : 'TA')}}</text>
|
||||
<text class="chip-name">{{item.type == 'first' ? myName : partnerName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 创建人 -->
|
||||
<view class="card-row">
|
||||
<text class="card-label">创建人</text>
|
||||
<view class="card-creator-info">
|
||||
<image wx:if="{{item.creator_avatar_url}}" class="creator-avatar" src="{{item.creator_avatar_url}}" mode="aspectFill" />
|
||||
<text wx:else class="creator-avatar-fallback">{{item.creator_name ? item.creator_name.charAt(0) : '?'}}</text>
|
||||
<text class="creator-name">{{item.creator_name || '未知'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<page-state wx:if="{{!loading && !loadError && !checkins.length}}" title="从一个小约定开始" description="一起运动、按时吃饭,也可以成为日常仪式" action="添加打卡" bind:action="showAddModal" />
|
||||
</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>
|
||||
@@ -0,0 +1,15 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
@import '../../styles/companion.wxss';
|
||||
.punch-banner-card { background:#FFF7FA; border:1rpx solid #F1DCE4; padding:28rpx; border-radius:16rpx; margin-bottom:32rpx; }
|
||||
.promise-summary-head { display:flex; justify-content:space-between; align-items:center; gap:16rpx; margin-bottom:24rpx; }
|
||||
.promise-avatars { display:flex; padding-right:8rpx; }.banner-avatar-circle { width:44rpx; height:44rpx; border-radius:50%; overflow:hidden; border:3rpx solid #FFF7FA; background:#F4E2E3; color:#943C58; font-size:20rpx; display:flex; align-items:center; justify-content:center; }.banner-avatar-circle + .banner-avatar-circle { margin-left:-8rpx; }.banner-avatar-circle image { width:100%; height:100%; }
|
||||
.banner-stat-grid { display:flex; gap:10rpx; }.banner-stat-card { flex:1; display:flex; flex-direction:column; align-items:flex-start; border-right:1rpx solid #F1DCE4; padding-left:20rpx; }.banner-stat-card:first-child { padding-left:0; }.banner-stat-card:last-child { border:0; }
|
||||
.stat-num { font-size:44rpx; line-height:1.2; font-weight:500; color:#943C58; }.stat-label { color:#85776D; font-size:21rpx; line-height:1.5; margin-top:8rpx; }
|
||||
.checkin-cards { display:flex; flex-direction:column; gap:20rpx; }
|
||||
.checkin-card:active { opacity:.8; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,400 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/settings/settings.js
|
||||
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
|
||||
const { requestSubscribe, getSubscribeStatus } = require('../../utils/subscribe');
|
||||
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: {},
|
||||
subscribeItems: [],
|
||||
subscribeLoading: false,
|
||||
|
||||
// 打卡:抽烟打卡默认事件是否显示
|
||||
smokingVisible: true,
|
||||
|
||||
// 我们past
|
||||
showAboutModal: false,
|
||||
// 联系我们
|
||||
showContactModal: false,
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.refreshSmokingVisible();},
|
||||
|
||||
onPullDownRefresh: function() {
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
onShow: async function() {
|
||||
this.refreshSmokingVisible();
|
||||
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' });
|
||||
},
|
||||
|
||||
// 加载解绑状态
|
||||
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() {},
|
||||
|
||||
// ===== 抽烟打卡默认事件显示开关 =====
|
||||
refreshSmokingVisible: function() {
|
||||
var v = wx.getStorageSync('smokingVisible');
|
||||
this.setData({ smokingVisible: (v === '' || v == null) ? true : !!v });
|
||||
},
|
||||
toggleSmoking: function(e) {
|
||||
var visible = !!e.detail.value;
|
||||
this.setData({ smokingVisible: visible });
|
||||
wx.setStorageSync('smokingVisible', visible);
|
||||
wx.showToast({ title: visible ? '已显示抽烟打卡' : '已隐藏抽烟打卡', icon: 'none' });
|
||||
},
|
||||
|
||||
// ===== 消息通知管理(统一使用一个模板) =====
|
||||
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 });
|
||||
},
|
||||
|
||||
// ===== 联系我们 =====
|
||||
showContactModal: function() {
|
||||
this.setData({ showContactModal: true });
|
||||
},
|
||||
hideContactModal: function() {
|
||||
this.setData({ showContactModal: false });
|
||||
},
|
||||
|
||||
toggleSubscribe: async function(e) {
|
||||
const type = 'notify';
|
||||
const current = this.data.subscribeChecked;
|
||||
|
||||
// 防止重复点击
|
||||
if (this._subscribing) return;
|
||||
this._subscribing = true;
|
||||
|
||||
if (current) {
|
||||
// 取消订阅
|
||||
try {
|
||||
await api.unsubscribeTemplate({ template_type: type });
|
||||
this.setData({ subscribeChecked: false });
|
||||
wx.setStorageSync('subscribed_notify', 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({ subscribeChecked: true });
|
||||
wx.setStorageSync('subscribed_notify', true);
|
||||
wx.showToast({ title: '已开启通知', icon: 'success' });
|
||||
} else {
|
||||
wx.showToast({ title: '订阅被拒绝,可在设置中重新开启', icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '订阅失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
this._subscribing = false;
|
||||
},
|
||||
|
||||
renewSubscribe: async function() {
|
||||
if (this._subscribing) return;
|
||||
this._subscribing = true;
|
||||
try {
|
||||
const results = await requestSubscribe('notify');
|
||||
if (results.notify) {
|
||||
this.setData({ subscribeChecked: true });
|
||||
wx.showToast({ title: '已订阅一次提醒', icon: 'success' });
|
||||
} else {
|
||||
wx.showToast({ title: '本次未授权提醒', icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '订阅失败,请重试', icon: 'none' });
|
||||
} 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;
|
||||
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(); },
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<view class="app-shell app-shell--secondary j-handwritten-page" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/settings/settings.wxml -->
|
||||
<app-nav title="设置" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="j-body">
|
||||
<view class="section-heading">外观与通知</view>
|
||||
<!-- 首页背景 -->
|
||||
<view class="j-profile-setting" bindtap="showBgModal">
|
||||
<view class="setting-info">
|
||||
<view class="j-small-title">首页背景</view>
|
||||
<view class="j-caption">自定义上传背景图片</view>
|
||||
</view>
|
||||
<text class="j-link">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息通知 -->
|
||||
<view class="j-profile-setting" bindtap="showSubscribeModal">
|
||||
<view class="setting-info">
|
||||
<view class="j-small-title">消息通知</view>
|
||||
<view class="j-caption">管理订阅消息推送</view>
|
||||
</view>
|
||||
<text class="j-link">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 抽烟打卡默认事件 -->
|
||||
<view class="j-profile-setting">
|
||||
<view class="setting-info">
|
||||
<view class="j-small-title">抽烟打卡</view>
|
||||
<view class="j-caption">在打卡页显示「抽烟记录」默认事件</view>
|
||||
</view>
|
||||
<switch checked="{{smokingVisible}}" bindchange="toggleSmoking" color="#B74768" style="transform:scale(0.75);" />
|
||||
</view>
|
||||
|
||||
<view class="section-heading">关系与账号</view>
|
||||
<!-- 操作记录 -->
|
||||
<view class="j-profile-setting" bindtap="goToHistory">
|
||||
<view class="setting-info">
|
||||
<view class="j-small-title">操作记录</view>
|
||||
<view class="j-caption">查看双方所有操作日志</view>
|
||||
</view>
|
||||
<text class="j-link">›</text>
|
||||
</view>
|
||||
|
||||
<page-state wx:if="{{bindingError}}" kind="error" title="绑定状态加载失败" action="重试" bind:action="onShow" />
|
||||
<view class="j-profile-setting" bindtap="onShow" wx:if="{{!bindingKnown && !bindingError}}">
|
||||
<view class="setting-info"><view class="j-small-title">正在获取绑定状态</view><view class="j-caption">若加载失败,点击重试</view></view>
|
||||
<text class="j-link">›</text>
|
||||
</view>
|
||||
|
||||
<view class="j-profile-setting" bindtap="goBind" wx:if="{{bindingKnown && !hasPartner}}">
|
||||
<view class="setting-info">
|
||||
<view class="j-small-title" style="color:#B74768;">绑定情侣</view>
|
||||
<view class="j-caption">输入对方 ID 或分享邀请</view>
|
||||
</view>
|
||||
<text class="j-link">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 解除绑定(根据状态动态显示) -->
|
||||
<view class="j-profile-setting" bindtap="showUnbind" wx:if="{{bindingKnown && hasPartner}}">
|
||||
<view class="setting-info">
|
||||
<view wx:if="{{unbindStatus == 'pending' && unbindIsRequester}}" class="j-small-title" style="color:#FF9800;">取消解绑请求</view>
|
||||
<view wx:elif="{{unbindStatus == 'pending' && !unbindIsRequester}}" class="j-small-title" style="color:#FF4757;">待回应的解绑请求</view>
|
||||
<view wx:elif="{{unbindStatus == 'cooling'}}" class="j-small-title">72小时冷静期</view>
|
||||
<view wx:else class="j-small-title" style="color:#FF4757;">解除绑定</view>
|
||||
<view wx:if="{{unbindStatus == 'pending' && unbindIsRequester}}" class="j-caption">点击撤销已发起的请求</view>
|
||||
<view wx:elif="{{unbindStatus == 'pending' && !unbindIsRequester}}" class="j-caption">{{unbindRequesterName}} 发起了解绑</view>
|
||||
<view wx:elif="{{unbindStatus == 'cooling'}}" class="j-caption">点击查看倒计时或取消解绑</view>
|
||||
<view wx:else class="j-caption">同意后进入72小时冷静期</view>
|
||||
</view>
|
||||
<text class="j-link">›</text>
|
||||
</view>
|
||||
|
||||
<view class="section-heading">关于与帮助</view>
|
||||
<!-- 我们past -->
|
||||
<view class="j-profile-setting" bindtap="showAboutModal">
|
||||
<view class="setting-info">
|
||||
<view class="j-small-title">我们past</view>
|
||||
<view class="j-caption">了解本小程序的功能</view>
|
||||
</view>
|
||||
<text class="j-link">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 联系我们 -->
|
||||
<view class="j-profile-setting" bindtap="showContactModal">
|
||||
<view class="setting-info">
|
||||
<view class="j-small-title">联系我们</view>
|
||||
<view class="j-caption">软件功能建议与反馈</view>
|
||||
</view>
|
||||
<text class="j-link">›</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<!-- 背景图编辑页 - 全屏背景预览 -->
|
||||
<view class="edit-page" wx:if="{{showBgModal}}">
|
||||
<!-- 全屏背景 -->
|
||||
<image wx:if="{{bgPreview}}" src="{{bgPreview}}" mode="aspectFill" class="bg-full" catchtouchstart="onBgTouchStart" catchtouchend="onBgTouchEnd" />
|
||||
<view wx:else class="bg-placeholder" catchtouchstart="onBgTouchStart" catchtouchend="onBgTouchEnd"></view>
|
||||
|
||||
<!-- 左滑或点击空白区域返回 -->
|
||||
<view class="edit-nav" bindtap="hideBgModal">
|
||||
<view class="back-btn-wrap transparent">
|
||||
<image src="/images/back.svg" mode="aspectFit" class="back-nav-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<view class="bg-edit-footer">
|
||||
<view class="bg-edit-card">
|
||||
<text class="modal-subtitle" style="text-align:center;display:block;margin-bottom:24rpx;">上传你喜欢的背景图片</text>
|
||||
<view class="bg-btn-row">
|
||||
<button class="btn btn-select" bindtap="chooseBgImage" hover-class="none">📷 选择</button>
|
||||
<button class="btn btn-save" bindtap="saveBgImage" hover-class="none">💾 保存</button>
|
||||
<button class="btn btn-restore" bindtap="removeBgImage" hover-class="none">🔄 恢复</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息通知管理弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showSubscribeModal}}" bindtap="hideSubscribeModal">
|
||||
<view class="modal" catchtap="noop" style="max-height:75vh;overflow-y:auto;">
|
||||
<text class="modal-heart">🔔</text>
|
||||
<text class="title">消息通知</text>
|
||||
<text class="modal-subtitle">开启后,当TA有操作时会通过微信服务通知提醒你</text>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{subscribeLoading}}" class="subscribe-loading">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 通知开关 -->
|
||||
<view wx:else class="subscribe-section">
|
||||
<view class="subscribe-card {{subscribeChecked ? 'active' : ''}}">
|
||||
<view class="subscribe-card-left">
|
||||
<view class="subscribe-icon-wrap" style="background:{{subscribeChecked ? '#FFE4EC' : '#F5F5F5'}};">
|
||||
<text class="subscribe-icon">💕</text>
|
||||
</view>
|
||||
<view class="subscribe-info">
|
||||
<text class="subscribe-name">操作通知</text>
|
||||
<text class="subscribe-desc">TA有新的操作时通知你</text>
|
||||
</view>
|
||||
</view>
|
||||
<switch checked="{{subscribeChecked}}" bindchange="toggleSubscribe" color="#B74768" style="transform:scale(0.75);" />
|
||||
</view>
|
||||
|
||||
<!-- 状态提示 -->
|
||||
<view class="subscribe-tip {{subscribeChecked ? 'on' : 'off'}}">
|
||||
<block wx:if="{{subscribeChecked}}">
|
||||
<text class="tip-icon">✅</text>
|
||||
<text class="tip-text">已开启接收提醒,订阅次数用完后需要再次授权</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="tip-icon">💡</text>
|
||||
<text class="tip-text">开启并授权后,可接收一次TA的操作提醒</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<button wx:if="{{subscribeChecked}}" class="subscribe-renew" bindtap="renewSubscribe">订阅一次 TA 的提醒</button>
|
||||
|
||||
<!-- 说明 -->
|
||||
<view class="subscribe-note">
|
||||
<text class="note-title">📌 温馨提示</text>
|
||||
<text class="note-item">• 通知通过微信「服务通知」推送</text>
|
||||
<text class="note-item">• 你授权的是自己接收TA提醒的次数,不能替TA授权</text>
|
||||
<text class="note-item">• 每次授权可接收一次提醒,用完后可点击上方按钮续订</text>
|
||||
<text class="note-item">• 勾选「总是保持以上选择」后,后续点击授权可免重复弹窗,仍需逐次申请</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 固定标题和关闭按钮,长内容独立滚动 -->
|
||||
<view class="modal-overlay info-overlay" wx:if="{{showAboutModal}}" bindtap="hideAboutModal">
|
||||
<view class="modal info-dialog" catchtap="noop">
|
||||
<view class="info-header">
|
||||
<image class="info-mark" src="/images/book.svg" mode="aspectFit" />
|
||||
<text class="info-title">我们 past</text>
|
||||
<text class="info-caption">把日常,写成我们的故事</text>
|
||||
</view>
|
||||
<scroll-view class="info-body" scroll-y show-scrollbar="{{false}}">
|
||||
<view class="info-content">
|
||||
<text class="about-intro">属于两个人的小世界,珍藏每一次相遇,也记录平凡日常里的心动。</text>
|
||||
<view class="about-block">
|
||||
<text class="about-block-title">我们的生活手账</text>
|
||||
<text class="about-item">日记 · 写下心情与故事</text>
|
||||
<text class="about-item">相册 · 珍藏照片与美好瞬间</text>
|
||||
<text class="about-item">记忆日 · 记住每一个重要日子</text>
|
||||
<text class="about-item">歌曲 · 收藏一起听过的旋律</text>
|
||||
<text class="about-item">打卡 · 一起完成日常约定</text>
|
||||
<text class="about-item">宠物 · 照顾属于我们的伙伴</text>
|
||||
<text class="about-item">姨姨 · 记录生理期与身体状态</text>
|
||||
</view>
|
||||
<view class="about-block">
|
||||
<text class="about-block-title">让陪伴更近一点</text>
|
||||
<text class="about-item">情侣绑定 · 邀请另一半,共享记录</text>
|
||||
<text class="about-item">消息通知 · 订阅对方的动态提醒</text>
|
||||
<text class="about-item">首页背景 · 换成喜欢的照片</text>
|
||||
<text class="about-item">所在城市 · 分享彼此的位置</text>
|
||||
</view>
|
||||
<text class="about-version info-signature">让所有的爱有迹可循</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="info-footer"><button class="info-dismiss" bindtap="hideAboutModal">知道了</button></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系我们弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showContactModal}}" bindtap="hideContactModal">
|
||||
<view class="modal" catchtap="noop">
|
||||
<text class="modal-heart">💬</text>
|
||||
<text class="title">联系我们</text>
|
||||
<text class="modal-subtitle">软件功能建议与反馈</text>
|
||||
|
||||
<view class="about-section">
|
||||
<text class="about-intro">如果你在使用过程中有任何软件功能建议、优化想法或遇到的问题,欢迎随时联系我们,我们会认真听取每一位用户的声音,不断把「我们past」做得更好。💕</text>
|
||||
</view>
|
||||
|
||||
<view class="about-block">
|
||||
<text class="about-block-title">📮 联系微信</text>
|
||||
<text class="about-item">微信:e909e10</text>
|
||||
<text class="about-item">添加时请备注「我们past」,方便我们快速响应</text>
|
||||
</view>
|
||||
|
||||
<view class="about-foot">
|
||||
<text class="about-version">让所有的爱有迹可循</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,388 @@
|
||||
/* pages/settings/settings.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
.container { padding-top: 16rpx; padding-bottom: 300rpx; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
|
||||
|
||||
/* 全屏编辑页样式 */
|
||||
.edit-page {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bg-full {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
.bg-placeholder {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 0;
|
||||
background: linear-gradient(135deg, #F4E2E3 0%, #FFF0F5 50%, #F8E1FF 100%);
|
||||
}
|
||||
.edit-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 100rpx 0 0 32rpx;
|
||||
background: transparent;
|
||||
height: 180rpx;
|
||||
}
|
||||
.back-btn-wrap {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.75);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.back-btn-wrap.transparent {
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.back-nav-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.edit-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.edit-body {
|
||||
flex: 1;
|
||||
padding: 0 48rpx 120rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.back-btn {
|
||||
width: 56rpx; height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
.back-icon {
|
||||
width: 32rpx; height: 32rpx;
|
||||
}
|
||||
|
||||
/* 背景编辑页卡片 - 固定在底部导航栏上方 */
|
||||
.bg-edit-footer {
|
||||
position: fixed;
|
||||
bottom: 360rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.bg-edit-card {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border-radius: 32rpx;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 恢复默认背景按钮 */
|
||||
.btn-restore {
|
||||
background: transparent;
|
||||
color: #FF6B81;
|
||||
border: 4rpx solid #FF6B81;
|
||||
margin-top: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.btn-restore:active {
|
||||
background: rgba(255,107,129,0.08);
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* ===== 消息通知弹窗样式 ===== */
|
||||
.subscribe-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid #F4E2E3;
|
||||
border-top-color: #B74768;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.subscribe-section {
|
||||
margin: 24rpx 0 8rpx;
|
||||
}
|
||||
.subscribe-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #FAFAFA;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
transition: all 0.3s ease;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
.subscribe-card.active {
|
||||
background: #FFF5F7;
|
||||
border-color: #FFD6E0;
|
||||
}
|
||||
.subscribe-card-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.subscribe-icon-wrap {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.subscribe-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.subscribe-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.subscribe-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
.subscribe-desc {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.subscribe-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.subscribe-tip.on {
|
||||
background: #E8F5E9;
|
||||
color: #2E7D32;
|
||||
}
|
||||
.subscribe-tip.off {
|
||||
background: #FFF8E1;
|
||||
color: #F57F17;
|
||||
}
|
||||
.tip-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tip-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.subscribe-renew {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
margin: 24rpx 0 0;
|
||||
padding: 0 16rpx;
|
||||
border: none;
|
||||
border-radius: 20rpx;
|
||||
background: #B74768;
|
||||
color: #FFFFFF;
|
||||
font-family: inherit;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
box-shadow: none;
|
||||
}
|
||||
.subscribe-renew::after {
|
||||
border: none;
|
||||
}
|
||||
.subscribe-renew:active {
|
||||
background: #943C58;
|
||||
}
|
||||
|
||||
.subscribe-note {
|
||||
background: #F8F8F8;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.note-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.note-item {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 背景编辑页按钮 - 水平排列,统一大小 */
|
||||
.bg-btn-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.bg-btn-row .btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
padding: 0;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
text-align: center;
|
||||
line-height: 88rpx;
|
||||
margin: 0;
|
||||
}
|
||||
.bg-btn-row .btn:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
/* 三个按钮统一为半透明白色毛玻璃风格 */
|
||||
.bg-btn-row .btn-select,
|
||||
.bg-btn-row .btn-save,
|
||||
.bg-btn-row .btn-restore {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: #FF6B81;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 2rpx solid rgba(255, 107, 129, 0.25);
|
||||
box-shadow: none;
|
||||
}
|
||||
.bg-btn-row .btn-select:active,
|
||||
.bg-btn-row .btn-save:active,
|
||||
.bg-btn-row .btn-restore:active {
|
||||
background: rgba(255, 107, 129, 0.15);
|
||||
transform: scale(0.94);
|
||||
}
|
||||
.bg-edit-card .btn-restore:active {
|
||||
background: rgba(255, 107, 129, 0.12);
|
||||
}
|
||||
|
||||
/* ===== 关于我们 ===== */
|
||||
.about-section {
|
||||
margin: 28rpx 0 8rpx;
|
||||
}
|
||||
.about-intro {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.7;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
.about-block {
|
||||
background: #FFF9F4;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
margin-top: 24rpx;
|
||||
border: 2rpx solid rgba(255, 107, 157, 0.12);
|
||||
}
|
||||
.about-block-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #B74768;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.about-item {
|
||||
font-size: 24rpx;
|
||||
color: #555;
|
||||
display: block;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.about-foot {
|
||||
margin-top: 28rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.about-version {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* 弹窗位于页面滚动容器外,盖住底部导航。 */
|
||||
.modal-overlay { z-index:1200; }
|
||||
.info-dialog { display:flex; flex-direction:column; height:76vh; padding:0; overflow:hidden; max-width:650rpx; }
|
||||
.info-header { flex-shrink:0; display:flex; flex-direction:column; align-items:center; padding:36rpx 32rpx 24rpx; border-bottom:1rpx solid #E9DDD0; }
|
||||
.info-mark { width:48rpx; height:48rpx; margin-bottom:16rpx; }
|
||||
.info-title { font-size:34rpx; font-weight:600; color:#352E2A; }
|
||||
.info-caption { font-size:24rpx; color:#85776D; margin-top:10rpx; }
|
||||
.info-body { flex:1; height:0; min-height:0; }
|
||||
.info-content { padding:28rpx 32rpx; }
|
||||
.about-intro { color:#85776D; text-align:left; line-height:1.75; }
|
||||
.about-block { background:#FAF5EE; border:1rpx solid #E9DDD0; border-radius:16rpx; padding:24rpx; }
|
||||
.about-block-title { color:#352E2A; font-weight:600; }
|
||||
.about-item { color:#65594F; line-height:1.8; margin-top:8rpx; }
|
||||
.info-signature { display:block; margin-top:28rpx; text-align:center; color:#A39386; }
|
||||
.info-footer { flex-shrink:0; padding:20rpx 32rpx; border-top:1rpx solid #E9DDD0; }
|
||||
.info-dismiss { margin:0; width:100%; padding:0; line-height:76rpx; border-radius:16rpx; background:#B74768; color:#FFFDF8; font-size:28rpx; }
|
||||
.info-dismiss::after { border:0; }
|
||||
|
||||
/* 设置分组和弹窗标题延续主页面的端正手写风格。 */
|
||||
.section-heading,
|
||||
.info-title,
|
||||
.modal-subtitle,
|
||||
.about-block-title {
|
||||
font-family:'Kaiti SC','STKaiti','KaiTi',cursive;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
letter-spacing:1rpx;
|
||||
}
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,8 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
Page({
|
||||
data:{},
|
||||
onLoad(){this.setData(getLayout());},
|
||||
onResize(){this.setData(getLayout());},
|
||||
loadMore(){const card=this.selectComponent('#smoking');if(card)return card.loadMoreLogs();},
|
||||
async onPullDownRefresh(){try{const card=this.selectComponent('#smoking');if(card)await card.load(true);}finally{wx.stopPullDownRefresh();}}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"navigationStyle":"custom","enablePullDownRefresh":true,"usingComponents":{"app-nav":"/components/app-nav/app-nav","smoking-card":"/components/smoking-card/smoking-card"}}
|
||||
@@ -0,0 +1 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}"><launch-transition /><app-nav title="抽烟记录" back="{{true}}" /><scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}"><view class="c-body"><smoking-card id="smoking" detailed="{{true}}" /></view></scroll-view></view>
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/companion.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,208 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/timeline-detail/timeline-detail.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const { createSwipeBack } = require('../../utils/util');
|
||||
|
||||
// 重复周期映射
|
||||
var repeatUnitMap = { 'day': '天', 'week': '周', 'month': '月', 'year': '年' };
|
||||
var weekdayNames = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
|
||||
// 计算距离今天最近的重复日期
|
||||
function calcNearestRepeatDate(eventDate, today, interval, unit) {
|
||||
var base = new Date(eventDate);
|
||||
base.setHours(0,0,0,0);
|
||||
if (unit === 'day') {
|
||||
var diff = Math.floor((today - base) / 86400000);
|
||||
var cycles = Math.floor(diff / interval);
|
||||
var nearest = new Date(base.getTime() + cycles * interval * 86400000);
|
||||
nearest.setHours(0,0,0,0);
|
||||
if (nearest > today) {
|
||||
nearest = new Date(nearest.getTime() - interval * 86400000);
|
||||
nearest.setHours(0,0,0,0);
|
||||
}
|
||||
return nearest;
|
||||
} else if (unit === 'week') {
|
||||
var diff = Math.floor((today - base) / 86400000);
|
||||
var cycles = Math.floor(diff / (7 * interval));
|
||||
var nearest = new Date(base.getTime() + cycles * 7 * interval * 86400000);
|
||||
nearest.setHours(0,0,0,0);
|
||||
if (nearest > today) {
|
||||
nearest = new Date(nearest.getTime() - 7 * interval * 86400000);
|
||||
nearest.setHours(0,0,0,0);
|
||||
}
|
||||
return nearest;
|
||||
} else if (unit === 'month') {
|
||||
var nearest = new Date(base);
|
||||
nearest.setHours(0,0,0,0);
|
||||
while (true) {
|
||||
var next = new Date(nearest);
|
||||
next.setMonth(next.getMonth() + interval);
|
||||
next.setHours(0,0,0,0);
|
||||
if (next > today) break;
|
||||
nearest = next;
|
||||
}
|
||||
return nearest;
|
||||
} else if (unit === 'year') {
|
||||
var nearest = new Date(base);
|
||||
nearest.setHours(0,0,0,0);
|
||||
while (true) {
|
||||
var next = new Date(nearest);
|
||||
next.setFullYear(next.getFullYear() + interval);
|
||||
next.setHours(0,0,0,0);
|
||||
if (next > today) break;
|
||||
nearest = next;
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
return new Date(eventDate);
|
||||
}
|
||||
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
item: null,
|
||||
detailId: 0,
|
||||
bgImageUrl: '',
|
||||
daysNumber: '',
|
||||
daysSign: '',
|
||||
weekdayText: '',
|
||||
isAnniversary: true
|
||||
},
|
||||
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
var id = options.id;
|
||||
if (id) {
|
||||
this.setData({ detailId: parseInt(id) });
|
||||
this.loadDetail(parseInt(id));
|
||||
}
|
||||
var self = this;
|
||||
var swipe = createSwipeBack(function() { wx.navigateBack(); });
|
||||
this.onTouchStart = swipe.onTouchStart;
|
||||
this.onTouchEnd = swipe.onTouchEnd;
|
||||
},
|
||||
|
||||
retryDetail: function() { this.loadDetail(this.data.detailId); },
|
||||
onShow: function() {
|
||||
this._syncNavigationColor();
|
||||
if (this.data.detailId) {
|
||||
this.loadDetail(this.data.detailId);
|
||||
}
|
||||
},
|
||||
|
||||
onHide: function() {
|
||||
if (wx.setNavigationBarColor) wx.setNavigationBarColor({frontColor:'#000000',backgroundColor:'#FDE4EC'});
|
||||
},
|
||||
_syncNavigationColor: function() {
|
||||
if (wx.setNavigationBarColor) wx.setNavigationBarColor({frontColor:this.data.bgImageUrl?'#ffffff':'#000000',backgroundColor:this.data.bgImageUrl?'#000000':'#FDE4EC'});
|
||||
},
|
||||
goBack: function() {
|
||||
wx.navigateBack({fail:function(){wx.switchTab({url:'/pages/discover/discover'});}});
|
||||
},
|
||||
|
||||
loadDetail: async function(id) {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
var item = await api.timelineDetail(id);
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var cid = parseInt(item.created_by) || 0;
|
||||
|
||||
// 获取情侣信息
|
||||
try {
|
||||
var info = await api.coupleInfo();
|
||||
var p1 = info.partner1 || {};
|
||||
var p2 = info.partner2 || {};
|
||||
var nameMap = {};
|
||||
var avatarMap = {};
|
||||
if (p1.id) { nameMap[p1.id] = p1.nickname || p1.username || 'TA'; avatarMap[p1.id] = getImageUrl(p1.avatar); }
|
||||
if (p2.id) { nameMap[p2.id] = p2.nickname || p2.username || 'TA'; avatarMap[p2.id] = getImageUrl(p2.avatar); }
|
||||
item.creator_name = nameMap[cid] || (cid == myUid ? '我' : 'TA');
|
||||
item.creator_avatar = avatarMap[cid] || '';
|
||||
} catch(e) {
|
||||
item.creator_name = cid == myUid ? '我' : 'TA';
|
||||
item.creator_avatar = '';
|
||||
}
|
||||
item.creator_initial = (item.creator_name || '?')[0].toUpperCase();
|
||||
|
||||
// 计算天数
|
||||
var today = new Date(); today.setHours(0,0,0,0);
|
||||
var daysNumber = '';
|
||||
var daysSign = '';
|
||||
var isAnniversary = true;
|
||||
|
||||
if (item.event_date) {
|
||||
var ed = new Date(item.event_date.replace(/-/g, '/'));
|
||||
ed.setHours(0,0,0,0);
|
||||
var repeatInterval = parseInt(item.repeat_interval) || 0;
|
||||
var repeatUnit = item.repeat_unit || '';
|
||||
|
||||
var diff;
|
||||
if (repeatInterval > 0 && repeatUnit) {
|
||||
var baseDate = calcNearestRepeatDate(ed, today, repeatInterval, repeatUnit);
|
||||
diff = Math.floor((today - baseDate) / 86400000);
|
||||
isAnniversary = diff >= 0;
|
||||
daysSign = isAnniversary ? '+' : '-';
|
||||
daysNumber = Math.abs(diff);
|
||||
item.repeat_label = repeatUnitMap[repeatUnit] + '重复';
|
||||
} else {
|
||||
diff = Math.floor((today - ed) / 86400000);
|
||||
isAnniversary = diff >= 0;
|
||||
daysSign = isAnniversary ? '+' : '-';
|
||||
daysNumber = Math.abs(diff);
|
||||
}
|
||||
}
|
||||
|
||||
// 星期
|
||||
var eventDateObj = new Date(item.event_date.replace(/-/g, '/'));
|
||||
var weekdayText = weekdayNames[eventDateObj.getDay()] || '';
|
||||
|
||||
item.is_anniversary = isAnniversary;
|
||||
this.setData({
|
||||
item: item,
|
||||
daysNumber: daysNumber,
|
||||
daysSign: daysSign,
|
||||
weekdayText: weekdayText,
|
||||
isAnniversary: isAnniversary
|
||||
});
|
||||
|
||||
// 从API数据加载背景图
|
||||
this.loadBgImage();
|
||||
|
||||
} catch(e) {
|
||||
this.setData({loadError:true});
|
||||
console.error(e);
|
||||
wx.showToast({ title: (e && (e.error || e.errMsg || e.message)) || '加载失败', icon: 'none' });
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
// ===== 背景图片 =====
|
||||
loadBgImage: function() {
|
||||
var item = this.data.item;
|
||||
if (item && item.bg_image) {
|
||||
var fullUrl = getImageUrl(item.bg_image);
|
||||
this.setData({ bgImageUrl: fullUrl });
|
||||
} else {
|
||||
this.setData({ bgImageUrl: '' });
|
||||
}
|
||||
this._syncNavigationColor();
|
||||
},
|
||||
|
||||
// ===== 编辑 =====
|
||||
goEdit: function() {
|
||||
var item = this.data.item;
|
||||
if (item && item.id) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/publish/publish?type=timeline&edit=' + item.id
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 导航 =====
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadDetail": ["timelineDetail", "coupleInfo"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<view class="app-shell app-shell--secondary {{bgImageUrl ? 'detail-immersive' : ''}}" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/timeline-detail/timeline-detail.wxml -->
|
||||
<page-meta page-style="{{bgImageUrl ? 'background: transparent;' : ''}}"></page-meta>
|
||||
<!-- 背景图片(固定全屏,参考首页实现) -->
|
||||
<image wx:if="{{bgImageUrl}}" src="{{bgImageUrl}}" mode="aspectFill" class="detail-bg-custom" />
|
||||
<view class="detail-bg-overlay" wx:if="{{bgImageUrl}}"></view>
|
||||
|
||||
<app-nav wx:if="{{!bgImageUrl}}" title="记忆日详情" back="{{true}}" />
|
||||
<view wx:else class="detail-photo-nav" style="height:{{barHeight}}px;" bindtap="goBack" aria-label="返回"><image src="/images/back-white.svg" mode="aspectFit" /></view>
|
||||
|
||||
<scroll-view class="page-bg {{bgImageUrl ? 'page-bg-has-bg' : ''}}" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" style="height:100vh;"
|
||||
bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
|
||||
<view class="container {{bgImageUrl ? 'has-bg' : ''}}" bindtap="goEdit">
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载详情" /><page-state wx:elif="{{loadError}}" kind="error" title="详情加载失败" action="重试" bind:action="retryDetail" />
|
||||
<block wx:else>
|
||||
|
||||
<!-- 大大的天数 -->
|
||||
<view class="detail-hero">
|
||||
<view class="detail-hero-num-wrap">
|
||||
<text class="detail-hero-num">{{daysNumber}}</text>
|
||||
<text class="detail-hero-sign">{{daysSign}}</text>
|
||||
</view>
|
||||
<!-- <text class="detail-hero-unit">天</text> -->
|
||||
<view class="detail-hero-bar"></view>
|
||||
</view>
|
||||
|
||||
<!-- 事件标题 -->
|
||||
<view class="detail-title-wrap" wx:if="{{item.title}}">
|
||||
<text class="detail-title">{{item.title}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 地点 . 备注 同排 -->
|
||||
<view class="detail-info-line" wx:if="{{item.location || item.content}}">
|
||||
<text>{{item.location}}{{item.location && item.content ? ' . ' : ''}}{{item.content}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 标签 + 日期同排 -->
|
||||
<view class="detail-meta-line">
|
||||
<text class="detail-meta-tag">{{isAnniversary ? '纪念日' : '目标日'}} {{item.event_date}}({{weekdayText}})</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,188 @@
|
||||
/* pages/timeline-detail/timeline-detail.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; }
|
||||
|
||||
.container { padding: 380rpx 24rpx 180rpx; position: relative; z-index: 1; }
|
||||
.page-bg-has-bg .container { padding-top: 400rpx; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; padding-top: 0; }
|
||||
.page-bg-has-bg { background: transparent !important; }
|
||||
.page-bg-has-bg::after { display: none !important; }
|
||||
|
||||
/* ========== 背景图片(参考首页实现) ========== */
|
||||
.detail-bg-custom {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.detail-bg-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
background: rgba(0,0,0,0.45);
|
||||
pointer-events: none;
|
||||
}
|
||||
.has-bg .detail-hero-num,
|
||||
.has-bg .detail-hero-sign,
|
||||
.has-bg .detail-hero-unit,
|
||||
.has-bg .detail-hero-bar,
|
||||
.has-bg .detail-title,
|
||||
.has-bg .detail-info-line text,
|
||||
.has-bg .detail-meta-tag,
|
||||
.has-bg .detail-meta-date,
|
||||
.has-bg .detail-repeat text {
|
||||
color: rgba(255,255,255,0.9) !important;
|
||||
}
|
||||
.has-bg .detail-hero-bar {
|
||||
background: rgba(255,255,255,0.35) !important;
|
||||
}
|
||||
.has-bg .detail-repeat {
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
.tab-bar-transparent {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.top-nav-card-transparent {
|
||||
background: transparent !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
.nav-back-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.top-nav-card-transparent .nav-back-icon {
|
||||
filter: brightness(0) invert(1) !important;
|
||||
}
|
||||
|
||||
/* 无背景图时,top-nav-card 背景与 page 背景融合 */
|
||||
.top-nav-card:not(.top-nav-card-transparent) {
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ========== 主区域(点击编辑) ========== */
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
/* ========== 大大的天数 ========== */
|
||||
.detail-hero {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.detail-hero-num-wrap {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
}
|
||||
.detail-hero-num {
|
||||
font-size: 120rpx;
|
||||
font-weight: 900;
|
||||
color: #B74768;
|
||||
line-height: 1;
|
||||
display: inline;
|
||||
}
|
||||
.detail-hero-sign {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #B74768;
|
||||
vertical-align: super;
|
||||
line-height: 1;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
.detail-hero-unit {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #B74768;
|
||||
margin-left: 8rpx;
|
||||
display: inline;
|
||||
}
|
||||
.detail-hero-bar {
|
||||
width: 400rpx;
|
||||
height: 4rpx;
|
||||
background: #B74768;
|
||||
opacity: 0.25;
|
||||
border-radius: 4rpx;
|
||||
margin: 40rpx auto 0;
|
||||
}
|
||||
.page-bg-has-bg .detail-hero-bar {
|
||||
background: rgba(255,255,255,0.5) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* ========== 事件标题 ========== */
|
||||
.detail-title-wrap {
|
||||
text-align: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.detail-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ========== 信息行 ========== */
|
||||
.detail-info-line {
|
||||
text-align: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.detail-info-line text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ========== 重复标签 ========== */
|
||||
.detail-repeat {
|
||||
display: inline-block;
|
||||
padding: 8rpx 24rpx;
|
||||
background: #FFF0F3;
|
||||
border-radius: 30rpx;
|
||||
font-size: 22rpx;
|
||||
color: #B74768;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ========== 空状态 ========== */
|
||||
.empty-state { text-align: center; padding: 80rpx; color: #999; font-size: 26rpx; }
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
/* 有背景图时采用全屏照片布局,顶部不再显示色块与标题。 */
|
||||
.detail-immersive { background:transparent!important; }
|
||||
.detail-photo-nav { position:fixed; top:var(--status-height,20px); left:16rpx; width:80rpx; display:flex; align-items:center; justify-content:center; z-index:900; }
|
||||
.detail-photo-nav image { width:36rpx; height:36rpx; }
|
||||
.detail-immersive .page-bg { padding-top:0!important; }
|
||||
.detail-immersive .page-bg > .container {
|
||||
padding:25vh 48rpx calc(56rpx + env(safe-area-inset-bottom))!important;
|
||||
min-height:100vh;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
.detail-immersive .detail-bg-overlay { background:rgba(0,0,0,.42); }
|
||||
.detail-immersive .detail-hero { margin-bottom:36rpx; }
|
||||
.detail-immersive .detail-hero-bar { margin-top:40rpx; }
|
||||
.detail-immersive .detail-title-wrap { margin-bottom:24rpx; }
|
||||
.detail-immersive .detail-info-line { margin-bottom:12rpx; }
|
||||
.detail-immersive .detail-title,
|
||||
.detail-immersive .detail-info-line,
|
||||
.detail-immersive .detail-meta-line { max-width:100%; overflow-wrap:break-word; text-shadow:0 2rpx 8rpx rgba(0,0,0,.2); }
|
||||
@@ -0,0 +1,195 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/timeline/timeline.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
|
||||
// 重复周期映射
|
||||
var repeatUnitMap = { 'day': '天', 'week': '周', 'month': '月', 'year': '年' };
|
||||
|
||||
// 计算距离今天最近的重复日期(返回 Date 对象,已归零)
|
||||
function calcNearestRepeatDate(eventDate, today, interval, unit) {
|
||||
var base = new Date(eventDate);
|
||||
base.setHours(0,0,0,0);
|
||||
if (unit === 'day') {
|
||||
var diff = Math.floor((today - base) / 86400000);
|
||||
var cycles = Math.floor(diff / interval);
|
||||
var nearest = new Date(base.getTime() + cycles * interval * 86400000);
|
||||
nearest.setHours(0,0,0,0);
|
||||
if (nearest > today) {
|
||||
nearest = new Date(nearest.getTime() - interval * 86400000);
|
||||
nearest.setHours(0,0,0,0);
|
||||
}
|
||||
return nearest;
|
||||
} else if (unit === 'week') {
|
||||
var diff = Math.floor((today - base) / 86400000);
|
||||
var cycles = Math.floor(diff / (7 * interval));
|
||||
var nearest = new Date(base.getTime() + cycles * 7 * interval * 86400000);
|
||||
nearest.setHours(0,0,0,0);
|
||||
if (nearest > today) {
|
||||
nearest = new Date(nearest.getTime() - 7 * interval * 86400000);
|
||||
nearest.setHours(0,0,0,0);
|
||||
}
|
||||
return nearest;
|
||||
} else if (unit === 'month') {
|
||||
var nearest = new Date(base);
|
||||
nearest.setHours(0,0,0,0);
|
||||
while (true) {
|
||||
var next = new Date(nearest);
|
||||
next.setMonth(next.getMonth() + interval);
|
||||
next.setHours(0,0,0,0);
|
||||
if (next > today) break;
|
||||
nearest = next;
|
||||
}
|
||||
return nearest;
|
||||
} else if (unit === 'year') {
|
||||
var nearest = new Date(base);
|
||||
nearest.setHours(0,0,0,0);
|
||||
while (true) {
|
||||
var next = new Date(nearest);
|
||||
next.setFullYear(next.getFullYear() + interval);
|
||||
next.setHours(0,0,0,0);
|
||||
if (next > today) break;
|
||||
nearest = next;
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
return new Date(eventDate);
|
||||
}
|
||||
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
list: []
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.loadData();
|
||||
},
|
||||
onShow: function() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadData();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
onShareAppMessage: function() {
|
||||
return { title: '让所有的爱有迹可循', path: '/pages/timeline/timeline', 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 {
|
||||
const list = await api.timelineList({page:ticket.page,page_size:20});
|
||||
const myUid = wx.getStorageSync('userId');
|
||||
var nameMap = {};
|
||||
var avatarMap = {};
|
||||
try {
|
||||
const info = await api.coupleInfo();
|
||||
var p1 = info.partner1 || {};
|
||||
var p2 = info.partner2 || {};
|
||||
if (p1.id) { nameMap[p1.id] = p1.nickname || p1.username || 'TA'; avatarMap[p1.id] = getImageUrl(p1.avatar); }
|
||||
if (p2.id) { nameMap[p2.id] = p2.nickname || p2.username || 'TA'; avatarMap[p2.id] = getImageUrl(p2.avatar); }
|
||||
} catch (e) {}
|
||||
|
||||
var today = new Date(); today.setHours(0,0,0,0);
|
||||
var items = itemsOf(list).map(function(item, idx) {
|
||||
var cid = parseInt(item.created_by) || 0;
|
||||
item.creator_name = nameMap[cid] || (cid == myUid ? '我' : 'TA');
|
||||
item.creator_avatar = avatarMap[cid] || '';
|
||||
item.creator_initial = (item.creator_name || '?')[0].toUpperCase();
|
||||
item.side = ((ticket.page-1)*20+idx) % 2 === 0 ? 'left' : 'right';
|
||||
item.bg_image_url = item.bg_image ? getImageUrl(item.bg_image) : '';
|
||||
|
||||
// 计算纪念日/倒数日(支持重复事件)
|
||||
if (item.event_date) {
|
||||
var ed = new Date(item.event_date.replace(/-/g, '/'));
|
||||
ed.setHours(0,0,0,0);
|
||||
var repeatInterval = parseInt(item.repeat_interval) || 0;
|
||||
var repeatUnit = item.repeat_unit || '';
|
||||
|
||||
// 计算实际用来对比的基准日期
|
||||
if (repeatInterval > 0 && repeatUnit) {
|
||||
// 重复事件:以今天的日期为基准,计算最近一次重复的日期
|
||||
var baseDate = calcNearestRepeatDate(ed, today, repeatInterval, repeatUnit);
|
||||
var diff = Math.floor((today - baseDate) / 86400000);
|
||||
if (diff >= 0) {
|
||||
item.is_anniversary = true;
|
||||
item.days_text = '已过去 ' + diff + ' 天';
|
||||
item.date_tag = '纪念日';
|
||||
item.repeat_label = repeatUnitMap[repeatUnit] + '重复';
|
||||
} else {
|
||||
item.is_anniversary = false;
|
||||
item.days_text = '还有 ' + Math.abs(diff) + ' 天';
|
||||
item.date_tag = '倒数日';
|
||||
item.repeat_label = repeatUnitMap[repeatUnit] + '重复';
|
||||
}
|
||||
} else {
|
||||
// 非重复事件:直接用事件日期计算
|
||||
var diff = Math.floor((today - ed) / 86400000);
|
||||
if (diff >= 0) {
|
||||
item.is_anniversary = true;
|
||||
item.days_text = '已过去 ' + diff + ' 天';
|
||||
item.date_tag = '纪念日';
|
||||
} else {
|
||||
item.is_anniversary = false;
|
||||
item.days_text = '还有 ' + Math.abs(diff) + ' 天';
|
||||
item.date_tag = '倒数日';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item.is_anniversary = true;
|
||||
item.days_text = '';
|
||||
}
|
||||
return item;
|
||||
});
|
||||
applyList(this, ticket, list, items, 'list');
|
||||
} catch (e) {
|
||||
failList(this,ticket);
|
||||
console.error(e);
|
||||
wx.showToast({ title: (e && (e.error || e.errMsg || e.message)) || '加载失败', icon: 'none' });
|
||||
}
|
||||
finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
|
||||
deleteTimeline: async function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这条记录吗?',
|
||||
success: async function(res) {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await api.timelineDelete({ id: id });
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
// need to re-get this context
|
||||
var pages = getCurrentPages();
|
||||
var page = pages[pages.length - 1];
|
||||
page.loadData();
|
||||
} catch (err) {
|
||||
wx.showToast({ title: err.error || '删除失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, goToPublish: function() { wx.navigateTo({ url: '/pages/publish/publish?type=timeline' }); },
|
||||
|
||||
goToDetail: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
if (id) {
|
||||
wx.navigateTo({ url: '/pages/timeline-detail/timeline-detail?id=' + id });
|
||||
}
|
||||
},
|
||||
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, 'loadData'), {"loadData": ["timelineList", "coupleInfo"]}));
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/timeline/timeline.wxml - 左右交替记忆日 -->
|
||||
<app-nav title="记忆日" back="{{true}}" />
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
|
||||
|
||||
<scroll-view class="page-bg tl-scroll" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="container"><view class="j-row"><view><text class="j-eyebrow">MEMORIES OF US</text><text class="j-page-title">值得记住的日子</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 记忆日</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="tl-list" wx:if="{{!loading && !loadError && list.length > 0}}">
|
||||
<view class="tl-item {{item.side}}" wx:for="{{list}}" wx:key="id" bindtap="goToDetail" bindlongpress="deleteTimeline" data-id="{{item.id}}">
|
||||
<!-- 背景图片 -->
|
||||
<view class="tl-bg-wrap" wx:if="{{item.bg_image_url}}">
|
||||
<image class="tl-bg" src="{{item.bg_image_url}}" mode="aspectFill" />
|
||||
<view class="tl-bg-overlay"></view>
|
||||
</view>
|
||||
<view class="tl-card {{item.is_anniversary ? '' : 'tl-card-countdown'}} {{item.bg_image_url ? 'tl-card-has-bg' : ''}}">
|
||||
<view class="tl-creator">
|
||||
<image wx:if="{{item.creator_avatar}}" class="tl-avatar" src="{{item.creator_avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="tl-avatar-placeholder">{{item.creator_initial}}</view>
|
||||
<text class="tl-name">{{item.creator_name}}</text>
|
||||
<text class="tl-date-tag {{item.is_anniversary ? 'tl-date-tag-anniversary' : 'tl-date-tag-countdown'}}">{{item.date_tag}}</text>
|
||||
<text class="tl-date">{{item.event_date}}</text>
|
||||
</view>
|
||||
<view class="tl-card-head">
|
||||
<text class="tl-title">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="tl-card-head" wx:if="{{item.location}}">
|
||||
<text class="tl-location">📍 {{item.location}}</text>
|
||||
</view>
|
||||
<view class="tl-card-head" wx:if="{{item.content}}">
|
||||
<text class="tl-note">💬 {{item.content}}</text>
|
||||
</view>
|
||||
<view class="tl-days-wrap">
|
||||
<text class="tl-repeat" wx:if="{{item.repeat_label}}">{{item.repeat_label}}</text>
|
||||
<text class="tl-days {{item.is_anniversary ? '' : 'tl-days-countdown'}}">{{item.days_text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" wx:if="{{!loading && !loadError && !list.length}}">
|
||||
<text style="font-size:72rpx;">📅</text>
|
||||
<text>还没有记忆日事件</text>
|
||||
<text style="font-size:24rpx;color:#bbb;">点击上方添加记忆日</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>
|
||||
@@ -0,0 +1,157 @@
|
||||
/* pages/timeline/timeline.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; }
|
||||
.container { padding: 16rpx 24rpx 300rpx; }
|
||||
.tl-scroll { height: 100vh; }
|
||||
|
||||
|
||||
/* ===== 列表 ===== */
|
||||
.tl-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
|
||||
/* ===== 卡片(与日记卡片等宽)===== */
|
||||
.tl-item {
|
||||
width: 100%;
|
||||
}
|
||||
.tl-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
.tl-card-countdown {
|
||||
background: linear-gradient(135deg, #F5F7FF, #EEF2FF);
|
||||
border-color: #D6E0FF;
|
||||
}
|
||||
|
||||
/* ===== 用户信息 + 日期并行 ===== */
|
||||
.tl-creator {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.tl-item.left .tl-creator { justify-content: flex-start; }
|
||||
.tl-item.right .tl-creator { justify-content: flex-start; flex-direction: row-reverse; }
|
||||
.tl-avatar { width: 72rpx; height: 72rpx; border-radius: 50%; flex-shrink: 0; align-self: center; }
|
||||
.tl-avatar-placeholder {
|
||||
width: 72rpx; height: 72rpx; border-radius: 50%; flex-shrink: 0; align-self: center;
|
||||
background: linear-gradient(135deg, #F4E2E3, #F0E6FF);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 22rpx; font-weight: 700; color: #FF6B81;
|
||||
}
|
||||
.tl-name { font-size: 28rpx; color: #333; font-weight: 600; flex-shrink: 0; }
|
||||
.tl-date { font-size: 22rpx; color: #999; white-space: nowrap; }
|
||||
.tl-date-tag { font-size: 22rpx; white-space: nowrap; margin-right: 6rpx; }
|
||||
.tl-date-tag-anniversary { color: #FF6B81; }
|
||||
.tl-date-tag-countdown { color: #6B8CFF; }
|
||||
.tl-location { font-size: 22rpx; color: #999; }
|
||||
.tl-note { font-size: 24rpx; color: #888; line-height: 1.5; }
|
||||
|
||||
/* ===== 内容(与用户信息反方向)===== */
|
||||
.tl-card-head {
|
||||
display: flex;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
.tl-card-head:first-of-type {
|
||||
margin-top: 14rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.tl-item.left .tl-card-head { justify-content: flex-end; }
|
||||
.tl-item.right .tl-card-head { justify-content: flex-start; }
|
||||
.tl-title { font-size: 30rpx; font-weight: 700; color: #333; }
|
||||
|
||||
/* ===== 已过去天数 ===== */
|
||||
.tl-days-wrap {
|
||||
display: flex;
|
||||
margin-top: 16rpx;
|
||||
padding-top: 14rpx;
|
||||
border-top: 1rpx dashed #ffe0e5;
|
||||
}
|
||||
.tl-item.left .tl-days-wrap { justify-content: flex-end; }
|
||||
.tl-item.right .tl-days-wrap { justify-content: flex-start; }
|
||||
.tl-days {
|
||||
display: inline-block;
|
||||
font-size: 22rpx;
|
||||
color: #FF6B81;
|
||||
background: linear-gradient(135deg, #FFF0F5, #F4E2E3);
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tl-days-countdown {
|
||||
color: #6B8CFF;
|
||||
background: linear-gradient(135deg, #EEF0FF, #E4ECFF);
|
||||
}
|
||||
.tl-repeat {
|
||||
display: inline-block;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
background: #f5f5f5;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-right: 10rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ===== 列表背景图 ===== */
|
||||
.tl-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tl-bg-wrap {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tl-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.tl-bg-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
}
|
||||
.tl-card-has-bg {
|
||||
background: transparent !important;
|
||||
border-color: rgba(255,255,255,0.3) !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff;
|
||||
}
|
||||
.tl-card-has-bg .tl-title,
|
||||
.tl-card-has-bg .tl-name,
|
||||
.tl-card-has-bg .tl-note,
|
||||
.tl-card-has-bg .tl-location,
|
||||
.tl-card-has-bg .tl-date {
|
||||
color: #fff !important;
|
||||
}
|
||||
.tl-card-has-bg .tl-repeat {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: rgba(255,255,255,0.85);
|
||||
}
|
||||
.tl-card-has-bg .tl-days {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: #fff !important;
|
||||
}
|
||||
.tl-card-has-bg .tl-card-head:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
.tl-card-has-bg .tl-days-wrap {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
.tl-card { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:16rpx; box-shadow:none; }.tl-card-countdown { background:#FFFDF8; border-color:#D9BBB0; }.tl-creator { flex-wrap:wrap; align-items:center; }.tl-item.right .tl-creator { flex-direction:row; }.tl-name { max-width:240rpx; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }.tl-card-head,.tl-days-wrap { justify-content:flex-start!important; }.tl-title { overflow-wrap:break-word; }.tl-days-countdown { color:#B74768; background:#F4E2E3; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,186 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/unbind/unbind.js - 情侣解绑确认页
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
Page({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
status: 'none',
|
||||
isRequester: false,
|
||||
requesterName: '',
|
||||
remaining: 0,
|
||||
timerText: '',
|
||||
createdTime: '',
|
||||
respondedTime: '',
|
||||
completedTime: '',
|
||||
_timer: null,
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
this._visible = true;
|
||||
// 每次页面显示时重新加载状态(对方可能已经操作了)
|
||||
this.loadStatus();
|
||||
},
|
||||
|
||||
onHide: function() { this._visible = false; this.stopTimers(); },
|
||||
onUnload: function() { this._visible = false; this.stopTimers(); },
|
||||
stopTimers: function() {
|
||||
if (this.data._timer) clearInterval(this.data._timer);
|
||||
if (this.data._pollTimer) clearInterval(this.data._pollTimer);
|
||||
this.data._timer = null;
|
||||
this.data._pollTimer = null;
|
||||
},
|
||||
|
||||
loadStatus: function() {
|
||||
if (this._statusLoading) return;
|
||||
this._statusLoading = true;
|
||||
var self = this;
|
||||
self.stopTimers();
|
||||
self.setData({ loading: true, loadError:false });
|
||||
api.unbindStatus().then(function(res) {
|
||||
var status = res.status || 'none';
|
||||
var remaining = res.remaining_seconds || 0;
|
||||
var timerText = self.formatRemaining(remaining);
|
||||
var createdTime = res.created_at ? res.created_at.substring(0, 16).replace('T', ' ') : '';
|
||||
var respondedTime = res.responded_at ? res.responded_at.substring(0, 16).replace('T', ' ') : '';
|
||||
|
||||
self.setData({
|
||||
loading: false,
|
||||
status: status,
|
||||
isRequester: !!res.is_requester,
|
||||
requesterName: res.requester_name || '对方',
|
||||
remaining: remaining,
|
||||
timerText: timerText,
|
||||
createdTime: createdTime,
|
||||
respondedTime: respondedTime,
|
||||
completedTime: res.completed_at ? res.completed_at.substring(0, 16).replace('T', ' ') : '',
|
||||
});
|
||||
|
||||
if (!self._visible) return;
|
||||
|
||||
// 清除之前的轮询
|
||||
if (self.data._pollTimer) {
|
||||
clearInterval(self.data._pollTimer);
|
||||
self.data._pollTimer = null;
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
wx.removeStorageSync('coupleId'); wx.removeStorageSync('petHasPet');
|
||||
require('../../utils/data-cache').clear();
|
||||
return;
|
||||
}
|
||||
if ((status === 'pending' || status === 'cooling') && remaining > 0) {
|
||||
self.startTimer(remaining);
|
||||
self.data._pollTimer = setInterval(function() { self.loadStatus(); }, 10000);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
self.setData({ loading: false, loadError:true });
|
||||
}).finally(function() { self._statusLoading = false; });
|
||||
},
|
||||
|
||||
startTimer: function(seconds) {
|
||||
var self = this;
|
||||
if (self.data._timer) clearInterval(self.data._timer);
|
||||
self._deadline = Date.now() + seconds * 1000;
|
||||
self.data._remaining = seconds;
|
||||
self.data._timer = setInterval(function() {
|
||||
self.data._remaining = Math.max(0, Math.ceil((self._deadline - Date.now()) / 1000));
|
||||
if (self.data._remaining <= 0) {
|
||||
clearInterval(self.data._timer);
|
||||
self.loadStatus();
|
||||
return;
|
||||
}
|
||||
self.setData({ timerText: self.formatRemaining(self.data._remaining) });
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
formatRemaining: function(seconds) {
|
||||
if (seconds <= 0) return '已过期';
|
||||
var h = Math.floor(seconds / 3600);
|
||||
var m = Math.floor((seconds % 3600) / 60);
|
||||
var s = seconds % 60;
|
||||
return h + '时 ' + m + '分 ' + s + '秒';
|
||||
},
|
||||
|
||||
// 同意解绑
|
||||
agreeUnbind: function() {
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '💔 确认解绑',
|
||||
content: '同意后进入72小时冷静期,期间任一方可以取消。到期自动解除关系并永久删除共同数据,个人抽烟记录保留。',
|
||||
cancelText: '再想想',
|
||||
confirmText: '确认解绑',
|
||||
confirmColor: '#FF4757',
|
||||
success: function(res) {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
api.unbindRespond({ agree: true }).then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已进入72小时冷静期', icon: 'none' });
|
||||
self.loadStatus();
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 拒绝解绑
|
||||
rejectUnbind: function() {
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '拒绝解绑',
|
||||
content: '确定拒绝对方的解绑请求吗?',
|
||||
cancelText: '取消',
|
||||
confirmText: '拒绝解绑',
|
||||
success: function(res) {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
api.unbindRespond({ agree: false }).then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已拒绝解绑', icon: 'success' });
|
||||
self.setData({ status: 'rejected' });
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 撤销解绑请求
|
||||
cancelUnbind: function() {
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '撤销请求',
|
||||
content: '确定撤销解绑请求吗?',
|
||||
cancelText: '取消',
|
||||
confirmText: '撤销',
|
||||
success: function(res) {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
api.unbindCancel().then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已撤销', icon: 'success' });
|
||||
self.loadStatus();
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); },
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "解除绑定",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/unbind/unbind.wxml - 情侣解绑确认页 -->
|
||||
<app-nav title="解绑" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="container" style="padding-top:120rpx;">
|
||||
|
||||
<!-- 加载中 -->
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在获取解绑状态" /><page-state wx:elif="{{loadError}}" kind="error" title="解绑状态加载失败" action="重试" bind:action="loadStatus" />
|
||||
|
||||
<!-- 无解绑请求 -->
|
||||
<view class="empty-state" wx:elif="{{!loading && !loadError && status == 'none'}}">
|
||||
<text class="empty-icon">💕</text>
|
||||
<text class="empty-title">当前没有解绑请求</text>
|
||||
<text class="empty-desc">在设置页面可以发起解绑</text>
|
||||
</view>
|
||||
|
||||
<!-- 待对方确认(我是发起方) -->
|
||||
<view class="unbind-card" wx:elif="{{!loading && !loadError && status == 'pending' && isRequester}}">
|
||||
<view class="unbind-icon">⏳</view>
|
||||
<text class="unbind-title">解绑请求已发送</text>
|
||||
<text class="unbind-desc">等待对方确认同意解绑</text>
|
||||
<view class="unbind-timer" wx:if="{{remaining > 0}}">
|
||||
<text class="timer-label">剩余时间</text>
|
||||
<text class="timer-value">{{timerText}}</text>
|
||||
</view>
|
||||
<view class="unbind-info">
|
||||
<text>发起时间:{{createdTime}}</text>
|
||||
<text>状态:待对方确认</text>
|
||||
</view>
|
||||
<button class="btn btn-outline" bindtap="cancelUnbind" style="margin-top:32rpx;border-color:#FF4757;color:#FF4757;">撤销解绑请求</button>
|
||||
</view>
|
||||
|
||||
<!-- 待我确认(我是对方) -->
|
||||
<view class="unbind-card" wx:elif="{{!loading && !loadError && status == 'pending' && !isRequester}}">
|
||||
<view class="unbind-icon">💔</view>
|
||||
<text class="unbind-title">{{requesterName}} 发起了解绑请求</text>
|
||||
<text class="unbind-desc">对方希望解除情侣绑定,请确认或拒绝</text>
|
||||
<view class="unbind-timer" wx:if="{{remaining > 0}}">
|
||||
<text class="timer-label">剩余时间</text>
|
||||
<text class="timer-value">{{timerText}}</text>
|
||||
</view>
|
||||
<view class="unbind-info">
|
||||
<text>发起时间:{{createdTime}}</text>
|
||||
<text>同意后进入72小时冷静期,期间任一方可取消</text>
|
||||
</view>
|
||||
<view class="unbind-actions" style="margin-top:32rpx;">
|
||||
<button class="btn btn-primary" bindtap="agreeUnbind" style="background:#FF4757;">💔 同意解绑</button>
|
||||
<button class="btn btn-outline" bindtap="rejectUnbind" style="margin-top:16rpx;">拒绝解绑</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="unbind-card" wx:elif="{{!loading && !loadError && status == 'cooling'}}">
|
||||
<view class="unbind-icon">⏳</view><text class="unbind-title">72小时冷静期</text>
|
||||
<text class="unbind-desc">冷静期内关系保留,双方均可撤销解绑;到期自动解绑并永久删除共同数据。个人抽烟记录保留。</text>
|
||||
<view class="unbind-timer"><text class="timer-label">距正式解绑</text><text class="timer-value">{{timerText}}</text></view>
|
||||
<button class="btn btn-outline" bindtap="cancelUnbind">撤销解绑,继续保留关系</button>
|
||||
</view>
|
||||
|
||||
<!-- 已同意解绑 -->
|
||||
<view class="unbind-card" wx:elif="{{!loading && !loadError && status == 'completed'}}">
|
||||
<view class="unbind-icon">💔</view>
|
||||
<text class="unbind-title">解绑完成</text>
|
||||
<text class="unbind-desc">冷静期已结束,已解除关系并删除共同数据。个人抽烟记录保留。</text>
|
||||
<view class="unbind-info">
|
||||
<text>完成时间:{{completedTime}}</text>
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="goHome" style="margin-top:32rpx;">返回首页</button>
|
||||
</view>
|
||||
|
||||
<!-- 已拒绝解绑 -->
|
||||
<view class="unbind-card" wx:elif="{{!loading && !loadError && status == 'rejected'}}">
|
||||
<view class="unbind-icon">💕</view>
|
||||
<text class="unbind-title">解绑请求已被拒绝</text>
|
||||
<text class="unbind-desc">对方拒绝了你的解绑请求</text>
|
||||
<view class="unbind-info">
|
||||
<text>拒绝时间:{{respondedTime}}</text>
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="goBack" style="margin-top:32rpx;">返回</button>
|
||||
</view>
|
||||
|
||||
<!-- 已撤销 -->
|
||||
<view class="unbind-card" wx:elif="{{!loading && !loadError && status == 'cancelled'}}">
|
||||
<view class="unbind-icon">💕</view>
|
||||
<text class="unbind-title">解绑请求已撤销</text>
|
||||
<text class="unbind-desc">解绑已取消,关系继续保留</text>
|
||||
<view class="unbind-info">
|
||||
<text>撤销时间:{{respondedTime}}</text>
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="goBack" style="margin-top:32rpx;">返回</button>
|
||||
</view>
|
||||
|
||||
<!-- 已过期 -->
|
||||
<view class="unbind-card" wx:elif="{{!loading && !loadError && status == 'expired'}}">
|
||||
<view class="unbind-icon">⏰</view>
|
||||
<text class="unbind-title">解绑请求已过期</text>
|
||||
<text class="unbind-desc">超过72小时未回应,解绑请求已自动取消</text>
|
||||
<view class="unbind-info">
|
||||
<text>发起时间:{{createdTime}}</text>
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="goBack" style="margin-top:32rpx;">返回</button>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,91 @@
|
||||
/* pages/unbind/unbind.wxss - 情侣解绑确认页 */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
.container { padding: 24rpx; }
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state { text-align: center; padding: 200rpx 40rpx; }
|
||||
.loading-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
|
||||
.loading-text { font-size: 28rpx; color: #999; }
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state { text-align: center; padding: 200rpx 40rpx; }
|
||||
.empty-icon { font-size: 80rpx; display: block; margin-bottom: 16rpx; }
|
||||
.empty-title { font-size: 30rpx; color: #999; display: block; margin-bottom: 8rpx; }
|
||||
.empty-desc { font-size: 24rpx; color: #bbb; display: block; }
|
||||
|
||||
/* 解绑卡片 */
|
||||
.unbind-card {
|
||||
background: white;
|
||||
border-radius: 28rpx;
|
||||
padding: 48rpx 36rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
|
||||
text-align: center;
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.unbind-icon { font-size: 80rpx; display: block; margin-bottom: 24rpx; }
|
||||
|
||||
.unbind-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.unbind-desc {
|
||||
font-size: 26rpx;
|
||||
color: #888;
|
||||
display: block;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
/* 倒计时 */
|
||||
.unbind-timer {
|
||||
background: #FFF8E1;
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.timer-label {
|
||||
font-size: 24rpx;
|
||||
color: #FF9800;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.timer-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #FF5722;
|
||||
display: block;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 信息区 */
|
||||
.unbind-info {
|
||||
background: #F9F9F9;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
text-align: left;
|
||||
}
|
||||
.unbind-info text {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
display: block;
|
||||
margin-bottom: 6rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.unbind-info text:last-child { margin-bottom: 0; }
|
||||
|
||||
/* 按钮区域 */
|
||||
.unbind-actions { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
Reference in New Issue
Block a user