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
+14 -6
View File
@@ -37,9 +37,14 @@ func coupleSpaceID(c *httpapi.Ctx, cid int64) string {
func albumList(c *httpapi.Ctx) error { func albumList(c *httpapi.Ctx) error {
ids := coupleUserIds(c, 0) ids := coupleUserIds(c, 0)
if len(ids) == 0 { if len(ids) == 0 {
return c.OK(demoData("album")) photoTotal := int64(0)
for _, album := range demoData("album") {
photoTotal += store.ToInt(album["photo_count"])
} }
items, err := c.DB.Query("SELECT a.*, (SELECT COUNT(*) FROM photos WHERE album_id = a.id) as photo_count, (SELECT p.path FROM photos p WHERE p.album_id = a.id ORDER BY p.created_at DESC LIMIT 1) as latest_photo FROM albums a WHERE a.created_by IN (?, ?) ORDER BY a.sort_order ASC, a.created_at DESC", ids[0], ids[1]) return demoListResponse(c, "album", M{"photo_total": photoTotal})
}
suffix, window := listWindow(c)
items, err := c.DB.Query("SELECT a.*, (SELECT COUNT(*) FROM photos WHERE album_id = a.id) as photo_count, (SELECT p.path FROM photos p WHERE p.album_id = a.id ORDER BY p.created_at DESC LIMIT 1) as latest_photo FROM albums a WHERE a.created_by IN (?, ?) ORDER BY a.sort_order ASC, a.created_at DESC, a.id DESC"+suffix, append([]any{ids[0], ids[1]}, window...)...)
if err != nil { if err != nil {
return err return err
} }
@@ -48,7 +53,9 @@ func albumList(c *httpapi.Ctx) error {
it["cover"] = it["latest_photo"] it["cover"] = it["latest_photo"]
} }
} }
return c.OK(rows(items)) total := countC(c, "SELECT COUNT(*) as c FROM albums WHERE created_by IN (?, ?)", ids[0], ids[1])
photoTotal := countC(c, "SELECT COUNT(*) as c FROM photos p JOIN albums a ON p.album_id=a.id WHERE a.created_by IN (?, ?)", ids[0], ids[1])
return listResponse(c, items, total, M{"photo_total": photoTotal})
} }
func albumAdd(c *httpapi.Ctx) error { func albumAdd(c *httpapi.Ctx) error {
@@ -185,13 +192,14 @@ func photos(c *httpapi.Ctx) error {
} }
ids := coupleUserIds(c, 0) ids := coupleUserIds(c, 0)
if len(ids) == 0 { if len(ids) == 0 {
return c.OK([]any{}) return listResponse(c, nil, 0, nil)
} }
items, err := c.DB.Query("SELECT p.* FROM photos p JOIN albums a ON p.album_id = a.id WHERE a.created_by IN (?, ?) AND p.album_id = ? ORDER BY p.sort_order ASC, p.created_at DESC", ids[0], ids[1], albumID) suffix, window := listWindow(c)
items, err := c.DB.Query("SELECT p.* FROM photos p JOIN albums a ON p.album_id = a.id WHERE a.created_by IN (?, ?) AND p.album_id = ? ORDER BY p.sort_order ASC, p.created_at DESC, p.id DESC"+suffix, append([]any{ids[0], ids[1], albumID}, window...)...)
if err != nil { if err != nil {
return err return err
} }
return c.OK(rows(items)) return listResponse(c, items, countC(c, "SELECT COUNT(*) as c FROM photos p JOIN albums a ON p.album_id=a.id WHERE a.created_by IN (?, ?) AND p.album_id=?", ids[0], ids[1], albumID), nil)
} }
// setFirstCover 相册第一张照片时自动设为封面 // setFirstCover 相册第一张照片时自动设为封面
+8 -1
View File
@@ -156,10 +156,14 @@ func decorateCheckin(item store.Row, uid int64, withAvatar bool) {
func checkinList(c *httpapi.Ctx) error { func checkinList(c *httpapi.Ctx) error {
ids := coupleUserIds(c, 0) ids := coupleUserIds(c, 0)
if len(ids) == 0 { if len(ids) == 0 {
if c.QHas("page") || c.QHas("page_size") {
return demoListResponse(c, "checkin", M{"week_both_count": 0, "checkin_log_total": 0})
}
return c.OK(M{"items": demoData("checkin"), "week_both_count": 0, "total": 5}) return c.OK(M{"items": demoData("checkin"), "week_both_count": 0, "total": 5})
} }
uid := c.UID() uid := c.UID()
items, err := c.DB.Query("SELECT c.*, u.nickname as creator_name, u.avatar as creator_avatar FROM checkins c LEFT JOIN users u ON c.created_by = u.id WHERE c.created_by IN (?, ?) AND NOT EXISTS (SELECT 1 FROM smoking_accounts sa WHERE sa.checkin_id = c.id) ORDER BY c.sort_order ASC, c.created_at DESC", ids[0], ids[1]) suffix, window := listWindow(c)
items, err := c.DB.Query("SELECT c.*, u.nickname as creator_name, u.avatar as creator_avatar FROM checkins c LEFT JOIN users u ON c.created_by = u.id WHERE c.created_by IN (?, ?) AND NOT EXISTS (SELECT 1 FROM smoking_accounts sa WHERE sa.checkin_id = c.id) ORDER BY c.sort_order ASC, c.created_at DESC, c.id DESC"+suffix, append([]any{ids[0], ids[1]}, window...)...)
if err != nil { if err != nil {
return err return err
} }
@@ -171,6 +175,9 @@ func checkinList(c *httpapi.Ctx) error {
if cid := c.CoupleID(); cid != 0 { if cid := c.CoupleID(); cid != 0 {
logTotal = countC(c, "SELECT COUNT(*) as c FROM checkin_logs WHERE couple_id = ?", cid) logTotal = countC(c, "SELECT COUNT(*) as c FROM checkin_logs WHERE couple_id = ?", cid)
} }
if c.QHas("page") || c.QHas("page_size") {
return listResponse(c, items, total, M{"week_both_count": 0, "checkin_log_total": logTotal})
}
return c.OK(M{"items": rows(items), "week_both_count": 0, "total": total, "checkin_log_total": logTotal}) return c.OK(M{"items": rows(items), "week_both_count": 0, "total": total, "checkin_log_total": logTotal})
} }
+3 -2
View File
@@ -251,14 +251,15 @@ func diaryCommentList(c *httpapi.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
comments, err := c.DB.Query("SELECT c.*, u.nickname, u.avatar FROM diary_comments c LEFT JOIN users u ON c.user_id = u.id WHERE c.diary_id = ? AND c.couple_id IN (SELECT couple_id FROM users WHERE id IN (?, ?)) ORDER BY c.created_at ASC", diaryID, ids[0], ids[1]) suffix, window := listWindow(c)
comments, err := c.DB.Query("SELECT c.*, u.nickname, u.avatar FROM diary_comments c LEFT JOIN users u ON c.user_id = u.id WHERE c.diary_id = ? AND c.couple_id IN (SELECT couple_id FROM users WHERE id IN (?, ?)) ORDER BY c.created_at DESC, c.id DESC"+suffix, append([]any{diaryID, ids[0], ids[1]}, window...)...)
if err != nil { if err != nil {
return err return err
} }
for _, cm := range comments { for _, cm := range comments {
cm["avatar_url"] = avatarURL(cm.Str("avatar")) cm["avatar_url"] = avatarURL(cm.Str("avatar"))
} }
return c.OK(rows(comments)) return listResponse(c, comments, countC(c, "SELECT COUNT(*) as c FROM diary_comments WHERE diary_id=? AND couple_id IN (SELECT couple_id FROM users WHERE id IN (?, ?))", diaryID, ids[0], ids[1]), nil)
} }
func diaryCommentAdd(c *httpapi.Ctx) error { func diaryCommentAdd(c *httpapi.Ctx) error {
@@ -0,0 +1,47 @@
package handler
import (
"past/internal/httpapi"
"past/internal/store"
)
// Keep legacy response shapes for released clients without pagination parameters.
func listWindow(c *httpapi.Ctx) (string, []any) {
if !c.QHas("page") && !c.QHas("page_size") {
return "", nil
}
_, size, offset := pageParams(c, 20, 100)
return " LIMIT ? OFFSET ?", []any{size, offset}
}
func listResponse(c *httpapi.Ctx, items []store.Row, total int64, extra M) error {
if !c.QHas("page") && !c.QHas("page_size") {
return c.OK(rows(items))
}
page, size, offset := pageParams(c, 20, 100)
result := M{"items": rows(items), "total": total, "page": page, "page_size": size, "has_more": offset+int64(len(items)) < total}
for k, v := range extra {
result[k] = v
}
return c.OK(result)
}
func demoListResponse(c *httpapi.Ctx, module string, extra M) error {
demo := demoData(module)
items := make([]store.Row, len(demo))
for i, item := range demo {
items[i] = store.Row(item)
}
total := int64(len(items))
if c.QHas("page") || c.QHas("page_size") {
_, size, offset := pageParams(c, 20, 100)
if offset >= total {
items = nil
} else {
end := offset + size
if end > total {
end = total
}
items = items[offset:end]
}
}
return listResponse(c, items, total, extra)
}
@@ -0,0 +1,71 @@
package handler
import (
"database/sql/driver"
"encoding/json"
"past/internal/httpapi"
"past/internal/testdb"
"strings"
"testing"
)
func TestPagedListsBoundQueriesAndKeepMemberScope(t *testing.T) {
for _, tt := range []struct {
name, uri, table string
handler httpapi.HandlerFunc
args []any
extra []testdb.Step
}{
{"songs", "/api/songsList", "songs", songsList, []any{int64(1), int64(3), int64(20), int64(20)}, nil},
{"albums", "/api/albumList", "albums", albumList, []any{int64(1), int64(3), int64(20), int64(20)}, []testdb.Step{{Match: "FROM photos p JOIN albums a", Columns: []string{"c"}, Rows: [][]driver.Value{{int64(83)}}}}},
{"photos", "/api/photos?album_id=9", "photos", photos, []any{int64(1), int64(3), "9", int64(20), int64(20)}, nil},
{"timeline", "/api/timelineList", "timeline", timelineList, []any{int64(1), int64(3), int64(20), int64(20)}, []testdb.Step{{Match: "event_date >= CURRENT_DATE", Columns: []string{"id", "event_date"}, Rows: [][]driver.Value{{int64(50), "2026-12-01"}}}}},
{"checkins", "/api/checkinList", "checkins", checkinList, []any{int64(1), int64(3), int64(20), int64(20)}, []testdb.Step{{Match: "FROM checkin_logs WHERE couple_id = ?", Args: []any{int64(22)}, Columns: []string{"c"}, Rows: [][]driver.Value{{int64(90)}}}}},
{"comments", "/api/diaryCommentList?diary_id=9", "diary_comments", diaryCommentList, []any{int64(9), int64(1), int64(3), int64(20), int64(20)}, nil},
} {
t.Run(tt.name, func(t *testing.T) {
uri := tt.uri
if strings.Contains(uri, "?") {
uri += "&"
} else {
uri += "?"
}
uri += "page=2&page_size=20"
steps := []testdb.Step{uidStep(1), cidStep(22), {Match: "SELECT user1_id, user2_id FROM couples", Columns: []string{"user1_id", "user2_id"}, Rows: [][]driver.Value{{int64(1), int64(3)}}},
{Match: "LIMIT ? OFFSET ?", Args: tt.args, Check: func(q string, _ []any) error {
if !strings.Contains(q, "IN (?, ?)") {
t.Errorf("missing member predicate: %s", q)
}
return nil
}, Columns: []string{"id"}, Rows: [][]driver.Value{{int64(21)}}},
{Match: "SELECT COUNT(*) as c FROM " + tt.table, Columns: []string{"c"}, Rows: [][]driver.Value{{int64(41)}}}}
steps = append(steps, tt.extra...)
c, out, _ := permissionCtx(t, "GET", uri, "", steps)
tt.handler(c)
if out.Code != 200 {
t.Fatalf("status %d: %s", out.Code, out.Body.String())
}
var result map[string]any
if err := json.Unmarshal(out.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result["total"] != float64(41) || result["page"] != float64(2) || result["has_more"] != true {
t.Fatalf("wrong pagination: %s", out.Body.String())
}
if len(result["items"].([]any)) != 1 {
t.Fatal("wrong items")
}
if tt.name == "albums" && result["photo_total"] != float64(83) {
t.Fatal("photo total must cover all albums")
}
})
}
}
func TestListPaginationCapsPageSizeAndClampsPage(t *testing.T) {
c, _, _ := permissionCtx(t, "GET", "/api/list?page=-1&page_size=9999", "", nil)
suffix, args := listWindow(c)
if suffix != " LIMIT ? OFFSET ?" || args[0] != int64(100) || args[1] != int64(0) {
t.Fatalf("unsafe window %s %v", suffix, args)
}
}
+6 -2
View File
@@ -55,7 +55,7 @@ func smokingDelta(kind string, quantity, stock int64) (int64, error) {
} }
func smokingSnapshot(c *httpapi.Ctx, uid int64) (M, error) { func smokingSnapshot(c *httpapi.Ctx, uid int64) (M, error) {
result := M{"stock": int64(0), "today_count": int64(0), "total_count": int64(0), "week_count": int64(0), "month_count": int64(0), "week_purchase_cents": int64(0), "month_purchase_cents": int64(0), "logs": []any{}, "daily": []any{}, "last_time": "", "user_id": uid, "read_only": uid != c.UID(), "available": true} result := M{"stock": int64(0), "today_count": int64(0), "total_count": int64(0), "week_count": int64(0), "month_count": int64(0), "week_purchase_cents": int64(0), "month_purchase_cents": int64(0), "logs": []any{}, "logs_total": int64(0), "logs_page": int64(1), "logs_has_more": false, "daily": []any{}, "last_time": "", "user_id": uid, "read_only": uid != c.UID(), "available": true}
owner, err := c.DB.QueryOne("SELECT nickname, username FROM users WHERE id = ?", uid) owner, err := c.DB.QueryOne("SELECT nickname, username FROM users WHERE id = ?", uid)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -97,11 +97,15 @@ func smokingSnapshot(c *httpapi.Ctx, uid int64) (M, error) {
} }
result["last_time"] = summary.Str("last_time") result["last_time"] = summary.Str("last_time")
} }
logs, err := c.DB.Query("SELECT id, kind, quantity, delta, balance, amount_cents, note, undone, created_at FROM smoking_entries WHERE user_id = ? ORDER BY id DESC LIMIT 50", aid) page, size, offset := pageParams(c, 20, 100)
result["logs_page"] = page
result["logs_total"] = countC(c, "SELECT COUNT(*) as c FROM smoking_entries WHERE user_id=?", aid)
logs, err := c.DB.Query("SELECT id, kind, quantity, delta, balance, amount_cents, note, undone, created_at FROM smoking_entries WHERE user_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", aid, size, offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result["logs"] = rows(logs) result["logs"] = rows(logs)
result["logs_has_more"] = offset+int64(len(logs)) < store.ToInt(result["logs_total"])
daily, err := c.DB.Query("SELECT DATE_FORMAT(created_at,'%Y-%m-%d') AS day, COUNT(*) AS count FROM smoking_entries WHERE user_id = ? AND kind IN ('consume','receive_smoke') AND undone = 0 AND created_at >= DATE_SUB(CURRENT_DATE, INTERVAL 6 DAY) GROUP BY DATE_FORMAT(created_at,'%Y-%m-%d') ORDER BY day", aid) daily, err := c.DB.Query("SELECT DATE_FORMAT(created_at,'%Y-%m-%d') AS day, COUNT(*) AS count FROM smoking_entries WHERE user_id = ? AND kind IN ('consume','receive_smoke') AND undone = 0 AND created_at >= DATE_SUB(CURRENT_DATE, INTERVAL 6 DAY) GROUP BY DATE_FORMAT(created_at,'%Y-%m-%d') ORDER BY day", aid)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -35,7 +35,8 @@ func TestSmokingSnapshotWeeklyMonthlyStats(t *testing.T) {
} }
return nil return nil
}, Columns: []string{"today_count", "total_count", "week_count", "month_count", "week_purchase_cents", "month_purchase_cents", "last_time"}, Rows: [][]driver.Value{{int64(1), int64(120), int64(7), int64(29), int64(3050), int64(12500), "2026-10-11 09:00:00"}}}, }, Columns: []string{"today_count", "total_count", "week_count", "month_count", "week_purchase_cents", "month_purchase_cents", "last_time"}, Rows: [][]driver.Value{{int64(1), int64(120), int64(7), int64(29), int64(3050), int64(12500), "2026-10-11 09:00:00"}}},
testdb.Step{Match: "ORDER BY id DESC LIMIT 50", Columns: []string{"id"}}, testdb.Step{Match: "SELECT COUNT(*) as c FROM smoking_entries WHERE user_id=?", Args: []any{int64(2)}, Columns: []string{"c"}, Rows: [][]driver.Value{{int64(120)}}},
testdb.Step{Match: "ORDER BY id DESC LIMIT ? OFFSET ?", Args: []any{int64(2), int64(20), int64(0)}, Columns: []string{"id"}},
testdb.Step{Match: "GROUP BY DATE_FORMAT", Columns: []string{"day", "count"}}, testdb.Step{Match: "GROUP BY DATE_FORMAT", Columns: []string{"day", "count"}},
) )
} }
+4 -3
View File
@@ -17,13 +17,14 @@ func registerSongs(r *httpapi.Router) {
func songsList(c *httpapi.Ctx) error { func songsList(c *httpapi.Ctx) error {
ids := coupleUserIds(c, 0) ids := coupleUserIds(c, 0)
if len(ids) == 0 { if len(ids) == 0 {
return c.OK(demoData("songs")) return demoListResponse(c, "songs", nil)
} }
items, err := c.DB.Query("SELECT s.*, u.nickname, u.avatar FROM songs s LEFT JOIN users u ON s.created_by = u.id WHERE s.created_by IN (?, ?) ORDER BY s.created_at DESC", ids[0], ids[1]) suffix, window := listWindow(c)
items, err := c.DB.Query("SELECT s.*, u.nickname, u.avatar FROM songs s LEFT JOIN users u ON s.created_by = u.id WHERE s.created_by IN (?, ?) ORDER BY s.created_at DESC, s.id DESC"+suffix, append([]any{ids[0], ids[1]}, window...)...)
if err != nil { if err != nil {
return err return err
} }
return c.OK(rows(items)) return listResponse(c, items, countC(c, "SELECT COUNT(*) as c FROM songs WHERE created_by IN (?, ?)", ids[0], ids[1]), nil)
} }
func songText(c *httpapi.Ctx) string { func songText(c *httpapi.Ctx) string {
+9 -3
View File
@@ -44,16 +44,22 @@ func checkTextOr400(c *httpapi.Ctx, text string) error {
func timelineList(c *httpapi.Ctx) error { func timelineList(c *httpapi.Ctx) error {
ids := coupleUserIds(c, 0) ids := coupleUserIds(c, 0)
if len(ids) == 0 { if len(ids) == 0 {
return c.OK(demoData("timeline")) return demoListResponse(c, "timeline", nil)
} }
items, err := c.DB.Query("SELECT * FROM timeline WHERE created_by IN (?, ?) ORDER BY event_date DESC, sort_order ASC", ids[0], ids[1]) suffix, window := listWindow(c)
items, err := c.DB.Query("SELECT * FROM timeline WHERE created_by IN (?, ?) ORDER BY event_date DESC, sort_order ASC, id DESC"+suffix, append([]any{ids[0], ids[1]}, window...)...)
if err != nil { if err != nil {
return err return err
} }
for _, it := range items { for _, it := range items {
it["images"] = jsonDecodeList(it["images"]) it["images"] = jsonDecodeList(it["images"])
} }
return c.OK(rows(items)) total := countC(c, "SELECT COUNT(*) as c FROM timeline WHERE created_by IN (?, ?)", ids[0], ids[1])
upcoming, err := c.DB.QueryOne("SELECT * FROM timeline WHERE created_by IN (?, ?) AND event_date >= CURRENT_DATE ORDER BY event_date ASC, id DESC LIMIT 1", ids[0], ids[1])
if err != nil {
return err
}
return listResponse(c, items, total, M{"upcoming": upcoming})
} }
func timelineAdd(c *httpapi.Ctx) error { func timelineAdd(c *httpapi.Ctx) error {
+16 -3
View File
@@ -1,9 +1,10 @@
const { mergeUnique } = require('../../utils/list-pagination');
const { api, getImageUrl } = require('../../utils/request'); const { api, getImageUrl } = require('../../utils/request');
const cache = require('../../utils/data-cache'); const cache = require('../../utils/data-cache');
const titles = {consume:'抽一支 · 打卡',purchase:'买烟',receive:'别人递烟',receive_smoke:'收到并抽一支',adjust:'调整库存',give:'递给别人一支',undo:'撤销记录'}; const titles = {consume:'抽一支 · 打卡',purchase:'买烟',receive:'别人递烟',receive_smoke:'收到并抽一支',adjust:'调整库存',give:'递给别人一支',undo:'撤销记录'};
Component({ Component({
properties: { detailed: Boolean }, 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();} }, lifetimes: { attached(){this.load();} },
pageLifetimes: { show(){this.load();} }, pageLifetimes: { show(){this.load();} },
methods: { methods: {
@@ -16,6 +17,7 @@ Component({
return this._loading; return this._loading;
}, },
applyInfo(info) { 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):''})); 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='还没有记录'; 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)+' 天';} 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 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 ownerName=owner.nickname||info.owner_name||owner.username||(info.read_only?'TA':'我');
const ownerAvatar=getImageUrl(owner.avatar||info.owner_avatar||''); 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:''});}, avatarFailed(){this.setData({ownerAvatar:''});},
retry(){this.load(true);}, retry(){this.load(true);},
openDetail(){if(!this.properties.detailed)wx.navigateTo({url:'/pages/smoking-detail/smoking-detail'});}, 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:''});}, 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:''});}, 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});}, 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> <text class="smoke-period-hint">购买金额按已填写金额累计,已撤销记录不计入</text>
<view class="section-title">最近七天<text>累计 {{info.total_count}} 支</text></view> <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="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="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="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>
</block> </block>
</view> </view>
@@ -14,3 +14,5 @@
@import '../../styles/typography.wxss'; @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-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}
+18 -14
View File
@@ -1,3 +1,4 @@
const { itemsOf, startList, current, mergeUnique, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
const { currentUserLocation } = require('../../utils/user-location'); const { currentUserLocation } = require('../../utils/user-location');
const { photoForm, metadataRows } = require('../../utils/photo-metadata'); const { photoForm, metadataRows } = require('../../utils/photo-metadata');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
@@ -5,7 +6,7 @@ const { getLayout } = require('../../utils/layout');
// pages/album/album.js - 日记式分组相册 // pages/album/album.js - 日记式分组相册
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request'); const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
const { requestSubscribeAfterAction } = require('../../utils/subscribe'); const { requestSubscribeAfterAction } = require('../../utils/subscribe');
Page(withDataCache({ Page(withDataCache(withListPagination({
onResize: function() { this.setData(getLayout()); }, onResize: function() { this.setData(getLayout()); },
data: { loading: true, loadError: false, data: { loading: true, loadError: false,
batches: [], batches: [],
@@ -86,7 +87,8 @@ Page(withDataCache({
return { ...batch, photos, metadata: metadataRows(photos) }; return { ...batch, photos, metadata: metadataRows(photos) };
}); });
const previewImages = this.data.previewImages.map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item); const previewImages = this.data.previewImages.map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item);
this.setData({ batches, previewImages, showPhotoEditor: false }); this._photoRows = (this._photoRows || []).map(item => String(item.id) === String(photo.id) ? { ...item, ...form } : item);
this.setData({ batches, photoItems: this._photoRows, previewImages, showPhotoEditor: false });
wx.showToast({ title: '照片信息已保存', icon: 'success' }); wx.showToast({ title: '照片信息已保存', icon: 'success' });
} catch (error) { } catch (error) {
wx.showToast({ title: (error && (error.error || error.message)) || '保存失败,请重试', icon: 'none' }); wx.showToast({ title: (error && (error.error || error.message)) || '保存失败,请重试', icon: 'none' });
@@ -129,15 +131,14 @@ Page(withDataCache({
}; };
}, },
loadData: async function() { loadData: async function(page) {
if (this._pageLoading) return; const ticket = startList(this,page);
this._pageLoading = true; if (!ticket) return;
this.setData({loading: !this._hasCachedData, loadError:false});
try { try {
var self = this; var self = this;
var albumId = this.data.albumId; var albumId = this.data.albumId;
if (!albumId) { if (!albumId) {
var albums = await api.albumList(); var albums = itemsOf(await api.albumList({page:1,page_size:1}));
if (albums && albums.length > 0) { if (albums && albums.length > 0) {
albumId = albums[0].id; albumId = albums[0].id;
this.setData({ albumId: albumId }); this.setData({ albumId: albumId });
@@ -145,11 +146,14 @@ Page(withDataCache({
} }
if (albumId) { if (albumId) {
// 共享内存缓存;写操作成功后自动失效。 // 共享内存缓存;写操作成功后自动失效。
var photos = await api.photos(albumId); var result = await api.photos(albumId, {page:ticket.page,page_size:20});
if (!current(this,ticket)) return;
var photos = ticket.page === 1 ? itemsOf(result) : mergeUnique(this._photoRows || [], itemsOf(result));
// 根据 batch_id 分组 // 根据 batch_id 分组
var batchMap = {}; var batchMap = {};
var myUid = wx.getStorageSync('userId'); var myUid = wx.getStorageSync('userId');
var info = await api.coupleInfo(); var info = await api.coupleInfo();
if (!current(this,ticket)) return;
var p1 = info.partner1 || {}, p2 = info.partner2 || {}; var p1 = info.partner1 || {}, p2 = info.partner2 || {};
var nameMap = {}; var nameMap = {};
var avatarMap = {}; var avatarMap = {};
@@ -182,13 +186,13 @@ Page(withDataCache({
var batches = Object.values(batchMap); var batches = Object.values(batchMap);
batches.sort(function(a, b) { return b.created_at > a.created_at ? 1 : -1; }); batches.sort(function(a, b) { return b.created_at > a.created_at ? 1 : -1; });
batches.forEach(function(batch) { batch.metadata = metadataRows(batch.photos); }); batches.forEach(function(batch) { batch.metadata = metadataRows(batch.photos); });
this.setData({ batches: batches }); this._photoRows = photos;
} else { this.setData({batches:[]}); } applyList(this,ticket,result,itemsOf(result),'photoItems',{batches:batches});
} else { applyList(this,ticket,{items:[],total:0,has_more:false},[],'photoItems',{batches:[]}); }
} catch (e) { } catch (e) {
this.setData({loadError:true}); console.error(e); } failList(this,ticket); console.error(e); }
finally { finally {
this._pageLoading = false; finishList(this,ticket);
this.setData({loading:false});
if (this._visible && this._pendingPhotoPicker && !this.data.loadError && this.data.albumId) { if (this._visible && this._pendingPhotoPicker && !this.data.loadError && this.data.albumId) {
this._pendingPhotoPicker = false; this._pendingPhotoPicker = false;
this.chooseImage(); this.chooseImage();
@@ -522,4 +526,4 @@ Page(withDataCache({
goToAlbums: function() { wx.navigateBack(); }, goToAlbums: function() { wx.navigateBack(); },
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
}, {"loadData": ["photos", "albumList"]})); }, 'loadData'), {"loadData": ["photos", "albumList"]}));
+2 -1
View File
@@ -5,7 +5,7 @@
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd" wx:if="{{!showUploadModal}}"> <scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd" wx:if="{{!showUploadModal}}">
<view class="container"><view class="j-row"><view><text class="j-eyebrow">MOMENTS WE KEEP</text><text class="j-page-title">相册里的片段</text></view><button class="j-button j-button-small" bindtap="chooseImage">+ 照片</button></view> <view class="container"><view class="j-row"><view><text class="j-eyebrow">MOMENTS WE KEEP</text><text class="j-page-title">相册里的片段</text></view><button class="j-button j-button-small" bindtap="chooseImage">+ 照片</button></view>
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" /> <page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
<view class="moments-list" wx:if="{{!loading && !loadError && batches.length > 0}}"> <view class="moments-list" wx:if="{{!loading && !loadError && batches.length > 0}}">
@@ -36,6 +36,7 @@
</view> </view>
</view> </view>
<view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部照片</text></view>
</scroll-view> </scroll-view>
<view class="preview-overlay" wx:if="{{showPreview}}" bindtap="closePreview"> <view class="preview-overlay" wx:if="{{showPreview}}" bindtap="closePreview">
+11 -10
View File
@@ -1,8 +1,9 @@
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout'); const { getLayout } = require('../../utils/layout');
// pages/albums/albums.js // pages/albums/albums.js
const { api, getImageUrl } = require('../../utils/request'); const { api, getImageUrl } = require('../../utils/request');
Page(withDataCache({ Page(withDataCache(withListPagination({
onResize: function() { this.setData(getLayout()); }, onResize: function() { this.setData(getLayout()); },
data: { data: {
loading: true, loadError: false, loading: true, loadError: false,
@@ -47,21 +48,21 @@ Page(withDataCache({
noop: function() {}, noop: function() {},
loadAlbums: async function() { loadAlbums: async function(page) {
if (this._loading) return; const ticket = startList(this, page);
this._loading = true; if (!ticket) return;
this.setData({ loading: !this._hasCachedData, loadError: false });
try { try {
var list = await api.albumList(null, 'GET', true); var list = await api.albumList({page:ticket.page,page_size:20});
list = (list || []).map(function(a) { var result = list;
list = itemsOf(list).map(function(a) {
a.cover = getImageUrl(a.cover); a.cover = getImageUrl(a.cover);
a.photo_count = a.photo_count || 0; a.photo_count = a.photo_count || 0;
// 地理位置:优先显示地址名称,其次显示经纬度 // 地理位置:优先显示地址名称,其次显示经纬度
a.location_text = a.location || ((a.lat && a.lng) ? (a.lat + ', ' + a.lng) : ''); a.location_text = a.location || ((a.lat && a.lng) ? (a.lat + ', ' + a.lng) : '');
return a; return a;
}); });
this.setData({ albums: list }); applyList(this, ticket, result, list, 'albums');
} catch(e) { console.error(e); this.setData({ loadError: true }); } finally { this._loading = false; this.setData({ loading: false }); } } catch(e) { console.error(e); failList(this,ticket); } finally { finishList(this,ticket); }
}, },
openAlbum: function(e) { openAlbum: function(e) {
@@ -174,4 +175,4 @@ Page(withDataCache({
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
goBack: function() { wx.navigateBack(); }, goBack: function() { wx.navigateBack(); },
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
}, {"loadAlbums": ["albumList"]})); }, 'loadAlbums'), {"loadAlbums": ["albumList"]}));
+2 -1
View File
@@ -3,7 +3,7 @@
<!-- pages/albums/albums.wxml - 相册列表 --> <!-- pages/albums/albums.wxml - 相册列表 -->
<app-nav title="{{uploadMode ? '选择相册' : '相册'}}" back="{{true}}" /> <app-nav title="{{uploadMode ? '选择相册' : '相册'}}" back="{{true}}" />
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}"> <scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
<view class="j-body"><view class="j-row"><view><text class="j-eyebrow">MOMENTS WE KEEP</text><text class="j-page-title">{{uploadMode ? '选择上传照片的相册' : '照片里的日常'}}</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 建相册</button></view> <view class="j-body"><view class="j-row"><view><text class="j-eyebrow">MOMENTS WE KEEP</text><text class="j-page-title">{{uploadMode ? '选择上传照片的相册' : '照片里的日常'}}</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 建相册</button></view>
<page-state wx:if="{{loading}}" kind="loading" title="正在打开相册" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadAlbums" /> <page-state wx:if="{{loading}}" kind="loading" title="正在打开相册" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadAlbums" />
<view class="j-album-list" wx:else><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><view class="j-row"><view><text class="j-story-title">{{item.title || '我们的相册'}}</text><text class="j-caption">{{item.photo_count || 0}} 张照片</text></view><view class="j-link j-album-menu" catchtap="showMoreMenu" data-id="{{item.id}}" data-title="{{item.title}}" data-desc="{{item.description}}" data-cover="{{item.cover}}">•••</view></view><text wx:if="{{item.description}}" class="j-caption">{{item.description}}</text></view></view> <view class="j-album-list" wx:else><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><view class="j-row"><view><text class="j-story-title">{{item.title || '我们的相册'}}</text><text class="j-caption">{{item.photo_count || 0}} 张照片</text></view><view class="j-link j-album-menu" catchtap="showMoreMenu" data-id="{{item.id}}" data-title="{{item.title}}" data-desc="{{item.description}}" data-cover="{{item.cover}}">•••</view></view><text wx:if="{{item.description}}" class="j-caption">{{item.description}}</text></view></view>
@@ -41,6 +41,7 @@
</view> </view>
</view> </view>
<view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部</text></view>
</scroll-view> </scroll-view>
+9 -39
View File
@@ -1,10 +1,11 @@
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout'); const { getLayout } = require('../../utils/layout');
// Diary journal with server pagination. // Diary journal with server pagination.
const { api, getImageUrl } = require('../../utils/request'); const { api, getImageUrl } = require('../../utils/request');
const { diaryEntry } = require('../../utils/journal'); const { diaryEntry } = require('../../utils/journal');
const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos'); const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos');
Page(withDataCache({ Page(withDataCache(withListPagination({
onResize: function() { this.setData(getLayout()); }, onResize: function() { this.setData(getLayout()); },
data: { data: {
loading: true, loadError: false, loading: true, loadError: false,
@@ -51,45 +52,14 @@ Page(withDataCache({
return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' }; return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' };
}, },
loadData: async function() { loadData: async function(page) {
if (this._loading) return; const ticket=startList(this,page);if(!ticket)return;
this._loading = true;
this.setData({ loading: !this._hasCachedData, loadError: false });
try { try {
var data = await api.diaryList({ page: 1, page_size: this.data.pageSize }); const data=await api.diaryList({page:ticket.page,page_size:20});
var items = this._formatItems(data.items || []); applyList(this,ticket,data,this._formatItems(itemsOf(data)),'list');
this.setData({
page: 1,
list: items,
total: data.total || 0,
hasMore: (data.total || 0) > this.data.pageSize
});
this._loadPartnerInfo(); this._loadPartnerInfo();
} catch (e) { } catch(e) { failList(this,ticket); }
this.setData({ loadError: true }); finally { finishList(this,ticket); }
console.error(e);
wx.showToast({ title: (e && (e.error || e.errMsg || e.message)) || '加载失败', icon: 'none' });
} finally { this._loading = false; this.setData({ loading: false }); }
},
loadMore: async function() {
if (this.data.loadingMore || !this.data.hasMore) return;
this.setData({ loadingMore: true });
try {
var nextPage = this.data.page + 1;
var data = await api.diaryList({ page: nextPage, page_size: this.data.pageSize });
var newItems = this._formatItems(data.items || []);
var merged = this.data.list.concat(newItems);
this.setData({
list: merged,
page: nextPage,
hasMore: merged.length < (data.total || 0),
loadingMore: false
});
this._loadPartnerInfo();
} catch (e) {
this.setData({ loadingMore: false });
}
}, },
_formatItems: function(items) { _formatItems: function(items) {
@@ -194,4 +164,4 @@ Page(withDataCache({
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); }, goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
goBack: function() { wx.navigateBack(); }, goBack: function() { wx.navigateBack(); },
goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); } goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); }
}, {"loadData": ["diaryList", "coupleInfo"]})); }, 'loadData'), {"loadData": ["diaryList", "coupleInfo"]}));
+2 -1
View File
@@ -5,4 +5,5 @@
<view class="j-body"><view class="j-row"><view><text class="j-eyebrow">LIFE IN WORDS</text><text class="j-page-title">生活里的每一页</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 写日记</button></view> <view class="j-body"><view class="j-row"><view><text class="j-eyebrow">LIFE IN WORDS</text><text class="j-page-title">生活里的每一页</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 写日记</button></view>
<page-state wx:if="{{loading}}" kind="loading" title="正在翻开日记" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadData" /> <page-state wx:if="{{loading}}" kind="loading" title="正在翻开日记" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadData" />
<block wx:else><block wx:for="{{list}}" wx:key="id"><template is="journal-entry" data="{{entry:item}}" /></block><page-state wx:if="{{!list.length}}" title="故事,从今天开始" description="写下第一篇,记录一个平常又特别的瞬间" action="写日记" bind:action="goToPublish" /></block> <block wx:else><block wx:for="{{list}}" wx:key="id"><template is="journal-entry" data="{{entry:item}}" /></block><page-state wx:if="{{!list.length}}" title="故事,从今天开始" description="写下第一篇,记录一个平常又特别的瞬间" action="写日记" bind:action="goToPublish" /></block>
<view class="j-caption j-center" wx:if="{{loadingMore}}">正在加载更多…</view></view></scroll-view></view> <view class="j-caption j-center" wx:if="{{loadingMore}}">正在加载更多…</view></view><view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部日记</text></view>
</scroll-view></view>
+25 -16
View File
@@ -1,3 +1,4 @@
const { itemsOf, totalOf, mergeUnique } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout'); const { getLayout } = require('../../utils/layout');
const { api, getImageUrl } = require('../../utils/request'); const { api, getImageUrl } = require('../../utils/request');
@@ -8,7 +9,7 @@ Page(withDataCache({
diary: null, diary: null,
loading: true, loading: true,
comments: [], comments: [],
commentPage: 1, commentPage: 1, commentsLoadingMore: false, commentsMoreError: false, commentsTotal: 0,
hasMoreComments: true, hasMoreComments: true,
showCommentInput: false, showCommentInput: false,
commentText: '', commentText: '',
@@ -78,24 +79,28 @@ Page(withDataCache({
return data; return data;
}, },
loadComments: async function() { loadComments: function() { return this._loadCommentsPage(1); },
this.setData({ commentsError: false }); _loadCommentsPage: async function(page) {
page = Number(page) || 1;
if (this._commentsFirst || (page > 1 && (this.data.commentsLoadingMore || !this.data.hasMoreComments))) return;
if (page === 1) { this._commentsFirst = true; this._commentsGeneration = (this._commentsGeneration || 0) + 1; }
var generation = this._commentsGeneration;
this.setData(page === 1 ? {commentsError:false,commentsMoreError:false,commentsLoadingMore:false} : {commentsLoadingMore:true,commentsMoreError:false});
try { try {
var data = await api.diaryCommentList({ diary_id: this.data.diaryId }); var result = await api.diaryCommentList({diary_id:this.data.diaryId,page:page,page_size:20});
if (data && data.length > 0) { if (generation !== this._commentsGeneration) return;
data.forEach(function(c) { var added = itemsOf(result).map(function(comment) {
if (c.created_at) { return Object.assign({}, comment, {_time:String(comment.created_at || comment.create_time || '').slice(0,16).replace('T',' ')});
c._time = c.created_at.substring(0, 16).replace('T', ' ');
} else if (c.create_time) {
c._time = c.create_time.substring(0, 16).replace('T', ' ');
} else {
c._time = '';
}
}); });
this.setData({comments:page===1?added:mergeUnique(this.data.comments,added),commentPage:page,commentsTotal:totalOf(result),hasMoreComments:!Array.isArray(result)&&(result.has_more != null?!!result.has_more:page*20<totalOf(result))});
} catch(e) {
if (generation === this._commentsGeneration) this.setData(page===1?{commentsError:true}:{commentsMoreError:true});
} finally {
if (generation === this._commentsGeneration) { this._commentsFirst=false;this.setData({commentsLoadingMore:false}); }
} }
this.setData({ comments: data || [] });
} catch (e) { this.setData({ commentsError: true }); console.error(e); }
}, },
loadMoreComments: function() { return this._loadCommentsPage(this.data.commentPage+1); },
onShareTap: function() { onShareTap: function() {
wx.showActionSheet({ wx.showActionSheet({
@@ -177,6 +182,9 @@ Page(withDataCache({
} }
var res = await api.diaryCommentAdd(params); var res = await api.diaryCommentAdd(params);
if (res.success) { if (res.success) {
this._commentsGeneration = (this._commentsGeneration || 0) + 1;
this._commentsFirst = false;
this.setData({commentsLoadingMore:false});
var newComment = res.comment || {}; var newComment = res.comment || {};
if (newComment.created_at) { if (newComment.created_at) {
newComment._time = newComment.created_at.substring(0, 16).replace('T', ' '); newComment._time = newComment.created_at.substring(0, 16).replace('T', ' ');
@@ -187,9 +195,10 @@ Page(withDataCache({
newComment._time = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + ' ' + String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0'); newComment._time = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + ' ' + String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
} }
var newComments = this.data.comments.slice(); var newComments = this.data.comments.slice();
newComments.push(newComment); newComments.unshift(newComment);
this.setData({ this.setData({
comments: newComments, comments: newComments,
commentsTotal: this.data.commentsTotal + 1,
showCommentInput: false, showCommentInput: false,
replyTo: null, replyTo: null,
commentText: '', commentText: '',
+3 -1
View File
@@ -3,7 +3,7 @@
<app-nav title="日记详情" back="{{true}}" /><view class="page-bg"> <app-nav title="日记详情" back="{{true}}" /><view class="page-bg">
<view class="read-state" wx:if="{{loading || loadError}}"><page-state wx:if="{{loading}}" kind="loading" title="正在加载日记" /><page-state wx:else kind="error" title="日记加载失败" action="重试" bind:action="loadDiary" /></view> <view class="read-state" wx:if="{{loading || loadError}}"><page-state wx:if="{{loading}}" kind="loading" title="正在加载日记" /><page-state wx:else kind="error" title="日记加载失败" action="重试" bind:action="loadDiary" /></view>
<scroll-view scroll-y class="read-scroll" wx:if="{{!loading && !loadError && diary}}" show-scrollbar="{{false}}" enhanced> <scroll-view scroll-y bindscrolltolower="loadMoreComments" lower-threshold="120" class="read-scroll" wx:if="{{!loading && !loadError && diary}}" show-scrollbar="{{false}}" enhanced>
<view class="read-page"> <view class="read-page">
<view class="read-author"><image class="read-avatar" src="{{diary.creator_avatar || '/images/default-avatar.svg'}}" mode="aspectFill" /><view class="read-author-info"><text class="read-name">{{diary.creator_name || 'TA'}}</text><text class="read-date">{{diary.created_time}}</text><text class="read-location" wx:if="{{diary.location_text}}">{{diary.location_text}}</text></view><view class="read-menu" catchtap="onMoreTap" aria-label="日记操作"><image src="/images/more.svg" mode="aspectFit" /></view></view> <view class="read-author"><image class="read-avatar" src="{{diary.creator_avatar || '/images/default-avatar.svg'}}" mode="aspectFill" /><view class="read-author-info"><text class="read-name">{{diary.creator_name || 'TA'}}</text><text class="read-date">{{diary.created_time}}</text><text class="read-location" wx:if="{{diary.location_text}}">{{diary.location_text}}</text></view><view class="read-menu" catchtap="onMoreTap" aria-label="日记操作"><image src="/images/more.svg" mode="aspectFit" /></view></view>
<view class="read-paper"> <view class="read-paper">
@@ -58,6 +58,8 @@
</text> </text>
</view> </view>
</view> </view>
<view class="j-pagination" wx:if="{{commentsError}}" bindtap="loadComments">留言加载失败,点击重试</view>
<view class="j-pagination" wx:elif="{{comments.length}}"><text wx:if="{{commentsLoadingMore}}">正在加载…</text><text wx:elif="{{commentsMoreError}}" bindtap="loadMoreComments">加载失败,点击重试</text><text wx:elif="{{hasMoreComments}}" bindtap="loadMoreComments">加载更多留言</text><text wx:else>已加载全部留言</text></view>
</view> </view>
</view> </view>
+4 -8
View File
@@ -1,3 +1,4 @@
const { itemsOf, totalOf } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout'); const { getLayout } = require('../../utils/layout');
// pages/discover/discover.js // pages/discover/discover.js
@@ -46,18 +47,13 @@ Page(withDataCache({
const [timeline, album, diary] = await Promise.all([ const [timeline, album, diary] = await Promise.all([
api.timelineList(), api.albumList(), api.diaryList() api.timelineList(), api.albumList(), api.diaryList()
]); ]);
var photoCount = 0; var photoCount = album && album.photo_total != null ? Number(album.photo_total) : itemsOf(album).reduce((sum,item)=>sum+(Number(item.photo_count)||0),0);
if (Array.isArray(album)) {
for (var i = 0; i < album.length; i++) {
photoCount += parseInt(album[i].photo_count || 0);
}
}
var stats = { var stats = {
timeline: Array.isArray(timeline) ? timeline.length : 0, timeline: totalOf(timeline),
album: photoCount, album: photoCount,
diary: (diary && diary.total) ? diary.total : 0, diary: (diary && diary.total) ? diary.total : 0,
}; };
this.setData({ stats: stats, entries: ((diary && diary.items) || []).map(diaryEntry), albums: (Array.isArray(album) ? album : []).map(albumEntry), memories: (Array.isArray(timeline) ? timeline : []).map(memoryEntry) }); this.setData({ stats: stats, entries: ((diary && diary.items) || []).map(diaryEntry), albums: itemsOf(album).map(albumEntry), memories: itemsOf(timeline).map(memoryEntry) });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
this.setData({loadError:true}); this.setData({loadError:true});
+7 -11
View File
@@ -1,3 +1,4 @@
const { itemsOf, totalOf } = require('../../utils/list-pagination');
let lastLocationReport = 0; let lastLocationReport = 0;
let locationReportUser = null; let locationReportUser = null;
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
@@ -221,29 +222,24 @@ Page(withDataCache({
api.songsList(), api.checkinList(), api.songsList(), api.checkinList(),
this.data.coupleId ? api.petInfo() : Promise.resolve({ has_pet: false }) this.data.coupleId ? api.petInfo() : Promise.resolve({ has_pet: false })
]); ]);
var photoCount = 0; var photoCount = album && album.photo_total != null ? Number(album.photo_total) : itemsOf(album).reduce((sum,item)=>sum+(Number(item.photo_count)||0),0);
if (Array.isArray(album)) {
for (var i = 0; i < album.length; i++) {
photoCount += parseInt(album[i].photo_count || 0);
}
}
var petLevel = 0; var petLevel = 0;
if (pet && pet.has_pet && pet.pet) { if (pet && pet.has_pet && pet.pet) {
petLevel = parseInt(pet.pet.level || 0); petLevel = parseInt(pet.pet.level || 0);
} }
var recentDiary = ((diary && diary.items) || []).slice(0, 1).map(diaryEntry); var recentDiary = ((diary && diary.items) || []).slice(0, 1).map(diaryEntry);
var today = new Date(); today.setHours(0,0,0,0); var today = new Date(); today.setHours(0,0,0,0);
var future = (Array.isArray(timeline) ? timeline : []).filter(function(item) { return item.event_date && new Date(item.event_date.replace(/-/g, '/')) >= today; }).sort(function(a,b) { return a.event_date.localeCompare(b.event_date); }); var future = itemsOf(timeline).filter(function(item) { return item.event_date && new Date(item.event_date.replace(/-/g, '/')) >= today; }).sort(function(a,b) { return a.event_date.localeCompare(b.event_date); });
var next = future[0]; var next = timeline && !Array.isArray(timeline) && 'upcoming' in timeline ? timeline.upcoming : future[0];
this.setData({ this.setData({
recentDiary: recentDiary, recentDiary: recentDiary,
recentAlbums: (Array.isArray(album) ? album : []).filter(function(item) { return !!item.cover; }).slice(0,3).map(function(item) { return { id:item.id, title:item.title || '我们的相册', cover:getImageUrl(item.cover) }; }), recentAlbums: itemsOf(album).filter(function(item) { return !!item.cover; }).slice(0,3).map(function(item) { return { id:item.id, title:item.title || '我们的相册', cover:getImageUrl(item.cover) }; }),
upcomingMemory: next ? { title: next.title || '下一个记忆日', date: next.event_date, dateMonth: next.event_date.slice(5,7), dateDay: next.event_date.slice(8,10), days: Math.ceil((new Date(next.event_date.replace(/-/g, '/')) - today) / 86400000) } : null, upcomingMemory: next ? { title: next.title || '下一个记忆日', date: next.event_date, dateMonth: next.event_date.slice(5,7), dateDay: next.event_date.slice(8,10), days: Math.ceil((new Date(next.event_date.replace(/-/g, '/')) - today) / 86400000) } : null,
stats: { stats: {
timeline: Array.isArray(timeline) ? timeline.length : 0, timeline: totalOf(timeline),
album: photoCount, album: photoCount,
diary: (diary && diary.total) ? diary.total : 0, diary: (diary && diary.total) ? diary.total : 0,
songs: Array.isArray(songs) ? songs.length : 0, songs: totalOf(songs),
checkin: (checkin && checkin.checkin_log_total !== undefined) ? checkin.checkin_log_total : ((checkin && checkin.items) ? checkin.items.length : (Array.isArray(checkin) ? checkin.length : 0)), checkin: (checkin && checkin.checkin_log_total !== undefined) ? checkin.checkin_log_total : ((checkin && checkin.items) ? checkin.items.length : (Array.isArray(checkin) ? checkin.length : 0)),
pet: petLevel pet: petLevel
} }
+11 -11
View File
@@ -1,8 +1,9 @@
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout'); const { getLayout } = require('../../utils/layout');
// pages/lovewords/lovewords.js - 一起听过的歌 // pages/lovewords/lovewords.js - 一起听过的歌
const { api } = require('../../utils/request'); const { api } = require('../../utils/request');
Page(withDataCache({ Page(withDataCache(withListPagination({
onResize: function() { this.setData(getLayout()); }, onResize: function() { this.setData(getLayout()); },
data: { loading: true, loadError: false, data: { loading: true, loadError: false,
list: [], list: [],
@@ -30,13 +31,12 @@ Page(withDataCache({
return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' }; return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' };
}, },
loadData: async function() { loadData: async function(page) {
if (this._pageLoading) return; const ticket = startList(this, page);
this._pageLoading = true; if (!ticket) return;
this.setData({loading: !this._hasCachedData, loadError:false});
try { try {
const list = await api.songsList(); const list = await api.songsList({page:ticket.page,page_size:20});
const items = (list || []).map(function(item) { const items = itemsOf(list).map(function(item) {
if (item.created_at) { if (item.created_at) {
var d = new Date(item.created_at); var d = new Date(item.created_at);
item.date_month = String(d.getMonth() + 1).padStart(2, '0'); item.date_month = String(d.getMonth() + 1).padStart(2, '0');
@@ -44,13 +44,13 @@ Page(withDataCache({
} }
return item; return item;
}); });
this.setData({ list: items }); applyList(this, ticket, list, items, 'list');
} catch (e) { } catch (e) {
this.setData({loadError:true}); failList(this,ticket);
console.error(e); console.error(e);
wx.showToast({ title: '加载失败', icon: 'none' }); wx.showToast({ title: '加载失败', icon: 'none' });
} }
finally { this._pageLoading = false; this.setData({loading:false}); } finally { finishList(this,ticket); }
}, },
noop: function() {}, noop: function() {},
@@ -124,4 +124,4 @@ Page(withDataCache({
goBack: function() { wx.navigateBack(); }, goBack: function() { wx.navigateBack(); },
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
}, {"loadData": ["songsList"]})); }, 'loadData'), {"loadData": ["songsList"]}));
+2 -1
View File
@@ -5,7 +5,7 @@
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}"> <scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
<view class="c-body"><view class="c-heading j-row"><view><text class="j-eyebrow">OUR SOUNDTRACK</text><text class="j-page-title">一起听过的旋律</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 添加</button></view> <view class="c-body"><view class="c-heading j-row"><view><text class="j-eyebrow">OUR SOUNDTRACK</text><text class="j-page-title">一起听过的旋律</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 添加</button></view>
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" /> <page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
<view class="song-list" wx:if="{{!loading && !loadError && list.length}}"> <view class="song-list" wx:if="{{!loading && !loadError && list.length}}">
@@ -21,6 +21,7 @@
</view> </view>
<page-state wx:if="{{!loading && !loadError && !list.length}}" title="收藏我们的第一首歌" description="让某一段旋律,成为两个人的记忆" action="添加歌曲" bind:action="goToPublish" /> <page-state wx:if="{{!loading && !loadError && !list.length}}" title="收藏我们的第一首歌" description="让某一段旋律,成为两个人的记忆" action="添加歌曲" bind:action="goToPublish" />
</view> </view>
<view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部</text></view>
</scroll-view> </scroll-view>
<!-- 添加弹窗 --> <!-- 添加弹窗 -->
+3 -2
View File
@@ -1,3 +1,4 @@
const { itemsOf, totalOf } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout'); const { getLayout } = require('../../utils/layout');
// pages/profile/profile.js - 与原网站一致的个人中心 // pages/profile/profile.js - 与原网站一致的个人中心
@@ -1025,11 +1026,11 @@ Page(withDataCache({
const key=keys[index]; const key=keys[index];
if(result.status !== 'fulfilled') { counts[key]='—';failed=true;return; } if(result.status !== 'fulfilled') { counts[key]='—';failed=true;return; }
const value=result.value; const value=result.value;
if(key==='album') counts[key]=(Array.isArray(value)?value:[]).reduce((sum,album)=>sum+(Number(album.photo_count)||0),0); if(key==='album') counts[key]=value && value.photo_total != null ? Number(value.photo_total) : itemsOf(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==='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==='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 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; else counts[key]=totalOf(value);
}); });
this.setData({statisticItems:this.data.statisticItems.map(item=>({...item,count:counts[item.key]===undefined?item.count:counts[item.key]})),statsLoading:false,statsError:failed}); this.setData({statisticItems:this.data.statisticItems.map(item=>({...item,count:counts[item.key]===undefined?item.count:counts[item.key]})),statsLoading:false,statsError:failed});
}, },
+12 -11
View File
@@ -1,8 +1,9 @@
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout'); const { getLayout } = require('../../utils/layout');
// pages/punch/punch.js - 情侣打卡 // pages/punch/punch.js - 情侣打卡
const { api, getImageUrl } = require('../../utils/request'); const { api, getImageUrl } = require('../../utils/request');
Page(withDataCache({ Page(withDataCache(withListPagination({
onResize: function() { this.setData(getLayout()); }, onResize: function() { this.setData(getLayout()); },
data: { loadError: false, data: { loadError: false,
checkins: [], checkins: [],
@@ -54,16 +55,15 @@ Page(withDataCache({
}); });
}, },
loadData: async function() { loadData: async function(page) {
if (this._pageLoading) return; const ticket = startList(this, page);
this._pageLoading = true; if (!ticket) return;
this.setData({loading: !this._hasCachedData, loadError:false});
try { try {
const [res, stats] = await Promise.all([ const [res, stats] = await Promise.all([
api.checkinList(), api.checkinList({page:ticket.page,page_size:20}),
api.checkinStats().catch(() => ({})) api.checkinStats().catch(() => ({}))
]); ]);
const list = (res.items || res || []).map(function(item) { const list = itemsOf(res).map(function(item) {
item.type = item.type || 'both'; item.type = item.type || 'both';
var typeLabels = { both: '双方', first: '第一视角', her: '她打卡' }; var typeLabels = { both: '双方', first: '第一视角', her: '她打卡' };
item.type_label = typeLabels[item.type] || '双方'; item.type_label = typeLabels[item.type] || '双方';
@@ -89,10 +89,11 @@ Page(withDataCache({
todayPartner: stats.today_partner_count || 0, todayPartner: stats.today_partner_count || 0,
loading: false loading: false
}; };
this.setData(data); delete data.checkins;
applyList(this, ticket, res, list, 'checkins', data);
} catch(e) { } catch(e) {
this.setData({loadError:true}); console.error(e); this.setData({ loading: false }); } failList(this,ticket); console.error(e); this.setData({ loading: false }); }
finally { this._pageLoading = false; this.setData({loading:false}); } finally { finishList(this,ticket); }
}, },
// ===== 点击卡片 → 详情弹窗 ===== // ===== 点击卡片 → 详情弹窗 =====
@@ -119,4 +120,4 @@ Page(withDataCache({
goBack: function() { wx.navigateBack(); }, goBack: function() { wx.navigateBack(); },
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
}, {"loadData": ["checkinList", "checkinStats", "coupleInfo"]})); }, 'loadData'), {"loadData": ["checkinList", "checkinStats", "coupleInfo"]}));
+2 -1
View File
@@ -3,7 +3,7 @@
<!-- pages/punch/punch.wxml - 情侣打卡 --> <!-- pages/punch/punch.wxml - 情侣打卡 -->
<app-nav title="打卡" back="{{true}}" /> <app-nav title="打卡" back="{{true}}" />
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}"> <scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
<view class="c-body"><view class="c-heading j-row"><view><text class="j-eyebrow">LITTLE PROMISES</text><text class="j-page-title">把约定变成习惯</text></view><button class="j-button j-button-small" bindtap="showAddModal">+ 添加</button></view> <view class="c-body"><view class="c-heading j-row"><view><text class="j-eyebrow">LITTLE PROMISES</text><text class="j-page-title">把约定变成习惯</text></view><button class="j-button j-button-small" bindtap="showAddModal">+ 添加</button></view>
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" /> <page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
<!-- 顶部 Banner - 卡片式 --> <!-- 顶部 Banner - 卡片式 -->
@@ -83,6 +83,7 @@
<page-state wx:if="{{!loading && !loadError && !checkins.length}}" title="从一个小约定开始" description="一起运动、按时吃饭,也可以成为日常仪式" action="添加打卡" bind:action="showAddModal" /> <page-state wx:if="{{!loading && !loadError && !checkins.length}}" title="从一个小约定开始" description="一起运动、按时吃饭,也可以成为日常仪式" action="添加打卡" bind:action="showAddModal" />
</view> </view>
<view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部</text></view>
</scroll-view> </scroll-view>
<!-- 浮动按钮 --> <!-- 浮动按钮 -->
@@ -3,5 +3,6 @@ Page({
data:{}, data:{},
onLoad(){this.setData(getLayout());}, onLoad(){this.setData(getLayout());},
onResize(){this.setData(getLayout());}, onResize(){this.setData(getLayout());},
loadMore(){const card=this.selectComponent('#smoking');if(card)return card.loadMoreLogs();},
async onPullDownRefresh(){try{const card=this.selectComponent('#smoking');if(card)await card.load(true);}finally{wx.stopPullDownRefresh();}} async onPullDownRefresh(){try{const card=this.selectComponent('#smoking');if(card)await card.load(true);}finally{wx.stopPullDownRefresh();}}
}); });
@@ -1 +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> <view class="app-shell app-shell--secondary" style="{{layoutStyle}}"><launch-transition /><app-nav title="抽烟记录" back="{{true}}" /><scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}"><view class="c-body"><smoking-card id="smoking" detailed="{{true}}" /></view></scroll-view></view>
+12 -12
View File
@@ -1,3 +1,4 @@
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
const { withDataCache } = require('../../utils/page-cache'); const { withDataCache } = require('../../utils/page-cache');
const { getLayout } = require('../../utils/layout'); const { getLayout } = require('../../utils/layout');
// pages/timeline/timeline.js // pages/timeline/timeline.js
@@ -56,7 +57,7 @@ function calcNearestRepeatDate(eventDate, today, interval, unit) {
return new Date(eventDate); return new Date(eventDate);
} }
Page(withDataCache({ Page(withDataCache(withListPagination({
onResize: function() { this.setData(getLayout()); }, onResize: function() { this.setData(getLayout()); },
data: { loading: true, loadError: false, data: { loading: true, loadError: false,
list: [] list: []
@@ -82,12 +83,11 @@ Page(withDataCache({
return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' }; return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' };
}, },
loadData: async function() { loadData: async function(page) {
if (this._pageLoading) return; const ticket = startList(this, page);
this._pageLoading = true; if (!ticket) return;
this.setData({loading: !this._hasCachedData, loadError:false});
try { try {
const list = await api.timelineList(); const list = await api.timelineList({page:ticket.page,page_size:20});
const myUid = wx.getStorageSync('userId'); const myUid = wx.getStorageSync('userId');
var nameMap = {}; var nameMap = {};
var avatarMap = {}; var avatarMap = {};
@@ -100,12 +100,12 @@ Page(withDataCache({
} catch (e) {} } catch (e) {}
var today = new Date(); today.setHours(0,0,0,0); var today = new Date(); today.setHours(0,0,0,0);
var items = (list || []).map(function(item, idx) { var items = itemsOf(list).map(function(item, idx) {
var cid = parseInt(item.created_by) || 0; var cid = parseInt(item.created_by) || 0;
item.creator_name = nameMap[cid] || (cid == myUid ? '我' : 'TA'); item.creator_name = nameMap[cid] || (cid == myUid ? '我' : 'TA');
item.creator_avatar = avatarMap[cid] || ''; item.creator_avatar = avatarMap[cid] || '';
item.creator_initial = (item.creator_name || '?')[0].toUpperCase(); item.creator_initial = (item.creator_name || '?')[0].toUpperCase();
item.side = idx % 2 === 0 ? 'left' : 'right'; item.side = ((ticket.page-1)*20+idx) % 2 === 0 ? 'left' : 'right';
item.bg_image_url = item.bg_image ? getImageUrl(item.bg_image) : ''; item.bg_image_url = item.bg_image ? getImageUrl(item.bg_image) : '';
// 计算纪念日/倒数日(支持重复事件) // 计算纪念日/倒数日(支持重复事件)
@@ -150,13 +150,13 @@ Page(withDataCache({
} }
return item; return item;
}); });
this.setData({ list: items }); applyList(this, ticket, list, items, 'list');
} catch (e) { } catch (e) {
this.setData({loadError:true}); failList(this,ticket);
console.error(e); console.error(e);
wx.showToast({ title: (e && (e.error || e.errMsg || e.message)) || '加载失败', icon: 'none' }); wx.showToast({ title: (e && (e.error || e.errMsg || e.message)) || '加载失败', icon: 'none' });
} }
finally { this._pageLoading = false; this.setData({loading:false}); } finally { finishList(this,ticket); }
}, },
@@ -192,4 +192,4 @@ Page(withDataCache({
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
goBack: function() { wx.navigateBack(); }, goBack: function() { wx.navigateBack(); },
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
}, {"loadData": ["timelineList", "coupleInfo"]})); }, 'loadData'), {"loadData": ["timelineList", "coupleInfo"]}));
+2 -1
View File
@@ -6,7 +6,7 @@
<!-- 添加按钮 --> <!-- 添加按钮 -->
<scroll-view class="page-bg tl-scroll" scroll-y show-scrollbar="{{false}}" bounces="{{true}}"> <scroll-view class="page-bg tl-scroll" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}" bounces="{{true}}">
<view class="container"><view class="j-row"><view><text class="j-eyebrow">MEMORIES OF US</text><text class="j-page-title">值得记住的日子</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 记忆日</button></view> <view class="container"><view class="j-row"><view><text class="j-eyebrow">MEMORIES OF US</text><text class="j-page-title">值得记住的日子</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 记忆日</button></view>
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" /> <page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
<view class="tl-list" wx:if="{{!loading && !loadError && list.length > 0}}"> <view class="tl-list" wx:if="{{!loading && !loadError && list.length > 0}}">
@@ -48,6 +48,7 @@
</view> </view>
</view> </view>
<view class="j-pagination" wx:if="{{!loading && !loadError && total > 0}}"><text wx:if="{{loadingMore}}">正在加载…</text><text wx:elif="{{moreError}}" bindtap="loadMore">加载失败,点击重试</text><text wx:elif="{{hasMore}}" bindtap="loadMore">加载更多</text><text wx:else>已加载全部</text></view>
</scroll-view> </scroll-view>
+2
View File
@@ -119,3 +119,5 @@ scroll-view.j-main-scroll { position:fixed; top:var(--nav-height,64px); left:0;
} }
.j-handwritten-page .j-link { font-size:26rpx; } .j-handwritten-page .j-link { font-size:26rpx; }
.j-handwritten-page .j-profile-setting .j-link { font-size:32rpx; } .j-handwritten-page .j-profile-setting .j-link { font-size:32rpx; }
.j-pagination { padding:28rpx 0 40rpx; text-align:center; color:#85776D; font-size:23rpx; }
+1
View File
@@ -15,6 +15,7 @@ function records(api) {
Page:p=>definition=p, console:{error(){}}, setTimeout:f=>f(), Page:p=>definition=p, console:{error(){}}, setTimeout:f=>f(),
wx:{getStorageSync:()=> 'token',navigateTo:p=>routes.push(p.url)}, wx:{getStorageSync:()=> 'token',navigateTo:p=>routes.push(p.url)},
require(name) { require(name) {
if(name.endsWith('/list-pagination'))return require('../utils/list-pagination');
if(name.endsWith('/page-cache'))return {withDataCache:p=>p}; if(name.endsWith('/page-cache'))return {withDataCache:p=>p};
if(name.endsWith('/request'))return {api}; if(name.endsWith('/request'))return {api};
if(name.endsWith('/journal'))return formatter(); if(name.endsWith('/journal'))return formatter();
+39
View File
@@ -0,0 +1,39 @@
const test=require('node:test');
const assert=require('node:assert/strict');
const path=require('node:path');
const helpers=require('../utils/list-pagination');
const envelope=(items,total=42,has_more=true)=>({items,total,has_more});
function page(file,api){
let definition;
const load=require('./load-module.cjs')({Page:p=>definition=p,wx:{getStorageSync:()=>1,showToast(){}},console,Date,Promise,setTimeout},{'../../utils/request':{api,getImageUrl:v=>v},'../../utils/page-cache':{withDataCache:p=>p}});
load(path.join(__dirname,'../pages',file));
return {...definition,data:{...definition.data},setData(values){Object.assign(this.data,values);}};
}
test('songs append once, preserve full total, and retry the failed page',async()=>{
let fail=true;const calls=[];
const p=page('lovewords/lovewords.js',{songsList:async({page,page_size})=>{calls.push(page);assert.equal(page_size,20);if(page===1)return envelope([{id:1}]);if(fail)throw Error('offline');return envelope([{id:1},{id:2}],42,false);}});
await p.loadData();assert.equal(p.data.total,42);
const originalError=console.error;console.error=()=>{};
try {await p.loadMore();}finally{console.error=originalError;}
assert.equal(p.data.page,1);assert.equal(p.data.list.length,1);assert.equal(p.data.moreError,true);
fail=false;await p.loadMore();assert.deepEqual(calls,[1,2,2]);assert.equal(p.data.list.length,2);assert.equal(p.data.hasMore,false);
await p.loadMore();assert.equal(calls.length,3);
});
test('refresh invalidates an in-flight next page',()=>{
const p={data:{list:[]},setData(v){Object.assign(this.data,v);}};
let t=helpers.startList(p,1);helpers.applyList(p,t,envelope([{id:1}]),[{id:1}],'list');helpers.finishList(p,t);
const old=helpers.startList(p,2);t=helpers.startList(p,1);
helpers.applyList(p,t,envelope([{id:9}]),[{id:9}],'list');helpers.finishList(p,t);
helpers.applyList(p,old,envelope([{id:2}]),[{id:2}],'list');helpers.finishList(p,old);
assert.deepEqual(p.data.list,[{id:9}]);assert.equal(p.data.page,1);
});
test('album list appends the next page instead of loading all albums',async()=>{
const calls=[];const p=page('albums/albums.js',{albumList:async({page,page_size})=>{calls.push([page,page_size]);return envelope([{id:page,title:'album'}],21,page===1);}});
await p.loadAlbums();await p.loadMore();assert.deepEqual(calls,[[1,20],[2,20]]);assert.equal(p.data.albums.length,2);assert.equal(p.data.total,21);
});
test('photos crossing page boundaries stay in the same upload card and retain saved metadata',async()=>{
const calls=[];const p=page('album/album.js',{photos:async(id,{page,page_size})=>{calls.push([id,page,page_size]);return envelope([{id:page,batch_id:'same',created_by:1,path:page+'.jpg',created_at:'2026-10-11',location:page===1?'old':'second'}],2,page===1);},coupleInfo:async()=>({partner1:{id:1,nickname:'我'},partner2:{id:2,nickname:'TA'}}),photoUpdate:async()=>({success:true})});
p.data.albumId=8;await p.loadData();
p.data.editPhotos=p.data.batches[0].photos;p.data.editForm={taken_at:'',location:'saved',activity:''};await p.savePhotoInfo();
await p.loadMore();assert.deepEqual(calls,[[8,1,20],[8,2,20]]);assert.equal(p.data.batches.length,1);assert.equal(p.data.batches[0].photos.length,2);assert.equal(p.data.batches[0].photos[0].location,'saved');assert.equal(p.data.hasMore,false);
});
+2 -2
View File
@@ -38,7 +38,7 @@ test('failed homepage load ends the recent-record spinner',async()=>{
let definition; let definition;
const api={coupleInfo:async()=>{throw { _statusCode:404 };}}; const api={coupleInfo:async()=>{throw { _statusCode:404 };}};
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{ vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{
Page(p){definition=p;},require(name){return name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:{saveScrollPosition(){},getScrollPosition(){return 0;}};}, Page(p){definition=p;},require(name){return name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:{saveScrollPosition(){},getScrollPosition(){return 0;}};},
wx:{getStorageSync:()=> 'test-token'}, console:{error(){}}, Date, setTimeout wx:{getStorageSync:()=> 'test-token'}, console:{error(){}}, Date, setTimeout
}); });
const page={...definition,data:{...definition.data},setData(data){Object.assign(this.data,data);}}; const page={...definition,data:{...definition.data},setData(data){Object.assign(this.data,data);}};
@@ -50,7 +50,7 @@ test('unbound newcomer loads homepage records without requesting couple-only pet
let definition;let petCalls=0; let definition;let petCalls=0;
const api={coupleInfo:async()=>({not_bound:true,partner1:{nickname:'新用户'}}),timelineList:async()=>[],albumList:async()=>[],diaryList:async()=>({items:[],total:0}),songsList:async()=>[],checkinList:async()=>({items:[]}),petInfo:async()=>{petCalls++;throw Error('请先绑定情侣');}}; const api={coupleInfo:async()=>({not_bound:true,partner1:{nickname:'新用户'}}),timelineList:async()=>[],albumList:async()=>[],diaryList:async()=>({items:[],total:0}),songsList:async()=>[],checkinList:async()=>({items:[]}),petInfo:async()=>{petCalls++;throw Error('请先绑定情侣');}};
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{ vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{
Page(p){definition=p;},require(name){return name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:name.endsWith('/journal')?{diaryEntry:v=>v}:{};}, Page(p){definition=p;},require(name){return name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:name.endsWith('/journal')?{diaryEntry:v=>v}:{};},
wx:{getStorageSync:()=>1,removeStorageSync(){}},console,Date,setTimeout wx:{getStorageSync:()=>1,removeStorageSync(){}},console,Date,setTimeout
}); });
const page={...definition,data:{...definition.data,coupleId:99},setData(data){Object.assign(this.data,data);}}; const page={...definition,data:{...definition.data,coupleId:99},setData(data){Object.assign(this.data,data);}};
+29
View File
@@ -0,0 +1,29 @@
function itemsOf(result) { return Array.isArray(result) ? result : (result && result.items) || []; }
function totalOf(result) { return result && result.total != null ? Number(result.total) : itemsOf(result).length; }
function startList(page, number) {
number = Number(number) || 1;
if (page._paginationFirst || (number > 1 && (page.data.loadingMore || page.data.loading || page.data.loadError || !page.data.hasMore))) return null;
if (number === 1) { page._paginationFirst = true; page._paginationGeneration = (page._paginationGeneration || 0) + 1; }
const ticket = { page: number, generation: page._paginationGeneration };
page.setData(number === 1 ? {loading: !page._hasCachedData, loadError:false, loadingMore:false, moreError:false} : {loadingMore:true, moreError:false});
return ticket;
}
function current(page, ticket) { return ticket.generation === page._paginationGeneration; }
function mergeUnique(previous, added) {
const seen=new Set();return previous.concat(added).filter(item=>{const id=String(item.id);if(seen.has(id))return false;seen.add(id);return true;});
}
function applyList(page, ticket, result, items, field, extra) {
if (!current(page,ticket)) return;
const list=ticket.page===1?items:mergeUnique(page.data[field]||[],items);
const total=totalOf(result);
page.setData(Object.assign({[field]:list,page:ticket.page,total,hasMore:!Array.isArray(result)&&(result.has_more != null ? !!result.has_more : ticket.page*20<total),moreError:false},extra));
}
function failList(page,ticket) { if(current(page,ticket))page.setData(ticket.page===1?{loadError:true}:{moreError:true}); }
function finishList(page,ticket) { if(current(page,ticket)){if(ticket.page===1)page._paginationFirst=false;page.setData({loading:false,loadingMore:false});} }
function withListPagination(definition, loader) {
Object.assign(definition.data,{page:1,total:0,hasMore:false,loadingMore:false,moreError:false});
const load=definition[loader];
definition.loadMore=function(){return load.call(this,(this.data.page||1)+1);};
return definition;
}
module.exports={itemsOf,totalOf,startList,current,mergeUnique,applyList,failList,finishList,withListPagination};
+7 -7
View File
@@ -263,21 +263,21 @@ const api = {
updatePartnerProfile: (data) => request('updatePartnerProfile', data), updatePartnerProfile: (data) => request('updatePartnerProfile', data),
// 时间线 // 时间线
timelineList: () => request('timelineList', null, 'GET'), timelineList: (data = {}) => request('timelineList', { page: 1, page_size: 20, ...data }, 'GET'),
timelineAdd: (data) => request('timelineAdd', data), timelineAdd: (data) => request('timelineAdd', data),
timelineUpdate: (data) => request('timelineUpdate', data), timelineUpdate: (data) => request('timelineUpdate', data),
timelineDelete: (data) => request('timelineDelete', data), timelineDelete: (data) => request('timelineDelete', data),
timelineDetail: (id) => request('timelineDetail', { id: id }, 'GET'), timelineDetail: (id) => request('timelineDetail', { id: id }, 'GET'),
// 相册 // 相册
albumList: (data, method) => request('albumList', data, method || 'GET'), albumList: (data, method) => request('albumList', { page: 1, page_size: 20, ...data }, method || 'GET'),
albumAdd: (data) => request('albumAdd', data), albumAdd: (data) => request('albumAdd', data),
albumUpdate: (data) => request('albumUpdate', data), albumUpdate: (data) => request('albumUpdate', data),
albumDelete: (data) => request('albumDelete', data), albumDelete: (data) => request('albumDelete', data),
photoDelete: (data) => request('photoDelete', data), photoDelete: (data) => request('photoDelete', data),
photoAdd: (data) => request('photoAdd', data), photoAdd: (data) => request('photoAdd', data),
photoUpdate: (data) => request('photoUpdate', data), photoUpdate: (data) => request('photoUpdate', data),
photos: (albumId, data, method) => request('photos', { album_id: albumId, ...data }, method || 'GET'), photos: (albumId, data, method) => request('photos', { page: 1, page_size: 20, album_id: albumId, ...data }, method || 'GET'),
photoUpload: async (filePath, albumId) => { photoUpload: async (filePath, albumId) => {
wx.showLoading({ title: '上传中...' }); wx.showLoading({ title: '上传中...' });
try { try {
@@ -351,21 +351,21 @@ const api = {
diaryUpdate: (data) => request('diaryUpdate', data), diaryUpdate: (data) => request('diaryUpdate', data),
diaryDelete: (data) => request('diaryDelete', data), diaryDelete: (data) => request('diaryDelete', data),
diaryDetail: (id) => request('diaryDetail', { id: id }, 'GET'), diaryDetail: (id) => request('diaryDetail', { id: id }, 'GET'),
diaryCommentList: (data) => request('diaryCommentList', data, 'GET'), diaryCommentList: (data) => request('diaryCommentList', { page: 1, page_size: 20, ...data }, 'GET'),
diaryCommentAdd: (data) => request('diaryCommentAdd', data), diaryCommentAdd: (data) => request('diaryCommentAdd', data),
diaryCommentDelete: (data) => request('diaryCommentDelete', data), diaryCommentDelete: (data) => request('diaryCommentDelete', data),
// 点赞/收藏功能已移除 // 点赞/收藏功能已移除
// 一起听过的歌 // 一起听过的歌
songsList: () => request('songsList', null, 'GET'), songsList: (data = {}) => request('songsList', { page: 1, page_size: 20, ...data }, 'GET'),
songsAdd: (data) => request('songsAdd', data), songsAdd: (data) => request('songsAdd', data),
songsUpdate: (data) => request('songsUpdate', data), songsUpdate: (data) => request('songsUpdate', data),
songsDelete: (data) => request('songsDelete', data), songsDelete: (data) => request('songsDelete', data),
// 打卡 // 打卡
smokingInfo: (userId) => request("smokingInfo", { user_id: userId || 0 }, "GET"), smokingInfo: (userId, data = {}) => request("smokingInfo", { page: 1, page_size: 20, ...data, user_id: userId || 0 }, "GET"),
smokingAction: (data) => request("smokingAction", data), smokingAction: (data) => request("smokingAction", data),
checkinList: () => request('checkinList', null, 'GET'), checkinList: (data = {}) => request('checkinList', { page: 1, page_size: 20, ...data }, 'GET'),
checkinAdd: (data) => request('checkinAdd', data), checkinAdd: (data) => request('checkinAdd', data),
checkinDo: (data) => request('checkinDo', data), checkinDo: (data) => request('checkinDo', data),
checkinStats: () => request('checkinStats', null, 'GET'), checkinStats: () => request('checkinStats', null, 'GET'),