+
29
-

回答

下面我将为你提供一个完整的 Chrome 扩展项目结构、所有必需的代码文件以及详细的安装和使用说明。

项目结构

首先,在你的电脑上创建一个文件夹,例如 tutorial-maker-extension。在这个文件夹中,创建以下文件和子文件夹:

tutorial-maker-extension/
├── manifest.json            # 扩展的配置文件 (核心)
├── background.js            # 后台脚本,用于监听图标点击
├── content.js               # 内容脚本,注入到网页中实现核心功能
├── style.css                # 注入到网页中的样式文件
├── icons/                     # 存放图标的文件夹
│   ├── icon16.png
│   ├── icon48.png
│   └── icon128.png
└── libs/                      # 存放第三方库的文件夹
    ├── html2canvas.min.js
    └── jspdf.umd.min.js

准备工作:

下载库文件:

下载 html2canvas: 点击这里 (右键 -> 另存为...),保存到 libs 文件夹。

下载 jsPDF: 点击这里 (右键 -> 另存为...),保存到 libs 文件夹。

准备图标:

你需要准备三个尺寸的 PNG 图标:16x16, 48x48, 128x128 像素。你可以使用任何你喜欢的图标,或者在网上找一个免费的 "tutorial" 或 "camera" 图标。将它们命名为 icon16.png, icon48.png, icon128.png 并放入 icons 文件夹。

第1步: manifest.json

这是扩展的配置文件,告诉 Chrome 扩展的名称、权限、需要运行的脚本等。

{
  "manifest_version": 3,
  "name": "网页图文教程生成器",
  "version": "1.0",
  "description": "点击网页元素,自动生成带截图和文本的步骤教程,并导出为PDF。",
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    },
    "default_title": "制作图文教程"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": [
        "libs/html2canvas.min.js",
        "libs/jspdf.umd.min.js",
        "content.js"
      ],
      "css": ["style.css"]
    }
  ]
}

关键点解释:

"manifest_version": 3: 使用最新的 Manifest V3 规范。

"permissions": 我们只需要 activeTab 和 scripting 权限,这比请求所有网站的权限更安全。activeTab 允许我们在用户点击扩展图标时与当前活动的标签页交互。

"background": 指定了后台服务工作线程,它将处理点击扩展图标的事件。

"action": 定义了浏览器工具栏中扩展图标的行为。

"content_scripts": 这是核心部分。它告诉 Chrome 自动将 libs 里的库文件、我们自己的 content.js 和 style.css 注入到所有页面 (<all_urls>) 中。注意JS文件的顺序很重要,必须先加载库文件。

第2步: background.js

这个脚本在后台运行,它的唯一任务是监听用户点击扩展图标的动作,并向当前页面的内容脚本发送一条消息,告诉它“该干活了”。

// background.js

chrome.action.onClicked.addListener((tab) => {
  // 当用户点击扩展图标时
  // 向当前激活的标签页发送一个消息
  chrome.tabs.sendMessage(tab.id, {
    action: "toggle_tutorial_maker"
  });
});

第3步: style.css

将之前油猴脚本中的所有 CSS 规则复制到这个文件中。

/* style.css */

.wtm-sidebar {
    position: fixed;
    top: 0;
    right: -450px; /* Initially hidden */
    width: 400px;
    height: 100%;
    background-color: #f8f9fa;
    border-left: 1px solid #dee2e6;
    box-shadow: -5px 0 15px rgba(0,0,0,0.1);
    z-index: 2147483647; /* Max z-index */
    transition: right 0.5s ease;
    display: flex;
    flex-direction: column;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.wtm-sidebar.show {
    right: 0;
}

.wtm-sidebar-header {
    padding: 15px;
    background-color: #e9ecef;
    border-bottom: 1px solid #ced4da;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.wtm-sidebar-header h3 {
    margin: 0;
    font-size: 18px;
}
.wtm-sidebar-header .wtm-controls button {
    margin-left: 10px;
    padding: 5px 10px;
    cursor: pointer;
    border-radius: 4px;
    border: 1px solid transparent;
    font-size: 14px;
}
.wtm-record-btn {
    background-color: #007bff; color: white;
}
.wtm-record-btn.recording {
    background-color: #dc3545;
}
.wtm-export-btn {
    background-color: #28a745; color: white;
}

.wtm-steps-container {
    flex-grow: 1;
    overflow-y: auto;
    padding: 10px;
}

.wtm-step {
    background-color: white;
    border: 1px solid #ddd;
    border-radius: 5px;
    padding: 15px;
    margin-bottom: 10px;
    position: relative;
    cursor: grab;
}
.wtm-step.dragging {
    opacity: 0.5;
}

.wtm-step-header {
    font-weight: bold;
    margin-bottom: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.wtm-step-delete-btn {
    background: none;
    border: none;
    color: #dc3545;
    font-size: 20px;
    cursor: pointer;
    padding: 0 5px;
}

.wtm-step textarea {
    width: 100%;
    min-height: 50px;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 8px;
    font-size: 14px;
    resize: vertical;
    margin-bottom: 10px;
    box-sizing: border-box;
}

.wtm-step img {
    max-width: 100%;
    border: 1px solid #eee;
    margin-top: 10px;
    border-radius: 4px;
}
.wtm-click-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 123, 255, 0.1);
    border: 2px dashed #007bff;
    z-index: 2147483646;
    pointer-events: none; /* Important */
    cursor: crosshair;
}
.wtm-loader {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: rgba(0, 0, 0, 0.7);
    color: white;
    padding: 20px;
    border-radius: 8px;
    z-index: 2147483647;
    display: none; /* Initially hidden */
}

第4步: content.js

这是扩展的核心逻辑,与油猴脚本非常相似,但做了一些调整以适应扩展的架构。

// content.js

// 使用 jspdf 库
const { jsPDF } = window.jspdf;

let isRecording = false;
let stepCounter = 1;
let tutorialStepsContainer;
let sidebar;
let recordButton;
let uiInitialized = false;

/**
 * 创建并初始化UI界面 (只执行一次)
 */
function createUI() {
    if (document.querySelector('.wtm-sidebar')) return; // 防止重复创建

    // 创建侧边栏
    sidebar = document.createElement('div');
    sidebar.className = 'wtm-sidebar';

    // 侧边栏头部
    const header = document.createElement('div');
    header.className = 'wtm-sidebar-header';
    header.innerHTML = `
        <h3>教程编辑器</h3>
        <div class="wtm-controls">
            <button class="wtm-record-btn">开始录制</button>
            <button class="wtm-export-btn">导出 PDF</button>
        </div>
    `;
    sidebar.appendChild(header);

    // 步骤容器
    tutorialStepsContainer = document.createElement('div');
    tutorialStepsContainer.className = 'wtm-steps-container';
    sidebar.appendChild(tutorialStepsContainer);

    document.body.appendChild(sidebar);

    // 添加头部按钮事件
    recordButton = sidebar.querySelector('.wtm-record-btn');
    recordButton.addEventListener('click', toggleRecording);
    sidebar.querySelector('.wtm-export-btn').addEventListener('click', exportToPDF);

    // 添加拖拽排序功能
    enableDragAndDrop();

    uiInitialized = true;
}

/**
 * 切换录制状态
 */
function toggleRecording() {
    isRecording = !isRecording;
    if (isRecording) {
        recordButton.textContent = '停止录制';
        recordButton.classList.add('recording');
        document.addEventListener('click', handlePageClick, true);
        addRecordingOverlay();
    } else {
        recordButton.textContent = '开始录制';
        recordButton.classList.remove('recording');
        document.removeEventListener('click', handlePageClick, true);
        removeRecordingOverlay();
    }
}

/**
 * 切换侧边栏的显示/隐藏
 */
function toggleSidebar() {
    if (!uiInitialized) {
        createUI();
    }
    sidebar.classList.toggle('show');
    // 如果侧边栏被关闭时仍在录制,则停止录制
    if (!sidebar.classList.contains('show') && isRecording) {
        toggleRecording();
    }
}


// --- 以下代码与油猴脚本几乎完全相同 ---

/**
 * 处理页面点击事件,用于捕获步骤
 * @param {MouseEvent} event
 */
function handlePageClick(event) {
    if (event.target.closest('.wtm-sidebar')) {
        return;
    }
    event.preventDefault();
    event.stopPropagation();

    const clickedElement = event.target;
    let description = clickedElement.innerText || clickedElement.value || clickedElement.alt || '点击页面区域';
    description = description.trim().substring(0, 100);

    showLoader('正在截取屏幕...');
    setTimeout(() => {
        html2canvas(document.body, {
            windowWidth: document.documentElement.clientWidth,
            windowHeight: document.documentElement.clientHeight,
            x: window.scrollX,
            y: window.scrollY,
            useCORS: true // 尝试解决跨域图片问题
        }).then(canvas => {
            const imageDataUrl = canvas.toDataURL('image/png');
            addStepToSidebar(description, imageDataUrl);
            hideLoader();
        }).catch(err => {
            console.error("html2canvas error:", err);
            alert("截图失败,如果页面包含跨域图片可能导致此问题。请查看控制台日志。");
            hideLoader();
        });
    }, 100);
}

function addStepToSidebar(text, imageDataUrl) {
    const stepDiv = document.createElement('div');
    stepDiv.className = 'wtm-step';
    stepDiv.setAttribute('draggable', true);
    const currentStep = tutorialStepsContainer.children.length + 1;

    stepDiv.innerHTML = `
        <div class="wtm-step-header">
            <span class="wtm-step-number">步骤 ${currentStep}:</span>
            <button class="wtm-step-delete-btn" title="删除此步骤">&times;</button>
        </div>
        <textarea>${text}</textarea>
        <img src="${imageDataUrl}" alt="步骤 ${currentStep} 的截图">
    `;

    tutorialStepsContainer.appendChild(stepDiv);

    stepDiv.querySelector('.wtm-step-delete-btn').addEventListener('click', () => {
        stepDiv.remove();
        updateStepNumbers();
    });

    // 更新编号,并滚动到底部
    updateStepNumbers();
    tutorialStepsContainer.scrollTop = tutorialStepsContainer.scrollHeight;
}

function updateStepNumbers() {
    const steps = tutorialStepsContainer.querySelectorAll('.wtm-step');
    steps.forEach((step, index) => {
        step.querySelector('.wtm-step-number').textContent = `步骤 ${index + 1}:`;
        step.querySelector('img').alt = `步骤 ${index + 1} 的截图`;
    });
}

function enableDragAndDrop() {
    let draggedItem = null;
    tutorialStepsContainer.addEventListener('dragstart', e => {
        if (e.target.classList.contains('wtm-step')) {
            draggedItem = e.target;
            setTimeout(() => e.target.classList.add('dragging'), 0);
        }
    });
    tutorialStepsContainer.addEventListener('dragend', () => {
        if (draggedItem) {
            draggedItem.classList.remove('dragging');
            draggedItem = null;
            updateStepNumbers();
        }
    });
    tutorialStepsContainer.addEventListener('dragover', e => {
        e.preventDefault();
        const afterElement = getDragAfterElement(tutorialStepsContainer, e.clientY);
        const currentDragged = document.querySelector('.dragging');
        if (currentDragged) {
            if (afterElement == null) {
                tutorialStepsContainer.appendChild(currentDragged);
            } else {
                tutorialStepsContainer.insertBefore(currentDragged, afterElement);
            }
        }
    });
}

function getDragAfterElement(container, y) {
    const draggableElements = [...container.querySelectorAll('.wtm-step:not(.dragging)')];
    return draggableElements.reduce((closest, child) => {
        const box = child.getBoundingClientRect();
        const offset = y - box.top - box.height / 2;
        if (offset < 0 && offset > closest.offset) {
            return { offset: offset, element: child };
        } else {
            return closest;
        }
    }, { offset: Number.NEGATIVE_INFINITY }).element;
}

async function exportToPDF() {
    const steps = tutorialStepsContainer.querySelectorAll('.wtm-step');
    if (steps.length === 0) {
        alert('没有可导出的步骤!');
        return;
    }

    showLoader('正在生成 PDF...');
    const doc = new jsPDF();
    const page_margin = 15;
    const page_width = doc.internal.pageSize.getWidth() - 2 * page_margin;
    let y_pos = 20;

    doc.setFontSize(22);
    doc.text("网页操作教程", doc.internal.pageSize.getWidth() / 2, y_pos, { align: 'center' });
    y_pos += 20;

    for (let i = 0; i < steps.length; i++) {
        const step = steps[i];
        const stepNumberText = step.querySelector('.wtm-step-number').textContent;
        const description = step.querySelector('textarea').value;
        const imgData = step.querySelector('img').src;

        if (y_pos > 250) {
            doc.addPage();
            y_pos = 20;
        }

        doc.setFontSize(16);
        doc.setFont(undefined, 'bold');
        doc.text(stepNumberText, page_margin, y_pos);
        y_pos += 8;

        doc.setFontSize(12);
        doc.setFont(undefined, 'normal');
        const splitDescription = doc.splitTextToSize(description, page_width);
        doc.text(splitDescription, page_margin, y_pos);
        y_pos += splitDescription.length * 5 + 5;

        if (y_pos > 200) {
            doc.addPage();
            y_pos = 20;
        }

        try {
            const img = new Image();
            img.src = imgData;
            await new Promise(resolve => img.onload = resolve);
            const imgHeight = (img.height * page_width) / img.width;
            doc.addImage(imgData, 'PNG', page_margin, y_pos, page_width, imgHeight);
            y_pos += imgHeight + 15;
        } catch (error) {
             console.error("Error adding image to PDF: ", error);
             doc.text("图片加载失败", page_margin, y_pos);
             y_pos += 10;
        }
    }

    doc.save('图文教程.pdf');
    hideLoader();
}

function addRecordingOverlay() {
    if (document.querySelector('.wtm-click-overlay')) return;
    const overlay = document.createElement('div');
    overlay.className = 'wtm-click-overlay';
    document.body.appendChild(overlay);
    document.body.style.cursor = 'crosshair';
}

function removeRecordingOverlay() {
    const overlay = document.querySelector('.wtm-click-overlay');
    if (overlay) overlay.remove();
    document.body.style.cursor = 'default';
}

function showLoader(message) {
    let loader = document.querySelector('.wtm-loader');
    if (!loader) {
        loader = document.createElement('div');
        loader.className = 'wtm-loader';
        document.body.appendChild(loader);
    }
    loader.textContent = message;
    loader.style.display = 'block';
}

function hideLoader() {
    const loader = document.querySelector('.wtm-loader');
    if (loader) loader.style.display = 'none';
}


// --- 扩展消息监听器 ---
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "toggle_tutorial_maker") {
    toggleSidebar();
  }
});

与油猴脚本的主要区别:

启动方式: 不再是页面右下角的悬浮按钮。而是通过监听来自 background.js 的消息 (chrome.runtime.onMessage.addListener) 来触发 toggleSidebar() 函数。

UI创建时机: UI(侧边栏等)是懒加载的。只有在用户第一次点击扩展图标时,createUI() 才会被调用。

按钮变化: "制作图文教程" 按钮被移到了侧边栏的头部,并改名为 "开始录制"。整个扩展的控制中心都在侧边栏内,页面更加整洁。

Z-index: 为UI元素设置了非常高的 z-index (2147483647),以确保它能显示在绝大多数网站内容的上方。

第5步: 安装和测试扩展

打开扩展管理页面: 在 Chrome 浏览器中,输入 chrome://extensions 并回车。

启用开发者模式: 在页面右上角,打开“开发者模式”的开关。

加载扩展: 点击“加载已解压的扩展程序”按钮。

选择文件夹: 在弹出的文件选择器中,选择你创建的 tutorial-maker-extension 整个文件夹。

完成: 如果一切顺利,你应该能在扩展列表中看到“网页图文教程生成器”,并在浏览器工具栏上看到它的图标。

如何使用

打开任何你想要制作教程的网站。

点击浏览器工具栏上的扩展图标。

右侧会滑出教程编辑器侧边栏。

点击侧边栏顶部的“开始录制”按钮。

现在,你在页面上的每一次点击都会被记录下来,生成一个带截图和描述的步骤。

你可以随时在侧边栏编辑文本、拖拽调整步骤顺序或删除步骤。

完成后,点击“停止录制”,然后点击“导出 PDF”即可下载你的教程。

再次点击扩展图标可以隐藏侧边栏。

网友回复

我知道答案,我要回答