// ==UserScript== // @name Discourse Comment Extractor | Discourse 评论提取器 // @name:zh-CN Discourse 评论提取器 // @name:en Discourse Comment Extractor // @name:ja Discourse コメント抽出器 // @name:ko Discourse 댓글 추출기 // @name:fr Extracteur de Commentaires Discourse // @name:de Discourse Kommentar-Extraktor // @name:es Extractor de Comentarios de Discourse // @name:ru Извлекатель Комментариев Discourse // @namespace https://github.com/discourse-tools/comment-extractor // @version 1.9.0 // @description Advanced comment extraction tool for Discourse forums with modern TailwindCSS interface, smart filtering, email extraction, and data export capabilities. Author-only access with API verification. // @description:zh-CN 提取 Discourse 帖子下的所有评论,支持楼层范围、随机提取、邮箱提取和数据导出功能。现代化TailwindCSS界面设计,仅限帖子作者使用,API权限验证。 // @description:en Advanced comment extraction tool for Discourse forums with modern TailwindCSS interface, smart filtering, email extraction, and data export capabilities. Author-only access with API verification. // @description:ja Discourse フォーラム用の高度なコメント抽出ツール。モダンな TailwindCSS インターフェース、スマートフィルタリング、メール抽出、データエクスポート機能付き。作成者のみアクセス可能、API認証。 // @description:ko Discourse 포럼용 고급 댓글 추출 도구. 현대적인 TailwindCSS 인터페이스, 스마트 필터링, 이메일 추출, 데이터 내보내기 기능. 작성자 전용 액세스, API 인증. // @description:fr Outil d'extraction de commentaires avancé pour les forums Discourse avec interface TailwindCSS moderne, filtrage intelligent, extraction d'emails et capacités d'export de données. Accès réservé aux auteurs, vérification API. // @description:de Erweiterte Kommentar-Extraktions-Tool für Discourse-Foren mit modernem TailwindCSS-Interface, intelligentem Filtern, E-Mail-Extraktion und Datenexport-Funktionen. Nur für Autoren zugänglich, API-Verifizierung. // @description:es Herramienta avanzada de extracción de comentarios para foros Discourse con interfaz TailwindCSS moderna, filtrado inteligente, extracción de emails y capacidades de exportación de datos. Solo acceso para autores, verificación API. // @description:ru Продвинутый инструмент извлечения комментариев для форумов Discourse с современным интерфейсом TailwindCSS, умной фильтрацией, извлечением email и возможностями экспорта данных. Доступ только для авторов, API-верификация. // @author dext7r // @license MIT // @homepageURL https://linux.do/t/topic/705152 // @supportURL https://linux.do/t/topic/705152 // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgo8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojNjY3ZWVhO3N0b3Atb3BhY2l0eToxIiAvPgo8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiM3NjRiYTI7c3RvcC1vcGFjaXR5OjEiIC8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPGV4dGdvbiB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHg9IjIiIHk9IjIiIHJ4PSIxMiIgZmlsbD0idXJsKCNncmFkaWVudCkiLz4KPHN2ZyB4PSIxNiIgeT0iMTYiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNOCAxMGg4TTE4IDE0aDZNNiA0aDEyYTIgMiAwIDAxMiAydjEyYTIgMiAwIDAxLTIgMkg2YTIgMiAwIDAxLTItMlY2YTIgMiAwIDAxMi0yeiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIi8+Cjwvc3ZnPgo8L3N2Zz4= // @icon64 data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgo8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojNjY3ZWVhO3N0b3Atb3BhY2l0eToxIiAvPgo8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiM3NjRiYTI7c3RvcC1vcGFjaXR5OjEiIC8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPGV4dGdvbiB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHg9IjIiIHk9IjIiIHJ4PSIxMiIgZmlsbD0idXJsKCNncmFkaWVudCkiLz4KPHN2ZyB4PSIxNiIgeT0iMTYiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNOCAxMGg4TTE4IDE0aDZNNiA0aDEyYTIgMiAwIDAxMiAydjEyYTIgMiAwIDAxLTIgMkg2YTIgMiAwIDAxLTItMlY2YTIgMiAwIDAxMi0yeiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIi8+Cjwvc3ZnPgo8L3N2Zz4= // @compatible chrome >=90 // @compatible firefox >=88 // @compatible edge >=90 // @compatible safari >=14 // @compatible opera >=76 // // @match https://*/t/* // @match https://*/topic/* // @match https://*/topics/* // @match https://*/discussion/* // @match https://*/discussions/* // @match http://*/t/* // @match http://*/topic/* // @match http://*/topics/* // // International Discourse Sites // @match https://community.*/t/* // @match https://discuss.*/t/* // @match https://forum.*/t/* // @match https://forums.*/t/* // @match https://support.*/t/* // @match https://help.*/t/* // @match https://talk.*/t/* // @match https://chat.*/t/* // @match https://discourse.*/t/* // // Popular Discourse Instances // @match https://meta.discourse.org/t/* // @match https://try.discourse.org/t/* // @match https://blog.discourse.org/t/* // @match https://developers.discourse.org/t/* // @match https://blog.codinghorror.com/t/* // @match https://what.thedailywtf.com/t/* // @match https://discuss.pytorch.org/t/* // @match https://discuss.tensorflow.org/t/* // @match https://discuss.atom.io/t/* // @match https://discuss.brew.sh/t/* // @match https://discuss.elastic.co/t/* // @match https://discuss.circleci.com/t/* // @match https://discuss.gradle.org/t/* // @match https://discuss.kotlinlang.org/t/* // @match https://discuss.ocaml.org/t/* // @match https://discuss.python.org/t/* // @match https://discuss.swift.org/t/* // @match https://discuss.vuejs.org/t/* // @match https://discuss.wxpython.org/t/* // @match https://discuss.yarnpkg.com/t/* // @match https://community.frame.work/t/* // @match https://community.fly.io/t/* // @match https://community.cloudflare.com/t/* // @match https://community.postman.com/t/* // @match https://community.render.com/t/* // @match https://community.spotify.com/t/* // @match https://community.openai.com/t/* // @match https://developers.google.com/t/* // @match https://forum.arduino.cc/t/* // @match https://forum.gitlab.com/t/* // @match https://forum.freecodecamp.org/t/* // @match https://forum.manjaro.org/t/* // @match https://forum.endeavouros.com/t/* // @match https://forum.kde.org/t/* // @match https://forum.snapcraft.io/t/* // @match https://forum.unity.com/t/* // // Chinese Discourse Communities // @match https://forum.ubuntu.org.cn/t/* // @match https://forum.deepin.org/t/* // @match https://bbs.archlinuxcn.org/t/* // @match https://discuss.flarum.org.cn/t/* // @match https://forum.gamer.com.tw/t/* // @match https://community.jiumodiary.com/t/* // @match https://forum.china-scratch.com/t/* // @match https://forum.freebuf.com/t/* // @match https://bbs.huaweicloud.com/t/* // @match https://developer.aliyun.com/t/* // @match https://juejin.cn/t/* // @match https://segmentfault.com/t/* // // European Discourse Sites // @match https://forum.ubuntu-fr.org/t/* // @match https://forum.ubuntu-it.org/t/* // @match https://forum.ubuntu-es.org/t/* // @match https://forum.ubuntu.de/t/* // @match https://forum.manjaro.de/t/* // @match https://forum.opensuse.org/t/* // @match https://discuss.kde.org/t/* // @match https://forum.fedoraproject.org/t/* // // Japanese Discourse Sites // @match https://forum.ubuntulinux.jp/t/* // @match https://discuss.elastic.co/t/* // @match https://jp.discourse.group/t/* // // Generic Wildcard Patterns (for discovery) // @match https://*.discourse.group/t/* // @match https://*.discoursehosting.com/t/* // @match https://*.discoursecdn.com/t/* // @match https://discourse-*.herokuapp.com/t/* // @match https://*-discourse.com/t/* // @match https://discourse.*.com/t/* // @match https://discourse.*.org/t/* // @match https://discourse.*.net/t/* // @match https://discourse.*.io/t/* // @match https://discourse.*.dev/t/* // // @grant none // @run-at document-end // @noframes // @require https://cdn.tailwindcss.com/3.3.0 // // @tag discourse // @tag comment // @tag extractor // @tag forum // @tag data-export // @tag email-extraction // @tag csv // @tag json // @tag tailwindcss // @tag modern-ui // @tag author-only // @tag api-verification // @downloadURL https://update.greasyfork.icu/scripts/538494/Discourse%20Comment%20Extractor%20%7C%20Discourse%20%E8%AF%84%E8%AE%BA%E6%8F%90%E5%8F%96%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/538494/Discourse%20Comment%20Extractor%20%7C%20Discourse%20%E8%AF%84%E8%AE%BA%E6%8F%90%E5%8F%96%E5%99%A8.meta.js // ==/UserScript== (function () { 'use strict'; /** * Discourse 评论提取器主类 * 使用现代 JavaScript 类语法和高级编程模式 */ class DiscourseCommentExtractor { constructor() { // 常量配置 this.config = { emailRegex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, storageKey: 'discourse_extractor_history', maxHistoryRecords: 100, initDelay: 2000, permissionCheckDelay: 1000, loadingTimeout: 30000, maxLoadAttempts: 30 }; // 状态管理 this.state = { isInitialized: false, currentUser: null, topicAuthor: null, hasPermission: false, isLoading: false }; // 缓存DOM查询结果 this.cache = new Map(); // API管理器 this.api = new DiscourseAPIManager(); // 权限管理器 this.permissionManager = new PermissionManager(this.api); // UI管理器 this.uiManager = new UIManager(); // 存储管理器 this.storageManager = new StorageManager(this.config.storageKey, this.config.maxHistoryRecords); // 绑定方法 this.init = this.init.bind(this); this.handleExtractClick = this.handleExtractClick.bind(this); } /** * 初始化提取器 */ async init() { if (this.state.isInitialized) return; try { console.log('🚀 初始化 Discourse 评论提取器...'); // 检查是否为 Discourse 论坛 if (!this.isDiscourse()) { console.log('❌ 非 Discourse 论坛,跳过初始化'); return; } // 等待页面加载完成 await this.waitForPageReady(); // 加载样式 this.uiManager.loadStyles(); // 检查权限并创建按钮 await this.checkPermissionAndCreateButton(); this.state.isInitialized = true; console.log('✅ 评论提取器初始化完成'); } catch (error) { console.error('❌ 初始化失败:', error); } } /** * 检查是否为 Discourse 论坛 */ isDiscourse() { return !!( document.querySelector('meta[name="generator"][content*="Discourse"]') || document.querySelector('.topic-post, [data-post-id]') || window.location.pathname.includes('/t/') || document.body.classList.contains('discourse') ); } /** * 等待页面准备就绪 */ async waitForPageReady() { return new Promise((resolve) => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(resolve, this.config.initDelay); }); } else { setTimeout(resolve, this.config.initDelay); } }); } /** * 检查权限并创建按钮 */ async checkPermissionAndCreateButton() { try { // 获取用户和帖子信息 const [currentUser, topicAuthor] = await Promise.all([ this.permissionManager.getCurrentUser(), this.permissionManager.getTopicAuthor() ]); this.state.currentUser = currentUser; this.state.topicAuthor = topicAuthor; console.log('👤 当前用户:', currentUser); console.log('📝 帖子作者:', topicAuthor); // 检查权限 this.state.hasPermission = this.permissionManager.checkPermission(currentUser, topicAuthor); console.log('🔒 权限检查结果:', this.state.hasPermission); // 创建按钮 this.uiManager.createButton(this.state.hasPermission, this.handleExtractClick, this.handlePermissionError.bind(this)); } catch (error) { console.error('❌ 权限检查失败:', error); this.uiManager.createButton(false, null, this.handlePermissionError.bind(this)); } } /** * 处理提取按钮点击 */ async handleExtractClick() { try { // 双重权限检查 if (!await this.revalidatePermission()) { await this.handlePermissionError(); return; } // 显示配置模态框 this.uiManager.showConfigModal((config) => { this.startExtraction(config); }); } catch (error) { console.error('❌ 提取过程失败:', error); this.uiManager.showToast('提取失败,请重试', 'error'); } } /** * 重新验证权限 */ async revalidatePermission() { const [currentUser, topicAuthor] = await Promise.all([ this.permissionManager.getCurrentUser(), this.permissionManager.getTopicAuthor() ]); return this.permissionManager.checkPermission(currentUser, topicAuthor); } /** * 处理权限错误 */ async handlePermissionError() { const [currentUser, topicAuthor] = await Promise.all([ this.permissionManager.getCurrentUser(), this.permissionManager.getTopicAuthor() ]); this.uiManager.showPermissionError(currentUser, topicAuthor); } /** * 开始提取评论 */ async startExtraction(config) { if (this.state.isLoading) return; this.state.isLoading = true; const progressModal = this.uiManager.showLoadingProgress(); try { // 创建评论加载器 const loader = new CommentLoader(this.api); // 加载所有评论 const comments = await loader.loadAllComments((current, total, attempts) => { this.uiManager.updateProgress(current, total, attempts); }); // 创建评论提取器 const extractor = new CommentExtractor(this.config.emailRegex); // 提取评论 const extractedData = extractor.extractComments({ comments, mode: config.mode, startFloor: config.startFloor, endFloor: config.endFloor, randomCount: config.randomCount, extractEmails: config.extractEmails }); // 关闭进度模态框 this.uiManager.closeModal(progressModal); // 显示结果 this.uiManager.showResults(extractedData); // 保存到历史记录 this.storageManager.saveRecord({ timestamp: Date.now(), url: window.location.href, title: document.title, mode: config.mode, totalComments: extractedData.comments.length, emailCount: extractedData.emails.length, config: config }); this.uiManager.showToast(`成功提取 ${extractedData.comments.length} 条评论`, 'success'); } catch (error) { console.error('提取失败:', error); this.uiManager.closeModal(progressModal); this.uiManager.showToast('提取失败,请重试', 'error'); } finally { this.state.isLoading = false; } } } /** * Discourse API 管理器 */ class DiscourseAPIManager { constructor() { this.cache = new Map(); this.sessionData = null; } /** * 获取当前用户信息 */ async getCurrentUser() { if (this.cache.has('currentUser')) { return this.cache.get('currentUser'); } try { const response = await fetch('/session/current.json', { method: 'GET', credentials: 'include', headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); const user = data.current_user; this.cache.set('currentUser', user); this.sessionData = data; console.log('🔍 API获取当前用户:', user); return user; } catch (error) { console.warn('⚠️ API获取用户信息失败,回退到DOM解析:', error); return null; } } /** * 获取完整的主题信息 */ async getFullTopicInfo() { const topicId = this.extractTopicId(); if (!topicId) { throw new Error('无法提取主题ID'); } const cacheKey = `fullTopicInfo_${topicId}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } try { const response = await fetch(`/t/${topicId}.json`, { method: 'GET', credentials: 'include', headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const topicData = await response.json(); console.log('🔍 API获取完整主题信息:', { id: topicData.id, title: topicData.title, posts_count: topicData.posts_count, created_by: topicData.details?.created_by }); this.cache.set(cacheKey, topicData); return topicData; } catch (error) { console.warn('⚠️ API获取主题信息失败:', error); throw error; } } /** * 获取总帖子数量 - 新增方法 */ async getTotalPostsCount() { try { const topicInfo = await this.getFullTopicInfo(); return topicInfo.posts_count || 0; } catch (error) { console.warn('⚠️ 无法从API获取帖子数量:', error); return 0; } } /** * 获取主题信息(简化版本) */ async getTopicInfo() { return this.getFullTopicInfo(); } /** * 获取主题帖子数量(兼容方法) */ async getTopicPostsCount() { return this.getTotalPostsCount(); } /** * 清除缓存 */ clearCache() { this.cache.clear(); this.sessionData = null; } /** * 从URL提取主题ID */ extractTopicId() { const pathMatch = window.location.pathname.match(/\/t\/[^\/]+\/(\d+)/); if (pathMatch) { const topicId = parseInt(pathMatch[1], 10); console.log('🔍 从路径提取主题ID:', topicId); return topicId; } const hashMatch = window.location.hash.match(/#\/t\/[^\/]+\/(\d+)/); if (hashMatch) { const topicId = parseInt(hashMatch[1], 10); console.log('🔍 从Hash提取主题ID:', topicId); return topicId; } const urlParams = new URLSearchParams(window.location.search); const topicIdParam = urlParams.get('topic_id') || urlParams.get('id'); if (topicIdParam) { const topicId = parseInt(topicIdParam, 10); console.log('🔍 从查询参数提取主题ID:', topicId); return topicId; } const metaTopicId = document.querySelector('meta[property="discourse:topic_id"]'); if (metaTopicId) { const topicId = parseInt(metaTopicId.getAttribute('content'), 10); console.log('🔍 从Meta标签提取主题ID:', topicId); return topicId; } const bodyDataset = document.body.dataset; if (bodyDataset.topicId) { const topicId = parseInt(bodyDataset.topicId, 10); console.log('🔍 从Body数据提取主题ID:', topicId); return topicId; } console.warn('⚠️ 无法提取主题ID'); return null; } } /** * 权限管理器 */ class PermissionManager { constructor(apiManager) { this.api = apiManager; } /** * 获取当前用户信息 */ async getCurrentUser() { // 先尝试从API获取 const apiUser = await this.api.getCurrentUser(); if (apiUser) { return apiUser; } // 回退到DOM解析 return this.getCurrentUserFromDOM(); } /** * 从DOM获取当前用户信息 */ getCurrentUserFromDOM() { const userSelectors = [ '.current-user .username', '[data-username]', '.header-dropdown-toggle.current-user', '.user-menu .username', '.current-user-info .username', 'meta[name="discourse_current_user_id"]' ]; for (const selector of userSelectors) { const element = document.querySelector(selector); if (element) { if (selector.includes('meta')) { const userId = element.getAttribute('content'); if (userId) { console.log('🔍 DOM获取用户ID:', userId); return { id: parseInt(userId, 10) }; } } else { const username = element.textContent?.trim() || element.getAttribute('data-username'); if (username) { console.log('🔍 DOM获取用户名:', username); return { username }; } } } } console.warn('⚠️ 无法从DOM获取用户信息'); return null; } /** * 获取帖子作者信息 */ async getTopicAuthor() { // 先尝试从API获取 try { const topicInfo = await this.api.getFullTopicInfo(); if (topicInfo && topicInfo.details && topicInfo.details.created_by) { const author = topicInfo.details.created_by; console.log('🔍 API获取帖子作者:', author); return author; } } catch (error) { console.warn('⚠️ API获取帖子作者失败,回退到DOM解析:', error); } // 回退到DOM解析 return this.getTopicAuthorFromDOM(); } /** * 从DOM获取帖子作者信息 */ getTopicAuthorFromDOM() { const authorSelectors = [ '.topic-post:first-child .username', '[data-post-number="1"] .username', '.topic-avatar .username', '.original-poster .username', '.first-post .username', '.topic-meta-data .username', '.creator .username' ]; for (const selector of authorSelectors) { const element = document.querySelector(selector); if (element) { const username = element.textContent?.trim() || element.getAttribute('data-username'); if (username) { console.log('🔍 DOM获取帖子作者:', username); return { username }; } } } const postElement = document.querySelector('.topic-post[data-post-number="1"], .topic-post:first-child'); if (postElement) { const userElement = postElement.querySelector('[data-username], .username'); if (userElement) { const username = userElement.textContent?.trim() || userElement.getAttribute('data-username'); if (username) { console.log('🔍 DOM获取首个帖子作者:', username); return { username }; } } } console.warn('⚠️ 无法从DOM获取帖子作者'); return null; } /** * 检查权限 */ checkPermission(currentUser, topicAuthor) { if (!currentUser || !topicAuthor) { console.log('🔒 权限检查:用户或作者信息缺失'); return false; } const normalizeUsername = (username) => { return username ? username.toString().toLowerCase().trim() : ''; }; const currentUsername = normalizeUsername(currentUser.username); const authorUsername = normalizeUsername(topicAuthor.username); const hasPermission = currentUsername === authorUsername; console.log('🔒 权限检查详情:', { currentUser: currentUsername, topicAuthor: authorUsername, hasPermission }); return hasPermission; } } /** * UI管理器 - 现代化TailwindCSS设计 */ class UIManager { constructor() { this.stylesLoaded = false; } /** * 加载样式 */ loadStyles() { if (this.stylesLoaded) return; this.loadTailwindCSS(); this.addCustomStyles(); this.stylesLoaded = true; } /** * 加载 Tailwind CSS */ loadTailwindCSS() { if (document.querySelector('#tailwind-css-discourse-extractor')) return; const link = document.createElement('link'); link.id = 'tailwind-css-discourse-extractor'; link.rel = 'stylesheet'; link.href = 'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css'; document.head.appendChild(link); } /** * 添加现代化自定义样式 - 增强移动端支持 */ addCustomStyles() { if (document.querySelector('#discourse-extractor-styles')) return; const style = document.createElement('style'); style.id = 'discourse-extractor-styles'; style.textContent = ` /* 主容器样式 - 响应式优化 */ .discourse-extractor-modal { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; background: linear-gradient(135deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.8) 100%) !important; backdrop-filter: blur(8px) !important; display: flex !important; align-items: center !important; justify-content: center !important; z-index: 999999 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; animation: fadeInModal 0.3s ease-out !important; padding: 0.5rem !important; } .discourse-extractor-content { background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%) !important; border-radius: 20px !important; max-width: 100% !important; max-height: 100% !important; width: 100% !important; overflow-y: auto !important; padding: 0 !important; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.05) !important; position: relative !important; animation: slideUpContent 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) !important; } /* 桌面端样式 */ @media (min-width: 768px) { .discourse-extractor-modal { padding: 2rem !important; } .discourse-extractor-content { max-width: 95vw !important; max-height: 95vh !important; width: 1000px !important; border-radius: 24px !important; } } /* 移动端全屏优化 */ @media (max-width: 767px) { .discourse-extractor-modal { padding: 0 !important; } .discourse-extractor-content { border-radius: 0 !important; height: 100vh !important; max-height: 100vh !important; } } /* 现代化按钮 - 响应式优化 */ .discourse-extractor-btn { position: fixed !important; top: 1rem !important; right: 1rem !important; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; border: none !important; border-radius: 16px !important; padding: 12px 20px !important; font-size: 14px !important; font-weight: 600 !important; cursor: pointer !important; z-index: 999998 !important; display: flex !important; align-items: center !important; gap: 8px !important; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1) inset !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; backdrop-filter: blur(10px) !important; min-height: 44px !important; /* 移动端触摸目标最小尺寸 */ min-width: 44px !important; } /* 桌面端按钮样式 */ @media (min-width: 768px) { .discourse-extractor-btn { top: 20px !important; right: 20px !important; padding: 14px 24px !important; gap: 10px !important; border-radius: 18px !important; } } /* 移动端按钮优化 */ @media (max-width: 767px) { .discourse-extractor-btn { top: 0.75rem !important; right: 0.75rem !important; padding: 10px 16px !important; font-size: 13px !important; border-radius: 14px !important; box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1) inset !important; } } .discourse-extractor-btn:hover { transform: translateY(-3px) scale(1.02) !important; box-shadow: 0 15px 35px rgba(102, 126, 234, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.2) inset !important; background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%) !important; } /* 移动端触摸优化 */ @media (max-width: 767px) { .discourse-extractor-btn:hover { transform: scale(1.05) !important; } } .discourse-extractor-btn:active { transform: translateY(-1px) scale(1.01) !important; } /* 移动端触摸反馈 */ @media (max-width: 767px) { .discourse-extractor-btn:active { transform: scale(0.98) !important; } } /* 关闭按钮 - 移动端优化 */ .discourse-extractor-close { position: absolute !important; top: 12px !important; right: 12px !important; background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%) !important; border: none !important; border-radius: 12px !important; width: 44px !important; /* 移动端触摸目标最小尺寸 */ height: 44px !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; font-size: 20px !important; color: #64748b !important; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; z-index: 10 !important; } /* 桌面端关闭按钮 */ @media (min-width: 768px) { .discourse-extractor-close { top: 16px !important; right: 16px !important; width: 40px !important; height: 40px !important; font-size: 18px !important; } } /* 移动端关闭按钮优化 */ @media (max-width: 767px) { .discourse-extractor-close { top: 8px !important; right: 8px !important; width: 48px !important; height: 48px !important; font-size: 22px !important; border-radius: 14px !important; } } .discourse-extractor-close:hover { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important; color: white !important; transform: rotate(90deg) scale(1.1) !important; box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3) !important; } /* 移动端触摸优化 */ @media (max-width: 767px) { .discourse-extractor-close:hover { transform: scale(1.1) !important; } .discourse-extractor-close:active { transform: scale(0.95) !important; } } /* Toast通知 - 移动端优化 */ .toast-notification { position: fixed !important; top: 80px !important; right: 1rem !important; left: 1rem !important; background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; color: white !important; padding: 16px 20px !important; border-radius: 12px !important; font-size: 14px !important; font-weight: 600 !important; z-index: 999999 !important; box-shadow: 0 10px 25px rgba(16, 185, 129, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1) inset !important; animation: slideInDown 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), slideOutUp 0.3s ease 2.7s !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; backdrop-filter: blur(10px) !important; display: flex !important; align-items: center !important; gap: 8px !important; max-width: none !important; } /* 桌面端Toast */ @media (min-width: 768px) { .toast-notification { top: 90px !important; right: 20px !important; left: auto !important; max-width: 400px !important; padding: 16px 24px !important; animation: slideInRight 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), slideOutRight 0.3s ease 2.7s !important; } } /* 移动端Toast优化 */ @media (max-width: 767px) { .toast-notification { top: 70px !important; margin: 0 0.75rem !important; padding: 14px 18px !important; font-size: 13px !important; border-radius: 10px !important; } } .toast-notification.error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important; box-shadow: 0 10px 25px rgba(239, 68, 68, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1) inset !important; } /* 动画效果 */ @keyframes fadeInModal { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUpContent { from { opacity: 0; transform: translateY(30px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes slideInRight { from { transform: translateX(100%) scale(0.8); opacity: 0; } to { transform: translateX(0) scale(1); opacity: 1; } } @keyframes slideOutRight { from { transform: translateX(0) scale(1); opacity: 1; } to { transform: translateX(100%) scale(0.8); opacity: 0; } } /* 移动端Toast动画 */ @keyframes slideInDown { from { transform: translateY(-100%) scale(0.9); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } } @keyframes slideOutUp { from { transform: translateY(0) scale(1); opacity: 1; } to { transform: translateY(-100%) scale(0.9); opacity: 0; } } /* 自定义滚动条 */ .discourse-extractor-content::-webkit-scrollbar { width: 6px; } .discourse-extractor-content::-webkit-scrollbar-track { background: transparent; } .discourse-extractor-content::-webkit-scrollbar-thumb { background: linear-gradient(to bottom, #cbd5e1, #94a3b8); border-radius: 3px; } .discourse-extractor-content::-webkit-scrollbar-thumb:hover { background: linear-gradient(to bottom, #94a3b8, #64748b); } /* 自定义滚动条增强版 */ .custom-scrollbar::-webkit-scrollbar { width: 8px; height: 8px; } .custom-scrollbar::-webkit-scrollbar-track { background: linear-gradient(to bottom, #f1f5f9, #e2e8f0); border-radius: 4px; } .custom-scrollbar::-webkit-scrollbar-thumb { background: linear-gradient(to bottom, #64748b, #475569); border-radius: 4px; border: 1px solid #e2e8f0; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: linear-gradient(to bottom, #475569, #334155); } .custom-scrollbar::-webkit-scrollbar-corner { background: #f1f5f9; } /* 行高增强 */ .line-height-7 { line-height: 1.75; } /* 文本截断 */ .line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } /* 特殊效果 */ .glass-effect { background: rgba(255, 255, 255, 0.85) !important; backdrop-filter: blur(20px) !important; border: 1px solid rgba(255, 255, 255, 0.2) !important; } .gradient-border { position: relative; } .gradient-border::before { content: ''; position: absolute; inset: 0; padding: 2px; background: linear-gradient(135deg, #667eea, #764ba2, #f093fb); border-radius: inherit; mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude; } /* 按钮加载状态 */ .btn-loading { position: relative; overflow: hidden; } .btn-loading::after { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); animation: shimmer 1.5s infinite; } @keyframes shimmer { 0% { left: -100%; } 100% { left: 100%; } } /* 卡片悬停效果 */ .card-hover { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .card-hover:hover { transform: translateY(-4px); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); } /* 粒子动画效果 */ @keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } } @keyframes glow { 0%, 100% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.3); } 50% { box-shadow: 0 0 30px rgba(59, 130, 246, 0.6); } } /* 背景粒子效果 */ .particle { position: absolute; border-radius: 50%; background: radial-gradient(circle, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 70%); animation: float 3s ease-in-out infinite; } .particle:nth-child(1) { animation-delay: 0s; } .particle:nth-child(2) { animation-delay: 0.5s; } .particle:nth-child(3) { animation-delay: 1s; } .particle:nth-child(4) { animation-delay: 1.5s; } .particle:nth-child(5) { animation-delay: 2s; } /* 增强的渐变文字效果 */ .gradient-text { background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); background-size: 200% 200%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; animation: gradientShift 3s ease infinite; } @keyframes gradientShift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } /* 增强的弹出动画 */ @keyframes bounceInUp { 0% { opacity: 0; transform: translateY(100px) scale(0.3); } 50% { opacity: 1; transform: translateY(-30px) scale(1.05); } 70% { transform: translateY(10px) scale(0.9); } 100% { opacity: 1; transform: translateY(0) scale(1); } } /* 脉冲效果 */ @keyframes pulse-slow { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.8; transform: scale(1.05); } } .pulse-slow { animation: pulse-slow 2s ease-in-out infinite; } /* 移动端专用样式 */ @media (max-width: 767px) { /* 防止缩放 */ .discourse-extractor-modal { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; } /* 优化触摸滚动 */ .discourse-extractor-content { -webkit-overflow-scrolling: touch; overscroll-behavior: contain; } /* 移动端输入框优化 */ input[type="number"], input[type="text"] { font-size: 16px !important; /* 防止iOS缩放 */ -webkit-appearance: none; border-radius: 8px !important; } /* 移动端按钮优化 */ button { -webkit-tap-highlight-color: transparent; touch-action: manipulation; } /* 移动端模态框手势支持 */ .discourse-extractor-content { touch-action: pan-y; } /* 移动端文字选择优化 */ .selectable-text { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; } } /* 平板端样式 */ @media (min-width: 768px) and (max-width: 1023px) { .discourse-extractor-content { max-width: 90vw !important; width: 90vw !important; } } /* 大屏幕优化 */ @media (min-width: 1440px) { .discourse-extractor-content { max-width: 1200px !important; } } /* 横屏移动端优化 */ @media (max-width: 767px) and (orientation: landscape) { .discourse-extractor-content { max-height: 90vh !important; } } /* 高分辨率屏幕优化 */ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .discourse-extractor-btn { border: 0.5px solid rgba(255, 255, 255, 0.2) !important; } } `; document.head.appendChild(style); } /** * 创建现代化按钮 */ createButton(hasPermission, onExtractClick, onPermissionError) { if (document.querySelector('#discourse-extract-btn')) return; const button = document.createElement('button'); button.id = 'discourse-extract-btn'; button.className = 'discourse-extractor-btn'; if (hasPermission) { button.innerHTML = ` 智能提取 `; button.addEventListener('click', onExtractClick); } else { button.style.opacity = '0.6'; button.style.cursor = 'not-allowed'; button.style.background = 'linear-gradient(135deg, #64748b 0%, #475569 100%)'; button.innerHTML = ` 仅限作者 `; button.addEventListener('click', onPermissionError); } document.body.appendChild(button); } /** * 显示现代化Toast通知 */ showToast(message, type = 'success') { const existingToast = document.querySelector('.toast-notification'); if (existingToast) existingToast.remove(); const toast = document.createElement('div'); toast.className = `toast-notification ${type}`; const icon = type === 'success' ? '' : ''; toast.innerHTML = `${icon}${message}`; document.body.appendChild(toast); setTimeout(() => { if (toast.parentNode) toast.remove(); }, 3000); } /** * 显示权限错误 - 现代化设计 */ showPermissionError(currentUser, topicAuthor) { const existingError = document.querySelector('#permission-error-modal'); if (existingError) return; const modal = document.createElement('div'); modal.id = 'permission-error-modal'; modal.className = 'discourse-extractor-modal'; modal.innerHTML = `
抱歉,此功能仅限帖子作者使用
正在扫描页面...
选择您的提取偏好和参数
共 ${data.emails.length} 个邮箱地址
共 ${history.length} 条记录
开始提取评论后,记录将显示在这里
${new Date(record.timestamp).toLocaleString()}
数据提取成功,一切准备就绪!
💡 点击任意邮箱地址即可复制
📊 共显示 ${data.comments.length} 条评论内容