feat: v2.0 — 插件改造,集成 PPTX 导出

改造内容:
- 改名 Web to Pixso → Web to PPT
- 删除 pixso-plugin/、element-picker.js、popup-panel.js
- 新增 convert-browser.js(浏览器版转化逻辑)
- 新增 lib-pptxgen.js(pptxgenjs 浏览器 bundle)
- 重写 popup.js(触发捕获→转化→下载)
- 重写 background.js(缓存采集数据,支持 WEB_TO_PPT_GET_DATA)
- manifest.json 改名 + 添加 default_popup

流程:popup 点击导出 → background 抓取 DOM → popup 运行转化 → chrome.downloads 下载 PPTX
This commit is contained in:
李进
2026-07-27 13:54:11 +08:00
parent 330f4e96ce
commit e1c8b3d94e
16 changed files with 858 additions and 3214 deletions
+83 -162
View File
@@ -1,45 +1,32 @@
const SETTINGS_KEY = "webToPixsoSettings";
const HELP_URL = "https://z8qrcvi3n5.feishu.cn/wiki/RV8TwlhFyiGsEekQXk8cX5SHn6f";
const DEFAULT_SETTINGS = {
useProxy: false,
concurrency: "8",
captureMode: "mixed",
captureWidth: null
};
const SETTINGS_KEY = 'webToPixsoSettings';
const DEFAULT_SETTINGS = { useProxy: false, concurrency: '8', captureMode: 'mixed', captureWidth: null };
const MIN_CAPTURE_WIDTH = 320;
const MAX_CAPTURE_WIDTH = 3840;
const captureModeSelect = document.getElementById("captureMode");
const proxyToggle = document.getElementById("proxyToggle");
const captureWidthInput = document.getElementById("captureWidth");
const concurrencySelect = document.getElementById("concurrency");
const captureBtn = document.getElementById("captureBtn");
const btnText = document.getElementById("btnText");
const selectBtn = document.getElementById("selectBtn");
const selectBtnText = document.getElementById("selectBtnText");
const status = document.getElementById("status");
const statusText = document.getElementById("statusText");
const progressFill = document.getElementById("progressFill");
const closeBtn = document.getElementById("closeBtn");
const helpLink = document.getElementById("helpLink");
const captureWidthInput = document.getElementById('captureWidth');
const exportBtn = document.getElementById('exportBtn');
const btnText = document.getElementById('btnText');
const status = document.getElementById('status');
const statusText = document.getElementById('statusText');
const progressFill = document.getElementById('progressFill');
function normalizeCaptureWidth(value, fallback = null) {
const number = Number.parseInt(String(value || '').replace(/\D+/g, ''), 10);
if (!Number.isFinite(number)) return fallback;
return Math.max(MIN_CAPTURE_WIDTH, Math.min(MAX_CAPTURE_WIDTH, number));
}
function normalizeSettings(value = {}) {
const concurrency = String(value.concurrency || DEFAULT_SETTINGS.concurrency);
const captureWidth = normalizeCaptureWidth(value.captureWidth, null);
return {
useProxy: Boolean(value.useProxy),
concurrency: ["4", "6", "8", "10", "12", "16", "20", "infinite"].includes(concurrency)
? concurrency
: DEFAULT_SETTINGS.concurrency,
captureMode: value.captureMode === "editable" ? "editable" : "mixed",
captureWidth
captureMode: 'mixed',
captureWidth: normalizeCaptureWidth(value.captureWidth, null)
};
}
function normalizeCaptureWidth(value, fallback = null) {
const number = Number.parseInt(String(value || "").replace(/\D+/g, ""), 10);
if (!Number.isFinite(number)) return fallback;
return Math.max(MIN_CAPTURE_WIDTH, Math.min(MAX_CAPTURE_WIDTH, number));
async function getSettings() {
const result = await chrome.storage.local.get({ [SETTINGS_KEY]: DEFAULT_SETTINGS });
return normalizeSettings(result[SETTINGS_KEY]);
}
async function getCurrentViewportWidth() {
@@ -51,18 +38,7 @@ async function getCurrentViewportWidth() {
func: () => Math.round(window.innerWidth || document.documentElement.clientWidth || 0)
});
return normalizeCaptureWidth(result, null);
} catch {
return null;
}
}
async function getSettings() {
const result = await chrome.storage.local.get({ [SETTINGS_KEY]: DEFAULT_SETTINGS });
return normalizeSettings(result[SETTINGS_KEY]);
}
async function saveSettings(settings) {
await chrome.storage.local.set({ [SETTINGS_KEY]: normalizeSettings(settings) });
} catch { return null; }
}
function setProgress(percent, text) {
@@ -72,150 +48,95 @@ function setProgress(percent, text) {
}
function setBusy(isBusy) {
captureBtn.disabled = isBusy;
selectBtn.disabled = isBusy;
captureBtn.classList.toggle("loading", isBusy);
selectBtn.classList.toggle("loading", isBusy);
captureBtn.classList.remove("success", "error");
btnText.textContent = isBusy ? "采集中..." : "开始采集";
selectBtnText.textContent = isBusy ? "正在打开浮窗..." : "打开页面浮窗";
exportBtn.disabled = isBusy;
exportBtn.classList.toggle('loading', isBusy);
exportBtn.classList.remove('success', 'error');
btnText.textContent = isBusy ? '处理中...' : '导出 PPTX';
}
function setResult(kind, text) {
captureBtn.classList.remove("loading", "success", "error");
captureBtn.classList.add(kind);
exportBtn.classList.remove('loading', 'success', 'error');
exportBtn.classList.add(kind);
btnText.textContent = text;
}
async function syncSettingsFromUI() {
await saveSettings({
captureMode: captureModeSelect.value,
useProxy: proxyToggle.checked,
concurrency: concurrencySelect.value,
captureWidth: normalizeCaptureWidth(captureWidthInput.value, null)
});
}
function sanitizeCaptureWidthInput({ clamp = false } = {}) {
const digits = captureWidthInput.value.replace(/\D+/g, "");
captureWidthInput.value = digits;
const width = normalizeCaptureWidth(digits, null);
const rawNumber = Number.parseInt(digits, 10);
let invalid = Boolean(digits) && Number.isFinite(rawNumber) &&
(rawNumber < MIN_CAPTURE_WIDTH || rawNumber > MAX_CAPTURE_WIDTH);
captureWidthInput.classList.toggle("invalid", invalid);
if (clamp && digits) {
captureWidthInput.value = String(width);
invalid = false;
captureWidthInput.classList.remove("invalid");
}
return width;
}
function readSettingsFromUI() {
return normalizeSettings({
captureMode: captureModeSelect.value,
useProxy: proxyToggle.checked,
concurrency: concurrencySelect.value,
captureWidth: sanitizeCaptureWidthInput({ clamp: true })
});
}
async function startCapture() {
async function exportPptx() {
setBusy(true);
setProgress(12, "准备当前网页...");
setProgress(5, '准备采集...');
const settings = readSettingsFromUI();
await saveSettings(settings);
const captureWidth = normalizeCaptureWidth(captureWidthInput.value, null);
const settings = { ...DEFAULT_SETTINGS, captureWidth };
try {
setProgress(28, "注入采集脚本...");
// 1. 抓取 DOM
setProgress(15, '正在采集页面...');
const response = await chrome.runtime.sendMessage({
type: "PIXSO_CAPTURE_START",
type: 'PIXSO_CAPTURE_START',
options: settings
});
if (!response || !response.ok) {
throw new Error(response?.error || "采集失败");
throw new Error(response?.error || '采集失败');
}
const viewportText = response.actualViewportWidth
? `已按 ${response.actualViewportWidth}px 采集`
: "已按当前页面尺寸采集";
setProgress(100, `${viewportText}${response.filename}`);
setResult("success", "采集完成");
setTimeout(() => window.close(), 900);
// 2. 获取 JSON 数据(从 background 返回的文件路径读取不了,直接从 background 拿数据)
// 需要让 background 返回 JSON 数据而不是文件
setProgress(40, '采集完成,正在转化...');
// background 已经下载了 JSON 文件,但我们需要数据来转化
// 用另一个消息获取数据
const dataResponse = await chrome.runtime.sendMessage({
type: 'WEB_TO_PPT_GET_DATA'
});
if (!dataResponse || !dataResponse.ok) {
throw new Error(dataResponse?.error || '获取数据失败');
}
const jsonData = dataResponse.data;
// 3. 转化
setProgress(50, '正在生成 PPTX...');
const schema = await WebToPPT.convertToPptx(jsonData, (msg) => {
setProgress(50 + Math.min(40, Math.floor(Math.random() * 40)), msg);
});
// 4. 生成 PPTX
setProgress(90, '正在打包...');
const blob = await WebToPPT.schemaToPptxBlob(schema);
// 5. 下载
setProgress(95, '正在下载...');
const url = URL.createObjectURL(blob);
const title = jsonData.source?.title || 'webpage';
const safeTitle = title.replace(/[\\/:*?"<>|]+/g, '-').replace(/\s+/g, '-').slice(0, 64) || 'webpage';
const filename = `web-to-ppt/${safeTitle}-${Date.now()}.pptx`;
await chrome.downloads.download({ url, filename, saveAs: true });
URL.revokeObjectURL(url);
setProgress(100, `已导出:${filename}`);
setResult('success', '导出完成');
setTimeout(() => window.close(), 1500);
} catch (error) {
setProgress(0, error.message || String(error));
setResult("error", "采集失败");
setResult('error', '导出失败');
setTimeout(() => {
setBusy(false);
status.hidden = true;
progressFill.style.width = "0";
}, 2600);
progressFill.style.width = '0';
}, 3000);
}
}
async function startFloatingToolbar() {
setBusy(true);
setProgress(18, "正在打开页面采集浮窗...");
const settings = readSettingsFromUI();
await saveSettings(settings);
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) throw new Error("没有可选择的当前标签页");
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["element-picker.js"]
});
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: pickerSettings => window.__webToPixsoShowCaptureToolbar?.(pickerSettings),
args: [settings]
});
setProgress(100, "请在页面浮窗中选择整页或元素采集。");
setTimeout(() => window.close(), 500);
} catch (error) {
setProgress(0, error.message || String(error));
setResult("error", "采集失败");
setTimeout(() => {
setBusy(false);
status.hidden = true;
progressFill.style.width = "0";
}, 2600);
}
}
document.addEventListener("DOMContentLoaded", async () => {
const settings = await getSettings();
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
const currentWidth = await getCurrentViewportWidth();
captureModeSelect.value = settings.captureMode;
proxyToggle.checked = settings.useProxy;
concurrencySelect.value = settings.concurrency;
captureWidthInput.value = String(settings.captureWidth || currentWidth || "");
captureWidthInput.value = String(currentWidth || '');
});
captureModeSelect.addEventListener("change", syncSettingsFromUI);
proxyToggle.addEventListener("change", syncSettingsFromUI);
concurrencySelect.addEventListener("change", syncSettingsFromUI);
captureWidthInput.addEventListener("input", () => {
sanitizeCaptureWidthInput();
syncSettingsFromUI();
});
captureWidthInput.addEventListener("blur", () => {
sanitizeCaptureWidthInput({ clamp: true });
syncSettingsFromUI();
});
captureBtn.addEventListener("click", startCapture);
selectBtn.addEventListener("click", startFloatingToolbar);
closeBtn.addEventListener("click", () => window.close());
helpLink?.addEventListener("click", async event => {
event.preventDefault();
await chrome.tabs.create({ url: HELP_URL });
window.close();
exportBtn.addEventListener('click', exportPptx);
captureWidthInput.addEventListener('input', () => {
captureWidthInput.value = captureWidthInput.value.replace(/\D+/g, '');
});