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:[], 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 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,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});} } } });