仰望星辰工作室

fix(front): 修复无限滚动只加载一批就卡在"加载中..."的问题

F fqh 提交于 2026-09-13 23:19 · 3d65635 ·父提交 8b6a61d
fix(front): 修复无限滚动只加载一批就卡在"加载中..."的问题

现象
- 首页首批帖子正常显示,继续下滑一直停留在"加载中...",不再加载后续帖子
- 上一步把话题墙收起为单行后,页面变短才暴露出来(此前话题栏很高,哨兵一开始
  就远在预加载范围之外,靠滚动就能正常触发)

根因(两个缺陷叠加)
1. IntersectionObserver 只在相交状态**变化**时回调。首批 10 条插入后若哨兵仍在
   rootMargin(400px) 范围内,状态仍是 intersecting -> 不会再有回调 -> 卡死
2. 满 10 条的分支从不更新提示文案,初始的"加载中..."永远留在页面上

修复
- 每批加载完成后重新 observe 哨兵:observe() 会立即按当前相交状态回调一次,
  因此内容填不满屏幕时会自动续加载,直到哨兵离开预加载范围
- 加载中 / 已到底 / 首次为空 三种文案分别维护,满页时清空提示
- 结束时断开观察器;滚动兜底路径同样在首屏内容不足时主动续加载
- 新增 .load-state:empty 收起空提示的上下内边距

验证
- node --check 通过
- 分页游标接口回归:25 条数据按 before 游标依次返回 10/10/5/0 条,id 严格递减,
  与前端"少于一批即到底"的判定一致
2 个文件变更 +41 -11 fangqihang1717@163.com
•web/static/app.css +1 -0
•web/static/app.js +40 -11
变更内容
diff --git a/web/static/app.css b/web/static/app.css
index 1652474..6d9eead 100644
--- a/web/static/app.css
+++ b/web/static/app.css
@@ -178,6 +178,7 @@ button { font-family: inherit; }
 .view-all:hover { color: var(--clv-accent); }
 
 .load-state { text-align: center; color: var(--clv-muted); font-size: 13px; padding: 16px 0; }
+.load-state:empty { padding: 0; }
 .sentinel { height: 1px; }
 
 /* 悬浮发帖按钮 */
diff --git a/web/static/app.js b/web/static/app.js
index 867d5a3..da78477 100644
--- a/web/static/app.js
+++ b/web/static/app.js
@@ -207,11 +207,39 @@
     var sentinel = document.getElementById('sentinel');
     var topic = box.dataset.topic || '';
     var last = 0, loading = false, done = false;
+    var io = null;
+    var PAGE = 10;
+
+    // 页面底部是否已进入预加载范围
+    function nearBottom() {
+      return window.innerHeight + window.scrollY > document.body.offsetHeight - 500;
+    }
+
+    // 重新观察哨兵。observe() 会立即按"当前"相交状态回调一次,而这正是关键:
+    // IntersectionObserver 只在相交状态**变化**时回调,若一批数据插入后哨兵仍在
+    // 预加载范围内,状态没变就不会再回调,页面就会永远卡在"加载中..."。
+    // 内容不足以填满屏幕时(话题栏收起后页面变短尤其明显),还要靠它续加载。
+    function kick() {
+      if (io && sentinel && sentinel.parentNode) {
+        io.unobserve(sentinel);
+        io.observe(sentinel);
+      } else if (!io && nearBottom()) {
+        load();
+      }
+    }
+
+    function finish(msg) {
+      done = true;
+      state.textContent = msg;
+      if (sentinel) sentinel.remove();
+      if (io) { io.disconnect(); io = null; }
+    }
 
     function load() {
       if (loading || done) return;
       loading = true;
-      api('/api/v1/posts?before=' + last + '&limit=10&topic=' + encodeURIComponent(topic))
+      state.textContent = '加载中...';
+      api('/api/v1/posts?before=' + last + '&limit=' + PAGE + '&topic=' + encodeURIComponent(topic))
         .then(function (r) {
           loading = false;
           if (!r || !r.ok) {
@@ -221,11 +249,13 @@
           var posts = r.posts || [];
           posts.forEach(function (p) { box.appendChild(cardEl(p)); });
           if (posts.length) last = posts[posts.length - 1].id;
-          if (posts.length < 10) {
-            done = true;
-            state.textContent = last ? '已经到底啦' : '还没有帖子,点击右下角发布第一条';
-            if (sentinel) sentinel.remove();
+          if (posts.length < PAGE) {
+            finish(last ? '已经到底啦' : '还没有帖子,点击右下角发布第一条');
+            return;
           }
+          // 还有更多:清掉加载提示,并让哨兵重新判断是否需要立即续加载
+          state.textContent = '';
+          kick();
         })
         .catch(function () {
           loading = false;
@@ -234,14 +264,13 @@
     }
 
     if (sentinel && 'IntersectionObserver' in window) {
-      new IntersectionObserver(function (entries) {
+      io = new IntersectionObserver(function (entries) {
         if (entries[0].isIntersecting) load();
-      }, { rootMargin: '400px' }).observe(sentinel);
+      }, { rootMargin: '400px' });
+      io.observe(sentinel);
     } else {
-      // 兜底:滚动监听
-      window.addEventListener('scroll', function () {
-        if (window.innerHeight + window.scrollY > document.body.offsetHeight - 500) load();
-      });
+      // 兜底:滚动监听(首屏内容不足时同样需要主动续加载)
+      window.addEventListener('scroll', function () { if (nearBottom()) load(); });
     }
     load();
   }