|
@@ -254,31 +254,6 @@
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ---------- KPI 卡片(数据采集页) ----------
|
|
|
|
|
-
|
|
|
|
|
- function renderKpis() {
|
|
|
|
|
- const wrap = $('reportKpis');
|
|
|
|
|
- wrap.innerHTML = '';
|
|
|
|
|
- const s = state.summary || {};
|
|
|
|
|
- const cards = [
|
|
|
|
|
- { label: '营业额(元)', value: fmtMoney(s.grossAmt), cls: 'accent' },
|
|
|
|
|
- { label: '收入(元)', value: fmtMoney(s.incomeAmt), cls: 'ok' },
|
|
|
|
|
- { label: '有效订单', value: fmtNum(s.validOrderCnt), cls: 'accent' },
|
|
|
|
|
- { label: '曝光人数', value: fmtNum(s.expUv), cls: '' },
|
|
|
|
|
- { label: '下单人数', value: fmtNum(s.orderUserCnt), cls: '' },
|
|
|
|
|
- { label: '进店转化率', value: fmtPct(s.clkExpRate), cls: 'warn', rate: 'clkExpRate' },
|
|
|
|
|
- { label: '客单价(元)', value: fmtMoney(s.perActualPay), cls: 'ok' },
|
|
|
|
|
- ];
|
|
|
|
|
- for (const c of cards) {
|
|
|
|
|
- const div = document.createElement('div');
|
|
|
|
|
- div.className = 'metric ' + (c.cls || '');
|
|
|
|
|
- div.innerHTML = `<span class="metric-body"><span class="metric-label"></span><span class="metric-value ${c.rate ? rateClass(c.rate, s[c.rate]) : ''}"></span></span>`;
|
|
|
|
|
- div.querySelector('.metric-label').textContent = c.label;
|
|
|
|
|
- div.querySelector('.metric-value').textContent = c.value;
|
|
|
|
|
- wrap.appendChild(div);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// ---------- 报表表格(数据采集页) ----------
|
|
// ---------- 报表表格(数据采集页) ----------
|
|
|
|
|
|
|
|
function metricColumns() {
|
|
function metricColumns() {
|
|
@@ -339,6 +314,8 @@
|
|
|
const cols = tableColumns();
|
|
const cols = tableColumns();
|
|
|
const thead = $('reportTableHead');
|
|
const thead = $('reportTableHead');
|
|
|
const tbody = $('reportTableBody');
|
|
const tbody = $('reportTableBody');
|
|
|
|
|
+ // 全部字段模式列多且长:列宽随内容、容器横向滚动,避免 60 列挤成一团
|
|
|
|
|
+ $('reportTable').classList.toggle('raw-cols', state.rawView);
|
|
|
$('reportRowCount').textContent = `${state.rows.length} 行`;
|
|
$('reportRowCount').textContent = `${state.rows.length} 行`;
|
|
|
const empty = $('reportEmpty');
|
|
const empty = $('reportEmpty');
|
|
|
if (!state.rows.length) {
|
|
if (!state.rows.length) {
|
|
@@ -368,7 +345,7 @@
|
|
|
const entity = state.entity;
|
|
const entity = state.entity;
|
|
|
if (!entity) {
|
|
if (!entity) {
|
|
|
state.rows = []; state.summary = null;
|
|
state.rows = []; state.summary = null;
|
|
|
- renderTable(); renderKpis();
|
|
|
|
|
|
|
+ renderTable(); renderPager();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
if (state.reportAbort) state.reportAbort.abort();
|
|
if (state.reportAbort) state.reportAbort.abort();
|
|
@@ -387,10 +364,8 @@
|
|
|
state.summary = data.summary || null;
|
|
state.summary = data.summary || null;
|
|
|
state.total = state.mode === 'detail' ? (data.total ?? state.rows.length) : state.rows.length;
|
|
state.total = state.mode === 'detail' ? (data.total ?? state.rows.length) : state.rows.length;
|
|
|
state.pages = state.mode === 'detail' ? (data.pages ?? 1) : 1;
|
|
state.pages = state.mode === 'detail' ? (data.pages ?? 1) : 1;
|
|
|
- renderKpis();
|
|
|
|
|
renderTable();
|
|
renderTable();
|
|
|
renderPager();
|
|
renderPager();
|
|
|
- loadTrend();
|
|
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
if (e.name === 'AbortError') return;
|
|
if (e.name === 'AbortError') return;
|
|
|
showToast('报表加载失败:' + e.message, 'error');
|
|
showToast('报表加载失败:' + e.message, 'error');
|
|
@@ -414,7 +389,7 @@
|
|
|
const from = (state.page - 1) * state.pageSize + 1;
|
|
const from = (state.page - 1) * state.pageSize + 1;
|
|
|
const to = Math.min(state.total, state.page * state.pageSize);
|
|
const to = Math.min(state.total, state.page * state.pageSize);
|
|
|
const btn = (label, target, disabled) =>
|
|
const btn = (label, target, disabled) =>
|
|
|
- `<button type="button" class="pager-btn" data-page="${target}" ${disabled ? 'disabled' : ''}>${label}</button>`;
|
|
|
|
|
|
|
+ `<button type="button" class="btn btn-ghost btn-small" data-page="${target}" ${disabled ? 'disabled' : ''}>${label}</button>`;
|
|
|
el.innerHTML = `
|
|
el.innerHTML = `
|
|
|
${btn('‹ 上一页', state.page - 1, state.page <= 1)}
|
|
${btn('‹ 上一页', state.page - 1, state.page <= 1)}
|
|
|
<span class="pager-info">第 ${state.page} / ${state.pages} 页 · ${from}-${to} 行 · 共 ${state.total} 行</span>
|
|
<span class="pager-info">第 ${state.page} / ${state.pages} 页 · ${from}-${to} 行 · 共 ${state.total} 行</span>
|
|
@@ -1269,62 +1244,6 @@
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ---------- 主指标趋势(明细 tab 顶部,BI 图表层) ----------
|
|
|
|
|
- // 数据独立于表格分页:按日聚合全区间(行数=天数,轻量);懒 init——容器隐藏时 ECharts 拿不到宽高
|
|
|
|
|
-
|
|
|
|
|
- let trendChart = null;
|
|
|
|
|
- let trendRows = [];
|
|
|
|
|
-
|
|
|
|
|
- function buildTrendMetricOptions() {
|
|
|
|
|
- const sel = $('reportTrendMetric');
|
|
|
|
|
- if (!sel) return;
|
|
|
|
|
- const current = sel.value || 'grossAmt';
|
|
|
|
|
- const cols = metricColumns().filter((c) => ['money', 'int', 'num', 'pct'].includes(c.type));
|
|
|
|
|
- sel.innerHTML = cols.map((c) => `<option value="${c.key}" ${c.key === current ? 'selected' : ''}>${c.label}</option>`).join('');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function loadTrend() {
|
|
|
|
|
- const entity = state.entity;
|
|
|
|
|
- const el = $('reportTrendChart');
|
|
|
|
|
- if (!el || !entity) { trendRows = []; return; }
|
|
|
|
|
- const { from, to } = currentRange();
|
|
|
|
|
- let shops = '';
|
|
|
|
|
- if (state.shopFilter.size) shops = `&shops=${encodeURIComponent(Array.from(state.shopFilter).join(','))}`;
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await jsonFetch(`/api/reports/aggregate?entity=${encodeURIComponent(entity)}&from=${from}&to=${to}&group=date${shops}`);
|
|
|
|
|
- if (state.entity !== entity) return;
|
|
|
|
|
- trendRows = data.rows || [];
|
|
|
|
|
- buildTrendMetricOptions();
|
|
|
|
|
- renderTrend();
|
|
|
|
|
- } catch { /* 趋势图失败不打扰表格 */ }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function renderTrend() {
|
|
|
|
|
- const el = $('reportTrendChart');
|
|
|
|
|
- if (!el || !el.offsetWidth) return; // 容器隐藏(tab 未激活):等激活后再画
|
|
|
|
|
- if (!trendRows.length) { if (trendChart) trendChart.clear(); return; }
|
|
|
|
|
- if (!trendChart) {
|
|
|
|
|
- trendChart = echarts.init(el);
|
|
|
|
|
- window.addEventListener('resize', () => { if (trendChart) trendChart.resize(); });
|
|
|
|
|
- }
|
|
|
|
|
- const metric = $('reportTrendMetric').value || 'grossAmt';
|
|
|
|
|
- const col = state.columns.find((c) => c.key === metric) || {};
|
|
|
|
|
- const rows = trendRows.slice().sort((a, b) => (a.date < b.date ? -1 : 1));
|
|
|
|
|
- const isRate = col.type === 'pct';
|
|
|
|
|
- trendChart.setOption({
|
|
|
|
|
- grid: { left: 56, right: 20, top: 24, bottom: 36 },
|
|
|
|
|
- tooltip: { trigger: 'axis' },
|
|
|
|
|
- xAxis: { type: 'category', data: rows.map((r) => String(r.date).replace(/^(\d{4})(\d{2})(\d{2})$/, '$2/$3')) },
|
|
|
|
|
- yAxis: { type: 'value', axisLabel: { formatter: (v) => (isRate ? (v * 100).toFixed(0) + '%' : v >= 10000 ? (v / 10000) + '万' : v) } },
|
|
|
|
|
- series: [{
|
|
|
|
|
- type: 'line', smooth: true, symbolSize: 5, data: rows.map((r) => r[metric]),
|
|
|
|
|
- lineStyle: { width: 2.2 }, areaStyle: { opacity: 0.08 },
|
|
|
|
|
- tooltip: { valueFormatter: (v) => (v == null ? '—' : isRate ? (v * 100).toFixed(2) + '%' : String(v)) },
|
|
|
|
|
- }],
|
|
|
|
|
- }, { notMerge: true });
|
|
|
|
|
- trendChart.resize();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// ---------- 初始化 ----------
|
|
// ---------- 初始化 ----------
|
|
|
|
|
|
|
|
// 数据采集页工具栏:推送当前主体到云端(本地湖 → 钉钉 AI 表格)
|
|
// 数据采集页工具栏:推送当前主体到云端(本地湖 → 钉钉 AI 表格)
|
|
@@ -1381,7 +1300,6 @@
|
|
|
const btn = event.target.closest('button[data-page]');
|
|
const btn = event.target.closest('button[data-page]');
|
|
|
if (btn) changePage(Number(btn.dataset.page));
|
|
if (btn) changePage(Number(btn.dataset.page));
|
|
|
});
|
|
});
|
|
|
- $('reportTrendMetric').addEventListener('change', renderTrend);
|
|
|
|
|
try {
|
|
try {
|
|
|
const data = await jsonFetch('/api/reports/columns');
|
|
const data = await jsonFetch('/api/reports/columns');
|
|
|
state.columns = data.columns || [];
|
|
state.columns = data.columns || [];
|