Add server and mini-program pagination for shared lists
This commit is contained in:
@@ -37,9 +37,14 @@ func coupleSpaceID(c *httpapi.Ctx, cid int64) string {
|
||||
func albumList(c *httpapi.Ctx) error {
|
||||
ids := coupleUserIds(c, 0)
|
||||
if len(ids) == 0 {
|
||||
return c.OK(demoData("album"))
|
||||
photoTotal := int64(0)
|
||||
for _, album := range demoData("album") {
|
||||
photoTotal += store.ToInt(album["photo_count"])
|
||||
}
|
||||
return demoListResponse(c, "album", M{"photo_total": photoTotal})
|
||||
}
|
||||
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])
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -48,7 +53,9 @@ func albumList(c *httpapi.Ctx) error {
|
||||
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 {
|
||||
@@ -185,13 +192,14 @@ func photos(c *httpapi.Ctx) error {
|
||||
}
|
||||
ids := coupleUserIds(c, 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 {
|
||||
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 相册第一张照片时自动设为封面
|
||||
|
||||
@@ -156,10 +156,14 @@ func decorateCheckin(item store.Row, uid int64, withAvatar bool) {
|
||||
func checkinList(c *httpapi.Ctx) error {
|
||||
ids := coupleUserIds(c, 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})
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -171,6 +175,9 @@ func checkinList(c *httpapi.Ctx) error {
|
||||
if cid := c.CoupleID(); cid != 0 {
|
||||
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})
|
||||
}
|
||||
|
||||
|
||||
@@ -251,14 +251,15 @@ func diaryCommentList(c *httpapi.Ctx) error {
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
for _, cm := range comments {
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func smokingDelta(kind string, quantity, stock int64) (int64, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -97,11 +97,15 @@ func smokingSnapshot(c *httpapi.Ctx, uid int64) (M, error) {
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -35,7 +35,8 @@ func TestSmokingSnapshotWeeklyMonthlyStats(t *testing.T) {
|
||||
}
|
||||
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"}}},
|
||||
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"}},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@ func registerSongs(r *httpapi.Router) {
|
||||
func songsList(c *httpapi.Ctx) error {
|
||||
ids := coupleUserIds(c, 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 {
|
||||
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 {
|
||||
|
||||
@@ -44,16 +44,22 @@ func checkTextOr400(c *httpapi.Ctx, text string) error {
|
||||
func timelineList(c *httpapi.Ctx) error {
|
||||
ids := coupleUserIds(c, 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 {
|
||||
return err
|
||||
}
|
||||
for _, it := range items {
|
||||
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 {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const { mergeUnique } = require('../../utils/list-pagination');
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const cache = require('../../utils/data-cache');
|
||||
const titles = {consume:'抽一支 · 打卡',purchase:'买烟',receive:'别人递烟',receive_smoke:'收到并抽一支',adjust:'调整库存',give:'递给别人一支',undo:'撤销记录'};
|
||||
Component({
|
||||
properties: { detailed: Boolean },
|
||||
data: { loading:true, error:false, info:null, saving:false, sheet:false, mode:'purchase', quantity:'1', packSize:'20', amount:'', note:'', viewUserId:0, lastLabel:'还没有记录', daily:[], logs:[], periodStats:[], ownerName:'', ownerAvatar:'', ownerInitial:'我' },
|
||||
data: { loading:true, error:false, info:null, saving:false, sheet:false, mode:'purchase', quantity:'1', packSize:'20', amount:'', note:'', viewUserId:0, lastLabel:'还没有记录', daily:[], logs:[], periodStats:[], logsPage:1, logsTotal:0, logsHasMore:false, logsLoadingMore:false, logsMoreError:false, ownerName:'', ownerAvatar:'', ownerInitial:'我' },
|
||||
lifetimes: { attached(){this.load();} },
|
||||
pageLifetimes: { show(){this.load();} },
|
||||
methods: {
|
||||
@@ -16,6 +17,7 @@ Component({
|
||||
return this._loading;
|
||||
},
|
||||
applyInfo(info) {
|
||||
this._logsGeneration = (this._logsGeneration || 0) + 1;
|
||||
const logs=(info.logs||[]).map(log=>({...log,undone:log.undone === true || Number(log.undone) === 1,title:titles[log.kind]||log.kind,change:Number(log.delta)>0?'+'+log.delta:String(log.delta),time:String(log.created_at||'').slice(5,16),money:Number(log.amount_cents)>0?(Number(log.amount_cents)/100).toFixed(2):''}));
|
||||
let lastLabel='还没有记录';
|
||||
if(info.last_time){const minutes=Math.max(0,Math.floor((Date.now()-new Date(String(info.last_time).replace(/-/g,'/')).getTime())/60000));lastLabel=minutes<60?'距上次 '+minutes+' 分钟':minutes<1440?'距上次 '+Math.floor(minutes/60)+' 小时':'距上次 '+Math.floor(minutes/1440)+' 天';}
|
||||
@@ -32,12 +34,23 @@ Component({
|
||||
const owner=[profile.partner1,profile.partner2].find(user=>user && Number(user.id)===ownerId)||{};
|
||||
const ownerName=owner.nickname||info.owner_name||owner.username||(info.read_only?'TA':'我');
|
||||
const ownerAvatar=getImageUrl(owner.avatar||info.owner_avatar||'');
|
||||
this.setData({info,logs,daily,periodStats,lastLabel,ownerName,ownerAvatar,ownerInitial:ownerName.slice(0,1),error:false,loading:false});
|
||||
this.setData({info,logs,daily,periodStats,logsPage:Number(info.logs_page)||1,logsTotal:Number(info.logs_total)||logs.length,logsHasMore:info.logs_has_more===true,logsLoadingMore:false,logsMoreError:false,lastLabel,ownerName,ownerAvatar,ownerInitial:ownerName.slice(0,1),error:false,loading:false});
|
||||
},
|
||||
async loadMoreLogs(){
|
||||
if(this._loading || this.data.loading || this.data.saving || this.data.logsLoadingMore || !this.data.logsHasMore)return;
|
||||
const generation=this._logsGeneration,target=this.data.viewUserId,page=this.data.logsPage+1;
|
||||
this.setData({logsLoadingMore:true,logsMoreError:false});
|
||||
try{
|
||||
const info=await api.smokingInfo(target,{page,page_size:20});
|
||||
if(generation!==this._logsGeneration || target!==this.data.viewUserId)return;
|
||||
this.applyInfo({...info,logs:mergeUnique(this.data.info.logs||[],info.logs||[])});
|
||||
}catch(e){if(generation===this._logsGeneration)this.setData({logsMoreError:true});}
|
||||
finally{if(generation===this._logsGeneration)this.setData({logsLoadingMore:false});}
|
||||
},
|
||||
avatarFailed(){this.setData({ownerAvatar:''});},
|
||||
retry(){this.load(true);},
|
||||
openDetail(){if(!this.properties.detailed)wx.navigateTo({url:'/pages/smoking-detail/smoking-detail'});},
|
||||
async switchOwner(e){if(this.data.saving)return;this.setData({viewUserId:Number(e.currentTarget.dataset.user)||0,loading:true,info:null});if(this._loading)await this._loading;await this.load();},
|
||||
async switchOwner(e){if(this.data.saving)return;this._logsGeneration=(this._logsGeneration||0)+1;this.setData({viewUserId:Number(e.currentTarget.dataset.user)||0,loading:true,info:null});if(this._loading)await this._loading;await this.load();},
|
||||
replenish(){if(this.data.saving)return;this.setData({sheet:true,mode:'purchase',quantity:'1',packSize:'20',amount:'',note:''});},
|
||||
mode(e){if(this.data.saving)return;const mode=e.currentTarget.dataset.mode;this.setData({mode,quantity:mode==='adjust'?String(this.data.info.stock):'1',amount:'',note:''});},
|
||||
input(e){if(!this.data.saving)this.setData({[e.currentTarget.dataset.field]:e.detail.value});},
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
<text class="smoke-period-hint">购买金额按已填写金额累计,已撤销记录不计入</text>
|
||||
<view class="section-title">最近七天<text>累计 {{info.total_count}} 支</text></view>
|
||||
<view class="week-chart"><view class="chart-day" wx:for="{{daily}}" wx:key="day"><text>{{item.count}}</text><view class="chart-track"><view class="chart-bar" style="height:{{item.height}}%" /></view><text>{{item.label}}</text></view></view>
|
||||
<view class="section-title">库存与打卡流水<text>最近 50 条</text></view>
|
||||
<view class="section-title">库存与打卡流水<text>共 {{logsTotal}} 条</text></view>
|
||||
<view class="smoke-hint" wx:if="{{!logs.length}}">从补充库存开始,记录每一次消耗</view>
|
||||
<view class="ledger {{item.undone ? 'undone' : ''}}" wx:for="{{logs}}" wx:key="id"><view class="ledger-avatar"><image wx:if="{{ownerAvatar}}" src="{{ownerAvatar}}" mode="aspectFill" binderror="avatarFailed" /><text wx:else>{{ownerInitial}}</text></view><view class="ledger-main"><view class="ledger-owner">{{ownerName}}</view><text>{{item.title}}{{item.undone ? ' · 已撤销' : ''}}</text><text>{{item.time}}{{item.money ? ' · ¥' + item.money : ''}}</text><text wx:if="{{item.note}}">{{item.note}}</text></view><view class="ledger-right"><text>{{item.change}} 支 · 余 {{item.balance}}</text><text wx:if="{{!info.read_only && !item.undone && item.kind !== 'undo'}}" data-id="{{item.id}}" bindtap="undo">撤销</text></view></view>
|
||||
<view class="smoke-hint smoke-ledger-footer" wx:if="{{logs.length}}"><text wx:if="{{logsLoadingMore}}">正在加载…</text><text wx:elif="{{logsMoreError}}" bindtap="loadMoreLogs">加载失败,点击重试</text><text wx:elif="{{logsHasMore}}" bindtap="loadMoreLogs">加载更多流水</text><text wx:else>已加载全部流水</text></view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -14,3 +14,5 @@
|
||||
@import '../../styles/typography.wxss';
|
||||
|
||||
.smoke-periods{display:flex;gap:16rpx}.smoke-period{flex:1;min-width:0;padding:22rpx 18rpx;border:1rpx solid #F1DCE4;border-radius:20rpx;background:#FFF5F8}.smoke-period-head{display:flex;align-items:center;justify-content:space-between;gap:8rpx;font-size:25rpx;color:#64424D}.smoke-period-head text+text{font-size:18rpx;color:#A39386}.smoke-period-row{display:flex;justify-content:space-between;gap:10rpx;font-size:23rpx;margin-top:18rpx;color:#85776D}.smoke-period-row text+text{color:#B74768;overflow-wrap:anywhere}.smoke-period-hint{display:block;margin-top:14rpx;font-size:20rpx;color:#A39386;line-height:1.6}
|
||||
|
||||
.smoke-ledger-footer{text-align:center;padding-top:20rpx}
|
||||
|
||||
+18
-14
@@ -1,3 +1,4 @@
|
||||
const { itemsOf, startList, current, mergeUnique, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { currentUserLocation } = require('../../utils/user-location');
|
||||
const { photoForm, metadataRows } = require('../../utils/photo-metadata');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
@@ -5,7 +6,7 @@ const { getLayout } = require('../../utils/layout');
|
||||
// pages/album/album.js - 日记式分组相册
|
||||
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
|
||||
const { requestSubscribeAfterAction } = require('../../utils/subscribe');
|
||||
Page(withDataCache({
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
batches: [],
|
||||
@@ -86,7 +87,8 @@ Page(withDataCache({
|
||||
return { ...batch, photos, metadata: metadataRows(photos) };
|
||||
});
|
||||
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' });
|
||||
} catch (error) {
|
||||
wx.showToast({ title: (error && (error.error || error.message)) || '保存失败,请重试', icon: 'none' });
|
||||
@@ -129,15 +131,14 @@ Page(withDataCache({
|
||||
};
|
||||
},
|
||||
|
||||
loadData: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
loadData: async function(page) {
|
||||
const ticket = startList(this,page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
var self = this;
|
||||
var albumId = this.data.albumId;
|
||||
if (!albumId) {
|
||||
var albums = await api.albumList();
|
||||
var albums = itemsOf(await api.albumList({page:1,page_size:1}));
|
||||
if (albums && albums.length > 0) {
|
||||
albumId = albums[0].id;
|
||||
this.setData({ albumId: albumId });
|
||||
@@ -145,11 +146,14 @@ Page(withDataCache({
|
||||
}
|
||||
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 分组
|
||||
var batchMap = {};
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var info = await api.coupleInfo();
|
||||
if (!current(this,ticket)) return;
|
||||
var p1 = info.partner1 || {}, p2 = info.partner2 || {};
|
||||
var nameMap = {};
|
||||
var avatarMap = {};
|
||||
@@ -182,13 +186,13 @@ Page(withDataCache({
|
||||
var batches = Object.values(batchMap);
|
||||
batches.sort(function(a, b) { return b.created_at > a.created_at ? 1 : -1; });
|
||||
batches.forEach(function(batch) { batch.metadata = metadataRows(batch.photos); });
|
||||
this.setData({ batches: batches });
|
||||
} else { this.setData({batches:[]}); }
|
||||
this._photoRows = photos;
|
||||
applyList(this,ticket,result,itemsOf(result),'photoItems',{batches:batches});
|
||||
} else { applyList(this,ticket,{items:[],total:0,has_more:false},[],'photoItems',{batches:[]}); }
|
||||
} catch (e) {
|
||||
this.setData({loadError:true}); console.error(e); }
|
||||
failList(this,ticket); console.error(e); }
|
||||
finally {
|
||||
this._pageLoading = false;
|
||||
this.setData({loading:false});
|
||||
finishList(this,ticket);
|
||||
if (this._visible && this._pendingPhotoPicker && !this.data.loadError && this.data.albumId) {
|
||||
this._pendingPhotoPicker = false;
|
||||
this.chooseImage();
|
||||
@@ -522,4 +526,4 @@ Page(withDataCache({
|
||||
goToAlbums: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["photos", "albumList"]}));
|
||||
}, 'loadData'), {"loadData": ["photos", "albumList"]}));
|
||||
|
||||
@@ -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>
|
||||
<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}}">
|
||||
@@ -36,6 +36,7 @@
|
||||
</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 class="preview-overlay" wx:if="{{showPreview}}" bindtap="closePreview">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/albums/albums.js
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page(withDataCache({
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
@@ -47,21 +48,21 @@ Page(withDataCache({
|
||||
|
||||
noop: function() {},
|
||||
|
||||
loadAlbums: async function() {
|
||||
if (this._loading) return;
|
||||
this._loading = true;
|
||||
this.setData({ loading: !this._hasCachedData, loadError: false });
|
||||
loadAlbums: async function(page) {
|
||||
const ticket = startList(this, page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
var list = await api.albumList(null, 'GET', true);
|
||||
list = (list || []).map(function(a) {
|
||||
var list = await api.albumList({page:ticket.page,page_size:20});
|
||||
var result = list;
|
||||
list = itemsOf(list).map(function(a) {
|
||||
a.cover = getImageUrl(a.cover);
|
||||
a.photo_count = a.photo_count || 0;
|
||||
// 地理位置:优先显示地址名称,其次显示经纬度
|
||||
a.location_text = a.location || ((a.lat && a.lng) ? (a.lat + ', ' + a.lng) : '');
|
||||
return a;
|
||||
});
|
||||
this.setData({ albums: list });
|
||||
} catch(e) { console.error(e); this.setData({ loadError: true }); } finally { this._loading = false; this.setData({ loading: false }); }
|
||||
applyList(this, ticket, result, list, 'albums');
|
||||
} catch(e) { console.error(e); failList(this,ticket); } finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
openAlbum: function(e) {
|
||||
@@ -174,4 +175,4 @@ Page(withDataCache({
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadAlbums": ["albumList"]}));
|
||||
}, 'loadAlbums'), {"loadAlbums": ["albumList"]}));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- pages/albums/albums.wxml - 相册列表 -->
|
||||
<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>
|
||||
<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>
|
||||
@@ -41,6 +41,7 @@
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// Diary journal with server pagination.
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
const { diaryEntry } = require('../../utils/journal');
|
||||
const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos');
|
||||
Page(withDataCache({
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: {
|
||||
loading: true, loadError: false,
|
||||
@@ -51,45 +52,14 @@ Page(withDataCache({
|
||||
return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' };
|
||||
},
|
||||
|
||||
loadData: async function() {
|
||||
if (this._loading) return;
|
||||
this._loading = true;
|
||||
this.setData({ loading: !this._hasCachedData, loadError: false });
|
||||
loadData: async function(page) {
|
||||
const ticket=startList(this,page);if(!ticket)return;
|
||||
try {
|
||||
var data = await api.diaryList({ page: 1, page_size: this.data.pageSize });
|
||||
var items = this._formatItems(data.items || []);
|
||||
this.setData({
|
||||
page: 1,
|
||||
list: items,
|
||||
total: data.total || 0,
|
||||
hasMore: (data.total || 0) > this.data.pageSize
|
||||
});
|
||||
const data=await api.diaryList({page:ticket.page,page_size:20});
|
||||
applyList(this,ticket,data,this._formatItems(itemsOf(data)),'list');
|
||||
this._loadPartnerInfo();
|
||||
} catch (e) {
|
||||
this.setData({ loadError: true });
|
||||
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 });
|
||||
}
|
||||
} catch(e) { failList(this,ticket); }
|
||||
finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
_formatItems: function(items) {
|
||||
@@ -194,4 +164,4 @@ Page(withDataCache({
|
||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["diaryList", "coupleInfo"]}));
|
||||
}, 'loadData'), {"loadData": ["diaryList", "coupleInfo"]}));
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { itemsOf, totalOf, mergeUnique } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
@@ -8,7 +9,7 @@ Page(withDataCache({
|
||||
diary: null,
|
||||
loading: true,
|
||||
comments: [],
|
||||
commentPage: 1,
|
||||
commentPage: 1, commentsLoadingMore: false, commentsMoreError: false, commentsTotal: 0,
|
||||
hasMoreComments: true,
|
||||
showCommentInput: false,
|
||||
commentText: '',
|
||||
@@ -78,24 +79,28 @@ Page(withDataCache({
|
||||
return data;
|
||||
},
|
||||
|
||||
loadComments: async function() {
|
||||
this.setData({ commentsError: false });
|
||||
loadComments: function() { return this._loadCommentsPage(1); },
|
||||
_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 {
|
||||
var data = await api.diaryCommentList({ diary_id: this.data.diaryId });
|
||||
if (data && data.length > 0) {
|
||||
data.forEach(function(c) {
|
||||
if (c.created_at) {
|
||||
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: data || [] });
|
||||
} catch (e) { this.setData({ commentsError: true }); console.error(e); }
|
||||
var result = await api.diaryCommentList({diary_id:this.data.diaryId,page:page,page_size:20});
|
||||
if (generation !== this._commentsGeneration) return;
|
||||
var added = itemsOf(result).map(function(comment) {
|
||||
return Object.assign({}, comment, {_time:String(comment.created_at || comment.create_time || '').slice(0,16).replace('T',' ')});
|
||||
});
|
||||
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}); }
|
||||
}
|
||||
},
|
||||
loadMoreComments: function() { return this._loadCommentsPage(this.data.commentPage+1); },
|
||||
|
||||
|
||||
onShareTap: function() {
|
||||
wx.showActionSheet({
|
||||
@@ -177,6 +182,9 @@ Page(withDataCache({
|
||||
}
|
||||
var res = await api.diaryCommentAdd(params);
|
||||
if (res.success) {
|
||||
this._commentsGeneration = (this._commentsGeneration || 0) + 1;
|
||||
this._commentsFirst = false;
|
||||
this.setData({commentsLoadingMore:false});
|
||||
var newComment = res.comment || {};
|
||||
if (newComment.created_at) {
|
||||
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');
|
||||
}
|
||||
var newComments = this.data.comments.slice();
|
||||
newComments.push(newComment);
|
||||
newComments.unshift(newComment);
|
||||
this.setData({
|
||||
comments: newComments,
|
||||
commentsTotal: this.data.commentsTotal + 1,
|
||||
showCommentInput: false,
|
||||
replyTo: null,
|
||||
commentText: '',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<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>
|
||||
<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-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">
|
||||
@@ -58,6 +58,8 @@
|
||||
</text>
|
||||
</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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/discover/discover.js
|
||||
@@ -46,18 +47,13 @@ Page(withDataCache({
|
||||
const [timeline, album, diary] = await Promise.all([
|
||||
api.timelineList(), api.albumList(), api.diaryList()
|
||||
]);
|
||||
var photoCount = 0;
|
||||
if (Array.isArray(album)) {
|
||||
for (var i = 0; i < album.length; i++) {
|
||||
photoCount += parseInt(album[i].photo_count || 0);
|
||||
}
|
||||
}
|
||||
var photoCount = album && album.photo_total != null ? Number(album.photo_total) : itemsOf(album).reduce((sum,item)=>sum+(Number(item.photo_count)||0),0);
|
||||
var stats = {
|
||||
timeline: Array.isArray(timeline) ? timeline.length : 0,
|
||||
timeline: totalOf(timeline),
|
||||
album: photoCount,
|
||||
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) {
|
||||
console.error(e);
|
||||
this.setData({loadError:true});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||
let lastLocationReport = 0;
|
||||
let locationReportUser = null;
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
@@ -221,29 +222,24 @@ Page(withDataCache({
|
||||
api.songsList(), api.checkinList(),
|
||||
this.data.coupleId ? api.petInfo() : Promise.resolve({ has_pet: false })
|
||||
]);
|
||||
var photoCount = 0;
|
||||
if (Array.isArray(album)) {
|
||||
for (var i = 0; i < album.length; i++) {
|
||||
photoCount += parseInt(album[i].photo_count || 0);
|
||||
}
|
||||
}
|
||||
var photoCount = album && album.photo_total != null ? Number(album.photo_total) : itemsOf(album).reduce((sum,item)=>sum+(Number(item.photo_count)||0),0);
|
||||
var petLevel = 0;
|
||||
if (pet && pet.has_pet && pet.pet) {
|
||||
petLevel = parseInt(pet.pet.level || 0);
|
||||
}
|
||||
var recentDiary = ((diary && diary.items) || []).slice(0, 1).map(diaryEntry);
|
||||
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 next = future[0];
|
||||
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 = timeline && !Array.isArray(timeline) && 'upcoming' in timeline ? timeline.upcoming : future[0];
|
||||
this.setData({
|
||||
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,
|
||||
stats: {
|
||||
timeline: Array.isArray(timeline) ? timeline.length : 0,
|
||||
timeline: totalOf(timeline),
|
||||
album: photoCount,
|
||||
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)),
|
||||
pet: petLevel
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/lovewords/lovewords.js - 一起听过的歌
|
||||
const { api } = require('../../utils/request');
|
||||
Page(withDataCache({
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
list: [],
|
||||
@@ -30,13 +31,12 @@ Page(withDataCache({
|
||||
return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' };
|
||||
},
|
||||
|
||||
loadData: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
loadData: async function(page) {
|
||||
const ticket = startList(this, page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
const list = await api.songsList();
|
||||
const items = (list || []).map(function(item) {
|
||||
const list = await api.songsList({page:ticket.page,page_size:20});
|
||||
const items = itemsOf(list).map(function(item) {
|
||||
if (item.created_at) {
|
||||
var d = new Date(item.created_at);
|
||||
item.date_month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
@@ -44,13 +44,13 @@ Page(withDataCache({
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setData({ list: items });
|
||||
applyList(this, ticket, list, items, 'list');
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
failList(this,ticket);
|
||||
console.error(e);
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
noop: function() {},
|
||||
@@ -124,4 +124,4 @@ Page(withDataCache({
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["songsList"]}));
|
||||
}, 'loadData'), {"loadData": ["songsList"]}));
|
||||
|
||||
@@ -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>
|
||||
<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}}">
|
||||
@@ -21,6 +21,7 @@
|
||||
</view>
|
||||
<page-state wx:if="{{!loading && !loadError && !list.length}}" title="收藏我们的第一首歌" description="让某一段旋律,成为两个人的记忆" action="添加歌曲" bind:action="goToPublish" />
|
||||
</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>
|
||||
|
||||
<!-- 添加弹窗 -->
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/profile/profile.js - 与原网站一致的个人中心
|
||||
@@ -1025,11 +1026,11 @@ Page(withDataCache({
|
||||
const key=keys[index];
|
||||
if(result.status !== 'fulfilled') { counts[key]='—';failed=true;return; }
|
||||
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==='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 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});
|
||||
},
|
||||
|
||||
+12
-11
@@ -1,8 +1,9 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/punch/punch.js - 情侣打卡
|
||||
const { api, getImageUrl } = require('../../utils/request');
|
||||
Page(withDataCache({
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loadError: false,
|
||||
checkins: [],
|
||||
@@ -54,16 +55,15 @@ Page(withDataCache({
|
||||
});
|
||||
},
|
||||
|
||||
loadData: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
loadData: async function(page) {
|
||||
const ticket = startList(this, page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
const [res, stats] = await Promise.all([
|
||||
api.checkinList(),
|
||||
api.checkinList({page:ticket.page,page_size:20}),
|
||||
api.checkinStats().catch(() => ({}))
|
||||
]);
|
||||
const list = (res.items || res || []).map(function(item) {
|
||||
const list = itemsOf(res).map(function(item) {
|
||||
item.type = item.type || 'both';
|
||||
var typeLabels = { both: '双方', first: '第一视角', her: '她打卡' };
|
||||
item.type_label = typeLabels[item.type] || '双方';
|
||||
@@ -89,10 +89,11 @@ Page(withDataCache({
|
||||
todayPartner: stats.today_partner_count || 0,
|
||||
loading: false
|
||||
};
|
||||
this.setData(data);
|
||||
delete data.checkins;
|
||||
applyList(this, ticket, res, list, 'checkins', data);
|
||||
} catch(e) {
|
||||
this.setData({loadError:true}); console.error(e); this.setData({ loading: false }); }
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
failList(this,ticket); console.error(e); this.setData({ loading: false }); }
|
||||
finally { finishList(this,ticket); }
|
||||
},
|
||||
|
||||
// ===== 点击卡片 → 详情弹窗 =====
|
||||
@@ -119,4 +120,4 @@ Page(withDataCache({
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["checkinList", "checkinStats", "coupleInfo"]}));
|
||||
}, 'loadData'), {"loadData": ["checkinList", "checkinStats", "coupleInfo"]}));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- pages/punch/punch.wxml - 情侣打卡 -->
|
||||
<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>
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
|
||||
<!-- 顶部 Banner - 卡片式 -->
|
||||
@@ -83,6 +83,7 @@
|
||||
<page-state wx:if="{{!loading && !loadError && !checkins.length}}" title="从一个小约定开始" description="一起运动、按时吃饭,也可以成为日常仪式" action="添加打卡" bind:action="showAddModal" />
|
||||
</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>
|
||||
|
||||
<!-- 浮动按钮 -->
|
||||
|
||||
@@ -3,5 +3,6 @@ Page({
|
||||
data:{},
|
||||
onLoad(){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();}}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/timeline/timeline.js
|
||||
@@ -56,7 +57,7 @@ function calcNearestRepeatDate(eventDate, today, interval, unit) {
|
||||
return new Date(eventDate);
|
||||
}
|
||||
|
||||
Page(withDataCache({
|
||||
Page(withDataCache(withListPagination({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
list: []
|
||||
@@ -82,12 +83,11 @@ Page(withDataCache({
|
||||
return { title: '让所有的爱有迹可循', imageUrl: '/images/share.png' };
|
||||
},
|
||||
|
||||
loadData: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
loadData: async function(page) {
|
||||
const ticket = startList(this, page);
|
||||
if (!ticket) return;
|
||||
try {
|
||||
const list = await api.timelineList();
|
||||
const list = await api.timelineList({page:ticket.page,page_size:20});
|
||||
const myUid = wx.getStorageSync('userId');
|
||||
var nameMap = {};
|
||||
var avatarMap = {};
|
||||
@@ -100,12 +100,12 @@ Page(withDataCache({
|
||||
} catch (e) {}
|
||||
|
||||
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;
|
||||
item.creator_name = nameMap[cid] || (cid == myUid ? '我' : 'TA');
|
||||
item.creator_avatar = avatarMap[cid] || '';
|
||||
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) : '';
|
||||
|
||||
// 计算纪念日/倒数日(支持重复事件)
|
||||
@@ -150,13 +150,13 @@ Page(withDataCache({
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setData({ list: items });
|
||||
applyList(this, ticket, list, items, 'list');
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
failList(this,ticket);
|
||||
console.error(e);
|
||||
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' }); },
|
||||
goBack: function() { wx.navigateBack(); },
|
||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||
}, {"loadData": ["timelineList", "coupleInfo"]}));
|
||||
}, 'loadData'), {"loadData": ["timelineList", "coupleInfo"]}));
|
||||
|
||||
@@ -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>
|
||||
<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}}">
|
||||
@@ -48,6 +48,7 @@
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
@@ -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-profile-setting .j-link { font-size:32rpx; }
|
||||
|
||||
.j-pagination { padding:28rpx 0 40rpx; text-align:center; color:#85776D; font-size:23rpx; }
|
||||
|
||||
@@ -15,6 +15,7 @@ function records(api) {
|
||||
Page:p=>definition=p, console:{error(){}}, setTimeout:f=>f(),
|
||||
wx:{getStorageSync:()=> 'token',navigateTo:p=>routes.push(p.url)},
|
||||
require(name) {
|
||||
if(name.endsWith('/list-pagination'))return require('../utils/list-pagination');
|
||||
if(name.endsWith('/page-cache'))return {withDataCache:p=>p};
|
||||
if(name.endsWith('/request'))return {api};
|
||||
if(name.endsWith('/journal'))return formatter();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -38,7 +38,7 @@ test('failed homepage load ends the recent-record spinner',async()=>{
|
||||
let definition;
|
||||
const api={coupleInfo:async()=>{throw { _statusCode:404 };}};
|
||||
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
|
||||
});
|
||||
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;
|
||||
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'),{
|
||||
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
|
||||
});
|
||||
const page={...definition,data:{...definition.data,coupleId:99},setData(data){Object.assign(this.data,data);}};
|
||||
|
||||
@@ -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};
|
||||
@@ -263,21 +263,21 @@ const api = {
|
||||
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),
|
||||
timelineUpdate: (data) => request('timelineUpdate', data),
|
||||
timelineDelete: (data) => request('timelineDelete', data),
|
||||
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),
|
||||
albumUpdate: (data) => request('albumUpdate', data),
|
||||
albumDelete: (data) => request('albumDelete', data),
|
||||
photoDelete: (data) => request('photoDelete', data),
|
||||
photoAdd: (data) => request('photoAdd', 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) => {
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
try {
|
||||
@@ -351,21 +351,21 @@ const api = {
|
||||
diaryUpdate: (data) => request('diaryUpdate', data),
|
||||
diaryDelete: (data) => request('diaryDelete', data),
|
||||
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),
|
||||
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),
|
||||
songsUpdate: (data) => request('songsUpdate', 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),
|
||||
checkinList: () => request('checkinList', null, 'GET'),
|
||||
checkinList: (data = {}) => request('checkinList', { page: 1, page_size: 20, ...data }, 'GET'),
|
||||
checkinAdd: (data) => request('checkinAdd', data),
|
||||
checkinDo: (data) => request('checkinDo', data),
|
||||
checkinStats: () => request('checkinStats', null, 'GET'),
|
||||
|
||||
Reference in New Issue
Block a user