Add server and mini-program pagination for shared lists

This commit is contained in:
2026-10-11 10:10:49 +08:00
parent c0a3db5c8e
commit 7854d94e7b
37 changed files with 395 additions and 175 deletions
+16 -3
View File
@@ -1,9 +1,10 @@
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:[], ownerName:'', ownerAvatar:'', ownerInitial:'我' },
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: {
@@ -16,6 +17,7 @@ Component({
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)+' 天';}
@@ -32,12 +34,23 @@ Component({
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});
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.setData({viewUserId:Number(e.currentTarget.dataset.user)||0,loading:true,info:null});if(this._loading)await this._loading;await this.load();},
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});},
@@ -19,9 +19,10 @@
<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>最近 50 条</text></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>
@@ -14,3 +14,5 @@
@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}