更新最新代码

This commit is contained in:
CodeBuddy
2026-10-26 17:32:47 +08:00
commit 0f5dc01674
315 changed files with 42640 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
const { getLayout } = require('../../utils/layout');
Component({
properties: { title: String, back: Boolean },
data: { statusBarHeight: 20, barHeight: 44, navSide: 100 },
lifetimes: { attached: function() { this.setData(getLayout()); } },
pageLifetimes: { resize: function() { this.setData(getLayout()); } },
methods: { goBack: function() {
this.triggerEvent('back');
if (getCurrentPages().length > 1) wx.navigateBack();
else wx.switchTab({ url: '/pages/index/index' });
} }
});
+1
View File
@@ -0,0 +1 @@
{"component":true}
+6
View File
@@ -0,0 +1,6 @@
<view class="nav" style="padding-top:{{statusBarHeight}}px;">
<view class="bar" style="height:{{barHeight}}px;padding:0 {{navSide}}px;">
<view wx:if="{{back}}" class="back" bindtap="goBack" aria-label="返回"><image src="/images/back.svg" /></view>
<text class="title">{{title}}</text>
</view>
</view>
+5
View File
@@ -0,0 +1,5 @@
.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';
+20
View File
@@ -0,0 +1,20 @@
const { tabForPage, paths } = require('../../utils/navigation');
Component({
data: { active: '' },
lifetimes: { attached: function() { this.syncActive(); } },
pageLifetimes: { show: function() { this.syncActive(); } },
methods: {
syncActive: function() {
var pages = getCurrentPages();
var current = pages[pages.length - 1];
this.setData({ active: tabForPage(current && current.route) });
},
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;
wx.switchTab({ url: '/' + paths[tab] });
}
} });
@@ -0,0 +1 @@
{"component":true}
@@ -0,0 +1,11 @@
<view class="tabs" wx:if="{{active}}">
<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;
transition: background-color .18s ease;
}
.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 28 28" fill="#F4E2E3" stroke="#B74768" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 7.5c-3.4-2-7.4-2-11-.7v16c3.6-1.3 7.6-1.3 11 .7 3.4-2 7.4-2 11-.7v-16c-3.6-1.3-7.6-1.3-11 .7Z"/><path d="M14 7.5v16M7 11h3.5M7 15h3.5M18 11h3M18 15h3"/></svg>

After

Width:  |  Height:  |  Size: 329 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="none" stroke="#8B827B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 7.5c-3.4-2-7.4-2-11-.7v16c3.6-1.3 7.6-1.3 11 .7 3.4-2 7.4-2 11-.7v-16c-3.6-1.3-7.6-1.3-11 .7Z"/><path d="M14 7.5v16M7 11h3.5M7 15h3.5M18 11h3M18 15h3"/></svg>

After

Width:  |  Height:  |  Size: 326 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="#F4E2E3" stroke="#B74768" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="14" cy="10" r="4.5"/><path d="M5 23v-1.4a6 6 0 0 1 6-6h6a6 6 0 0 1 6 6V23Z"/></svg>

After

Width:  |  Height:  |  Size: 253 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="none" stroke="#8B827B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="14" cy="10" r="4.5"/><path d="M5 23v-1.4a6 6 0 0 1 6-6h6a6 6 0 0 1 6 6V23Z"/></svg>

After

Width:  |  Height:  |  Size: 250 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="#F4E2E3" stroke="#B74768" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 23S4 17.2 4 10.6A5.3 5.3 0 0 1 14 8a5.3 5.3 0 0 1 10 2.6C24 17.2 14 23 14 23Z"/><path d="M9 11.5c0-1.3.8-2.3 2-2.6"/></svg>

After

Width:  |  Height:  |  Size: 294 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="none" stroke="#8B827B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 23S4 17.2 4 10.6A5.3 5.3 0 0 1 14 8a5.3 5.3 0 0 1 10 2.6C24 17.2 14 23 14 23Z"/><path d="M9 11.5c0-1.3.8-2.3 2-2.6"/></svg>

After

Width:  |  Height:  |  Size: 291 B

@@ -0,0 +1,56 @@
Component({
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._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.setData({ visible: !!(this._showIntro || this._retrying), intro: !!this._showIntro, state: state, message: message });
}
});
},
detached: function() {
clearTimeout(this._exitTimer);
if (this._unobserve) this._unobserve();
}
},
methods: {
preventMove: function() {},
retry: async function() {
try {
await getApp().silentLogin();
// 重新加载失败的页面,保留分享链接、相册 id 等原始入口参数。
var pages = getCurrentPages();
var page = pages[pages.length - 1];
if (!page) return;
var query = Object.keys(page.options || {}).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(page.options[key]);
}).join('&');
wx.reLaunch({ url: '/' + page.route + (query ? '?' + query : '') });
} catch (error) {
// App 已发布失败状态,保留重试画面。
}
}
}
});
@@ -0,0 +1 @@
{"component":true}
@@ -0,0 +1,16 @@
<view wx:if="{{visible}}" class="launch-screen" catchtouchmove="preventMove">
<view class="launch-content">
<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 wx:if="{{intro}}" class="launch-footer">PAGES OF US</text>
</view>
@@ -0,0 +1,18 @@
.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;}
.launch-title{font-size:44rpx;font-weight:600;letter-spacing:8rpx;margin:48rpx 0 18rpx;}
.launch-caption{font-size:26rpx;color:#85776D;}
.launch-progress{display:flex;gap:12rpx;margin-top:54rpx;}
.launch-progress view{width:8rpx;height:8rpx;border-radius:50%;background:#B74768;animation:dot-breathe 1.2s ease-in-out infinite;}
.launch-progress view:nth-child(2){animation-delay:.15s;}.launch-progress view:nth-child(3){animation-delay:.3s;}
.launch-error{margin-top:42rpx;font-size:26rpx;line-height:1.6;text-align:center;color:#85776D;}
.launch-retry{margin:28rpx 0 0;width:300rpx;background:#B74768;color:#FFFDF8;border-radius:18rpx;font-size:28rpx;line-height:84rpx;}.launch-retry::after{border:0;}
.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';
@@ -0,0 +1 @@
Component({ properties: { kind: { type: String, value: 'empty' }, title: String, description: String, action: String }, methods: { act: function() { this.triggerEvent('action'); } } });
@@ -0,0 +1 @@
{"component":true}
@@ -0,0 +1 @@
<view class="state state-{{kind}}"><view class="mark {{kind}}">{{kind === 'loading' ? '…' : kind === 'error' ? '!' : '♡'}}</view><text class="title">{{title}}</text><text wx:if="{{description}}" class="desc">{{description}}</text><button wx:if="{{action}}" bindtap="act">{{action}}</button></view>
@@ -0,0 +1,8 @@
.state{padding:64rpx 32rpx;display:flex;flex-direction:column;align-items:center;text-align:center;gap:16rpx}.mark{width:88rpx;height:88rpx;line-height:88rpx;border-radius:28rpx;background:#F4E2E3;color:#B74768;font-size:44rpx}.title{color:#352E2A;font-size:30rpx;font-weight:600}.desc{color:#85776D;font-size:26rpx;line-height:1.6}.state button{margin-top:12rpx;background:#B74768;color:#FFFDF8;font-size:26rpx;border-radius:20rpx;padding:0 32rpx}.state button:after{border:0}
.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,94 @@
const { mergeUnique } = require('../../utils/list-pagination');
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:[], periodStats:[], logsPage:1, logsTotal:0, logsHasMore:false, logsLoadingMore:false, logsMoreError:false, 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) {
this._logsGeneration = (this._logsGeneration || 0) + 1;
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 periodStats=[{key:'week',title:'本周',range:'周一至今'},{key:'month',title:'本月',range:'本月1日至今'}].map(period=>({
...period,
count:info[period.key+'_count'] == null ? '—' : Number(info[period.key+'_count']),
money:info[period.key+'_purchase_cents'] == null ? '—' : (Number(info[period.key+'_purchase_cents'])/100).toFixed(2)
}));
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,periodStats,logsPage:Number(info.logs_page)||1,logsTotal:Number(info.logs_total)||logs.length,logsHasMore:info.logs_has_more===true,logsLoadingMore:false,logsMoreError:false,lastLabel,ownerName,ownerAvatar,ownerInitial:ownerName.slice(0,1),error:false,loading:false});
},
async loadMoreLogs(){
if(this._loading || this.data.loading || this.data.saving || this.data.logsLoadingMore || !this.data.logsHasMore)return;
const generation=this._logsGeneration,target=this.data.viewUserId,page=this.data.logsPage+1;
this.setData({logsLoadingMore:true,logsMoreError:false});
try{
const info=await api.smokingInfo(target,{page,page_size:20});
if(generation!==this._logsGeneration || target!==this.data.viewUserId)return;
this.applyInfo({...info,logs:mergeUnique(this.data.info.logs||[],info.logs||[])});
}catch(e){if(generation===this._logsGeneration)this.setData({logsMoreError:true});}
finally{if(generation===this._logsGeneration)this.setData({logsLoadingMore: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._logsGeneration=(this._logsGeneration||0)+1;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,30 @@
<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">抽烟与购买统计</view>
<view class="smoke-periods"><view class="smoke-period" wx:for="{{periodStats}}" wx:key="key"><view class="smoke-period-head"><text>{{item.title}}</text><text>{{item.range}}</text></view><view class="smoke-period-row"><text>已抽</text><text>{{item.count}} 支</text></view><view class="smoke-period-row"><text>买烟金额</text><text>¥{{item.money}}</text></view></view></view>
<text class="smoke-period-hint">购买金额按已填写金额累计,已撤销记录不计入</text>
<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>共 {{logsTotal}} 条</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>
<view class="smoke-hint smoke-ledger-footer" wx:if="{{logs.length}}"><text wx:if="{{logsLoadingMore}}">正在加载…</text><text wx:elif="{{logsMoreError}}" bindtap="loadMoreLogs">加载失败,点击重试</text><text wx:elif="{{logsHasMore}}" bindtap="loadMoreLogs">加载更多流水</text><text wx:else>已加载全部流水</text></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,18 @@
.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';
.smoke-periods{display:flex;gap:16rpx}.smoke-period{flex:1;min-width:0;padding:22rpx 18rpx;border:1rpx solid #F1DCE4;border-radius:20rpx;background:#FFF5F8}.smoke-period-head{display:flex;align-items:center;justify-content:space-between;gap:8rpx;font-size:25rpx;color:#64424D}.smoke-period-head text+text{font-size:18rpx;color:#A39386}.smoke-period-row{display:flex;justify-content:space-between;gap:10rpx;font-size:23rpx;margin-top:18rpx;color:#85776D}.smoke-period-row text+text{color:#B74768;overflow-wrap:anywhere}.smoke-period-hint{display:block;margin-top:14rpx;font-size:20rpx;color:#A39386;line-height:1.6}
.smoke-ledger-footer{text-align:center;padding-top:20rpx}