lake.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  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 pullAll() {
  80. const btn = $('btnLakePull');
  81. btn.disabled = true;
  82. try {
  83. await window.shanAitableAction('pull', 'lakeSyncBadge', {});
  84. await loadLake();
  85. } catch { /* 徽标已由 aitableAction 写入 */ } finally {
  86. btn.disabled = false;
  87. }
  88. }
  89. // 页内 tab:湖全景 / 明细数据(明细区首次激活时才初始化报表浏览,懒加载)
  90. function activateLakeTab(tab) {
  91. const tabs = $('lakeTabs');
  92. if (!tabs) return;
  93. tabs.querySelectorAll('button').forEach((b) => b.classList.toggle('active', b.dataset.tab === tab));
  94. $('lakeTabOverview').hidden = tab !== 'overview';
  95. $('lakeTabDetail').hidden = tab !== 'detail';
  96. if (tab === 'overview') loadLake();
  97. else if (window.shanReportsInit) window.shanReportsInit();
  98. }
  99. window.shanLakeInit = function () {
  100. if (!inited) {
  101. inited = true;
  102. $('btnLakeRefresh').addEventListener('click', loadLake);
  103. $('btnLakePull').addEventListener('click', pullAll);
  104. $('lakeTabs').addEventListener('click', (event) => {
  105. const btn = event.target.closest('button[data-tab]');
  106. if (btn) activateLakeTab(btn.dataset.tab);
  107. });
  108. }
  109. activateLakeTab('overview');
  110. };
  111. // 供问数「应用到报表」等外部入口直接落到明细 tab
  112. window.shanLakeShowDetail = function () {
  113. activateLakeTab('detail');
  114. };
  115. })();