/* ========================================================================== components.js — 共享交互组件(toast / modal / 图标 / 格式化) ========================================================================== */ /* ------------------------------------------------------------ 内联图标 */ const ICONS = { images: '', history: '', plug: '', boxes: '', fileText: '', plus: '', trash: '', search: '', refresh: '', check: '', checkCircle: '', x: '', chevronLeft: '', chevronRight: '', chevronDown: '', eye: '', eyeOff: '', copy: '', key: '', alert: '', info: '', loader: '', download: '', externalLink: '', image: '', filter: '', code: '', sparkles: '', database: '', clock: '', zap: '', settings: '', }; function icon(name, size = 16) { const body = ICONS[name] || ICONS.info; return ``; } /* --------------------------------------------------------------- Toast */ function toast(msg, type = "ok") { let host = document.getElementById("toast-host"); if (!host) { host = document.createElement("div"); host.id = "toast-host"; document.body.appendChild(host); } const ic = type === "err" ? "alert" : type === "info" ? "info" : "checkCircle"; const el = document.createElement("div"); el.className = "toast" + (type === "err" ? " err" : type === "info" ? " info" : ""); el.innerHTML = `${icon(ic, 16)}${esc(msg)}`; host.appendChild(el); setTimeout(() => { el.style.transition = "opacity .25s ease, transform .25s ease"; el.style.opacity = "0"; el.style.transform = "translateY(6px)"; setTimeout(() => el.remove(), 260); }, 2600); } /* --------------------------------------------------------------- Modal */ function openModal(id) { const m = document.getElementById(id); if (m) m.classList.add("open"); } function closeModal(id) { const m = document.getElementById(id); if (m) m.classList.remove("open"); } function paintIcons(root = document) { root.querySelectorAll("[data-icon]").forEach((el) => { el.outerHTML = icon(el.getAttribute("data-icon"), 16); }); } async function runTask(task, button) { if (button && button.disabled) return; if (button) { button.disabled = true; button.classList.add("is-loading"); } try { return await task(); } catch (err) { toast(err.message || "操作失败,请重试", "err"); } finally { if (button) { button.disabled = false; button.classList.remove("is-loading"); } } } document.addEventListener("click", (e) => { if (e.target.classList && e.target.classList.contains("modal-mask")) { e.target.classList.remove("open"); } }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") { document.querySelectorAll(".modal-mask.open").forEach((m) => m.classList.remove("open")); document.querySelectorAll(".lightbox.open").forEach((m) => m.classList.remove("open")); } }); /* ------------------------------------------------------------ 格式化 */ function fmtBytes(n) { if (!n && n !== 0) return "—"; if (n < 1024) return n + " B"; if (n < 1048576) return (n / 1024).toFixed(1) + " KB"; return (n / 1048576).toFixed(2) + " MB"; } function fmtDuration(ms) { if (ms == null) return "—"; return ms >= 1000 ? (ms / 1000).toFixed(1) + " s" : ms + " ms"; } function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]) ); } function copyText(text) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then( () => toast("已复制到剪贴板"), () => toast("复制失败,请手动选择", "err") ); return; } const ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); toast("已复制到剪贴板"); } catch (err) { toast("复制失败,请手动选择", "err"); } ta.remove(); } /* --------------------------------------------------- Markdown 极简渲染 */ function renderMarkdown(md) { const lines = String(md).split("\n"); let out = ""; let inCode = false; let inList = false; let inTable = false; const inline = (s) => esc(s) .replace(/`([^`]+)`/g, "$1") .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/(^|[^*])\*([^*]+)\*/g, "$1$2") .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); const closeList = () => { if (inList) { out += ""; inList = false; } }; const closeTable = () => { if (inTable) { out += ""; inTable = false; } }; for (const raw of lines) { const line = raw.replace(/\s+$/, ""); if (line.trim().startsWith("```")) { closeList(); closeTable(); out += inCode ? "" : "
";
      inCode = !inCode;
      continue;
    }
    if (inCode) { out += esc(raw) + "\n"; continue; }

    if (!line.trim()) { closeList(); closeTable(); continue; }

    const h = line.match(/^(#{1,4})\s+(.*)$/);
    if (h) {
      closeList(); closeTable();
      const lv = h[1].length;
      out += `${inline(h[2])}`;
      continue;
    }

    if (/^\s*\|/.test(line)) {
      const cells = line.split("|").slice(1, -1).map((c) => c.trim());
      if (/^[\s|:-]+$/.test(line)) continue;
      if (!inTable) {
        closeList();
        out += "" + cells.map((c) => ``).join("") + "";
        inTable = true;
      } else {
        out += "" + cells.map((c) => ``).join("") + "";
      }
      continue;
    }
    closeTable();

    const li = line.match(/^\s*[-*]\s+(.*)$/);
    if (li) {
      if (!inList) { out += "
    "; inList = true; } out += `
  • ${inline(li[1])}
  • `; continue; } closeList(); const bq = line.match(/^>\s?(.*)$/); if (bq) { out += `
    ${inline(bq[1])}
    `; continue; } out += `

    ${inline(line)}

    `; } closeList(); closeTable(); if (inCode) out += ""; return out; } /* --------------------------------------------------- 激活导航项(CSS 兜底) */ document.addEventListener("DOMContentLoaded", () => { const page = document.body.getAttribute("data-page"); if (!page) return; const item = document.querySelector(`.app-nav a[data-nav="${page}"]`); if (item) item.setAttribute("aria-current", "page"); });
${inline(c)}
${inline(c)}