// ==UserScript== // @name 番茄作家网 · 章节同步助手 // @namespace fanqie-novel-sync // @version 1.0.0 // @description 从本地JSON批量上传章节到番茄作家网,支持断点续传、进度保存、可视化操作 // @author Pluto // @match https://fanqienovel.com/main/writer/* // @icon https://lf-lv-buz.qingting.fm/bucket/fanqienovel-common-icon/favicon.ico // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect 127.0.0.1 // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // ============================================================ // 常量 // ============================================================ const STORAGE_KEY = 'fanqie_sync_chapters'; const PROGRESS_KEY = 'fanqie_sync_progress'; const SETTINGS_KEY = 'fanqie_sync_settings'; const LOG_KEY = 'fanqie_sync_logs'; const DELAY = 3500; const MAX_LOGS = 500; // ============================================================ // 工具函数 // ============================================================ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function getBookId() { const m = location.pathname.match(/\/writer\/(\d+)/); return m ? m[1] : null; } function extractTitle(fullTitle) { const m = fullTitle.match(/^第[零一二三四五六七八九十百千\d]+章\s*(.+)/); return m ? m[1] : fullTitle; } function extractBody(content) { const lines = content.split('\n').filter(l => l.trim().length > 0); if (lines[0] && lines[0].includes('第') && lines[0].includes('章')) { return lines.slice(1).join('\n'); } return lines.join('\n'); } function contentToHtml(content) { return content.split('\n') .filter(l => l.trim().length > 0) .map(l => `
${l.trim().replace(//g, '>')}
`) .join(''); } function now() { return new Date().toLocaleTimeString('zh-CN', { hour12: false }); } // React 受控输入兼容写法 function setNativeValue(el, value) { const proto = Object.getPrototypeOf(el); const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set || Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; if (setter) setter.call(el, value); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } // ============================================================ // 数据管理 // ============================================================ function loadChapters() { return GM_getValue(STORAGE_KEY, null); } function saveChapters(chapters) { GM_setValue(STORAGE_KEY, chapters); } function loadProgress() { return GM_getValue(PROGRESS_KEY, { lastSynced: 0, errors: [] }); } function saveProgress(p) { GM_setValue(PROGRESS_KEY, p); } function loadSettings() { return GM_getValue(SETTINGS_KEY, { localServer: 'http://127.0.0.1:19888', autoSaveLog: true }); } function saveSettings(s) { GM_setValue(SETTINGS_KEY, s); } // 日志持久化 function loadLogs() { return GM_getValue(LOG_KEY, []); } function saveLogs(logs) { if (logs.length > MAX_LOGS) logs = logs.slice(-MAX_LOGS); GM_setValue(LOG_KEY, logs); } function appendLog(msg, type = 'info') { const logs = loadLogs(); logs.push({ time: now(), msg, type, ts: Date.now() }); saveLogs(logs); } // ============================================================ // 样式 // ============================================================ GM_addStyle(` /* ===== 番茄同步助手 面板 ===== */ #fq-sync-panel { position: fixed; right: 16px; bottom: 16px; z-index: 99999; width: 420px; max-height: 85vh; background: #fff; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.1); font-family: "Microsoft YaHei","PingFang SC","Noto Sans CJK SC",sans-serif; font-size: 13px; color: #333; line-height: 1.5; display: flex; flex-direction: column; overflow: hidden; transition: all 0.3s ease; } #fq-sync-panel.collapsed { width: 56px; height: 56px; border-radius: 50%; cursor: pointer; } #fq-sync-panel.collapsed .fq-body, #fq-sync-panel.collapsed .fq-header-text { display: none; } #fq-sync-panel.collapsed .fq-toggle-btn { right: 14px; } .fq-header { background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: #fff; padding: 12px 16px; display: flex; align-items: center; flex-shrink: 0; position: relative; min-height: 44px; } .fq-header-text h3 { font-size: 15px; font-weight: 600; letter-spacing: 0.5px; } .fq-header-text span { font-size: 11px; opacity: 0.8; } .fq-toggle-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: rgba(255,255,255,0.2); border: none; color: #fff; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .fq-toggle-btn:hover { background: rgba(255,255,255,0.35); } .fq-body { flex: 1; overflow-y: auto; display: flex; flex-direction: column; max-height: calc(85vh - 44px); } .fq-body::-webkit-scrollbar { width: 4px; } .fq-body::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; } /* Tabs */ .fq-tabs { display: flex; border-bottom: 1px solid #eee; flex-shrink: 0; } .fq-tab { flex: 1; padding: 8px 4px; text-align: center; cursor: pointer; font-size: 12px; color: #999; border-bottom: 2px solid transparent; transition: all 0.2s; user-select: none; } .fq-tab:hover { color: #666; } .fq-tab.active { color: #e74c3c; border-bottom-color: #e74c3c; font-weight: 600; } .fq-tab-content { display: none; padding: 12px; flex: 1; overflow-y: auto; } .fq-tab-content.active { display: flex; flex-direction: column; } /* Status bar */ .fq-status { background: #f8f9fa; border-radius: 8px; padding: 10px 12px; margin-bottom: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 6px; font-size: 12px; flex-shrink: 0; } .fq-status-item { display: flex; flex-direction: column; } .fq-status-item .fq-label { color: #999; font-size: 11px; } .fq-status-item .fq-value { font-weight: 600; } .fq-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; vertical-align: middle; } .fq-dot.green { background: #27ae60; box-shadow: 0 0 4px #27ae60; } .fq-dot.red { background: #e74c3c; box-shadow: 0 0 4px #e74c3c; } /* Progress */ .fq-progress-bg { height: 6px; background: #eee; border-radius: 3px; overflow: hidden; margin: 4px 0; } .fq-progress-fill { height: 100%; background: linear-gradient(90deg, #e74c3c, #f39c12); border-radius: 3px; transition: width 0.5s ease; } /* Buttons */ .fq-btn { padding: 7px 14px; border: none; border-radius: 6px; font-size: 12px; cursor: pointer; transition: all 0.2s; font-family: inherit; display: inline-flex; align-items: center; gap: 4px; } .fq-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0,0,0,0.12); } .fq-btn:active { transform: translateY(0); } .fq-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .fq-btn-primary { background: #e74c3c; color: #fff; } .fq-btn-primary:hover { background: #c0392b; } .fq-btn-secondary { background: #f1f3f5; color: #495057; } .fq-btn-secondary:hover { background: #e9ecef; } .fq-btn-danger { background: #fff; color: #e74c3c; border: 1px solid #e74c3c; } .fq-btn-danger:hover { background: #fadbd8; } .fq-btn-sm { padding: 4px 10px; font-size: 11px; } .fq-btn-block { width: 100%; justify-content: center; } .fq-btn-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; } /* Inputs */ .fq-input { padding: 6px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 12px; font-family: inherit; outline: none; transition: border-color 0.2s; } .fq-input:focus { border-color: #e74c3c; } .fq-input-sm { width: 60px; text-align: center; } /* Range row */ .fq-range { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; flex-shrink: 0; } .fq-range label { font-size: 12px; color: #666; white-space: nowrap; } /* Chapter list */ .fq-ch-list { flex: 1; overflow-y: auto; border: 1px solid #eee; border-radius: 6px; min-height: 100px; max-height: 250px; } .fq-ch-item { display: flex; align-items: center; padding: 5px 8px; border-bottom: 1px solid #f5f5f5; font-size: 12px; gap: 6px; transition: background 0.15s; cursor: default; } .fq-ch-item:hover { background: #f8f9fa; } .fq-ch-num { width: 28px; text-align: center; color: #aaa; font-size: 11px; flex-shrink: 0; } .fq-ch-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .fq-ch-words { font-size: 10px; color: #aaa; flex-shrink: 0; } .fq-ch-badge { font-size: 10px; padding: 1px 6px; border-radius: 8px; flex-shrink: 0; } .fq-ch-badge.ok { background: #d5f5e3; color: #27ae60; } .fq-ch-badge.err { background: #fadbd8; color: #e74c3c; } .fq-ch-badge.wait { background: #f1f3f5; color: #adb5bd; } .fq-ch-badge.uploading { background: #d6eaf8; color: #2980b9; } .fq-ch-badge.skip { background: #fdebd0; color: #f39c12; } /* Filter */ .fq-filter { display: flex; gap: 6px; margin-bottom: 8px; flex-shrink: 0; } .fq-filter input { flex: 1; } /* Drop zone */ .fq-dropzone { border: 2px dashed #ddd; border-radius: 8px; padding: 24px 12px; text-align: center; color: #999; font-size: 12px; transition: all 0.2s; cursor: pointer; flex-shrink: 0; } .fq-dropzone:hover, .fq-dropzone.drag-over { border-color: #e74c3c; color: #e74c3c; background: #fef5f5; } .fq-dropzone .fq-dz-icon { font-size: 28px; margin-bottom: 6px; } /* Log */ .fq-log { flex: 1; overflow-y: auto; font-family: "Cascadia Code","Consolas","Courier New",monospace; font-size: 11px; line-height: 1.5; background: #212529; color: #ccc; border-radius: 6px; padding: 6px 8px; min-height: 120px; max-height: 300px; } .fq-log::-webkit-scrollbar { width: 4px; } .fq-log::-webkit-scrollbar-thumb { background: #555; border-radius: 2px; } .fq-log-line { padding: 1px 0; } .fq-log-line .fq-log-ts { color: #666; margin-right: 6px; } .fq-log-line.info .fq-log-msg { color: #d6eaf8; } .fq-log-line.success .fq-log-msg { color: #d5f5e3; } .fq-log-line.error .fq-log-msg { color: #ff6b6b; } .fq-log-line.warn .fq-log-msg { color: #fdebd0; } /* Instructions */ .fq-instructions { font-size: 12px; color: #666; line-height: 1.7; } .fq-instructions ol { padding-left: 18px; } .fq-instructions li { margin-bottom: 3px; } .fq-instructions code { background: #f1f3f5; padding: 1px 5px; border-radius: 3px; font-size: 11px; color: #e74c3c; } /* Settings */ .fq-settings-row { margin-bottom: 10px; } .fq-settings-row label { display: block; font-size: 12px; color: #666; margin-bottom: 3px; } .fq-settings-row .fq-input { width: 100%; } /* Animations */ @keyframes fqPulse { 0%,100%{opacity:1} 50%{opacity:0.5} } .fq-pulse { animation: fqPulse 1.5s infinite; } @keyframes fqFadeIn { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:translateY(0)} } .fq-fade-in { animation: fqFadeIn 0.25s ease; } /* Toggle button when collapsed */ #fq-sync-panel.collapsed::after { content: '番'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 20px; font-weight: bold; color: #fff; pointer-events: none; } #fq-sync-panel.collapsed .fq-header { border-radius: 50%; width: 56px; height: 56px; justify-content: center; padding: 0; } `); // ============================================================ // UI 创建 // ============================================================ function createPanel() { const panel = document.createElement('div'); panel.id = 'fq-sync-panel'; panel.innerHTML = `在项目目录运行:
node export-chapters.js
生成 chapters-export.json,然后拖入「章节」页面。
运行本地服务器可实现:
node fanqie-gui/server.js