为打卡详情记录增加分页并保持全量统计

This commit is contained in:
2026-10-11 09:50:36 +08:00
parent 7133ea583d
commit 2ffee9d7fb
7 changed files with 153 additions and 29 deletions
+38 -7
View File
@@ -7,7 +7,7 @@ const { requestSubscribeAfterAction } = require('../../utils/subscribe');
const COOLDOWN = 5000;
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' },
data: { loading: true, loadError: false, item: null, logs: [], logsPage: 1, logsTotal: 0, logsHasMore: false, logsLoading: false, logsError: false, freqLabel: '', typeLabel: '', myStreak: 0, partnerStreak: 0, showCheckModal: false, checkPhoto: '', checkNote: '', lastActionTime: 0, detailId: 0, myAvatar: '', partnerAvatar: '', myName: '我', partnerName: 'TA' },
onLoad: function(options) {
this.setData(getLayout());
@@ -57,9 +57,11 @@ Page(withDataCache({
loadDetail: async function(id) {
if (this._pageLoading) return;
this._pageLoading = true;
this._logsGeneration = (this._logsGeneration || 0) + 1;
this.setData({ logsLoading: false, logsError: false });
this.setData({loading: !this._hasCachedData, loadError:false});
try {
var data = await api.checkinDetail(id);
var data = await api.checkinDetail(id, { page: 1, page_size: 20 });
var item = data.item || {};
var myUid = wx.getStorageSync('userId');
var freqLabels = { daily: '每天', weekly: '每周', monthly: '每月', once: '一次', unlimited: '不限' };
@@ -73,11 +75,7 @@ Page(withDataCache({
var partnerAvatar = this.data.partnerAvatar;
var myName = this.data.myName;
var partnerName = this.data.partnerName;
var logs = (data.logs || []).map(function(l) {
l.time = l.created_at ? l.created_at.substring(0, 16).replace('T', ' ') : '';
l.photoUrl = l.photo ? getImageUrl(l.photo) : '';
return l;
});
var logs = this.formatLogs(data.logs || []);
// 根据打卡权限类型映射统计数据
var isCreator = (myUid == item.created_by);
var showMyStats = (item.type == 'both') || (item.type == 'first');
@@ -93,6 +91,8 @@ Page(withDataCache({
var pageData = {
item: item,
logs: logs,
logsPage: 1, logsTotal: Number(data.total) || logs.length,
logsHasMore: data.has_more === true, logsError: false,
freqLabel: freqLabel,
typeLabel: typeLabel,
myStreak: (item.type == 'both') ? (item.my_streak || 0) : checkinStreak,
@@ -118,6 +118,37 @@ Page(withDataCache({
finally { this._pageLoading = false; this.setData({loading:false}); }
},
formatLogs: function(logs) {
return logs.map(function(log) {
return Object.assign({}, log, {
time: log.created_at ? log.created_at.substring(0, 16).replace('T', ' ') : '',
photoUrl: log.photo ? getImageUrl(log.photo) : ''
});
});
},
loadMoreLogs: async function() {
if (this._pageLoading || this.data.loading || this.data.loadError || this.data.logsLoading || !this.data.logsHasMore) return;
var generation = this._logsGeneration;
var page = this.data.logsPage + 1;
this.setData({ logsLoading: true, logsError: false });
try {
var data = await api.checkinDetail(this.data.detailId, { page: page, page_size: 20 });
if (generation !== this._logsGeneration) return;
var seen = new Set(this.data.logs.map(function(log) { return String(log.id); }));
var added = this.formatLogs(data.logs || []).filter(function(log) {
var id = String(log.id);
if (seen.has(id)) return false;
seen.add(id); return true;
});
this.setData({ logs: this.data.logs.concat(added), logsPage: page,
logsTotal: Number(data.total) || 0, logsHasMore: data.has_more === true });
} catch (e) {
if (generation === this._logsGeneration) this.setData({ logsError: true });
} finally {
if (generation === this._logsGeneration) this.setData({ logsLoading: false });
}
},
// ===== 打卡操作 =====
showCheckinModal: function() {
var now = Date.now();
@@ -2,7 +2,7 @@
<launch-transition />
<!-- pages/checkin-detail/checkin-detail.wxml - 打卡详情 -->
<app-nav title="打卡详情" back="{{true}}" />
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" style="height:100vh;" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" bindscrolltolower="loadMoreLogs" lower-threshold="120" style="height:100vh;" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
<view class="c-body">
<page-state wx:if="{{loading}}" kind="loading" title="正在加载详情" /><page-state wx:elif="{{loadError}}" kind="error" title="详情加载失败" action="重试" bind:action="retryDetail" />
<block wx:else>
@@ -92,7 +92,7 @@
<button class="detail-checkin-btn" wx:if="{{item.can_checkin}}" bindtap="showCheckinModal" hover-class="none">记录这一次</button>
<!-- 打卡记录列表 -->
<text class="section-title">打卡记录</text>
<text class="section-title">打卡记录 · {{logsTotal}} 条</text>
<view class="log-list" wx:if="{{logs.length > 0}}">
<view class="log-item" wx:for="{{logs}}" wx:key="id">
<view class="log-dot" style="background:{{item.avatar ? '#B74768' : '#9C27B0'}};"></view>
@@ -116,6 +116,12 @@
<text>📭</text>
<text>暂无打卡记录</text>
</view>
<view class="logs-footer" wx:if="{{logs.length > 0}}">
<text wx:if="{{logsLoading}}">正在加载…</text>
<text wx:elif="{{logsError}}" bindtap="loadMoreLogs">加载失败,点击重试</text>
<text wx:elif="{{logsHasMore}}" bindtap="loadMoreLogs">加载更多</text>
<text wx:else>已加载全部记录</text>
</view>
</block>
</view>
@@ -22,3 +22,5 @@
@import '../../styles/button-theme.wxss';
@import '../../styles/typography.wxss';
.logs-footer { padding:28rpx 0; text-align:center; color:#85776D; font-size:23rpx; }
+37
View File
@@ -0,0 +1,37 @@
const test=require('node:test');
const assert=require('node:assert/strict');
const path=require('node:path');
const load=require('./load-module.cjs');
function setup(checkinDetail){
let definition;
load({Page:p=>definition=p,wx:{getStorageSync:()=>1},console,Date,Promise,Set},{
'../../utils/page-cache':{withDataCache:p=>p},'../../utils/layout':{getLayout:()=>({})},
'../../utils/request':{api:{checkinDetail},getImageUrl:v=>v},
'../../utils/util':{createSwipeBack:()=>({})},'../../utils/subscribe':{requestSubscribeAfterAction(){}}
})(path.join(__dirname,'../pages/checkin-detail/checkin-detail.js'));
const page={...definition,data:JSON.parse(JSON.stringify(definition.data)),setData(patch){Object.assign(this.data,patch);}};
page.data.detailId=9;return page;
}
const first={item:{id:9,type:'both',created_by:1},logs:[{id:3,created_at:'2026-10-11 09:00:00'}],total:3,has_more:true,my_year:99};
test('first page resets records; next page appends once without replacing full stats',async()=>{
let calls=[];let release;
const page=setup((id,params)=>{calls.push(params);return params.page===1?Promise.resolve(first):new Promise(resolve=>release=resolve);});
await page.loadDetail(9);const pending=page.loadMoreLogs();await page.loadMoreLogs();
assert.equal(calls.length,2);assert.equal(calls[1].page,2);
release({logs:[{id:3},{id:2},{id:1}],total:3,has_more:false,my_year:1});await pending;
assert.deepEqual(page.data.logs.map(x=>x.id),[3,2,1]);assert.equal(page.data.myYear,99);
await page.loadMoreLogs();assert.equal(calls.length,2);
await page.loadDetail(9);assert.equal(page.data.logs.length,1);assert.equal(page.data.logsPage,1);
});
test('failed next page preserves list and retries the same page',async()=>{
let fail=true;const calls=[];
const page=setup(async(id,params)=>{calls.push(params.page);if(params.page===1)return first;if(fail)throw Error('offline');return {logs:[{id:2}],total:3,has_more:true};});
await page.loadDetail(9);await page.loadMoreLogs();assert.equal(page.data.logsPage,1);assert.equal(page.data.logsError,true);assert.equal(page.data.logs.length,1);
fail=false;await page.loadMoreLogs();assert.deepEqual(calls,[1,2,2]);assert.equal(page.data.logsError,false);
});
test('an old next-page response cannot append after refreshed first page',async()=>{
let release;const page=setup(async(id,params)=>params.page===1?first:new Promise(resolve=>release=resolve));
await page.loadDetail(9);const pending=page.loadMoreLogs();await page.loadDetail(9);
release({logs:[{id:1}],has_more:false});await pending;
assert.deepEqual(page.data.logs.map(x=>x.id),[3]);assert.equal(page.data.logsPage,1);
});
+1 -1
View File
@@ -369,7 +369,7 @@ const api = {
checkinAdd: (data) => request('checkinAdd', data),
checkinDo: (data) => request('checkinDo', data),
checkinStats: () => request('checkinStats', null, 'GET'),
checkinDetail: (id) => request('checkinDetail', { id: id }, 'GET'),
checkinDetail: (id, pagination = {}) => request('checkinDetail', { ...pagination, id: id }, 'GET'),
checkinDelete: (data) => request('checkinDelete', data),
// 背景图