522 lines
16 KiB
JavaScript
522 lines
16 KiB
JavaScript
(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 `
|
||
<div class="backdrop">
|
||
<main class="panel" role="dialog" aria-label="Web to Pixso">
|
||
<div class="header">
|
||
<div class="logo-title">
|
||
<img src="${chrome.runtime.getURL("logo/plugin-logo.png")}" alt="" class="logo">
|
||
<span class="title">Web to Pixso</span>
|
||
<span class="version-badge">v1.1.1</span>
|
||
</div>
|
||
<button class="close-btn" type="button" aria-label="关闭">×</button>
|
||
</div>
|
||
<div class="content">
|
||
<div class="setting-row">
|
||
<span class="setting-label">采集模式</span>
|
||
<select class="setting-select mode-select" data-field="captureMode">
|
||
<option value="mixed">混合高保真</option>
|
||
<option value="editable">可编辑优先</option>
|
||
</select>
|
||
</div>
|
||
<div class="setting-row">
|
||
<span class="setting-label">跨域图片代理模式</span>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" data-field="useProxy">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="setting-row">
|
||
<span class="setting-label">页面采集宽度</span>
|
||
<label class="width-input-wrap">
|
||
<input class="width-input" data-field="captureWidth" type="text" inputmode="numeric" autocomplete="off">
|
||
<span class="width-unit">px</span>
|
||
</label>
|
||
</div>
|
||
<div class="setting-row">
|
||
<span class="setting-label">图片采集并发</span>
|
||
<select class="setting-select" data-field="concurrency">
|
||
<option value="4">4</option>
|
||
<option value="6">6</option>
|
||
<option value="8">8</option>
|
||
<option value="10">10</option>
|
||
<option value="12">12</option>
|
||
<option value="16">16</option>
|
||
<option value="20">20</option>
|
||
<option value="infinite">无限</option>
|
||
</select>
|
||
</div>
|
||
<p class="description">页面采集宽度默认使用当前窗口宽度,可输入 320-3840px 触发响应式布局后采集。</p>
|
||
<button class="capture-btn" type="button">开始采集</button>
|
||
<button class="select-btn" type="button">打开页面浮窗</button>
|
||
<div class="status" hidden>
|
||
<div class="progress-bar" aria-hidden="true"><div class="progress-fill"></div></div>
|
||
<span class="status-text">准备中...</span>
|
||
</div>
|
||
<a class="help-link" href="https://z8qrcvi3n5.feishu.cn/wiki/RV8TwlhFyiGsEekQXk8cX5SHn6f" target="_blank" rel="noopener noreferrer">使用说明</a>
|
||
</div>
|
||
<div class="footer">
|
||
<span>by 大非</span>
|
||
<a class="support-email" href="mailto:270310136@qq.com">270310136@qq.com 给我发邮件哦,我光速改</a>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 = `<style>${css()}</style>${panelHtml()}`;
|
||
document.documentElement.appendChild(root);
|
||
await bind(root);
|
||
};
|
||
})();
|