|
|
@@ -0,0 +1,144 @@
|
|
|
+'use strict';
|
|
|
+
|
|
|
+// 数据中台页:本地湖全景 + 云端同步(推送/拉取)
|
|
|
+// 读取 GET /api/reports/lake;推送逐主体执行(POST /api/reports/aitable/push)。
|
|
|
+(function () {
|
|
|
+ const $ = (id) => document.getElementById(id);
|
|
|
+ let inited = false;
|
|
|
+ let loading = false;
|
|
|
+
|
|
|
+ function fmtDate(ymd) {
|
|
|
+ const s = String(ymd || '');
|
|
|
+ return /^\d{8}$/.test(s) ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}` : (s || '–');
|
|
|
+ }
|
|
|
+
|
|
|
+ function fmtTime(iso) {
|
|
|
+ if (!iso) return '–';
|
|
|
+ const d = new Date(iso);
|
|
|
+ return Number.isNaN(d.getTime()) ? String(iso) : `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function esc(value) {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.textContent = String(value ?? '');
|
|
|
+ return div.innerHTML;
|
|
|
+ }
|
|
|
+
|
|
|
+ function setBadge(id, text, cls) {
|
|
|
+ const badge = $(id);
|
|
|
+ if (!badge) return;
|
|
|
+ badge.textContent = text;
|
|
|
+ badge.className = 'badge ' + (cls || 'badge-info');
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderKpis(data) {
|
|
|
+ $('lakeEntityCount').textContent = data.entities.length;
|
|
|
+ $('lakeRowCount').textContent = data.totalRows;
|
|
|
+ $('lakeCoverage').textContent = data.coverage ? `${fmtDate(data.coverage.from)} ~ ${fmtDate(data.coverage.to)}` : '–';
|
|
|
+ const lastSyncs = data.entities.map((e) => e.lastSync && e.lastSync.at).filter(Boolean).sort();
|
|
|
+ $('lakeLastSync').textContent = lastSyncs.length ? fmtTime(lastSyncs[lastSyncs.length - 1]) : '–';
|
|
|
+ $('lakeRowsBadge').textContent = `${data.entities.length} 主体`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderEntities(entities) {
|
|
|
+ const tbody = $('lakeEntityRows');
|
|
|
+ if (!entities.length) {
|
|
|
+ tbody.innerHTML = '<tr><td colspan="10" class="empty-cell">本地湖为空 —— 取数机先「同步数据」,其他电脑点右上「从云端拉取」</td></tr>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ tbody.innerHTML = entities.map((e) => {
|
|
|
+ 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} 行` : '未推送';
|
|
|
+ return `<tr>
|
|
|
+ <td>${esc(e.name || e.key)}</td>
|
|
|
+ <td>${e.shopCount || 0} / ${e.chainCount || 0}</td>
|
|
|
+ <td>${e.rows}</td>
|
|
|
+ <td>${e.byScope.company || 0}</td>
|
|
|
+ <td>${e.byScope.chain || 0}</td>
|
|
|
+ <td>${e.byScope.shop || 0}</td>
|
|
|
+ <td>${e.byScope.summary || 0}</td>
|
|
|
+ <td>${cov}</td>
|
|
|
+ <td>${esc(sync)}</td>
|
|
|
+ <td>${esc(push)}</td>
|
|
|
+ </tr>`;
|
|
|
+ }).join('');
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadLake() {
|
|
|
+ if (loading) return;
|
|
|
+ loading = true;
|
|
|
+ try {
|
|
|
+ const res = await fetch('/api/reports/lake');
|
|
|
+ const data = await res.json();
|
|
|
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
|
+ renderKpis(data);
|
|
|
+ renderEntities(data.entities);
|
|
|
+ if (!data.aitable.enabled) setBadge('lakeSyncBadge', '云端同步未启用(系统设置)', 'badge-info');
|
|
|
+ else if (!data.aitable.operatorConfigured) setBadge('lakeSyncBadge', '云端配置不完整', 'badge-fail');
|
|
|
+ else setBadge('lakeSyncBadge', '云端就绪', 'badge-ok');
|
|
|
+ } catch (error) {
|
|
|
+ setBadge('lakeSyncBadge', `加载失败:${error.message}`, 'badge-fail');
|
|
|
+ } finally {
|
|
|
+ loading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 推送:逐主体执行,聚合结果(单主体失败不中断其余,最后统一汇报)
|
|
|
+ async function pushAll() {
|
|
|
+ const btn = $('btnLakePush');
|
|
|
+ btn.disabled = true;
|
|
|
+ setBadge('lakeSyncBadge', '推送中…');
|
|
|
+ try {
|
|
|
+ const res = await fetch('/api/reports/lake');
|
|
|
+ const data = await res.json();
|
|
|
+ const targets = (data.entities || []).map((e) => e.key);
|
|
|
+ if (!targets.length) throw new Error('本地湖为空,没有可推送的主体');
|
|
|
+ let created = 0;
|
|
|
+ let updated = 0;
|
|
|
+ let stale = 0;
|
|
|
+ const errors = [];
|
|
|
+ for (const key of targets) {
|
|
|
+ try {
|
|
|
+ const r = await window.shanAitableAction('push', 'lakeSyncBadge', { entity: key });
|
|
|
+ created += r.created || 0;
|
|
|
+ updated += r.updated || 0;
|
|
|
+ stale += r.skippedStale || 0;
|
|
|
+ } catch (e) {
|
|
|
+ errors.push(`${key}:${e.message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (errors.length) {
|
|
|
+ setBadge('lakeSyncBadge', `部分失败(成功 新增${created}/更新${updated}):${errors[0]}`, 'badge-fail');
|
|
|
+ } else {
|
|
|
+ setBadge('lakeSyncBadge', `已推送:新增 ${created} / 更新 ${updated}${stale ? ` / 跳过云端较新 ${stale}` : ''}`, 'badge-ok');
|
|
|
+ }
|
|
|
+ await loadLake();
|
|
|
+ } catch (error) {
|
|
|
+ setBadge('lakeSyncBadge', error.message, 'badge-fail');
|
|
|
+ } finally {
|
|
|
+ btn.disabled = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function pullAll() {
|
|
|
+ const btn = $('btnLakePull');
|
|
|
+ btn.disabled = true;
|
|
|
+ try {
|
|
|
+ await window.shanAitableAction('pull', 'lakeSyncBadge', {});
|
|
|
+ await loadLake();
|
|
|
+ } catch { /* 徽标已由 aitableAction 写入 */ } finally {
|
|
|
+ btn.disabled = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ window.shanLakeInit = function () {
|
|
|
+ if (!inited) {
|
|
|
+ inited = true;
|
|
|
+ $('btnLakeRefresh').addEventListener('click', loadLake);
|
|
|
+ $('btnLakePush').addEventListener('click', pushAll);
|
|
|
+ $('btnLakePull').addEventListener('click', pullAll);
|
|
|
+ }
|
|
|
+ loadLake();
|
|
|
+ };
|
|
|
+})();
|