// ==UserScript== // @name 网页剪藏工具 Pro // @namespace http://tampermonkey.net/ // @version 4.2 // @description 增强版网页剪藏工具,支持自定义标签和现代化UI,完全兼容PWA // @author You // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // ===== 1. 配置管理模块 ===== const Config = { keys: { backendUrl: 'web_clipper_backend_url', activeSites: 'web_clipper_active_sites', buttonPosition: 'web_clipper_button_position' }, getAll: function() { return { backendUrl: GM_getValue(this.keys.backendUrl, 'https://your-backend.com/api/clipping'), activeSites: GM_getValue(this.keys.activeSites, []), buttonPosition: GM_getValue(this.keys.buttonPosition, { top: '100px', right: '20px' }) }; }, setAll: function(newConfig) { GM_setValue(this.keys.backendUrl, newConfig.backendUrl); GM_setValue(this.keys.activeSites, newConfig.activeSites); GM_setValue(this.keys.buttonPosition, newConfig.buttonPosition); }, isComplete: function() { const config = this.getAll(); return config.backendUrl && config.backendUrl !== 'https://your-backend.com/api/clipping'; }, showSettings: function() { const config = this.getAll(); // 移除已存在的模态框 const existingModal = document.getElementById('web-clipper-settings-modal'); if (existingModal) { existingModal.remove(); } // 添加样式 GM_addStyle(` #web-clipper-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); z-index: 2147483647; display: flex; justify-content: center; align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #web-clipper-settings-content { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); padding: 32px; border-radius: 20px; width: 90%; max-width: 500px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); position: relative; } #web-clipper-settings-content h2 { margin-top: 0; color: #1a1a1a; font-size: 20px; display: flex; align-items: center; gap: 8px; } #web-clipper-settings-content label { display: block; margin-top: 16px; margin-bottom: 6px; font-weight: 500; color: #4a4a4a; font-size: 14px; } #web-clipper-settings-content input, #web-clipper-settings-content textarea { width: 100%; padding: 12px; box-sizing: border-box; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 12px; font-size: 14px; transition: all 0.3s; background: rgba(255, 255, 255, 0.8); } #web-clipper-settings-content input:focus, #web-clipper-settings-content textarea:focus { outline: none; border-color: rgba(76, 175, 80, 0.5); box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); background: rgba(255, 255, 255, 0.95); } #web-clipper-settings-buttons { margin-top: 24px; text-align: right; display: flex; gap: 12px; justify-content: flex-end; } #web-clipper-settings-buttons button { padding: 12px 24px; border: none; border-radius: 12px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.3s; min-width: 80px; } #web-clipper-save-btn { background: rgba(76, 175, 80, 0.9); color: white; } #web-clipper-save-btn:hover { background: rgba(76, 175, 80, 1); transform: translateY(-1px); } #web-clipper-cancel-btn { background: rgba(0, 0, 0, 0.1); color: #4a4a4a; } #web-clipper-cancel-btn:hover { background: rgba(0, 0, 0, 0.15); } `); // 创建模态框 const modal = document.createElement('div'); modal.id = 'web-clipper-settings-modal'; const content = document.createElement('div'); content.id = 'web-clipper-settings-content'; // 创建标题 const title = document.createElement('h2'); title.textContent = '⚙️ 剪藏工具配置'; content.appendChild(title); // 创建后端地址输入 const backendUrlLabel = document.createElement('label'); backendUrlLabel.textContent = '后端服务地址'; backendUrlLabel.setAttribute('for', 'backendUrl'); content.appendChild(backendUrlLabel); const backendUrlInput = document.createElement('input'); backendUrlInput.type = 'url'; backendUrlInput.id = 'backendUrl'; backendUrlInput.placeholder = 'https://your-domain.com/api/clipping'; backendUrlInput.value = config.backendUrl; content.appendChild(backendUrlInput); // 创建激活网站列表 const activeSitesLabel = document.createElement('label'); activeSitesLabel.textContent = '激活网站列表 (每行一个,留空表示全部)'; activeSitesLabel.setAttribute('for', 'activeSites'); content.appendChild(activeSitesLabel); const activeSitesTextarea = document.createElement('textarea'); activeSitesTextarea.id = 'activeSites'; activeSitesTextarea.rows = '5'; activeSitesTextarea.value = config.activeSites.join('\n'); content.appendChild(activeSitesTextarea); // 创建按钮 const buttons = document.createElement('div'); buttons.id = 'web-clipper-settings-buttons'; const cancelButton = document.createElement('button'); cancelButton.id = 'web-clipper-cancel-btn'; cancelButton.textContent = '取消'; cancelButton.addEventListener('click', () => { modal.remove(); }); buttons.appendChild(cancelButton); const saveButton = document.createElement('button'); saveButton.id = 'web-clipper-save-btn'; saveButton.textContent = '保存'; saveButton.addEventListener('click', () => { const newConfig = { backendUrl: backendUrlInput.value.trim(), activeSites: activeSitesTextarea.value.trim() ? activeSitesTextarea.value.trim().split('\n') : [], buttonPosition: config.buttonPosition }; this.setAll(newConfig); modal.remove(); Toast.show('配置已保存!', 'success'); setTimeout(() => window.location.reload(), 1000); }); buttons.appendChild(saveButton); content.appendChild(buttons); modal.appendChild(content); // 添加到body document.body.appendChild(modal); // 绑定背景点击事件 modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); } }; // ===== 2. Toast 通知系统 ===== const Toast = { show: function(message, type = 'info', duration = 3000) { // 添加样式 if (!document.querySelector('#toast-styles')) { GM_addStyle(` .toast { position: fixed; top: 20px; right: 20px; z-index: 2147483647; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 12px; padding: 16px 20px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2); display: flex; align-items: center; gap: 12px; min-width: 250px; animation: slideInRight 0.3s ease; max-width: 90vw; } .toast-success { border-left: 4px solid rgba(76, 175, 80, 0.8); } .toast-error { border-left: 4px solid rgba(244, 67, 54, 0.8); } .toast-info { border-left: 4px solid rgba(33, 150, 243, 0.8); } .toast-icon { font-size: 20px; } .toast-message { flex: 1; font-size: 14px; color: #1a1a1a; } @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `); } // 创建toast元素 const toast = document.createElement('div'); toast.className = `toast toast-${type}`; // 创建图标 const icon = document.createElement('div'); icon.className = 'toast-icon'; icon.textContent = this.getIcon(type); // 创建消息 const messageEl = document.createElement('div'); messageEl.className = 'toast-message'; messageEl.textContent = message; // 组装 toast.appendChild(icon); toast.appendChild(messageEl); // 添加到body document.body.appendChild(toast); // 自动移除 setTimeout(() => { toast.style.animation = 'slideInRight 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, duration); }, getIcon: function(type) { const icons = { success: '✅', error: '❌', info: 'ℹ️' }; return icons[type] || icons.info; } }; // ===== 3. 剪藏面板模块 ===== const ClipperPanel = { isVisible: false, panel: null, show: function() { if (this.isVisible) return; this.isVisible = true; // 移除已存在的面板 const existingPanel = document.getElementById('clipper-panel'); if (existingPanel) { existingPanel.remove(); } // 添加样式 if (!document.querySelector('#clipper-styles')) { GM_addStyle(` #clipper-panel { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); z-index: 2147483647; display: flex; justify-content: center; align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .clipper-content { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 20px; width: 90%; max-width: 500px; max-height: 80vh; overflow-y: auto; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); position: relative; } .clipper-header { padding: 24px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); display: flex; justify-content: space-between; align-items: center; } .clipper-title { font-size: 18px; font-weight: 600; color: #1a1a1a; } .clipper-close { width: 32px; height: 32px; border-radius: 50%; border: none; background: rgba(0, 0, 0, 0.1); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s; font-size: 18px; color: #4a4a4a; } .clipper-close:hover { background: rgba(0, 0, 0, 0.15); transform: rotate(90deg); } .clipper-body { padding: 24px; } .info-item { margin-bottom: 20px; } .info-label { font-size: 12px; color: #6a6a6a; margin-bottom: 8px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } .info-input { width: 100%; padding: 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 12px; font-size: 14px; transition: all 0.3s; background: rgba(255, 255, 255, 0.8); box-sizing: border-box; } .info-input:focus { outline: none; border-color: rgba(76, 175, 80, 0.5); box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); background: rgba(255, 255, 255, 0.95); } .info-input:hover { border-color: rgba(0, 0, 0, 0.2); } .tags-hint { font-size: 12px; color: #6a6a6a; margin-top: 8px; display: flex; align-items: center; gap: 4px; } .clipper-footer { padding: 24px; border-top: 1px solid rgba(0, 0, 0, 0.1); display: flex; gap: 12px; justify-content: flex-end; } .clipper-btn { padding: 12px 24px; border: none; border-radius: 12px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.3s; min-width: 100px; } .btn-clip { background: rgba(76, 175, 80, 0.9); color: white; } .btn-clip:hover:not(:disabled) { background: rgba(76, 175, 80, 1); transform: translateY(-1px); } .btn-clip:disabled { background: rgba(0, 0, 0, 0.2); cursor: not-allowed; } .btn-cancel { background: rgba(0, 0, 0, 0.1); color: #4a4a4a; } .btn-cancel:hover { background: rgba(0, 0, 0, 0.15); } .loading-spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid #ffffff; border-radius: 50%; border-top-color: transparent; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } `); } // 创建面板 const panel = document.createElement('div'); panel.id = 'clipper-panel'; const content = document.createElement('div'); content.className = 'clipper-content'; // 创建头部 const header = document.createElement('div'); header.className = 'clipper-header'; const title = document.createElement('div'); title.className = 'clipper-title'; title.textContent = '⚡️ 网页剪藏'; const closeButton = document.createElement('button'); closeButton.className = 'clipper-close'; closeButton.textContent = '×'; closeButton.addEventListener('click', () => { this.hide(); }); header.appendChild(title); header.appendChild(closeButton); // 创建主体 const body = document.createElement('div'); body.className = 'clipper-body'; // 标题输入 const titleContainer = document.createElement('div'); titleContainer.className = 'info-item'; const titleLabel = document.createElement('div'); titleLabel.className = 'info-label'; titleLabel.textContent = '标题'; const titleInput = document.createElement('input'); titleInput.type = 'text'; titleInput.className = 'info-input'; titleInput.id = 'clip-title-input'; titleInput.value = document.title; titleInput.placeholder = '输入文章标题'; titleContainer.appendChild(titleLabel); titleContainer.appendChild(titleInput); // URL输入 const urlContainer = document.createElement('div'); urlContainer.className = 'info-item'; const urlLabel = document.createElement('div'); urlLabel.className = 'info-label'; urlLabel.textContent = '链接'; const urlInput = document.createElement('input'); urlInput.type = 'url'; urlInput.className = 'info-input'; urlInput.id = 'clip-url-input'; urlInput.value = window.location.href; urlInput.placeholder = '输入文章链接'; urlContainer.appendChild(urlLabel); urlContainer.appendChild(urlInput); // 标签输入 const tagsContainer = document.createElement('div'); tagsContainer.className = 'info-item'; const tagsLabel = document.createElement('div'); tagsLabel.className = 'info-label'; tagsLabel.textContent = '自定义标签(可选)'; const tagsInput = document.createElement('input'); tagsInput.type = 'text'; tagsInput.className = 'info-input'; tagsInput.id = 'custom-tags-input'; tagsInput.placeholder = '输入标签,用逗号或空格分隔'; const tagsHint = document.createElement('div'); tagsHint.className = 'tags-hint'; tagsHint.textContent = '💡 AI将根据您的标签生成补充标签,避免重复'; tagsContainer.appendChild(tagsLabel); tagsContainer.appendChild(tagsInput); tagsContainer.appendChild(tagsHint); body.appendChild(titleContainer); body.appendChild(urlContainer); body.appendChild(tagsContainer); // 创建底部 const footer = document.createElement('div'); footer.className = 'clipper-footer'; const cancelButton = document.createElement('button'); cancelButton.className = 'clipper-btn btn-cancel'; cancelButton.textContent = '取消'; cancelButton.addEventListener('click', () => { this.hide(); }); const clipButton = document.createElement('button'); clipButton.className = 'clipper-btn btn-clip'; const buttonText = document.createElement('span'); buttonText.id = 'clip-button-text'; buttonText.textContent = '开始剪藏'; clipButton.appendChild(buttonText); clipButton.addEventListener('click', () => { WebClipper.doClip(); }); footer.appendChild(cancelButton); footer.appendChild(clipButton); // 组装面板 content.appendChild(header); content.appendChild(body); content.appendChild(footer); panel.appendChild(content); // 添加到body document.body.appendChild(panel); this.panel = panel; // 绑定背景点击事件 panel.addEventListener('click', (e) => { if (e.target === panel) { this.hide(); } }); // 自动聚焦标题输入框 setTimeout(() => { titleInput.focus(); titleInput.select(); }, 300); }, hide: function() { if (this.panel) { this.panel.remove(); this.panel = null; } this.isVisible = false; }, setLoading: function(loading) { const button = document.querySelector('.btn-clip'); const buttonText = document.getElementById('clip-button-text'); if (button && buttonText) { if (loading) { button.disabled = true; buttonText.textContent = ''; const spinner = document.createElement('span'); spinner.className = 'loading-spinner'; buttonText.parentNode.insertBefore(spinner, buttonText); buttonText.textContent = ' 剪藏中...'; } else { button.disabled = false; const spinner = button.querySelector('.loading-spinner'); if (spinner) spinner.remove(); buttonText.textContent = '开始剪藏'; } } } }; // ===== 4. 核心剪藏模块 ===== const WebClipper = { config: {}, button: null, dragThrottle: null, init: function() { this.config = Config.getAll(); if (!Config.isComplete()) { console.warn('脚本配置不完整,请在菜单中打开设置进行配置。'); return; } if (this.config.activeSites.length > 0) { const currentHostname = window.location.hostname; const isAllowed = this.config.activeSites.some(site => { const regex = new RegExp('^' + site.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); return regex.test(currentHostname); }); if (!isAllowed) { return; } } console.log(`Web Clipper: 启动`); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.createFloatingButton()); } else { setTimeout(() => this.createFloatingButton(), 500); } }, fetchWithGM: function(details) { return new Promise((resolve, reject) => { details.onload = (res) => { if (res.status >= 200 && res.status < 300) { resolve(res); } else { reject(new Error(`请求失败: ${res.status} ${res.statusText}`)); } }; details.onerror = (err) => reject(err); GM_xmlhttpRequest(details); }); }, doClip: async function() { const titleInput = document.getElementById('clip-title-input'); const urlInput = document.getElementById('clip-url-input'); const tagsInput = document.getElementById('custom-tags-input'); const title = titleInput ? titleInput.value.trim() : document.title; const url = urlInput ? urlInput.value.trim() : window.location.href; const customTags = tagsInput ? tagsInput.value.split(/[,,\s]+/).map(tag => tag.trim()).filter(tag => tag) : []; // 验证必填字段 if (!title) { Toast.show('请输入标题', 'error'); return; } if (!url) { Toast.show('请输入链接', 'error'); return; } ClipperPanel.setLoading(true); try { const data = { title: title, url: url, html: document.documentElement.outerHTML, customTags: customTags }; const response = await this.fetchWithGM({ method: 'POST', url: this.config.backendUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data) }); const result = JSON.parse(response.responseText); if (result.success) { ClipperPanel.hide(); Toast.show(`剪藏成功!生成标签:${result.tags}`, 'success'); if (this.button) { const originalText = this.button.textContent; this.button.textContent = '✅'; setTimeout(() => { this.button.textContent = originalText; }, 2000); } } else { throw new Error(result.error || '剪藏操作失败'); } } catch (error) { console.error('剪藏失败:', error); Toast.show(`剪藏失败: ${error.message}`, 'error'); } finally { ClipperPanel.setLoading(false); } }, makeDraggable: function(element) { let isDragging = false; let startX, startY, initialTop, initialRight; function handleStart(e) { isDragging = true; startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; startY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY; const rect = element.getBoundingClientRect(); initialTop = rect.top; initialRight = window.innerWidth - rect.right; element.style.cursor = 'grabbing'; element.style.transition = 'none'; } function handleMove(e) { if (!isDragging) return; // 使用节流优化性能 if (WebClipper.dragThrottle) return; WebClipper.dragThrottle = requestAnimationFrame(() => { WebClipper.dragThrottle = null; e.preventDefault(); const currentX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; const currentY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY; const deltaY = currentY - startY; const deltaX = currentX - startX; const newTop = initialTop + deltaY; const newRight = initialRight - deltaX; element.style.top = `${newTop}px`; element.style.right = `${newRight}px`; element.style.left = 'auto'; element.style.bottom = 'auto'; }); } function handleEnd() { if (!isDragging) return; isDragging = false; element.style.cursor = 'move'; element.style.transition = ''; if (WebClipper.dragThrottle) { cancelAnimationFrame(WebClipper.dragThrottle); WebClipper.dragThrottle = null; } const style = window.getComputedStyle(element); const newPosition = { top: style.top, right: style.right }; WebClipper.config.buttonPosition = newPosition; Config.setAll(WebClipper.config); } element.addEventListener('mousedown', handleStart); document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); element.addEventListener('touchstart', handleStart, { passive: false }); document.addEventListener('touchmove', handleMove, { passive: false }); element.addEventListener('touchend', handleEnd); }, createFloatingButton: function() { if (document.getElementById('web-clipper-btn')) return; // 添加样式 - 修复按钮可见性问题 GM_addStyle(` #web-clipper-btn { position: fixed; z-index: 2147483646; cursor: move; user-select: none; font-size: 24px; top: ${this.config.buttonPosition.top}; right: ${this.config.buttonPosition.right}; width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, rgba(76, 175, 80, 0.9), rgba(69, 160, 73, 0.9)); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); color: white; display: flex; align-items: center; justify-content: center; box-shadow: 0 8px 32px -8px rgba(76, 175, 80, 0.4); transition: all 0.2s ease; border: 1px solid rgba(255, 255, 255, 0.2); } #web-clipper-btn:hover { transform: scale(1.05); box-shadow: 0 12px 40px -8px rgba(76, 175, 80, 0.5); background: linear-gradient(135deg, rgba(76, 175, 80, 1), rgba(69, 160, 73, 1)); } #web-clipper-btn:active { transform: scale(0.95); } @media (max-width: 768px) { #web-clipper-btn { width: 44px; height: 44px; font-size: 20px; } } `); this.button = document.createElement('div'); this.button.id = 'web-clipper-btn'; this.button.title = '点击剪藏,双击打开设置'; this.button.textContent = '⚡️'; this.button.style.cssText = ` position: fixed; z-index: 2147483646; cursor: move; user-select: none; font-size: 24px; top: ${this.config.buttonPosition.top}; right: ${this.config.buttonPosition.right}; `; document.body.appendChild(this.button); this.makeDraggable(this.button); this.button.addEventListener('click', (e) => { if (e.detail === 1) { setTimeout(() => ClipperPanel.show(), 200); } }); this.button.addEventListener('dblclick', (e) => { e.stopPropagation(); Config.showSettings(); }); console.log('剪藏按钮已创建'); } }; // ===== 5. 初始化和菜单注册 ===== GM_registerMenuCommand('⚙️ 剪藏工具设置', () => { Config.showSettings(); }); console.log('Web Clipper Pro 脚本已加载'); if (!Config.isComplete()) { console.warn('检测到配置不完整,已弹出设置窗口。'); Config.showSettings(); } WebClipper.init(); })();