'use strict'; /** * 数据报表 · 经营周报导出 * ---------------------------------------------------------------------------- * 参考 DataAgent ReportGeneratorNode 的「自包含 HTML 报告」产物形态: * 单文件、内联 ECharts、离线可打开,适合发群/汇报。 */ const fs = require('fs'); const path = require('path'); const svc = require('./napos-service.js'); const ECHARTS_SRC = path.join(__dirname, 'public', 'vendor', 'echarts.min.js'); function esc(v) { return String(v == null ? '' : v) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function fmtMoney(v) { return v == null ? '—' : Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function fmtInt(v) { return v == null ? '—' : Number(v).toLocaleString('zh-CN', { maximumFractionDigits: 0 }); } function fmtPct(v) { return v == null || v === '' ? '—' : (Number(v) * 100).toFixed(2) + '%'; } function fmtByType(type, v) { if (type === 'money') return fmtMoney(v); if (type === 'int') return fmtInt(v); if (type === 'pct') return fmtPct(v); if (type === 'num') return v == null ? '—' : Number(v).toFixed(2); if (type === 'date') return /^\d{8}$/.test(String(v)) ? `${v.slice(0, 4)}-${v.slice(4, 6)}-${v.slice(6, 8)}` : (v || '—'); return v == null ? '—' : String(v); } function buildReportHtml({ entity, from, to }) { const meta = svc.store.getEntity(entity) || { key: entity, name: entity, shops: [] }; const shopNameById = new Map((meta.shops || []).map((s) => [String(s.shopId), s.shopName])); const daily = svc.queryReport({ entity, from, to, scope: 'shop' }); const aggDate = svc.aggregateReport({ entity, from, to, group: 'date' }); const aggShop = svc.aggregateReport({ entity, from, to, group: 'shop' }); const s = daily.summary || {}; // 图表数据:日趋势(营业额/有效订单 双轴)+ 门店排行 Top15 const trendDates = aggDate.rows.map((r) => fmtByType('date', r.date)); const trendGross = aggDate.rows.map((r) => Number(r.grossAmt) || 0); const trendOrders = aggDate.rows.map((r) => Number(r.validOrderCnt) || 0); const topShops = aggShop.rows.slice(0, 15); const topNames = topShops.map((r) => shopNameById.get(String(r.shopId)) || r.shopName || r.shopId); const topGross = topShops.map((r) => Number(r.grossAmt) || 0); const tableRows = aggShop.rows.map((r, i) => { const name = esc(shopNameById.get(String(r.shopId)) || r.shopName || r.shopId); return `
| # | 门店 | 营业额(元) | 有效订单 | 收入(元) | 曝光人数 | 下单人数 | 进店转化率 | 客单价(元) |
|---|---|---|---|---|---|---|---|---|
| 区间内无数据 | ||||||||