(function () { "use strict"; const ROOT_ID = "__web_to_pixso_panel_root__"; const SETTINGS_KEY = "webToPixsoSettings"; const MIN_CAPTURE_WIDTH = 320; const MAX_CAPTURE_WIDTH = 3840; const DEFAULT_SETTINGS = { useProxy: false, concurrency: "8", captureMode: "mixed", 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)); } function normalizeSettings(value = {}) { const concurrency = String(value.concurrency || DEFAULT_SETTINGS.concurrency); 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: normalizeCaptureWidth(value.captureWidth, null) }; } function css() { return ` :host { all: initial; color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; } .backdrop { background: transparent; inset: 0; pointer-events: none; position: fixed; z-index: 2147483647; } .panel { background: #fff; border: 1px solid rgba(10, 10, 18, 0.14); border-radius: 22px; box-shadow: 0 18px 46px rgba(10, 10, 18, 0.18); box-sizing: border-box; color: #1a1a2e; overflow: hidden; pointer-events: auto; position: fixed; right: 28px; top: 24px; width: 320px; } .header { align-items: center; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; padding: 16px 20px 15px; } .logo-title { align-items: center; display: flex; gap: 10px; } .logo { border-radius: 10px; box-shadow: 0 4px 12px rgba(10, 10, 18, 0.12); display: block; height: 28px; object-fit: cover; width: 28px; } .title { color: #1a1a2e; font-size: 16px; font-weight: 650; line-height: 1; } .version-badge { background: #f2f4ff; border: 1px solid #dfe5ff; border-radius: 999px; color: #2450ff; font-size: 10px; font-weight: 600; line-height: 1; padding: 3px 6px; white-space: nowrap; } button, select, input { font: inherit; } .close-btn { align-items: center; background: transparent; border: 0; border-radius: 12px; color: #999; cursor: pointer; display: flex; font-size: 22px; height: 28px; justify-content: center; line-height: 1; transition: background 0.2s, color 0.2s; width: 28px; } .close-btn:hover { background: #f4f4f6; color: #666; } .content { padding: 20px; } .setting-row { align-items: center; display: flex; justify-content: space-between; margin-bottom: 16px; } .setting-label { color: #1a1a2e; font-size: 14px; } .setting-select { appearance: none; background: #fff; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-position: right 10px center; background-repeat: no-repeat; border: 1px solid #e4e4e4; border-radius: 12px; color: #1a1a2e; cursor: pointer; font-size: 14px; min-width: 80px; outline: none; padding: 8px 30px 8px 13px; } .mode-select { min-width: 116px; } .toggle-switch { display: inline-block; height: 26px; position: relative; width: 48px; } .toggle-switch input { height: 0; opacity: 0; width: 0; } .toggle-slider { background-color: #e4e4e4; border-radius: 999px; bottom: 0; cursor: pointer; left: 0; position: absolute; right: 0; top: 0; transition: 0.3s; } .toggle-slider::before { background-color: #fff; border-radius: 50%; bottom: 3px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); content: ""; height: 20px; left: 3px; position: absolute; transition: 0.3s; width: 20px; } input:checked + .toggle-slider { background-color: #1a1a2e; } input:checked + .toggle-slider::before { transform: translateX(22px); } .width-input-wrap { align-items: center; background: #fff; border: 1px solid #e4e4e4; border-radius: 12px; display: flex; height: 36px; min-width: 116px; padding: 0 10px 0 12px; } .width-input { background: transparent; border: 0; color: #1a1a2e; font-size: 14px; min-width: 0; outline: none; text-align: right; width: 68px; } .width-input.invalid { color: #ef4444; } .width-unit { color: #999; font-size: 12px; margin-left: 5px; } .description { color: #999; font-size: 12px; line-height: 1.6; margin-bottom: 20px; } .capture-btn, .select-btn { border: 0; border-radius: 12px; cursor: pointer; font-size: 15px; font-weight: 600; padding: 14px 24px; transition: background 0.2s, transform 0.2s; width: 100%; } .capture-btn { background: #1a1a2e; color: #fff; } .capture-btn:hover { background: #2d2d44; transform: translateY(-1px); } .select-btn { background: #f4f4f6; color: #1a1a2e; margin-top: 10px; } .select-btn:hover { background: #ececf1; transform: translateY(-1px); } .capture-btn:disabled, .select-btn:disabled { cursor: not-allowed; opacity: 0.6; transform: none; } .capture-btn.success { background: #10b981; } .capture-btn.error { background: #ef4444; } .status { margin-top: 16px; padding: 12px 0 0; } .progress-bar { background: #f0f0f0; border-radius: 999px; height: 4px; margin-bottom: 8px; overflow: hidden; } .progress-fill { background: linear-gradient(90deg, #00d2ff, #7c3aed); border-radius: 999px; height: 100%; transition: width 0.3s ease; width: 0; } .status-text { color: #666; font-size: 12px; } .help-link { align-items: center; border: 1px solid #e6e8f2; border-radius: 12px; color: #2450ff; display: flex; font-size: 12px; font-weight: 600; justify-content: center; margin-top: 16px; padding: 10px 12px; text-decoration: none; transition: background 0.18s, border-color 0.18s, color 0.18s; width: 100%; } .help-link:hover { background: #f6f8ff; border-color: #dfe5ff; } .footer { align-items: center; background: #fff; border-top: 1px solid #f0f0f0; color: #666; display: flex; font-size: 12px; gap: 8px; justify-content: space-between; padding: 12px 20px; } .support-email { color: #888; font-size: 10px; line-height: 1.35; min-width: 0; text-align: right; text-decoration: none; } .support-email:hover { color: #2450ff; } `; } function panelHtml() { return `
Web to Pixso v1.1.1
采集模式
跨域图片代理模式
页面采集宽度
图片采集并发

页面采集宽度默认使用当前窗口宽度,可输入 320-3840px 触发响应式布局后采集。

使用说明
`; } function getField(root, name) { return root.shadowRoot.querySelector(`[data-field="${name}"]`); } function sanitizeWidth(input, clamp = false) { const digits = input.value.replace(/\D+/g, ""); input.value = digits; const raw = Number.parseInt(digits, 10); const invalid = Boolean(digits) && Number.isFinite(raw) && (raw < MIN_CAPTURE_WIDTH || raw > MAX_CAPTURE_WIDTH); input.classList.toggle("invalid", invalid); const normalized = normalizeCaptureWidth(digits, null); if (clamp && digits) { input.value = String(normalized); input.classList.remove("invalid"); } return normalized; } 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) }); } function readSettings(root, options = {}) { const { clampWidth = false } = options; return normalizeSettings({ captureMode: getField(root, "captureMode").value, useProxy: getField(root, "useProxy").checked, concurrency: getField(root, "concurrency").value, captureWidth: sanitizeWidth(getField(root, "captureWidth"), clampWidth) }); } function setProgress(root, percent, text) { const status = root.shadowRoot.querySelector(".status"); const fill = root.shadowRoot.querySelector(".progress-fill"); const statusText = root.shadowRoot.querySelector(".status-text"); status.hidden = false; fill.style.width = `${Math.max(0, Math.min(100, percent))}%`; statusText.textContent = text; } function setBusy(root, isBusy) { const captureButton = root.shadowRoot.querySelector(".capture-btn"); const selectButton = root.shadowRoot.querySelector(".select-btn"); captureButton.disabled = isBusy; selectButton.disabled = isBusy; captureButton.classList.remove("success", "error"); captureButton.textContent = isBusy ? "采集中..." : "开始采集"; selectButton.textContent = isBusy ? "处理中..." : "打开页面浮窗"; } function setResult(root, kind, text) { const captureButton = root.shadowRoot.querySelector(".capture-btn"); captureButton.classList.remove("success", "error"); captureButton.classList.add(kind); captureButton.textContent = text; } async function startCapture(root) { setBusy(root, true); setProgress(root, 12, "准备当前网页..."); const settings = readSettings(root, { clampWidth: true }); await saveSettings(settings); try { setProgress(root, 28, "注入采集脚本..."); const response = await chrome.runtime.sendMessage({ type: "PIXSO_CAPTURE_CURRENT_TAB", options: settings }); if (!response?.ok) throw new Error(response?.error || "采集失败"); const widthText = response.actualViewportWidth ? `已按 ${response.actualViewportWidth}px 采集` : "采集完成"; setProgress(root, 100, `${widthText}:${response.filename || ""}`); setResult(root, "success", "采集完成"); setTimeout(() => root.remove(), 900); } catch (error) { setProgress(root, 0, error.message || String(error)); setResult(root, "error", "采集失败"); setTimeout(() => setBusy(root, false), 2600); } } async function openToolbar(root) { const settings = readSettings(root, { clampWidth: true }); await saveSettings(settings); root.remove(); window.__webToPixsoShowCaptureToolbar?.(settings); } async function bind(root) { const settings = await getSettings(); getField(root, "captureMode").value = settings.captureMode; getField(root, "useProxy").checked = settings.useProxy; getField(root, "concurrency").value = settings.concurrency; getField(root, "captureWidth").value = String(settings.captureWidth || Math.round(window.innerWidth || document.documentElement.clientWidth || 1440)); for (const field of ["captureMode", "useProxy", "concurrency"]) { getField(root, field).addEventListener("change", () => saveSettings(readSettings(root))); } getField(root, "captureWidth").addEventListener("input", event => { sanitizeWidth(event.currentTarget); saveSettings(readSettings(root, { clampWidth: false })); }); getField(root, "captureWidth").addEventListener("blur", event => { sanitizeWidth(event.currentTarget, true); saveSettings(readSettings(root, { clampWidth: true })); }); root.shadowRoot.querySelector(".close-btn").addEventListener("click", () => root.remove()); root.shadowRoot.querySelector(".capture-btn").addEventListener("click", () => startCapture(root)); root.shadowRoot.querySelector(".select-btn").addEventListener("click", () => openToolbar(root)); } window.__webToPixsoShowPanel = async function showPanel() { document.getElementById(ROOT_ID)?.remove(); const root = document.createElement("div"); root.id = ROOT_ID; const shadow = root.attachShadow({ mode: "open" }); shadow.innerHTML = `${panelHtml()}`; document.documentElement.appendChild(root); await bind(root); }; })();