您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
当前为
// ==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 // ==/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/[email protected]/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 = ` <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/> <path d="M14 2v6h6"/> <path d="M16 13H8"/> <path d="M16 17H8"/> <path d="M10 9H8"/> </svg> <span>智能提取</span> `; 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 = ` <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> <path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6z"/> </svg> <span>仅限作者</span> `; 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' ? '<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>' : '<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24"><path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'; toast.innerHTML = `${icon}<span>${message}</span>`; 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 = ` <div class="discourse-extractor-content max-w-md"> <button class="discourse-extractor-close">×</button> <div class="p-8 text-center"> <!-- 错误图标 --> <div class="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-red-100 to-red-200 rounded-full flex items-center justify-center"> <svg class="w-10 h-10 text-red-500" fill="currentColor" viewBox="0 0 24 24"> <path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6z"/> </svg> </div> <!-- 标题 --> <h2 class="text-2xl font-bold text-gray-800 mb-4">权限不足</h2> <!-- 描述 --> <div class="text-gray-600 mb-6 space-y-3"> <p class="leading-relaxed">抱歉,此功能仅限帖子作者使用</p> <div class="bg-gray-50 rounded-lg p-4 text-sm"> <div class="space-y-2"> <div class="flex justify-between items-center"> <span class="font-medium text-gray-700">当前用户:</span> <span class="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium"> ${currentUser?.username || '未登录(不可用)'} </span> </div> <div class="flex justify-between items-center"> <span class="font-medium text-gray-700">帖子作者:</span> <span class="px-3 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium"> ${topicAuthor?.username || '未知'} </span> </div> </div> </div> </div> <!-- 按钮 --> <button class="w-full bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white font-semibold py-3 px-6 rounded-xl transition-all duration-300 transform hover:scale-105 shadow-lg"> 我知道了 </button> </div> </div> `; const closeModal = () => { modal.remove(); }; modal.querySelector('.discourse-extractor-close').addEventListener('click', closeModal); modal.querySelector('button:last-child').addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); document.body.appendChild(modal); } /** * 显示加载进度 */ showLoadingProgress() { const modal = document.createElement('div'); modal.id = 'loading-progress-modal'; modal.className = 'discourse-extractor-modal'; modal.innerHTML = ` <div class="discourse-extractor-content max-w-md glass-effect"> <div class="p-8 text-center"> <div class="w-16 h-16 mx-auto mb-6 bg-gradient-to-br from-blue-100 to-indigo-200 rounded-full flex items-center justify-center"> <div class="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div> </div> <h3 class="text-xl font-bold text-gray-800 mb-4">正在加载评论</h3> <div class="text-gray-600 mb-6"> <p id="progress-text">正在扫描页面...</p> <div class="w-full bg-gray-200 rounded-full h-2 mt-4"> <div id="progress-bar" class="bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div> </div> </div> </div> </div> `; document.body.appendChild(modal); return modal; } /** * 更新进度 */ updateProgress(current, total, attempts) { const progressText = document.getElementById('progress-text'); const progressBar = document.getElementById('progress-bar'); if (progressText && progressBar) { progressText.textContent = `已加载 ${current}/${total} 条评论 (第 ${attempts} 次尝试)`; const percentage = total > 0 ? (current / total) * 100 : 0; progressBar.style.width = `${Math.min(percentage, 100)}%`; } } /** * 关闭模态框 */ closeModal(modal) { if (modal && modal.parentNode) { modal.remove(); } } /** * 显示配置模态框 - 现代化TailwindCSS设计 */ showConfigModal(onConfirm) { const existingModal = document.querySelector('#discourse-config-modal'); if (existingModal) existingModal.remove(); const modal = document.createElement('div'); modal.id = 'discourse-config-modal'; modal.className = 'discourse-extractor-modal'; modal.innerHTML = ` <div class="discourse-extractor-content max-w-lg md:max-w-2xl lg:max-w-4xl"> <button class="discourse-extractor-close">×</button> <!-- Header with beautiful gradient - 响应式优化 --> <div class="bg-gradient-to-r from-indigo-500 via-purple-600 to-pink-500 p-4 md:p-8 rounded-t-xl md:rounded-t-2xl text-white relative overflow-hidden"> <div class="absolute inset-0 bg-white bg-opacity-10 backdrop-blur-sm"></div> <div class="relative z-10 text-center"> <div class="w-16 h-16 md:w-20 md:h-20 mx-auto mb-3 md:mb-4 bg-white bg-opacity-20 rounded-full flex items-center justify-center backdrop-blur-sm"> <svg class="w-8 h-8 md:w-10 md:h-10 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> </svg> </div> <h2 class="text-2xl md:text-3xl font-bold mb-2">智能提取配置</h2> <p class="text-white text-opacity-90 text-sm md:text-base">选择您的提取偏好和参数</p> </div> </div> <!-- Content - 响应式间距 --> <div class="p-4 md:p-8"> <form id="extract-config-form" class="space-y-6 md:space-y-8"> <!-- 提取模式选择 --> <div class="space-y-3 md:space-y-4"> <h3 class="text-base md:text-lg font-semibold text-gray-800 mb-3 md:mb-4 flex items-center"> <svg class="w-4 h-4 md:w-5 md:h-5 mr-2 text-indigo-500" fill="currentColor" viewBox="0 0 24 24"> <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> 提取模式 </h3> <div class="space-y-2 md:space-y-3"> <!-- 全部评论模式 - 移动端优化 --> <label class="group relative flex items-center p-3 md:p-4 border-2 border-indigo-500 bg-gradient-to-r from-indigo-50 to-blue-50 rounded-lg md:rounded-xl cursor-pointer hover:shadow-lg transition-all duration-300 transform hover:scale-102 min-h-[60px] md:min-h-auto"> <input type="radio" name="mode" value="all" checked class="sr-only"> <div class="w-5 h-5 border-2 border-indigo-500 rounded-full mr-3 md:mr-4 flex items-center justify-center relative flex-shrink-0"> <div class="w-3 h-3 bg-indigo-500 rounded-full opacity-100 transform scale-100 transition-all duration-200"></div> </div> <div class="flex-1 min-w-0"> <div class="font-semibold text-gray-800 group-hover:text-indigo-700 transition-colors text-sm md:text-base">全部评论</div> <div class="text-xs md:text-sm text-gray-600 mt-1">提取页面上所有可见的评论内容</div> </div> <svg class="w-5 h-5 md:w-6 md:h-6 text-indigo-500 opacity-100 transition-all duration-200 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"> <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> </label> <!-- 楼层范围模式 - 移动端优化 --> <label class="group relative flex items-center p-3 md:p-4 border-2 border-gray-200 bg-white rounded-lg md:rounded-xl cursor-pointer hover:border-orange-400 hover:bg-gradient-to-r hover:from-orange-50 hover:to-amber-50 hover:shadow-lg transition-all duration-300 transform hover:scale-102 min-h-[60px] md:min-h-auto"> <input type="radio" name="mode" value="range" class="sr-only"> <div class="w-5 h-5 border-2 border-orange-500 rounded-full mr-3 md:mr-4 flex items-center justify-center flex-shrink-0"> <div class="w-3 h-3 bg-orange-500 rounded-full opacity-0 transform scale-0 transition-all duration-200"></div> </div> <div class="flex-1 min-w-0"> <div class="font-semibold text-gray-800 group-hover:text-orange-700 transition-colors text-sm md:text-base">楼层范围</div> <div class="text-xs md:text-sm text-gray-600 mt-1">指定起始楼层和结束楼层进行精确提取</div> </div> <svg class="w-5 h-5 md:w-6 md:h-6 text-orange-500 opacity-0 transition-all duration-200 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"> <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> </label> <!-- 随机提取模式 - 移动端优化 --> <label class="group relative flex items-center p-3 md:p-4 border-2 border-gray-200 bg-white rounded-lg md:rounded-xl cursor-pointer hover:border-green-400 hover:bg-gradient-to-r hover:from-green-50 hover:to-emerald-50 hover:shadow-lg transition-all duration-300 transform hover:scale-102 min-h-[60px] md:min-h-auto"> <input type="radio" name="mode" value="random" class="sr-only"> <div class="w-5 h-5 border-2 border-green-500 rounded-full mr-3 md:mr-4 flex items-center justify-center flex-shrink-0"> <div class="w-3 h-3 bg-green-500 rounded-full opacity-0 transform scale-0 transition-all duration-200"></div> </div> <div class="flex-1 min-w-0"> <div class="font-semibold text-gray-800 group-hover:text-green-700 transition-colors text-sm md:text-base">随机提取</div> <div class="text-xs md:text-sm text-gray-600 mt-1">从所有评论中随机选择指定数量</div> </div> <svg class="w-5 h-5 md:w-6 md:h-6 text-green-500 opacity-0 transition-all duration-200 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"> <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> </label> </div> </div> <!-- 楼层范围配置 - 移动端优化 --> <div id="range-config" class="hidden opacity-0 transform translate-y-4 transition-all duration-500"> <div class="bg-gradient-to-br from-orange-50 to-amber-100 rounded-lg md:rounded-xl p-4 md:p-6 border-2 border-orange-200 card-hover"> <h4 class="font-semibold text-orange-800 mb-3 md:mb-4 flex items-center text-sm md:text-base"> <svg class="w-4 h-4 md:w-5 md:h-5 mr-2" fill="currentColor" viewBox="0 0 24 24"> <path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/> </svg> 楼层范围设置 </h4> <div class="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4"> <div> <label class="block text-xs md:text-sm font-medium text-orange-700 mb-2">起始楼层</label> <input type="number" id="start-floor" min="1" value="1" class="w-full px-3 md:px-4 py-2 md:py-3 border-2 border-orange-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-all duration-200 bg-white text-sm md:text-base min-h-[44px]"> </div> <div> <label class="block text-xs md:text-sm font-medium text-orange-700 mb-2">结束楼层</label> <input type="number" id="end-floor" min="1" placeholder="默认到最后" class="w-full px-3 md:px-4 py-2 md:py-3 border-2 border-orange-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-all duration-200 bg-white text-sm md:text-base min-h-[44px]"> </div> </div> </div> </div> <!-- 随机提取配置 - 移动端优化 --> <div id="random-config" class="hidden opacity-0 transform translate-y-4 transition-all duration-500"> <div class="bg-gradient-to-br from-green-50 to-emerald-100 rounded-lg md:rounded-xl p-4 md:p-6 border-2 border-green-200 card-hover"> <h4 class="font-semibold text-green-800 mb-3 md:mb-4 flex items-center text-sm md:text-base"> <svg class="w-4 h-4 md:w-5 md:h-5 mr-2" fill="currentColor" viewBox="0 0 24 24"> <path d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547L3.71 16.292a1 1 0 001.4 1.43l.93-.93c.22-.22.546-.31.858-.243l2.387.477a8 8 0 005.147-.689l.318-.158a4 4 0 012.573-.345l2.387.477c.312.067.638-.023.858-.243l.93-.93a1 1 0 001.4-1.43l-.534-.535z"/> </svg> 随机提取设置 </h4> <div> <label class="block text-xs md:text-sm font-medium text-green-700 mb-2">提取数量</label> <input type="number" id="random-count" min="1" value="10" class="w-full px-3 md:px-4 py-2 md:py-3 border-2 border-green-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white text-sm md:text-base min-h-[44px]"> <p class="text-xs md:text-sm text-green-600 mt-2">从所有评论中随机选择指定数量</p> </div> </div> </div> <!-- 邮箱提取选项 - 移动端优化 --> <div class="bg-gradient-to-br from-blue-50 to-indigo-100 rounded-lg md:rounded-xl p-4 md:p-6 border-2 border-blue-200 card-hover"> <label class="flex items-center cursor-pointer group min-h-[60px] md:min-h-auto"> <input type="checkbox" id="extract-emails" checked class="sr-only"> <div class="relative mr-3 md:mr-4 flex-shrink-0"> <div class="w-12 h-6 bg-blue-500 rounded-full shadow-inner transition-all duration-300"></div> <div class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full shadow-md transition-transform duration-300 transform translate-x-6"></div> </div> <div class="flex-1 min-w-0"> <div class="font-semibold text-blue-800 group-hover:text-blue-900 transition-colors text-sm md:text-base">同时提取邮箱地址</div> <div class="text-xs md:text-sm text-blue-600 mt-1">自动识别并提取评论中的邮箱地址</div> </div> </label> </div> <!-- 按钮组 - 移动端优化 --> <div class="flex flex-col md:flex-row gap-3 md:gap-4 pt-4 md:pt-6"> <button type="button" id="cancel-btn" class="flex-1 px-4 md:px-6 py-3 md:py-4 bg-gradient-to-r from-gray-100 to-gray-200 hover:from-gray-200 hover:to-gray-300 text-gray-700 font-semibold rounded-lg md:rounded-xl transition-all duration-300 transform hover:scale-105 shadow-md hover:shadow-lg text-sm md:text-base min-h-[48px] md:min-h-auto"> 取消 </button> <button type="submit" class="flex-1 px-4 md:px-6 py-3 md:py-4 bg-gradient-to-r from-indigo-500 via-purple-600 to-pink-500 hover:from-indigo-600 hover:via-purple-700 hover:to-pink-600 text-white font-semibold rounded-lg md:rounded-xl transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-xl btn-loading text-sm md:text-base min-h-[48px] md:min-h-auto"> 开始提取 </button> </div> </form> </div> </div> `; this.attachConfigEventListeners(modal, onConfirm); document.body.appendChild(modal); return modal; } /** * 添加配置弹窗的事件监听器 */ attachConfigEventListeners(modal, onConfirm) { const form = modal.querySelector('#extract-config-form'); const modeRadios = modal.querySelectorAll('input[name="mode"]'); const rangeConfig = modal.querySelector('#range-config'); const randomConfig = modal.querySelector('#random-config'); const emailToggle = modal.querySelector('#extract-emails'); // 单选按钮样式切换 modeRadios.forEach(radio => { const label = radio.closest('label'); const radioCircle = label.querySelector('div:first-child div'); const checkIcon = label.querySelector('svg:last-child'); radio.addEventListener('change', () => { // 重置所有选项 modeRadios.forEach(r => { const rLabel = r.closest('label'); const rCircle = rLabel.querySelector('div:first-child div'); const rIcon = rLabel.querySelector('svg:last-child'); rCircle.style.opacity = '0'; rCircle.style.transform = 'scale(0)'; rIcon.style.opacity = '0'; rLabel.classList.remove('border-indigo-500', 'border-orange-400', 'border-green-400'); rLabel.classList.add('border-gray-200'); }); // 激活当前选项 radioCircle.style.opacity = '1'; radioCircle.style.transform = 'scale(1)'; checkIcon.style.opacity = '1'; // 根据模式设置不同的边框颜色和背景 if (radio.value === 'all') { label.classList.remove('border-gray-200'); label.classList.add('border-indigo-500', 'bg-gradient-to-r', 'from-indigo-50', 'to-blue-50'); } else if (radio.value === 'range') { label.classList.remove('border-gray-200'); label.classList.add('border-orange-400', 'bg-gradient-to-r', 'from-orange-50', 'to-amber-50'); } else if (radio.value === 'random') { label.classList.remove('border-gray-200'); label.classList.add('border-green-400', 'bg-gradient-to-r', 'from-green-50', 'to-emerald-50'); } // 显示/隐藏配置面板 if (radio.value === 'range') { randomConfig.classList.add('hidden', 'opacity-0', 'translate-y-4'); rangeConfig.classList.remove('hidden'); setTimeout(() => { rangeConfig.classList.remove('opacity-0', 'translate-y-4'); }, 10); } else if (radio.value === 'random') { rangeConfig.classList.add('hidden', 'opacity-0', 'translate-y-4'); randomConfig.classList.remove('hidden'); setTimeout(() => { randomConfig.classList.remove('opacity-0', 'translate-y-4'); }, 10); } else { rangeConfig.classList.add('hidden', 'opacity-0', 'translate-y-4'); randomConfig.classList.add('hidden', 'opacity-0', 'translate-y-4'); } }); }); // 邮箱开关切换动画 emailToggle.addEventListener('change', () => { const toggleContainer = emailToggle.closest('label').querySelector('div'); const toggleSwitch = toggleContainer.querySelector('div:last-child'); const toggleBg = toggleContainer.querySelector('div:first-child'); if (emailToggle.checked) { toggleSwitch.classList.remove('translate-x-0'); toggleSwitch.classList.add('translate-x-6'); toggleBg.classList.remove('bg-gray-300'); toggleBg.classList.add('bg-blue-500'); } else { toggleSwitch.classList.remove('translate-x-6'); toggleSwitch.classList.add('translate-x-0'); toggleBg.classList.remove('bg-blue-500'); toggleBg.classList.add('bg-gray-300'); } }); // 取消按钮 modal.querySelector('#cancel-btn').addEventListener('click', () => { modal.remove(); }); // 关闭按钮 modal.querySelector('.discourse-extractor-close').addEventListener('click', () => { modal.remove(); }); // 点击背景关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); // 移动端手势支持 this.addMobileGestureSupport(modal); // 表单提交 form.addEventListener('submit', (e) => { e.preventDefault(); const submitBtn = form.querySelector('button[type="submit"]'); submitBtn.classList.add('btn-loading'); submitBtn.innerHTML = ` <div class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> 正在配置... `; setTimeout(() => { const formData = new FormData(form); const config = { mode: formData.get('mode'), extractEmails: emailToggle.checked, startFloor: parseInt(modal.querySelector('#start-floor').value) || 1, endFloor: parseInt(modal.querySelector('#end-floor').value) || null, randomCount: parseInt(modal.querySelector('#random-count').value) || 10 }; modal.remove(); onConfirm(config); }, 800); }); } /** * 获取模式文本 */ getModeText(mode) { const modeMap = { 'all': '全部评论', 'range': '楼层范围', 'random': '随机提取' }; return modeMap[mode] || '未知模式'; } /** * 下载JSON文件 */ downloadJSON(data) { try { const jsonData = JSON.stringify(data, null, 2); const blob = new Blob([jsonData], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const filename = `discourse-comments-${data.extractConfig?.mode || 'all'}-${timestamp}.json`; const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); this.showToast('JSON文件下载成功'); } catch (error) { console.error('下载JSON失败:', error); this.showToast('下载失败,请重试', 'error'); } } /** * 下载CSV文件 */ downloadCSV(data) { try { const headers = ['楼层', '作者', '时间', '内容', '邮箱']; const csvContent = [ '\ufeff', // BOM for Excel Chinese support headers.join(','), ...data.comments.map(comment => { const content = `"${comment.content.replace(/"/g, '""').replace(/\n/g, ' ')}"`; const emails = comment.emails ? comment.emails.join(';') : ''; return [ comment.floor, `"${comment.author}"`, `"${comment.time}"`, content, `"${emails}"` ].join(','); }) ].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const filename = `discourse-comments-${data.extractConfig?.mode || 'all'}-${timestamp}.csv`; const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); this.showToast('CSV文件下载成功'); } catch (error) { console.error('下载CSV失败:', error); this.showToast('下载失败,请重试', 'error'); } } /** * 复制完整数据 */ async copyData(data) { try { const textData = [ `=== Discourse 评论提取结果 ===`, `提取时间: ${data.extractTime}`, `页面标题: ${data.pageTitle}`, `页面链接: ${data.pageUrl}`, `提取模式: ${this.getModeText(data.extractConfig?.mode)}`, `评论总数: ${data.comments.length}`, `邮箱总数: ${data.emails.length}`, '', '=== 评论详情 ===', ...data.comments.map(comment => [ `楼层 #${comment.floor} - ${comment.author} - ${comment.time}`, comment.content, comment.emails && comment.emails.length > 0 ? `邮箱: ${comment.emails.join(', ')}` : '', '---' ].filter(line => line).join('\n')) ].join('\n'); await navigator.clipboard.writeText(textData); this.showToast('数据已复制到剪贴板'); } catch (error) { console.error('复制数据失败:', error); this.showToast('复制失败,请重试', 'error'); } } /** * 复制单个邮箱地址 */ async copySingleEmail(email) { try { await navigator.clipboard.writeText(email); this.showToast(`✨ 已复制: ${email}`); } catch (error) { console.error('复制邮箱失败:', error); this.showToast('复制失败,请重试', 'error'); } } /** * 显示邮箱复制选项模态框 */ showEmailCopyOptions(data) { if (data.emails.length === 0) { this.showToast('没有邮箱地址可复制', 'error'); return; } const existingModal = document.querySelector('#email-copy-options-modal'); if (existingModal) existingModal.remove(); const modal = document.createElement('div'); modal.id = 'email-copy-options-modal'; modal.className = 'discourse-extractor-modal'; modal.innerHTML = ` <div class="discourse-extractor-content max-w-md"> <button class="discourse-extractor-close">×</button> <div class="p-6 md:p-8"> <!-- Header --> <div class="text-center mb-6"> <div class="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-orange-400 to-amber-500 rounded-full flex items-center justify-center"> <svg class="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M3 8l7.89 7.89a1 1 0 001.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> </div> <h2 class="text-2xl font-bold text-gray-800 mb-2">选择复制格式</h2> <p class="text-gray-600">共 ${data.emails.length} 个邮箱地址</p> </div> <!-- Separator Options --> <div class="space-y-3 mb-6"> <button class="copy-option-btn w-full p-4 bg-gradient-to-r from-blue-50 to-indigo-50 hover:from-blue-100 hover:to-indigo-100 border-2 border-blue-200 hover:border-blue-300 rounded-xl transition-all duration-300 text-left group" data-separator=","> <div class="flex items-center justify-between"> <div> <div class="font-semibold text-blue-800 mb-1">逗号分隔</div> <div class="text-sm text-blue-600">[email protected], [email protected]</div> </div> <svg class="w-5 h-5 text-blue-500 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 24 24"> <path d="M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11v14z"/> </svg> </div> </button> <button class="copy-option-btn w-full p-4 bg-gradient-to-r from-green-50 to-emerald-50 hover:from-green-100 hover:to-emerald-100 border-2 border-green-200 hover:border-green-300 rounded-xl transition-all duration-300 text-left group" data-separator=" "> <div class="flex items-center justify-between"> <div> <div class="font-semibold text-green-800 mb-1">空格分隔</div> <div class="text-sm text-green-600">[email protected] [email protected]</div> </div> <svg class="w-5 h-5 text-green-500 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 24 24"> <path d="M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11v14z"/> </svg> </div> </button> <button class="copy-option-btn w-full p-4 bg-gradient-to-r from-purple-50 to-violet-50 hover:from-purple-100 hover:to-violet-100 border-2 border-purple-200 hover:border-purple-300 rounded-xl transition-all duration-300 text-left group" data-separator=";"> <div class="flex items-center justify-between"> <div> <div class="font-semibold text-purple-800 mb-1">分号分隔</div> <div class="text-sm text-purple-600">[email protected]; [email protected]</div> </div> <svg class="w-5 h-5 text-purple-500 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 24 24"> <path d="M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11v14z"/> </svg> </div> </button> <button class="copy-option-btn w-full p-4 bg-gradient-to-r from-amber-50 to-orange-50 hover:from-amber-100 hover:to-orange-100 border-2 border-amber-200 hover:border-amber-300 rounded-xl transition-all duration-300 text-left group" data-separator="\\n"> <div class="flex items-center justify-between"> <div> <div class="font-semibold text-amber-800 mb-1">换行分隔</div> <div class="text-sm text-amber-600">每行一个邮箱地址</div> </div> <svg class="w-5 h-5 text-amber-500 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 24 24"> <path d="M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11v14z"/> </svg> </div> </button> </div> <!-- Cancel Button --> <button class="cancel-btn w-full p-3 bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium rounded-lg transition-all duration-300"> 取消 </button> </div> </div> `; const closeModal = () => { modal.remove(); }; // Event listeners modal.querySelector('.discourse-extractor-close').addEventListener('click', closeModal); modal.querySelector('.cancel-btn').addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); // Copy option buttons modal.querySelectorAll('.copy-option-btn').forEach(btn => { btn.addEventListener('click', async () => { const separator = btn.getAttribute('data-separator'); const actualSeparator = separator === '\\n' ? '\n' : separator; await this.copyEmailsWithSeparator(data.emails, actualSeparator); closeModal(); }); }); document.body.appendChild(modal); } /** * 复制邮箱地址(带分隔符选择) */ async copyEmailsWithSeparator(emails, separator) { try { const emailText = emails.join(separator); await navigator.clipboard.writeText(emailText); const separatorName = { ',': '逗号', ' ': '空格', ';': '分号', '\n': '换行' }[separator] || '自定义'; this.showToast(`已复制 ${emails.length} 个邮箱地址 (${separatorName}分隔)`); } catch (error) { console.error('复制邮箱失败:', error); this.showToast('复制失败,请重试', 'error'); } } /** * 复制邮箱地址(兼容旧方法) */ async copyEmails(data) { this.showEmailCopyOptions(data); } /** * 显示历史记录 - 现代化设计 */ showHistory() { const existingModal = document.querySelector('#discourse-history-modal'); if (existingModal) existingModal.remove(); const storageManager = new StorageManager('discourse_extractor_history', 100); const history = storageManager.getHistory(); const modal = document.createElement('div'); modal.id = 'discourse-history-modal'; modal.className = 'discourse-extractor-modal'; modal.innerHTML = ` <div class="discourse-extractor-content max-w-5xl"> <button class="discourse-extractor-close">×</button> <!-- Header --> <div class="bg-gradient-to-r from-amber-500 via-orange-500 to-red-500 p-8 rounded-t-xl text-white relative overflow-hidden"> <div class="absolute inset-0 bg-white bg-opacity-10 backdrop-blur-sm"></div> <div class="relative z-10 text-center"> <div class="w-20 h-20 mx-auto mb-4 bg-white bg-opacity-20 rounded-full flex items-center justify-center backdrop-blur-sm"> <svg class="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> </div> <h2 class="text-3xl font-bold mb-2">📚 提取历史</h2> <p class="text-white text-opacity-90">共 ${history.length} 条记录</p> </div> </div> <!-- Content --> <div class="p-8 max-h-[60vh] overflow-y-auto"> ${history.length === 0 ? ` <div class="text-center py-16"> <div class="w-24 h-24 mx-auto mb-6 bg-gradient-to-br from-gray-100 to-gray-200 rounded-full flex items-center justify-center"> <svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 24 24"> <path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> </svg> </div> <h3 class="text-2xl font-semibold text-gray-700 mb-3">暂无历史记录</h3> <p class="text-gray-500 text-lg">开始提取评论后,记录将显示在这里</p> </div> ` : ` <div class="grid gap-6"> ${history.map((record, index) => ` <div class="bg-gradient-to-r from-white to-gray-50 border-2 border-gray-200 rounded-xl p-6 card-hover shadow-md"> <div class="flex justify-between items-start mb-4"> <div class="flex-1"> <h4 class="text-xl font-semibold text-gray-800 mb-2 line-clamp-2">${record.pageTitle || '未知页面'}</h4> <p class="text-sm text-gray-500 mb-3">${new Date(record.timestamp).toLocaleString()}</p> <div class="flex flex-wrap gap-2"> <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"> ${record.totalComments || 0} 评论 </span> <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800"> ${record.emailCount || 0} 邮箱 </span> <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800"> ${this.getModeText(record.mode)} </span> </div> </div> </div> <div class="flex gap-3"> <button onclick="window.historyActions.reloadRecord(${record.id})" class="flex-1 px-4 py-3 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-medium rounded-lg transition-all duration-300 transform hover:scale-105 shadow-md"> 重新查看 </button> <button onclick="window.historyActions.copyRecord(${record.id})" class="flex-1 px-4 py-3 bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white font-medium rounded-lg transition-all duration-300 transform hover:scale-105 shadow-md"> 复制数据 </button> <a href="${record.url || '#'}" target="_blank" class="flex-1 px-4 py-3 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-700 text-white font-medium rounded-lg transition-all duration-300 transform hover:scale-105 shadow-md text-center"> 查看原帖 </a> </div> </div> `).join('')} </div> <div class="mt-8 pt-6 border-t-2 border-gray-200 text-center"> <button onclick="window.historyActions.clearHistory()" class="px-8 py-4 bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white font-semibold rounded-xl transition-all duration-300 transform hover:scale-105 shadow-lg"> 清空历史记录 </button> </div> `} </div> </div> `; // 添加全局操作函数 window.historyActions = { reloadRecord: (recordId) => { const record = history.find(r => r.id === recordId); if (record) { modal.remove(); this.showResults(record); } }, copyRecord: async (recordId) => { const record = history.find(r => r.id === recordId); if (record) { await this.copyData(record); } }, clearHistory: () => { if (confirm('确定要清空所有历史记录吗?此操作不可恢复。')) { storageManager.clearHistory(); modal.remove(); this.showToast('历史记录已清空'); } } }; // 关闭功能 const closeModal = () => { delete window.historyActions; modal.remove(); }; modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); modal.querySelector('.discourse-extractor-close').addEventListener('click', closeModal); // 添加移动端手势支持 this.addMobileGestureSupport(modal); document.body.appendChild(modal); } /** * 显示结果 - 现代化TailwindCSS设计 */ showResults(data) { const existingModal = document.querySelector('#discourse-results-modal'); if (existingModal) existingModal.remove(); const modal = document.createElement('div'); modal.id = 'discourse-results-modal'; modal.className = 'discourse-extractor-modal'; modal.innerHTML = ` <div class="discourse-extractor-content max-w-full md:max-w-6xl"> <button class="discourse-extractor-close">×</button> <!-- Header with enhanced gradient and particles effect - 移动端优化 --> <div class="bg-gradient-to-br from-emerald-400 via-teal-500 to-cyan-600 p-4 md:p-8 rounded-t-xl text-white relative overflow-hidden"> <!-- Background decorative elements --> <div class="absolute inset-0 opacity-10"> <div class="absolute top-4 left-8 w-32 h-32 bg-white rounded-full blur-2xl animate-pulse"></div> <div class="absolute top-16 right-12 w-24 h-24 bg-white rounded-full blur-xl animate-pulse delay-1000"></div> <div class="absolute bottom-8 left-1/3 w-20 h-20 bg-white rounded-full blur-lg animate-pulse delay-500"></div> </div> <!-- Glassmorphism overlay --> <div class="absolute inset-0 bg-gradient-to-r from-white/10 to-transparent backdrop-blur-sm"></div> <div class="relative z-10"> <div class="flex flex-col md:flex-row items-center justify-between mb-6 md:mb-8 space-y-4 md:space-y-0"> <div class="flex items-center space-x-3 md:space-x-4 text-center md:text-left"> <!-- Enhanced success icon with animation - 移动端优化 --> <div class="w-12 h-12 md:w-16 md:h-16 bg-white/20 rounded-full flex items-center justify-center backdrop-blur-sm border border-white/30 animate-bounce"> <svg class="w-6 h-6 md:w-8 md:h-8 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> </div> <div> <h2 class="text-2xl md:text-4xl font-bold mb-1 md:mb-2 bg-gradient-to-r from-white to-emerald-100 bg-clip-text text-transparent"> 🎉 提取完成 </h2> <p class="text-emerald-100 text-sm md:text-lg font-medium">数据提取成功,一切准备就绪!</p> </div> </div> <!-- Floating stats badge - 移动端优化 --> <div class="text-center md:text-right"> <div class="bg-white/20 backdrop-blur-sm rounded-xl md:rounded-2xl p-3 md:p-4 border border-white/30"> <div class="text-2xl md:text-3xl font-bold mb-1">${data.comments.length}</div> <div class="text-emerald-100 text-xs md:text-sm font-medium">条精彩评论</div> </div> </div> </div> <!-- Enhanced statistics cards with hover effects - 移动端优化 --> <div class="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-6"> <div class="group bg-white/15 backdrop-blur-md rounded-xl md:rounded-2xl p-4 md:p-6 text-center border border-white/20 hover:bg-white/25 transition-all duration-500 hover:scale-105 hover:shadow-2xl cursor-pointer"> <div class="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 md:mb-3 bg-white/20 rounded-full flex items-center justify-center group-hover:rotate-12 transition-transform duration-300"> <svg class="w-5 h-5 md:w-6 md:h-6 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M8 10h8M8 14h6M6 4h12a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2z"/> </svg> </div> <div class="text-2xl md:text-3xl font-bold mb-1 md:mb-2 group-hover:text-yellow-200 transition-colors">${data.comments.length}</div> <div class="text-emerald-100 text-xs md:text-sm font-medium">评论总数</div> <div class="text-xs text-emerald-200 mt-1 opacity-75">已成功解析</div> </div> <div class="group bg-white/15 backdrop-blur-md rounded-xl md:rounded-2xl p-4 md:p-6 text-center border border-white/20 hover:bg-white/25 transition-all duration-500 hover:scale-105 hover:shadow-2xl cursor-pointer"> <div class="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 md:mb-3 bg-white/20 rounded-full flex items-center justify-center group-hover:rotate-12 transition-transform duration-300"> <svg class="w-5 h-5 md:w-6 md:h-6 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M3 8l7.89 7.89a1 1 0 001.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> </div> <div class="text-2xl md:text-3xl font-bold mb-1 md:mb-2 group-hover:text-yellow-200 transition-colors">${data.emails.length}</div> <div class="text-emerald-100 text-xs md:text-sm font-medium">邮箱地址</div> <div class="text-xs text-emerald-200 mt-1 opacity-75">自动识别</div> </div> <div class="group bg-white/15 backdrop-blur-md rounded-xl md:rounded-2xl p-4 md:p-6 text-center border border-white/20 hover:bg-white/25 transition-all duration-500 hover:scale-105 hover:shadow-2xl cursor-pointer"> <div class="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 md:mb-3 bg-white/20 rounded-full flex items-center justify-center group-hover:rotate-12 transition-transform duration-300"> <svg class="w-5 h-5 md:w-6 md:h-6 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> </svg> </div> <div class="text-lg md:text-2xl font-bold mb-1 md:mb-2 group-hover:text-yellow-200 transition-colors">${this.getModeText(data.extractConfig?.mode)}</div> <div class="text-emerald-100 text-xs md:text-sm font-medium">提取模式</div> <div class="text-xs text-emerald-200 mt-1 opacity-75">智能算法</div> </div> </div> </div> <!-- Floating particles animation --> <div class="absolute inset-0 overflow-hidden pointer-events-none"> <div class="absolute -top-2 -left-2 w-4 h-4 bg-white/30 rounded-full animate-ping"></div> <div class="absolute top-1/2 -right-2 w-3 h-3 bg-white/20 rounded-full animate-ping delay-700"></div> <div class="absolute -bottom-2 left-1/4 w-2 h-2 bg-white/25 rounded-full animate-ping delay-1000"></div> </div> </div> <!-- Content with enhanced design - 移动端优化 --> <div class="p-4 md:p-8 bg-gradient-to-b from-gray-50 to-white"> <!-- Email tags section with improved design - 移动端优化 --> ${data.emails.length > 0 ? ` <div class="mb-6 md:mb-10"> <div class="flex flex-col md:flex-row md:items-center justify-between mb-4 md:mb-6 space-y-2 md:space-y-0"> <h3 class="text-lg md:text-2xl font-bold text-gray-800 flex items-center"> <div class="w-8 h-8 md:w-10 md:h-10 bg-gradient-to-br from-green-400 to-emerald-500 rounded-lg md:rounded-xl flex items-center justify-center mr-2 md:mr-3"> <svg class="w-4 h-4 md:w-5 md:h-5 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M3 8l7.89 7.89a1 1 0 001.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> </div> 邮箱地址收集 </h3> <div class="bg-gradient-to-r from-green-500 to-emerald-600 text-white px-3 md:px-4 py-1 md:py-2 rounded-full text-xs md:text-sm font-semibold shadow-lg"> ${data.emails.length} 个地址 </div> </div> <div class="bg-gradient-to-br from-green-50 via-emerald-50 to-teal-50 rounded-xl md:rounded-2xl p-4 md:p-6 border-2 border-green-100 shadow-inner"> <div class="flex flex-wrap gap-2 md:gap-3 max-h-32 md:max-h-40 overflow-y-auto custom-scrollbar"> ${data.emails.map((email, index) => ` <span class="email-tag group inline-flex items-center px-3 md:px-4 py-1 md:py-2 bg-gradient-to-r from-green-100 to-emerald-100 hover:from-green-200 hover:to-emerald-200 text-green-800 text-xs md:text-sm rounded-full border border-green-200 hover:border-green-300 transition-all duration-300 cursor-pointer hover:shadow-lg hover:scale-105 min-h-[32px] md:min-h-auto" data-email="${email}" data-index="${index}"> <svg class="w-3 h-3 md:w-4 md:h-4 mr-1 md:mr-2 group-hover:animate-spin" fill="currentColor" viewBox="0 0 24 24"> <path d="M3 8l7.89 7.89a1 1 0 001.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> ${email} </span> `).join('')} </div> <div class="mt-3 md:mt-4 text-center"> <p class="text-green-700 text-xs md:text-sm font-medium">💡 点击任意邮箱地址即可复制</p> </div> </div> </div> ` : ''} <!-- Enhanced action buttons with better visual hierarchy - 移动端优化 --> <div class="mb-6 md:mb-10"> <h3 class="text-lg md:text-2xl font-bold text-gray-800 mb-4 md:mb-6 flex items-center"> <div class="w-8 h-8 md:w-10 md:h-10 bg-gradient-to-br from-blue-400 to-indigo-500 rounded-lg md:rounded-xl flex items-center justify-center mr-2 md:mr-3"> <svg class="w-4 h-4 md:w-5 md:h-5 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"/> </svg> </div> 数据操作中心 </h3> <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3 md:gap-4"> <button id="download-json" class="group relative overflow-hidden bg-gradient-to-br from-blue-50 to-indigo-100 hover:from-blue-100 hover:to-indigo-200 border-2 border-blue-200 hover:border-blue-300 rounded-xl md:rounded-2xl p-3 md:p-6 transition-all duration-500 transform hover:scale-105 hover:shadow-xl min-h-[80px] md:min-h-auto"> <div class="absolute inset-0 bg-gradient-to-r from-blue-600/0 via-blue-600/10 to-blue-600/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div> <div class="relative z-10 flex flex-col items-center"> <div class="w-10 h-10 md:w-14 md:h-14 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-2 md:mb-4 group-hover:rotate-12 transition-transform duration-300 shadow-lg"> <svg class="w-5 h-5 md:w-7 md:h-7 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/> <path d="M14 2v6h6"/> <path d="M16 13H8"/> <path d="M16 17H8"/> <path d="M10 9H8"/> </svg> </div> <span class="font-bold text-blue-800 text-sm md:text-lg group-hover:text-blue-900">下载 JSON</span> <span class="text-blue-600 text-xs mt-1 hidden md:block">结构化数据</span> </div> </button> <button id="download-csv" class="group relative overflow-hidden bg-gradient-to-br from-green-50 to-emerald-100 hover:from-green-100 hover:to-emerald-200 border-2 border-green-200 hover:border-green-300 rounded-xl md:rounded-2xl p-3 md:p-6 transition-all duration-500 transform hover:scale-105 hover:shadow-xl min-h-[80px] md:min-h-auto"> <div class="absolute inset-0 bg-gradient-to-r from-green-600/0 via-green-600/10 to-green-600/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div> <div class="relative z-10 flex flex-col items-center"> <div class="w-10 h-10 md:w-14 md:h-14 bg-gradient-to-br from-green-500 to-green-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-2 md:mb-4 group-hover:rotate-12 transition-transform duration-300 shadow-lg"> <svg class="w-5 h-5 md:w-7 md:h-7 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-7 14a1 1 0 0 1-1-1V8a1 1 0 0 1 2 0v8a1 1 0 0 1-1 1z"/> </svg> </div> <span class="font-bold text-green-800 text-sm md:text-lg group-hover:text-green-900">下载 CSV</span> <span class="text-green-600 text-xs mt-1 hidden md:block">电子表格</span> </div> </button> <button id="copy-data" class="group relative overflow-hidden bg-gradient-to-br from-purple-50 to-violet-100 hover:from-purple-100 hover:to-violet-200 border-2 border-purple-200 hover:border-purple-300 rounded-xl md:rounded-2xl p-3 md:p-6 transition-all duration-500 transform hover:scale-105 hover:shadow-xl min-h-[80px] md:min-h-auto"> <div class="absolute inset-0 bg-gradient-to-r from-purple-600/0 via-purple-600/10 to-purple-600/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div> <div class="relative z-10 flex flex-col items-center"> <div class="w-10 h-10 md:w-14 md:h-14 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-2 md:mb-4 group-hover:rotate-12 transition-transform duration-300 shadow-lg"> <svg class="w-5 h-5 md:w-7 md:h-7 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11v14z"/> </svg> </div> <span class="font-bold text-purple-800 text-sm md:text-lg group-hover:text-purple-900">复制数据</span> <span class="text-purple-600 text-xs mt-1 hidden md:block">到剪贴板</span> </div> </button> <button id="copy-emails" class="group relative overflow-hidden bg-gradient-to-br from-orange-50 to-amber-100 hover:from-orange-100 hover:to-amber-200 border-2 border-orange-200 hover:border-orange-300 rounded-xl md:rounded-2xl p-3 md:p-6 transition-all duration-500 transform hover:scale-105 hover:shadow-xl min-h-[80px] md:min-h-auto"> <div class="absolute inset-0 bg-gradient-to-r from-orange-600/0 via-orange-600/10 to-orange-600/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div> <div class="relative z-10 flex flex-col items-center"> <div class="w-10 h-10 md:w-14 md:h-14 bg-gradient-to-br from-orange-500 to-orange-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-2 md:mb-4 group-hover:rotate-12 transition-transform duration-300 shadow-lg"> <svg class="w-5 h-5 md:w-7 md:h-7 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M3 8l7.89 7.89a1 1 0 001.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> </div> <span class="font-bold text-orange-800 text-sm md:text-lg group-hover:text-orange-900">复制邮箱</span> <span class="text-orange-600 text-xs mt-1 hidden md:block">邮件地址</span> </div> </button> <button id="view-history" class="group relative overflow-hidden bg-gradient-to-br from-gray-50 to-slate-100 hover:from-gray-100 hover:to-slate-200 border-2 border-gray-200 hover:border-gray-300 rounded-xl md:rounded-2xl p-3 md:p-6 transition-all duration-500 transform hover:scale-105 hover:shadow-xl min-h-[80px] md:min-h-auto"> <div class="absolute inset-0 bg-gradient-to-r from-gray-600/0 via-gray-600/10 to-gray-600/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div> <div class="relative z-10 flex flex-col items-center"> <div class="w-10 h-10 md:w-14 md:h-14 bg-gradient-to-br from-gray-500 to-gray-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-2 md:mb-4 group-hover:rotate-12 transition-transform duration-300 shadow-lg"> <svg class="w-5 h-5 md:w-7 md:h-7 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> </div> <span class="font-bold text-gray-800 text-sm md:text-lg group-hover:text-gray-900">查看历史</span> <span class="text-gray-600 text-xs mt-1 hidden md:block">操作记录</span> </div> </button> </div> </div> <!-- Enhanced comments list with better styling --> <div> <div class="flex items-center justify-between mb-6"> <h3 class="text-2xl font-bold text-gray-800 flex items-center"> <div class="w-10 h-10 bg-gradient-to-br from-indigo-400 to-purple-500 rounded-xl flex items-center justify-center mr-3"> <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24"> <path d="M8 10h8M8 14h6M6 4h12a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2z"/> </svg> </div> 评论内容详览 </h3> <div class="bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg"> ${data.comments.length} 条评论 </div> </div> <div class="bg-white rounded-2xl border-2 border-gray-100 shadow-xl overflow-hidden"> <div class="max-h-96 overflow-y-auto custom-scrollbar bg-gradient-to-b from-gray-50 to-white"> <div class="divide-y divide-gray-100"> ${data.comments.map((comment, index) => ` <div class="p-6 hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 transition-all duration-300 group"> <div class="flex items-start space-x-4"> <!-- Enhanced floor badge --> <div class="flex-shrink-0"> <div class="w-12 h-12 bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-2xl flex items-center justify-center text-white font-bold text-sm shadow-lg group-hover:rotate-6 transition-transform duration-300"> ${comment.floor} </div> </div> <!-- Comment content with improved typography --> <div class="flex-1 min-w-0"> <div class="flex items-center justify-between mb-3"> <h4 class="font-bold text-gray-900 text-lg group-hover:text-blue-700 transition-colors"> ${comment.author} </h4> <span class="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full"> ${comment.time} </span> </div> <div class="bg-white bg-opacity-60 rounded-xl p-4 border border-gray-100 group-hover:border-blue-200 transition-colors"> <p class="text-gray-800 leading-relaxed line-height-7">${comment.content}</p> </div> ${comment.emails && comment.emails.length > 0 ? ` <div class="mt-4 flex flex-wrap gap-2"> ${comment.emails.map((email, emailIndex) => ` <span class="comment-email-tag inline-flex items-center px-3 py-1 bg-gradient-to-r from-green-100 to-emerald-100 text-green-800 text-xs rounded-full border border-green-200 hover:from-green-200 hover:to-emerald-200 transition-colors cursor-pointer hover:shadow-md hover:scale-105" data-email="${email}" data-comment-index="${index}" data-email-index="${emailIndex}"> <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 24 24"> <path d="M3 8l7.89 7.89a1 1 0 001.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> ${email} </span> `).join('')} </div> ` : ''} </div> </div> </div> `).join('')} </div> </div> <!-- Comments list footer --> <div class="bg-gradient-to-r from-gray-100 to-gray-200 px-6 py-4 text-center"> <p class="text-gray-700 font-medium"> 📊 共显示 <span class="font-bold text-indigo-600">${data.comments.length}</span> 条评论内容 </p> </div> </div> </div> </div> </div> `; this.attachResultsEventListeners(modal, data); document.body.appendChild(modal); } /** * 添加移动端手势支持 */ addMobileGestureSupport(modal) { if (!('ontouchstart' in window)) return; // 非触摸设备跳过 const content = modal.querySelector('.discourse-extractor-content'); let startY = 0; let currentY = 0; let isDragging = false; let startTime = 0; const handleTouchStart = (e) => { startY = e.touches[0].clientY; currentY = startY; startTime = Date.now(); isDragging = true; content.style.transition = 'none'; }; const handleTouchMove = (e) => { if (!isDragging) return; currentY = e.touches[0].clientY; const deltaY = currentY - startY; // 只允许向下滑动 if (deltaY > 0) { const opacity = Math.max(0.3, 1 - deltaY / 300); const scale = Math.max(0.9, 1 - deltaY / 1000); content.style.transform = `translateY(${deltaY}px) scale(${scale})`; modal.style.backgroundColor = `rgba(0, 0, 0, ${0.8 * opacity})`; } }; const handleTouchEnd = (e) => { if (!isDragging) return; isDragging = false; const deltaY = currentY - startY; const deltaTime = Date.now() - startTime; const velocity = deltaY / deltaTime; content.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; // 判断是否应该关闭模态框 if (deltaY > 100 || velocity > 0.5) { // 关闭模态框 content.style.transform = 'translateY(100vh) scale(0.8)'; modal.style.backgroundColor = 'rgba(0, 0, 0, 0)'; setTimeout(() => modal.remove(), 300); } else { // 恢复原位 content.style.transform = 'translateY(0) scale(1)'; modal.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; } }; // 添加触摸事件监听器 content.addEventListener('touchstart', handleTouchStart, { passive: true }); content.addEventListener('touchmove', handleTouchMove, { passive: true }); content.addEventListener('touchend', handleTouchEnd, { passive: true }); } /** * 添加结果模态框事件监听器 */ attachResultsEventListeners(modal, data) { const closeModal = () => { modal.remove(); }; // 关闭按钮 modal.querySelector('.discourse-extractor-close').addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); // 功能按钮事件 modal.querySelector('#download-json').addEventListener('click', () => { this.downloadJSON(data); }); modal.querySelector('#download-csv').addEventListener('click', () => { this.downloadCSV(data); }); modal.querySelector('#copy-data').addEventListener('click', () => { this.copyData(data); }); modal.querySelector('#copy-emails').addEventListener('click', () => { this.copyEmails(data); }); // Email tag click handlers (main email section) modal.querySelectorAll('.email-tag').forEach(tag => { tag.addEventListener('click', () => { const email = tag.getAttribute('data-email'); this.copySingleEmail(email); }); }); // Comment email tag click handlers modal.querySelectorAll('.comment-email-tag').forEach(tag => { tag.addEventListener('click', () => { const email = tag.getAttribute('data-email'); this.copySingleEmail(email); }); }); modal.querySelector('#view-history').addEventListener('click', () => { closeModal(); this.showHistory(); }); // 按钮点击效果 modal.querySelectorAll('button[id]').forEach(btn => { btn.addEventListener('mousedown', () => { btn.style.transform = 'scale(0.95)'; }); btn.addEventListener('mouseup', () => { btn.style.transform = 'scale(1.05)'; }); }); // 添加移动端手势支持 this.addMobileGestureSupport(modal); } } /** * 存储管理器 */ class StorageManager { constructor(storageKey, maxRecords) { this.storageKey = storageKey; this.maxRecords = maxRecords; } /** * 获取历史记录 */ getHistory() { try { const history = localStorage.getItem(this.storageKey); return history ? JSON.parse(history) : []; } catch (error) { console.error('读取历史记录失败:', error); return []; } } /** * 保存记录 */ saveRecord(record) { try { const history = this.getHistory(); const newRecord = { id: Date.now(), timestamp: new Date().toLocaleString('zh-CN'), url: window.location.href, pageTitle: document.title, ...record }; history.unshift(newRecord); if (history.length > this.maxRecords) { history.splice(this.maxRecords); } localStorage.setItem(this.storageKey, JSON.stringify(history)); return newRecord; } catch (error) { console.error('保存历史记录失败:', error); return null; } } /** * 清除历史记录 */ clearHistory() { try { localStorage.removeItem(this.storageKey); return true; } catch (error) { console.error('清除历史记录失败:', error); return false; } } } /** * 评论加载器 */ class CommentLoader { constructor(apiManager) { this.api = apiManager; this.maxAttempts = 30; this.loadDelay = 2500; this.cachedTotalPosts = null; // 缓存总帖子数 } /** * 获取帖子总数 */ async getTotalPostsCount() { if (this.cachedTotalPosts !== null) { return this.cachedTotalPosts; } try { // 优先从 API 获取 const apiCount = await this.api.getTopicPostsCount(); if (apiCount > 0) { console.log('✅ 从 API 获取帖子总数:', apiCount); this.cachedTotalPosts = apiCount; return apiCount; } // 备选方案1: 从页面进度指示器获取 const progressElement = document.querySelector('.topic-timeline .timeline-last-read, .timeline-last-read'); if (progressElement) { const progressText = progressElement.textContent.trim(); const match = progressText.match(/\d+\s*\/\s*(\d+)/); if (match) { const domCount = parseInt(match[1]); console.log('✅ 从页面进度获取帖子总数:', domCount); this.cachedTotalPosts = domCount; return domCount; } } // 备选方案2: 从全局对象获取 if (window.Discourse?.currentTopic?.posts_count) { const globalCount = window.Discourse.currentTopic.posts_count; console.log('✅ 从全局对象获取帖子总数:', globalCount); this.cachedTotalPosts = globalCount; return globalCount; } // 备选方案3: 估算当前可见帖子数量 const posts = document.querySelectorAll('.topic-post[data-post-id], article[data-post-id]'); const estimatedCount = Math.max(posts.length, 50); // 至少假设50个帖子 console.log('⚠️ 使用估算帖子总数:', estimatedCount); this.cachedTotalPosts = estimatedCount; return estimatedCount; } catch (error) { console.error('❌ 获取帖子总数失败:', error); // 最后的默认值 this.cachedTotalPosts = 100; return 100; } } /** * 加载所有评论 */ async loadAllComments(progressCallback) { let attempts = 0; let consecutiveNoProgress = 0; console.log('🔄 开始自动加载所有评论...'); // 获取真实的帖子总数 const totalPosts = await this.getTotalPostsCount(); console.log('📊 帖子总数:', totalPosts); while (attempts < this.maxAttempts) { attempts++; const progress = await this.getLoadingProgress(); console.log(`第 ${attempts} 次尝试,当前进度: ${progress.current}/${progress.total}`); if (progressCallback) { progressCallback(progress.current, progress.total, attempts); } if (progress.current >= progress.total) { console.log('✅ 已达到目标数量,停止加载'); break; } if (!this.hasMoreContent(progress)) { console.log('❌ 没有更多内容可加载'); break; } const beforeProgress = progress.current; const loaded = await this.loadMoreContent(); if (!loaded) { consecutiveNoProgress++; } else { consecutiveNoProgress = 0; } await this.sleep(this.loadDelay); const afterProgress = await this.getLoadingProgress(); if (afterProgress.current === beforeProgress) { consecutiveNoProgress++; if (consecutiveNoProgress >= 5) { console.log('❌ 连续多次无进度,停止加载'); break; } } } const finalProgress = await this.getLoadingProgress(); console.log(`✅ 加载完成,最终进度: ${finalProgress.current}/${finalProgress.total}`); return finalProgress; } /** * 获取加载进度 */ async getLoadingProgress() { // 获取真实的总帖子数 const totalPosts = await this.getTotalPostsCount(); // 从页面右侧的进度指示器获取当前进度 const progressNavigation = document.querySelector('.topic-timeline .timeline-last-read, .timeline-last-read'); if (progressNavigation) { const progressText = progressNavigation.textContent.trim(); const match = progressText.match(/(\d+)\s*\/\s*(\d+)/); if (match) { return { current: parseInt(match[1]), total: parseInt(match[2]) // 使用页面显示的真实总数 }; } } // 备选方案:计算当前可见的帖子数量 const posts = document.querySelectorAll('.topic-post[data-post-id], article[data-post-id]'); const uniquePostIds = new Set(); posts.forEach(post => { const postId = post.getAttribute('data-post-id'); if (postId) uniquePostIds.add(postId); }); return { current: uniquePostIds.size, total: totalPosts // 使用从 API 获取的真实总数 }; } /** * 检查是否有更多内容 */ hasMoreContent(progress) { if (!progress) return true; return progress.current < progress.total; } /** * 加载更多内容 */ async loadMoreContent() { const beforeHeight = document.body.scrollHeight; this.scrollToBottom(); await this.sleep(1500); const afterHeight = document.body.scrollHeight; return afterHeight > beforeHeight; } /** * 滚动到底部 */ scrollToBottom() { window.scrollTo(0, document.body.scrollHeight); } /** * 等待函数 */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } /** * 评论提取器 */ class CommentExtractor { constructor(emailRegex) { this.emailRegex = emailRegex; } /** * 提取评论 */ extractComments(options = {}) { const { mode = 'all', startFloor = 1, endFloor = null, randomCount = 10, extractEmails = true } = options; const allComments = []; const allEmails = new Set(); const processedPostIds = new Set(); const posts = document.querySelectorAll('.topic-post[data-post-id], article[data-post-id]'); console.log(`找到 ${posts.length} 个帖子元素`); // 首先提取所有评论 posts.forEach((post, index) => { const postId = post.getAttribute('data-post-id'); if (!postId || processedPostIds.has(postId)) return; processedPostIds.add(postId); const authorElement = post.querySelector('.username, .trigger-user-card'); const author = authorElement ? authorElement.textContent.trim() : '未知用户'; const timeElement = post.querySelector('.post-date, .relative-date, time'); const time = timeElement ? timeElement.textContent.trim() : '未知时间'; const contentElement = post.querySelector('.cooked, .post-content'); const content = contentElement ? contentElement.textContent.trim() : ''; if (content && content.length > 0) { // 只在需要时提取邮箱 const postEmails = extractEmails ? (content.match(this.emailRegex) || []) : []; const comment = { id: postId, floor: index + 1, author: author, time: time, content: content, emails: postEmails }; allComments.push(comment); // 收集所有邮箱 if (extractEmails) { postEmails.forEach(email => allEmails.add(email)); } } }); // 根据模式过滤评论 let filteredComments = []; switch (mode) { case 'range': const actualEndFloor = endFloor || allComments.length; filteredComments = allComments.filter(comment => comment.floor >= startFloor && comment.floor <= actualEndFloor ); console.log(`楼层范围过滤: ${startFloor}-${actualEndFloor}, 结果: ${filteredComments.length} 条评论`); break; case 'random': const shuffled = [...allComments].sort(() => 0.5 - Math.random()); filteredComments = shuffled.slice(0, Math.min(randomCount, allComments.length)); filteredComments.sort((a, b) => a.floor - b.floor); console.log(`随机提取: ${randomCount} 条, 实际获得: ${filteredComments.length} 条评论`); break; default: // 'all' filteredComments = allComments; console.log(`全部提取: ${filteredComments.length} 条评论`); break; } // 如果是范围或随机模式,重新计算邮箱 const finalEmails = new Set(); if (extractEmails && (mode === 'range' || mode === 'random')) { filteredComments.forEach(comment => { if (comment.emails) { comment.emails.forEach(email => finalEmails.add(email)); } }); } else if (extractEmails) { // 全部模式使用之前收集的所有邮箱 allEmails.forEach(email => finalEmails.add(email)); } const result = { comments: filteredComments, emails: extractEmails ? Array.from(finalEmails) : [], pageTitle: document.title, pageUrl: window.location.href, extractTime: new Date().toLocaleString('zh-CN'), extractConfig: options, totalComments: allComments.length }; console.log('📊 提取结果:', { 模式: mode, 总评论数: result.totalComments, 提取评论数: result.comments.length, 邮箱数: result.emails.length, 是否提取邮箱: extractEmails }); return result; } } // 初始化 function init() { const extractor = new DiscourseCommentExtractor(); extractor.init(); } // 启动 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址