// ==UserScript== // @name Websites Base64 Helper // @icon https://raw.githubusercontent.com/XavierBar/Discourse-Base64-Helper/refs/heads/main/discourse.svg // @namespace http://tampermonkey.net/ // @version 1.4.15 // @description Base64编解码工具 for all websites // @author Xavier // @match *://*/* // @grant GM_notification // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 常量定义 const Z_INDEX = 2147483647; const STORAGE_KEYS = { BUTTON_POSITION: 'btnPosition', }; const BASE64_REGEX = /(? { GM_addStyle(STYLES.GLOBAL + STYLES.NOTIFICATION); }; class Base64Helper { constructor() { // 确保只在主文档中创建实例 if (window.top !== window.self) { throw new Error( 'Base64Helper can only be instantiated in the main window' ); } this.originalContents = new Map(); this.isDragging = false; this.isRealDragging = false; // 添加真实拖动标记 this.touchStartX = 0; // 添加触摸开始位置 this.touchStartY = 0; // 添加触摸开始位置 this.menuVisible = false; this.resizeTimer = null; this.notifications = []; this.notificationContainer = null; this.notificationEventListeners = []; // 添加通知事件监听器集合 this.initUI(); this.eventListeners = []; // 用于存储事件监听器以便后续清理 this.initEventListeners(); this.addRouteListeners(); } // 添加正则常量 static URL_PATTERNS = { // 更新 URL 检测正则,添加常见域名特征的判断 URL: /^(?!.*(?:[a-z0-9-]+\.(?:com|net|org|edu|gov|mil|biz|info|io|cn|me|tv|cc|uk|jp|ru|eu|au|de|fr)(?:\/|\?|#|$)))(?:(?:https?|ftp):\/\/)?(?:(?:[\w-]+\.)+[a-z]{2,}|localhost)(?::\d+)?(?:\/[^\s]*)?$/i, EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, // 添加更多的域名关键词特征 DOMAIN_PATTERNS: { // 主流网站特征 POPULAR_SITES: /(?:google|youtube|facebook|twitter|instagram|linkedin|github|gitlab|bitbucket|stackoverflow|reddit|discord|twitch|tiktok|snapchat|pinterest|netflix|amazon|microsoft|apple|adobe)/i, // 视频网站特征 VIDEO_SITES: /(?:bilibili|youku|iqiyi|douyin|kuaishou|nicovideo|vimeo|dailymotion)/i, // 中文网站特征 CN_SITES: /(?:baidu|weibo|zhihu|taobao|tmall|jd|qq|163|sina|sohu|csdn|aliyun|tencent)/i, // 常见域名后缀 TLD: /\.(?:com|net|org|edu|gov|mil|biz|info|io|cn|me|tv|cc|uk|jp|ru|eu|au|de|fr)(?:\/|\?|#|$)/i, }, }; // UI 初始化 initUI() { // 确保只在主文档中初始化UI if ( window.top !== window.self || document.getElementById('base64-helper-root') ) { return; } this.container = document.createElement('div'); this.container.id = 'base64-helper-root'; document.body.append(this.container); this.shadowRoot = this.container.attachShadow({ mode: 'open' }); this.shadowRoot.appendChild(this.createShadowStyles()); this.shadowRoot.appendChild(this.createMainUI()); this.initPosition(); } createShadowStyles() { const style = document.createElement('style'); style.textContent = STYLES.SHADOW_DOM; return style; } createMainUI() { const uiContainer = document.createElement('div'); uiContainer.className = 'base64-helper'; uiContainer.style.cursor = 'grab'; this.mainBtn = this.createButton('Base64', 'main-btn'); this.mainBtn.style.cursor = 'grab'; this.menu = this.createMenu(); uiContainer.append(this.mainBtn, this.menu); return uiContainer; } createButton(text, className) { const btn = document.createElement('button'); btn.className = className; btn.textContent = text; return btn; } createMenu() { const menu = document.createElement('div'); menu.className = 'menu'; this.decodeBtn = this.createMenuItem('解析本页 Base64', 'decode'); this.encodeBtn = this.createMenuItem('文本转 Base64'); menu.append(this.decodeBtn, this.encodeBtn); return menu; } createMenuItem(text, mode) { const item = document.createElement('div'); item.className = 'menu-item'; item.textContent = text; if (mode) item.dataset.mode = mode; return item; } // 位置管理 initPosition() { const pos = this.positionManager.get() || { x: window.innerWidth - 120, y: window.innerHeight - 80, }; const ui = this.shadowRoot.querySelector('.base64-helper'); ui.style.left = `${pos.x}px`; ui.style.top = `${pos.y}px`; } get positionManager() { return { get: () => { const saved = GM_getValue(STORAGE_KEYS.BUTTON_POSITION); if (!saved) return null; const ui = this.shadowRoot.querySelector('.base64-helper'); const maxX = window.innerWidth - ui.offsetWidth - 20; const maxY = window.innerHeight - ui.offsetHeight - 20; return { x: Math.min(Math.max(saved.x, 20), maxX), y: Math.min(Math.max(saved.y, 20), maxY), }; }, set: (x, y) => { const ui = this.shadowRoot.querySelector('.base64-helper'); const pos = { x: Math.max( 20, Math.min(x, window.innerWidth - ui.offsetWidth - 20) ), y: Math.max( 20, Math.min(y, window.innerHeight - ui.offsetHeight - 20) ), }; GM_setValue(STORAGE_KEYS.BUTTON_POSITION, pos); return pos; }, }; } // 初始化事件监听器 initEventListeners() { // 分别为触屏和鼠标添加事件监听 this.addTouchListeners(); this.addMouseListeners(); // 其他通用事件监听 const commonListeners = [ { element: this.decodeBtn, events: [ { name: 'click', handler: (e) => { e.stopPropagation(); this.handleDecode(); } }, { name: 'touchstart', handler: (e) => { e.stopPropagation(); } } ] }, { element: this.encodeBtn, events: [ { name: 'click', handler: (e) => { e.stopPropagation(); this.handleEncode(); } }, { name: 'touchstart', handler: (e) => { e.stopPropagation(); } } ] }, { element: window, events: [{ name: 'resize', handler: () => this.handleResize() }] } ]; commonListeners.forEach(({ element, events }) => { events.forEach(({ name, handler }) => { element.addEventListener(name, handler); this.eventListeners.push({ element, event: name, handler }); }); }); // 将全局点击事件的监听放在最后 this.addGlobalClickListeners(); } addGlobalClickListeners() { const handleOutsideClick = (e) => { // 确保点击不是来自菜单或按钮本身 const helper = this.shadowRoot.querySelector('.base64-helper'); if (helper && !helper.contains(e.target) && this.menuVisible) { this.menuVisible = false; this.menu.style.display = 'none'; } }; document.addEventListener('click', handleOutsideClick); document.addEventListener('touchstart', handleOutsideClick); this.eventListeners.push( { element: document, event: 'click', handler: handleOutsideClick }, { element: document, event: 'touchstart', handler: handleOutsideClick } ); } addTouchListeners() { const touchHandlers = [ { event: 'touchstart', handler: (e) => this.handleTouchStart(e), options: { passive: false }, }, { event: 'touchmove', handler: (e) => this.handleTouchMove(e), options: { passive: false }, }, { event: 'touchend', handler: (e) => this.handleTouchEnd(e) }, { event: 'click', handler: (e) => this.toggleMenu(e) }, ]; touchHandlers.forEach(({ event, handler, options }) => { this.mainBtn.addEventListener(event, handler, options); this.eventListeners.push({ element: this.mainBtn, event, handler }); }); // 更新全局点击事件监听 const handleGlobalClick = (e) => { // 如果菜单没有显示,不需要处理 if (!this.menuVisible) return; // 获取事件目标的组合路径 const path = e.composedPath(); // 检查点击路径中是否包含我们的 helper 容器 const helperContainer = this.shadowRoot.querySelector('.base64-helper'); if (!path.includes(helperContainer)) { this.menuVisible = false; this.menu.style.display = 'none'; } }; document.addEventListener('click', handleGlobalClick, true); document.addEventListener('touchstart', handleGlobalClick, true); this.eventListeners.push( { element: document, event: 'click', handler: handleGlobalClick }, { element: document, event: 'touchstart', handler: handleGlobalClick } ); } addMouseListeners() { const mouseHandlers = [ { element: this.mainBtn, event: 'mousedown', handler: (e) => this.handleMouseDown(e), }, { element: document, event: 'mousemove', handler: (e) => this.handleMouseMove(e), }, { element: document, event: 'mouseup', handler: (e) => this.handleMouseUp(e), }, { element: this.mainBtn, event: 'click', handler: (e) => this.toggleMenu(e), }, ]; mouseHandlers.forEach(({ element, event, handler }) => { element.addEventListener(event, handler); this.eventListeners.push({ element, event, handler }); }); } // 鼠标事件处理 handleMouseDown(e) { if (e.button !== 0) return; // 只处理左键点击 this.isMouseDragging = true; this.startX = e.clientX; this.startY = e.clientY; const rect = this.shadowRoot .querySelector('.base64-helper') .getBoundingClientRect(); this.initialX = rect.left; this.initialY = rect.top; const ui = this.shadowRoot.querySelector('.base64-helper'); const btn = ui.querySelector('.main-btn'); ui.style.transition = 'none'; ui.style.cursor = 'grabbing'; btn.style.cursor = 'grabbing'; this.mouseStartTime = Date.now(); } handleMouseMove(e) { if (!this.isMouseDragging) return; const moveX = Math.abs(e.clientX - this.startX); const moveY = Math.abs(e.clientY - this.startY); if (moveX > 5 || moveY > 5) { this.hasMouseMoved = true; } const dx = e.clientX - this.startX; const dy = e.clientY - this.startY; const newX = Math.min( Math.max(20, this.initialX + dx), window.innerWidth - this.mainBtn.offsetWidth - 20 ); const newY = Math.min( Math.max(20, this.initialY + dy), window.innerHeight - this.mainBtn.offsetHeight - 20 ); const ui = this.shadowRoot.querySelector('.base64-helper'); ui.style.left = `${newX}px`; ui.style.top = `${newY}px`; } handleMouseUp(e) { if (!this.isMouseDragging) return; const ui = this.shadowRoot.querySelector('.base64-helper'); const btn = ui.querySelector('.main-btn'); const currentRect = ui.getBoundingClientRect(); this.isMouseDragging = false; const pos = this.positionManager.set(currentRect.left, currentRect.top); ui.style.left = `${pos.x}px`; ui.style.top = `${pos.y}px`; ui.style.transition = 'opacity 0.3s ease'; ui.style.cursor = 'grab'; btn.style.cursor = 'grab'; // 如果没有移动,则触发菜单 if (!this.hasMouseMoved) { this.toggleMenu(e); } this.hasMouseMoved = false; } // 触屏事件处理 handleTouchStart(e) { if (e.touches.length === 1) { e.preventDefault(); this.isTouchDragging = true; const touch = e.touches[0]; this.startX = touch.clientX; this.startY = touch.clientY; const rect = this.shadowRoot .querySelector('.base64-helper') .getBoundingClientRect(); this.initialX = rect.left; this.initialY = rect.top; this.touchStartTime = Date.now(); const ui = this.shadowRoot.querySelector('.base64-helper'); ui.style.transition = 'none'; } } handleTouchMove(e) { if (!this.isTouchDragging || e.touches.length !== 1) return; e.preventDefault(); const touch = e.touches[0]; const moveX = Math.abs(touch.clientX - this.startX); const moveY = Math.abs(touch.clientY - this.startY); if (moveX > 5 || moveY > 5) { this.hasTouchMoved = true; } const dx = touch.clientX - this.startX; const dy = touch.clientY - this.startY; const newX = Math.min( Math.max(20, this.initialX + dx), window.innerWidth - this.mainBtn.offsetWidth - 20 ); const newY = Math.min( Math.max(20, this.initialY + dy), window.innerHeight - this.mainBtn.offsetHeight - 20 ); const ui = this.shadowRoot.querySelector('.base64-helper'); ui.style.left = `${newX}px`; ui.style.top = `${newY}px`; } handleTouchEnd(e) { if (!this.isTouchDragging) return; const ui = this.shadowRoot.querySelector('.base64-helper'); const currentRect = ui.getBoundingClientRect(); const touchDuration = Date.now() - this.touchStartTime; this.isTouchDragging = false; const pos = this.positionManager.set(currentRect.left, currentRect.top); ui.style.left = `${pos.x}px`; ui.style.top = `${pos.y}px`; ui.style.transition = 'opacity 0.3s ease'; // 如果触摸时间短且没有明显移动,则触发菜单 if (touchDuration < 200 && !this.hasTouchMoved) { this.toggleMenu(e); } this.hasTouchMoved = false; } toggleMenu(e) { e.stopPropagation(); // 如果正在拖动,不处理点击 if (this.isTouchDragging || this.isMouseDragging) return; // 获取当前位置,检查是否有移动 const hasMoved = this.hasTouchMoved || this.hasMouseMoved; // 如果没有移动,则切换菜单 if (!hasMoved) { this.menuVisible = !this.menuVisible; this.menu.style.display = this.menuVisible ? 'block' : 'none'; } } // 窗口resize处理 handleResize() { clearTimeout(this.resizeTimer); this.resizeTimer = setTimeout(() => { const pos = this.positionManager.get(); if (pos) { const ui = this.shadowRoot.querySelector('.base64-helper'); ui.style.left = `${pos.x}px`; ui.style.top = `${pos.y}px`; } }, 100); } // 路由监听 addRouteListeners() { this.handleRouteChange = () => { clearTimeout(this.routeTimer); this.routeTimer = setTimeout(() => this.resetState(), 100); }; const routeEvents = [ { event: 'popstate', target: window }, { event: 'hashchange', target: window }, { event: 'DOMContentLoaded', target: window }, ]; routeEvents.forEach(({ event, target }) => { target.addEventListener(event, this.handleRouteChange); this.eventListeners.push({ element: target, event, handler: this.handleRouteChange, }); }); this.originalPushState = history.pushState; this.originalReplaceState = history.replaceState; history.pushState = (...args) => { this.originalPushState.apply(history, args); this.handleRouteChange(); }; history.replaceState = (...args) => { this.originalReplaceState.apply(history, args); this.handleRouteChange(); }; } // 核心功能 handleDecode() { if (this.decodeBtn.dataset.mode === 'restore') { this.restoreContent(); return; } try { const { nodesToReplace, validDecodedCount } = this.processTextNodes(); if (validDecodedCount === 0) { this.showNotification('本页未发现有效 Base64 内容', 'info'); return; } this.replaceNodes(nodesToReplace); this.addClickListenersToDecodedText(); this.decodeBtn.textContent = '恢复本页 Base64'; this.decodeBtn.dataset.mode = 'restore'; this.showNotification( `解析完成,共找到 ${validDecodedCount} 个 Base64 内容`, 'success' ); } catch (e) { console.error('Base64 decode error:', e); this.showNotification(`解析失败: ${e.message}`, 'error'); } this.menuVisible = false; this.menu.style.display = 'none'; } processTextNodes() { // 添加超时保护 const startTime = Date.now(); const TIMEOUT = 5000; // 5秒超时 // 扩展排除的标签列表 const excludeTags = new Set([ 'script', 'style', 'noscript', 'iframe', 'img', 'input', 'textarea', 'svg', 'canvas', 'template', 'pre', 'code', 'button', 'meta', 'link', 'head', 'title', 'select', 'form', 'object', 'embed', 'video', 'audio', 'source', 'track', 'map', 'area', 'math', 'figure', 'picture', 'portal', 'slot', 'data', ]); // 排除的属性名 const excludeAttrs = new Set([ 'src', 'data-src', 'href', 'data-url', 'content', 'background', 'poster', 'data-image', 'srcset', ]); const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // Helper functions to check node validity const isExcludedTag = (parent) => { const tagName = parent.tagName?.toLowerCase(); return excludeTags.has(tagName); }; const isHiddenElement = (parent) => { if (!(parent instanceof HTMLElement)) return false; const style = window.getComputedStyle(parent); return ( style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || style.clipPath === 'inset(100%)' || (style.height === '0px' && style.overflow === 'hidden') ); }; const isOutOfViewport = (parent) => { if (!(parent instanceof HTMLElement)) return false; const rect = parent.getBoundingClientRect(); return rect.width === 0 || rect.height === 0; }; const hasBase64Attributes = (parent) => { if (!parent.hasAttributes()) return false; for (const attr of parent.attributes) { if (excludeAttrs.has(attr.name)) { const value = attr.value.toLowerCase(); if ( value.includes('base64') || value.match(/^[a-z0-9+/=]+$/i) ) { return true; } } } return false; }; // Check parent chain let parent = node.parentNode; while (parent && parent !== document.body) { if ( isExcludedTag(parent) || isHiddenElement(parent) || isOutOfViewport(parent) || hasBase64Attributes(parent) ) { return NodeFilter.FILTER_REJECT; } parent = parent.parentNode; } // Check text content const text = node.textContent?.trim(); if (!text || text.length < 8) { return NodeFilter.FILTER_SKIP; } // Check for potential base64 content return /[A-Za-z0-9+/]{6,}/.exec(text) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }, }, false ); let nodesToReplace = []; let processedMatches = new Set(); let validDecodedCount = 0; while (walker.nextNode()) { // 检查是否超时 if (Date.now() - startTime > TIMEOUT) { console.warn('Base64 processing timeout'); break; } const node = walker.currentNode; const { modified, newHtml, count } = this.processMatches( node.nodeValue, processedMatches ); if (modified) { nodesToReplace.push({ node, newHtml }); validDecodedCount += count; } } return { nodesToReplace, validDecodedCount }; } processMatches(text, processedMatches) { const matches = Array.from(text.matchAll(BASE64_REGEX)); if (!matches.length) return { modified: false, newHtml: text, count: 0 }; let modified = false; let newHtml = text; let count = 0; for (const match of matches.reverse()) { const original = match[0]; if (!this.validateBase64(original)) continue; try { const decoded = this.decodeBase64(original); // 检查解码结果是否为有效文本 if (!decoded || !this.isValidText(decoded)) continue; const matchId = `${original}-${match.index}`; if (processedMatches.has(matchId)) continue; processedMatches.add(matchId); newHtml = `${newHtml.substring( 0, match.index )}${decoded}${newHtml.substring( match.index + original.length )}`; modified = true; count++; } catch (e) { continue; } } return { modified, newHtml, count }; } // 添加新的辅助方法来验证解码后的文本 isValidText(text) { // 检查解码后的文本是否包含可打印字符 if (!text || text.length === 0) return false; // 检查是否包含过多的不可打印字符 const printableChars = text.replace(/[^\x20-\x7E]/g, '').length; return printableChars / text.length > 0.5; // 可打印字符需占50%以上 } replaceNodes(nodesToReplace) { // 检查是否包含过多的不可打印字符 const printableChars = text.replace(/[^\x20-\x7E]/g, '').length; return printableChars / text.length > 0.5; // 可打印字符需占50%以上 } replaceNodes(nodesToReplace) { nodesToReplace.forEach(({ node, newHtml }) => { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); }); } addClickListenersToDecodedText() { document.querySelectorAll('.decoded-text').forEach((el) => { el.addEventListener('click', async (e) => { const success = await this.copyToClipboard(e.target.textContent); this.showNotification( success ? '已复制文本内容' : '复制失败,请手动复制', success ? 'success' : 'error' ); e.stopPropagation(); }); }); } async handleEncode() { const text = prompt('请输入要编码的文本:'); if (text === null) return; try { const encoded = this.encodeBase64(text); const success = await this.copyToClipboard(encoded); this.showNotification( success ? 'Base64 已复制' : '编码成功但复制失败,请手动复制:' + encoded, success ? 'success' : 'info' ); } catch (e) { this.showNotification('编码失败: ' + e.message, 'error'); } this.menu.style.display = 'none'; } // 工具方法 validateBase64(str) { // 1. 快速预检查 if (!str || str.length < 8 || str.length > 1000) return false; // 2. 检查是否包含常见的网址相关字符串 const patterns = Base64Helper.URL_PATTERNS.DOMAIN_PATTERNS; // 快速检查是否包含常见网站特征 if ( patterns.POPULAR_SITES.test(str) || patterns.VIDEO_SITES.test(str) || patterns.CN_SITES.test(str) || patterns.TLD.test(str) ) { return false; } // 3. Base64 格式检查 if (!str.match(/^[A-Za-z0-9+/]*={0,2}$/)) return false; // 4. Base64 规则验证 if (str.length % 4 !== 0) return false; if (str.includes('==')) { if (!str.endsWith('==')) return false; } else if (str.includes('=')) { if (!str.endsWith('=')) return false; } // 5. 检查有效长度(去掉padding后) return str.replace(/=+$/, '').length >= 8; } decodeBase64(str) { return decodeURIComponent( atob(str) .split('') .map((c) => `%${c.charCodeAt(0).toString(16).padStart(2, '0')}`) .join('') ); } encodeBase64(str) { return btoa( encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(`0x${p1}`) ) ); } copyToClipboard(text) { // 尝试使用 navigator.clipboard API if (navigator.clipboard && window.isSecureContext) { return navigator.clipboard .writeText(text) .then(() => true) .catch(() => this.fallbackCopy(text)); } return this.fallbackCopy(text); } fallbackCopy(text) { // 尝试使用 GM_setClipboard if (typeof GM_setClipboard !== 'undefined') { try { GM_setClipboard(text); return true; } catch (e) { console.debug('GM_setClipboard failed:', e); } } // 使用 execCommand 作为后备方案 try { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.cssText = 'position:fixed;opacity:0;'; document.body.appendChild(textarea); if (navigator.userAgent.match(/ipad|iphone/i)) { // iOS 设备特殊处理 textarea.contentEditable = true; textarea.readOnly = false; const range = document.createRange(); range.selectNodeContents(textarea); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); textarea.setSelectionRange(0, 999999); } else { textarea.select(); } const success = document.execCommand('copy'); document.body.removeChild(textarea); return success; } catch (e) { console.debug('execCommand copy failed:', e); return false; } } restoreContent() { document.querySelectorAll('.decoded-text').forEach((el) => { const textNode = document.createTextNode(el.dataset.original); el.parentNode.replaceChild(textNode, el); }); this.originalContents.clear(); this.decodeBtn.textContent = '解析本页 Base64'; this.decodeBtn.dataset.mode = 'decode'; this.showNotification('已恢复原始内容', 'success'); this.menu.style.display = 'none'; } resetState() { if (this.decodeBtn.dataset.mode === 'restore') { this.restoreContent(); } } // 处理通知的动画 animateNotification(notification, index) { const currentTransform = getComputedStyle(notification).transform; notification.style.transform = currentTransform; notification.style.transition = 'all 0.3s ease-out'; notification.style.transform = 'translateY(-100%)'; } // 处理通知的淡出 handleNotificationFadeOut(notification) { notification.classList.add('fade-out'); const index = this.notifications.indexOf(notification); this.notifications.slice(0, index).forEach((prev) => { if (prev.parentNode) { prev.style.transform = 'translateY(-100%)'; } }); } // 处理通知容器的清理 cleanupNotificationContainer() { this.notificationEventListeners.forEach(({ element, event, handler }) => { element.removeEventListener(event, handler); }); this.notificationEventListeners = []; this.notificationContainer.remove(); this.notificationContainer = null; } // 处理通知过渡结束 handleNotificationTransitionEnd(e) { if ( e.propertyName === 'opacity' && e.target.classList.contains('fade-out') ) { const notification = e.target; const index = this.notifications.indexOf(notification); this.notifications.forEach((notif, i) => { if (i > index && notif.parentNode) { this.animateNotification(notif, i); } }); if (index > -1) { this.notifications.splice(index, 1); notification.remove(); } if (this.notifications.length === 0) { this.cleanupNotificationContainer(); } } } showNotification(text, type) { if (!this.notificationContainer) { this.notificationContainer = document.createElement('div'); this.notificationContainer.className = 'base64-notifications-container'; document.body.appendChild(this.notificationContainer); const handler = (e) => this.handleNotificationTransitionEnd(e); this.notificationContainer.addEventListener('transitionend', handler); this.notificationEventListeners.push({ element: this.notificationContainer, event: 'transitionend', handler, }); } const notification = document.createElement('div'); notification.className = 'base64-notification'; notification.setAttribute('data-type', type); notification.textContent = text; this.notifications.push(notification); this.notificationContainer.appendChild(notification); setTimeout(() => { if (notification.parentNode) { this.handleNotificationFadeOut(notification); } }, 2000); } } // 初始化逻辑修改 if (window.__base64HelperInstance || window.top !== window.self) { return window.__base64HelperInstance; } // 只在主文档中初始化 if (window.top === window.self) { initStyles(); const instance = new Base64Helper(); if (instance) { // 确保实例创建成功 window.__base64HelperInstance = instance; } } // 页面卸载时清理 window.addEventListener('unload', () => { if (window.__base64HelperInstance) { window.__base64HelperInstance.destroy(); delete window.__base64HelperInstance; } }); })();