lake.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. 'use strict';
  2. // 数据中台页:本地湖全景 + 云端同步(推送/拉取)
  3. // 读取 GET /api/reports/lake;推送逐主体执行(POST /api/reports/aitable/push)。
  4. (function () {
  5. const $ = (id) => document.getElementById(id);
  6. let inited = false;
  7. let loading = false;
  8. function fmtDate(ymd) {
  9. const s = String(ymd || '');
  10. return /^\d{8}$/.test(s) ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}` : (s || '–');
  11. }
  12. function fmtTime(iso) {
  13. if (!iso) return '–';
  14. const d = new Date(iso);
  15. return Number.isNaN(d.getTime()) ? String(iso) : `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
  16. }
  17. function esc(value) {
  18. const div = document.createElement('div');
  19. div.textContent = String(value ?? '');
  20. return div.innerHTML;
  21. }
  22. function setBadge(id, text, cls) {
  23. const badge = $(id);
  24. if (!badge) return;
  25. badge.textContent = text;
  26. badge.className = 'badge ' + (cls || 'badge-info');
  27. }
  28. function renderKpis(data) {
  29. $('lakeEntityCount').textContent = data.entities.length;
  30. $('lakeRowCount').textContent = data.totalRows;
  31. $('lakeCoverage').textContent = data.coverage ? `${fmtDate(data.coverage.from)} ~ ${fmtDate(data.coverage.to)}` : '–';
  32. const lastSyncs = data.entities.map((e) => e.lastSync && e.lastSync.at).filter(Boolean).sort();
  33. $('lakeLastSync').textContent = lastSyncs.length ? fmtTime(lastSyncs[lastSyncs.length - 1]) : '–';
  34. $('lakeRowsBadge').textContent = `${data.entities.length} 主体`;
  35. }
  36. function renderEntities(entities) {
  37. const tbody = $('lakeEntityRows');
  38. if (!entities.length) {
  39. tbody.innerHTML = '<tr><td colspan="10" class="empty-cell">本地湖为空 —— 取数机先「同步数据」,其他电脑点右上「从云端拉取」</td></tr>';
  40. return;
  41. }
  42. tbody.innerHTML = entities.map((e) => {
  43. const cov = e.coverage ? `${fmtDate(e.coverage.from)} ~ ${fmtDate(e.coverage.to)}` : '–';
  44. const sync = e.lastSync ? `${fmtTime(e.lastSync.at)}(${fmtDate(e.lastSync.from)}~${fmtDate(e.lastSync.to)})${e.lastSync.ok ? '' : ' ⚠'}` : '–';
  45. const push = e.lastPush ? `${fmtTime(e.lastPush.at)} / ${e.lastPush.pushedRows || 0} 行` : '未推送';
  46. return `<tr>
  47. <td>${esc(e.name || e.key)}</td>
  48. <td>${e.shopCount || 0} / ${e.chainCount || 0}</td>
  49. <td>${e.rows}</td>
  50. <td>${e.byScope.company || 0}</td>
  51. <td>${e.byScope.chain || 0}</td>
  52. <td>${e.byScope.shop || 0}</td>
  53. <td>${e.byScope.summary || 0}</td>
  54. <td>${cov}</td>
  55. <td>${esc(sync)}</td>
  56. <td>${esc(push)}</td>
  57. </tr>`;
  58. }).join('');
  59. }
  60. async function loadLake() {
  61. if (loading) return;
  62. loading = true;
  63. try {
  64. const res = await fetch('/api/reports/lake');
  65. const data = await res.json();
  66. if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
  67. renderKpis(data);
  68. renderEntities(data.entities);
  69. if (!data.aitable.enabled) setBadge('lakeSyncBadge', '云端同步未启用(系统设置)', 'badge-info');
  70. else if (!data.aitable.operatorConfigured) setBadge('lakeSyncBadge', '云端配置不完整', 'badge-fail');
  71. else setBadge('lakeSyncBadge', '云端就绪', 'badge-ok');
  72. } catch (error) {
  73. setBadge('lakeSyncBadge', `加载失败:${error.message}`, 'badge-fail');
  74. } finally {
  75. loading = false;
  76. }
  77. }
  78. // 推送:逐主体执行,聚合结果(单主体失败不中断其余,最后统一汇报)
  79. async function pushAll() {
  80. const btn = $('btnLakePush');
  81. btn.disabled = true;
  82. setBadge('lakeSyncBadge', '推送中…');
  83. try {
  84. const res = await fetch('/api/reports/lake');
  85. const data = await res.json();
  86. const targets = (data.entities || []).map((e) => e.key);
  87. if (!targets.length) throw new Error('本地湖为空,没有可推送的主体');
  88. let created = 0;
  89. let updated = 0;
  90. let stale = 0;
  91. const errors = [];
  92. for (const key of targets) {
  93. try {
  94. const r = await window.shanAitableAction('push', 'lakeSyncBadge', { entity: key });
  95. created += r.created || 0;
  96. updated += r.updated || 0;
  97. stale += r.skippedStale || 0;
  98. } catch (e) {
  99. errors.push(`${key}:${e.message}`);
  100. }
  101. }
  102. if (errors.length) {
  103. setBadge('lakeSyncBadge', `部分失败(成功 新增${created}/更新${updated}):${errors[0]}`, 'badge-fail');
  104. } else {
  105. setBadge('lakeSyncBadge', `已推送:新增 ${created} / 更新 ${updated}${stale ? ` / 跳过云端较新 ${stale}` : ''}`, 'badge-ok');
  106. }
  107. await loadLake();
  108. } catch (error) {
  109. setBadge('lakeSyncBadge', error.message, 'badge-fail');
  110. } finally {
  111. btn.disabled = false;
  112. }
  113. }
  114. async function pullAll() {
  115. const btn = $('btnLakePull');
  116. btn.disabled = true;
  117. try {
  118. await window.shanAitableAction('pull', 'lakeSyncBadge', {});
  119. await loadLake();
  120. } catch { /* 徽标已由 aitableAction 写入 */ } finally {
  121. btn.disabled = false;
  122. }
  123. }
  124. window.shanLakeInit = function () {
  125. if (!inited) {
  126. inited = true;
  127. $('btnLakeRefresh').addEventListener('click', loadLake);
  128. $('btnLakePush').addEventListener('click', pushAll);
  129. $('btnLakePull').addEventListener('click', pullAll);
  130. }
  131. loadLake();
  132. };
  133. })();