Files
saasshop/public/js/admin.js

474 lines
16 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) {}
});
});
})();
// 仪表盘:迷你图表(趋势图渐进增强)
// 说明:不引入图表库,避免构建链;用 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;
}
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 = '';
try {
var tableLink = qs('[data-role="merchant-revenue-rank-7d"] a.link[href*="merchant_id=' + String(mid) + '"]');
if (tableLink) {
href = String(tableLink.getAttribute('href') || '');
}
} catch (e) {
href = '';
}
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;
}
// 视觉口径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 row = document.createElement('div');
row.className = 'adm-mini-share-row';
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', '进入站点订单集合:' + mname);
}
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();
}
}
});
}
})();