Jelajahi Sumber

fix: 从驾驶舱跳转报告时丢失店铺/学校筛选

- 「查看完整报告」链接改为携带 schools/shops 参数
- 报告页解析 URL 中的筛选并透传给 /api/ops/report 与保存接口
- 工具条新增「已按 N 家门店筛选」提示与一键清除
- 修复后单店报告口径正确(1 家门店 / 该店 KPI / 单店 AI 描述)
liangzhao 2 minggu lalu
induk
melakukan
2058a069ab
3 mengubah file dengan 45 tambahan dan 2 penghapusan
  1. 9 1
      public/cockpit.js
  2. 33 1
      public/report.js
  3. 3 0
      public/style.css

+ 9 - 1
public/cockpit.js

@@ -283,6 +283,14 @@
 
   // ---------- AI 解读 ----------
 
+  /** 报告链接:带上当前学校/店铺筛选,避免打开后变成全部门店 */
+  function reportLink(from, to) {
+    const params = new URLSearchParams({ view: 'report', from, to });
+    if (state.schools && state.schools.size) params.set('schools', [...state.schools].join(','));
+    if (state.shops && state.shops.size) params.set('shops', [...state.shops].join(','));
+    return '/?' + params.toString();
+  }
+
   async function requestInsight(refresh) {
     const body = $('ckptAiBody');
     const btn = $('ckptAiBtn');
@@ -327,7 +335,7 @@
       const rangeTo = data.range && data.range.to ? String(data.range.to).replace(/-/g, '') : '';
       const foot = [
         data.aiError ? `<span class="ckpt-ai-note">${esc(data.aiError)}</span>` : '',
-        rangeFrom && rangeTo ? `<a class="ckpt-ai-link" href="/?view=report&from=${rangeFrom}&to=${rangeTo}">查看完整报告 →</a>` : '',
+        rangeFrom && rangeTo ? `<a class="ckpt-ai-link" href="${reportLink(rangeFrom, rangeTo)}">查看完整报告 →</a>` : '',
       ].filter(Boolean).join('');
       body.innerHTML = `<ul class="ckpt-ai-list">${points.map((point) => `<li>${esc(point)}</li>`).join('')}</ul>${foot ? `<div class="ckpt-ai-foot">${foot}</div>` : ''}`;
       const time = data.generatedAt ? new Date(data.generatedAt).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) : '';

+ 33 - 1
public/report.js

@@ -13,6 +13,8 @@
     mode: 'text', // text | interactive
     from: '',
     to: '',
+    schools: [],
+    shops: [],
     charts: {},
   };
 
@@ -67,6 +69,7 @@
           <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="rp-tabs" id="rpTabs">
           <button type="button" data-mode="text" class="on">图文报告</button>
           <button type="button" data-mode="interactive">交互报告</button>
@@ -351,7 +354,7 @@
       const { res, body } = await api('/api/ops/reports', {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({ from: state.from, to: state.to }),
+        body: JSON.stringify({ from: state.from, to: state.to, schools: state.schools.join(','), shops: state.shops.join(',') }),
       });
       if (!res.ok) throw new Error((body && (body.detail || body.error)) || ('HTTP ' + res.status));
       btn.textContent = '已保存';
@@ -436,6 +439,8 @@
       const params = new URLSearchParams();
       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(','));
       params.set('ai', '1'); // 默认走 AI,失败自动退回规则版
       const { res, body } = await api('/api/ops/report?' + params.toString());
       if (!res.ok) throw new Error((body && (body.detail || body.error)) || ('HTTP ' + res.status));
@@ -452,16 +457,42 @@
     }
   }
 
+  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;
     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;
     }
     const { body } = await api('/api/ops/summary');
@@ -474,6 +505,7 @@
     state.from = from < coverage.from ? coverage.from : from;
     $('rpFrom').value = toInput(state.from);
     $('rpTo').value = toInput(state.to);
+    renderFilterChip();
   }
 
   window.shanReportInit = function () {

+ 3 - 0
public/style.css

@@ -1467,3 +1467,6 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 .rp-history-kpis { color: var(--text-3); font-size: 11.5px; }
 .rp-col-actions { text-align: right; white-space: nowrap; }
 .rp-col-actions .btn { margin-left: 4px; }
+.rp-filter-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; margin-right: 8px; font-size: 11.5px; color: var(--text-2); background: #fff5ef; border: 1px solid rgba(255,107,53,.25); border-radius: 999px; }
+.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; } }