Преглед изворни кода

refactor: lake detail tab back to pure table; conflict policy into help popover; reuse ui kit for pager

KenZ1117 пре 2 недеља
родитељ
комит
35053beb91
4 измењених фајлова са 40 додато и 100 уклоњено
  1. 15 11
      public/index.html
  2. 18 0
      public/lake.js
  3. 4 86
      public/reports.js
  4. 3 3
      public/style.css

+ 15 - 11
public/index.html

@@ -360,7 +360,21 @@
         <div class="panel panel-fixed report-table-panel">
           <div class="panel-title">
             <span>主体明细</span>
-            <span class="badge badge-info" id="lakeRowsBadge">0 主体</span>
+            <span class="actions" style="display:flex;align-items:center;gap:8px;">
+              <span class="badge badge-info" id="lakeRowsBadge">0 主体</span>
+              <span class="conn-help-wrap">
+                <button type="button" class="help-btn" id="btnLakeHelp" title="多机数据与云端同步规则" aria-expanded="false">?</button>
+                <div class="help-popover" id="lakeHelpPopover" hidden>
+                  <div class="help-popover-title">多机与云端同步规则</div>
+                  <ol class="help-popover-steps">
+                    <li><b>不产生重复:</b>每一行的身份是「日期×账号×维度×门店」,多台电脑写同一行只会覆盖,不会重复。</li>
+                    <li><b>数据新者胜:</b>推送时对比本地取数时间与云端修改时间,旧数据不会覆盖云端新数据(跳过并计数)。</li>
+                    <li><b>全零拦截:</b>大批量指标全零(疑似取数异常)时拒绝推送,防止坏数据污染云端主档案。</li>
+                    <li><b>拉取以云端为准:</b>其他电脑「从云端拉取」后,本地同键行被云端版本覆盖。</li>
+                  </ol>
+                </div>
+              </span>
+            </span>
           </div>
           <div class="table-wrap">
             <table class="data-table">
@@ -374,7 +388,6 @@
               </tbody>
             </table>
           </div>
-          <div class="help">冲突策略:同一行(日期×账号×维度×门店)多机写入时以取数时间新者为准;云端更新的行不会被本机旧数据覆盖。大批量全零数据会被拦截,防止取数异常污染云端主档案。</div>
         </div>
 
         </div>
@@ -435,15 +448,6 @@
           </div>
         </div>
 
-        <div class="stats-grid" id="reportKpis"></div>
-
-        <div class="panel panel-fixed" id="reportTrendPanel">
-          <div class="panel-title">
-            <span>主指标趋势</span>
-            <span class="actions"><select id="reportTrendMetric" class="chart-metric-select" title="切换趋势指标"></select></span>
-          </div>
-          <div id="reportTrendChart" class="chart chart-lg"></div>
-        </div>
 
         <div class="panel panel-fixed report-table-panel">
           <div class="panel-title">

+ 18 - 0
public/lake.js

@@ -117,6 +117,24 @@
         const btn = event.target.closest('button[data-tab]');
         if (btn) activateLakeTab(btn.dataset.tab);
       });
+      // 「?」弹窗:与采集页连接帮助同款交互(点按钮开合,点外部关闭)
+      const helpBtn = $('btnLakeHelp');
+      const helpPop = $('lakeHelpPopover');
+      if (helpBtn && helpPop) {
+        helpBtn.addEventListener('click', (event) => {
+          event.stopPropagation();
+          helpPop.hidden = !helpPop.hidden;
+          helpBtn.classList.toggle('active', !helpPop.hidden);
+          helpBtn.setAttribute('aria-expanded', String(!helpPop.hidden));
+        });
+        document.addEventListener('click', (event) => {
+          if (!helpPop.hidden && !event.target.closest('.conn-help-wrap')) {
+            helpPop.hidden = true;
+            helpBtn.classList.remove('active');
+            helpBtn.setAttribute('aria-expanded', 'false');
+          }
+        });
+      }
     }
     activateLakeTab('overview');
   };

+ 4 - 86
public/reports.js

@@ -254,31 +254,6 @@
     };
   }
 
-  // ---------- KPI 卡片(数据采集页) ----------
-
-  function renderKpis() {
-    const wrap = $('reportKpis');
-    wrap.innerHTML = '';
-    const s = state.summary || {};
-    const cards = [
-      { label: '营业额(元)', value: fmtMoney(s.grossAmt), cls: 'accent' },
-      { label: '收入(元)', value: fmtMoney(s.incomeAmt), cls: 'ok' },
-      { label: '有效订单', value: fmtNum(s.validOrderCnt), cls: 'accent' },
-      { label: '曝光人数', value: fmtNum(s.expUv), cls: '' },
-      { label: '下单人数', value: fmtNum(s.orderUserCnt), cls: '' },
-      { label: '进店转化率', value: fmtPct(s.clkExpRate), cls: 'warn', rate: 'clkExpRate' },
-      { label: '客单价(元)', value: fmtMoney(s.perActualPay), cls: 'ok' },
-    ];
-    for (const c of cards) {
-      const div = document.createElement('div');
-      div.className = 'metric ' + (c.cls || '');
-      div.innerHTML = `<span class="metric-body"><span class="metric-label"></span><span class="metric-value ${c.rate ? rateClass(c.rate, s[c.rate]) : ''}"></span></span>`;
-      div.querySelector('.metric-label').textContent = c.label;
-      div.querySelector('.metric-value').textContent = c.value;
-      wrap.appendChild(div);
-    }
-  }
-
   // ---------- 报表表格(数据采集页) ----------
 
   function metricColumns() {
@@ -339,6 +314,8 @@
     const cols = tableColumns();
     const thead = $('reportTableHead');
     const tbody = $('reportTableBody');
+    // 全部字段模式列多且长:列宽随内容、容器横向滚动,避免 60 列挤成一团
+    $('reportTable').classList.toggle('raw-cols', state.rawView);
     $('reportRowCount').textContent = `${state.rows.length} 行`;
     const empty = $('reportEmpty');
     if (!state.rows.length) {
@@ -368,7 +345,7 @@
     const entity = state.entity;
     if (!entity) {
       state.rows = []; state.summary = null;
-      renderTable(); renderKpis();
+      renderTable(); renderPager();
       return;
     }
     if (state.reportAbort) state.reportAbort.abort();
@@ -387,10 +364,8 @@
       state.summary = data.summary || null;
       state.total = state.mode === 'detail' ? (data.total ?? state.rows.length) : state.rows.length;
       state.pages = state.mode === 'detail' ? (data.pages ?? 1) : 1;
-      renderKpis();
       renderTable();
       renderPager();
-      loadTrend();
     } catch (e) {
       if (e.name === 'AbortError') return;
       showToast('报表加载失败:' + e.message, 'error');
@@ -414,7 +389,7 @@
     const from = (state.page - 1) * state.pageSize + 1;
     const to = Math.min(state.total, state.page * state.pageSize);
     const btn = (label, target, disabled) =>
-      `<button type="button" class="pager-btn" data-page="${target}" ${disabled ? 'disabled' : ''}>${label}</button>`;
+      `<button type="button" class="btn btn-ghost btn-small" data-page="${target}" ${disabled ? 'disabled' : ''}>${label}</button>`;
     el.innerHTML = `
       ${btn('‹ 上一页', state.page - 1, state.page <= 1)}
       <span class="pager-info">第 ${state.page} / ${state.pages} 页 · ${from}-${to} 行 · 共 ${state.total} 行</span>
@@ -1269,62 +1244,6 @@
       });
   }
 
-  // ---------- 主指标趋势(明细 tab 顶部,BI 图表层) ----------
-  // 数据独立于表格分页:按日聚合全区间(行数=天数,轻量);懒 init——容器隐藏时 ECharts 拿不到宽高
-
-  let trendChart = null;
-  let trendRows = [];
-
-  function buildTrendMetricOptions() {
-    const sel = $('reportTrendMetric');
-    if (!sel) return;
-    const current = sel.value || 'grossAmt';
-    const cols = metricColumns().filter((c) => ['money', 'int', 'num', 'pct'].includes(c.type));
-    sel.innerHTML = cols.map((c) => `<option value="${c.key}" ${c.key === current ? 'selected' : ''}>${c.label}</option>`).join('');
-  }
-
-  async function loadTrend() {
-    const entity = state.entity;
-    const el = $('reportTrendChart');
-    if (!el || !entity) { trendRows = []; return; }
-    const { from, to } = currentRange();
-    let shops = '';
-    if (state.shopFilter.size) shops = `&shops=${encodeURIComponent(Array.from(state.shopFilter).join(','))}`;
-    try {
-      const data = await jsonFetch(`/api/reports/aggregate?entity=${encodeURIComponent(entity)}&from=${from}&to=${to}&group=date${shops}`);
-      if (state.entity !== entity) return;
-      trendRows = data.rows || [];
-      buildTrendMetricOptions();
-      renderTrend();
-    } catch { /* 趋势图失败不打扰表格 */ }
-  }
-
-  function renderTrend() {
-    const el = $('reportTrendChart');
-    if (!el || !el.offsetWidth) return; // 容器隐藏(tab 未激活):等激活后再画
-    if (!trendRows.length) { if (trendChart) trendChart.clear(); return; }
-    if (!trendChart) {
-      trendChart = echarts.init(el);
-      window.addEventListener('resize', () => { if (trendChart) trendChart.resize(); });
-    }
-    const metric = $('reportTrendMetric').value || 'grossAmt';
-    const col = state.columns.find((c) => c.key === metric) || {};
-    const rows = trendRows.slice().sort((a, b) => (a.date < b.date ? -1 : 1));
-    const isRate = col.type === 'pct';
-    trendChart.setOption({
-      grid: { left: 56, right: 20, top: 24, bottom: 36 },
-      tooltip: { trigger: 'axis' },
-      xAxis: { type: 'category', data: rows.map((r) => String(r.date).replace(/^(\d{4})(\d{2})(\d{2})$/, '$2/$3')) },
-      yAxis: { type: 'value', axisLabel: { formatter: (v) => (isRate ? (v * 100).toFixed(0) + '%' : v >= 10000 ? (v / 10000) + '万' : v) } },
-      series: [{
-        type: 'line', smooth: true, symbolSize: 5, data: rows.map((r) => r[metric]),
-        lineStyle: { width: 2.2 }, areaStyle: { opacity: 0.08 },
-        tooltip: { valueFormatter: (v) => (v == null ? '—' : isRate ? (v * 100).toFixed(2) + '%' : String(v)) },
-      }],
-    }, { notMerge: true });
-    trendChart.resize();
-  }
-
   // ---------- 初始化 ----------
 
   // 数据采集页工具栏:推送当前主体到云端(本地湖 → 钉钉 AI 表格)
@@ -1381,7 +1300,6 @@
         const btn = event.target.closest('button[data-page]');
         if (btn) changePage(Number(btn.dataset.page));
       });
-      $('reportTrendMetric').addEventListener('change', renderTrend);
       try {
         const data = await jsonFetch('/api/reports/columns');
         state.columns = data.columns || [];

+ 3 - 3
public/style.css

@@ -994,8 +994,8 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 /* ── 报表分页条 ───────────────────────────────────────────── */
 .report-pager { display: flex; align-items: center; gap: 10px; padding: 10px 12px 2px; flex-wrap: wrap; }
 .report-pager:empty { display: none; }
-.pager-btn { border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 8px; padding: 4px 12px; font-size: var(--font-size-small); cursor: pointer; transition: border-color 0.15s var(--ease-out); }
-.pager-btn:hover:not(:disabled) { border-color: var(--text-3); }
-.pager-btn:disabled { opacity: 0.4; cursor: default; }
 .pager-info { font-size: var(--font-size-small); color: var(--text-2); }
 .pager-note { color: var(--text-3); margin-left: auto; }
+/* 全部字段模式:列宽随内容自适应,容器横向滚动 */
+.report-table.raw-cols { min-width: max-content; }
+.report-table.raw-cols th, .report-table.raw-cols td { white-space: nowrap; }