// ==UserScript== // @name 网站快照存储与恢复助手 // @namespace https://scriptcat.org/zh-CN/script-show-page/7633 // @version 1.0.1 // @description 针对指定网站实现快照(Cookie、LocalStorage、SessionStorage)的一键存储、命名、加密备份与一键恢复 // @author MOYEFU // @license MIT // @match http*://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM_cookie // @grant GM_setClipboard // @tag MYF // @run-at document-idle // @noframes // ==/UserScript== /* ==UserConfig== Config: show_host: title: 显示的主机 (多个用换行区分) description: 每行一条,支持通配符 * ,例:https://*.example.org*;不填写则所有网站默认不显示 type: textarea default: "" enable_encryption: title: 本地数据加密 description: 启用 AES-GCM 256 位本地数据加密存储 type: checkbox default: true auto_reload_after_restore: title: 恢复后直接刷新/跳转 description: 恢复快照成功后直接刷新或跳转至来源页面(不再弹窗确认) type: checkbox default: false ==/UserConfig== */ // 全局暴露的 UI 实例,供菜单命令与外部调度使用 let LSM_UI = null; (async () => { "use strict"; // ========================================================================= // 0. 用户配置:show_host(每行一条,支持 * 通配符)→ 默认所有网站不显示, // 仅匹配到列表中的网站才运行脚本 // ========================================================================= function hostBlocked() { try { const raw = GM_getValue("Config.show_host", ""); const lines = String(raw || "") .split("\n") .map((s) => s.trim()) .filter(Boolean); if (!lines.length) return true; // 未配置任何显示网站 → 默认全部不显示 const candidates = [ location.href, location.origin, location.protocol + "//" + location.host, location.host, location.hostname ]; const matched = lines.some((line) => { const re = new RegExp( "^" + line.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$", "i" ); return candidates.some((c) => re.test(c)); }); return !matched; } catch { return true; } } // 始终注册菜单命令(ScriptCat/Tampermonkey 菜单),按当前状态分发弹窗 GM_registerMenuCommand("🔑 快照管理助手", () => { if (hostBlocked()) { showBlockedDialog(); } else { showMainDialog(); } }); if (hostBlocked()) return; initApp(); })(); // 把当前站点(origin)加入 show_host 显示列表(永久开启) function addHostToShowList() { try { const raw = GM_getValue("Config.show_host", ""); const lines = String(raw || "") .split("\n") .map((s) => s.trim()) .filter(Boolean); const entry = location.origin; if (!lines.some((l) => l === entry)) { lines.push(entry); GM_setValue("Config.show_host", lines.join("\n")); } } catch (e) { console.error("[LSM] 写入配置失败:", e); } } // 把当前站点从 show_host 显示列表中移除(永久关闭) function removeHostFromShowList() { try { const raw = GM_getValue("Config.show_host", ""); const lines = String(raw || "") .split("\n") .map((s) => s.trim()) .filter(Boolean); if (!lines.length) return; const candidates = [ location.href, location.origin, location.protocol + "//" + location.host, location.host, location.hostname ]; const kept = lines.filter((line) => { const re = new RegExp( "^" + line.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$", "i" ); return !candidates.some((c) => re.test(c)); }); GM_setValue("Config.show_host", kept.join("\n")); } catch (e) { console.error("[LSM] 移除配置失败:", e); } } // 本站在 show_host 之外被禁用时的处理:弹窗选择「临时显示」或「永久开启」 function showBlockedDialog() { if (document.querySelector(".lsm-dlg-mask")) return; const mask = document.createElement("div"); mask.className = "lsm-dlg-mask"; mask.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:rgba(15,23,42,0.55);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);" + "display:flex;align-items:center;justify-content:center;overscroll-behavior:contain;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;"; mask.addEventListener("wheel", (e) => { e.preventDefault(); e.stopPropagation(); }, { passive: false }); const box = document.createElement("div"); box.style.cssText = "width:360px;max-width:calc(100vw - 40px);background:#ffffff;border-radius:16px;" + "padding:24px;box-shadow:0 20px 45px -10px rgba(15,23,42,0.25),0 0 0 1px rgba(15,23,42,0.06);box-sizing:border-box;animation:lsmFadeIn .2s ease-out;"; const title = document.createElement("div"); title.innerHTML = "🔑 快照管理助手未激活"; title.style.cssText = "margin-bottom:10px;display:flex;align-items:center;gap:6px;"; const desc = document.createElement("div"); desc.textContent = "当前网站不在「显示的主机」配置白名单中。你可以选择:"; desc.style.cssText = "font-size:13px;color:#64748b;line-height:1.6;margin-bottom:18px;"; const tempBtn = document.createElement("button"); tempBtn.textContent = "临时显示(仅本次生效)"; tempBtn.style.cssText = "display:block;width:100%;padding:10px 0;margin-bottom:10px;border:none;border-radius:10px;" + "background:linear-gradient(135deg,#3b82f6,#2563eb);color:#ffffff;font-size:13px;cursor:pointer;font-weight:600;box-shadow:0 2px 8px rgba(37,99,235,0.25);"; const permBtn = document.createElement("button"); permBtn.textContent = "永久开启(加入显示列表)"; permBtn.style.cssText = "display:block;width:100%;padding:10px 0;border:1px solid #e2e8f0;border-radius:10px;" + "background:#f8fafc;color:#1e293b;font-size:13px;cursor:pointer;font-weight:600;"; const cancelBtn = document.createElement("button"); cancelBtn.textContent = "取消"; cancelBtn.style.cssText = "display:block;width:100%;padding:10px 0;margin-top:8px;border:none;background:none;" + "color:#94a3b8;font-size:12px;cursor:pointer;"; const close = () => mask.remove(); tempBtn.addEventListener("click", () => { close(); initApp(); }); permBtn.addEventListener("click", () => { close(); addHostToShowList(); initApp(); }); cancelBtn.addEventListener("click", close); mask.addEventListener("click", (e) => { if (e.target === mask) close(); }); box.append(title, desc, tempBtn, permBtn, cancelBtn); mask.appendChild(box); document.documentElement.appendChild(mask); } // 脚本正常运行时的菜单弹窗:打开管理窗 / 临时关闭 / 永久关闭 function showMainDialog() { if (document.querySelector(".lsm-dlg-mask")) return; const mask = document.createElement("div"); mask.className = "lsm-dlg-mask"; mask.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:rgba(15,23,42,0.55);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);" + "display:flex;align-items:center;justify-content:center;overscroll-behavior:contain;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;"; mask.addEventListener("wheel", (e) => { e.preventDefault(); e.stopPropagation(); }, { passive: false }); const box = document.createElement("div"); box.style.cssText = "width:360px;max-width:calc(100vw - 40px);background:#ffffff;border-radius:16px;" + "padding:24px;box-shadow:0 20px 45px -10px rgba(15,23,42,0.25),0 0 0 1px rgba(15,23,42,0.06);box-sizing:border-box;"; const title = document.createElement("div"); title.innerHTML = "🔑 快照管理助手"; title.style.cssText = "margin-bottom:10px;display:flex;align-items:center;gap:6px;"; const desc = document.createElement("div"); desc.textContent = "当前网站已加入「显示的主机」,功能就绪。你可以选择:"; desc.style.cssText = "font-size:13px;color:#64748b;line-height:1.6;margin-bottom:18px;"; const openBtn = document.createElement("button"); openBtn.textContent = "打开管理窗口"; openBtn.style.cssText = "display:block;width:100%;padding:10px 0;margin-bottom:10px;border:none;border-radius:10px;" + "background:linear-gradient(135deg,#3b82f6,#2563eb);color:#ffffff;font-size:13px;cursor:pointer;font-weight:600;box-shadow:0 2px 8px rgba(37,99,235,0.25);"; const tmpBtn = document.createElement("button"); tmpBtn.textContent = "临时隐藏悬浮球(刷新后恢复)"; tmpBtn.style.cssText = "display:block;width:100%;padding:10px 0;margin-bottom:10px;border:1px solid #e2e8f0;border-radius:10px;" + "background:#f8fafc;color:#334155;font-size:13px;cursor:pointer;font-weight:500;"; const permBtn = document.createElement("button"); permBtn.textContent = "永久关闭(从显示列表移除)"; permBtn.style.cssText = "display:block;width:100%;padding:10px 0;border:1px solid #fecdd3;border-radius:10px;" + "background:#fff1f2;color:#e11d48;font-size:13px;cursor:pointer;font-weight:500;"; const cancelBtn = document.createElement("button"); cancelBtn.textContent = "取消"; cancelBtn.style.cssText = "display:block;width:100%;padding:10px 0;margin-top:8px;border:none;background:none;" + "color:#94a3b8;font-size:12px;cursor:pointer;"; const close = () => mask.remove(); const hideAll = () => { if (LSM_UI) { if (LSM_UI.ball) LSM_UI.ball.style.display = "none"; if (LSM_UI.win) { LSM_UI.win.style.display = "none"; LSM_UI.win.classList.add("hidden"); } } }; openBtn.addEventListener("click", async () => { close(); if (!LSM_UI) { await initApp(); } if (LSM_UI && typeof LSM_UI.openWindow === "function") { LSM_UI.openWindow(); } }); tmpBtn.addEventListener("click", () => { close(); hideAll(); }); permBtn.addEventListener("click", () => { close(); removeHostFromShowList(); hideAll(); }); cancelBtn.addEventListener("click", close); mask.addEventListener("click", (e) => { if (e.target === mask) close(); }); box.append(title, desc, openBtn, tmpBtn, permBtn, cancelBtn); mask.appendChild(box); document.documentElement.appendChild(mask); } // ========================================================================= // 主应用逻辑初始化 // ========================================================================= async function initApp() { if (document.getElementById("lsm-session-manager-root")) { if (LSM_UI && LSM_UI.ball) { LSM_UI.ball.style.display = ""; LSM_UI.ball.classList.remove("hidden"); } return; } const isEncryptionEnabled = () => GM_getValue("Config.enable_encryption", true); const isAutoReloadEnabled = () => GM_getValue("Config.auto_reload_after_restore", false); // ----------------------------------------------------------------------- // 加密与安全擦除引擎 (AES-GCM 256) // ----------------------------------------------------------------------- const CryptoEngine = { async getDerivedKey(saltString, domain) { const enc = new TextEncoder(); const host = domain || location.hostname; const baseKeyMaterial = `LSM_KEY_${navigator.userAgent.slice(0, 32)}_${host}`; const keyMaterial = await crypto.subtle.importKey( "raw", enc.encode(baseKeyMaterial), { name: "PBKDF2" }, false, ["deriveKey"] ); return crypto.subtle.deriveKey( { name: "PBKDF2", salt: enc.encode(saltString || "SESSION_MGR_SALT_2026"), iterations: 100000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); }, async encrypt(plainObject, domain) { if (!isEncryptionEnabled() || !crypto.subtle) { return { encrypted: false, payload: JSON.stringify(plainObject) }; } try { const iv = crypto.getRandomValues(new Uint8Array(12)); const key = await this.getDerivedKey("SESSION_SALT_GCM", domain); const encodedData = new TextEncoder().encode(JSON.stringify(plainObject)); const cipherBuffer = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, encodedData ); const ivBase64 = btoa(String.fromCharCode(...iv)); const cipherBase64 = btoa(String.fromCharCode(...new Uint8Array(cipherBuffer))); return { encrypted: true, iv: ivBase64, payload: cipherBase64 }; } catch (err) { console.warn("[LSM] 加密失败,使用原始格式:", err); return { encrypted: false, payload: JSON.stringify(plainObject) }; } }, async decrypt(cipherObj, domain) { if (!cipherObj) return null; if (!cipherObj.encrypted) { return typeof cipherObj.payload === "string" ? JSON.parse(cipherObj.payload) : cipherObj.payload; } try { const iv = new Uint8Array( atob(cipherObj.iv) .split("") .map((c) => c.charCodeAt(0)) ); const cipherData = new Uint8Array( atob(cipherObj.payload) .split("") .map((c) => c.charCodeAt(0)) ); const key = await this.getDerivedKey("SESSION_SALT_GCM", domain); const decryptedBuffer = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, key, cipherData ); const decryptedStr = new TextDecoder().decode(decryptedBuffer); return JSON.parse(decryptedStr); } catch (err) { // 如果指定 domain 解密失败且未指定 domain 时,尝试用 location.hostname 解密一次 if (domain && domain !== location.hostname) { try { const fallbackKey = await this.getDerivedKey("SESSION_SALT_GCM", location.hostname); const fallbackBuffer = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, fallbackKey, cipherData ); return JSON.parse(new TextDecoder().decode(fallbackBuffer)); } catch (e2) {} } console.error("[LSM] 解密失败:", err); throw new Error("数据解密失败,可能是环境变更或跨设备未迁移密钥"); } }, wipeMemory(obj) { if (typeof obj === "object" && obj !== null) { for (const key of Object.keys(obj)) { if (typeof obj[key] === "string") { obj[key] = ""; } else if (typeof obj[key] === "object") { this.wipeMemory(obj[key]); } delete obj[key]; } } } }; // ----------------------------------------------------------------------- // Cookie & WebStorage 捕获与恢复 // ----------------------------------------------------------------------- const SessionManager = { hasGmCookie() { return ( typeof GM_cookie !== "undefined" && GM_cookie && typeof GM_cookie.list === "function" && typeof GM_cookie.set === "function" ); }, async getCookies() { if (this.hasGmCookie()) { return new Promise((resolve) => { try { GM_cookie.list({ url: location.href }, (cookies, error) => { if (error || !cookies) { resolve(this.getDocumentCookies()); } else { resolve( cookies.map((c) => ({ name: c.name, value: c.value, domain: c.domain, path: c.path || "/", secure: !!c.secure, httpOnly: !!c.httpOnly, sameSite: c.sameSite || "unspecified", expirationDate: c.expirationDate })) ); } }); } catch (e) { resolve(this.getDocumentCookies()); } }); } return this.getDocumentCookies(); }, getDocumentCookies() { const raw = document.cookie; if (!raw || !raw.trim()) return []; return raw .split(";") .map((pair) => { const idx = pair.indexOf("="); if (idx === -1) return null; const name = pair.slice(0, idx).trim(); const value = pair.slice(idx + 1).trim(); if (!name) return null; return { name, value, domain: location.hostname, path: "/", secure: location.protocol === "https:", httpOnly: false }; }) .filter(Boolean); }, getWebStorage() { const local = {}; const session = {}; try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key) local[key] = localStorage.getItem(key); } } catch (e) {} try { for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key) session[key] = sessionStorage.getItem(key); } } catch (e) {} return { localStorage: local, sessionStorage: session }; }, async captureCurrentSession() { const cookies = await this.getCookies(); const storage = this.getWebStorage(); return { domain: location.hostname, url: location.href, timestamp: Date.now(), cookies: cookies, localStorage: storage.localStorage, sessionStorage: storage.sessionStorage, summary: { cookieCount: cookies.length, localCount: Object.keys(storage.localStorage).length, sessionCount: Object.keys(storage.sessionStorage).length } }; }, async clearAllData() { let cookieCount = 0; if (this.hasGmCookie()) { try { const cookies = await new Promise((resolve) => { GM_cookie.list({ url: location.href }, (c, err) => { if (err || !c) resolve([]); else resolve(c); }); }); for (const c of cookies) { await new Promise((resolve) => { GM_cookie.delete({ url: location.href, name: c.name }, () => { cookieCount++; resolve(); }); }); } } catch (e) {} } else { const cookies = this.getDocumentCookies(); const hostname = location.hostname; const hostParts = hostname.split("."); for (const c of cookies) { document.cookie = `${encodeURIComponent(c.name)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; for (let i = 0; i < hostParts.length - 1; i++) { const domain = hostParts.slice(i).join("."); document.cookie = `${encodeURIComponent(c.name)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${domain}`; document.cookie = `${encodeURIComponent(c.name)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`; } cookieCount++; } } let storageCount = 0; try { storageCount += localStorage.length; localStorage.clear(); } catch (e) {} try { storageCount += sessionStorage.length; sessionStorage.clear(); } catch (e) {} return { cookieCount, storageCount }; }, async restoreSession(sessionData) { // 切换与恢复前,先彻底清空当前所有 Cookie 与 WebStorage await this.clearAllData(); let cookieSuccessCount = 0; let cookieFailCount = 0; if (Array.isArray(sessionData.cookies)) { if (this.hasGmCookie()) { for (const c of sessionData.cookies) { try { await new Promise((resolve) => { const cookieDetails = { url: location.href, name: c.name, value: c.value, path: c.path || "/", domain: c.domain || location.hostname, secure: !!c.secure, httpOnly: !!c.httpOnly }; if (c.expirationDate) cookieDetails.expirationDate = c.expirationDate; GM_cookie.set(cookieDetails, (err) => { if (err) cookieFailCount++; else cookieSuccessCount++; resolve(); }); }); } catch (e) { cookieFailCount++; } } } else { for (const c of sessionData.cookies) { try { let cookieStr = `${encodeURIComponent(c.name)}=${encodeURIComponent(c.value)}; path=${c.path || "/"}`; if (c.domain && !c.domain.startsWith(".")) cookieStr += `; domain=${c.domain}`; if (c.secure || location.protocol === "https:") cookieStr += "; Secure"; if (c.expirationDate) cookieStr += `; expires=${new Date(c.expirationDate * 1000).toUTCString()}`; document.cookie = cookieStr; cookieSuccessCount++; } catch (e) { cookieFailCount++; } } } } let localCount = 0; if (sessionData.localStorage && typeof sessionData.localStorage === "object") { try { localStorage.clear(); for (const [k, v] of Object.entries(sessionData.localStorage)) { if (v !== null && v !== undefined) { localStorage.setItem(k, v); localCount++; } } } catch (e) {} } let sessionCount = 0; if (sessionData.sessionStorage && typeof sessionData.sessionStorage === "object") { try { sessionStorage.clear(); for (const [k, v] of Object.entries(sessionData.sessionStorage)) { if (v !== null && v !== undefined) { sessionStorage.setItem(k, v); sessionCount++; } } } catch (e) {} } return { cookieSuccessCount, cookieFailCount, localCount, sessionCount }; } }; // ----------------------------------------------------------------------- // 数据库与存储管理 // ----------------------------------------------------------------------- const DB = { getStorageKey(domain) { return `SESSION_DATA_${domain || location.hostname}`; }, getRecords(domain) { const key = this.getStorageKey(domain); const raw = GM_getValue(key, []); return Array.isArray(raw) ? raw : []; }, saveRecords(records, domain) { const key = this.getStorageKey(domain); GM_setValue(key, records); }, async addRecord(name, rawSessionData) { const domain = location.hostname; const records = this.getRecords(domain); const cipherObject = await CryptoEngine.encrypt(rawSessionData); const newRecord = { id: "sess_" + Date.now() + "_" + Math.random().toString(36).slice(2, 7), name: name.trim(), domain: domain, url: location.href, createdAt: Date.now(), updatedAt: Date.now(), summary: rawSessionData.summary, cipherData: cipherObject }; records.unshift(newRecord); this.saveRecords(records, domain); CryptoEngine.wipeMemory(rawSessionData); return newRecord; }, updateRecordName(id, newName, domain) { const d = domain || location.hostname; const records = this.getRecords(d); const target = records.find((r) => r.id === id); if (target) { target.name = newName.trim(); target.updatedAt = Date.now(); this.saveRecords(records, d); return true; } return false; }, deleteRecord(id, domain) { const d = domain || location.hostname; let records = this.getRecords(d); const initialLen = records.length; records = records.filter((r) => r.id !== id); if (records.length !== initialLen) { this.saveRecords(records, d); return true; } return false; }, importRecords(newRecords, domain) { const d = domain || location.hostname; const existing = this.getRecords(d); let count = 0; for (const item of newRecords) { if (!item || !item.name || !item.cipherData) continue; // 如果 ID 冲突则重新生成,避免重复 const record = { ...item, id: "sess_" + Date.now() + "_" + Math.random().toString(36).slice(2, 7), importedAt: Date.now() }; existing.unshift(record); count++; } this.saveRecords(existing, d); return count; }, async getDecryptedSession(id, domain) { const d = domain || location.hostname; const records = this.getRecords(d); const target = records.find((r) => r.id === id); if (!target) throw new Error("未找到对应快照记录"); const recDomain = target.domain || d; return await CryptoEngine.decrypt(target.cipherData, recDomain); } }; // ----------------------------------------------------------------------- // 辅助工具 // ----------------------------------------------------------------------- function formatTime(timestamp) { if (!timestamp) return "-"; const d = new Date(timestamp); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } function getDefaultName() { const d = new Date(); const pad = (n) => String(n).padStart(2, "0"); return `${location.hostname}_${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; } function escapeHtml(str) { if (!str) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function downloadJsonFile(filename, contentObj) { try { const jsonStr = JSON.stringify(contentObj, null, 2); const blob = new Blob([jsonStr], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); } catch (e) { alert("下载文件失败: " + e.message); } } function readFileAsJson(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { try { const json = JSON.parse(e.target.result); resolve(json); } catch (err) { reject(new Error("文件解析失败,请确认选择的是正确的 JSON 格式文件")); } }; reader.onerror = () => reject(new Error("读取文件出错")); reader.readAsText(file); }); } // ----------------------------------------------------------------------- // UI 结构与样式 // ----------------------------------------------------------------------- const uid = "lsm-" + Math.random().toString(36).slice(2, 8); const container = document.createElement("div"); container.id = "lsm-session-manager-root"; const shadow = container.attachShadow({ mode: "open" }); document.documentElement.appendChild(container); const style = document.createElement("style"); style.textContent = ` #${uid}-root { all: initial; display: block; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif; color: #0f172a; font-size: 13px; line-height: 1.5; text-align: left; -webkit-font-smoothing: antialiased; } #${uid}-root *, #${uid}-root *::before, #${uid}-root *::after { box-sizing: border-box; } .${uid}-root input, .${uid}-root select, .${uid}-root textarea, .${uid}-root button { height: auto; width: auto; min-width: 0; max-width: none; font-family: inherit; font-size: inherit; color: inherit; line-height: inherit; } /* 滚动条美化 */ .${uid}-content::-webkit-scrollbar { width: 6px; } .${uid}-content::-webkit-scrollbar-track { background: transparent; } .${uid}-content::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; } .${uid}-content::-webkit-scrollbar-thumb:hover { background: #94a3b8; } /* 悬浮球 */ #${uid}-ball { position: fixed; left: auto; top: auto; right: 25px; bottom: 80px; z-index: 2147483646; width: 50px; height: 50px; border-radius: 50%; background: linear-gradient(135deg, #1e40af 0%, #2563eb 50%, #3b82f6 100%); color: #ffffff; font-weight: 700; font-size: 14px; display: flex; align-items: center; justify-content: center; box-shadow: 0 8px 24px -4px rgba(37, 99, 235, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.2) inset; cursor: grab; user-select: none; transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease; } #${uid}-ball:hover { transform: scale(1.06); box-shadow: 0 12px 30px -4px rgba(37, 99, 235, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.3) inset; } #${uid}-ball.dragging { cursor: grabbing; transform: scale(0.96); } #${uid}-ball svg { width: 24px; height: 24px; fill: currentColor; pointer-events: none; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.15)); } /* 悬浮球右上角微型关闭/菜单按钮 */ .${uid}-ball-close { position: absolute; top: -3px; right: -3px; width: 18px; height: 18px; border-radius: 50%; line-height: 16px; background: #0f172a; color: #ffffff; font-size: 11px; text-align: center; cursor: pointer; display: none; z-index: 3; border: 1.5px solid #ffffff; box-shadow: 0 2px 6px rgba(0,0,0,0.25); transition: background 0.15s ease, transform 0.15s ease; } #${uid}-ball:hover .${uid}-ball-close { display: block; } .${uid}-ball-close:hover { background: #e11d48; transform: scale(1.15); } /* 徽标 */ .${uid}-badge { position: absolute; top: -3px; left: -3px; background: linear-gradient(135deg, #f43f5e, #e11d48); color: #ffffff; font-size: 10px; font-weight: 700; min-width: 18px; height: 18px; border-radius: 9999px; display: flex; align-items: center; justify-content: center; padding: 0 4px; border: 2px solid #ffffff; box-shadow: 0 2px 6px rgba(225, 29, 72, 0.4); } /* 悬浮球快捷菜单遮罩与弹窗 */ .${uid}-menu-mask { position: fixed; inset: 0; z-index: 2147483646; background: rgba(15, 23, 42, 0.45); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); } .${uid}-menu-mask.hidden { display: none; } .${uid}-ball-menu { position: fixed; left: 50%; top: 45%; transform: translate(-50%, -50%); z-index: 2147483647; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 45px -10px rgba(15, 23, 42, 0.25), 0 0 0 1px rgba(15, 23, 42, 0.06); padding: 18px; width: 280px; } .${uid}-ball-menu-title { font-weight: 700; margin: 0 0 12px; font-size: 14px; color: #0f172a; display: flex; align-items: center; gap: 6px; } .${uid}-ball-menu button { display: flex; align-items: center; width: 100%; margin-top: 8px; padding: 9px 12px; border: 1px solid #e2e8f0; border-radius: 10px; background: #f8fafc; cursor: pointer; font-size: 12px; font-weight: 500; text-align: left; color: #334155; transition: all 0.15s ease; } .${uid}-ball-menu button:hover { background: #f1f5f9; border-color: #cbd5e1; color: #0f172a; } .${uid}-ball-menu button[data-a="forever"] { border-color: #fecdd3; background: #fff1f2; color: #e11d48; } .${uid}-ball-menu button[data-a="forever"]:hover { background: #ffe4e6; } /* 主管理窗口 */ #${uid}-window { position: fixed; left: auto; top: auto; right: 30px; bottom: 90px; z-index: 2147483646; width: 500px; height: 560px; max-width: calc(100vw - 20px); max-height: calc(100vh - 30px); background: #ffffff; border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 25px 50px -12px rgba(15, 23, 42, 0.25), 0 0 0 1px rgba(15, 23, 42, 0.08); overscroll-behavior: contain; } #${uid}-window.hidden, #${uid}-ball.hidden { display: none !important; } /* 头部 Header */ #${uid}-header { flex: none; display: flex; align-items: center; justify-content: space-between; padding: 13px 18px; background: linear-gradient(135deg, #0f172a 0%, #1e293b 60%, #334155 100%); color: #f8fafc; font-size: 14px; font-weight: 600; cursor: grab; user-select: none; border-bottom: 1px solid rgba(255, 255, 255, 0.08); } #${uid}-header.dragging { cursor: grabbing; } .${uid}-header-left { display: flex; align-items: center; gap: 10px; } .${uid}-header-title { display: flex; align-items: center; gap: 6px; letter-spacing: 0.2px; } .${uid}-domain-tag { background: rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.18); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); color: #f8fafc; font-size: 11px; padding: 2px 10px; border-radius: 9999px; font-weight: 500; max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .${uid}-header-actions { display: flex; align-items: center; gap: 6px; } .${uid}-header-actions button { border: none; background: rgba(255, 255, 255, 0.12); color: #f8fafc; border-radius: 8px; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 13px; transition: all 0.15s ease; } .${uid}-header-actions button:hover { background: rgba(255, 255, 255, 0.25); transform: scale(1.05); } /* 状态条 */ .${uid}-status-bar { padding: 7px 18px; background: #f8fafc; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; justify-content: space-between; font-size: 11px; color: #64748b; flex: none; display: none; } .${uid}-status-item { display: flex; align-items: center; gap: 6px; font-weight: 500; } .${uid}-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; } .${uid}-dot-green { background: #10b981; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); } .${uid}-dot-amber { background: #f59e0b; box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2); } /* 操作工具栏 */ .${uid}-toolbar { padding: 10px 18px; background: #ffffff; border-bottom: 1px solid #f1f5f9; display: flex; flex-direction: column; gap: 8px; flex: none; } .${uid}-toolbar-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: nowrap; } .${uid}-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 7px 12px; font-size: 12px; font-weight: 600; border-radius: 8px; border: 1px solid transparent; cursor: pointer; user-select: none; white-space: nowrap; transition: all 0.15s ease; } .${uid}-btn-primary { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; color: #ffffff !important; box-shadow: 0 2px 8px rgba(37, 99, 235, 0.25); } .${uid}-btn-primary:hover { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.35); color: #ffffff !important; } .${uid}-btn-secondary { background: #f8fafc; color: #334155; border-color: #e2e8f0; } .${uid}-btn-secondary:hover { background: #f1f5f9; border-color: #cbd5e1; color: #0f172a; } .${uid}-btn-danger { background: #fff1f2; color: #e11d48; border-color: #fecdd3; } .${uid}-btn-danger:hover { background: #ffe4e6; border-color: #fda4af; } .${uid}-btn-restore-pill { background: #f0fdf4 !important; color: #15803d !important; border-color: #bbf7d0 !important; font-weight: 600; } .${uid}-btn-restore-pill:hover { background: #dcfce7 !important; border-color: #86efac !important; } .${uid}-btn-sm { padding: 4px 10px; font-size: 11px; border-radius: 6px; } .${uid}-btn-icon { padding: 7px 9px !important; min-width: 32px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } /* 更多操作下拉菜单 */ .${uid}-dropdown-wrapper { position: relative; display: inline-flex; flex-shrink: 0; } .${uid}-dropdown-menu { position: absolute; top: calc(100% + 6px); right: 0; min-width: 175px; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 6px; box-shadow: 0 12px 30px -4px rgba(15, 23, 42, 0.18), 0 0 0 1px rgba(15, 23, 42, 0.05); z-index: 50; display: flex; flex-direction: column; gap: 2px; } .${uid}-dropdown-menu.hidden { display: none !important; } .${uid}-dropdown-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; font-size: 12px; font-weight: 500; color: #334155; border-radius: 8px; cursor: pointer; user-select: none; transition: all 0.12s ease; white-space: nowrap; } .${uid}-dropdown-item:hover { background: #f1f5f9; color: #0f172a; } .${uid}-dropdown-divider { height: 1px; background: #f1f5f9; margin: 4px 0; } .${uid}-item-accent { color: #15803d !important; font-weight: 600; } .${uid}-item-accent:hover { background: #f0fdf4 !important; color: #166534 !important; } /* 移动端与小屏幕自适应响应式布局 */ @media (max-width: 480px) { #${uid}-window { left: 10px !important; right: 10px !important; bottom: 15px !important; width: auto !important; max-width: calc(100vw - 20px) !important; height: 82vh !important; border-radius: 14px; } #${uid}-header { padding: 10px 14px; } .${uid}-domain-tag { max-width: 110px; font-size: 10px; padding: 1px 6px; } .${uid}-toolbar { padding: 8px 12px; gap: 6px; } .${uid}-toolbar-row { gap: 6px; } .${uid}-btn { padding: 6px 8px; font-size: 11px; gap: 4px; } .${uid}-content { padding: 10px 12px; gap: 10px; } .${uid}-card { padding: 10px 12px; } .${uid}-card-chips { gap: 4px; } .${uid}-chip { font-size: 10px; padding: 1px 6px; } .${uid}-card-actions { flex-wrap: wrap; gap: 4px; justify-content: flex-end; } .${uid}-card-actions .${uid}-btn { padding: 4px 7px; font-size: 10px; } } /* 记录列表区域 */ .${uid}-content { padding: 14px 18px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 12px; min-height: 0; background: #f8fafc; overscroll-behavior: contain; } .${uid}-card { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); } .${uid}-card:hover { border-color: #cbd5e1; transform: translateY(-2px); box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.08); } .${uid}-card-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .${uid}-card-name { font-weight: 700; font-size: 13px; color: #0f172a; word-break: break-all; display: flex; align-items: center; gap: 6px; } .${uid}-card-time { font-size: 11px; color: #94a3b8; flex-shrink: 0; } /* 凭证 Chips 徽章组 */ .${uid}-card-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } .${uid}-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 9999px; line-height: 1.4; } .${uid}-chip-cookie { background: #fffbeb; color: #b45309; border: 1px solid #fef3c7; } .${uid}-chip-local { background: #f0fdf4; color: #15803d; border: 1px solid #dcfce7; } .${uid}-chip-session { background: #faf5ff; color: #7e22ce; border: 1px solid #f3e8ff; } .${uid}-chip-encrypted { background: #f0f9ff; color: #0284c7; border: 1px solid #e0f2fe; } /* 来源链接小标签 */ .${uid}-card-origin { display: flex; align-items: center; gap: 6px; background: #f8fafc; border: 1px solid #f1f5f9; border-radius: 6px; padding: 4px 8px; margin-top: 2px; font-size: 11px; color: #64748b; } .${uid}-card-url { color: #2563eb; text-decoration: none; word-break: break-all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; max-width: calc(100% - 60px); } .${uid}-card-url:hover { text-decoration: underline; color: #1d4ed8; } /* 卡片操作栏 */ .${uid}-card-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; border-top: 1px solid #f1f5f9; padding-top: 8px; margin-top: 2px; } .${uid}-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 50px 0; color: #94a3b8; text-align: center; gap: 10px; } /* 内置保存抽屉弹窗 */ .${uid}-save-dialog { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #ffffff; display: flex; flex-direction: column; padding: 24px; gap: 16px; transform: translateY(100%); transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); z-index: 10; overscroll-behavior: contain; overflow-y: auto; } .${uid}-save-dialog.open { transform: translateY(0); } .${uid}-save-dialog-title { font-size: 15px; font-weight: 700; color: #0f172a; display: flex; align-items: center; gap: 6px; } .${uid}-input-group { display: flex; flex-direction: column; gap: 6px; } .${uid}-input-label { font-size: 12px; font-weight: 600; color: #334155; } .${uid}-input { width: 100%; padding: 9px 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 13px; outline: none; transition: all 0.15s ease; } .${uid}-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } .${uid}-grid-preview { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 4px; } .${uid}-stat-box { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 10px; text-align: center; } .${uid}-stat-num { font-size: 18px; font-weight: 700; color: #0f172a; margin-top: 2px; } .${uid}-stat-label { font-size: 11px; color: #64748b; } /* Toast 提示 */ .${uid}-toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%) translateY(-10px); background: #0f172a; color: #ffffff; padding: 8px 16px; border-radius: 10px; font-size: 12px; font-weight: 500; box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.3); opacity: 0; pointer-events: none; z-index: 2147483647; transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); } .${uid}-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .${uid}-toast.success { background: #059669; } .${uid}-toast.error { background: #dc2626; } .${uid}-toast.info { background: #0f172a; } `; shadow.appendChild(style); const wrapper = document.createElement("div"); wrapper.id = `${uid}-root`; wrapper.className = `${uid}-root`; wrapper.innerHTML = `