Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.
Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)
Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
(dropped 3 stale entries, added the 17 missing ones)
Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
when it is empty
Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.
Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
408 lines
17 KiB
JavaScript
408 lines
17 KiB
JavaScript
/**
|
||
* 支付中心概览:区间筛选 + 指标卡 + 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 = '<span class="jio-po-rank' + (i < 3 ? ' is-top' : '') + '"></span>';
|
||
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 = '<span class="jio-po-share"><i></i></span><span></span>';
|
||
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 + '<br>' + 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();
|
||
})();
|