// 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)时,自动展开对应
。 // 说明:避免“跳过去但面板是折叠的”,造成运营以为没跳到。 (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) // 用法:
(function () { // 兼容历史:旧实现要求 data-role="collapsible";现在只要存在 data-storage-key 就启用记忆。 var nodes = document.querySelectorAll('details[data-storage-key], 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/warning),error 保留更久 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(); } } }); } })();