lake.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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. // 空湖:与明细 tab 同款 .empty-hint 空态(图标+标题+引导文案),表格整体隐藏
  39. $('lakeEntityTable').hidden = !entities.length;
  40. $('lakeEmpty').hidden = !!entities.length;
  41. if (!entities.length) return;
  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. const helpBtn = $('btnLakeHelp');
  111. const helpPop = $('lakeHelpPopover');
  112. if (helpBtn && helpPop) {
  113. helpBtn.addEventListener('click', (event) => {
  114. event.stopPropagation();
  115. helpPop.hidden = !helpPop.hidden;
  116. helpBtn.classList.toggle('active', !helpPop.hidden);
  117. helpBtn.setAttribute('aria-expanded', String(!helpPop.hidden));
  118. });
  119. document.addEventListener('click', (event) => {
  120. if (!helpPop.hidden && !event.target.closest('.conn-help-wrap')) {
  121. helpPop.hidden = true;
  122. helpBtn.classList.remove('active');
  123. helpBtn.setAttribute('aria-expanded', 'false');
  124. }
  125. });
  126. }
  127. }
  128. activateLakeTab('overview');
  129. };
  130. // 供问数「应用到报表」等外部入口直接落到明细 tab
  131. window.shanLakeShowDetail = function () {
  132. activateLakeTab('detail');
  133. };
  134. })();