130 lines
6.2 KiB
JavaScript
130 lines
6.2 KiB
JavaScript
/* ============ 工具库:DOM / 存储 / 事件总线 ============ */
|
||
'use strict';
|
||
const $ = (sel, root) => (root || document).querySelector(sel);
|
||
const $$ = (sel, root) => Array.from((root || document).querySelectorAll(sel));
|
||
|
||
function el(tag, attrs, ...children) {
|
||
const node = document.createElement(tag);
|
||
if (attrs) for (const [k, v] of Object.entries(attrs)) {
|
||
if (v == null || v === false) continue;
|
||
if (k === 'class') node.className = v;
|
||
else if (k === 'text') node.textContent = v;
|
||
else if (k === 'html') node.innerHTML = v; // 仅用于受信任的静态内容
|
||
else if (k === 'style' && typeof v === 'object') Object.assign(node.style, v);
|
||
else if (k.startsWith('on') && typeof v === 'function') node.addEventListener(k.slice(2), v);
|
||
else if (k === 'dataset') Object.assign(node.dataset, v);
|
||
else node.setAttribute(k, v === true ? '' : v);
|
||
}
|
||
for (const c of children.flat(9)) {
|
||
if (c == null || c === false) continue;
|
||
node.append(c.nodeType ? c : document.createTextNode(c));
|
||
}
|
||
return node;
|
||
}
|
||
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
|
||
const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
|
||
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||
|
||
/* 事件总线 */
|
||
const Bus = {
|
||
map: {},
|
||
on(ev, fn) { (this.map[ev] ||= []).push(fn); return () => this.off(ev, fn); },
|
||
off(ev, fn) { this.map[ev] = (this.map[ev] || []).filter(f => f !== fn); },
|
||
emit(ev, data) { (this.map[ev] || []).slice().forEach(fn => { try { fn(data); } catch (e) { console.error('[bus]', ev, e); } }); }
|
||
};
|
||
|
||
/* localStorage 安全封装:每类数据独立 key,解析失败回退默认值 */
|
||
const Store = {
|
||
PREFIX: 'macos-web:',
|
||
get(key, fallback) {
|
||
try {
|
||
const raw = localStorage.getItem(this.PREFIX + key);
|
||
if (raw == null) return structuredClone(fallback);
|
||
const v = JSON.parse(raw);
|
||
return v == null ? structuredClone(fallback) : v;
|
||
} catch (e) { console.warn('[store] 解析失败,回退默认:', key); return structuredClone(fallback); }
|
||
},
|
||
set(key, val) {
|
||
try { localStorage.setItem(this.PREFIX + key, JSON.stringify(val)); }
|
||
catch (e) { console.warn('[store] 写入失败:', key, e); }
|
||
},
|
||
remove(key) { localStorage.removeItem(this.PREFIX + key); },
|
||
clearAll() {
|
||
Object.keys(localStorage).filter(k => k.startsWith(this.PREFIX)).forEach(k => localStorage.removeItem(k));
|
||
}
|
||
};
|
||
|
||
/* 时间格式化 */
|
||
const WEEK_CN = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
|
||
function fmtDateCN(d) { return `${d.getMonth() + 1}月${d.getDate()}日${WEEK_CN[d.getDay()]}`; }
|
||
/* 本地日期键 YYYY-MM-DD(避免 toISOString().slice(0,10) 的 UTC 错位) */
|
||
const localDateKey = (d = new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
/* IANA 时区有效性检查('local' 表示跟随浏览器本地时区) */
|
||
const tzValid = tz => {
|
||
if (!tz || tz === 'local') return false;
|
||
try { new Intl.DateTimeFormat('en', { timeZone: tz }); return true; } catch (e) { return false; }
|
||
};
|
||
function fmtTime(d, h24, tz) {
|
||
if (tzValid(tz)) return new Intl.DateTimeFormat('zh-CN', { hour: 'numeric', minute: '2-digit', hour12: !h24, timeZone: tz }).format(d);
|
||
let h = d.getHours(), m = String(d.getMinutes()).padStart(2, '0');
|
||
if (h24) return `${h}:${m}`;
|
||
const ap = h < 12 ? '上午' : '下午'; h = h % 12 || 12;
|
||
return `${ap}${h}:${m}`;
|
||
}
|
||
function fmtTimeHM(d, h24, tz) {
|
||
if (typeof tzValid === 'function' && tzValid(tz)) return new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: !h24, timeZone: tz }).format(d);
|
||
let h = d.getHours(), m = String(d.getMinutes()).padStart(2, '0');
|
||
if (h24) return `${String(h).padStart(2, '0')}:${m}`;
|
||
const ap = h < 12 ? '上午' : '下午'; h = h % 12 || 12;
|
||
return `${ap} ${h}:${m}`;
|
||
}
|
||
function fmtMenuClock(d, h24, tz) {
|
||
if (typeof tzValid === 'function' && tzValid(tz)) {
|
||
const date = new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric', weekday: 'short', timeZone: tz }).format(d);
|
||
return `${date} ${fmtTime(d, h24, tz)}`;
|
||
}
|
||
const base = `${d.getMonth() + 1}月${d.getDate()}日 ${WEEK_CN[d.getDay()]} `;
|
||
let h = d.getHours(); const m = String(d.getMinutes()).padStart(2, '0');
|
||
if (h24) return base + `${h}:${m}`;
|
||
const ap = h < 12 ? '上午' : '下午'; h = h % 12 || 12;
|
||
return base + `${ap}${h}:${m}`;
|
||
}
|
||
function fmtBytes(n) {
|
||
if (n < 1024) return n + ' B';
|
||
if (n < 1048576) return (n / 1024).toFixed(1) + ' KB';
|
||
if (n < 1073741824) return (n / 1048576).toFixed(1) + ' MB';
|
||
return (n / 1073741824).toFixed(2) + ' GB';
|
||
}
|
||
function fmtDur(sec) {
|
||
if (!isFinite(sec)) return '--:--';
|
||
sec = Math.round(sec);
|
||
return `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}`;
|
||
}
|
||
function relTime(ts) {
|
||
const d = new Date(ts), now = new Date();
|
||
const sameDay = d.toDateString() === now.toDateString();
|
||
if (sameDay) return fmtTimeHM(d, Sys?.settings?.h24);
|
||
const yest = new Date(now); yest.setDate(now.getDate() - 1);
|
||
if (d.toDateString() === yest.toDateString()) return '昨天';
|
||
return `${d.getMonth() + 1}月${d.getDate()}日`;
|
||
}
|
||
|
||
/* 资源路径(带回退的图片) */
|
||
function iconImg(src, cls, alt) {
|
||
const img = el('img', { src, class: cls || '', alt: alt || '', draggable: 'false' });
|
||
img.addEventListener('error', () => {
|
||
if (img.dataset.fbk) return;
|
||
img.dataset.fbk = '1';
|
||
const span = el('span', { class: 'icon-fallback', text: '❓', style: { fontSize: 'inherit' } });
|
||
img.replaceWith(span);
|
||
}, { once: true });
|
||
return img;
|
||
}
|
||
window.$ = $; window.$$ = $$; window.el = el; window.esc = esc; window.clamp = clamp;
|
||
window.debounce = debounce; window.uid = uid; window.Bus = Bus; window.Store = Store;
|
||
window.fmtDateCN = fmtDateCN; window.fmtTime = fmtTime; window.fmtTimeHM = fmtTimeHM;
|
||
window.fmtMenuClock = fmtMenuClock; window.fmtBytes = fmtBytes; window.fmtDur = fmtDur;
|
||
window.relTime = relTime; window.iconImg = iconImg;
|
||
window.localDateKey = localDateKey; window.tzValid = tzValid;
|