Files
saasshop/public/js/admin.js

401 lines
13 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);
}
}
// 通用:表单提交后禁用按钮,避免运营重复点击造成重复请求
// 用法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;
}
// 清空(避免 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 bar = document.createElement('div');
bar.className = 'adm-mini-chart-bar';
bar.style.height = h + 'px';
var date = (p && p.date) ? String(p.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 row = document.createElement('div');
row.className = 'adm-mini-rank-row';
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(barRatio * 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);
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);
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();
}
}
});
}
})();