/**
* 支付中心概览:区间筛选 + 指标卡 + ECharts 图表(成交趋势 / 分类成交 / 支付方式)+ 转化与退款 + 热销排行。
*
* 首屏报表来自页面内 #aksrt-pay-overview-data(PayOverview 输出),之后切换区间 / 分类请求
* GET aksrt/v1/admin/pay-overview(PayOverviewController)。ECharts 子集由
* assets/js/runtime/libs/echarts-pay.min.js 提供(window.echarts)。仅本页入队。
*/
(function () {
'use strict';
var root = document.querySelector('.aksrt-po-page');
var dataEl = document.getElementById('aksrt-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-aksrt-po-body]');
if (body) body.setAttribute('aria-busy', busy ? 'true' : 'false');
}
function showError(msg) {
var box = root.querySelector('[data-aksrt-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 = 'aksrt-po-delta ' + cls;
el.textContent = i18n.basis + ' ' + d.text;
});
var label = root.querySelector('[data-aksrt-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-aksrt-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 = 'aksrt-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 = 'aksrt-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 = 'aksrt-po-delta is-flat';
deltaEl.textContent = '';
}
});
}
function renderTop(rep) {
var body = root.querySelector('[data-aksrt-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 = 'aksrt-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-aksrt-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-aksrt-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-aksrt-po-presets]');
var form = root.querySelector('[data-aksrt-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();
})();