Kaynağa Gözat

refactor: 移除经营报告的「图文/交互」双 tab

两个 tab 渲染的是同一份内容和同一组图表,只有图表是否响应悬浮的差别,
容易误导用户以为有两份报告。现在只保留一个视图,图表默认支持悬浮提示,
打印/PDF 样式不变。
liangzhao 2 hafta önce
ebeveyn
işleme
d4d1330d3c
4 değiştirilmiş dosya ile 10 ekleme ve 28 silme
  1. 1 1
      README.md
  2. 1 1
      public/index.html
  3. 8 22
      public/report.js
  4. 0 4
      public/style.css

+ 1 - 1
README.md

@@ -56,7 +56,7 @@
 - **报告中心**:报告页可「保存报告」,历史列表显示保存时间、区间、学校、店铺范围与来源(AI/规则),支持查看、下载 PDF(浏览器打印)、下载 JSON 与删除;保存的是完整 Report JSON 快照(含 AI 文字与当时数值),数据后续变化不影响已保存版本。
 - **解读与报告同源**:驾驶舱的「经营解读」是经营报告 JSON 的摘要视图(同一份生成结果、同一缓存),卡片上可一键跳到同区间的完整报告;报告页「重新生成」会调用 AI 补齐文字,模型不可用时自动退回规则版并标注。
 - **AI 经营解读**:点「生成解读」,把当前范围的 KPI、趋势、门店增减、漏斗与成本结构整理成上下文交给对话模型,输出 3~5 条结论与建议(引用具体数字);结果缓存 10 分钟,可复制。未配置对话模型时会明确提示去「系统设置 → API 设置」配置。
-- **经营报告**:按统一 Report JSON 渲染「图文报告 / 交互报告」两版,支持选择日期区间、重新生成、浏览器打印导出 PDF(A4 打印样式已适配)。报告含管理层摘要(带证据)、核心指标、经营/门店/流量/费用四节图表、风险与行动计划、数据质量与方法论。
+- **经营报告**:按统一 Report JSON 渲染,图表可交互(悬浮提示),支持日期/学校/店铺筛选、重新生成、浏览器打印导出 PDF(A4 打印样式已适配)。报告含管理层摘要(带证据)、核心指标、经营/门店/流量/费用四节图表、风险与行动计划、数据质量与方法论。
 - **字段与店铺筛选**:字段下拉可勾选平台导出里的任意列(默认 4 个基础指标),勾几个就显示几列;店铺下拉可多选门店,只看选中的店;选择保存在浏览器本地。
 - **学校归属**:平台表里没有学校列时,优先用上传时选的学校,其次从文件名识别(如「浙江工商桐庐校区20261018…」匹配到已登记学校);都匹配不到会要求先选学校。
 - **原件存档**:每次上传的 xlsx 原样入库,可在「导入记录」里下载回;解析规则升级后可「重新解析」而不必重传。

+ 1 - 1
public/index.html

@@ -374,7 +374,7 @@
         <div class="page-header">
           <div>
             <h1>经营报告</h1>
-            <div class="sub">同一份数据生成的图文报告与交互报告,可下载 PDF</div>
+            <div class="sub">按统一口径生成的分析报告,图表可交互,可下载 PDF</div>
           </div>
         </div>
         <div id="reportRoot"></div>

+ 8 - 22
public/report.js

@@ -10,7 +10,6 @@
     inited: false,
     loading: false,
     report: null,
-    mode: 'text', // text | interactive
     from: '',
     to: '',
     schools: [],
@@ -71,10 +70,6 @@
         </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>
-        </div>
         <div class="rp-actions">
           <button type="button" class="btn btn-ghost btn-small" id="rpArchive">历史报告</button>
           <button type="button" class="btn btn-ghost btn-small" id="rpSave" disabled>保存报告</button>
@@ -92,14 +87,6 @@
           <div class="modal-body" id="rpHistoryList"></div>
         </div>
       </div>`;
-    root.querySelectorAll('#rpTabs [data-mode]').forEach((btn) => {
-      btn.addEventListener('click', () => {
-        state.mode = btn.dataset.mode;
-        root.querySelectorAll('#rpTabs [data-mode]').forEach((b) => b.classList.toggle('on', b === btn));
-        $('rpPage').classList.toggle('interactive', state.mode === 'interactive');
-        if (state.report) drawCharts(state.report);
-      });
-    });
     $('rpFrom').addEventListener('change', (e) => { state.from = String(e.target.value || '').replace(/-/g, ''); load(true); });
     $('rpTo').addEventListener('change', (e) => { state.to = String(e.target.value || '').replace(/-/g, ''); load(true); });
     $('rpRefresh').addEventListener('click', () => load(true));
@@ -262,16 +249,15 @@
 
   // ---------- 图表 ----------
 
-  function chartBase(interactive) {
+  function chartBase() {
     return {
-      animation: !interactive,
-      silent: !interactive,
-      tooltip: interactive ? { trigger: 'axis' } : undefined,
+      animation: true,
+      tooltip: { trigger: 'axis' },
     };
   }
 
   function drawCharts(report) {
-    const interactive = state.mode === 'interactive';
+    
     const byId = new Map((report.charts || []).map((c) => [c.id, c]));
     Object.values(state.charts).forEach((chart) => chart && chart.dispose());
     state.charts = {};
@@ -288,7 +274,7 @@
     if (trend) {
       const chart = mount(trend);
       chart && chart.setOption({
-        ...chartBase(interactive),
+        ...chartBase(),
         grid: { left: 56, right: 48, top: 24, bottom: 30 },
         legend: { right: 0, top: 0, itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 11 } },
         xAxis: { type: 'category', data: trend.data.map((d) => d.date.slice(4)), axisTick: { show: false }, axisLine: { lineStyle: { color: '#e5e0da' } } },
@@ -305,7 +291,7 @@
       const rows = rank.data.slice().reverse();
       const chart = mount(rank);
       chart && chart.setOption({
-        ...chartBase(interactive),
+        ...chartBase(),
         grid: { left: 8, right: 60, top: 10, bottom: 10, containLabel: true },
         xAxis: { type: 'value', splitLine: { lineStyle: { color: '#f2efea' } } },
         yAxis: { type: 'category', data: rows.map((r) => r.shopName), axisTick: { show: false }, axisLine: { show: false }, axisLabel: { width: 140, overflow: 'truncate', fontSize: 11 } },
@@ -318,7 +304,7 @@
       const f = funnel.data;
       const chart = mount(funnel);
       chart && chart.setOption({
-        ...chartBase(interactive),
+        ...chartBase(),
         series: [{
           type: 'funnel', left: '10%', right: '10%', top: 12, bottom: 12, minSize: '30%', gap: 3,
           label: { show: true, position: 'inside', color: '#fff', fontSize: 11, formatter: (p) => `${p.name} ${Number(p.value).toLocaleString('zh-CN')}` },
@@ -333,7 +319,7 @@
     if (structure) {
       const chart = mount(structure);
       chart && chart.setOption({
-        ...chartBase(interactive),
+        ...chartBase(),
         grid: { left: 56, right: 20, top: 28, bottom: 30 },
         legend: { right: 0, top: 0, itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 11 } },
         xAxis: { type: 'category', data: structure.data.map((d) => d.date.slice(4)), axisTick: { show: false }, axisLine: { lineStyle: { color: '#e5e0da' } } },

+ 0 - 4
public/style.css

@@ -1383,9 +1383,6 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 
 /* ── 经营报告 ─────────────────────────────────────────── */
 .rp-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
-.rp-tabs { display: inline-flex; padding: 3px; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; }
-.rp-tabs button { padding: 6px 16px; font-size: 12.5px; color: var(--text-2); background: none; border: 0; border-radius: 7px; cursor: pointer; }
-.rp-tabs button.on { background: #fff; color: var(--text); box-shadow: 0 1px 3px rgba(31,27,24,.08); font-weight: 600; }
 .rp-actions { display: flex; gap: 8px; }
 .rp-page { max-width: 1020px; margin: 0 auto; padding: 44px 52px 40px; background: #fff; border: 1px solid var(--border); border-radius: 14px; box-shadow: 0 18px 48px rgba(31,27,24,.06); color: var(--text); }
 .rp-loading { padding: 80px 0; text-align: center; color: var(--text-3); font-size: 13px; }
@@ -1428,7 +1425,6 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 .rp-figure figcaption b { font-size: 12.5px; }
 .rp-figure figcaption span { font-size: 11.5px; color: var(--text-3); }
 .rp-chart { width: 100%; height: 240px; }
-.rp-page.interactive .rp-chart { cursor: crosshair; }
 .rp-table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-top: 8px; }
 .rp-table th { padding: 8px 10px; text-align: left; font-weight: 600; color: var(--text-2); background: var(--bg); border-bottom: 1px solid var(--border); }
 .rp-table td { padding: 8px 10px; border-bottom: 1px solid var(--border-light); }