lake.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  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. const pending = typeof e.pending === 'number' && e.pending > 0 ? `<span class="badge badge-warn">待推 ${e.pending} 行</span>` : '';
  47. return `<tr>
  48. <td>${esc(e.name || e.key)}</td>
  49. <td>${e.shopCount || 0} / ${e.chainCount || 0}</td>
  50. <td>${e.rows}</td>
  51. <td>${e.byScope.company || 0}</td>
  52. <td>${e.byScope.chain || 0}</td>
  53. <td>${e.byScope.shop || 0}</td>
  54. <td>${e.byScope.summary || 0}</td>
  55. <td>${cov}</td>
  56. <td>${esc(sync)}</td>
  57. <td>${esc(push)} ${pending}</td>
  58. </tr>`;
  59. }).join('');
  60. }
  61. async function loadLake() {
  62. if (loading) return;
  63. loading = true;
  64. try {
  65. const res = await fetch('/api/reports/lake');
  66. const data = await res.json();
  67. if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
  68. renderKpis(data);
  69. renderEntities(data.entities);
  70. if (!data.aitable.enabled) setBadge('lakeSyncBadge', '云端同步未启用(系统设置)', 'badge-info');
  71. else if (!data.aitable.operatorConfigured) setBadge('lakeSyncBadge', '云端配置不完整', 'badge-fail');
  72. else setBadge('lakeSyncBadge', '云端就绪', 'badge-ok');
  73. } catch (error) {
  74. setBadge('lakeSyncBadge', `加载失败:${error.message}`, 'badge-fail');
  75. } finally {
  76. loading = false;
  77. }
  78. }
  79. // 推送动作在数据采集页(取数机的动作);中台只负责看数与从云端拉取
  80. async function pullAll() {
  81. const btn = $('btnLakePull');
  82. btn.disabled = true;
  83. try {
  84. await window.shanAitableAction('pull', 'lakeSyncBadge', {});
  85. await loadLake();
  86. } catch { /* 徽标已由 aitableAction 写入 */ } finally {
  87. btn.disabled = false;
  88. }
  89. }
  90. // 页内 tab:湖全景 / 明细数据(明细区首次激活时才初始化报表浏览,懒加载)
  91. function activateLakeTab(tab) {
  92. const tabs = $('lakeTabs');
  93. if (!tabs) return;
  94. tabs.querySelectorAll('button').forEach((b) => b.classList.toggle('active', b.dataset.tab === tab));
  95. $('lakeTabOverview').hidden = tab !== 'overview';
  96. $('lakeTabDetail').hidden = tab !== 'detail';
  97. if (tab === 'overview') loadLake();
  98. else if (window.shanReportsInit) window.shanReportsInit();
  99. }
  100. window.shanLakeInit = function () {
  101. if (!inited) {
  102. inited = true;
  103. $('btnLakeRefresh').addEventListener('click', loadLake);
  104. $('btnLakePull').addEventListener('click', pullAll);
  105. $('lakeTabs').addEventListener('click', (event) => {
  106. const btn = event.target.closest('button[data-tab]');
  107. if (btn) activateLakeTab(btn.dataset.tab);
  108. });
  109. }
  110. activateLakeTab('overview');
  111. };
  112. // 供问数「应用到报表」等外部入口直接落到明细 tab
  113. window.shanLakeShowDetail = function () {
  114. activateLakeTab('detail');
  115. };
  116. })();