同步小程序与管理端更新,完善抽烟记录和解绑冷静期
@@ -23,6 +23,29 @@ App({
|
||||
});
|
||||
},
|
||||
|
||||
onRelationChanged: function() {
|
||||
if (this._relationRedirect) return;
|
||||
wx.removeStorageSync('petHasPet');
|
||||
this._relationRedirect = true;
|
||||
setTimeout(() => { this._relationRedirect=false; wx.reLaunch({url:'/pages/index/index'}); }, 0);
|
||||
},
|
||||
onShow: function() {
|
||||
const sync = async () => {
|
||||
if (this._relationSync || this.globalData.authState !== 'ready') return;
|
||||
this._relationSync = true;
|
||||
const before = String(wx.getStorageSync('coupleId') || '');
|
||||
try {
|
||||
await api.checkAuth();
|
||||
if (before !== String(wx.getStorageSync('coupleId') || '')) {
|
||||
this.onRelationChanged();
|
||||
}
|
||||
} catch(e) {} finally { this._relationSync = false; }
|
||||
};
|
||||
sync();
|
||||
if (!this._relationTimer) this._relationTimer = setInterval(sync, 30000);
|
||||
},
|
||||
onHide: function() { if(this._relationTimer) clearInterval(this._relationTimer); this._relationTimer=null; },
|
||||
|
||||
setAuthState: function(state, message) {
|
||||
this.globalData.authState = state;
|
||||
this.globalData.authMessage = message || '';
|
||||
|
||||
@@ -19,13 +19,16 @@
|
||||
"pages/diary_detail/diary_detail",
|
||||
"pages/diary_edit/diary_edit",
|
||||
"pages/pet/pet",
|
||||
"pages/pet/achievements"
|
||||
"pages/pet/achievements",
|
||||
"pages/smoking-detail/smoking-detail"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#FAF5EE"
|
||||
"backgroundColor": "#FDE4EC",
|
||||
"backgroundColorTop": "#FDE4EC",
|
||||
"backgroundColorBottom": "#FFF7FA"
|
||||
},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
@@ -43,5 +46,25 @@
|
||||
],
|
||||
"usingComponents": {
|
||||
"launch-transition": "/components/launch-transition/launch-transition"
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#8B827B",
|
||||
"selectedColor": "#B74768",
|
||||
"backgroundColor": "#FFF0F5",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "我们"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/discover/discover",
|
||||
"text": "记录"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ page {
|
||||
--text-main: #333333;
|
||||
--text-sub: #888888;
|
||||
--text-light: #BBBBBB;
|
||||
--bg: #FAF5EE;
|
||||
--bg: #FFF7FA;
|
||||
--bg-soft: #F3EBE1;
|
||||
--white: #FFFFFF;
|
||||
|
||||
@@ -295,3 +295,5 @@ scroll-view ::-webkit-scrollbar { display: none; }
|
||||
border: none;
|
||||
}
|
||||
.empty-btn::after { border: none; }
|
||||
|
||||
@import './styles/typography.wxss';
|
||||
|
||||
@@ -7,6 +7,6 @@ Component({
|
||||
methods: { goBack: function() {
|
||||
this.triggerEvent('back');
|
||||
if (getCurrentPages().length > 1) wx.navigateBack();
|
||||
else wx.reLaunch({ url: '/pages/index/index' });
|
||||
else wx.switchTab({ url: '/pages/index/index' });
|
||||
} }
|
||||
});
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
.nav{position:fixed;top:0;left:0;right:0;z-index:900;background:#FAF5EE;box-sizing:border-box}.bar{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box}.title{font-size:32rpx;font-weight:600;color:#352E2A;white-space:nowrap}.back{position:absolute;left:20rpx;top:0;bottom:0;width:80rpx;display:flex;align-items:center;justify-content:center}.back image{width:36rpx;height:36rpx}
|
||||
.nav{position:fixed;top:0;left:0;right:0;z-index:900;background:#FDE4EC;box-sizing:border-box}.bar{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box}.title{font-size:32rpx;font-weight:600;color:#352E2A;white-space:nowrap}.back{position:absolute;left:20rpx;top:0;bottom:0;width:80rpx;display:flex;align-items:center;justify-content:center}.back image{width:36rpx;height:36rpx}
|
||||
/* 二级页面导航标题也统一,组件隔离下需在组件内部设置字体。 */
|
||||
.title { font-family:'Kaiti SC','STKaiti','KaiTi',cursive; font-weight:400; font-style:normal; letter-spacing:1rpx; }
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -15,6 +15,6 @@ Component({
|
||||
var pages = getCurrentPages();
|
||||
// 当前主页面重复点击不重新加载。
|
||||
if (pages.length && pages[pages.length - 1].route === paths[tab]) return;
|
||||
wx.reLaunch({ url: '/' + paths[tab] });
|
||||
wx.switchTab({ url: '/' + paths[tab] });
|
||||
}
|
||||
} });
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
z-index: 900;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFFDF8;
|
||||
border-top: 1rpx solid #E9DDD0;
|
||||
background: #FFF0F5;
|
||||
border-top: 1rpx solid #F3DCE5;
|
||||
padding: 10rpx 20rpx calc(12rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -36,6 +36,8 @@
|
||||
.tab-icon image { width: 48rpx; height: 48rpx; }
|
||||
.tab-label { font-size: 23rpx; line-height: 1.4; letter-spacing: 1rpx; }
|
||||
.active { color: #B74768; }
|
||||
.active .tab-icon { background: #F4E2E3; }
|
||||
.active .tab-icon { background: #FADCE7; }
|
||||
.active .tab-label { font-weight: 600; }
|
||||
.tab-pressed .tab-icon { background: #F3EBE1; }
|
||||
.tab-pressed .tab-icon { background: #FCE5ED; }
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
Component({
|
||||
data: { visible: true, state: 'loading', message: '' },
|
||||
data: { visible: false, intro: false, state: 'loading', message: '' },
|
||||
lifetimes: {
|
||||
attached: function() {
|
||||
this._startedAt = Date.now();
|
||||
try { this._showIntro = !wx.getStorageSync('launchIntroSeen'); } catch (e) { this._showIntro = false; }
|
||||
if (this._showIntro) {
|
||||
this.setData({ visible: true, intro: true });
|
||||
// 设备首次展示即记住,跳转页面或再次启动都不会重复展示。
|
||||
try { wx.setStorageSync('launchIntroSeen', true); } catch (e) {}
|
||||
}
|
||||
this._unobserve = getApp().observeAuth((state, message) => {
|
||||
clearTimeout(this._exitTimer);
|
||||
if (state === 'ready' && !this._waiting) {
|
||||
this.setData({ visible: false });
|
||||
} else if (state === 'ready') {
|
||||
this._exitTimer = setTimeout(() => { this.setData({ visible: false }); }, Math.max(0, 350 - (Date.now() - this._startedAt)));
|
||||
if (state === 'ready') {
|
||||
this._retrying = false;
|
||||
if (this._showIntro) {
|
||||
this._exitTimer = setTimeout(() => {
|
||||
this._showIntro = false;
|
||||
this.setData({ visible: false, intro: false });
|
||||
}, Math.max(0, 350 - (Date.now() - this._startedAt)));
|
||||
} else {
|
||||
this.setData({ visible: false, intro: false });
|
||||
}
|
||||
} else if (state === 'error') {
|
||||
this._showIntro = false;
|
||||
this._retrying = true;
|
||||
this.setData({ visible: true, intro: false, state: state, message: message });
|
||||
} else {
|
||||
this._waiting = true;
|
||||
this.setData({ visible: true, state: state, message: message });
|
||||
this.setData({ visible: !!(this._showIntro || this._retrying), intro: !!this._showIntro, state: state, message: message });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<view wx:if="{{visible}}" class="launch-screen" catchtouchmove="preventMove">
|
||||
<view class="launch-content">
|
||||
<text class="launch-mark {{state === 'loading' ? 'launch-animated' : ''}}">💝</text>
|
||||
<text class="launch-title">我们</text>
|
||||
<text class="launch-caption">把日常,写成我们的故事</text>
|
||||
<block wx:if="{{intro}}">
|
||||
<image class="launch-mark launch-animated" src="/images/share.png" mode="aspectFit" />
|
||||
<text class="launch-title">我们past</text>
|
||||
<text class="launch-caption">让所有的爱都有迹可循</text>
|
||||
</block>
|
||||
<view wx:if="{{state === 'loading'}}" class="launch-progress"><view></view><view></view><view></view></view>
|
||||
<block wx:else>
|
||||
<text class="launch-title">连接暂时没有成功</text>
|
||||
<text class="launch-error">{{message}}</text>
|
||||
<button class="launch-retry" bindtap="retry">重新连接</button>
|
||||
</block>
|
||||
</view>
|
||||
<text class="launch-footer">PAGES OF US</text>
|
||||
<text wx:if="{{intro}}" class="launch-footer">PAGES OF US</text>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.launch-screen{position:fixed;top:0;right:0;bottom:0;left:0;z-index:99999;background:#FAF5EE;display:flex;align-items:center;justify-content:center;color:#352E2A;}
|
||||
.launch-screen{position:fixed;top:0;right:0;bottom:0;left:0;z-index:99999;background:linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);display:flex;align-items:center;justify-content:center;color:#352E2A;}
|
||||
.launch-content{display:flex;align-items:center;flex-direction:column;width:100%;padding:0 60rpx;box-sizing:border-box;transform:translateY(-40rpx);}
|
||||
.launch-mark{display:block;width:152rpx;height:152rpx;line-height:152rpx;font-size:120rpx;text-align:center;}
|
||||
.launch-animated{animation:book-breathe 1.8s ease-in-out infinite;}
|
||||
@@ -12,3 +12,7 @@
|
||||
.launch-footer{position:absolute;bottom:calc(56rpx + env(safe-area-inset-bottom));font-size:20rpx;letter-spacing:6rpx;color:#A39386;}
|
||||
@keyframes book-breathe{0%,100%{transform:translateY(0) rotate(-3deg);}50%{transform:translateY(-10rpx) rotate(1deg);}}
|
||||
@keyframes dot-breathe{0%,100%{opacity:.25;}50%{opacity:1;}}
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -2,3 +2,7 @@
|
||||
.state button{min-height:80rpx;line-height:1.4;display:flex;align-items:center;justify-content:center;padding:20rpx 40rpx;box-sizing:border-box;flex-shrink:0;min-width:220rpx}
|
||||
|
||||
.state-empty { background:#FFFDF8; border:1rpx dashed #E9DDD0; border-radius:12rpx 50rpx 12rpx 50rpx; margin:20rpx 0; }.mark { background:transparent; border-radius:0; }.state button { border-radius:26rpx; }.state .desc { font-size:24rpx; line-height:1.8; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const cache = require('../../utils/data-cache');
|
||||
const titles = {consume:'抽一支 · 打卡',purchase:'买烟',receive:'别人递烟',receive_smoke:'收到并抽一支',adjust:'调整库存',give:'递给别人一支',undo:'撤销记录'};
|
||||
Component({
|
||||
properties: { detailed: Boolean },
|
||||
data: { loading:true, error:false, info:null, saving:false, sheet:false, mode:'purchase', quantity:'1', packSize:'20', amount:'', note:'', viewUserId:0, lastLabel:'还没有记录', daily:[], logs:[], ownerName:'', ownerAvatar:'', ownerInitial:'我' },
|
||||
lifetimes: { attached(){this.load();} },
|
||||
pageLifetimes: { show(){this.load();} },
|
||||
methods: {
|
||||
async load(force) {
|
||||
if(this._loading) return this._loading;
|
||||
if(force)cache.invalidate(['smokingInfo']);
|
||||
this.setData({error:false});
|
||||
const target = this.data.viewUserId;
|
||||
this._loading=Promise.all([api.smokingInfo(target), api.coupleInfo().catch(()=>null)]).then(([info,profile])=>{if(target===this.data.viewUserId){this._profile=profile;this.applyInfo(info);}}).catch(()=>this.setData({error:true})).finally(()=>{this._loading=null;if(target===this.data.viewUserId)this.setData({loading:false});});
|
||||
return this._loading;
|
||||
},
|
||||
applyInfo(info) {
|
||||
const logs=(info.logs||[]).map(log=>({...log,undone:log.undone === true || Number(log.undone) === 1,title:titles[log.kind]||log.kind,change:Number(log.delta)>0?'+'+log.delta:String(log.delta),time:String(log.created_at||'').slice(5,16),money:Number(log.amount_cents)>0?(Number(log.amount_cents)/100).toFixed(2):''}));
|
||||
let lastLabel='还没有记录';
|
||||
if(info.last_time){const minutes=Math.max(0,Math.floor((Date.now()-new Date(String(info.last_time).replace(/-/g,'/')).getTime())/60000));lastLabel=minutes<60?'距上次 '+minutes+' 分钟':minutes<1440?'距上次 '+Math.floor(minutes/60)+' 小时':'距上次 '+Math.floor(minutes/1440)+' 天';}
|
||||
const map={};(info.daily||[]).forEach(row=>{map[row.day]=Number(row.count);});const daily=[];
|
||||
for(let ago=6;ago>=0;ago--){const day=new Date();day.setDate(day.getDate()-ago);const key=day.getFullYear()+'-'+String(day.getMonth()+1).padStart(2,'0')+'-'+String(day.getDate()).padStart(2,'0');daily.push({day:key,label:ago===0?'今天':(day.getMonth()+1)+'/'+day.getDate(),count:map[key]||0});}
|
||||
const max=Math.max(1,...daily.map(day=>day.count));daily.forEach(day=>day.height=Math.max(4,day.count/max*100));
|
||||
const profile=this._profile||{};
|
||||
const ownerId=Number(info.user_id || this.data.viewUserId || wx.getStorageSync('userId'));
|
||||
const owner=[profile.partner1,profile.partner2].find(user=>user && Number(user.id)===ownerId)||{};
|
||||
const ownerName=owner.nickname||info.owner_name||owner.username||(info.read_only?'TA':'我');
|
||||
const ownerAvatar=getImageUrl(owner.avatar||info.owner_avatar||'');
|
||||
this.setData({info,logs,daily,lastLabel,ownerName,ownerAvatar,ownerInitial:ownerName.slice(0,1),error:false,loading:false});
|
||||
},
|
||||
avatarFailed(){this.setData({ownerAvatar:''});},
|
||||
retry(){this.load(true);},
|
||||
openDetail(){if(!this.properties.detailed)wx.navigateTo({url:'/pages/smoking-detail/smoking-detail'});},
|
||||
async switchOwner(e){if(this.data.saving)return;this.setData({viewUserId:Number(e.currentTarget.dataset.user)||0,loading:true,info:null});if(this._loading)await this._loading;await this.load();},
|
||||
replenish(){if(this.data.saving)return;this.setData({sheet:true,mode:'purchase',quantity:'1',packSize:'20',amount:'',note:''});},
|
||||
mode(e){if(this.data.saving)return;const mode=e.currentTarget.dataset.mode;this.setData({mode,quantity:mode==='adjust'?String(this.data.info.stock):'1',amount:'',note:''});},
|
||||
input(e){if(!this.data.saving)this.setData({[e.currentTarget.dataset.field]:e.detail.value});},
|
||||
close(){if(!this.data.saving)this.setData({sheet:false});},
|
||||
preventMove(){},
|
||||
smoke(){return this.act({kind:'consume',quantity:1});},
|
||||
receiveSmoke(){return this.act({kind:'receive_smoke',quantity:1});},
|
||||
giveSmoke(){
|
||||
if(this.data.saving || !this.data.info || this.data.info.read_only || !this.data.info.available)return;
|
||||
if(Number(this.data.info.stock)<1)return wx.showToast({title:'库存不足,请先补充库存',icon:'none'});
|
||||
return this.act({kind:'give',quantity:1});
|
||||
},
|
||||
submit() {
|
||||
const mode=this.data.mode;
|
||||
const quantityText=this.data.quantity.trim(),packText=this.data.packSize.trim();
|
||||
if(!/^\d+$/.test(quantityText)||(mode==='purchase'&&!/^\d+$/.test(packText)))return wx.showToast({title:'请填写整数数量',icon:'none'});
|
||||
let quantity=Number(quantityText);if(mode==='purchase'){if(Number(packText)<1||Number(packText)>100)return wx.showToast({title:'每包数量应为1到100支',icon:'none'});quantity*=Number(packText);}
|
||||
if(quantity>(mode==='adjust'?1000000:10000)||(mode!=='adjust'&&quantity<1))return wx.showToast({title:'数量超出范围',icon:'none'});
|
||||
const money=this.data.amount.trim();if(mode==='purchase'&&money&&!/^\d+(\.\d{1,2})?$/.test(money))return wx.showToast({title:'金额最多两位小数',icon:'none'});
|
||||
const amount=mode==='purchase'?Math.round(Number(money||0)*100):0;
|
||||
if(amount>100000000)return wx.showToast({title:'金额超出范围',icon:'none'});
|
||||
return this.act({kind:mode,quantity,amount_cents:amount,note:this.data.note.trim()});
|
||||
},
|
||||
undo(e){if(this.data.saving)return;wx.showModal({title:'撤销这条记录?',content:'恢复该操作前的库存变化和抽烟次数。赠出一支仅扣库存,撤销会加回一支;“收到并抽一支”会同时撤销收到和抽烟。',success:res=>{if(res.confirm)this.act({kind:'undo',original_id:Number(e.currentTarget.dataset.id),quantity:0});}});},
|
||||
async act(payload) {
|
||||
if(this.data.saving||!this.data.info||this.data.info.read_only||!this.data.info.available)return;
|
||||
const signature=JSON.stringify(payload);
|
||||
if(!this._pendingAction||this._pendingAction.signature!==signature)this._pendingAction={signature,key:'smoke-'+Date.now()+'-'+Math.random().toString(36).slice(2)};
|
||||
this.setData({saving:true});
|
||||
try {
|
||||
const result=await api.smokingAction({...payload,request_key:this._pendingAction.key});
|
||||
if(!result.success)throw result;
|
||||
this._pendingAction=null;this.applyInfo(result.info);this.setData({sheet:false});this.triggerEvent('updated');wx.showToast({title:'已记录',icon:'none'});
|
||||
} catch(error) {
|
||||
// 明确的业务拒绝不会提交;网络不确定时保留请求标识,重试不会重复扣减。
|
||||
if(error&&error._statusCode&&error._statusCode!==500)this._pendingAction=null;
|
||||
wx.showToast({title:error&&error.error||'请求失败,请重试',icon:'none'});
|
||||
} finally {this.setData({saving:false});}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"component":true,"usingComponents":{}}
|
||||
@@ -0,0 +1,26 @@
|
||||
<view class="smoke-card">
|
||||
<view class="smoke-head" bindtap="openDetail"><view><text class="smoke-eyebrow">默认事件 · 按支记录</text><text class="smoke-title">抽烟记录</text></view><text class="smoke-link" wx:if="{{!detailed}}">明细 ›</text></view>
|
||||
<view class="owners" wx:if="{{detailed && info && info.partner_id}}"><view class="owner {{!viewUserId ? 'selected' : ''}}" data-user="0" bindtap="switchOwner">我的记录</view><view class="owner {{viewUserId ? 'selected' : ''}}" data-user="{{info.partner_id}}" bindtap="switchOwner">TA的记录</view></view>
|
||||
<view wx:if="{{loading}}" class="smoke-hint">正在加载记录…</view>
|
||||
<view wx:elif="{{error}}" class="smoke-hint" bindtap="retry">记录加载失败,点击重试</view>
|
||||
<block wx:elif="{{info}}">
|
||||
<view class="smoke-stats"><view><text class="smoke-number">{{info.today_count}}<text>支</text></text><text class="smoke-label">今日已抽</text></view><view><text class="smoke-number">{{info.stock}}<text>支</text></text><text class="smoke-label">剩余库存</text></view></view>
|
||||
<text class="smoke-hint">{{info.read_only ? info.owner_name + ' · 仅查看' : lastLabel}}</text>
|
||||
<text class="smoke-hint" wx:if="{{!info.available}}">绑定伴侣后可记录个人库存和打卡</text>
|
||||
<block wx:elif="{{!info.read_only}}">
|
||||
<view class="smoke-actions"><button class="smoke-primary" disabled="{{saving || info.stock < 1}}" bindtap="smoke">抽一支,打卡</button><button class="smoke-secondary" disabled="{{saving}}" bindtap="replenish">+ 补充库存</button></view>
|
||||
<view class="smoke-inline-actions"><view class="smoke-inline" bindtap="receiveSmoke">{{saving ? '正在保存…' : '别人递来一支,当场抽掉 →'}}</view><view class="smoke-inline smoke-give {{saving || info.stock < 1 ? 'smoke-give-disabled' : ''}}" bindtap="giveSmoke">递给别人一支 →</view></view>
|
||||
<view class="smoke-hint" wx:if="{{info.stock < 1}}">还没有库存,先录入已有的烟或补充库存</view>
|
||||
</block>
|
||||
<view wx:if="{{!detailed && logs.length && !info.read_only}}" class="smoke-recent"><text>{{logs[0].title}} · {{logs[0].time}}</text><text wx:if="{{!logs[0].undone && logs[0].kind !== 'undo'}}" data-id="{{logs[0].id}}" bindtap="undo">撤销</text></view>
|
||||
<block wx:if="{{detailed}}">
|
||||
<view class="section-title">最近七天<text>累计 {{info.total_count}} 支</text></view>
|
||||
<view class="week-chart"><view class="chart-day" wx:for="{{daily}}" wx:key="day"><text>{{item.count}}</text><view class="chart-track"><view class="chart-bar" style="height:{{item.height}}%" /></view><text>{{item.label}}</text></view></view>
|
||||
<view class="section-title">库存与打卡流水<text>最近 50 条</text></view>
|
||||
<view class="smoke-hint" wx:if="{{!logs.length}}">从补充库存开始,记录每一次消耗</view>
|
||||
<view class="ledger {{item.undone ? 'undone' : ''}}" wx:for="{{logs}}" wx:key="id"><view class="ledger-avatar"><image wx:if="{{ownerAvatar}}" src="{{ownerAvatar}}" mode="aspectFill" binderror="avatarFailed" /><text wx:else>{{ownerInitial}}</text></view><view class="ledger-main"><view class="ledger-owner">{{ownerName}}</view><text>{{item.title}}{{item.undone ? ' · 已撤销' : ''}}</text><text>{{item.time}}{{item.money ? ' · ¥' + item.money : ''}}</text><text wx:if="{{item.note}}">{{item.note}}</text></view><view class="ledger-right"><text>{{item.change}} 支 · 余 {{item.balance}}</text><text wx:if="{{!info.read_only && !item.undone && item.kind !== 'undo'}}" data-id="{{item.id}}" bindtap="undo">撤销</text></view></view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
<view class="smoke-mask" wx:if="{{sheet}}" catchtouchmove="preventMove"><view class="smoke-backdrop" bindtap="close" /><view class="smoke-sheet"><view class="sheet-grip" /><view class="sheet-head"><text>补充与调整库存</text><text bindtap="close">关闭</text></view><view class="sheet-tabs"><view class="{{mode === 'purchase' ? 'selected' : ''}}" data-mode="purchase" bindtap="mode">买烟</view><view class="{{mode === 'receive' ? 'selected' : ''}}" data-mode="receive" bindtap="mode">别人递烟</view><view class="{{mode === 'adjust' ? 'selected' : ''}}" data-mode="adjust" bindtap="mode">调整库存</view></view>
|
||||
<scroll-view class="sheet-body" scroll-y><text class="field-label">{{mode === 'purchase' ? '购买包数' : (mode === 'adjust' ? '盘点后实际剩余(支)' : '收到数量(支)')}}</text><input class="smoke-input" type="number" value="{{quantity}}" data-field="quantity" bindinput="input" disabled="{{saving}}" /><block wx:if="{{mode === 'purchase'}}"><text class="field-label">每包支数</text><input class="smoke-input" type="number" value="{{packSize}}" data-field="packSize" bindinput="input" disabled="{{saving}}" /><text class="field-label">购买金额(选填,元)</text><input class="smoke-input" type="digit" value="{{amount}}" data-field="amount" bindinput="input" disabled="{{saving}}" /></block><text class="field-label">备注(选填)</text><input class="smoke-input" value="{{note}}" maxlength="200" data-field="note" bindinput="input" disabled="{{saving}}" /><text class="smoke-hint">{{mode === 'adjust' ? '库存将调整为填写的数量,不计入抽烟打卡。' : '只增加库存,不计入抽烟打卡。'}}</text></scroll-view><button class="smoke-primary sheet-save" loading="{{saving}}" disabled="{{saving}}" bindtap="submit">{{mode === 'adjust' ? '确认调整' : '确认补充'}}</button></view></view>
|
||||
@@ -0,0 +1,14 @@
|
||||
.smoke-card{padding:32rpx;border:1rpx solid #F1DCE4;border-radius:28rpx;background:#FFFCFD;margin-bottom:28rpx;color:#352E2A}.smoke-head{display:flex;justify-content:space-between;align-items:center}.smoke-eyebrow{display:block;font-size:21rpx;color:#A39386;margin-bottom:10rpx}.smoke-title{font-size:33rpx;font-weight:600}.smoke-link{font-size:24rpx;color:#B74768}.smoke-stats{display:flex;margin:30rpx 0 20rpx}.smoke-stats>view{flex:1}.smoke-stats>view+view{padding-left:32rpx;border-left:1rpx solid #F1DCE4}.smoke-number{display:block;font-size:54rpx;color:#B74768;line-height:1.2}.smoke-number text{font-size:23rpx;margin-left:10rpx;color:#A39386}.smoke-label{display:block;font-size:23rpx;color:#85776D;margin-top:10rpx}.smoke-hint{display:block;font-size:22rpx;color:#A39386;line-height:1.7;margin:12rpx 0}.smoke-actions{display:flex;gap:16rpx;margin-top:24rpx}.smoke-primary,.smoke-secondary{flex:1;margin:0;border-radius:20rpx;font-size:25rpx;height:88rpx;min-height:88rpx;padding:0;display:flex;align-items:center;justify-content:center;line-height:normal;box-sizing:border-box}.smoke-primary{background:#B74768;color:#FFF}.smoke-secondary{background:#FFF0F5;color:#B74768;border:1rpx solid #F1DCE4}button::after{border:0}.smoke-primary[disabled]{background:#F0E2E8;color:#A39386}.smoke-inline{font-size:22rpx;color:#B74768;padding:24rpx 0 10rpx}.smoke-recent{display:flex;justify-content:space-between;gap:16rpx;border-top:1rpx solid #F1DCE4;margin-top:20rpx;padding-top:20rpx;font-size:22rpx;color:#A39386}.smoke-recent text+text{color:#B74768}.owners,.sheet-tabs{display:flex;gap:10rpx;margin:24rpx 0}.owner,.sheet-tabs>view{flex:1;text-align:center;background:#FFF5F8;color:#85776D;padding:18rpx 8rpx;border-radius:16rpx;font-size:24rpx}.owners .selected,.sheet-tabs .selected{background:#F4E2E3;color:#B74768;font-weight:600}.section-title{font-size:27rpx;font-weight:600;display:flex;justify-content:space-between;margin:40rpx 0 22rpx}.section-title text{font-size:20rpx;color:#A39386;font-weight:400}.week-chart{display:flex;gap:10rpx}.chart-day{flex:1;text-align:center;font-size:20rpx;color:#A39386}.chart-track{height:110rpx;display:flex;align-items:flex-end;justify-content:center;margin:12rpx 0}.chart-bar{width:24rpx;border-radius:8rpx;background:#D5A6AE}.ledger{display:flex;justify-content:space-between;gap:20rpx;padding:24rpx 0;border-top:1rpx solid #F1DCE4;font-size:24rpx}.ledger-main{flex:1;min-width:0}.ledger-main text{display:block;overflow-wrap:break-word}.ledger-main text+text{font-size:21rpx;color:#A39386;margin-top:8rpx}.ledger-right{text-align:right;font-size:22rpx}.ledger-right text{display:block}.ledger-right text+text{color:#B74768;padding-top:14rpx}.undone{opacity:.55}.smoke-mask{position:fixed;inset:0;z-index:1300;display:flex;align-items:flex-end}.smoke-backdrop{position:absolute;inset:0;background:rgba(53,46,42,.35)}.smoke-sheet{position:relative;width:100%;max-height:90vh;background:#FFFCFD;border-radius:40rpx 40rpx 0 0;padding:16rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));box-sizing:border-box}.sheet-grip{width:64rpx;height:7rpx;background:#EBD5DE;border-radius:8rpx;margin:0 auto 26rpx}.sheet-head{display:flex;justify-content:space-between;font-size:30rpx;font-weight:600}.sheet-head text+text{font-size:24rpx;color:#B74768;font-weight:400}.sheet-body{height:600rpx;max-height:52vh}.field-label{display:block;font-size:24rpx;color:#85776D;margin:20rpx 0 12rpx}.smoke-input{height:84rpx;width:100%;background:#FFF5F8;border:1rpx solid #F1DCE4;border-radius:16rpx;padding:0 24rpx;box-sizing:border-box;font-size:27rpx;color:#352E2A}.sheet-save{width:100%;margin-top:24rpx}
|
||||
|
||||
.ledger-avatar{width:54rpx;height:54rpx;flex-shrink:0;border-radius:50%;overflow:hidden;background:#FADCE7;color:#B74768;display:flex;align-items:center;justify-content:center;font-size:24rpx}
|
||||
.ledger-avatar image{width:100%;height:100%}
|
||||
.ledger-owner{font-size:24rpx;color:#64424D;margin-bottom:10rpx;overflow-wrap:break-word}
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
.smoke-inline-actions{display:flex;align-items:center;gap:20rpx}
|
||||
.smoke-inline-actions .smoke-inline:first-child{flex:1;min-width:0}
|
||||
.smoke-give{flex-shrink:0;text-align:right}
|
||||
.smoke-give-disabled{color:#A39386;opacity:.65}
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,23 @@
|
||||
const { tabForPage, paths } = require('../utils/navigation');
|
||||
Component({
|
||||
data: { active: '', hidden: false },
|
||||
lifetimes: { attached: function() { this.syncActive(); } },
|
||||
pageLifetimes: { show: function() { this.syncActive(); } },
|
||||
methods: {
|
||||
setHidden: function(hidden) { this.setData({ hidden: !!hidden }); },
|
||||
syncActive: function() {
|
||||
var pages = getCurrentPages();
|
||||
var current = pages[pages.length - 1];
|
||||
this.setData({ active: tabForPage(current && current.route), hidden: !!(current && current.data && (current.data.publishOpen || current.data.showEditProfileModal || current.data.showEditPartnerModal)) });
|
||||
},
|
||||
navigate: function(e) {
|
||||
var tab = e.currentTarget.dataset.tab;
|
||||
if (!paths[tab]) return;
|
||||
var pages = getCurrentPages();
|
||||
// 当前主页面重复点击不重新加载。
|
||||
if (pages.length && pages[pages.length - 1].route === paths[tab]) return;
|
||||
if (this._switching) return;
|
||||
this._switching = true;
|
||||
wx.switchTab({ url: '/' + paths[tab], complete: () => { this._switching = false; } });
|
||||
}
|
||||
} });
|
||||
@@ -0,0 +1 @@
|
||||
{"component":true}
|
||||
@@ -0,0 +1,11 @@
|
||||
<view class="tabs" wx:if="{{active && !hidden}}">
|
||||
<view class="tab {{active === 'home' ? 'active' : ''}}" data-tab="home" bindtap="navigate" hover-class="tab-pressed" aria-label="我们" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'home' ? '/components/app-tabbar/icons/us-active.svg' : '/components/app-tabbar/icons/us.svg'}}" mode="aspectFit" /></view><text class="tab-label">我们</text>
|
||||
</view>
|
||||
<view class="tab {{active === 'records' ? 'active' : ''}}" data-tab="records" bindtap="navigate" hover-class="tab-pressed" aria-label="记录" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'records' ? '/components/app-tabbar/icons/journal-active.svg' : '/components/app-tabbar/icons/journal.svg'}}" mode="aspectFit" /></view><text class="tab-label">记录</text>
|
||||
</view>
|
||||
<view class="tab {{active === 'profile' ? 'active' : ''}}" data-tab="profile" bindtap="navigate" hover-class="tab-pressed" aria-label="我的" aria-role="button">
|
||||
<view class="tab-icon"><image src="{{active === 'profile' ? '/components/app-tabbar/icons/me-active.svg' : '/components/app-tabbar/icons/me.svg'}}" mode="aspectFit" /></view><text class="tab-label">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,43 @@
|
||||
.tabs {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 900;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFF0F5;
|
||||
border-top: 1rpx solid #F3DCE5;
|
||||
padding: 10rpx 20rpx calc(12rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 92rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 0;
|
||||
color: #8B827B;
|
||||
}
|
||||
.tab-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 54rpx;
|
||||
border-radius: 32rpx;
|
||||
background: transparent;
|
||||
|
||||
}
|
||||
.tab-icon image { width: 48rpx; height: 48rpx; }
|
||||
.tab-label { font-size: 23rpx; line-height: 1.4; letter-spacing: 1rpx; }
|
||||
.active { color: #B74768; }
|
||||
.active .tab-icon { background: #FADCE7; }
|
||||
.active .tab-label { font-weight: 600; }
|
||||
.tab-pressed .tab-icon { background: #FCE5ED; }
|
||||
|
||||
@import '../styles/typography.wxss';
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 5-7 7 7 7"/></svg>
|
||||
|
After Width: | Height: | Size: 185 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#F47FA5" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
||||
|
After Width: | Height: | Size: 268 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="10" y="8" width="28" height="31" rx="5" fill="#F9E1E9" transform="rotate(9 24 24)" stroke="#D8B8C0"/><rect x="8" y="11" width="29" height="30" rx="5" fill="#FFFDF8"/><path d="M12 28l7-7 7 7 4-4 3 4v6H12Z" fill="#DFEEE5" stroke="#9AAF9F"/><circle cx="29" cy="19" r="3" fill="#F8B3C9" stroke="none"/><path d="M19 37h7" stroke="#D8B8C0"/></svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8h23a4 4 0 0 1 4 4v27H13a5 5 0 0 1-5-5V13a5 5 0 0 1 4-5Z" fill="#FFFDF8"/><path d="M14 8v31M8 34a5 5 0 0 1 5-5h26" stroke="#BFA1AA"/><path d="M28 8v13l4-3 4 3V8" fill="#F8B3C9" stroke="#D4819B"/><path d="M20 17h4M20 23h10" stroke="#CDB4BA"/></svg>
|
||||
|
After Width: | Height: | Size: 439 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="10" width="30" height="30" rx="8" fill="#FFFDF8"/><path d="M17 25l6 6 12-15" stroke="#D4819B" stroke-width="3.3"/><path d="M17 8v5M30 8v5"/><circle cx="11" cy="37" r="4" fill="#F8B3C9" stroke="none"/></svg>
|
||||
|
After Width: | Height: | Size: 400 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M24 40V27m0 9q-9 0-12-8 9-1 12 8m0-3q8-1 11-7" stroke="#91AC9B"/><path d="M24 28C14 28 10 21 12 14c5 0 9 3 12 7 3-4 7-7 12-7 2 7-2 14-12 14Z" fill="#F8B3C9" stroke="#D4819B"/><path d="M17 16q1-8 7-10 6 2 7 10l-7 9Z" fill="#FCE4EC" stroke="#D4819B"/><path d="M7 10v4M5 12h4" stroke="#D8B8C0"/></svg>
|
||||
|
After Width: | Height: | Size: 486 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 31V13l21-5v20"/><path d="M18 19l21-5" stroke="#D4819B"/><ellipse cx="12.5" cy="33" rx="6.5" ry="5" fill="#F8B3C9" transform="rotate(-16 12.5 33)"/><ellipse cx="33.5" cy="30" rx="6.5" ry="5" fill="#F8B3C9" transform="rotate(-16 33.5 30)"/><path d="M9 12v5M6.5 14.5h5" stroke="#CDB4BA"/></svg>
|
||||
|
After Width: | Height: | Size: 483 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 20 10 9l11 7a25 25 0 0 1 7 0l10-7v11c3 3 4 7 3 11-1 7-9 11-17 11S9 38 7 32c-1-5 0-9 3-12Z" fill="#FFFDF8"/><path d="m13 14 5 4-5 2Zm22 0-5 4 5 2Z" fill="#F8B3C9" stroke="none"/><path d="M16 26v2M31 26v2" stroke-width="2.8"/><path d="m21 31 3 2 3-2Z" fill="#D4819B" stroke="#D4819B"/><path d="M24 33v3m0 0q-4 3-6 0m6 0q4 3 6 0M5 28l7 2M4 34l8-1M36 30l7-2M36 33l8 1" stroke-width="1.5"/></svg>
|
||||
|
After Width: | Height: | Size: 583 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="13" height="13" rx="4" fill="#F8D5E0"/><rect x="27" y="8" width="13" height="13" rx="4" fill="#DFEEE5"/><rect x="8" y="27" width="13" height="13" rx="4" fill="#F7E8CA"/><rect x="27" y="27" width="13" height="13" rx="4" fill="#EAE1F4"/></svg>
|
||||
|
After Width: | Height: | Size: 445 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#77545E" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="11" width="30" height="29" rx="7" fill="#FFFDF8"/><path d="M9 20h30M17 8v8M31 8v8"/><path d="M24 34s-8-4.7-8-9a4.3 4.3 0 0 1 8-2 4.3 4.3 0 0 1 8 2c0 4.3-8 9-8 9Z" fill="#F8B3C9" stroke="#D4819B"/></svg>
|
||||
|
After Width: | Height: | Size: 396 B |
@@ -1,11 +1,16 @@
|
||||
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({
|
||||
Page(withDataCache({
|
||||
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: [],
|
||||
@@ -27,6 +32,67 @@ Page({
|
||||
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.setData({ batches, 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) {
|
||||
@@ -66,7 +132,7 @@ Page({
|
||||
loadData: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
var self = this;
|
||||
var albumId = this.data.albumId;
|
||||
@@ -78,8 +144,8 @@ Page({
|
||||
}
|
||||
}
|
||||
if (albumId) {
|
||||
// 使用 noCache 禁用缓存,确保获取最新数据
|
||||
var photos = await api.photos(albumId, null, 'GET', true);
|
||||
// 共享内存缓存;写操作成功后自动失效。
|
||||
var photos = await api.photos(albumId);
|
||||
// 根据 batch_id 分组
|
||||
var batchMap = {};
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
@@ -115,6 +181,7 @@ Page({
|
||||
// 按时间排序(最新的在最前)
|
||||
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.setData({ batches: batches });
|
||||
} else { this.setData({batches:[]}); }
|
||||
} catch (e) {
|
||||
@@ -142,6 +209,7 @@ Page({
|
||||
wx.showToast({ title: '请选择照片', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
self._loadCurrentAddress();
|
||||
var formList = paths.map(function() { return { taken_at: '', location: '', activity: '' }; });
|
||||
self.setData({
|
||||
showUploadModal: true,
|
||||
@@ -152,6 +220,8 @@ Page({
|
||||
});
|
||||
},
|
||||
fail: function(err) {
|
||||
// 微信选图器关闭时也会触发 fail,主动取消无需提示错误。
|
||||
if (/cancel/i.test(String(err && err.errMsg || ''))) return;
|
||||
console.error('Choose image failed:', err);
|
||||
wx.showToast({ title: '选择照片失败', icon: 'none' });
|
||||
}
|
||||
@@ -263,17 +333,7 @@ Page({
|
||||
|
||||
// 自动定位:为未填写位置的照片自动获取地理位置(发布后自动显示)
|
||||
var autoLoc = null;
|
||||
var needLoc = formList.some(function(x) { return !(x.location && x.location.trim()); });
|
||||
if (needLoc) {
|
||||
try {
|
||||
autoLoc = await new Promise(function(resolve) {
|
||||
wx.getFuzzyLocation({
|
||||
success: function(res) { resolve({ lat: res.latitude, lng: res.longitude }); },
|
||||
fail: function() { resolve(null); }
|
||||
});
|
||||
});
|
||||
} catch (e) { 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 });
|
||||
@@ -293,7 +353,7 @@ Page({
|
||||
album_id: String(albumId),
|
||||
path: uploadedPath,
|
||||
taken_at: f.taken_at || '',
|
||||
location: f.location || '',
|
||||
location: String(f.location || '').trim() || (autoLoc ? autoLoc.location : ''),
|
||||
activity: f.activity || '',
|
||||
batch_id: batchId,
|
||||
// 未手动填写位置时,自动定位并记录经纬度(发布后自动显示地址)
|
||||
@@ -426,11 +486,7 @@ Page({
|
||||
if (!batch || !batch.photos || !batch.photos[imgIdx]) return;
|
||||
var allImages = [];
|
||||
batch.photos.forEach(function(p) {
|
||||
// 经纬度为0(定位失败)时不显示位置信息
|
||||
var hasCoord = p.lat && p.lng && parseFloat(p.lat) !== 0 && parseFloat(p.lng) !== 0;
|
||||
var loc = p.location;
|
||||
if (!hasCoord) loc = '';
|
||||
allImages.push({ url: p.url, taken_at: p.taken_at, location: loc, activity: p.activity });
|
||||
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 });
|
||||
},
|
||||
@@ -464,6 +520,6 @@ Page({
|
||||
},
|
||||
|
||||
goToAlbums: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["photos", "albumList"]}));
|
||||
|
||||
@@ -18,13 +18,38 @@
|
||||
<view class="moments-name-row">
|
||||
<text class="moments-name">{{item.creator_name || '情侣'}}</text>
|
||||
<text class="moments-count">{{item.photos.length}}张</text>
|
||||
<text class="moments-time">{{item.created_time}}</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 class="moments-metadata {{item.metadataExpanded ? 'is-expanded' : ''}}" wx:if="{{item.metadata.length}}">
|
||||
<block wx:for="{{item.metadata}}" wx:for-item="note" wx:for-index="noteIndex" wx:key="id">
|
||||
<view class="moment-note" wx:if="{{item.metadataExpanded || noteIndex === 0}}">
|
||||
<block wx:if="{{item.metadataExpanded}}">
|
||||
<text class="moment-photo-label" wx:if="{{note.label}}">{{note.label}}</text>
|
||||
<view class="moment-meta-row" wx:if="{{note.taken_at}}"><text class="moment-meta-label">拍摄</text><text class="moment-meta-value">{{note.taken_at}}</text></view>
|
||||
<view class="moment-meta-row" wx:if="{{note.location}}"><text class="moment-meta-label">地点</text><text class="moment-meta-value">{{note.location}}</text></view>
|
||||
<view class="moment-meta-row" wx:if="{{note.activity}}"><text class="moment-meta-label">故事</text><text class="moment-meta-value moment-story">{{note.activity}}</text></view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="moment-summary-line" wx:if="{{note.label || note.taken_at || note.location}}">
|
||||
<text class="moment-summary-badge" wx:if="{{note.label}}">{{note.label}}</text>
|
||||
<text class="moment-summary-date" wx:if="{{note.taken_at}}">{{note.taken_at}}</text>
|
||||
<text class="moment-summary-location" wx:if="{{note.location}}">{{note.location}}</text>
|
||||
</view>
|
||||
<text class="moment-summary-story" wx:if="{{note.activity}}">{{note.activity}}</text>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
<view class="moment-metadata-toggle" data-batch="{{index}}" catchtap="toggleMetadata" aria-role="button" aria-label="{{item.metadataExpanded ? '收起照片信息' : '展开照片信息'}}">
|
||||
<text>{{item.metadataExpanded ? '收起详情' : (item.metadata.length > 1 ? '还有 ' + (item.metadata.length - 1) + ' 张照片信息 · 展开' : '展开详情')}}</text><text>{{item.metadataExpanded ? '⌃' : '⌄'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="moment-add-note" wx:else data-batch="{{index}}" catchtap="openPhotoEditor">+ 补充拍摄时间、地点和故事</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -117,11 +142,11 @@
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>地点</label>
|
||||
<input class="input" placeholder="例如:北京颐和园" value="{{uploadForm.location}}" bindinput="onUploadInput" data-field="location" />
|
||||
<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 class="input" placeholder="例如:第一次一起看日落" value="{{uploadForm.activity}}" bindinput="onUploadInput" data-field="activity" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:第一次一起看日落" value="{{uploadForm.activity}}" bindinput="onUploadInput" data-field="activity" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -137,4 +162,23 @@
|
||||
</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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/album/album.wxss - 日记式分组相册 */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
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 {
|
||||
@@ -329,3 +329,74 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
@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';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/albums/albums.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
@@ -49,7 +50,7 @@ Page({
|
||||
loadAlbums: async function() {
|
||||
if (this._loading) return;
|
||||
this._loading = true;
|
||||
this.setData({ loading: true, loadError: false });
|
||||
this.setData({ loading: !this._hasCachedData, loadError: false });
|
||||
try {
|
||||
var list = await api.albumList(null, 'GET', true);
|
||||
list = (list || []).map(function(a) {
|
||||
@@ -170,7 +171,7 @@ Page({
|
||||
});
|
||||
}, goToPublish: function() { wx.navigateTo({ url: '/pages/publish/publish?type=album' }); },
|
||||
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goToProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadAlbums": ["albumList"]}));
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
<text class="title">编辑相册</text>
|
||||
<view class="input-group">
|
||||
<label>相册标题</label>
|
||||
<input class="input" placeholder="例如:旅行记录" value="{{editForm.title}}" bindinput="onEditTitleInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:旅行记录" value="{{editForm.title}}" bindinput="onEditTitleInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>描述(选填)</label>
|
||||
<input class="input" placeholder="简单描述这个相册" value="{{editForm.desc}}" bindinput="onEditDescInput" />
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/albums/albums.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
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; }
|
||||
|
||||
|
||||
@@ -126,3 +126,7 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false, item: null, logs: [], freqLabel: '', typeLabel: '', myStreak: 0, partnerStreak: 0, showCheckModal: false, checkPhoto: '', checkNote: '', lastActionTime: 0, detailId: 0, myAvatar: '', partnerAvatar: '', myName: '我', partnerName: 'TA' },
|
||||
|
||||
@@ -34,7 +35,7 @@ Page({
|
||||
// 从接口加载情侣头像和名称
|
||||
loadCoupleInfo: function() {
|
||||
var self = this;
|
||||
api.coupleInfo().then(function(info) {
|
||||
return api.coupleInfo().then(function(info) {
|
||||
self._applyCoupleInfo(info);
|
||||
}).catch(function(e) { console.error(e); });
|
||||
},
|
||||
@@ -56,7 +57,7 @@ Page({
|
||||
loadDetail: async function(id) {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
var data = await api.checkinDetail(id);
|
||||
var item = data.item || {};
|
||||
@@ -188,6 +189,6 @@ Page({
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadDetail": ["checkinDetail", "coupleInfo"]}));
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<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 class="input" style="min-height:100rpx;" placeholder="一句话..." value="{{checkNote}}" bindinput="onCheckNoteInput" maxlength="60" /></view>
|
||||
<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" />
|
||||
|
||||
@@ -18,3 +18,7 @@
|
||||
.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';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
@@ -22,18 +23,13 @@ Page({
|
||||
this.setData({ scrollTop: getScrollPosition('diary') });
|
||||
},
|
||||
onShow: function() {
|
||||
var self = this;
|
||||
var savedTop = getScrollPosition('diary');
|
||||
this.setData({ scrollTop: savedTop });
|
||||
setTimeout(function() {
|
||||
self.setData({ scrollTop: savedTop });
|
||||
}, 100);
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// 收藏 tab 切换已移除
|
||||
|
||||
onScroll: function(e) {
|
||||
this._scrollTop = e.detail.scrollTop;
|
||||
saveScrollPosition('diary', e.detail.scrollTop);
|
||||
},
|
||||
|
||||
@@ -58,7 +54,7 @@ Page({
|
||||
loadData: async function() {
|
||||
if (this._loading) return;
|
||||
this._loading = true;
|
||||
this.setData({ loading: true, loadError: false });
|
||||
this.setData({ loading: !this._hasCachedData, loadError: false });
|
||||
try {
|
||||
var data = await api.diaryList({ page: 1, page_size: this.data.pageSize });
|
||||
var items = this._formatItems(data.items || []);
|
||||
@@ -185,17 +181,17 @@ Page({
|
||||
goToDetail: function(e) {
|
||||
var id = e.currentTarget.dataset.id;
|
||||
if (!id) return;
|
||||
saveScrollPosition('diary', this.data.scrollTop);
|
||||
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.data.scrollTop);
|
||||
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.redirectTo({ url: '/pages/index/index' }); },
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goProfile: function() { this._savePosBeforeLeave(); wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["diaryList", "coupleInfo"]}));
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
noop: function() {},
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loadError: false,
|
||||
@@ -28,10 +29,14 @@ Page({
|
||||
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:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
var data = await api.diaryDetail(this.data.diaryId);
|
||||
data = this._formatDiary(data);
|
||||
@@ -68,6 +73,7 @@ Page({
|
||||
},
|
||||
|
||||
loadComments: async function() {
|
||||
this.setData({ commentsError: false });
|
||||
try {
|
||||
var data = await api.diaryCommentList({ diary_id: this.data.diaryId });
|
||||
if (data && data.length > 0) {
|
||||
@@ -82,7 +88,7 @@ Page({
|
||||
});
|
||||
}
|
||||
this.setData({ comments: data || [] });
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) { this.setData({ commentsError: true }); console.error(e); }
|
||||
},
|
||||
|
||||
onShareTap: function() {
|
||||
@@ -226,5 +232,5 @@ Page({
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); }
|
||||
});
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }
|
||||
}, {"loadDiary": ["diaryDetail"], "loadComments": ["diaryCommentList"]}));
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<text class="modal-title">{{replyTo ? '回复 @' + replyTo.nickname : '发表评论'}}</text>
|
||||
<text class="modal-send" catchtap="submitComment">发送</text>
|
||||
</view>
|
||||
<textarea class="modal-input" placeholder="说点什么..." value="{{commentText}}" bindinput="onCommentInput" auto-focus maxlength="500" />
|
||||
<textarea placeholder-class="handwritten-placeholder" class="modal-input" placeholder="说点什么..." value="{{commentText}}" bindinput="onCommentInput" auto-focus maxlength="500" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -15,3 +15,7 @@
|
||||
.modal-mask { position:fixed; top:0; left:0; right:0; bottom:0; z-index:1200; display:flex; align-items:flex-end; }.modal-box { width:100%; border-radius:24rpx 24rpx 0 0!important; padding:28rpx 32rpx calc(28rpx + env(safe-area-inset-bottom)); }.modal-top { display:flex; align-items:center; justify-content:space-between; gap:16rpx; margin-bottom:24rpx; }.modal-cancel,.modal-send { padding:12rpx 0; font-size:26rpx; color:#85776D; }.modal-send { color:#B74768; }.modal-title { font-size:28rpx; font-weight:500; color:#352E2A; }.modal-input { width:100%; height:200rpx; background:#FAF5EE; border:1rpx solid #E9DDD0; border-radius:16rpx; padding:20rpx; font-size:27rpx; box-sizing:border-box; }
|
||||
.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';
|
||||
|
||||
@@ -6,7 +6,8 @@ var WEATHER_OPTIONS = ['☀️', '⛅', '☁️', '🌧️', '⛈️', '❄️',
|
||||
|
||||
Page({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
data: {
|
||||
editorContent: '', loading: true, loadError: false,
|
||||
diaryId: 0,
|
||||
form: { title: '', content: '' },
|
||||
tempImages: [],
|
||||
@@ -43,6 +44,7 @@ Page({
|
||||
this.setData({
|
||||
'form.title': title,
|
||||
'form.content': content,
|
||||
editorContent: content,
|
||||
'form.lat': data.lat || 0,
|
||||
'form.lng': data.lng || 0,
|
||||
'form.location': data.location || '',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- 标题 -->
|
||||
<view class="input-group">
|
||||
<label>标题(选填)</label>
|
||||
<input class="input diary-title-input" placeholder="给日记起个标题..." value="{{form.title}}" bindinput="onTitleInput" maxlength="100" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input diary-title-input" placeholder="给日记起个标题..." value="{{form.title}}" bindinput="onTitleInput" maxlength="100" />
|
||||
</view>
|
||||
|
||||
<!-- 内容编辑器(支持换行) -->
|
||||
@@ -15,7 +15,7 @@
|
||||
<label>内容</label>
|
||||
<text class="char-count {{contentLength > 5000 ? 'char-warn' : ''}}">{{contentLength}}/5000</text>
|
||||
</view>
|
||||
<textarea class="input textarea diary-editor" placeholder="想写些什么..." value="{{form.content}}" bindinput="onContentInput" maxlength="5000" auto-height />
|
||||
<textarea placeholder-class="handwritten-placeholder" class="input textarea diary-editor" placeholder="想写些什么..." value="{{editorContent}}" bindinput="onContentInput" maxlength="{{5000}}" auto-height="{{false}}" cursor-spacing="24" />
|
||||
</view>
|
||||
|
||||
<!-- 图片(可删除) -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import '../../styles/common.wxss';
|
||||
|
||||
.container { padding: 84rpx 32rpx 300rpx; }
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
|
||||
/* ===== 输入框美化 ===== */
|
||||
.input-group { margin-bottom: 36rpx; }
|
||||
@@ -140,3 +140,10 @@ page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* 长正文在输入框内滚动,避免输入时整页不断增高。 */
|
||||
textarea.diary-editor { height:420rpx; min-height:420rpx; box-sizing:border-box; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
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, getScrollPosition } = require('../../utils/scroll-pos');
|
||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
stats: null, loading: true, loadError: false, refreshing: false, publishOpen: false,
|
||||
activeFilter: 'diary', filters: [{value:'diary',label:'日记'}, {value:'album',label:'相册'}, {value:'memory',label:'记忆日'}],
|
||||
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: getScrollPosition('discover') });
|
||||
this.setData({ scrollTop: 0 });
|
||||
},
|
||||
onShow: function() {
|
||||
// 延迟设置 scrollTop,确保 scroll-view 在页面显示后重新应用滚动位置
|
||||
// 解决从子页面 navigateBack 返回后滚动位置丢失的问题
|
||||
var self = this;
|
||||
var savedTop = getScrollPosition('discover');
|
||||
this.setData({ scrollTop: savedTop });
|
||||
setTimeout(function() {
|
||||
self.setData({ scrollTop: savedTop });
|
||||
}, 100);
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -42,7 +40,7 @@ Page({
|
||||
loadStats: async function() {
|
||||
if (this._loading) return;
|
||||
this._loading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
|
||||
try {
|
||||
const [timeline, album, diary] = await Promise.all([
|
||||
@@ -68,8 +66,13 @@ Page({
|
||||
|
||||
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}); },
|
||||
showPublish: function() { this.setData({publishOpen:true}); },
|
||||
hidePublish: function() { this.setData({publishOpen:false}); },
|
||||
_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'); },
|
||||
@@ -80,13 +83,13 @@ Page({
|
||||
openAlbum: function(e) { this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/album/album?album_id=' + e.currentTarget.dataset.id}); },
|
||||
// 跳转前保存当前滚动位置
|
||||
_savePosBeforeLeave: function() {
|
||||
saveScrollPosition('discover', this.data.scrollTop);
|
||||
saveScrollPosition('discover', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
},
|
||||
|
||||
// Tab 导航(替换,不留栈)
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
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.redirectTo({ url: '/pages/profile/profile' }); },
|
||||
goToProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); },
|
||||
|
||||
// 功能入口(推入栈,支持左滑返回)
|
||||
goToTimeline: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/timeline/timeline' }); },
|
||||
@@ -99,4 +102,4 @@ Page({
|
||||
this._savePosBeforeLeave();
|
||||
wx.navigateTo({ url: '/pages/period/period' });
|
||||
}
|
||||
});
|
||||
}, {"loadStats": ["timelineList", "albumList", "diaryList"]}));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"app-tabbar": "/components/app-tabbar/app-tabbar",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"enablePullDownRefresh": true
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<import src="../../templates/journal.wxml" />
|
||||
<launch-transition />
|
||||
<view class="app-shell" 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>
|
||||
<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"><button class="j-button j-button-small" bindtap="showPublish">+ 记录</button></view>
|
||||
<view class="j-tools records-tools"><text class="j-small-title">一起的日常</text><view class="j-tool-grid"><view bindtap="goToSongs"><image src="/images/music.svg" /><text>歌曲</text></view><view bindtap="goToCheckin"><image src="/images/check.svg" /><text>打卡</text></view><view bindtap="goToPet"><image src="/images/pet.svg" /><text>宠物</text></view><view bindtap="goToPeriod"><image src="/images/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">{{item.label}}</view></view>
|
||||
<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/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: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><app-tabbar />
|
||||
<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/book.svg" /><text>写日记</text></view><view bindtap="uploadPhotos"><image src="/images/album.svg" /><text>上传照片</text></view><view bindtap="createMemory"><image src="/images/timeline.svg" /><text>记忆日</text></view></view></view></view>
|
||||
</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>
|
||||
|
||||
@@ -5,7 +5,20 @@
|
||||
.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:32rpx; height:32rpx; flex-shrink:0; }
|
||||
.records-tools .j-tool-grid image { width:42rpx; height:42rpx; flex-shrink:0; }
|
||||
|
||||
.records-action-row { display:flex; justify-content:flex-end; margin-bottom:24rpx; }
|
||||
.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; } }
|
||||
|
||||
@@ -101,6 +101,6 @@ Page({
|
||||
},
|
||||
|
||||
goToSettings: function() { wx.navigateTo({ url: '/pages/settings/settings' }); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goToProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/history/history.wxss - 操作记录优化版 */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
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; }
|
||||
|
||||
@@ -75,3 +75,7 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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, getScrollPosition } = require('../../utils/scroll-pos');
|
||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
userId: null,
|
||||
@@ -42,26 +45,22 @@ Page({
|
||||
this.setData({
|
||||
userId: wx.getStorageSync('userId'),
|
||||
coupleId: wx.getStorageSync('coupleId'),
|
||||
scrollTop: getScrollPosition('index')
|
||||
scrollTop: 0
|
||||
});
|
||||
this.loadBg();
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
var self = this;
|
||||
var savedTop = getScrollPosition('index');
|
||||
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,
|
||||
scrollTop: savedTop
|
||||
coupleId: curCoupleId
|
||||
});
|
||||
// 延迟重新设置 scrollTop,解决从子页面 navigateBack 返回后滚动位置丢失的问题
|
||||
setTimeout(function() {
|
||||
self.setData({ scrollTop: savedTop });
|
||||
}, 100);
|
||||
// 检查是否已被解绑(之前有 coupleId 但被清除了)
|
||||
if (prevCoupleId && !curCoupleId) {
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
@@ -74,6 +73,7 @@ Page({
|
||||
|
||||
// 保存滚动位置
|
||||
onScroll: function(e) {
|
||||
this._scrollTop = e.detail.scrollTop;
|
||||
saveScrollPosition('index', e.detail.scrollTop);
|
||||
},
|
||||
|
||||
@@ -166,7 +166,8 @@ Page({
|
||||
if (nb < t) nb.setFullYear(nb.getFullYear()+1);
|
||||
var c = Math.ceil((nb - t) / 86400000);
|
||||
if (c === 0) return '今天!';
|
||||
if (c === 1) return '明天';
|
||||
if (c === 1) return '1天';
|
||||
if (!Number.isFinite(c)) return '';
|
||||
return c + '天';
|
||||
}
|
||||
|
||||
@@ -182,6 +183,7 @@ Page({
|
||||
}
|
||||
|
||||
var dn = {
|
||||
coupleId: info.id,
|
||||
daysCount: daysCount,
|
||||
daysLabel: '我们在一起',
|
||||
datePrefix: '始于',
|
||||
@@ -203,7 +205,7 @@ Page({
|
||||
await this.loadStats();
|
||||
|
||||
} catch (e) {
|
||||
this.setData({ homeError: true, relationshipKnown: false, statsLoading: false, statsError: true, recentDiary: [], recentAlbums: [], upcomingMemory: null });
|
||||
this.setData({ homeError: true, relationshipKnown: !!this._hasCachedData, statsLoading: false, statsError: true });
|
||||
console.error(e);
|
||||
|
||||
} finally {
|
||||
@@ -212,11 +214,12 @@ Page({
|
||||
},
|
||||
|
||||
loadStats: async function() {
|
||||
this.setData({ statsLoading: true, statsError: false });
|
||||
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(), api.petInfo()
|
||||
api.songsList(), api.checkinList(),
|
||||
this.data.coupleId ? api.petInfo() : Promise.resolve({ has_pet: false })
|
||||
]);
|
||||
var photoCount = 0;
|
||||
if (Array.isArray(album)) {
|
||||
@@ -245,11 +248,15 @@ Page({
|
||||
pet: petLevel
|
||||
}
|
||||
});
|
||||
} catch (e) { console.error(e); this.setData({ statsError: true, recentDiary: [], recentAlbums: [], upcomingMemory: null }); } finally { this.setData({ statsLoading: false }); }
|
||||
} 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 = {};
|
||||
@@ -258,7 +265,7 @@ Page({
|
||||
if (resp && resp.location) {
|
||||
self.loadData();
|
||||
}
|
||||
}).catch(function() {});
|
||||
}).catch(function() { lastLocationReport = 0; });
|
||||
};
|
||||
// 优先用模糊定位获取经纬度(getFuzzyLocation 与 getLocation 互斥,统一用模糊定位)
|
||||
if (wx.getFuzzyLocation) {
|
||||
@@ -279,32 +286,32 @@ Page({
|
||||
// 背景图以接口返回为准
|
||||
loadBg: function() {
|
||||
var self = this;
|
||||
api.getBgConfig().then(function(cfg) {
|
||||
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.data.scrollTop);
|
||||
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.redirectTo({ url: '/pages/profile/profile' }); },
|
||||
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() { wx.reLaunch({ url: '/pages/profile/profile?bind=1' }); },
|
||||
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.reLaunch({ url: '/pages/discover/discover' });
|
||||
wx.switchTab({ url: '/pages/discover/discover' });
|
||||
},
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadData();
|
||||
@@ -339,4 +346,4 @@ Page({
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
}
|
||||
});
|
||||
}, {"loadData": ["coupleInfo", "timelineList", "albumList", "diaryList", "songsList", "checkinList", "petInfo", "getBgConfig"]}));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"app-tabbar": "/components/app-tabbar/app-tabbar",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
<import src="../../templates/journal.wxml" />
|
||||
<launch-transition />
|
||||
<view class="app-shell" style="{{layoutStyle}}">
|
||||
<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><text class="j-page-title">让所有的爱有迹可循</text></view></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/heart.svg" /></view>
|
||||
<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="j-couple-line"><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 class="j-couple-names"><text>{{partner1Name}}{{coupleId ? ' & ' + partner2Name : ''}}</text><text class="j-caption" wx:if="{{partner1Location || partner2Location}}">{{partner1Location}}{{partner2Location ? ' / ' + partner2Location : ''}}</text></view></view>
|
||||
<view class="j-cover-meta" wx:if="{{coupleDistance || partner1Birthday || partner2Birthday}}"><text wx:if="{{coupleDistance}}">{{coupleDistance}}</text><text wx:if="{{partner1Birthday}}">你的生日:{{partner1Birthday}}</text><text wx:if="{{partner2Birthday}}">TA的生日:{{partner2Birthday}}</text></view>
|
||||
<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>
|
||||
@@ -21,5 +37,5 @@
|
||||
<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><app-tabbar />
|
||||
</view></scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -8,6 +8,50 @@
|
||||
.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-fixed-heading .j-page-title { font-size:34rpx; line-height:1.35; margin:4rpx 0 0; 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); }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/lovewords/lovewords.js - 一起听过的歌
|
||||
const { api } = require('../../utils/request');
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
list: [],
|
||||
@@ -32,7 +33,7 @@ Page({
|
||||
loadData: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
const list = await api.songsList();
|
||||
const items = (list || []).map(function(item) {
|
||||
@@ -121,6 +122,6 @@ Page({
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["songsList"]}));
|
||||
|
||||
@@ -31,15 +31,15 @@
|
||||
<text class="modal-subtitle">记录我们一起听过的歌</text>
|
||||
<view class="input-group">
|
||||
<label>歌曲名</label>
|
||||
<input class="input" placeholder="输入歌曲名" value="{{form.song_name}}" bindinput="onSongNameInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="输入歌曲名" value="{{form.song_name}}" bindinput="onSongNameInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>歌手</label>
|
||||
<input class="input" placeholder="输入歌手名" value="{{form.artist}}" bindinput="onArtistInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="输入歌手名" value="{{form.artist}}" bindinput="onArtistInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>备注(选填)</label>
|
||||
<input class="input" placeholder="一句话,限60字" maxlength="60" value="{{form.note}}" bindinput="onNoteInput" />
|
||||
<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>
|
||||
|
||||
@@ -11,3 +11,7 @@
|
||||
.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';
|
||||
|
||||
@@ -345,7 +345,7 @@
|
||||
<view class="modal-heart">🌡️</view>
|
||||
<text class="modal-title">记录体温</text>
|
||||
<view class="temp-input-row">
|
||||
<input class="temp-input" type="digit" placeholder="36.5" value="{{tempValue}}" bindinput="onTempInput" />
|
||||
<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">
|
||||
@@ -361,7 +361,7 @@
|
||||
<view class="modal-heart">⚖️</view>
|
||||
<text class="modal-title">记录体重</text>
|
||||
<view class="temp-input-row">
|
||||
<input class="temp-input" type="digit" placeholder="50.0" value="{{weightValue}}" bindinput="onWeightInput" />
|
||||
<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">
|
||||
@@ -376,7 +376,7 @@
|
||||
<view class="modal period-modal">
|
||||
<view class="modal-heart">📝</view>
|
||||
<text class="modal-title">添加备注</text>
|
||||
<textarea class="note-textarea" placeholder="记录一下今天的状态..." value="{{noteValue}}" bindinput="onNoteInput" maxlength="500" />
|
||||
<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>
|
||||
@@ -430,7 +430,7 @@
|
||||
<!-- 备注 -->
|
||||
<view class="sex-form-row">
|
||||
<text class="sex-form-label">备注</text>
|
||||
<input class="sex-form-input" placeholder="选填" value="{{sexForm.remark}}" bindinput="onSexRemarkInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="sex-form-input" placeholder="选填" value="{{sexForm.remark}}" bindinput="onSexRemarkInput" />
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/period/period.wxss - 姨姨(美柚风格日历模式) */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
.container { padding-top: 16rpx; padding-left: 20rpx; padding-right: 20rpx; }
|
||||
scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
@@ -1083,3 +1083,7 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
.j-body .quick-event-btn .qe-icon { width:34rpx; height:34rpx; }.j-body .qe-label { color:#85776D; font-size:23rpx; }
|
||||
.j-body .quick-event-btn.active { background:#F4E2E3; border-color:#D5A6AE; }.j-body .quick-event-btn.active .qe-label { color:#B74768; }
|
||||
.j-body .cycle-card { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:20rpx; box-shadow:none; }.j-body .cycle-date { font-size:26rpx; }.j-body .cycle-detail { color:#85776D; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/pet/achievements.js
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
achievements: [],
|
||||
@@ -23,7 +24,7 @@ Page({
|
||||
loadAchievements: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
const res = await api.petAchievements();
|
||||
let list = res.achievements || [];
|
||||
@@ -60,4 +61,4 @@ Page({
|
||||
goBack: function() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
});
|
||||
}, {"loadAchievements": ["petAchievements"]}));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import '../../styles/common.wxss';
|
||||
|
||||
page {
|
||||
background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%);
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -11,12 +11,12 @@ scroll-view.page-bg {
|
||||
padding-top: 196rpx;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%);
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
|
||||
/* ===== 顶部导航(与其他页面一致,fixed 定位与微信胶囊对齐) ===== */
|
||||
.top-nav-card {
|
||||
background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%);
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
@@ -181,3 +181,7 @@ scroll-view.page-bg {
|
||||
@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';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/pet/pet.js
|
||||
const { api } = require('../../utils/request');
|
||||
@@ -18,9 +19,9 @@ const BREED_EMOJIS = {
|
||||
|
||||
const STAGE_NAMES = ['', '幼犬期', '少年期', '青年期', '成年期', '终极期'];
|
||||
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
data: { loading: true, loadError: false, needsBinding: false,
|
||||
hasPet: false,
|
||||
pet: null,
|
||||
daily: null,
|
||||
@@ -70,8 +71,14 @@ Page({
|
||||
loadPetInfo: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
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;
|
||||
@@ -130,8 +137,12 @@ Page({
|
||||
this.setData({ hasPet: false, pet:null, daily:null, achievements:[], dynamics:[], displayDynamics:[] });
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
console.error(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}); }
|
||||
},
|
||||
@@ -300,10 +311,15 @@ Page({
|
||||
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"]}));
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<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="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadPetInfo" /><block wx:else>
|
||||
<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">
|
||||
@@ -34,7 +34,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="input-group" style="margin-top:32rpx;">
|
||||
<input class="input" placeholder="给宠物取个名字吧" value="{{petName}}" bindinput="onNameInput" maxlength="20" />
|
||||
<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">
|
||||
@@ -214,7 +214,7 @@
|
||||
<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" value="{{renameValue}}" bindinput="onRenameInput" maxlength="20" />
|
||||
<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>
|
||||
|
||||
@@ -17,3 +17,7 @@
|
||||
.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';
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/profile/profile.js - 与原网站一致的个人中心
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos');
|
||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||
const { requestSubscribe } = require('../../utils/subscribe');
|
||||
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
scrollTop: 0,
|
||||
statsLoading:false, statsError:false,
|
||||
statisticItems:[
|
||||
{key:'timeline',label:'记忆日',icon:'stat-timeline',count:'—',color:'pink'},
|
||||
{key:'album',label:'相册',icon:'stat-album',count:'—',color:'yellow'},
|
||||
{key:'diary',label:'日记',icon:'stat-book',count:'—',color:'mint'},
|
||||
{key:'songs',label:'歌曲',icon:'stat-music',count:'—',color:'blue'},
|
||||
{key:'checkin',label:'打卡',icon:'stat-check',count:'—',color:'purple'},
|
||||
{key:'pet',label:'宠物',icon:'stat-pet',count:'—',color:'pink'},
|
||||
{key:'period',label:'姨姨',icon:'stat-j-flower',count:'',color:'pink'},
|
||||
{key:'settings',label:'更多',icon:'stat-settings',count:'',color:'gray'}
|
||||
],
|
||||
|
||||
// 我的信息
|
||||
myNickname: '未登录',
|
||||
@@ -26,6 +39,7 @@ Page({
|
||||
bindingKnown: false, bindingError: false,
|
||||
partnerName: 'TA',
|
||||
partnerDisplayId: '',
|
||||
partnerId: 0,
|
||||
partnerDetail: '',
|
||||
partnerAvatarUrl: '',
|
||||
partnerInitial: '?',
|
||||
@@ -54,8 +68,9 @@ Page({
|
||||
tempLoveDate: '',
|
||||
tempLoveStory: '',
|
||||
|
||||
// 查看TA资料(只读)弹窗
|
||||
// 编辑TA资料弹窗
|
||||
showEditPartnerModal: false,
|
||||
partnerSaving: false, partnerAvatarUploading: false,
|
||||
editPartnerNickname: '',
|
||||
editPartnerGender: 'male',
|
||||
editPartnerGenderText: '男',
|
||||
@@ -127,6 +142,7 @@ Page({
|
||||
_needRefresh: true,
|
||||
|
||||
onScroll: function(e) {
|
||||
this._scrollTop = e.detail.scrollTop;
|
||||
saveScrollPosition('profile', e.detail.scrollTop);
|
||||
},
|
||||
|
||||
@@ -137,7 +153,7 @@ Page({
|
||||
this.setData({
|
||||
userId: wx.getStorageSync('userId'),
|
||||
coupleId: wx.getStorageSync('coupleId'),
|
||||
scrollTop: getScrollPosition('profile')
|
||||
scrollTop: 0
|
||||
});
|
||||
this.loadData();
|
||||
this.loadUnbindStatus();
|
||||
@@ -153,23 +169,28 @@ Page({
|
||||
// 从分享邀请链接进入
|
||||
if (options && options.inviter) {
|
||||
var self = this;
|
||||
setTimeout(function() { self.handleShareInvite(options.inviter); }, 800);
|
||||
setTimeout(function() { self.handleShareInvite(options.inviter, options.invite_code); }, 800);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onShow: function() {
|
||||
resetScrollOnTabShow(this, 'profile');
|
||||
if (!wx.getStorageSync('coupleId') && !this.data.shareOutgoingCode) api.generateInviteLink().then(res => this.setData({shareOutgoingCode:res.code || ''})).catch(() => {});
|
||||
const tabBar = this.getTabBar && this.getTabBar();
|
||||
if (tabBar) tabBar.syncActive();
|
||||
var uid = wx.getStorageSync('userId');
|
||||
var cid = wx.getStorageSync('coupleId');
|
||||
var self = this;
|
||||
var savedTop = getScrollPosition('profile');
|
||||
this.setData({ scrollTop: savedTop, bindingKnown: false, myLocation: '' });
|
||||
// 延迟重新设置 scrollTop,解决从子页面 navigateBack 返回后滚动位置丢失的问题
|
||||
setTimeout(function() {
|
||||
self.setData({ scrollTop: savedTop });
|
||||
}, 100);
|
||||
this.setData({ userId: uid, coupleId: cid });
|
||||
const app = getApp();
|
||||
if (app.globalData.openProfileBind) {
|
||||
app.globalData.openProfileBind = false;
|
||||
this._openBindOnLoad = true;
|
||||
this._dataLoads = {};
|
||||
}
|
||||
this.loadData();
|
||||
this.loadProfileStats();
|
||||
if (!cid && this.data.showInvitePanel && !this._pollTimer) this.startPollCoupleStatus();
|
||||
// 如果已绑定情侣,关闭邀请面板
|
||||
if (cid && this.data.showInvitePanel) {
|
||||
this.setData({ showInvitePanel: false });
|
||||
@@ -180,6 +201,10 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
onHide: function() {
|
||||
if (this._pollTimer) { clearInterval(this._pollTimer); this._pollTimer = null; }
|
||||
},
|
||||
|
||||
onUnload: function() {
|
||||
if (this._pollTimer) {
|
||||
clearInterval(this._pollTimer);
|
||||
@@ -188,7 +213,7 @@ Page({
|
||||
},
|
||||
|
||||
onPullDownRefresh: async function() {
|
||||
await this.loadData();
|
||||
await Promise.all([this.loadData(),this.loadProfileStats()]);
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
@@ -229,6 +254,7 @@ Page({
|
||||
hasPartner: false,
|
||||
partnerName: 'TA',
|
||||
partnerDisplayId: '',
|
||||
partnerId: 0,
|
||||
partnerDetail: '',
|
||||
partnerAvatarUrl: '',
|
||||
partnerInitial: '?',
|
||||
@@ -322,6 +348,7 @@ Page({
|
||||
hasPartner: true,
|
||||
partnerName: partner.nickname || partner.username || 'TA',
|
||||
partnerDisplayId: partner.user_id || '',
|
||||
partnerId: Number(partner.id),
|
||||
partnerDetail: details.join(' · ') || 'TA的信息',
|
||||
partnerAvatarUrl: getImageUrl(partner.avatar),
|
||||
partnerInitial: (partner.nickname || partner.username || '?')[0],
|
||||
@@ -405,6 +432,11 @@ Page({
|
||||
}).catch(function() {});
|
||||
},
|
||||
|
||||
_syncProfileTabBar: function() {
|
||||
const tabBar = this.getTabBar && this.getTabBar();
|
||||
if (tabBar && tabBar.setHidden) tabBar.setHidden(this.data.showEditProfileModal || this.data.showEditPartnerModal);
|
||||
},
|
||||
|
||||
// 编辑资料
|
||||
showEditProfileModal: function() {
|
||||
var d = this.data;
|
||||
@@ -421,6 +453,7 @@ Page({
|
||||
editAvatarUrl: d.myAvatarUrl || '',
|
||||
editNicknameInitial: d.myInitial || '?'
|
||||
});
|
||||
this._syncProfileTabBar();
|
||||
},
|
||||
onEditGenderChange: function(e) {
|
||||
var idx = e.detail.value;
|
||||
@@ -430,6 +463,7 @@ Page({
|
||||
},
|
||||
hideEditProfileModal: function() {
|
||||
this.setData({ showEditProfileModal: false });
|
||||
this._syncProfileTabBar();
|
||||
},
|
||||
onEditNicknameInput: function(e) {
|
||||
this.setData({ editNickname: e.detail.value });
|
||||
@@ -559,9 +593,11 @@ Page({
|
||||
this.setData({ showPartnerDetailModal: false });
|
||||
},
|
||||
|
||||
// 查看TA资料(只读)
|
||||
// 编辑TA资料
|
||||
showEditPartnerModal: function() {
|
||||
const d = this.data;
|
||||
this._editPartnerId = d.partnerId;
|
||||
this._partnerOriginalStory = d.tempLoveStory;
|
||||
var rawGender = d.partnerGenderRaw || '';
|
||||
var genderText = rawGender === 'male' ? '男' : (rawGender === 'female' ? '女' : '未设置');
|
||||
// 从 partnerDetail 中解析年龄(格式:XX岁)
|
||||
@@ -570,7 +606,7 @@ Page({
|
||||
this.setData({
|
||||
showEditPartnerModal: true,
|
||||
editPartnerNickname: d.partnerName === 'TA' ? '' : d.partnerName,
|
||||
editPartnerGender: rawGender || 'male',
|
||||
editPartnerGender: rawGender || '',
|
||||
editPartnerGenderText: genderText,
|
||||
editPartnerAge: partnerAge,
|
||||
editPartnerBirthday: d.partnerBirthday || '',
|
||||
@@ -578,10 +614,65 @@ Page({
|
||||
editPartnerAvatarPath: '',
|
||||
editPartnerNameInitial: d.partnerInitial || '?'
|
||||
});
|
||||
this._syncProfileTabBar();
|
||||
},
|
||||
hideEditPartnerModal: function() {
|
||||
if (this.data.partnerSaving || this.data.partnerAvatarUploading) return;
|
||||
this.setData({ showEditPartnerModal: false });
|
||||
this._syncProfileTabBar();
|
||||
},
|
||||
onEditPartnerNicknameInput: function(e) {
|
||||
if (!this.data.partnerSaving) this.setData({editPartnerNickname:e.detail.value});
|
||||
},
|
||||
onEditPartnerGenderChange: function(e) {
|
||||
if (this.data.partnerSaving) return;
|
||||
const index = Number(e.detail.value);
|
||||
this.setData({editPartnerGender:['male','female'][index],editPartnerGenderText:['男','女'][index]});
|
||||
},
|
||||
onEditPartnerBirthdayInput: function(e) {
|
||||
if (this.data.partnerSaving) return;
|
||||
const birthday = e.detail.value;
|
||||
const birth = new Date(birthday), today = new Date();
|
||||
let age = today.getFullYear() - birth.getFullYear();
|
||||
if (today.getMonth() < birth.getMonth() || (today.getMonth() === birth.getMonth() && today.getDate() < birth.getDate())) age--;
|
||||
this.setData({editPartnerBirthday:birthday,editPartnerAge:String(Math.max(0,age))});
|
||||
},
|
||||
choosePartnerAvatar: function() {
|
||||
if (this.data.partnerSaving || this.data.partnerAvatarUploading || !this._editPartnerId) return;
|
||||
const partnerId = this._editPartnerId;
|
||||
wx.chooseImage({count:1,sizeType:['compressed'],sourceType:['album','camera'],success:async res=>{
|
||||
if (!this.data.showEditPartnerModal || partnerId !== this._editPartnerId) return;
|
||||
this.setData({partnerAvatarUploading:true});
|
||||
try {
|
||||
const uploaded = await api.uploadPartnerAvatar(res.tempFilePaths[0],partnerId);
|
||||
this.setData({editPartnerAvatarUrl:getImageUrl(uploaded.path),editPartnerAvatarPath:uploaded.path});
|
||||
} catch(error) { wx.showToast({title:error.error || '头像上传失败',icon:'none'}); }
|
||||
finally { this.setData({partnerAvatarUploading:false}); }
|
||||
}});
|
||||
},
|
||||
savePartnerProfile: async function() {
|
||||
const d = this.data;
|
||||
if (d.partnerSaving || d.partnerAvatarUploading) return;
|
||||
if (!d.hasPartner || !this._editPartnerId || this._editPartnerId !== d.partnerId) {
|
||||
wx.showToast({title:'绑定信息已变化,请重新打开资料',icon:'none'});return;
|
||||
}
|
||||
const nickname = d.editPartnerNickname.trim();
|
||||
if (!nickname) { wx.showToast({title:'请输入TA的昵称',icon:'none'});return; }
|
||||
this.setData({partnerSaving:true});
|
||||
try {
|
||||
const result = await api.updatePartnerProfile({partner_id:this._editPartnerId,nickname,gender:d.editPartnerGender,age:parseInt(d.editPartnerAge) || 0,birthday:d.editPartnerBirthday,avatar:d.editPartnerAvatarPath || undefined});
|
||||
if (!result || !result.success) throw result || {};
|
||||
if (d.coupleId && (d.tempLoveDate !== d.loveStartDate || d.tempLoveStory !== this._partnerOriginalStory)) {
|
||||
await api.updateCoupleInfo({love_start_date:d.tempLoveDate,love_story:d.tempLoveStory || ''});
|
||||
}
|
||||
this.setData({partnerSaving:false});
|
||||
this.hideEditPartnerModal();
|
||||
await this.loadData();
|
||||
wx.showToast({title:'TA的资料已保存',icon:'success'});
|
||||
} catch(error) { wx.showToast({title:error.error || '保存失败,请重试',icon:'none'}); }
|
||||
finally { this.setData({partnerSaving:false}); }
|
||||
},
|
||||
|
||||
// 加载解绑状态
|
||||
loadUnbindStatus: function() {
|
||||
var self = this;
|
||||
@@ -610,8 +701,8 @@ Page({
|
||||
} else {
|
||||
this.doRespondUnbind();
|
||||
}
|
||||
} else if (status === 'agreed') {
|
||||
wx.showToast({ title: '双方已同意解绑,正在处理中', icon: 'none' });
|
||||
} else if (status === 'cooling') {
|
||||
wx.showToast({ title: '正在72小时冷静期', icon: 'none' });
|
||||
wx.navigateTo({ url: '/pages/unbind/unbind' });
|
||||
} else {
|
||||
this.doRequestUnbind();
|
||||
@@ -622,7 +713,7 @@ Page({
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '💔 解除绑定',
|
||||
content: '发起解绑请求后,对方需要在72小时内确认同意。\n\n对方同意后,所有情侣记录将被永久删除(记忆日、日记、相册、纪念日、情话、歌曲、打卡),仅保留双方个人资料。\n\n你也可以在对方确认前撤销请求。',
|
||||
content: '对方同意后进入72小时冷静期,期间任一方可取消。到期自动解绑并永久删除共同数据;个人抽烟记录保留。',
|
||||
cancelText: '取消',
|
||||
confirmText: '发起解绑',
|
||||
confirmColor: '#FF4757',
|
||||
@@ -647,7 +738,7 @@ Page({
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '↩️ 撤销解绑',
|
||||
content: '确定要撤销解绑请求吗?对方将不会收到任何通知。',
|
||||
content: '取消后关系继续保留。确定取消解绑吗?',
|
||||
cancelText: '取消',
|
||||
confirmText: '撤销请求',
|
||||
confirmColor: '#FF6B9D',
|
||||
@@ -676,7 +767,7 @@ Page({
|
||||
if (res.tapIndex === 0) {
|
||||
wx.showModal({
|
||||
title: '⚠️ 确认同意',
|
||||
content: name + ' 发起了解绑请求。\n\n同意后,所有情侣记录将被永久删除(记忆日、日记、相册、歌曲、打卡),仅保留双方个人资料。\n\n此操作不可恢复!',
|
||||
content: name + ' 发起了解绑请求。同意后进入72小时冷静期,期间可取消;到期自动解绑并永久删除共同数据,个人抽烟记录保留。',
|
||||
cancelText: '取消',
|
||||
confirmText: '同意解绑',
|
||||
confirmColor: '#FF4757',
|
||||
@@ -685,15 +776,8 @@ Page({
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
api.unbindRespond({ agree: true }).then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已同意解绑', icon: 'success' });
|
||||
wx.removeStorageSync('coupleId');
|
||||
wx.removeStorageSync('partnerId');
|
||||
wx.removeStorageSync('partnerName');
|
||||
wx.removeStorageSync('partnerAvatar');
|
||||
wx.removeStorageSync('petHasPet');
|
||||
setTimeout(function() {
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
}, 1500);
|
||||
wx.showToast({ title: '已进入72小时冷静期', icon: 'none' });
|
||||
self.loadUnbindStatus();
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
|
||||
@@ -747,7 +831,7 @@ Page({
|
||||
|
||||
// 跳转前保存当前滚动位置
|
||||
_savePosBeforeLeave: function() {
|
||||
saveScrollPosition('profile', this.data.scrollTop);
|
||||
saveScrollPosition('profile', this._scrollTop == null ? this.data.scrollTop : this._scrollTop);
|
||||
},
|
||||
|
||||
// ===== 邀请绑定 =====
|
||||
@@ -796,9 +880,12 @@ Page({
|
||||
},
|
||||
|
||||
sendInvite: async function() {
|
||||
const { inviteTargetId, inviteMessage } = this.data;
|
||||
const inviteTargetId = String(this.data.inviteTargetId || '').trim();
|
||||
const { inviteMessage } = this.data;
|
||||
if (!inviteTargetId) { wx.showToast({ title: '请输入对方ID', icon: 'none' }); return; }
|
||||
if (inviteTargetId.length < 6) { wx.showToast({ title: 'ID为6位数字', icon: 'none' }); return; }
|
||||
if (!/^[0-9]{6}$/.test(inviteTargetId)) { wx.showToast({ title: 'ID为6位数字', icon: 'none' }); return; }
|
||||
// 邀请成功且对方接受时,需要通知邀请人,提前申请一次性订阅授权
|
||||
setTimeout(function() { requestSubscribe('notify').catch(function() {}); }, 1500);
|
||||
try {
|
||||
wx.showLoading({ title: '发送中...' });
|
||||
const res = await api.invitePartner({ target_user_id: inviteTargetId, message: inviteMessage });
|
||||
@@ -880,6 +967,7 @@ Page({
|
||||
|
||||
// ===== 分享绑定 =====
|
||||
onShareAppMessage: function() {
|
||||
if (!this.data.shareOutgoingCode) return {title:'邀请你绑定情侣', path:'/pages/profile/profile'};
|
||||
var displayId = wx.getStorageSync('displayId') || this.data.myDisplayId;
|
||||
var nick = wx.getStorageSync('nickname') || '我';
|
||||
if (!displayId) {
|
||||
@@ -888,22 +976,22 @@ Page({
|
||||
}
|
||||
return {
|
||||
title: nick + ' 邀请你绑定情侣 💕',
|
||||
path: '/pages/profile/profile?inviter=' + displayId,
|
||||
path: '/pages/profile/profile?inviter=' + displayId + '&invite_code=' + encodeURIComponent(this.data.shareOutgoingCode || ''),
|
||||
imageUrl: '/images/share.png'
|
||||
};
|
||||
},
|
||||
|
||||
// 从分享链接进入时处理邀请绑定
|
||||
handleShareInvite: function(inviterId) {
|
||||
handleShareInvite: function(inviterId, code) {
|
||||
if (!inviterId) return;
|
||||
this.setData({ shareInviterId: inviterId, showShareBindModal: true, shareDisagree: false });
|
||||
this.setData({ shareInviterId: inviterId, shareInviteCode: code || '', showShareBindModal: true, shareDisagree: false });
|
||||
},
|
||||
|
||||
acceptShareBind: async function() {
|
||||
var self = this;
|
||||
try {
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
const res = await api.acceptShareInvite({ inviter_id: this.data.shareInviterId });
|
||||
const res = await api.acceptShareInvite({ inviter_id: this.data.shareInviterId, invite_code: this.data.shareInviteCode });
|
||||
wx.hideLoading();
|
||||
if (res.couple_id) {
|
||||
wx.setStorageSync('coupleId', res.couple_id);
|
||||
@@ -925,8 +1013,36 @@ Page({
|
||||
this.setData({ showShareBindModal: false });
|
||||
},
|
||||
|
||||
loadProfileStats: async function() {
|
||||
this.setData({statsLoading:true,statsError:false});
|
||||
const keys=['timeline','album','diary','songs','checkin','pet'];
|
||||
const results=await Promise.allSettled([
|
||||
api.timelineList(),api.albumList(),api.diaryList(),api.songsList(),api.checkinList(),
|
||||
(this.data.coupleId || wx.getStorageSync('coupleId')) ? api.petInfo() : Promise.resolve({has_pet:false})
|
||||
]);
|
||||
const counts={};let failed=false;
|
||||
results.forEach((result,index)=>{
|
||||
const key=keys[index];
|
||||
if(result.status !== 'fulfilled') { counts[key]='—';failed=true;return; }
|
||||
const value=result.value;
|
||||
if(key==='album') counts[key]=(Array.isArray(value)?value:[]).reduce((sum,album)=>sum+(Number(album.photo_count)||0),0);
|
||||
else if(key==='diary') counts[key]=Number(value && value.total)||0;
|
||||
else if(key==='checkin') counts[key]=value && value.checkin_log_total !== undefined ? Number(value.checkin_log_total)||0 : (Array.isArray(value)?value.length:(value && value.items || []).length);
|
||||
else if(key==='pet') counts[key]=value && value.has_pet && value.pet ? 'Lv.'+(Number(value.pet.level)||0) : '未领养';
|
||||
else counts[key]=Array.isArray(value)?value.length:0;
|
||||
});
|
||||
this.setData({statisticItems:this.data.statisticItems.map(item=>({...item,count:counts[item.key]===undefined?item.count:counts[item.key]})),statsLoading:false,statsError:failed});
|
||||
},
|
||||
openStatistic: function(e) {
|
||||
const routes={timeline:'timeline',album:'albums',diary:'diary',songs:'lovewords',checkin:'punch',pet:'pet',period:'period',settings:'settings'};
|
||||
const route=routes[e.currentTarget.dataset.key];
|
||||
if(!route)return;
|
||||
this._savePosBeforeLeave();
|
||||
wx.navigateTo({url:'/pages/'+route+'/'+route});
|
||||
},
|
||||
|
||||
// 页面跳转
|
||||
manageRelationship: function() { wx.navigateTo({ url: '/pages/unbind/unbind' }); },
|
||||
goToSettings: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/settings/settings' }); },
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.redirectTo({ url: '/pages/index/index' }); }
|
||||
});
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); }
|
||||
}, {"loadData": ["coupleInfo"], "loadProfileStats": ["timelineList","albumList","diaryList","songsList","checkinList","petInfo"]}));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"app-tabbar": "/components/app-tabbar/app-tabbar",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
|
||||
@@ -1,47 +1,56 @@
|
||||
<view class="app-shell" style="{{layoutStyle}}">
|
||||
<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><text class="j-link">编辑资料 →</text></view><view class="j-avatar"><image wx:if="{{myAvatarUrl}}" src="{{myAvatarUrl}}" mode="aspectFill" /><text wx:else>{{myInitial}}</text></view></view>
|
||||
<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"><view class="j-row" bindtap="showEditPartnerModal"><view><text class="j-small-title">{{partnerName}}</text><text class="j-caption" wx:if="{{partnerDisplayId}}">ID {{partnerDisplayId}}</text><text class="j-caption">{{partnerDetail}}</text><text class="j-link">查看TA的资料 →</text></view><view class="j-avatar"><image wx:if="{{partnerAvatarUrl}}" src="{{partnerAvatarUrl}}" mode="aspectFill" /><text wx:else>{{partnerInitial}}</text></view></view><view class="j-profile-setting" bindtap="manageRelationship"><image src="/images/heart.svg" /><view><text class="j-small-title">关系管理</text><text class="j-caption">绑定信息与解绑</text></view><text class="j-link">›</text></view></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="j-profile-setting" bindtap="goToSettings"><image src="/images/settings.svg" /><view><text class="j-small-title">外观与通知</text><text class="j-caption">背景、通知与账号设置</text></view><text class="j-link">›</text></view>
|
||||
<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>
|
||||
|
||||
<app-tabbar />
|
||||
|
||||
|
||||
<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>
|
||||
<view class="edit-body" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<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 class="input" placeholder="你的昵称" value="{{editNickname}}" bindinput="onEditNicknameInput" /></view>
|
||||
<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 class="input" style="min-height:120rpx;" placeholder="记录你们的故事..." value="{{tempLoveStory}}" bindinput="onLoveStoryInput"></textarea></view>
|
||||
</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-view" 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>
|
||||
<view class="edit-body" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
|
||||
<view class="edit-avatar-wrap"><image wx:if="{{editPartnerAvatarUrl}}" src="{{editPartnerAvatarUrl}}" mode="aspectFill" class="edit-avatar-img"></image><text wx:else class="edit-avatar-placeholder">{{editPartnerNameInitial || '?'}}</text></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 readonly"><label>昵称</label><view class="input readonly-text">{{editPartnerNickname || '未设置'}}</view></view>
|
||||
<view class="input-group readonly"><label>性别</label><view class="input readonly-text">{{editPartnerGenderText || '-'}}</view></view>
|
||||
<view class="input-group readonly"><label>生日</label><view class="input readonly-text">{{editPartnerBirthday || '-'}}</view></view>
|
||||
<view class="input-group readonly"><label>恋爱纪念日</label><view class="input readonly-text">{{tempLoveDate || '未设置'}}</view></view>
|
||||
<view class="input-group readonly"><label>恋爱故事</label><view class="input readonly-text" style="min-height:60rpx;">{{tempLoveStory || '未设置'}}</view></view>
|
||||
</view>
|
||||
<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>
|
||||
|
||||
<!-- ===== 邀请绑定面板 ===== -->
|
||||
@@ -62,7 +71,7 @@
|
||||
|
||||
<view class="input-group">
|
||||
<label>TA的ID</label>
|
||||
<input class="input" type="tel" placeholder="输入对方的6位ID" value="{{inviteTargetId}}" bindinput="onInviteTargetIdInput" maxlength="6" />
|
||||
<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> 发送邀请
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/profile/profile.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
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; }
|
||||
@@ -301,8 +301,67 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* TA 的资料仅供查看,不预留编辑操作区。 */
|
||||
.partner-view .edit-body { min-height: 0; padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); }
|
||||
.partner-view .edit-nav { position: relative; }
|
||||
/* 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-view .readonly-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.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';
|
||||
|
||||
@@ -22,6 +22,7 @@ Page({
|
||||
titleText: '',
|
||||
btnText: '',
|
||||
form: {},
|
||||
editorContent: '',
|
||||
tempImages: [],
|
||||
coverPreview: '',
|
||||
coverPath: '',
|
||||
@@ -95,6 +96,7 @@ Page({
|
||||
}
|
||||
}
|
||||
patch.form = form;
|
||||
patch.editorContent = form.content || '';
|
||||
patch.contentLength = (form.content || '').length;
|
||||
}
|
||||
if (typeof data.repeatOptionIdx === 'number') patch.repeatOptionIdx = data.repeatOptionIdx;
|
||||
@@ -109,8 +111,23 @@ Page({
|
||||
} 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; // 编辑模式不保存草稿
|
||||
@@ -131,6 +148,9 @@ Page({
|
||||
|
||||
// 发布成功后清除草稿
|
||||
_clearDraft: function() {
|
||||
clearTimeout(this._draftTimer);
|
||||
this._draftTimer = null;
|
||||
this._draftCleared = true;
|
||||
try {
|
||||
var type = this.data.type;
|
||||
wx.removeStorageSync('publishDraft_' + type);
|
||||
@@ -166,6 +186,7 @@ Page({
|
||||
}
|
||||
this.setData({
|
||||
form: form,
|
||||
editorContent: form.content || '',
|
||||
repeatOptionIdx: repeatOptionIdx,
|
||||
bgImageUrl: bgUrl,
|
||||
bgImagePath: bgPath
|
||||
@@ -209,7 +230,7 @@ Page({
|
||||
} else {
|
||||
form = { title: '', date: '', location: '', content: '', repeat_interval: 0, repeat_unit: '' };
|
||||
}
|
||||
this.setData({ form: form, tempImages: [], repeatOptionIdx: 0, contentLength: (form.content || '').length });
|
||||
this.setData({ form: form, editorContent: form.content || '', tempImages: [], repeatOptionIdx: 0, contentLength: (form.content || '').length });
|
||||
},
|
||||
|
||||
// ===== 自动定位(日记 / 相册发布时自动获取经纬度)=====
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<block wx:if="{{type == 'timeline' || type == 'checkin' || type == 'album'}}">
|
||||
<view class="input-group">
|
||||
<label>{{type == 'album' ? '相册名称' : (type == 'timeline' ? '事件内容' : '标题')}}</label>
|
||||
<input class="input" placeholder="{{type == 'album' ? '例如:我们的旅行' : (type == 'checkin' ? '例如:吃饭、睡觉、运动' : '例如:第一次约会')}}" value="{{form.title}}" bindinput="onTitleInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="{{type == 'album' ? '例如:我们的旅行' : (type == 'checkin' ? '例如:吃饭、睡觉、运动' : '例如:第一次约会')}}" value="{{form.title}}" bindinput="onTitleInput" />
|
||||
</view>
|
||||
</block>
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
<block wx:if="{{type == 'timeline'}}">
|
||||
<view class="input-group">
|
||||
<label>事件地点(选填)</label>
|
||||
<input class="input" placeholder="例如:西湖边" value="{{form.location}}" bindinput="onLocationInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:西湖边" value="{{form.location}}" bindinput="onLocationInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>备注(选填)</label>
|
||||
<textarea class="input textarea" placeholder="记录一些想说的话..." value="{{form.content}}" bindinput="onContentInput" auto-height maxlength="500" />
|
||||
<textarea placeholder-class="handwritten-placeholder" class="input textarea" placeholder="记录一些想说的话..." value="{{editorContent}}" bindinput="onContentInput" auto-height maxlength="500" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>重复选项(选填)</label>
|
||||
@@ -72,7 +72,7 @@
|
||||
<!-- ===== 相册(描述 + 封面)===== -->
|
||||
<view class="input-group" wx:if="{{type == 'album'}}">
|
||||
<label>描述</label>
|
||||
<input class="input" placeholder="简单描述一下这个相册" value="{{form.desc}}" bindinput="onDescInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="简单描述一下这个相册" value="{{form.desc}}" bindinput="onDescInput" />
|
||||
</view>
|
||||
<view class="input-group" wx:if="{{type == 'album'}}">
|
||||
<label>封面图片</label>
|
||||
@@ -92,7 +92,7 @@
|
||||
<!-- 标题 -->
|
||||
<view class="input-group">
|
||||
<label>标题(选填)</label>
|
||||
<input class="input diary-title-input" placeholder="给日记起个标题..." value="{{form.title}}" bindinput="onTitleInput" maxlength="100" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input diary-title-input" placeholder="给日记起个标题..." value="{{form.title}}" bindinput="onTitleInput" maxlength="100" />
|
||||
</view>
|
||||
|
||||
<!-- 内容编辑器(支持换行) -->
|
||||
@@ -101,7 +101,7 @@
|
||||
<label>内容</label>
|
||||
<text class="char-count {{contentLength > 5000 ? 'char-warn' : ''}}">{{contentLength}}/5000</text>
|
||||
</view>
|
||||
<textarea class="input textarea diary-editor" placeholder="今天发生了什么有趣的事呢? 写下你的心情和感受吧..." value="{{form.content}}" bindinput="onContentInput" maxlength="5000" auto-height />
|
||||
<textarea placeholder-class="handwritten-placeholder" class="input textarea diary-editor" placeholder="今天发生了什么有趣的事呢?写下你的心情和感受吧…" value="{{editorContent}}" bindinput="onContentInput" maxlength="{{5000}}" auto-height="{{false}}" cursor-spacing="24" />
|
||||
</view>
|
||||
|
||||
<!-- 图片选择(可删除) -->
|
||||
@@ -129,15 +129,15 @@
|
||||
<block wx:if="{{type == 'song'}}">
|
||||
<view class="input-group">
|
||||
<label>歌曲名</label>
|
||||
<input class="input" placeholder="例如:小幸运" value="{{form.song_name}}" bindinput="onTitleInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:小幸运" value="{{form.song_name}}" bindinput="onTitleInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>歌手</label>
|
||||
<input class="input" placeholder="例如:田馥甄" value="{{form.artist}}" bindinput="onArtistInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="例如:田馥甄" value="{{form.artist}}" bindinput="onArtistInput" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<label>备注(选填)</label>
|
||||
<input class="input" placeholder="一句话,限60字" maxlength="60" value="{{form.note}}" bindinput="onNoteInput" />
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="一句话,限60字" maxlength="60" value="{{form.note}}" bindinput="onNoteInput" />
|
||||
</view>
|
||||
</block>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import '../../styles/common.wxss';
|
||||
|
||||
.container { padding: 84rpx 32rpx 300rpx; }
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
|
||||
/* ===== 输入框美化 ===== */
|
||||
.input-group { margin-bottom: 36rpx; }
|
||||
@@ -206,3 +206,10 @@ page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
/* 长正文在输入框内滚动,避免输入时整页不断增高。 */
|
||||
textarea.diary-editor { height:420rpx; min-height:420rpx; box-sizing:border-box; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/punch/punch.js - 情侣打卡
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loadError: false,
|
||||
checkins: [],
|
||||
@@ -20,7 +21,11 @@ Page({
|
||||
},
|
||||
onShow: function() { this.loadCoupleInfo(); this.loadData(); },
|
||||
|
||||
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();
|
||||
@@ -29,7 +34,7 @@ Page({
|
||||
// 从接口加载情侣头像和名称
|
||||
loadCoupleInfo: function() {
|
||||
var self = this;
|
||||
api.coupleInfo().then(function(info) {
|
||||
return api.coupleInfo().then(function(info) {
|
||||
self._applyCoupleInfo(info);
|
||||
}).catch(function(e) { console.error(e); });
|
||||
},
|
||||
@@ -52,7 +57,7 @@ Page({
|
||||
loadData: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
const [res, stats] = await Promise.all([
|
||||
api.checkinList(),
|
||||
@@ -112,6 +117,6 @@ Page({
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["checkinList", "checkinStats", "coupleInfo"]}));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
"page-state": "/components/page-state/page-state",
|
||||
"smoking-card": "/components/smoking-card/smoking-card"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<smoking-card id="smoking" 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}}">
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
@import '../../styles/companion.wxss';
|
||||
.punch-banner-card { background:#F1EBE2; border:1rpx solid #E9DDD0; padding:28rpx; border-radius:16rpx; margin-bottom:32rpx; }
|
||||
.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 #F1EBE2; 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 #DFD2C3; padding-left:20rpx; }.banner-stat-card:first-child { padding-left:0; }.banner-stat-card:last-child { border:0; }
|
||||
.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';
|
||||
|
||||
@@ -58,7 +58,8 @@ Page({
|
||||
},
|
||||
|
||||
goBind: function() {
|
||||
wx.redirectTo({ url: '/pages/profile/profile?bind=1' });
|
||||
getApp().globalData.openProfileBind = true;
|
||||
wx.switchTab({ url: '/pages/profile/profile' });
|
||||
},
|
||||
|
||||
// 加载解绑状态
|
||||
@@ -240,8 +241,8 @@ Page({
|
||||
// 对方:同意或拒绝
|
||||
this.doRespondUnbind();
|
||||
}
|
||||
} else if (status === 'agreed') {
|
||||
wx.showToast({ title: '双方已同意解绑,正在处理中', icon: 'none' });
|
||||
} else if (status === 'cooling') {
|
||||
wx.showToast({ title: '正在72小时冷静期', icon: 'none' });
|
||||
wx.navigateTo({ url: '/pages/unbind/unbind' });
|
||||
} else {
|
||||
// 无解绑请求或已结束:发起新解绑
|
||||
@@ -254,7 +255,7 @@ Page({
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '💔 解除绑定',
|
||||
content: '发起解绑请求后,对方需要在72小时内确认同意。\n\n对方同意后,所有情侣记录将被永久删除(记忆日、日记、相册、纪念日、情话、歌曲、打卡),仅保留双方个人资料。\n\n你也可以在对方确认前撤销请求。',
|
||||
content: '对方同意后进入72小时冷静期,期间任一方可取消。到期自动解绑并永久删除共同数据;个人抽烟记录保留。',
|
||||
cancelText: '取消',
|
||||
confirmText: '发起解绑',
|
||||
confirmColor: '#FF4757',
|
||||
@@ -280,7 +281,7 @@ Page({
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '↩️ 撤销解绑',
|
||||
content: '确定要撤销解绑请求吗?对方将不会收到任何通知。',
|
||||
content: '取消后关系继续保留。确定取消解绑吗?',
|
||||
cancelText: '取消',
|
||||
confirmText: '撤销请求',
|
||||
confirmColor: '#FF6B9D',
|
||||
@@ -311,7 +312,7 @@ Page({
|
||||
// 同意
|
||||
wx.showModal({
|
||||
title: '⚠️ 确认同意',
|
||||
content: name + ' 发起了解绑请求。\n\n同意后,所有情侣记录将被永久删除(记忆日、日记、相册、歌曲、打卡),仅保留双方个人资料。\n\n此操作不可恢复!',
|
||||
content: name + ' 发起了解绑请求。同意后进入72小时冷静期,期间可取消;到期自动解绑并永久删除共同数据,个人抽烟记录保留。',
|
||||
cancelText: '取消',
|
||||
confirmText: '同意解绑',
|
||||
confirmColor: '#FF4757',
|
||||
@@ -320,15 +321,9 @@ Page({
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
api.unbindRespond({ agree: true }).then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已同意解绑', icon: 'success' });
|
||||
// 清除本地缓存
|
||||
wx.removeStorageSync('coupleId');
|
||||
wx.removeStorageSync('partnerId');
|
||||
wx.removeStorageSync('partnerName');
|
||||
wx.removeStorageSync('partnerAvatar');
|
||||
setTimeout(function() {
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
}, 1500);
|
||||
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' });
|
||||
@@ -365,6 +360,6 @@ Page({
|
||||
|
||||
// 导航
|
||||
goToHistory: function() { wx.navigateTo({ url: '/pages/history/history' }); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goToProfile: function() { wx.navigateBack(); },
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<view class="app-shell app-shell--secondary j-handwritten-page" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/settings/settings.wxml -->
|
||||
<app-nav title="设置" back="{{true}}" />
|
||||
@@ -53,10 +53,12 @@
|
||||
<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:else class="j-caption">发起解绑需对方确认</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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/settings/settings.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
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; }
|
||||
|
||||
@@ -344,3 +344,18 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
.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,7 @@
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
Page({
|
||||
data:{},
|
||||
onLoad(){this.setData(getLayout());},
|
||||
onResize(){this.setData(getLayout());},
|
||||
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 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';
|
||||
@@ -1,3 +1,4 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/timeline-detail/timeline-detail.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
@@ -57,7 +58,7 @@ function calcNearestRepeatDate(eventDate, today, interval, unit) {
|
||||
return new Date(eventDate);
|
||||
}
|
||||
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
item: null,
|
||||
@@ -84,15 +85,26 @@ Page({
|
||||
|
||||
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:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
var item = await api.timelineDetail(id);
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
@@ -176,6 +188,7 @@ Page({
|
||||
} else {
|
||||
this.setData({ bgImageUrl: '' });
|
||||
}
|
||||
this._syncNavigationColor();
|
||||
},
|
||||
|
||||
// ===== 编辑 =====
|
||||
@@ -190,6 +203,6 @@ Page({
|
||||
|
||||
// ===== 导航 =====
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadDetail": ["timelineDetail", "coupleInfo"]}));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<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>
|
||||
@@ -6,7 +6,8 @@
|
||||
<image wx:if="{{bgImageUrl}}" src="{{bgImageUrl}}" mode="aspectFill" class="detail-bg-custom" />
|
||||
<view class="detail-bg-overlay" wx:if="{{bgImageUrl}}"></view>
|
||||
|
||||
<app-nav title="记忆日详情" back="{{true}}" />
|
||||
<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">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/timeline-detail/timeline-detail.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; }
|
||||
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; }
|
||||
@@ -62,7 +62,7 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
|
||||
/* 无背景图时,top-nav-card 背景与 page 背景融合 */
|
||||
.top-nav-card:not(.top-nav-card-transparent) {
|
||||
background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%);
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,3 +163,26 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
@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); }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/timeline/timeline.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
@@ -55,7 +56,7 @@ function calcNearestRepeatDate(eventDate, today, interval, unit) {
|
||||
return new Date(eventDate);
|
||||
}
|
||||
|
||||
Page({
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
list: []
|
||||
@@ -84,7 +85,7 @@ Page({
|
||||
loadData: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading:true, loadError:false});
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
const list = await api.timelineList();
|
||||
const myUid = wx.getStorageSync('userId');
|
||||
@@ -188,7 +189,7 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||
});
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["timelineList", "coupleInfo"]}));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/timeline/timeline.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
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; }
|
||||
@@ -151,3 +151,7 @@ scroll-view.page-bg { height: 100vh; overflow-y: scroll; -webkit-overflow-scroll
|
||||
@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';
|
||||
|
||||
@@ -13,6 +13,7 @@ Page({
|
||||
timerText: '',
|
||||
createdTime: '',
|
||||
respondedTime: '',
|
||||
completedTime: '',
|
||||
_timer: null,
|
||||
},
|
||||
|
||||
@@ -57,6 +58,7 @@ Page({
|
||||
timerText: timerText,
|
||||
createdTime: createdTime,
|
||||
respondedTime: respondedTime,
|
||||
completedTime: res.completed_at ? res.completed_at.substring(0, 16).replace('T', ' ') : '',
|
||||
});
|
||||
|
||||
if (!self._visible) return;
|
||||
@@ -67,37 +69,14 @@ Page({
|
||||
self.data._pollTimer = null;
|
||||
}
|
||||
|
||||
// 如果状态为 agreed(双方已同意解绑),清除缓存后跳转到登录页
|
||||
if (status === 'agreed') {
|
||||
wx.removeStorageSync('coupleId');
|
||||
wx.removeStorageSync('petHasPet');
|
||||
wx.showToast({ title: '情侣关系已解除', icon: 'none' });
|
||||
setTimeout(function() {
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
}, 2000);
|
||||
if (status === 'completed') {
|
||||
wx.removeStorageSync('coupleId'); wx.removeStorageSync('petHasPet');
|
||||
require('../../utils/data-cache').clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动倒计时
|
||||
if (status === 'pending' && remaining > 0) {
|
||||
if ((status === 'pending' || status === 'cooling') && remaining > 0) {
|
||||
self.startTimer(remaining);
|
||||
// 如果是发起方,启动轮询检查对方是否已同意
|
||||
if (self.data.isRequester) {
|
||||
self.data._pollTimer = setInterval(function() {
|
||||
api.unbindStatus().then(function(pollRes) {
|
||||
if (pollRes.status === 'agreed') {
|
||||
clearInterval(self.data._pollTimer);
|
||||
self.data._pollTimer = null;
|
||||
wx.removeStorageSync('coupleId');
|
||||
wx.removeStorageSync('petHasPet');
|
||||
wx.showToast({ title: '对方已同意解绑,正在跳转...', icon: 'none' });
|
||||
setTimeout(function() {
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
}, 2000);
|
||||
}
|
||||
}).catch(function() {});
|
||||
}, 5000);
|
||||
}
|
||||
self.data._pollTimer = setInterval(function() { self.loadStatus(); }, 10000);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
self.setData({ loading: false, loadError:true });
|
||||
@@ -107,12 +86,13 @@ Page({
|
||||
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--;
|
||||
self.data._remaining = Math.max(0, Math.ceil((self._deadline - Date.now()) / 1000));
|
||||
if (self.data._remaining <= 0) {
|
||||
clearInterval(self.data._timer);
|
||||
self.setData({ status: 'expired', timerText: '已过期' });
|
||||
self.loadStatus();
|
||||
return;
|
||||
}
|
||||
self.setData({ timerText: self.formatRemaining(self.data._remaining) });
|
||||
@@ -132,7 +112,7 @@ Page({
|
||||
var self = this;
|
||||
wx.showModal({
|
||||
title: '💔 确认解绑',
|
||||
content: '确定同意解除情侣绑定吗?\n\n确认后所有情侣记录(记忆日、日记、相册、歌曲、打卡)将被永久删除,且不可恢复!\n\n仅保留双方的个人资料。',
|
||||
content: '同意后进入72小时冷静期,期间任一方可以取消。到期自动解除关系并永久删除共同数据,个人抽烟记录保留。',
|
||||
cancelText: '再想想',
|
||||
confirmText: '确认解绑',
|
||||
confirmColor: '#FF4757',
|
||||
@@ -141,11 +121,8 @@ Page({
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
api.unbindRespond({ agree: true }).then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已解除绑定', icon: 'success' });
|
||||
wx.removeStorageSync('coupleId');
|
||||
wx.removeStorageSync('petHasPet');
|
||||
self.setData({ status: 'agreed' });
|
||||
setTimeout(function() { wx.reLaunch({ url: '/pages/index/index' }); }, 1500);
|
||||
wx.showToast({ title: '已进入72小时冷静期', icon: 'none' });
|
||||
self.loadStatus();
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
|
||||
@@ -193,7 +170,7 @@ Page({
|
||||
api.unbindCancel().then(function() {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已撤销', icon: 'success' });
|
||||
self.setData({ status: 'cancelled' });
|
||||
self.loadStatus();
|
||||
}).catch(function(err) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: err.error || '操作失败', icon: 'none' });
|
||||
@@ -204,6 +181,6 @@ Page({
|
||||
},
|
||||
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.reLaunch({ url: '/pages/index/index' }); },
|
||||
goToProfile: function() { wx.reLaunch({ url: '/pages/profile/profile' }); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); },
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</view>
|
||||
<view class="unbind-info">
|
||||
<text>发起时间:{{createdTime}}</text>
|
||||
<text>⚠️ 确认解绑后,所有情侣记录将被永久删除</text>
|
||||
<text>同意后进入72小时冷静期,期间任一方可取消</text>
|
||||
</view>
|
||||
<view class="unbind-actions" style="margin-top:32rpx;">
|
||||
<button class="btn btn-primary" bindtap="agreeUnbind" style="background:#FF4757;">💔 同意解绑</button>
|
||||
@@ -51,13 +51,20 @@
|
||||
</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 == 'agreed'}}">
|
||||
<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>
|
||||
<text class="unbind-title">解绑完成</text>
|
||||
<text class="unbind-desc">冷静期已结束,已解除关系并删除共同数据。个人抽烟记录保留。</text>
|
||||
<view class="unbind-info">
|
||||
<text>回应时间:{{respondedTime}}</text>
|
||||
<text>完成时间:{{completedTime}}</text>
|
||||
</view>
|
||||
<button class="btn btn-primary" bindtap="goHome" style="margin-top:32rpx;">返回首页</button>
|
||||
</view>
|
||||
@@ -77,7 +84,7 @@
|
||||
<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>
|
||||
<text class="unbind-desc">解绑已取消,关系继续保留</text>
|
||||
<view class="unbind-info">
|
||||
<text>撤销时间:{{respondedTime}}</text>
|
||||
</view>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* pages/unbind/unbind.wxss - 情侣解绑确认页 */
|
||||
@import '../../styles/common.wxss';
|
||||
page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%); height: 100vh; overflow: hidden; }
|
||||
page { background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%); height: 100vh; overflow: hidden; }
|
||||
.container { padding: 24rpx; }
|
||||
|
||||
/* 加载状态 */
|
||||
@@ -85,3 +85,7 @@ page { background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* 所有页面和独立组件共用按钮配色,与首页天数一致。 */
|
||||
button:not(.modal-close),
|
||||
.btn-primary,
|
||||
.btn-camera,
|
||||
.access-panel-action {
|
||||
background:#F47FA5!important;
|
||||
color:#FFFFFF!important;
|
||||
border-color:#F47FA5!important;
|
||||
box-shadow:none!important;
|
||||
}
|
||||
/* 次要操作保留描边形式,统一使用同一粉色。 */
|
||||
.btn-outline,
|
||||
.btn-secondary,
|
||||
button.smoke-secondary,
|
||||
button.photo-editor-cancel {
|
||||
background:#FFF7FA!important;
|
||||
color:#F47FA5!important;
|
||||
border:1rpx solid #F47FA5!important;
|
||||
}
|
||||
button[disabled] {
|
||||
background:#F0E2E8!important;
|
||||
color:#A39386!important;
|
||||
border-color:#F0E2E8!important;
|
||||
}
|
||||
@@ -26,7 +26,8 @@ page {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
|
||||
font-size: 28rpx;
|
||||
color: var(--text);
|
||||
background: #FFF5F5;
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -445,7 +446,7 @@ page {
|
||||
|
||||
/* ========== 布局 ========== */
|
||||
.page-bg {
|
||||
background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%);
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
position: relative;
|
||||
@@ -869,7 +870,8 @@ page {
|
||||
.edit-page {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: #FFF5F5;
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
background-attachment: fixed;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -955,7 +957,7 @@ page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%);
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
.top-nav-card .nav-back {
|
||||
position: absolute;
|
||||
|
||||
@@ -8,9 +8,10 @@ page {
|
||||
--text: #352E2A;
|
||||
--text-primary: #352E2A;
|
||||
--text-secondary: #85776D;
|
||||
--bg: #FAF5EE;
|
||||
--bg: #FFF7FA;
|
||||
--radius: 28rpx;
|
||||
background: #FAF5EE!important;
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%)!important;
|
||||
background-attachment: fixed;
|
||||
color: #352E2A;
|
||||
}
|
||||
|
||||
@@ -18,7 +19,8 @@ page {
|
||||
--primary-color: #B74768;
|
||||
--secondary-color: #D5A6AE;
|
||||
min-height: 100vh;
|
||||
background: #FAF5EE;
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.page-bg {
|
||||
@@ -142,7 +144,8 @@ button:after {
|
||||
.edit-root {
|
||||
padding-top: var(--nav-height,64px)!important;
|
||||
box-sizing: border-box;
|
||||
background: #FAF5EE!important;
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%)!important;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.edit-body {
|
||||
@@ -256,7 +259,7 @@ button:after {
|
||||
.edit-nav {
|
||||
top: var(--status-height,20px)!important;
|
||||
height: 44px!important;
|
||||
background: #FAF5EE!important;
|
||||
background: transparent!important;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -270,7 +273,7 @@ button:after {
|
||||
}
|
||||
|
||||
.bg-gradient {
|
||||
background: linear-gradient(180deg,#FCEBF1 0%,#FAF5EE 58%)!important;
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%)!important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
.j-story-photo { position: relative; margin-top: 20rpx; }.j-story-photo image { display: block; width: 100%; height: 330rpx; border-radius: 20rpx; background: #EEE5DA; }
|
||||
.j-photo-count { position: absolute; right: 14rpx; bottom: 14rpx; color: #FFFFFF; background: rgba(53,46,42,.55); font-size: 21rpx; padding: 6rpx 14rpx; border-radius: 8rpx; }
|
||||
.j-story-footer { display: flex; align-items: center; gap: 8rpx; font-size: 22rpx; color: #85776D; padding-top: 22rpx; line-height: 1.6; flex-wrap: wrap; }.j-story-footer image { width: 25rpx; height: 25rpx; }
|
||||
.j-background { position: fixed; inset: 0; }.j-background > image { width: 100%; height: 100%; }.j-background-wash { position: absolute; inset: 0; background: rgba(250,245,238,.88); }
|
||||
.j-background { position: fixed; inset: 0; }.j-background > image { width: 100%; height: 100%; }.j-background-wash { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(253,228,236,.88) 0%, rgba(255,240,245,.88) 48%, rgba(255,247,250,.88) 100%); }
|
||||
.j-photo-strip { display: flex; gap: 20rpx; }.j-photo-print { min-width: 0; flex: 1; border-radius: 24rpx; padding: 12rpx 12rpx 20rpx; border: 1rpx solid #E9DDD0; background: #FFFDF8; box-shadow: 0 8rpx 20rpx rgba(114,82,64,.05); }.j-photo-print:nth-child(2) { transform: rotate(3deg); }.j-photo-print image { width: 100%; height: 160rpx; display: block; border-radius: 16rpx; }.j-photo-print text { display: block; font-size: 22rpx; color: #85776D; margin-top: 14rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.j-filters { display: flex; gap: 38rpx; border-bottom: 1rpx solid #E9DDD0; margin: -10rpx 0 42rpx; }.j-filter { color: #85776D; padding: 22rpx 4rpx; min-height: 44rpx; font-size: 27rpx; }.j-filter.is-active { color: #B74768; border-bottom: 3rpx solid #B74768; font-weight: 600; }
|
||||
/* Compact photo books: two columns, real covers, restrained empty states. */
|
||||
@@ -69,10 +69,53 @@
|
||||
@media (max-width: 350px) { .j-body { padding-left: 32rpx; padding-right: 32rpx; }.j-page-title { font-size: 36rpx; }.j-shortcuts > view { gap: 6rpx; font-size: 22rpx; }.j-cover { padding-left: 32rpx; padding-right: 32rpx; }.j-tool-grid { gap: 10rpx; } }
|
||||
|
||||
/* 主页面页眉与胶囊对齐,内容独立滚动。 */
|
||||
.j-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; background:#FAF5EE; }
|
||||
.j-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; background:#FDE4EC; }
|
||||
.j-fixed-heading { display:flex; flex-direction:column; align-items:flex-start; justify-content:center; box-sizing:border-box; padding-left:44rpx; }
|
||||
.j-fixed-heading .j-eyebrow { font-size:20rpx; line-height:1.2; letter-spacing:2rpx; white-space:nowrap; }
|
||||
.j-fixed-heading .j-page-title { font-size:34rpx; line-height:1.35; margin:4rpx 0 0; white-space:nowrap; }
|
||||
scroll-view.j-main-scroll { position:fixed; top:var(--nav-height,64px); left:0; right:0; height:calc(100vh - var(--nav-height,64px)); padding-top:0!important; }
|
||||
.j-main-scroll > .j-body { padding-top:48rpx; }
|
||||
@media (max-width:350px) { .j-fixed-heading { padding-left:32rpx; } }
|
||||
|
||||
/* 页面与分区标题统一使用手账笔迹,列表正文、字段和操作保持原有字体。 */
|
||||
.j-page-title,
|
||||
.j-section-head > text,
|
||||
.records-action-row > .j-small-title {
|
||||
font-family:'Kaiti SC','STKaiti','KaiTi',cursive;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
letter-spacing:1rpx;
|
||||
color:#64424D;
|
||||
}
|
||||
.j-section-head > text,
|
||||
.records-action-row > .j-small-title {
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
/* 分区右侧入口与手写标题配套,较小字号和粉色区分操作层级。 */
|
||||
.j-section-head > .j-link {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
font-family:'Kaiti SC','STKaiti','KaiTi',cursive;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
font-size:26rpx;
|
||||
letter-spacing:1rpx;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
/* 记录、我的:入口文字与页眉统一,正文和输入内容仍使用常规字体。 */
|
||||
.j-handwritten-page .j-link,
|
||||
.j-handwritten-page .j-small-title,
|
||||
.j-handwritten-page .j-filter,
|
||||
.j-handwritten-page .j-button,
|
||||
.j-handwritten-page .j-outline-label,
|
||||
.j-handwritten-page .j-tool-grid > view > text,
|
||||
.j-handwritten-page .j-publish-grid > view > text {
|
||||
font-family:'Kaiti SC','STKaiti','KaiTi',cursive;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
letter-spacing:1rpx;
|
||||
}
|
||||
.j-handwritten-page .j-link { font-size:26rpx; }
|
||||
.j-handwritten-page .j-profile-setting .j-link { font-size:32rpx; }
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/* 小程序统一字体:端正手写体,覆盖页面及组件中的旧字体。 */
|
||||
page,
|
||||
view,
|
||||
text,
|
||||
scroll-view,
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
label,
|
||||
picker,
|
||||
.handwritten-placeholder {
|
||||
font-family:'Kaiti SC','STKaiti','KaiTi',cursive!important;
|
||||
font-style:normal!important;
|
||||
font-weight:400!important;
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
<text class="j-story-date">{{entry.dateLabel}}</text><text class="j-story-title">{{entry.title}}</text>
|
||||
<text wx:if="{{entry.summary}}" class="j-story-summary">{{entry.summary}}</text>
|
||||
<view wx:if="{{entry.cover}}" class="j-story-photo"><image src="{{entry.cover}}" mode="aspectFill" /><text wx:if="{{entry.imageCount > 1}}" class="j-photo-count">{{entry.imageCount}} 张</text></view>
|
||||
<view class="j-story-footer"><image src="/images/heart.svg" /><text wx:if="{{entry.author}}">{{entry.author}} · </text><text>{{entry.date}}</text></view>
|
||||
<view class="j-story-footer"><image src="/images/home-cover-heart.svg" /><text wx:if="{{entry.author}}">{{entry.author}} · </text><text>{{entry.date}}</text></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const path=require('node:path');
|
||||
const createLoader=require('./load-module.cjs');
|
||||
const {metadataRows}=require('../utils/photo-metadata');
|
||||
function setup(update=async()=>({success:true})) {
|
||||
let definition;const calls=[],toasts=[],prompts=[];
|
||||
const wx={getStorageSync:key=>key==='userId'?1:2,showToast:v=>toasts.push(v),showModal:v=>prompts.push(v)};
|
||||
const api={photoUpdate:async data=>{calls.push(data);return update(data);}};
|
||||
const load=createLoader({wx,console,Promise,Date,Page:p=>definition=p},{'../../utils/request':{api,getImageUrl:v=>v}, '../../utils/user-location':{currentUserLocation:async()=>({location:'',lat:0,lng:0})}});
|
||||
load(path.join(__dirname,'../pages/album/album.js'));
|
||||
const photos=[{id:1,url:'one.jpg',taken_at:'2026-10-07',location:'公园',activity:'散步',lat:23},{id:2,url:'two.jpg',taken_at:'',location:'',activity:''}];
|
||||
const page={...definition,data:{...definition.data,batches:[{batch_id:'batch',photos,metadata:metadataRows(photos)}]},setData(v){Object.assign(this.data,v);}};
|
||||
return {page,calls,toasts,prompts};
|
||||
}
|
||||
const event=(dataset,value)=>({currentTarget:{dataset},detail:{value}});
|
||||
test('card combines identical information and labels individual photos when their stories differ',()=>{
|
||||
assert.deepEqual(metadataRows([{id:1},{id:2}]),[]);
|
||||
const same=metadataRows([{id:1,location:'公园'},{id:2,location:'公园'}]);assert.equal(same.length,1);assert.equal(same[0].label,'');
|
||||
const different=metadataRows([{id:1,activity:'散步'},{id:2,location:'公园'}]);assert.equal(different.length,2);assert.equal(different[1].label,'第 2 张');
|
||||
});
|
||||
test('saving updates only the chosen photo and preview while preserving the list and coordinates',async()=>{
|
||||
const e=setup(),p=e.page;p.onPhotoTap(event({batch:0,index:0}));assert.equal(p.data.previewImages[0].location,'公园');
|
||||
p.openPhotoEditor(event({batch:0}));p.onEditPhotoInput(event({field:'activity'},'第一次一起看日落'));await p.savePhotoInfo();
|
||||
assert.equal(e.calls.length,1);assert.equal(e.calls[0].id,1);assert.equal(p.data.batches[0].photos[0].activity,'第一次一起看日落');assert.equal(p.data.batches[0].photos[0].lat,23);
|
||||
assert.equal(p.data.batches[0].photos[1].activity,'');assert.equal(p.data.previewImages[0].activity,'第一次一起看日落');assert.equal(p.data.showPhotoEditor,false);assert.equal(p.data.savingPhoto,false);
|
||||
});
|
||||
test('failed saves keep the draft and original card; switching photos confirms discarding changes',async()=>{
|
||||
const e=setup(async()=>{throw {error:'保存失败'};}),p=e.page;p.openPhotoEditor(event({batch:0}));p.onEditPhotoInput(event({field:'location'},'新的地点'));
|
||||
p.selectEditPhoto(event({index:1}));assert.equal(e.prompts.length,1);assert.equal(p.data.editPhotoIdx,0);e.prompts[0].success({confirm:false});assert.equal(p.data.editForm.location,'新的地点');
|
||||
await p.savePhotoInfo();assert.equal(p.data.showPhotoEditor,true);assert.equal(p.data.savingPhoto,false);assert.equal(p.data.editForm.location,'新的地点');assert.equal(p.data.batches[0].photos[0].location,'公园');
|
||||
p.selectEditPhoto(event({index:1}));e.prompts[1].success({confirm:true});assert.equal(p.data.editPhotoIdx,1);assert.equal(p.data.editForm.location,'');
|
||||
});
|
||||
test('date and optional fields can be cleared without duplicate saves',async()=>{
|
||||
let resolve;const e=setup(()=>new Promise(done=>resolve=done)),p=e.page;p.openPhotoEditor(event({batch:0}));p.clearPhotoDate();
|
||||
p.onEditPhotoInput(event({field:'location'},''));p.onEditPhotoInput(event({field:'activity'},''));const saving=p.savePhotoInfo();await p.savePhotoInfo();assert.equal(e.calls.length,1);assert.equal(e.calls[0].taken_at,'');
|
||||
resolve({success:true});await saving;assert.equal(p.data.batches[0].metadata.length,0);
|
||||
});
|
||||
test('blank edit address uses current user location; a supplied address wins',async()=>{
|
||||
const e=setup(),p=e.page;
|
||||
// 使用实际页面的保存逻辑和可控的地址提供者。
|
||||
let definition;
|
||||
const loader=createLoader({wx:{getStorageSync:()=>1,showToast(){}},Date,Promise,console,Page:d=>definition=d},{'../../utils/request':{api:{photoUpdate:async data=>{e.calls.push(data);return {success:true,location:data.location};}},getImageUrl:v=>v},'../../utils/user-location':{currentUserLocation:async()=>({location:'当前用户地址',lat:25,lng:102})}});
|
||||
loader(path.join(__dirname,'../pages/album/album.js'));
|
||||
p.savePhotoInfo=definition.savePhotoInfo;
|
||||
p.openPhotoEditor(event({batch:0}));p.onEditPhotoInput(event({field:'location'},' '));await p.savePhotoInfo();assert.equal(e.calls[0].location,'当前用户地址');assert.equal(e.calls[0].lat,25);assert.equal(p.data.batches[0].photos[0].location,'当前用户地址');
|
||||
p.openPhotoEditor(event({batch:0}));p.onEditPhotoInput(event({field:'location'},' 手填地址 '));await p.savePhotoInfo();assert.equal(e.calls[1].location,'手填地址');assert.equal(e.calls[1].lat,0);
|
||||
});
|
||||
@@ -14,8 +14,9 @@ function setup() {
|
||||
getNetworkType: options => options.success({networkType:'wifi'}), onNetworkStatusChange(){},
|
||||
hideLoading(){}, showToast(){}
|
||||
};
|
||||
const loadModule = require('./load-module.cjs')({wx, getApp:()=>app, console, Promise, Date});
|
||||
const ctx = {wx, getApp:()=>app, console, Promise, setTimeout, clearTimeout,
|
||||
module:{exports:{}}, require: name => require(path.join(__dirname,'../utils', name))};
|
||||
module:{exports:{}}, require: name => loadModule(path.join(__dirname,'../utils', name + '.js'))};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../utils/request.js'),'utf8'),ctx);
|
||||
const request = ctx.module.exports;
|
||||
ctx.require = () => request;
|
||||
@@ -23,7 +24,7 @@ function setup() {
|
||||
vm.runInNewContext('(function(){' + fs.readFileSync(path.join(__dirname,'../app.js'),'utf8') + '\n})()',ctx);
|
||||
return {app,wx,storage,logins,requests,api:request.api};
|
||||
}
|
||||
const flush = async()=> {for(let i=0;i<12;i++) await Promise.resolve();};
|
||||
const flush = async()=> {for(let i=0;i<40;i++) await Promise.resolve();};
|
||||
async function finishLogin(env, token='fresh') {
|
||||
env.logins.at(-1).success({code:'wechat-code'});
|
||||
await flush();
|
||||
|
||||