Kaynağa Gözat

feat: pending-push count badge (per entity) on sync history and lake overview

KenZ1117 2 hafta önce
ebeveyn
işleme
ede9901b5a
4 değiştirilmiş dosya ile 27 ekleme ve 4 silme
  1. 2 1
      public/lake.js
  2. 10 2
      public/reports.js
  3. 2 0
      server/routes/reports.js
  4. 13 1
      server/services/aitable.js

+ 2 - 1
public/lake.js

@@ -50,6 +50,7 @@
       const cov = e.coverage ? `${fmtDate(e.coverage.from)} ~ ${fmtDate(e.coverage.to)}` : '–';
       const sync = e.lastSync ? `${fmtTime(e.lastSync.at)}(${fmtDate(e.lastSync.from)}~${fmtDate(e.lastSync.to)})${e.lastSync.ok ? '' : ' ⚠'}` : '–';
       const push = e.lastPush ? `${fmtTime(e.lastPush.at)} / ${e.lastPush.pushedRows || 0} 行` : '未推送';
+      const pending = typeof e.pending === 'number' && e.pending > 0 ? `<span class="badge badge-warn">待推 ${e.pending} 行</span>` : '';
       return `<tr>
         <td>${esc(e.name || e.key)}</td>
         <td>${e.shopCount || 0} / ${e.chainCount || 0}</td>
@@ -60,7 +61,7 @@
         <td>${e.byScope.summary || 0}</td>
         <td>${cov}</td>
         <td>${esc(sync)}</td>
-        <td>${esc(push)}</td>
+        <td>${esc(push)} ${pending}</td>
       </tr>`;
     }).join('');
   }

+ 10 - 2
public/reports.js

@@ -177,6 +177,7 @@
           renderShopOptions();
           reloadReport();
           renderCharts();
+          refreshPushBadge(); // 待推计数按主体算,切主体要重取
         });
         wrap.appendChild(btn);
       }
@@ -1346,7 +1347,8 @@
     const badge = $('reportPushBadge');
     if (!badge) return;
     try {
-      const res = await fetch('/api/reports/aitable/status');
+      const entity = state.entity || (state.entities[0] || {}).key;
+      const res = await fetch(`/api/reports/aitable/status${entity ? `?entity=${encodeURIComponent(entity)}` : ''}`);
       const data = await res.json();
       if (!data.enabled) {
         badge.textContent = '云端未启用';
@@ -1354,8 +1356,13 @@
       } else if (!data.configured) {
         badge.textContent = '云端配置不完整';
         badge.className = 'badge badge-fail';
+      } else if (typeof data.pending === 'number' && data.pending > 0) {
+        badge.textContent = `待推送 ${data.pending} 行`;
+        badge.title = '点「推送云端」会把上次推送之后的全部增量一次性推上钉钉 AI 表格';
+        badge.className = 'badge badge-warn';
       } else {
-        badge.textContent = '云端就绪';
+        badge.textContent = '已同步至云端';
+        badge.title = '';
         badge.className = 'badge badge-ok';
       }
     } catch { /* 状态刷新失败不打扰 */ }
@@ -1368,6 +1375,7 @@
       $('btnReportPush').addEventListener('click', async () => {
         await pushCurrentEntity();
         renderSyncHistory(); // 云端推送状态列随推送结果刷新
+        refreshPushBadge();  // 待推计数归零(或显示剩余)
       });
       $('reportPager').addEventListener('click', (event) => {
         const btn = event.target.closest('button[data-page]');

+ 2 - 0
server/routes/reports.js

@@ -76,6 +76,7 @@ function register(app, ctx) {
         rows: rows.length, byScope, coverage: rows.length ? { from, to } : null,
         lastSync: lastSync ? { at: lastSync.at || null, from: lastSync.from || null, to: lastSync.to || null, ok: !lastSync.cancelled && !lastSync.failed } : null,
         lastPush: ctx.aitable ? ctx.aitable.lastPushInfo(e.key) : null,
+        pending: ctx.aitable ? ctx.aitable.pendingCount(e.key) : null,
       };
     });
     const totalRows = entities.reduce((s, e) => s + e.rows, 0);
@@ -165,6 +166,7 @@ function register(app, ctx) {
       enabled: !!(ctx.config.aitable && ctx.config.aitable.enabled),
       configured: !!(ctx.config.aitable && ctx.config.aitable.baseId && ctx.config.aitable.sheetName && ctx.config.aitable.appKey),
       lastPush: entity ? ctx.aitable.lastPushInfo(entity) : null,
+      pending: entity ? ctx.aitable.pendingCount(entity) : null,
     });
   });
 

+ 13 - 1
server/services/aitable.js

@@ -352,8 +352,20 @@ function createAitableService(ctx) {
     return s;
   }
 
+  /** 待推送行数:水位之后有变化(syncedAt 更新)的行;配置不齐返回 null(调用方显示配置态) */
+  function pendingCount(entity) {
+    const cfg = config.aitable || {};
+    if (!cfg.enabled || !cfg.baseId || !cfg.sheetName || !cfg.appKey || !cfg.operatorId) return null;
+    const naposService = require('../../napos-service.js');
+    const key = naposService.store.sanitizeKey(entity);
+    const watermark = (readPushState()[key] || {}).lastPushedAt || 0;
+    const all = naposService.store.loadRows(key) || [];
+    if (!watermark) return all.length;
+    return all.filter((r) => new Date(r.syncedAt || 0).getTime() > watermark).length;
+  }
+
   logs.pushSystemLog('钉钉 AI 表格同步服务已就绪', 'info');
-  return { pushLake, pullLake, testConnection, lastPushInfo };
+  return { pushLake, pullLake, testConnection, lastPushInfo, pendingCount };
 }
 
 module.exports = { createAitableService, FIELD_COLUMNS, DIMENSION_LABEL, rowToRecord, recordKey, recordToRow };