浏览代码

feat: 经营报告页增加学校/店铺筛选,默认全部 + 近 7 天

- 复用驾驶舱的多选组件,筛选变化即重新生成报告并同步到 URL
- 默认不筛选(全部学校/店铺),日期默认最近 7 天(数据不足 7 天时从首日开始)
- 加载中再次修改筛选会排队重载,不再被 loading 守卫吞掉;加载时页面半透明提示
- 驾驶舱同步修复同类问题(筛选连点时后一次被忽略)
liangzhao 2 周之前
父节点
当前提交
7e0ac64c51
共有 3 个文件被更改,包括 80 次插入 和 43 次删除
  1. 6 1
      public/cockpit.js
  2. 71 42
      public/report.js
  3. 3 0
      public/style.css

+ 6 - 1
public/cockpit.js

@@ -25,6 +25,7 @@
     shops: null,
     charts: {},
     insightPoints: [],
+    pending: false,
   };
 
   function esc(value) {
@@ -352,7 +353,7 @@
   // ---------- 数据 ----------
 
   async function loadData() {
-    if (state.loading) return;
+    if (state.loading) { state.pending = true; return; }
     state.loading = true;
     const params = new URLSearchParams();
     if (state.from) params.set('from', state.from);
@@ -392,6 +393,10 @@
       $('ckptEmpty').querySelector('small').textContent = e.message || String(e);
     } finally {
       state.loading = false;
+      if (state.pending) {
+        state.pending = false;
+        loadData();
+      }
     }
   }
 

+ 71 - 42
public/report.js

@@ -69,7 +69,8 @@
           <span class="ops-tilde">~</span>
           <input type="date" class="ops-input" id="rpTo">
         </div>
-        <div class="rp-filter-chip" id="rpFilterChip" hidden></div>
+        <div class="ops-multi" id="rpSchool"></div>
+        <div class="ops-multi" id="rpShop"></div>
         <div class="rp-tabs" id="rpTabs">
           <button type="button" data-mode="text" class="on">图文报告</button>
           <button type="button" data-mode="interactive">交互报告</button>
@@ -486,9 +487,10 @@
   // ---------- 数据 ----------
 
   async function load(force) {
-    if (state.loading) return;
+    if (state.loading) { state.pending = true; return; }
     state.loading = true;
     const page = $('rpPage');
+    if (page) page.classList.add('rp-stale');
     const saveBtn = $('rpSave');
     if (saveBtn) saveBtn.disabled = true;
     if (page && !state.report) page.innerHTML = '<div class="rp-loading">正在生成报告…</div>';
@@ -511,58 +513,85 @@
       page.innerHTML = `<div class="rp-loading">报告生成失败:${esc(e.message || e)}</div>`;
     } finally {
       state.loading = false;
+      if (page) page.classList.remove('rp-stale');
+      if (state.pending) {
+        state.pending = false;
+        load(true);
+      }
     }
   }
 
-  function renderFilterChip() {
-    const chip = $('rpFilterChip');
-    if (!chip) return;
-    const parts = [];
-    if (state.schools.length) parts.push(state.schools.length === 1 ? state.schools[0] : `${state.schools.length} 个学校`);
-    if (state.shops.length) parts.push(state.shops.length === 1 ? `1 家门店` : `${state.shops.length} 家门店`);
-    if (!parts.length) {
-      chip.hidden = true;
-      chip.innerHTML = '';
-      return;
-    }
-    chip.hidden = false;
-    chip.innerHTML = `已按 ${parts.join(' · ')} 筛选 <button type="button" id="rpFilterClear">清除</button>`;
-    $('rpFilterClear').addEventListener('click', () => {
-      state.schools = [];
-      state.shops = [];
-      renderFilterChip();
-      load(true);
-    });
-  }
-
   async function initDates() {
     const search = new URLSearchParams(location.search);
     const qFrom = String(search.get('from') || '').replace(/\D/g, '');
     const qTo = String(search.get('to') || '').replace(/\D/g, '');
-    const qShops = String(search.get('shops') || '').split(',').map((s) => s.trim()).filter(Boolean);
-    const qSchools = String(search.get('schools') || '').split(',').map((s) => s.trim()).filter(Boolean);
-    state.shops = qShops;
-    state.schools = qSchools;
+    state.shops = String(search.get('shops') || '').split(',').map((s) => s.trim()).filter(Boolean);
+    state.schools = String(search.get('schools') || '').split(',').map((s) => s.trim()).filter(Boolean);
+
+    const { body } = await api('/api/ops/summary');
+    const summary = body || { schools: [], shops: [] };
+    const coverage = summary.coverage;
     const toInput = (ymd) => `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}`;
+
     if (/^\d{8}$/.test(qFrom) && /^\d{8}$/.test(qTo)) {
       state.from = qFrom;
       state.to = qTo;
-      $('rpFrom').value = toInput(qFrom);
-      $('rpTo').value = toInput(qTo);
-      renderFilterChip();
-      return;
+    } else if (coverage) {
+      const to = coverage.to;
+      const d = new Date(Date.UTC(Number(to.slice(0, 4)), Number(to.slice(4, 6)) - 1, Number(to.slice(6, 8)) - 6));
+      const from = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, '0')}${String(d.getUTCDate()).padStart(2, '0')}`;
+      state.to = to;
+      state.from = from < coverage.from ? coverage.from : from;
     }
-    const { body } = await api('/api/ops/summary');
-    const coverage = body && body.coverage;
-    if (!coverage) return;
-    const to = coverage.to;
-    const d = new Date(Date.UTC(Number(to.slice(0, 4)), Number(to.slice(4, 6)) - 1, Number(to.slice(6, 8)) - 6));
-    const from = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, '0')}${String(d.getUTCDate()).padStart(2, '0')}`;
-    state.to = to;
-    state.from = from < coverage.from ? coverage.from : from;
-    $('rpFrom').value = toInput(state.from);
-    $('rpTo').value = toInput(state.to);
-    renderFilterChip();
+    if (state.from && state.to) {
+      $('rpFrom').value = toInput(state.from);
+      $('rpTo').value = toInput(state.to);
+    }
+    mountFilters(summary);
+    syncUrl();
+  }
+
+  function mountFilters(summary) {
+    const schools = (summary.schools || []).map((s) => ({ key: s.name, label: s.name, hint: `${s.shopCount || 0} 家` }));
+    window.createMultiSelect($('rpSchool'), {
+      label: '学校',
+      items: schools,
+      selected: state.schools,
+      allowEmpty: true,
+      emptyLabel: '全部',
+      onChange: (keys) => {
+        state.schools = keys;
+        syncUrl();
+        load(true);
+      },
+    });
+    const shops = (summary.shops || []).map((s) => ({
+      key: String(s.key || s.shopId || ''),
+      label: s.shopName || s.key,
+      hint: s.school || '',
+    })).filter((item) => item.key);
+    window.createMultiSelect($('rpShop'), {
+      label: '店铺',
+      items: shops,
+      selected: state.shops,
+      allowEmpty: true,
+      emptyLabel: '全部',
+      onChange: (keys) => {
+        state.shops = keys;
+        syncUrl();
+        load(true);
+      },
+    });
+    window.bindMultiSelectDismiss();
+  }
+
+  function syncUrl() {
+    const params = new URLSearchParams({ view: 'report' });
+    if (state.from) params.set('from', state.from);
+    if (state.to) params.set('to', state.to);
+    if (state.schools.length) params.set('schools', state.schools.join(','));
+    if (state.shops.length) params.set('shops', state.shops.join(','));
+    history.replaceState(null, '', '/?' + params.toString());
   }
 
   window.shanReportInit = function () {

+ 3 - 0
public/style.css

@@ -1471,3 +1471,6 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 .rp-filter-chip button { padding: 0; font-size: 11.5px; color: var(--accent); background: none; border: 0; cursor: pointer; text-decoration: underline; }
 @media print { .rp-filter-chip { display: none !important; } }
 .rp-muted { color: var(--text-3); }
+.rp-toolbar { flex-wrap: wrap; }
+.rp-toolbar .ops-multi { min-width: 120px; }
+.rp-page.rp-stale { opacity: .55; transition: opacity .15s ease; }