Files
saasshop/public/js/admin.js

511 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SaaSShop Admin JS
// 说明:用于增强总台管理的运营交互体验(尽量保持小而可治理)。
// 原则:不引入复杂构建链;以渐进增强为主,页面无 JS 也应可用。
(function () {
if (window.__SAASSHOP_ADMIN_JS__) {
return;
}
window.__SAASSHOP_ADMIN_JS__ = true;
function qs(sel, root) {
return (root || document).querySelector(sel);
}
function formatMoney(v) {
var n = Number(v || 0);
if (!isFinite(n)) {
n = 0;
}
// 统一口径:两位小数 + 千分位
try {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
} catch (e) {
return n.toFixed(2);
}
}
function formatPct(ratio, digits) {
var d = (digits == null) ? 1 : Number(digits);
if (!isFinite(d) || d < 0) {
d = 1;
}
var r = Number(ratio || 0);
if (!isFinite(r)) {
r = 0;
}
return (r * 100).toFixed(d);
}
// 续费缺订阅治理:绑定成功后自动滚动到顶部提示区(让运营立刻看到 success/warning/error
// 说明:由后端 redirect url 追加 attached_subscription=1 触发。
if (window.location && window.location.search && window.location.search.indexOf('attached_subscription=1') >= 0) {
try {
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (e) {
window.scrollTo(0, 0);
}
}
// 平台订单详情页:当从仪表盘/列表跳转到某个治理锚点(例如 #add-payment-receipt自动展开对应 <details>。
// 说明:避免“跳过去但面板是折叠的”,造成运营以为没跳到。
(function () {
if (!window.location || !window.location.hash) {
return;
}
var h = String(window.location.hash || '');
var map = {
'#add-payment-receipt': 'details#add-payment-receipt',
'#add-refund-receipt': 'details#add-refund-receipt',
};
if (map[h]) {
var d = qs(map[h]);
if (d) {
d.open = true;
}
}
})();
// 通用:表单提交后禁用按钮,避免运营重复点击造成重复请求
// 用法form 标记 data-action="disable-on-submit"。
(function () {
var forms = document.querySelectorAll('form[data-action="disable-on-submit"]');
if (!forms || forms.length === 0) {
return;
}
forms.forEach(function (form) {
form.addEventListener('submit', function () {
try {
var btns = form.querySelectorAll('button, input[type="submit"]');
btns.forEach(function (b) {
b.disabled = true;
// 尽量不改文案(避免影响断言/文案口径);只做禁用
});
} catch (e) {}
});
});
})();
// 通用折叠面板collapsible记忆展开状态localStorage
// 用法:<details data-role="collapsible" data-storage-key="xxx">
(function () {
var nodes = document.querySelectorAll('details[data-role="collapsible"][data-storage-key]');
if (!nodes || nodes.length === 0) {
return;
}
nodes.forEach(function (d) {
var key = d.getAttribute('data-storage-key');
if (!key) {
return;
}
try {
var saved = window.localStorage.getItem(key);
if (saved === 'open') {
d.open = true;
} else if (saved === 'closed') {
d.open = false;
}
} catch (e) {}
d.addEventListener('toggle', function () {
try {
window.localStorage.setItem(key, d.open ? 'open' : 'closed');
} catch (e) {}
});
});
})();
// 仪表盘:列宽一致(趋势/收费工作台 与 最近平台订单同列宽度一致)
// 说明:用户明确需求是“宽度一致”,不是高度一致。
// 方案:基于 CSS Grid 的列宽天然一致;这里不再做任何 JS 对齐(避免误解/副作用)。
// 保留 data-role 供测试与未来渐进增强定位用。
// 仪表盘:迷你图表(趋势图渐进增强)
// 说明:不引入图表库,避免构建链;用 div bar 方式渲染。
(function () {
var el = qs('[data-role="platform-order-trend-7d-chart"][data-points]');
if (!el) {
return;
}
var raw = el.getAttribute('data-points') || '[]';
var points = [];
try {
points = JSON.parse(raw) || [];
} catch (e) {
points = [];
}
if (!points || points.length === 0) {
el.classList.add('is-empty');
el.textContent = '暂无趋势数据';
return;
}
var max = 0;
points.forEach(function (p) {
var v = Number(p && p.paid_sum ? p.paid_sum : 0);
if (v > max) {
max = v;
}
});
if (!max || max <= 0) {
max = 1;
}
// 渐进增强:从下方表格复用“日期→订单集合”链接口径(避免硬编码 URL 规则)。
var dateToHref = {};
try {
var links = document.querySelectorAll('[data-role="platform-order-trend-7d"] a.link');
links.forEach(function (a) {
var d = String((a.textContent || '')).trim();
if (!d) {
return;
}
dateToHref[d] = String(a.getAttribute('href') || '');
});
} catch (e) {
dateToHref = {};
}
// 清空(避免 SSR/重复执行污染)
el.innerHTML = '';
points.forEach(function (p) {
var paid = Number(p && p.paid_sum ? p.paid_sum : 0);
var h = Math.round(Math.max(6, (paid / max) * 72));
var date = (p && p.date) ? String(p.date) : '';
var href = (date && dateToHref[date]) ? String(dateToHref[date]) : '';
var bar = document.createElement(href ? 'a' : 'div');
bar.className = 'adm-mini-chart-bar' + (href ? ' adm-mini-chart-bar-link' : '');
bar.style.height = h + 'px';
if (href) {
bar.setAttribute('href', href);
bar.setAttribute('role', 'link');
bar.setAttribute('aria-label', '进入当日订单集合:' + date);
}
var countNum = Number(p && p.count != null ? p.count : 0);
if (!isFinite(countNum) || countNum < 0) {
countNum = 0;
}
var avg = countNum > 0 ? (paid / countNum) : 0;
bar.title = date + '|订单 ' + String(countNum) + ' 单|已付 ¥' + formatMoney(paid) + '|单均 ¥' + formatMoney(avg);
el.appendChild(bar);
});
})();
// 仪表盘迷你排行近7天站点收入 Top5
(function () {
var el = qs('[data-role="merchant-revenue-rank-7d-chart"][data-points]');
if (!el) {
return;
}
var raw = el.getAttribute('data-points') || '[]';
var points = [];
try {
points = JSON.parse(raw) || [];
} catch (e) {
points = [];
}
if (!points || points.length === 0) {
el.classList.add('is-empty');
el.textContent = '暂无排行数据';
return;
}
var max = 0;
points.forEach(function (p) {
var v = Number(p && p.paid_sum ? p.paid_sum : 0);
if (v > max) {
max = v;
}
});
if (!max || max <= 0) {
max = 1;
}
// 渐进增强:从下方表格复用“站点→订单集合”链接口径(避免硬编码/避免 merchant_id=1 命中 10 的误匹配)。
var merchantIdToHref = {};
try {
var merchantLinks = document.querySelectorAll('[data-role="merchant-revenue-rank-7d"] a.link[href*="merchant_id="]');
merchantLinks.forEach(function (a) {
var href = String(a.getAttribute('href') || '');
var m = href.match(/[?&]merchant_id=(\d+)/);
if (!m) {
return;
}
merchantIdToHref[String(m[1])] = href;
});
} catch (e) {
merchantIdToHref = {};
}
el.innerHTML = '';
points.forEach(function (p, idx) {
var paid = Number(p && p.paid_sum ? p.paid_sum : 0);
var ratio = Math.max(0, Math.min(1, paid / max));
var mid = Number(p && p.merchant_id != null ? p.merchant_id : 0);
if (!isFinite(mid) || mid < 0) {
mid = 0;
}
var href = merchantIdToHref[String(mid)] ? String(merchantIdToHref[String(mid)]) : '';
var row = document.createElement(href ? 'a' : 'div');
row.className = 'adm-mini-rank-row' + (href ? ' adm-mini-rank-row-link' : '');
if (href) {
row.setAttribute('href', href);
}
var name = document.createElement('div');
name.className = 'adm-mini-rank-name';
var mname = (p && p.name) ? String(p.name) : ('#' + (idx + 1));
name.textContent = mname;
// tooltip显示完整名称避免 ellipsis 后看不到)
name.title = mname;
var wrap = document.createElement('div');
wrap.className = 'adm-mini-rank-bar-wrap';
var bar = document.createElement('div');
bar.className = 'adm-mini-rank-bar';
bar.style.width = Math.round(ratio * 100) + '%';
wrap.appendChild(bar);
var val = document.createElement('div');
val.className = 'adm-mini-rank-value';
val.textContent = '¥' + formatMoney(paid);
var cntText = String(p && p.count != null ? p.count : 0);
row.title = 'Top' + (idx + 1) + '' + mname + '|已付 ¥' + formatMoney(paid) + '|订单数 ' + cntText;
row.appendChild(name);
row.appendChild(wrap);
row.appendChild(val);
// a 标签:避免默认下划线影响视觉(由 CSS 控制);并提升可访问性。
if (href) {
row.setAttribute('role', 'link');
row.setAttribute('aria-label', '进入站点订单集合:' + mname);
}
el.appendChild(row);
});
})();
// 仪表盘:迷你占比(套餐订单占比 Top5
(function () {
var el = qs('[data-role="plan-order-share-top5-chart"][data-points]');
if (!el) {
return;
}
var raw = el.getAttribute('data-points') || '[]';
var points = [];
try {
points = JSON.parse(raw) || [];
} catch (e) {
points = [];
}
var total = Number(el.getAttribute('data-total') || 0);
if (!total || total <= 0) {
total = 0;
}
if (!points || points.length === 0 || total === 0) {
el.classList.add('is-empty');
el.textContent = '暂无占比数据';
return;
}
// 渐进增强:从下方表格复用“套餐→订单集合”链接口径(避免硬编码 URL 规则)。
var planIdToHref = {};
try {
var planLinks = document.querySelectorAll('[data-role="plan-order-share-top5"] a.link[href*="plan_id="]');
planLinks.forEach(function (a) {
var href = String(a.getAttribute('href') || '');
var m = href.match(/[?&]plan_id=(\d+)/);
if (!m) {
return;
}
planIdToHref[String(m[1])] = href;
});
} catch (e) {
planIdToHref = {};
}
// 视觉口径bar 宽度按 Top5 内最大值归一(更易读);百分比仍按 total 分母计算。
var maxCnt = 0;
points.forEach(function (p) {
var v = Number(p && p.count ? p.count : 0);
if (v > maxCnt) {
maxCnt = v;
}
});
if (!maxCnt || maxCnt <= 0) {
maxCnt = 1;
}
el.innerHTML = '';
points.forEach(function (p, idx) {
var cnt = Number(p && p.count ? p.count : 0);
var ratio = total > 0 ? Math.max(0, Math.min(1, cnt / total)) : 0;
var barRatio = maxCnt > 0 ? Math.max(0, Math.min(1, cnt / maxCnt)) : 0;
var pid = Number(p && p.plan_id != null ? p.plan_id : 0);
if (!isFinite(pid) || pid < 0) {
pid = 0;
}
var href = planIdToHref[String(pid)] ? String(planIdToHref[String(pid)]) : '';
var row = document.createElement(href ? 'a' : 'div');
row.className = 'adm-mini-share-row' + (href ? ' adm-mini-share-row-link' : '');
if (href) {
row.setAttribute('href', href);
}
var name = document.createElement('div');
name.className = 'adm-mini-share-name';
var pname = (p && p.name) ? String(p.name) : ('#' + (idx + 1));
name.textContent = pname;
// tooltip显示完整名称避免 ellipsis 后看不到)
name.title = pname;
var wrap = document.createElement('div');
wrap.className = 'adm-mini-share-bar-wrap';
var bar = document.createElement('div');
bar.className = 'adm-mini-share-bar';
bar.style.width = Math.round(barRatio * 100) + '%';
wrap.appendChild(bar);
var val = document.createElement('div');
val.className = 'adm-mini-share-value';
val.textContent = formatPct(ratio, 1) + '%';
row.title = 'Top' + (idx + 1) + '' + pname + '' + cnt + ' 单|占比 ' + formatPct(ratio, 1) + '%';
row.appendChild(name);
row.appendChild(wrap);
row.appendChild(val);
// a 标签:避免默认下划线影响视觉(由 CSS 控制);并提升可访问性。
if (href) {
row.setAttribute('role', 'link');
row.setAttribute('aria-label', '进入套餐订单集合:' + pname);
}
el.appendChild(row);
});
})();
// 通用:将后端 flash 信息同步到 toast更像 Ant Design Pro 的反馈方式)
// 说明:渐进增强。页面仍保留原本的提示块,不依赖 JS。
(function () {
var container = qs('[data-role="toast-container"]');
if (!container) {
return;
}
var flashNodes = document.querySelectorAll('[data-flash]');
if (!flashNodes || flashNodes.length === 0) {
return;
}
function createToast(type, text) {
var div = document.createElement('div');
div.className = 'toast toast-' + type;
div.setAttribute('role', 'status');
var content = document.createElement('div');
content.className = 'toast-content';
content.textContent = text;
var close = document.createElement('button');
close.type = 'button';
close.className = 'toast-close';
close.textContent = '×';
close.addEventListener('click', function () {
try {
container.removeChild(div);
} catch (e) {}
});
div.appendChild(content);
div.appendChild(close);
return div;
}
flashNodes.forEach(function (node) {
var type = node.getAttribute('data-flash') || 'info';
var text = (node.textContent || '').trim();
if (!text) {
return;
}
container.appendChild(createToast(type, text));
});
// 自动消失success/warningerror 保留更久
setTimeout(function () {
try {
var toasts = container.querySelectorAll('.toast');
toasts.forEach(function (t) {
var cls = t.className || '';
var isError = cls.indexOf('toast-error') >= 0;
if (!isError) {
container.removeChild(t);
}
});
} catch (e) {}
}, 4500);
setTimeout(function () {
try {
var toasts = container.querySelectorAll('.toast');
toasts.forEach(function (t) {
try {
container.removeChild(t);
} catch (e) {}
});
} catch (e) {}
}, 9000);
})();
// 续费缺订阅治理订单详情页“绑定订阅ID”输入框小交互增强
// - 输入后按 Enter 直接提交
// - 自动聚焦,减少点击
var attachInput = qs('#attach_site_subscription_id');
if (attachInput) {
try {
attachInput.focus();
} catch (e) {}
attachInput.addEventListener('keydown', function (e) {
if (e && (e.key === 'Enter' || e.keyCode === 13)) {
var form = attachInput.form;
if (form) {
e.preventDefault();
form.submit();
}
}
});
}
})();