/** * 支付中心概览:区间筛选 + 指标卡 + ECharts 图表(成交趋势 / 分类成交 / 支付方式)+ 转化与退款 + 热销排行。 * * 首屏报表来自页面内 #jio-pay-overview-data(PayOverview 输出),之后切换区间 / 分类请求 * GET jio/v1/admin/pay-overview(PayOverviewController)。ECharts 子集由 * assets/js/runtime/libs/echarts-pay.min.js 提供(window.echarts)。仅本页入队。 */ (function () { 'use strict'; var root = document.querySelector('.jio-po-page'); var dataEl = document.getElementById('jio-pay-overview-data'); if (!root || !dataEl || !window.echarts) return; var boot; try { boot = JSON.parse(dataEl.textContent || '{}'); } catch (e) { return; } var echarts = window.echarts; var i18n = boot.i18n || {}; var currency = boot.currency || ''; var COLORS = { paid: '#18a058', net: '#2080f0', refund: '#d03050', count: '#f0a020' }; var PALETTE = ['#18a058', '#2080f0', '#f0a020', '#8a63d2', '#86909c', '#13c2c2', '#eb2f96']; var TEXT = '#4e5969', MUTED = '#86909c', LINE = '#f0f1f4'; var state = { from: boot.initial.from, to: boot.initial.to, group: boot.initial.group || '', report: boot.initial.report }; var charts = {}; var reqSeq = 0; /* ---------------- 格式化 ---------------- */ function money(v) { return currency + Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function int(v) { return Number(v || 0).toLocaleString(); } function compact(v) { var n = Math.abs(v); if (n >= 1e8) return (v / 1e8).toFixed(1).replace(/\.0$/, '') + '亿'; if (n >= 1e4) return (v / 1e4).toFixed(1).replace(/\.0$/, '') + '万'; return String(Math.round(v * 100) / 100); } function pct(v) { return v === null || v === undefined ? '—' : Number(v).toFixed(1) + '%'; } function shortLabel(label) { return label.length === 10 ? label.slice(5) : label; } /** 环比:基期为 0 时,本期有数为「新增」,否则不可比。 */ function deltaInfo(cur, prev) { cur = Number(cur || 0); prev = Number(prev || 0); if (prev <= 0) return cur > 0 ? { cls: 'is-up', text: i18n.new } : { cls: 'is-flat', text: '—' }; var d = (cur - prev) / prev * 100; var cls = d > 0.05 ? 'is-up' : (d < -0.05 ? 'is-down' : 'is-flat'); return { cls: cls, text: (d > 0.05 ? '↑' : (d < -0.05 ? '↓' : '')) + Math.abs(d).toFixed(1) + '%' }; } /* ---------------- 日期 ---------------- */ function parse(s) { var p = s.split('-'); return new Date(Date.UTC(+p[0], +p[1] - 1, +p[2])); } function fmt(d) { return d.toISOString().slice(0, 10); } function shift(s, days) { var d = parse(s); d.setUTCDate(d.getUTCDate() + days); return fmt(d); } function spanDays(a, b) { return Math.round((parse(b) - parse(a)) / 864e5) + 1; } function presetRange(key) { var today = boot.today; if (key === 'this_month') return [today.slice(0, 8) + '01', today]; if (key === 'last_month') { var first = parse(today.slice(0, 8) + '01'); var lastEnd = new Date(first.getTime() - 864e5); return [fmt(lastEnd).slice(0, 8) + '01', fmt(lastEnd)]; } var n = parseInt(key.slice(1), 10); return [shift(today, -(n - 1)), today]; } /* ---------------- 数据加载 ---------------- */ function setBusy(busy) { root.classList.toggle('is-loading', busy); var body = root.querySelector('[data-jio-po-body]'); if (body) body.setAttribute('aria-busy', busy ? 'true' : 'false'); } function showError(msg) { var box = root.querySelector('[data-jio-po-error]'); if (!box) return; box.hidden = !msg; box.querySelector('p').textContent = msg || ''; } function load(from, to, group) { if (spanDays(from, to) < 1 || spanDays(from, to) > boot.maxDays || to > boot.today) { showError(i18n.badRange); return; } var seq = ++reqSeq; var url = boot.rest.url + (boot.rest.url.indexOf('?') === -1 ? '?' : '&') + 'from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(to) + '&group=' + encodeURIComponent(group); setBusy(true); fetch(url, { credentials: 'same-origin', headers: { 'X-WP-Nonce': boot.rest.nonce } }) .then(function (res) { return res.json().then(function (json) { return { ok: res.ok, json: json }; }); }) .then(function (r) { if (seq !== reqSeq) return; if (!r.ok || !r.json || !r.json.success) throw new Error((r.json && r.json.message) || i18n.loadError); state = { from: from, to: to, group: group, report: r.json.data }; showError(''); renderAll(); }) .catch(function (err) { if (seq === reqSeq) showError(err && err.message ? err.message : i18n.loadError); }) .then(function () { if (seq === reqSeq) setBusy(false); }); } /* ---------------- 指标卡 / 转化 / 排行 / 提示 ---------------- */ function renderKpis(rep) { var t = rep.totals || {}, p = rep.previous || {}; var defs = { paid_amount: [money, function () { return int(t.paid_count) + ' ' + i18n.unit; }], net_amount: [money, function () { return i18n.refund + ' ' + money(t.refund_amount); }], paid_count: [int, function () { return ''; }], buyers: [int, function () { return ''; }], avg_amount: [money, function () { return ''; }], refund_amount: [money, function () { return int(t.refund_count) + ' ' + i18n.unit; }], }; root.querySelectorAll('[data-kpi]').forEach(function (card) { var key = card.getAttribute('data-kpi'), def = defs[key]; if (!def) return; card.querySelector('[data-kpi-value]').textContent = def[0](t[key]); card.querySelector('[data-kpi-meta]').textContent = def[1](); var d = deltaInfo(t[key], p[key]); var el = card.querySelector('[data-kpi-delta]'); // 退款上升是坏事:颜色反转。 var cls = key === 'refund_amount' ? (d.cls === 'is-up' ? 'is-down' : (d.cls === 'is-down' ? 'is-up' : d.cls)) : d.cls; el.className = 'jio-po-delta ' + cls; el.textContent = i18n.basis + ' ' + d.text; }); var label = root.querySelector('[data-jio-po-range-label]'); if (label && rep.range) label.textContent = rep.range.from + ' ~ ' + rep.range.to + '(' + rep.range.days + ' 天)'; } function renderFunnel(rep) { var t = rep.totals || {}, p = rep.previous || {}; root.querySelectorAll('[data-jio-po-funnel] [data-metric]').forEach(function (li) { var key = li.getAttribute('data-metric'); var isCount = key === 'created_count'; li.querySelector('[data-value]').textContent = isCount ? int(t[key]) : pct(t[key]); var bar = li.querySelector('[data-bar]'); bar.style.width = isCount ? '100%' : Math.max(0, Math.min(100, Number(t[key] || 0))) + '%'; var deltaEl = li.querySelector('[data-delta]'); if (isCount) { var d = deltaInfo(t[key], p[key]); deltaEl.className = 'jio-po-delta ' + d.cls; deltaEl.textContent = i18n.basis + ' ' + d.text; } else if (p[key] !== null && p[key] !== undefined && t[key] !== null && t[key] !== undefined) { var diff = Number(t[key]) - Number(p[key]); var good = key === 'conversion' ? diff >= 0 : diff <= 0; deltaEl.className = 'jio-po-delta ' + (Math.abs(diff) < 0.05 ? 'is-flat' : (good ? 'is-up' : 'is-down')); deltaEl.textContent = i18n.basis + ' ' + (diff > 0 ? '+' : '') + diff.toFixed(1) + 'pt'; } else { deltaEl.className = 'jio-po-delta is-flat'; deltaEl.textContent = ''; } }); } function renderTop(rep) { var body = root.querySelector('[data-jio-po-top]'); if (!body) return; body.textContent = ''; var rows = rep.top || []; if (!rows.length) { var tr = body.insertRow(), td = tr.insertCell(); td.colSpan = 5; td.className = 'jio-po-top__empty'; td.textContent = i18n.noTop; return; } var total = rows.reduce(function (s, r) { return s + Number(r.amount || 0); }, 0) || 1; rows.forEach(function (r, i) { var tr = body.insertRow(); var rank = tr.insertCell(); rank.className = 'column-rank'; rank.innerHTML = ''; rank.firstChild.textContent = String(i + 1); var title = tr.insertCell(); if (r.url) { var a = document.createElement('a'); a.href = r.url; a.textContent = r.title; title.appendChild(a); } else { title.textContent = r.title; } var count = tr.insertCell(); count.className = 'column-num'; count.textContent = int(r.count); var amount = tr.insertCell(); amount.className = 'column-num'; amount.textContent = money(r.amount); var share = tr.insertCell(); share.className = 'column-share'; var ratio = Number(r.amount || 0) / total * 100; share.innerHTML = ''; share.querySelector('i').style.width = ratio.toFixed(1) + '%'; share.lastChild.textContent = ratio.toFixed(1) + '%'; }); } function renderExcluded(rep) { var box = root.querySelector('[data-jio-po-excluded]'); if (!box) return; var list = rep.excluded || []; box.hidden = !list.length; box.textContent = list.length ? i18n.excluded + list.map(function (x) { return x.currency + ' ' + Number(x.amount).toFixed(2) + '(' + x.count + ' ' + i18n.unit + ')'; }).join(';') : ''; } /* ---------------- 图表 ---------------- */ function chart(key) { var host = root.querySelector('[data-jio-po-chart="' + key + '"]'); if (!host) return null; if (!charts[key]) charts[key] = echarts.init(host, null, { renderer: 'svg' }); return charts[key]; } function baseAxis(labels) { return { type: 'category', boundaryGap: false, data: labels, axisLine: { lineStyle: { color: '#e5e6eb' } }, axisTick: { show: false }, axisLabel: { color: MUTED, hideOverlap: true, formatter: shortLabel }, }; } function valueAxis(formatter, show) { return { type: 'value', show: show !== false, splitLine: { lineStyle: { color: LINE } }, axisLabel: { color: MUTED, formatter: formatter }, minInterval: 1, }; } function tooltip(formatters) { return { trigger: 'axis', confine: true, axisPointer: { type: 'line', lineStyle: { color: '#c9cdd4', type: 'dashed' } }, backgroundColor: '#fff', borderColor: '#e5e6eb', textStyle: { color: TEXT, fontSize: 12 }, extraCssText: 'box-shadow:0 8px 24px rgba(15,23,42,.12);border-radius:8px;', valueFormatter: formatters, }; } function area(color) { return { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: color + '38' }, { offset: 1, color: color + '00' }, ]), }; } function zoom(n) { return n > 45 ? [{ type: 'inside', start: 0, end: 100 }, { type: 'slider', height: 18, bottom: 6, borderColor: 'transparent', fillerColor: 'rgba(24,160,88,.12)', handleSize: '80%', showDetail: false }] : []; } function emptyGraphic(isEmpty) { return isEmpty ? [{ type: 'text', left: 'center', top: 'middle', silent: true, style: { text: i18n.empty, fill: MUTED, fontSize: 13 } }] : []; } function renderTrend(rep) { var c = chart('trend'); if (!c) return; var s = rep.series || { labels: [], paid: [], net: [], refund: [], count: [] }; var n = s.labels.length; var isEmpty = !s.paid.some(function (v) { return v > 0; }); var smoothLine = function (name, data, color, extra) { return Object.assign({ name: name, type: 'line', data: data, smooth: 0.35, showSymbol: false, symbolSize: 6, color: color, lineStyle: { width: 2.25 }, emphasis: { focus: 'series' } }, extra || {}); }; c.setOption({ aria: { enabled: true }, color: [COLORS.paid, COLORS.net, COLORS.refund, COLORS.count], grid: { left: 8, right: 8, top: 40, bottom: n > 45 ? 36 : 8, containLabel: true }, legend: { top: 0, left: 0, icon: 'roundRect', itemWidth: 12, itemHeight: 4, textStyle: { color: TEXT }, selected: { [i18n.refund]: true } }, tooltip: tooltip(function (v) { return typeof v === 'number' && !Number.isInteger(v) ? money(v) : v; }), xAxis: baseAxis(s.labels), yAxis: [valueAxis(compact), valueAxis(function (v) { return v; })], dataZoom: zoom(n), graphic: emptyGraphic(isEmpty), series: [ smoothLine(i18n.paid, s.paid, COLORS.paid, { areaStyle: area(COLORS.paid), tooltip: { valueFormatter: money } }), smoothLine(i18n.net, s.net, COLORS.net, { tooltip: { valueFormatter: money } }), smoothLine(i18n.refund, s.refund, COLORS.refund, { lineStyle: { width: 1.5, type: 'dashed' }, tooltip: { valueFormatter: money } }), { name: i18n.count, type: 'line', yAxisIndex: 1, data: s.count, smooth: false, showSymbol: n <= 31, symbol: 'circle', symbolSize: 5, color: COLORS.count, lineStyle: { width: 1.5 }, itemStyle: { borderColor: '#fff', borderWidth: 1.5 }, tooltip: { valueFormatter: function (v) { return int(v) + ' ' + i18n.unit; } }, }, ], }, true); } function renderGroups(rep) { var c = chart('groups'); if (!c) return; var s = rep.series || { labels: [], groups: [] }; var groups = s.groups || []; var n = s.labels.length; c.setOption({ aria: { enabled: true }, color: PALETTE, grid: { left: 8, right: 8, top: 40, bottom: n > 45 ? 36 : 8, containLabel: true }, legend: { top: 0, left: 0, icon: 'roundRect', itemWidth: 12, itemHeight: 4, textStyle: { color: TEXT } }, tooltip: tooltip(money), xAxis: baseAxis(s.labels), yAxis: valueAxis(compact), dataZoom: zoom(n), graphic: emptyGraphic(!groups.length), series: groups.map(function (g) { return { name: g.name, type: 'line', data: g.data, smooth: 0.35, showSymbol: false, lineStyle: { width: 2 }, emphasis: { focus: 'series' } }; }), }, true); } function renderMethods(rep) { var c = chart('methods'); if (!c) return; var list = rep.methods || []; var total = list.reduce(function (s, m) { return s + Number(m.amount || 0); }, 0); c.setOption({ aria: { enabled: true }, color: PALETTE, tooltip: { trigger: 'item', confine: true, backgroundColor: '#fff', borderColor: '#e5e6eb', textStyle: { color: TEXT, fontSize: 12 }, formatter: function (p) { return p.marker + p.name + '
' + money(p.value) + '(' + p.percent + '%,' + int(p.data.count) + ' ' + i18n.unit + ')'; }, }, legend: { bottom: 0, left: 'center', icon: 'circle', itemWidth: 8, itemHeight: 8, textStyle: { color: TEXT } }, graphic: list.length ? [{ type: 'group', left: 'center', top: '36%', silent: true, children: [ { type: 'text', style: { text: i18n.total, fill: MUTED, fontSize: 12, align: 'center' }, left: 'center', top: 0 }, { type: 'text', style: { text: money(total), fill: '#1f2329', fontSize: 16, fontWeight: 600, align: 'center' }, left: 'center', top: 18 }, ], }] : emptyGraphic(true), series: [{ type: 'pie', radius: ['52%', '72%'], center: ['50%', '44%'], avoidLabelOverlap: true, itemStyle: { borderColor: '#fff', borderWidth: 2, borderRadius: 4 }, label: { show: false }, labelLine: { show: false }, emphasis: { scale: true, scaleSize: 4 }, data: list.map(function (m) { return { name: m.name, value: m.amount, count: m.count }; }), }], }, true); } function renderAll() { var rep = state.report || {}; renderKpis(rep); renderFunnel(rep); renderTop(rep); renderExcluded(rep); renderTrend(rep); renderGroups(rep); renderMethods(rep); } /* ---------------- 交互 ---------------- */ var presets = root.querySelector('[data-jio-po-presets]'); var form = root.querySelector('[data-jio-po-range]'); function syncForm() { if (!form) return; form.elements.from.value = state.from; form.elements.to.value = state.to; form.elements.group.value = state.group; } function pressPreset(btn) { if (!presets) return; presets.querySelectorAll('button').forEach(function (b) { b.setAttribute('aria-pressed', b === btn ? 'true' : 'false'); }); } if (presets) { presets.addEventListener('click', function (e) { var btn = e.target.closest('button[data-preset]'); if (!btn) return; var r = presetRange(btn.getAttribute('data-preset')); pressPreset(btn); state.from = r[0]; state.to = r[1]; syncForm(); load(r[0], r[1], form ? form.elements.group.value : state.group); }); } if (form) { form.addEventListener('submit', function (e) { e.preventDefault(); var from = form.elements.from.value, to = form.elements.to.value; if (!from || !to || from > to) { showError(i18n.badRange); return; } pressPreset(null); load(from, to, form.elements.group.value); }); form.elements.group.addEventListener('change', function () { load(state.from, state.to, form.elements.group.value); }); } var resizeTimer; window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { Object.keys(charts).forEach(function (k) { charts[k].resize(); }); }, 120); }); syncForm(); renderAll(); })();