Explorar o código

style: 店铺运营表格按店铺分块显示

- 每个店铺一个色块:斑马底 + 6px 块间距,块首有分隔,不会再分不清第二行属于哪家店
- 店铺名改为顶部对齐,与其第一天的数据同一视觉行
- 悬停高亮整块(同店多天一起亮),日期列同时加深
liangzhao hai 2 semanas
pai
achega
60fcbecf43
Modificáronse 2 ficheiros con 36 adicións e 6 borrados
  1. 24 4
      public/ops.js
  2. 12 2
      public/style.css

+ 24 - 4
public/ops.js

@@ -280,6 +280,7 @@
     }
     if (!state.data) return;
     body.innerHTML = state.view === 'cards' ? renderCards(state.data) : renderTables(state.data);
+    if (state.view === 'table') bindRowHover();
   }
 
   function cellValue(shop, metric, date) {
@@ -332,14 +333,16 @@
       ${metrics.map((m) => `<th class="ops-metric-col">${META[m].label}</th>`).join('')}
     </tr>`;
 
-    const body = data.groups.map((group) => {
+    const body = data.groups.map((group, groupIdx) => {
       const schoolRow = `<tr class="ops-school-row"><td colspan="${colCount}">${esc(group.school)}<em>${group.shops.length} 家</em></td></tr>`;
 
-      const shopRows = group.shops.map((shop) => {
-        // 只展示该店有数据的日期(平台明细可能缺某天)
+      const shopRows = group.shops.map((shop, shopIdx) => {
         const shopDates = dates.filter((d) => metrics.some((m) => cellValue(shop, m, d) !== null));
         const useDates = shopDates.length ? shopDates : dates.slice(0, 1);
-        return useDates.map((date, i) => `<tr>
+        // 每个店铺一个色块与悬停组,日期行共用同一个 block key
+        const blockKey = `b${groupIdx}-${shopIdx}`;
+        const alt = shopIdx % 2 === 1 ? ' ops-block-alt' : '';
+        return useDates.map((date, i) => `<tr data-block="${blockKey}" class="ops-shop-row${i === 0 ? ' ops-block-start' : ''}${alt}">
           ${i === 0 ? `<td class="ops-shop-cell" rowspan="${useDates.length}" title="${esc(shop.shopName)}">${esc(shop.shopName)}${
             shop.status && shop.status !== '已上线' ? `<span class="badge badge-muted ops-status">${esc(shop.status)}</span>` : ''
           }</td>` : ''}
@@ -372,6 +375,23 @@
     </section>`;
   }
 
+  /** 悬停时整块高亮(同一店铺的多天一起亮),避免分不清行属于哪家店 */
+  function bindRowHover() {
+    document.querySelectorAll('.ops-table-wide tbody').forEach((tbody) => {
+      if (tbody.dataset.hoverBound === '1') return;
+      tbody.dataset.hoverBound = '1';
+      const clear = () => tbody.querySelectorAll('tr.ops-hover').forEach((tr) => tr.classList.remove('ops-hover'));
+      tbody.addEventListener('mouseover', (e) => {
+        const tr = e.target.closest('tr[data-block]');
+        if (!tr) return;
+        if (tr.classList.contains('ops-hover')) return;
+        clear();
+        tbody.querySelectorAll(`tr[data-block="${tr.dataset.block}"]`).forEach((row) => row.classList.add('ops-hover'));
+      });
+      tbody.addEventListener('mouseleave', clear);
+    });
+  }
+
   function sparkline(shop, metric, dates) {
     const values = dates.map((d) => {
       const v = (shop.series[metric] || {})[d];

+ 12 - 2
public/style.css

@@ -1209,8 +1209,8 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 .ops-date-col { font-variant-numeric: tabular-nums; min-width: 56px; }
 .ops-shop-col, .ops-shop-cell { position: sticky; left: 0; z-index: 3; min-width: 180px; max-width: 220px; background: var(--card-bg); text-align: left; overflow: hidden; text-overflow: ellipsis; }
 .ops-table thead .ops-shop-col { z-index: 4; background: var(--bg); }
-.ops-table tbody tr:hover td { background: #faf9f7; }
-.ops-table tbody tr:hover .ops-shop-cell { background: #faf9f7; }
+.ops-table:not(.ops-table-wide) tbody tr:hover td { background: #faf9f7; }
+.ops-table:not(.ops-table-wide) tbody tr:hover .ops-shop-cell { background: #faf9f7; }
 .ops-num { text-align: right; font-variant-numeric: tabular-nums; }
 .ops-null { color: var(--text-3); }
 .ops-warn { margin-left: 3px; color: var(--warn); cursor: help; font-size: 10px; }
@@ -1289,3 +1289,13 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 .ops-table-wide .ops-date-cell { text-align: center; color: var(--text-2); font-variant-numeric: tabular-nums; }
 .ops-subtotal-label { color: var(--text-2); font-weight: 600; }
 .ops-table-wide tbody tr.ops-subtotal-row td { border-top: 1px solid var(--border); }
+
+/* 店铺分块:斑马底 + 块间距 + 整块悬停 */
+.ops-table-wide .ops-shop-cell { vertical-align: top; padding-top: 11px; background: var(--card-bg); }
+.ops-table-wide tbody tr.ops-block-alt td { background: #fbf9f6; }
+.ops-table-wide tbody tr.ops-block-alt .ops-shop-cell { background: #f7f4ef; }
+.ops-table-wide tbody tr.ops-block-start td { border-top: 6px solid var(--bg); }
+.ops-table-wide tbody tr.ops-block-start .ops-shop-cell { border-top: 6px solid var(--bg); }
+.ops-table-wide tbody tr.ops-hover td { background: #fff3ec; }
+.ops-table-wide tbody tr.ops-hover .ops-shop-cell { background: #ffe9dc; }
+.ops-table-wide tbody tr.ops-hover .ops-date-cell { color: var(--text); }