// ==UserScript== // @name US Card Forum 全文下载器 // @name:en US Card Forum Full Topic Downloader // @namespace http://tampermonkey.net/ // @version 1.1 // @description 在 uscardforum.com 帖子页面右下角添加一个按钮,用于下载整个话题的 Markdown 全文。能正确处理 .../topic_id/post_id 格式的URL。 // @description:en Adds a button to the bottom right of uscardforum.com topic pages to download the entire topic as a single Markdown file. Correctly handles .../topic_id/post_id URLs. // @author Gemini // @match https://www.uscardforum.com/t/*/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // --- 配置 --- const BUTTON_STYLE = { position: 'fixed', bottom: '20px', right: '20px', zIndex: '9999', backgroundColor: '#313131', color: '#A6A6A6', border: 'none', padding: '10px 20px', borderRadius: '5px', cursor: 'pointer', fontSize: '14px', fontFamily: 'sans-serif', boxShadow: '0 2px 5px rgba(0,0,0,0.2)' }; const BUTTON_TEXT = '下载全文'; const BUTTON_TEXT_LOADING = '下载中...'; const BUTTON_TEXT_DONE = '下载完成'; // --- 主逻辑 --- /** * 从当前 URL 中提取 topic_id (已修正) * @returns {string|null} topic_id 或 null */ function getTopicId() { // 修正正则表达式以匹配 /t/slug/ 结构 // 这样可以避免捕获末尾的 post_id const match = window.location.pathname.match(/\/t\/[^\/]+\/(\d+)/); return match ? match[1] : null; } /** * 从页面标题中提取并清理话题名称 * @returns {string} 清理后的话题名 */ function getTopicName() { // 例如: "这是一个话题标题 - 美国信用卡指南" -> "这是一个话题标题" const title = document.title.replace(/\s-\s(美国信用卡指南|US Card Forum)$/, '').trim(); // 替换在文件名中非法的字符 return title.replace(/[\\/:*?"<>|]/g, '_'); } /** * 触发文件下载 * @param {string} filename - 下载的文件名 * @param {string} content - 文件内容 */ function downloadFile(filename, content) { const blob = new Blob([content], { type: 'text/markdown;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.setAttribute('download', filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); } /** * 递归获取所有页面的 Markdown 内容 * @param {string} topicId - 话题 ID * @param {number} page - 当前要获取的页码 * @param {string[]} accumulatedContent - 已累积的内容数组 * @returns {Promise} 拼接后的完整内容 */ async function fetchAllPages(topicId, page = 1, accumulatedContent = []) { const url = `https://www.uscardforum.com/raw/${topicId}?page=${page}`; try { const response = await fetch(url, { credentials: 'include' }); if (!response.ok) { throw new Error(`网络请求失败: ${response.status} ${response.statusText}`); } const text = await response.text(); if (text && text.trim().length > 0) { accumulatedContent.push(text); // 为了避免请求过于频繁,可以加入一个小的延时 await new Promise(resolve => setTimeout(resolve, 100)); return fetchAllPages(topicId, page + 1, accumulatedContent); } else { return accumulatedContent.join('\n\n---\n\n'); // 使用分隔符拼接不同页面的内容 } } catch (error) { console.error(`获取第 ${page} 页内容时出错:`, error); alert(`获取第 ${page} 页内容时出错,请检查控制台获取更多信息。`); return null; // 返回 null 表示失败 } } /** * 按钮点击事件处理函数 * @param {Event} event - 点击事件对象 */ async function handleDownloadClick(event) { const button = event.target; const topicId = getTopicId(); const topicName = getTopicName(); if (!topicId || !topicName) { alert('无法从此页面提取话题 ID 或标题。'); return; } button.textContent = BUTTON_TEXT_LOADING; button.disabled = true; const fullMarkdown = await fetchAllPages(topicId); if (fullMarkdown !== null) { downloadFile(`${topicName}.md`, fullMarkdown); button.textContent = BUTTON_TEXT_DONE; } else { button.textContent = '下载失败'; } // 几秒后恢复按钮状态 setTimeout(() => { button.textContent = BUTTON_TEXT; button.disabled = false; }, 3000); } /** * 创建并显示下载按钮 */ function createDownloadButton() { if (!getTopicId()) { console.log('当前页面不是一个有效的话题页面,不加载下载按钮。'); return; } const button = document.createElement('button'); button.textContent = BUTTON_TEXT; Object.assign(button.style, BUTTON_STYLE); button.addEventListener('click', handleDownloadClick); document.body.appendChild(button); } // --- 启动脚本 --- // 等待页面加载完成再执行 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createDownloadButton); } else { createDownloadButton(); } })();