文件预览

script.js

查看 Wechat Article Formatter 技能包中的文件内容。

文件内容

references/template-parts/script.js

function setStyle(el, style) {
      el.setAttribute("style", style);
    }

    const themeSceneColors = {
      "warm-editorial": {
        summarySurface: "#faf7f0",
        summaryText: "#3d3d3a",
        warningSurface: "#f6e8e4",
        warningText: "#7a2f29",
        warningBorder: "#d97757",
        positiveSurface: "#f1f5ec",
        positiveText: "#5f7748",
        positiveBorder: "#cbd6bd"
      },
      "light-simple": {
        summarySurface: "#f9fafb",
        summaryText: "#2c3e50",
        warningSurface: "#fff1f2",
        warningText: "#e11d48",
        warningBorder: "#fecdd3",
        positiveSurface: "#f0fdf4",
        positiveText: "#166534",
        positiveBorder: "#bbf7d0"
      },
      "tech-blue": {
        summarySurface: "#eff6ff",
        summaryText: "#1e3a8a",
        warningSurface: "#fff1f0",
        warningText: "#cf222e",
        warningBorder: "#fecaca",
        positiveSurface: "#ecfdf5",
        positiveText: "#047857",
        positiveBorder: "#a7f3d0"
      },
      "indigo-card": {
        summarySurface: "#e0e7ff",
        summaryText: "#4338ca",
        warningSurface: "#fff1f2",
        warningText: "#be123c",
        warningBorder: "#fecdd3",
        positiveSurface: "#eff6ff",
        positiveText: "#2563eb",
        positiveBorder: "#bfdbfe"
      },
      "swiss-grid": {
        summarySurface: "#f8f8f8",
        summaryText: "#000000",
        warningSurface: "#fff1f1",
        warningText: "#b91c1c",
        warningBorder: "#000000",
        positiveSurface: "#f1f1f1",
        positiveText: "#1a1a1a",
        positiveBorder: "#1a1a1a"
      },
      "purple-highlight": {
        summarySurface: "#f5f3ff",
        summaryText: "#581c87",
        warningSurface: "#fff1f2",
        warningText: "#be123c",
        warningBorder: "#fecdd3",
        positiveSurface: "#f3e8ff",
        positiveText: "#6b21a8",
        positiveBorder: "#c4b5fd"
      },
      "warm-nature": {
        summarySurface: "#ecfccb",
        summaryText: "#3f6212",
        warningSurface: "#fdf6e3",
        warningText: "#78350f",
        warningBorder: "#fde68a",
        positiveSurface: "#f5f5f4",
        positiveText: "#4d7c0f",
        positiveBorder: "#d9f99d"
      },
      "vitality-orange": {
        summarySurface: "#fff7ed",
        summaryText: "#c2410c",
        warningSurface: "#ffedd5",
        warningText: "#431407",
        warningBorder: "#fdba74",
        positiveSurface: "#fff7ed",
        positiveText: "#db2777",
        positiveBorder: "#fed7aa"
      },
      "wechat-pure-glass": {
        summarySurface: "#f2fbf6",
        summaryText: "#064e3b",
        warningSurface: "#fff7ed",
        warningText: "#c2410c",
        warningBorder: "#fed7aa",
        positiveSurface: "#f2fbf6",
        positiveText: "#065f32",
        positiveBorder: "#07c160"
      },
      "modern-cobalt": {
        summarySurface: "#eff6ff",
        summaryText: "#1e40af",
        warningSurface: "#fff1f2",
        warningText: "#be123c",
        warningBorder: "#fecdd3",
        positiveSurface: "#f1f5f9",
        positiveText: "#2563eb",
        positiveBorder: "#93c5fd"
      },
      "deep-space": {
        summarySurface: "#1e1b4b",
        summaryText: "#e9d5ff",
        warningSurface: "#f3e8ff",
        warningText: "#581c87",
        warningBorder: "#7c3aed",
        positiveSurface: "#e2e8f0",
        positiveText: "#0f172a",
        positiveBorder: "#06b6d4"
      }
    };

    const centeredHeadingThemes = new Set([
      "purple-highlight",
      "modern-cobalt",
      "deep-space"
    ]);

    function headingAlignmentFor(themeId) {
      return centeredHeadingThemes.has(themeId) ? " text-align: center;" : "";
    }

    function getTheme(themeId) {
      const resolvedThemeId = themes[themeId] ? themeId : (themes["warm-editorial"] ? "warm-editorial" : "light-simple");
      const base = themes[resolvedThemeId] || {};
      const scene = themeSceneColors[resolvedThemeId] || themeSceneColors["warm-editorial"];
      return {
        text: base.text || "#3d3d3a",
        heading: base.heading || "#141413",
        accent: base.accent || "#b85c3e",
        muted: base.muted || "#8a8175",
        surface: base.surface || "#faf7f0",
        border: base.border || "#ded7ca",
        quoteSurface: base.quoteSurface || base.surface || "#f0eee6",
        codeSurface: base.codeSurface || "#f0eee6",
        codeText: base.codeText || "#b04a3f",
        tableHeader: base.tableHeader || base.surface || "#fbfaf6",
        tableBorder: base.tableBorder || base.border || "#d1cfc5",
        summarySurface: base.summarySurface || scene.summarySurface || base.surface || "#faf7f0",
        summaryText: base.summaryText || scene.summaryText || base.text || "#3d3d3a",
        warningSurface: base.warningSurface || scene.warningSurface || "#f6e8e4",
        warningText: base.warningText || scene.warningText || "#7a2f29",
        warningBorder: base.warningBorder || scene.warningBorder || base.border || "#ded7ca",
        positiveSurface: base.positiveSurface || scene.positiveSurface || "#f1f5ec",
        positiveText: base.positiveText || scene.positiveText || "#5f7748",
        positiveBorder: base.positiveBorder || scene.positiveBorder || base.border || "#ded7ca"
      };
    }

    function styleFor(name, fallback) {
      return styles[name] || fallback;
    }

    function applyToArticle(selector, style) {
      document.querySelectorAll("#wechatArticle " + selector).forEach(el => setStyle(el, style));
    }

    function applyTheme(themeId) {
      const t = getTheme(themeId);
      const article = document.querySelector("#wechatArticle");
      const articleStyle = styleFor("article", "max-width: 677px; margin: 0 auto; padding: 28px 18px 34px; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; font-size: 16px; line-height: 1.8; letter-spacing: 0; word-break: break-word; overflow-wrap: anywhere;");
      setStyle(article, articleStyle + " color: " + t.text + ";");

      const sectionStyle = styleFor("section", "margin: 30px 0 0;");
      const pStyle = styleFor("p", "margin: 0 0 14px; font-size: 16px; line-height: 1.8; letter-spacing: 0; word-break: break-word; overflow-wrap: anywhere;");
      const headingAlignment = headingAlignmentFor(themeId);
      const h1Style = styleFor("h1", "margin: 0 0 14px; font-size: 24px; line-height: 1.25; font-weight: 760; letter-spacing: 0;") + headingAlignment;
      const h2Style = styleFor("h2", "margin: 0 0 14px; font-size: 20px; line-height: 1.35; font-weight: 720; letter-spacing: 0;") + headingAlignment;
      const h3Style = styleFor("h3", "margin: 24px 0 10px; font-size: 16px; line-height: 1.45; font-weight: 700; letter-spacing: 0;");
      const labelStyle = styleFor("moduleLabel", styleFor("eyebrow", "margin: 0 0 10px; font-size: 12px; line-height: 1.5; letter-spacing: 0; font-family: Menlo, Consolas, monospace;"));
      const tableStyle = styleFor("table", "width: 100%; border-collapse: collapse; margin: 18px 0; table-layout: fixed; word-break: break-word; overflow-wrap: anywhere; font-size: 14px; line-height: 1.65;");
      const thStyle = styleFor("th", "padding: 12px 8px; text-align: left; border: 1px solid; font-weight: 700; word-break: break-word; overflow-wrap: anywhere;");
      const tdStyle = styleFor("td", "padding: 12px 8px; border: 1px solid; vertical-align: top; word-break: break-word; overflow-wrap: anywhere;");
      const codeStyle = styleFor("code", "padding: 2px 6px; border-radius: 5px; font-family: Menlo, Consolas, monospace; font-size: 14px; line-height: 1.6; word-break: break-word; overflow-wrap: anywhere;");
      const preStyle = styleFor("pre", "margin: 18px 0; padding: 14px; border-radius: 8px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; font-family: Menlo, Consolas, monospace; font-size: 13px; line-height: 1.65;");

      document.querySelectorAll("[data-role='hero']").forEach(el => setStyle(el, styleFor("hero", "margin: 0 0 24px; padding: 28px 18px 18px; border-bottom: 1px solid;") + " background: " + t.surface + "; border-color: " + t.border + ";"));
      document.querySelectorAll("[data-role='eyebrow']").forEach(el => setStyle(el, styleFor("eyebrow", labelStyle) + headingAlignment + " color: " + t.muted + ";"));
      document.querySelectorAll("[data-role='moduleLabel']").forEach(el => setStyle(el, labelStyle + headingAlignment + " color: " + t.accent + ";"));
      document.querySelectorAll("[data-role='h1']").forEach(el => setStyle(el, h1Style + " color: " + t.heading + ";"));
      document.querySelectorAll("[data-role='dek']").forEach(el => setStyle(el, styleFor("dek", "margin: 12px 0 0; font-size: 17px; line-height: 1.8; letter-spacing: 0;") + headingAlignment + " color: " + t.text + ";"));
      document.querySelectorAll("[data-role='p']").forEach(el => setStyle(el, pStyle + " color: " + t.text + ";"));
      document.querySelectorAll("[data-role='section']").forEach(el => setStyle(el, sectionStyle));
      document.querySelectorAll("[data-role='h2']").forEach(el => setStyle(el, h2Style + " color: " + t.heading + ";"));
      document.querySelectorAll("[data-role='h3']").forEach(el => setStyle(el, h3Style + " color: " + t.heading + ";"));
      document.querySelectorAll("[data-role='quote']").forEach(el => setStyle(el, styleFor("quote", "margin: 20px 0; padding: 16px; border: 1px solid; border-radius: 8px;") + " background: " + t.quoteSurface + "; border-color: " + t.border + "; color: " + t.heading + ";"));
      document.querySelectorAll("[data-role='quoteText']").forEach(el => setStyle(el, styleFor("quoteText", "margin: 0; font-size: 18px; line-height: 1.75; font-family: 'Songti SC', STSong, SimSun, serif;") + " color: inherit;"));
      document.querySelectorAll("[data-role='figure']").forEach(el => setStyle(el, styleFor("figure", "margin: 22px 0;")));
      document.querySelectorAll("[data-role='image']").forEach(el => setStyle(el, styleFor("image", "display: block; width: 100%; height: auto; border: 1px solid; border-radius: 8px;") + " border-color: " + t.border + ";"));
      document.querySelectorAll("[data-role='figcaption']").forEach(el => setStyle(el, styleFor("figcaption", "margin: 8px 0 0; font-size: 13px; line-height: 1.6; text-align: center;") + " color: " + t.muted + ";"));
      document.querySelectorAll("[data-role='callout']").forEach(el => setStyle(el, styleFor("callout", "margin: 20px 0; padding: 16px; border: 1px solid; border-radius: 8px;") + " background: " + t.surface + "; border-color: " + t.border + ";"));
      document.querySelectorAll("[data-role='calloutTitle']").forEach(el => setStyle(el, styleFor("calloutTitle", labelStyle + " font-weight: 700;") + " color: " + t.accent + ";"));
      document.querySelectorAll("[data-role='ul']").forEach(el => setStyle(el, styleFor("ul", "margin: 12px 0 16px; padding-left: 1.2em;") + " color: " + t.text + ";"));
      document.querySelectorAll("[data-role='ol']").forEach(el => setStyle(el, styleFor("ol", styleFor("ul", "margin: 12px 0 16px; padding-left: 1.2em;")) + " color: " + t.text + ";"));
      document.querySelectorAll("[data-role='li']").forEach(el => setStyle(el, styleFor("li", "margin: 0 0 8px; font-size: 16px; line-height: 1.8;") + " color: " + t.text + ";"));
      document.querySelectorAll("[data-role='summary']").forEach(el => setStyle(el, styleFor("summary", "margin: 24px 0 0; padding: 16px; border: 1px solid; border-radius: 8px;") + " background: " + t.summarySurface + "; border-color: " + t.border + ";"));
      document.querySelectorAll("[data-role='summaryTitle']").forEach(el => setStyle(el, styleFor("summaryTitle", labelStyle + " font-weight: 700;") + " color: " + t.accent + ";"));
      document.querySelectorAll("[data-role='summaryText']").forEach((el, index, list) => {
        const margin = index === list.length - 1 ? "margin: 0;" : styleFor("summaryText", "margin: 0 0 10px;");
        setStyle(el, margin + " color: " + t.summaryText + "; font-size: 16px; line-height: 1.8;");
      });
      document.querySelectorAll("[data-role='table']").forEach(el => setStyle(el, tableStyle + " border-color: " + t.tableBorder + ";"));
      document.querySelectorAll("[data-role='th']").forEach(el => setStyle(el, thStyle + " background: " + t.tableHeader + "; border-color: " + t.tableBorder + "; color: " + t.heading + ";"));
      document.querySelectorAll("[data-role='td']").forEach(el => setStyle(el, tdStyle + " border-color: " + t.tableBorder + "; color: " + t.text + ";"));
      document.querySelectorAll("[data-role='code']").forEach(el => setStyle(el, codeStyle + " background: " + t.codeSurface + "; color: " + t.codeText + ";"));
      document.querySelectorAll("[data-role='pre']").forEach(el => setStyle(el, preStyle + " background: " + t.codeSurface + "; color: " + t.text + ";"));
      document.querySelectorAll("[data-role='strong']").forEach(el => setStyle(el, styleFor("strong", "font-weight: 700;") + " color: " + t.heading + ";"));
      document.querySelectorAll("[data-role='a']").forEach(el => setStyle(el, styleFor("a", "text-decoration: none; border-bottom: 1px solid;") + " color: " + t.accent + "; border-color: " + t.accent + ";"));
      document.querySelectorAll("[data-role='hr']").forEach(el => setStyle(el, styleFor("hr", "margin: 30px 0; border: 0; border-top: 1px solid;") + " border-color: " + t.border + ";"));
      document.querySelectorAll("[data-role='warning']").forEach(el => setStyle(el, styleFor("callout", "margin: 20px 0; padding: 16px; border: 1px solid; border-radius: 8px;") + " background: " + t.warningSurface + "; border-color: " + t.warningBorder + "; color: " + t.warningText + ";"));
      document.querySelectorAll("[data-role='positive']").forEach(el => setStyle(el, styleFor("callout", "margin: 20px 0; padding: 16px; border: 1px solid; border-radius: 8px;") + " background: " + t.positiveSurface + "; border-color: " + t.positiveBorder + "; color: " + t.positiveText + ";"));

      applyToArticle("h1:not([data-role])", h1Style + " color: " + t.heading + ";");
      applyToArticle("h2:not([data-role])", h2Style + " color: " + t.heading + ";");
      applyToArticle("h3:not([data-role])", h3Style + " color: " + t.heading + ";");
      applyToArticle("p:not([data-role])", pStyle + " color: " + t.text + ";");
      applyToArticle("ul:not([data-role])", styleFor("ul", "margin: 12px 0 16px; padding-left: 1.2em;") + " color: " + t.text + ";");
      applyToArticle("ol:not([data-role])", styleFor("ol", styleFor("ul", "margin: 12px 0 16px; padding-left: 1.2em;")) + " color: " + t.text + ";");
      applyToArticle("li:not([data-role])", styleFor("li", "margin: 0 0 8px; font-size: 16px; line-height: 1.8;") + " color: " + t.text + ";");
      applyToArticle("blockquote:not([data-role])", styleFor("quote", "margin: 20px 0; padding: 16px; border: 1px solid; border-radius: 8px;") + " background: " + t.quoteSurface + "; border-color: " + t.border + "; color: " + t.heading + ";");
      applyToArticle("figure:not([data-role])", styleFor("figure", "margin: 22px 0;"));
      applyToArticle("img:not([data-role])", styleFor("image", "display: block; width: 100%; height: auto; border: 1px solid; border-radius: 8px;") + " border-color: " + t.border + ";");
      applyToArticle("figcaption:not([data-role])", styleFor("figcaption", "margin: 8px 0 0; font-size: 13px; line-height: 1.6; text-align: center;") + " color: " + t.muted + ";");
      applyToArticle("table:not([data-role])", tableStyle + " border-color: " + t.tableBorder + ";");
      applyToArticle("th:not([data-role])", thStyle + " background: " + t.tableHeader + "; border-color: " + t.tableBorder + "; color: " + t.heading + ";");
      applyToArticle("td:not([data-role])", tdStyle + " border-color: " + t.tableBorder + "; color: " + t.text + ";");
      applyToArticle("pre:not([data-role])", preStyle + " background: " + t.codeSurface + "; color: " + t.text + ";");
      applyToArticle("code:not([data-role])", codeStyle + " background: " + t.codeSurface + "; color: " + t.codeText + ";");
      applyToArticle("strong:not([data-role])", styleFor("strong", "font-weight: 700;") + " color: " + t.heading + ";");
      applyToArticle("a:not([data-role])", styleFor("a", "text-decoration: none; border-bottom: 1px solid;") + " color: " + t.accent + "; border-color: " + t.accent + ";");
      applyToArticle("hr:not([data-role])", styleFor("hr", "margin: 30px 0; border: 0; border-top: 1px solid;") + " border-color: " + t.border + ";");
    }

    function blobToDataUrl(blob) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(String(reader.result || ""));
        reader.onerror = () => reject(reader.error || new Error("读取图片失败"));
        reader.readAsDataURL(blob);
      });
    }

    async function fetchImageAsDataUrl(src) {
      const absoluteUrl = new URL(src, document.baseURI).href;
      const response = await fetch(absoluteUrl);
      if (!response.ok) {
        throw new Error("图片请求失败:" + response.status);
      }
      const blob = await response.blob();
      if (!blob.type.startsWith("image/")) {
        throw new Error("资源不是图片:" + blob.type);
      }
      return await blobToDataUrl(blob);
    }

    function renderedImageToDataUrl(img) {
      if (!img.complete || !img.naturalWidth || !img.naturalHeight) {
        throw new Error("图片尚未加载完成");
      }
      const canvas = document.createElement("canvas");
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      const context = canvas.getContext("2d");
      if (!context) {
        throw new Error("无法创建图片画布");
      }
      context.drawImage(img, 0, 0);
      return canvas.toDataURL("image/png");
    }

    async function inlineImagesForCopy(sourceArticle, clonedArticle) {
      const sourceImages = Array.from(sourceArticle.querySelectorAll("img"));
      const clonedImages = Array.from(clonedArticle.querySelectorAll("img"));
      let inlined = 0;
      let failed = 0;

      for (let index = 0; index < clonedImages.length; index += 1) {
        const clonedImg = clonedImages[index];
        const sourceImg = sourceImages[index];
        const src = (clonedImg.getAttribute("src") || "").trim();
        if (!src) continue;
        if (/^data:image\//i.test(src)) {
          inlined += 1;
          continue;
        }

        try {
          const dataUrl = await fetchImageAsDataUrl(src);
          clonedImg.setAttribute("src", dataUrl);
          inlined += 1;
          continue;
        } catch (fetchError) {
          try {
            if (!sourceImg) throw fetchError;
            const dataUrl = renderedImageToDataUrl(sourceImg);
            clonedImg.setAttribute("src", dataUrl);
            inlined += 1;
          } catch (canvasError) {
            failed += 1;
          }
        }
      }

      return { inlined, failed, total: clonedImages.length };
    }

    // ========== 编辑模式逻辑 (Edit Mode Logic) ==========
    let isEditing = false;
    let currentEditingNode = null;
    let undoStack = [];
    const storageKey = 'wechat_article_' + location.pathname;
    const themeStorageKey = 'wechat_theme_' + location.pathname;
    
    function saveState() {
      const article = document.getElementById('wechatArticle');
      if (!article) return;
      undoStack.push(article.innerHTML);
      if (undoStack.length > 30) undoStack.shift();
    }
    
    function undoOperation() {
      if (undoStack.length > 0) {
        const lastState = undoStack.pop();
        document.getElementById('wechatArticle').innerHTML = lastState;
        currentEditingNode = null;
        saveToStorage();
      }
    }

    function saveToStorage() {
      try {
        const article = document.getElementById('wechatArticle');
        if (!article) return;
        const clone = article.cloneNode(true);
        clone.removeAttribute('contenteditable');
        clone.querySelectorAll('[contenteditable]').forEach(el => {
          el.removeAttribute('contenteditable');
        });
        // 彻底清理掉所有临时生成的悬浮气泡,不将它们持久化到缓存中
        clone.querySelectorAll('.aim-tooltip').forEach(tooltip => {
          tooltip.remove();
        });
        localStorage.setItem(storageKey, clone.innerHTML);
      } catch (e) {
        console.warn('无法保存到 localStorage:', e);
      }
    }

    function resetContent() {
      if (confirm('确定要恢复到初始生成的文章内容吗?所有修改都将丢失。')) {
        try {
          localStorage.removeItem(storageKey);
          localStorage.removeItem(themeStorageKey);
        } catch (e) {
          console.warn('无法清除 localStorage:', e);
        }
        location.reload();
      }
    }

    const editableRoles = ['h1', 'h2', 'h3', 'p', 'li', 'dek', 'quoteText', 'figcaption', 'calloutTitle', 'summaryTitle', 'summaryText', 'eyebrow', 'moduleLabel'];

    function enableEditMode() {
      if (isAnnotating) {
        disableAnnotateMode();
      }
      isEditing = true;
      document.getElementById('toolbarContainer').classList.add('is-editing-mode');
      document.getElementById('toggleEdit').textContent = '完成';
      
      const article = document.getElementById('wechatArticle');
      editableRoles.forEach(role => {
        article.querySelectorAll(`[data-role="${role}"]`).forEach(el => {
          el.setAttribute('contenteditable', 'true');
        });
      });
      article.querySelectorAll('h1:not([data-role]), h2:not([data-role]), h3:not([data-role]), p:not([data-role]), li:not([data-role]), figcaption:not([data-role])').forEach(el => {
        el.setAttribute('contenteditable', 'true');
      });
    }

    function disableEditMode() {
      isEditing = false;
      document.getElementById('toolbarContainer').classList.remove('is-editing-mode');
      document.getElementById('toggleEdit').textContent = '编辑';
      currentEditingNode = null;
      
      const article = document.getElementById('wechatArticle');
      article.querySelectorAll('[contenteditable]').forEach(el => {
        el.removeAttribute('contenteditable');
      });
    }

    function toggleEditMode() {
      if (isEditing) {
        disableEditMode();
      } else {
        enableEditMode();
      }
    }

    // 追踪当前编辑节点
    const articleEl = document.getElementById('wechatArticle');

    articleEl.addEventListener('focusin', function(e) {
      if (isEditing && e.target.hasAttribute('contenteditable')) {
        currentEditingNode = e.target;
      }
    });

    function focusEditableTarget(target) {
      if (!isEditing) return;
      const editable = target.closest && target.closest('[contenteditable="true"]');
      if (!editable || !articleEl.contains(editable)) return;
      currentEditingNode = editable;
      try {
        editable.focus({ preventScroll: true });
      } catch (e) {
        editable.focus();
      }
    }

    articleEl.addEventListener('pointerup', function(e) {
      focusEditableTarget(e.target);
    });

    articleEl.addEventListener('touchend', function(e) {
      const touch = e.changedTouches && e.changedTouches[0];
      if (!touch) return;
      const target = document.elementFromPoint(touch.clientX, touch.clientY);
      if (target) focusEditableTarget(target);
    }, { passive: true });

    // 拦截回车键,防止生成多余标签
    articleEl.addEventListener('keydown', function(e) {
      if (isEditing && e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        insertBlock('after');
      }
    });

    function getCurrentThemeId() {
      return document.getElementById('themePreset').value;
    }

    function changeBlockRole(newRole, tagName) {
      if (!currentEditingNode) return;
      saveState();
      const newNode = document.createElement(tagName);
      newNode.innerHTML = currentEditingNode.innerHTML;
      newNode.setAttribute('data-role', newRole);
      newNode.setAttribute('contenteditable', 'true');
      
      currentEditingNode.replaceWith(newNode);
      currentEditingNode = newNode;
      
      // 重新应用样式
      applyTheme(getCurrentThemeId());
      // 保持焦点
      newNode.focus();
      saveToStorage();
    }

    function insertBlock(position) {
      if (!currentEditingNode) return;
      saveState();
      const newNode = document.createElement('p');
      newNode.setAttribute('data-role', 'p');
      newNode.setAttribute('contenteditable', 'true');
      newNode.innerHTML = '<br>'; // 用于占位,保证可点击
      
      if (position === 'before') {
        currentEditingNode.before(newNode);
      } else {
        currentEditingNode.after(newNode);
      }
      
      applyTheme(getCurrentThemeId());
      newNode.focus();
      saveToStorage();
    }

    function deleteBlock() {
      if (!currentEditingNode) return;
      saveState();
      const parent = currentEditingNode.parentElement;
      currentEditingNode.remove();
      currentEditingNode = null;
      
      // 当容器中没有内容时,删除容器
      if (parent && parent.id !== 'wechatArticle') {
        if (!parent.textContent.trim() && parent.querySelectorAll('img, video, iframe').length === 0) {
          parent.remove();
        }
      }
      saveToStorage();
    }

    // 绑定编辑工具栏按钮
    document.getElementById('toggleEdit').addEventListener('click', toggleEditMode);
    document.getElementById('btnToH1').addEventListener('click', () => changeBlockRole('h1', 'h1'));
    document.getElementById('btnToH2').addEventListener('click', () => changeBlockRole('h2', 'h2'));
    document.getElementById('btnToH3').addEventListener('click', () => changeBlockRole('h3', 'h3'));
    document.getElementById('btnToP').addEventListener('click', () => changeBlockRole('p', 'p'));
    document.getElementById('btnUndo').addEventListener('click', undoOperation);
    document.getElementById('btnReset').addEventListener('click', resetContent);
    document.getElementById('btnInsertBefore').addEventListener('click', () => insertBlock('before'));
    document.getElementById('btnInsertAfter').addEventListener('click', () => insertBlock('after'));
    document.getElementById('btnDeleteBlock').addEventListener('click', deleteBlock);
    
    // 监听输入事件进行自动保存
    articleEl.addEventListener('input', saveToStorage);

    // ========== 批注模式逻辑 (Annotation Mode Logic) ==========
    let isAnnotating = false;
    let savedSelectionRange = null;
    let pendingAnnotationMark = null;
    let annotationSelectionTimer = null;

    function enableAnnotateMode() {
      if (isEditing) {
        disableEditMode();
      }
      isAnnotating = true;
      document.getElementById('toolbarContainer').classList.add('is-annotating-mode');
      document.getElementById('toggleAnnotate').textContent = '完成批注';
      document.getElementById('toggleAnnotate').style.cssText = 'background: #fef08a; border-color: #eab308; color: #854d0e; font-weight: bold;';
    }

    function disableAnnotateMode() {
      isAnnotating = false;
      document.getElementById('toolbarContainer').classList.remove('is-annotating-mode');
      document.getElementById('toggleAnnotate').textContent = '批注';
      document.getElementById('toggleAnnotate').style.cssText = '';
      clearPendingAnnotation();
      hideAnnotationPopup();
    }

    function toggleAnnotateMode() {
      if (isAnnotating) {
        disableAnnotateMode();
      } else {
        enableAnnotateMode();
      }
    }

    function hideAnnotationPopup() {
      document.getElementById('annotationPopup').style.display = 'none';
      document.getElementById('annotationInput').value = '';
      savedSelectionRange = null;
    }

    function unwrapAnnotationMark(mark) {
      const parent = mark.parentNode;
      if (!parent) return;
      while (mark.firstChild) {
        parent.insertBefore(mark.firstChild, mark);
      }
      mark.remove();
      parent.normalize();
    }

    function clearPendingAnnotation() {
      if (pendingAnnotationMark && pendingAnnotationMark.isConnected) {
        unwrapAnnotationMark(pendingAnnotationMark);
        undoStack.pop();
      }
      pendingAnnotationMark = null;
      savedSelectionRange = null;
    }

    function isSelectionInsideArticle(selection) {
      if (!selection || selection.isCollapsed || selection.rangeCount === 0) return false;
      const article = document.getElementById('wechatArticle');
      const range = selection.getRangeAt(0);
      return article.contains(range.commonAncestorContainer);
    }

    function positionAnnotationPopup(anchorEl) {
      const rect = anchorEl.getBoundingClientRect();
      const popup = document.getElementById('annotationPopup');
      const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
      const popupWidth = Math.min(280, Math.max(0, viewportWidth - 24));
      popup.style.display = 'flex';
      popup.style.width = popupWidth + 'px';
      popup.style.top = (window.scrollY + rect.bottom + 10) + 'px';

      let leftPos = window.scrollX + rect.left;
      const minLeft = window.scrollX + 12;
      const maxLeft = window.scrollX + viewportWidth - popupWidth - 12;
      if (leftPos > maxLeft) leftPos = maxLeft;
      if (leftPos < minLeft) leftPos = minLeft;
      popup.style.left = leftPos + 'px';
    }

    function handleAnnotationSelectionEvent(e) {
      if (!isAnnotating) return;
      
      // 不要在弹窗上触发
      if (e.target.closest('#annotationPopup')) return;
      // 不要在工具栏上触发
      if (e.target.closest('#toolbarContainer')) return;
      
      const selection = window.getSelection();
      if (!isSelectionInsideArticle(selection) || selection.toString().trim() === '') {
        return;
      }

      if (pendingAnnotationMark && pendingAnnotationMark.isConnected) {
        return;
      }

      clearPendingAnnotation();

      const range = selection.getRangeAt(0);
      saveState();
      savedSelectionRange = range.cloneRange();

      try {
        const mark = document.createElement('mark');
        mark.className = 'aim-annotation is-pending';
        savedSelectionRange.surroundContents(mark);
        pendingAnnotationMark = mark;
      } catch (err) {
        undoStack.pop();
        pendingAnnotationMark = null;
        savedSelectionRange = null;
        console.warn('批注选区高亮失败,可能由于跨节点选择', err);
        alert('暂不支持跨越多个段落的批注,请仅选择单一节点内的纯文本。');
        return;
      }

      positionAnnotationPopup(pendingAnnotationMark);
      setTimeout(() => document.getElementById('annotationInput').focus(), 50);
    }

    function scheduleAnnotationSelection(e, delay) {
      if (!isAnnotating) return;
      if (annotationSelectionTimer) clearTimeout(annotationSelectionTimer);
      annotationSelectionTimer = setTimeout(() => {
        annotationSelectionTimer = null;
        handleAnnotationSelectionEvent(e);
      }, delay);
    }

    articleEl.addEventListener('mouseup', function(e) {
      scheduleAnnotationSelection(e, 0);
    });

    articleEl.addEventListener('pointerup', function(e) {
      scheduleAnnotationSelection(e, 80);
    });

    articleEl.addEventListener('touchend', function(e) {
      const touch = e.changedTouches && e.changedTouches[0];
      const target = touch ? document.elementFromPoint(touch.clientX, touch.clientY) : e.target;
      const selectionEvent = { target: target || e.target };
      scheduleAnnotationSelection(selectionEvent, 180);
    }, { passive: true });

    document.addEventListener('selectionchange', function() {
      if (!isAnnotating) return;
      const selection = window.getSelection();
      if (!isSelectionInsideArticle(selection)) return;
      scheduleAnnotationSelection({ target: articleEl }, 260);
    });

    document.getElementById('btnCancelAnnotation').addEventListener('click', function() {
      clearPendingAnnotation();
      hideAnnotationPopup();
      window.getSelection().removeAllRanges();
    });

    document.getElementById('btnSaveAnnotation').addEventListener('click', function() {
      const comment = document.getElementById('annotationInput').value.trim();
      if (!comment || !pendingAnnotationMark) return;

      try {
        pendingAnnotationMark.className = 'aim-annotation';
        pendingAnnotationMark.setAttribute('data-comment', comment);
        createTooltip(pendingAnnotationMark);
        pendingAnnotationMark = null;
        saveToStorage();
      } catch (err) {
        console.warn('批注保存失败', err);
        alert('批注保存失败,请重新选择文本后再试。');
      }
      
      hideAnnotationPopup();
      window.getSelection().removeAllRanges();
    });

    // 创建批注悬浮气泡及其关闭与二次确认按钮
    function createTooltip(mark) {
      // 移除已有的临时气泡以确保升级和缓存兼容性
      const existing = mark.querySelector('.aim-tooltip');
      if (existing) existing.remove();

      const comment = mark.getAttribute('data-comment');
      if (!comment) return;

      const tooltip = document.createElement('span');
      tooltip.className = 'aim-tooltip';
      tooltip.setAttribute('contenteditable', 'false');

      const content = document.createElement('span');
      content.className = 'aim-tooltip-content';
      content.textContent = comment;

      const delBtn = document.createElement('span');
      delBtn.className = 'aim-tooltip-delete';
      delBtn.innerHTML = '&times;';
      delBtn.title = '删除此批注';

      const confirmSpan = document.createElement('span');
      confirmSpan.className = 'aim-tooltip-confirm';
      confirmSpan.style.display = 'none';
      confirmSpan.innerHTML = '确定删除? <span class="aim-confirm-yes" style="color: #ef4444; font-weight: bold; cursor: pointer; margin-right: 6px; margin-left: 4px;">是</span><span class="aim-confirm-no" style="color: #64748b; font-weight: bold; cursor: pointer;">否</span>';

      tooltip.appendChild(content);
      tooltip.appendChild(delBtn);
      tooltip.appendChild(confirmSpan);
      mark.appendChild(tooltip);
    }

    // 事件委托:处理批注删除按钮及二次确认的点击
    document.getElementById('wechatArticle').addEventListener('click', function(e) {
      // 1. 点击删除图标
      const delBtn = e.target.closest('.aim-tooltip-delete');
      if (delBtn) {
        e.stopPropagation();
        e.preventDefault();
        const tooltip = delBtn.closest('.aim-tooltip');
        if (tooltip) {
          const content = tooltip.querySelector('.aim-tooltip-content');
          const confirmSpan = tooltip.querySelector('.aim-tooltip-confirm');
          if (content && confirmSpan) {
            content.style.display = 'none';
            delBtn.style.display = 'none';
            confirmSpan.style.display = 'inline';
          }
        }
        return;
      }

      // 2. 点击取消删除
      const noBtn = e.target.closest('.aim-confirm-no');
      if (noBtn) {
        e.stopPropagation();
        e.preventDefault();
        const tooltip = noBtn.closest('.aim-tooltip');
        if (tooltip) {
          const content = tooltip.querySelector('.aim-tooltip-content');
          const delBtn = tooltip.querySelector('.aim-tooltip-delete');
          const confirmSpan = tooltip.querySelector('.aim-tooltip-confirm');
          if (content && delBtn && confirmSpan) {
            confirmSpan.style.display = 'none';
            content.style.display = 'inline';
            delBtn.style.display = 'inline';
          }
        }
        return;
      }

      // 3. 点击确认删除
      const yesBtn = e.target.closest('.aim-confirm-yes');
      if (yesBtn) {
        e.stopPropagation();
        e.preventDefault();
        const mark = yesBtn.closest('.aim-annotation');
        if (mark) {
          saveState();
          const clone = mark.cloneNode(true);
          const t = clone.querySelector('.aim-tooltip');
          if (t) t.remove();
          const textNode = document.createTextNode(clone.textContent);
          mark.parentNode.replaceChild(textNode, mark);
          saveToStorage();
        }
      }
    });

    async function copyAnnotations() {
      const status = document.querySelector("#copyStatus");
      const marks = document.querySelectorAll('.aim-annotation');
      if (marks.length === 0) {
        if (status) status.textContent = "无批注";
        setTimeout(() => { if (status) status.textContent = ""; }, 3000);
        return;
      }
      
      let markdown = "# 文章批注列表\n\n";
      marks.forEach((mark, index) => {
        const comment = mark.getAttribute('data-comment');
        
        const cleanMark = mark.cloneNode(true);
        const tooltip = cleanMark.querySelector('.aim-tooltip');
        if (tooltip) tooltip.remove();
        const text = cleanMark.textContent;
        
        const container = mark.closest('[data-role]') || mark.parentNode;
        
        let contextHtml = '';
        if (container) {
          const clone = container.cloneNode(true);
          // 清理冗余属性,方便丢给大模型处理(去除 style, contenteditable 等)
          clone.removeAttribute('contenteditable');
          clone.removeAttribute('style');
          clone.querySelectorAll('*').forEach(el => {
            el.removeAttribute('contenteditable');
            el.removeAttribute('style');
          });
          // 进一步精简 mark 标签,去除 class,并移除内部的 tooltip 气泡
          clone.querySelectorAll('mark.aim-annotation').forEach(el => {
            el.removeAttribute('class');
            const t = el.querySelector('.aim-tooltip');
            if (t) t.remove();
          });
          contextHtml = clone.outerHTML;
        }
        
        markdown += `## 批注 ${index + 1}\n`;
        markdown += `- **原文选段**:\`${text}\`\n`;
        markdown += `- **修改意见**:${comment}\n`;
        markdown += `- **上下文HTML**:\n\`\`\`html\n${contextHtml}\n\`\`\`\n\n`;
      });
      
      try {
        if (navigator.clipboard) {
          await navigator.clipboard.writeText(markdown);
          if (status) status.textContent = "批注已复制";
        }
      } catch (e) {
        if (status) status.textContent = "复制失败";
      }
      
      if (copyTimeout) clearTimeout(copyTimeout);
      copyTimeout = setTimeout(() => {
        if (status) status.textContent = "";
      }, 3000);
    }

    document.getElementById('toggleAnnotate').addEventListener('click', toggleAnnotateMode);
    document.getElementById('copyAnnotations').addEventListener('click', copyAnnotations);

    // ====================================================

    let copyTimeout = null;
    async function copyArticle() {
      const status = document.querySelector("#copyStatus");
      const article = document.querySelector("#wechatArticle");
      status.textContent = "正在处理图片...";
      if (copyTimeout) clearTimeout(copyTimeout);
      
      const clonedArticle = article.cloneNode(true);
      
      // 在克隆节点中彻底清理编辑相关的属性
      clonedArticle.removeAttribute('contenteditable');
      clonedArticle.querySelectorAll('[contenteditable]').forEach(el => {
        el.removeAttribute('contenteditable');
      });

      // 清理掉所有的批注标签,将其替换为纯文本,防止复制正文时把批注也带入到微信或剪贴板中
      clonedArticle.querySelectorAll('.aim-annotation').forEach(mark => {
        const tooltip = mark.querySelector('.aim-tooltip');
        if (tooltip) tooltip.remove();
        const textNode = document.createTextNode(mark.textContent);
        mark.parentNode.replaceChild(textNode, mark);
      });

      // 将 h1, h2, h3 转换为 p 标签,防止复制到微信后台后被微信默认样式重置并放大字号
      clonedArticle.querySelectorAll('h1, h2, h3').forEach(heading => {
        const p = document.createElement('p');
        p.innerHTML = heading.innerHTML;
        for (let attr of heading.attributes) {
          p.setAttribute(attr.name, attr.value);
        }
        heading.parentNode.replaceChild(p, heading);
      });

      const imageResult = await inlineImagesForCopy(article, clonedArticle);
      const articleStyle = clonedArticle.getAttribute('style') || '';
      const html = `<section style="${articleStyle}">${clonedArticle.innerHTML}</section>`;
      const text = article.innerText;
      
      try {
        if (navigator.clipboard && window.ClipboardItem) {
          const item = new ClipboardItem({
            "text/html": new Blob([html], { type: "text/html" }),
            "text/plain": new Blob([text], { type: "text/plain" })
          });
          await navigator.clipboard.write([item]);
          status.textContent = "已复制";
          copyTimeout = setTimeout(() => {
            status.textContent = "";
          }, 3000);
          return;
        }
        if (navigator.clipboard) {
          await navigator.clipboard.writeText(text);
          status.textContent = "已复制";
          copyTimeout = setTimeout(() => {
            status.textContent = "";
          }, 3000);
          return;
        }
        status.textContent = "请选择正文后手动复制";
      } catch (error) {
        status.textContent = "复制失败,请手动选择正文";
      }
    }

    document.querySelector("#themePreset").addEventListener("change", event => {
      const themeVal = event.target.value;
      applyTheme(themeVal);
      try {
        localStorage.setItem(themeStorageKey, themeVal);
      } catch (e) {
        console.warn('无法保存主题设置:', e);
      }
      const status = document.querySelector("#copyStatus");
      status.textContent = "主题已切换";
      if (copyTimeout) clearTimeout(copyTimeout);
      copyTimeout = setTimeout(() => {
        status.textContent = "";
      }, 3000);
    });
    document.querySelector("#copyWechat").addEventListener("click", copyArticle);
    
    // 初始化加载缓存数据与主题,并检测内容是否变更以使缓存失效
    try {
      const currentOriginalHTML = document.getElementById('wechatArticle').innerHTML;
      
      // 简单且高效的 String Hash 函数
      function getHtmlHash(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
          hash = (hash << 5) - hash + str.charCodeAt(i);
          hash |= 0;
        }
        return String(hash);
      }
      
      const currentHash = getHtmlHash(currentOriginalHTML);
      const savedHashKey = storageKey + '_hash';
      const savedHash = localStorage.getItem(savedHashKey);
      const savedHTML = localStorage.getItem(storageKey);
      
      if (savedHash === currentHash && savedHTML) {
        // 如果文件没有被重新生成(Hash相同),且存在修改缓存,则应用缓存
        document.getElementById('wechatArticle').innerHTML = savedHTML;
      } else {
        // 如果文件已更新(Hash不同)或无缓存,使用新内容并更新 Hash 缓存
        localStorage.setItem(savedHashKey, currentHash);
        localStorage.removeItem(storageKey);
      }
    } catch (e) {
      console.warn('无法加载缓存内容:', e);
    }

    try {
      document.querySelectorAll('.aim-annotation').forEach(createTooltip);
    } catch (e) {
      console.warn('无法初始化批注气泡:', e);
    }

    try {
      const savedTheme = localStorage.getItem(themeStorageKey);
      if (savedTheme) {
        document.getElementById('themePreset').value = savedTheme;
        applyTheme(savedTheme);
      } else {
        applyTheme("warm-editorial");
      }
    } catch (e) {
      applyTheme("warm-editorial");
    }