|
|
@@ -282,9 +282,13 @@
|
|
|
body.innerHTML = state.view === 'cards' ? renderCards(state.data) : renderTables(state.data);
|
|
|
}
|
|
|
|
|
|
- function formatCell(shop, metric, date) {
|
|
|
+ function cellValue(shop, metric, date) {
|
|
|
const series = shop.series[metric] || {};
|
|
|
- const value = Object.prototype.hasOwnProperty.call(series, date) ? series[date] : null;
|
|
|
+ return Object.prototype.hasOwnProperty.call(series, date) ? series[date] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ function metricCell(shop, metric, date) {
|
|
|
+ const value = cellValue(shop, metric, date);
|
|
|
const quality = (shop.quality || {})[date];
|
|
|
const warn = quality && quality.length
|
|
|
? `<span class="ops-warn" title="${esc('解析标记:' + quality.join('、'))}">⚠</span>`
|
|
|
@@ -292,59 +296,80 @@
|
|
|
return `<td class="ops-num${value == null ? ' ops-null' : ''}">${fmtValue(value, metric)}${warn}</td>`;
|
|
|
}
|
|
|
|
|
|
- function subtotalRow(group, metric, dates) {
|
|
|
- const cells = dates.map((date) => {
|
|
|
- if (metric === 'click_rate') {
|
|
|
- let weight = 0;
|
|
|
- let acc = 0;
|
|
|
- for (const shop of group.shops) {
|
|
|
- const exposure = (shop.series.exposure || {})[date];
|
|
|
- const rate = (shop.series.click_rate || {})[date];
|
|
|
- if (typeof exposure === 'number' && exposure > 0 && typeof rate === 'number') {
|
|
|
- weight += exposure;
|
|
|
- acc += rate * exposure;
|
|
|
- }
|
|
|
- }
|
|
|
- return `<td class="ops-num ops-subtotal">${weight > 0 ? fmtValue(acc / weight, metric) : '–'}</td>`;
|
|
|
- }
|
|
|
- let sum = 0;
|
|
|
- let seen = false;
|
|
|
+ /** 学校小计:数量/金额求和,转化率按曝光加权 */
|
|
|
+ function subtotalValue(group, metric, date) {
|
|
|
+ if (metric === 'click_rate') {
|
|
|
+ let weight = 0;
|
|
|
+ let acc = 0;
|
|
|
for (const shop of group.shops) {
|
|
|
- const v = (shop.series[metric] || {})[date];
|
|
|
- if (typeof v === 'number') { sum += v; seen = true; }
|
|
|
+ const exposure = cellValue(shop, 'exposure', date);
|
|
|
+ const rate = cellValue(shop, 'click_rate', date);
|
|
|
+ if (typeof exposure === 'number' && exposure > 0 && typeof rate === 'number') {
|
|
|
+ weight += exposure;
|
|
|
+ acc += rate * exposure;
|
|
|
+ }
|
|
|
}
|
|
|
- return `<td class="ops-num ops-subtotal">${seen ? fmtValue(sum, metric) : '–'}</td>`;
|
|
|
- }).join('');
|
|
|
- return `<tr class="ops-subtotal-row"><td class="ops-shop-cell">${esc(group.school)} · 小计</td>${cells}</tr>`;
|
|
|
+ return weight > 0 ? acc / weight : null;
|
|
|
+ }
|
|
|
+ let sum = 0;
|
|
|
+ let seen = false;
|
|
|
+ for (const shop of group.shops) {
|
|
|
+ const v = cellValue(shop, metric, date);
|
|
|
+ if (typeof v === 'number') { sum += v; seen = true; }
|
|
|
+ }
|
|
|
+ return seen ? sum : null;
|
|
|
}
|
|
|
|
|
|
function renderTables(data) {
|
|
|
- const dates = data.dates || [];
|
|
|
const metrics = state.metrics.filter((m) => META[m]);
|
|
|
if (!metrics.length) return '<div class="empty-hint"><p>请至少选择一个指标</p></div>';
|
|
|
- return metrics.map((metric) => {
|
|
|
- const head = `<tr><th class="ops-shop-col">店铺</th>${dates.map((d) => `<th class="ops-date-col">${shortDate(d)}</th>`).join('')}</tr>`;
|
|
|
- const rows = data.groups.map((group) => {
|
|
|
- const schoolRow = `<tr class="ops-school-row"><td class="ops-shop-cell" colspan="${dates.length + 1}">${esc(group.school)}<em>${group.shops.length} 家</em></td></tr>`;
|
|
|
- const shopRows = group.shops.map((shop) => `<tr>
|
|
|
- <td class="ops-shop-cell" title="${esc(shop.shopName)}">${esc(shop.shopName)}${
|
|
|
+ const dates = data.dates || [];
|
|
|
+ const colCount = metrics.length + 2;
|
|
|
+
|
|
|
+ const head = `<tr>
|
|
|
+ <th class="ops-shop-col">店铺</th>
|
|
|
+ <th class="ops-date-col">日期</th>
|
|
|
+ ${metrics.map((m) => `<th class="ops-metric-col">${META[m].label}</th>`).join('')}
|
|
|
+ </tr>`;
|
|
|
+
|
|
|
+ const body = data.groups.map((group) => {
|
|
|
+ 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 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>
|
|
|
+ ${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>
|
|
|
- ${dates.map((d) => formatCell(shop, metric, d)).join('')}
|
|
|
+ }</td>` : ''}
|
|
|
+ <td class="ops-date-cell">${shortDate(date)}</td>
|
|
|
+ ${metrics.map((m) => metricCell(shop, m, date)).join('')}
|
|
|
</tr>`).join('');
|
|
|
- return schoolRow + shopRows + subtotalRow(group, metric, dates);
|
|
|
}).join('');
|
|
|
- const shopCount = data.groups.reduce((n, g) => n + g.shops.length, 0);
|
|
|
- return `<section class="ops-block">
|
|
|
- <div class="ops-block-head"><h2>${META[metric].label}</h2><span>${shopCount} 家门店 · ${dates.length} 天</span></div>
|
|
|
- <div class="ops-table-wrap">
|
|
|
- <table class="ops-table">
|
|
|
- <thead>${head}</thead>
|
|
|
- <tbody>${rows}</tbody>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
- </section>`;
|
|
|
+
|
|
|
+ const subtotalDates = dates.filter((d) => group.shops.some((shop) => metrics.some((m) => cellValue(shop, m, d) !== null)));
|
|
|
+ const subtotalRows = subtotalDates.map((date) => `<tr class="ops-subtotal-row">
|
|
|
+ <td class="ops-subtotal-label" colspan="2">${esc(group.school)} · 小计 · ${shortDate(date)}</td>
|
|
|
+ ${metrics.map((m) => {
|
|
|
+ const v = subtotalValue(group, m, date);
|
|
|
+ return `<td class="ops-num ops-subtotal">${v == null ? '–' : fmtValue(v, m)}</td>`;
|
|
|
+ }).join('')}
|
|
|
+ </tr>`).join('');
|
|
|
+
|
|
|
+ return schoolRow + shopRows + subtotalRows;
|
|
|
}).join('');
|
|
|
+
|
|
|
+ const shopCount = data.groups.reduce((n, g) => n + g.shops.length, 0);
|
|
|
+ return `<section class="ops-block">
|
|
|
+ <div class="ops-block-head"><h2>运营明细</h2><span>${shopCount} 家门店 · ${dates.length} 天 · ${metrics.length} 个指标</span></div>
|
|
|
+ <div class="ops-table-wrap">
|
|
|
+ <table class="ops-table ops-table-wide">
|
|
|
+ <thead>${head}</thead>
|
|
|
+ <tbody>${body}</tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </section>`;
|
|
|
}
|
|
|
|
|
|
function sparkline(shop, metric, dates) {
|