r112/app.js · 约 4016 字符
12/* r112 运行时:原生 JS,零依赖(无 jQuery / 无框架) */3var $ = function (s, r) { return (r || document).querySelector(s); };4var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };56/* 数据加载器:优先 fetch('data/name.json'),失败回落 window.__DATA__(离线副本) */7var DS = (function () {8 var cache = {};9 function get(name, cb) {10 if (cache[name]) { cb && cb(cache[name]); return; }11 fetch('data/' + name + '.json')12 .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })13 .then(function (d) { cache[name] = d; cb && cb(d); })14 .catch(function () {15 var fb = (window.__DATA__ || {})[name];16 if (fb) { cache[name] = fb; cb && cb(fb); }17 else if (cb) { cb(null); }18 });19 }20 function all(names, cb) {21 var out = {}, left = names.length;22 if (!left) { cb(out); return; }23 names.forEach(function (n) {24 get(n, function (d) { out[n] = d; if (--left === 0) cb(out); });25 });26 }27 return { get: get, all: all };28})();2930function el(tag, cls, html) {31 var e = document.createElement(tag);32 if (cls) e.className = cls;33 if (html != null) e.innerHTML = html;34 return e;35}36function esc(s) {37 return String(s == null ? '' : s)38 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')39 .replace(/"/g, '"');40}41function fmt(n) { return (n == null ? '' : String(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }4243/* 轻提示 */44function toast(msg, ms) {45 var t = $('#toast'); if (!t) return;46 t.textContent = msg; t.classList.add('show');47 clearTimeout(toast._t);48 toast._t = setTimeout(function () { t.classList.remove('show'); }, ms || 1800);49}50/* 全屏 loading */51function loading(on) {52 var m = $('#mask'); if (!m) return;53 m.classList[on ? 'add' : 'remove']('loading');54 if (on && !$('.spinner', m)) {55 m.innerHTML = '<div class="spinner"></div>';56 } else if (!on) { m.innerHTML = ''; }57}58/* 失败占位 */59function failBox(host, msg, retry) {60 host.innerHTML = '<div class="fail">'61 + '<div class="fail-ic">!</div><p>' + esc(msg || '数据加载失败') + '</p>'62 + (retry ? '<button class="btn" onclick="'+retry+'">重试</button>' : '')63 + '</div>';64}6566/* 分段控件组:data-seg 容器内按钮互斥高亮,触发 onchange(key) */67function segGroup(host, onchange) {68 $$('.seg-btn', host).forEach(function (b) {69 b.addEventListener('click', function () {70 $$('.seg-btn', host).forEach(function (x) { x.classList.remove('on'); });71 b.classList.add('on');72 onchange && onchange(b.getAttribute('data-v'));73 });74 });75}76function segValue(host) {77 var b = $('.seg-btn.on', host); return b ? b.getAttribute('data-v') : null;78}7980/* 底部弹层(sheet) */81function openSheet(title, bodyHtml) {82 var m = $('#mask');83 m.classList.add('sheet-on');84 m.innerHTML = '<div class="sheet"><div class="sheet-hd">'85 + '<span>' + esc(title) + '</span><button class="sheet-x" onclick="closeSheet()">✕</button></div>'86 + '<div class="sheet-bd">' + bodyHtml + '</div></div>';87 m.onclick = function (e) { if (e.target === m) closeSheet(); };88}89function closeSheet() {90 var m = $('#mask');91 m.classList.remove('sheet-on'); m.innerHTML = '';92 m.onclick = null;93}9495/* 高亮 JSON */96function hlJSON(obj) {97 try { return JSON.stringify(obj, null, 2); }98 catch (e) { return String(obj); }99}100101/* 源码徽标 */102function srcBadge() { return '<span class="src-badge">JSON</span>'; }103104/* 安全 localStorage 封装 */105var STORE = {106 get: function (k) { try { return JSON.parse(localStorage.getItem(k) || 'null'); } catch (e) { return null; } },107 set: function (k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch (e) { return false; } }108};109110document.addEventListener('click', function (e) {111 var a = e.target.closest && e.target.closest('a[data-confirm]');112 if (a) {113 if (!confirm(a.getAttribute('data-confirm'))) { e.preventDefault(); }114 }115});116