diff --git a/.deploy.env.example b/.deploy.env.example index 65c695a..f126d2f 100644 --- a/.deploy.env.example +++ b/.deploy.env.example @@ -32,8 +32,10 @@ SRC_DIR="/root/code/novel/fanqie-go" DOMAIN="fq.20010117.xyz" # ---------- 证书 ---------- -# Certbot 申请证书的邮箱,用于接收过期提醒 +# acme.sh 申请证书的邮箱,用于账户注册与过期提醒 CERT_EMAIL="you@example.com" +# 证书安装目录(acme.sh --install-cert 目标,nginx 配置从此处读取) +# CERT_DIR="/etc/nginx/ssl" # ---------- 编译 ---------- # BUILD_TARGET="./cmd/fanqie" diff --git a/.gitignore b/.gitignore index 57ea76e..460fc18 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ fanqie-* .env .finesse/ fanqie.log +.codegraph/ diff --git a/bookSource.json b/bookSource.json index 04295dc..2455572 100644 --- a/bookSource.json +++ b/bookSource.json @@ -1,59 +1,63 @@ [ { - "bookSourceComment": "fanqie-go 自建服务代理版 v1\n签名(Gorgon/Argus/Ladon)、设备注册、正文解密、缓存全部由服务端完成,本地零计算、零设备注册。\n接口:/search /detail /catalog /content /category-landing。\n正文参数固定 text_mode=plain&review_mode=remove,不依赖服务器配置。\n发现页:男频/女频 × 精选/标签 四组共 280 个入口(已剔除空分类与推荐流降级标签)。\n依赖服务器可用,服务失效则书源整体失效,无本地回退。", - "bookSourceGroup": "🍅 番茄", - "bookSourceName": "🍅 番茄小说", + "bookSourceName": "🔅番茄聚合", + "bookSourceGroup": "📖番茄小说", "bookSourceType": 0, "bookSourceUrl": "https://fq.20010117.xyz", - "customOrder": 0, - "enabled": true, - "enabledCookieJar": false, + "bookSourceComment": "番茄聚合书源(fanqie-go 后端)\n\n基础使用教程:\n1、搜索:\n 1) 基础搜索:直接搜,默认搜索当前模式内容\n 2) 快捷搜索:使用特殊关键词(x,t,m,d)搜索\n x:书名 如(x:十日终焉) 搜小说(可省略)\n t:书名 如(t:十日终焉) 搜听书\n m:书名 如(m:十日终焉) 搜漫画\n d:书名 如(d:十日终焉) 搜短剧\n 冒号支持中文英文,也可在书源设置中切换默认搜索模式\n2、发现页:\n 顶部筛选选择 类型/频道/字数/更新/排序 后,点击分类宫格浏览,下拉翻页\n3、段评:\n 阅读正文时点击段尾气泡即可查看段落评论,样式可在段评设置中调整\n4、听书/漫画/短剧:\n 听书自动用播放器播放;漫画走服务端解密代理;短剧打开章节自动唤起播放\n\n更换服务器地址:只需修改源URL(bookSourceUrl),全部规则使用相对地址自动跟随", + "bookSourceEnabled": true, "enabledExplore": true, - "header": "{\"User-Agent\": \"Mozilla/5.0 (Linux; Android 13; 2210132C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.5845.92 Mobile Safari/537.36\"}", - "lastUpdateTime": 1791554265601, + "enabledCookieJar": true, + "customOrder": 0, + "weight": 0, "respondTime": 180000, - "ruleBookInfo": { - "author": "$.data.author", - "coverUrl": "$.data.thumb_url", - "init": "", - "intro": "$.data.abstract", - "kind": "$.data.category_v2[*].Name", - "lastChapter": "$.data.last_chapter_title", - "name": "$.data.book_name", - "tocUrl": "/catalog?book_id={{$.data.book_id}}", - "wordCount": "$.data.word_number" - }, - "ruleContent": { - "content": "$.data.content" + "lastUpdateTime": 1791607295124, + "bookUrlPattern": "https?:\\/\\/(?:[a-zA-Z0-9-]+\\.)?[a-zA-Z0-9-]+(?::\\d+)?\\/detail\\?book_id=\\d+.*", + "jsLib": "// 当前书源版本号\nlet localVersion = '26.10.10';\n\n// 初始配置\nconst defaultConfig = {\n 发现页类型: \"小说\",\n 频道: \"男频\",\n 搜索模式: \"小说\"\n};\n\n// 获取源变量\nfunction getVariable(k) {\n if (k == undefined) k = \"\";\n const { source } = this;\n let parsed = {};\n try {\n parsed = JSON.parse(source.getVariable());\n } catch {}\n if (k == \"\") {\n return parsed;\n }\n let value = parsed[k];\n if (value == undefined) {\n value = defaultConfig[k];\n }\n return value != undefined ? value : \"\";\n}\n\n// 设置源变量\nfunction setVariable(k, v, t) {\n if (t == undefined) t = true;\n const { source, java } = this;\n const vs = this.getVariable();\n vs[k] = v;\n source.setVariable(JSON.stringify(vs, null, 4));\n if (k != '搜索关键词' && t) {\n java.toast(`\\n设置 ${k} 为 ${v}`)\n }\n}\n\n// 还原\nfunction deleteVariable() {\n const { source, java } = this;\n source.setVariable('');\n java.longToast('\\n所有设置已还原到初始化导入状态');\n}\n\n// 从 URL 提取内容类型标记(n小说/t听书/m漫画/d短剧)。\n// 注意:jsLib 函数作用域看不到规则变量 baseUrl,必须由调用方显式传入\nfunction fqTab(url, def) {\n if (def == undefined) def = 'n';\n let m = String(url).match(/fqtab=([ntmd])/);\n return m ? m[1] : def;\n}\n\n// 判断当前环境\nfunction checkEnv() {\n const {\n java,\n source\n } = this;\n\n try {\n java.qread();\n return \"轻阅读\";\n } catch (e) {}\n\n try {\n if (typeof java.reLoginView == 'function') {\n return \"改版\";\n }\n new Packages.io.legato.kazusa.utils.TimeoutCancellationException('');\n return \"改版\";\n } catch (e) {}\n try {\n java.deviceID();\n return \"苹果\";\n } catch (e) {}\n\n if (typeof source.loginUi == 'function') {\n return \"安卓\";\n }\n\n return \"改版\";\n}\n\n// 备用设置方案(登录页按钮)\nfunction ste(v) {\n const { java } = this;\n let moreSettings = this.getVariable(\"更多设置\") || {};\n let paraSettings = this.getVariable(\"段评设置\") || {};\n if (v == \"段评\") {\n if (paraSettings[\"段评开关\"] != \"false\") {\n paraSettings[\"段评开关\"] = \"false\";\n java.longToast(\"\\n❌段评已关闭\");\n } else {\n paraSettings[\"段评开关\"] = \"true\";\n java.longToast(\"\\n✅段评已开启\");\n }\n }\n if (v == \"小说\" || v == \"听书\" || v == \"漫画\" || v == \"短剧\") {\n moreSettings[\"搜索模式\"] = v;\n java.longToast(`\\n已设置搜索模式为 ${v}`);\n }\n if (v == \"图片\") {\n if (moreSettings[\"显示图片\"] != \"false\") {\n moreSettings[\"显示图片\"] = \"false\";\n java.longToast(\"\\n❌已关闭图片显示\");\n } else {\n moreSettings[\"显示图片\"] = \"true\";\n java.longToast(\"\\n✅已开启图片显示\");\n }\n }\n if (v == \"简介\") {\n if (moreSettings[\"完整简介\"] != \"false\") {\n moreSettings[\"完整简介\"] = \"false\";\n java.longToast(\"\\n❌已关闭完整简介\");\n } else {\n moreSettings[\"完整简介\"] = \"true\";\n java.longToast(\"\\n✅已开启完整简介\");\n }\n }\n this.setVariable(\"更多设置\", moreSettings, false);\n this.setVariable(\"段评设置\", paraSettings, false);\n}\n\n// 按钮切换时改变源变量并刷新发现\nfunction show(m, t) {\n const { java, source } = this;\n let data = {};\n try {\n data = JSON.parse(source.getVariable())\n } catch {}\n data[t] = m;\n if (t == \"发现页类型\") {\n data[\"频道\"] = '男频';\n }\n source.setVariable(JSON.stringify(data, null, 2));\n java.refreshExplore();\n}\n\nfunction createFilter(title, chars, defaultVal, paramKey, size, viewName) {\n if (viewName == undefined) viewName = \"'请选择类型'\";\n return {\n title: title,\n type: \"select\",\n chars: chars,\n default: defaultVal,\n \"viewName\": viewName,\n action: `show(infoMap['${title}'],'${paramKey}')`,\n style: {\n layout_flexGrow: 1,\n layout_flexBasisPercent: size\n }\n };\n}\n\nfunction createText(title, action, defaultValue, size, viewName) {\n return {\n title: title,\n type: 'text',\n default: defaultValue || '',\n action: action,\n viewName: viewName || title,\n style: {\n layout_flexGrow: 1,\n layout_flexBasisPercent: size\n }\n };\n}\n\nfunction createButton(title, action, size) {\n return {\n title: title,\n type: 'button',\n action: action,\n style: {\n layout_flexGrow: 1,\n layout_flexBasisPercent: size\n }\n };\n}\n\nfunction exploreSearch() {\n let {java, source} = this;\n var key = this.getVariable('搜索关键词');\n var tab = this.getVariable('发现页类型') || '小说';\n if (!key) {\n try { java.toast('请输入搜索关键词'); } catch (e) {}\n return;\n }\n var tabMap = {\n '小说': 'x:',\n '漫画': 'm:',\n '听书': 't:',\n '短剧': 'd:',\n '': ''\n };\n if (['t:', 'x:', 'd:', 'm:', 't:', 'x:', 'd:', 'm:'].some(p => key.startsWith(p))) {\n tab = '';\n }\n key = `${tabMap[tab]}${key}`;\n try {\n java.searchBook(key, source.bookSourceName + \"::\" + source.getKey());\n } catch (e1) {\n try { java.searchBook(key); } catch (e2) {\n try { java.open('search', key); } catch (e3) { java.toast('搜索失败,请手动搜索'); }\n }\n }\n java.refreshExplore();\n}\n\n//去除图片\nfunction removeAllImgTags(htmlString) {\n const imgTagRegex = /]*>|<\\/img>|]*\\/>/gi;\n return htmlString.replace(imgTagRegex, '');\n}\n\nlet svgs = [\n \" $displayText\",\n\n \"$displayText\",\n\n \"$displayText\",\n\n\n\n \"$displayText\",\n\n\n\n \"$displayText\",\n\n\n\n\n `\n \n $displayText\n`\n\n];\n\n// 创建svg\nfunction createSvg(number, encodedUrl, sources) {\n var displayText = number > 99 ? \"99+\" : number.toString();\n let data = this.getVariable('段评设置');\n let svgstyle = data['段评样式'] || \"0\";\n let color = data['段评边框颜色'] || \"#A7A7A7\";\n let tccolor = data['段评填充颜色'] || \"transparent\";\n let fontcolor = data['段评字体颜色'] || \"#A7A7A7\";\n let zdy = svgs[0];\n try {\n zdy = data['自定义段评样式'][svgstyle] || svgs[0];\n } catch {}\n const styleMap = {\n \"0\": svgs[0],\n \"1\": svgs[1],\n \"2\": svgs[2],\n \"3\": svgs[3],\n \"4\": svgs[4],\n \"5\": svgs[5],\n };\n let svg = styleMap[svgstyle] || zdy;\n svg = svg.replace('$color', color)\n .replace('$tccolor', tccolor)\n .replace('$fontcolor', fontcolor)\n .replace('$displayText', displayText);\n var jc = 'js';\n let islyc = this.checkEnv()\n if (islyc == \"改版\") {\n jc = 'click'\n }\n let style = data['气泡增大'] == \"true\" ? 'TEXT' : 'text';\n var encodedSvg = this.java.base64Encode(svg);\n return `data:image/svg+xml;base64,${encodedSvg},{\"${jc}\":\"showCmt('${encodedUrl}', '${sources}' ,'段评')\",\"style\":\"${style}\"}`;\n}\n\n\n// 段评气泡注入(正文

支持任意属性)\nfunction paraForAndroid(content, sources) {\n let { cache } = this;\n const createSvg = this.createSvg.bind(this);\n return content.replace(/(]*>)([\\s\\S]*?)(?:)?(<\\/p>)/g,\n function (match, open, text, url, count, close) {\n if (url && count) {\n cache.putMemory(url, 0);\n return `

${text}

`;\n }\n return `

${text}

`;\n });\n}\n\n//源阅(iOS)段评\nfunction paraForiOS(content, sources) {\n return content.replace(/(]*>)([\\s\\S]*?)(?:)?(<\\/p>)/g,\n function (match, open, text, url, count, close) {\n if (url && count) {\n const numCount = Number(count);\n const displayCount = !isNaN(numCount) && numCount > 99 ? '99' : count;\n return `
${text}
`;\n }\n return `

${text}

`;\n });\n}\n\n// 显示段评:优先魔改版(阅读R/Sigma等)的原地底部弹窗(点击遮罩关闭、可拖拽下滑);\n// 官方版无此 API,自动降级为 startBrowser 打开自包含评论页\nfunction showCmt(url, sources, ident) {\n let { java } = this;\n if (ident == null) ident = '段评';\n var title = (sources || '番茄') + ident;\n var hasPopup = false;\n try { if (typeof java.showBrowser == 'function') hasPopup = true; } catch (e) {}\n if (hasPopup) {\n try {\n let html = '';\n // 正文注入的 ident 可能是 http://(反代 301 到 https),ajax 对 301 处理不稳,强制 https\n try { html = String(java.ajax(url.replace(/^http:\\/\\//, 'https://'))); } catch (e) {}\n java.showBrowser(url, html, 'window.java=java;', JSON.stringify({\n expandedCornersRadius: 20,\n dismissOnTouchOutside: true,\n isDraggable: true,\n shouldDimBackground: true,\n backgroundDimAmount: 0.5,\n hardwareAccelerated: true,\n isNestedScrollingEnabled: true,\n setFitToContents: true,\n isHideable: true,\n heightPercentage: 0.8\n }));\n return;\n } catch (e) {}\n }\n try {\n java.startBrowser(url, title);\n return;\n } catch (e) {}\n try { java.startBrowserDp(url, title); } catch (e2) { java.toast('当前版本不支持查看评论'); }\n}\n\n// ============ 书源设置页 ============\nfunction getHtmlSettings() {\n const { java } = this;\n let data = this.getVariable('更多设置');\n let html = `\n\n\n\n\n番茄聚合 - 书源设置\n\n\n\n
\n
\n
\n
🍅 番茄聚合 · 书源设置
\n
数据服务由 fanqie-go 提供
\n
\n
\n

设置完成后请点击右上角的√应用设置

\n
\n
\n
\n
搜索模式
\n
切换后搜索对应类型,也可以在搜索时使用关键词:t:书名 搜听书,d:书名 搜短剧,m:书名 搜漫画
\n
\n
\n \n
\n
\n
\n
\n
显示图片
\n
关闭后,文章内的图片将会清除
\n
\n \n
\n
\n
\n
完整简介
\n
关闭后,书籍详情页会精简简介内容
\n
\n \n
\n
\n ${(data && data.搜索模式) || '小说'}\n ${(data && data.显示图片) || 'true'}\n ${(data && data.完整简介) || 'true'}\n
\n
\n
\n\n\n`;\n let body;\n try {\n body = java.startBrowserAwait(`data:text/html;base64,${java.base64Encode(html)}`, '番茄聚合书源设置', false).body();\n } catch (e) {\n java.longToast(\"\\n请点击页面右上角√才能设置成功哦~\");\n return;\n }\n let settings = {};\n try {\n const getSpanText = (spanId) => {\n const regex = new RegExp('id=\"' + spanId + '\"\\s*>\\s*([^<]*?)\\s*<\\/span>');\n const match = body.match(regex);\n return match ? match[1].trim() : \"\";\n };\n settings = {\n '搜索模式': getSpanText('searchModeValue') || '小说',\n '显示图片': getSpanText('showImageSwitchValue') || 'true',\n '完整简介': getSpanText('fullDescSwitchValue') || 'true',\n };\n const settingsText = Object.keys(settings).map(key => `${key}: ${settings[key]}`).join('\\n');\n java.longToast(\"\\n\" + settingsText);\n } catch (e) {}\n this.setVariable(\"更多设置\", settings, false);\n}\n\n// ============ 段评设置页 ============\nfunction getSvgSettings() {\n const { java } = this;\n const islyc = this.checkEnv();\n if (islyc == \"苹果\") {\n java.longToast(\"\\n当前软件不支持设置段评样式\");\n return;\n }\n let data = this.getVariable('段评设置');\n let html = `\n\n\n\n\n番茄聚合 - 段评设置\n\n\n\n
\n
\n
\n
🗨 番茄聚合 · 段评设置
\n
点击正文段尾气泡查看段落评论
\n
\n
\n

设置完成后请点击右上角的√应用设置

\n
\n
\n
\n
段评
\n
阅读正文时在段落末尾显示评论气泡
\n
\n \n
\n
\n
\n
气泡增大
\n
开启后,气泡显示会加大一些
\n
\n \n
\n
\n
\n
边框颜色
\n
设置段评边框颜色
\n
\n
\n \n \n
\n
\n
\n
\n
字体颜色
\n
设置段评字体颜色
\n
\n
\n \n \n
\n
\n
\n
\n
填充颜色
\n
设置段评背景填充颜色
\n
\n
\n \n \n \n
\n
\n
\n
实时预览(点击选择样式)
\n
\n
\n
\n ${(data && data.段评开关) || 'true'}\n ${(data && data.气泡增大) || 'false'}\n ${(data && data.段评样式) || '0'}\n ${(data && data.段评边框颜色) || '#A7A7A7'}\n ${(data && data.段评字体颜色) || '#A7A7A7'}\n ${(data && data.段评填充颜色) || 'transparent'}\n
\n
\n
\n\n\n`;\n let body;\n try {\n body = java.startBrowserAwait(`data:text/html;base64,${java.base64Encode(html)}`, '番茄聚合段评设置', false).body();\n } catch (e) {\n if (islyc == \"安卓\") {\n java.longToast(\"\\n当前软件版本过低,请更新软件,或者使用底部备用设置方案~\");\n } else {\n java.longToast(\"\\n请点击页面右上角√才能设置成功哦~\");\n }\n return;\n }\n let settings = {};\n try {\n const getSpanText = (spanId) => {\n const regex = new RegExp('id=\"' + spanId + '\"\\s*>\\s*([^<]*?)\\s*<\\/span>');\n const match = body.match(regex);\n return match ? match[1].trim() : \"\";\n };\n settings = {\n '段评开关': getSpanText('duanpingSwitchValue') || 'true',\n '气泡增大': getSpanText('bubbleLargeSwitchValue') || 'false',\n '段评样式': getSpanText('duanpingStyleValue') || '0',\n '段评边框颜色': getSpanText('borderColorValue') || '#A7A7A7',\n '段评字体颜色': getSpanText('fontColorValue') || '#A7A7A7',\n '段评填充颜色': getSpanText('fillColorValue') || 'transparent',\n };\n const settingsText = Object.keys(settings).map(key => `${key}: ${settings[key]}`).join('\\n');\n java.longToast(\"\\n\" + settingsText);\n } catch (e) {\n settings = {\n '段评开关': 'true',\n '气泡增大': 'false',\n '段评样式': '0',\n '段评边框颜色': '#A7A7A7',\n '段评字体颜色': '#A7A7A7',\n '段评填充颜色': 'transparent'\n };\n }\n this.setVariable(\"段评设置\", settings, false);\n}\n", + "loginUi": "[\n {\n \"name\": \"⚙️ 书源设置\",\n \"type\": \"button\",\n \"action\": \"getHtmlSettings()\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.4 }\n }, {\n \"name\": \"🗨 段评设置\",\n \"type\": \"button\",\n \"action\": \"getSvgSettings()\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.4 }\n }, {\n \"name\": \"===下方为备用设置方案===\",\n \"type\": \"button\",\n \"action\": \"\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 1 }\n }, {\n \"name\": \"📖搜索小说\",\n \"type\": \"button\",\n \"action\": \"ste('小说')\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.4 }\n }, {\n \"name\": \"🎧搜索听书\",\n \"type\": \"button\",\n \"action\": \"ste('听书')\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.4 }\n }, {\n \"name\": \"🎥搜索短剧\",\n \"type\": \"button\",\n \"action\": \"ste('短剧')\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.4 }\n }, {\n \"name\": \"🎨搜索漫画\",\n \"type\": \"button\",\n \"action\": \"ste('漫画')\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.4 }\n }, {\n \"name\": \"段评开关\",\n \"type\": \"button\",\n \"action\": \"ste('段评')\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.25 }\n }, {\n \"name\": \"图片开关\",\n \"type\": \"button\",\n \"action\": \"ste('图片')\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.25 }\n }, {\n \"name\": \"简介开关\",\n \"type\": \"button\",\n \"action\": \"ste('简介')\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.25 }\n }, {\n \"name\": \"⛔‼️ 清空还原设置\",\n \"type\": \"button\",\n \"action\": \"deleteVariable()\",\n \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 1 }\n }\n]", + "searchUrl": "\nlet moreSettings = getVariable('更多设置');\nlet tab = moreSettings && moreSettings['搜索模式'] || '小说';\nvar k = String(key);\nlet m = k.match(/^[xtmdXTMD][::]\\s*/);\nif (m) {\n tab = { x: '小说', t: '听书', m: '漫画', d: '短剧' }[m[0][0].toLowerCase()];\n k = k.slice(m[0].length);\n}\nlet tabType = { 小说: 3, 听书: 2, 漫画: 8, 短剧: 11 }[tab] || 3;\n`/search?query=${encodeURIComponent(k)}&offset=${(page - 1) * 10}&tab_type=${tabType}`;\n", + "exploreUrl": "\nvar base = String(source.getKey()).replace(/\\/$/, '');\nvar tab = getVariable('发现页类型') || '小说';\nvar channel = getVariable('频道') || '男频';\nvar wordNumMap = { '不限': '0', '30万字以内': '4', '100万字以内': '1', '30万-100万': '5', '100万-300万': '2', '300万字以上': '3', '100万字以上': '9' };\nvar updateMap = { '不限': '2', '完结': '0', '连载中': '1', '今日更新': '3' };\nvar sortMap = { '综合': '0', '热门': '24', '最新': '76', '评分': '12' };\nvar wordNum = getVariable('字数') || '不限';\nvar update = getVariable('更新') || '不限';\nvar sort = getVariable('排序') || '综合';\nvar q = `&word_number=${wordNumMap[wordNum] || '0'}&book_status=${updateMap[update] || '2'}&sort_by=${sortMap[sort] || '0'}`;\nvar tabCode = { 小说: 'n', 听书: 't', 漫画: 'm', 短剧: 'd' }[tab] || 'n';\n\nvar list = [];\nlist.push(createFilter('类型', ['小说', '听书', '短剧', '漫画'], tab, '发现页类型', 0.4));\nlist.push(createFilter('频道', ['男频', '女频'], channel, '频道', 0.4));\nlist.push(createText('搜索关键词', \"setVariable('搜索关键词',(infoMap['搜索关键词'] || (infoMap.get && infoMap.get('搜索关键词'))))\", getVariable('搜索关键词') || '', 0.5, \"'搜索书名或作者'\"));\nlist.push(createButton('🔍搜索', 'exploreSearch()', 0.3));\nlist.push({ title: '⚙️书源设置', type: 'button', action: 'getHtmlSettings()', style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.25 } });\nlist.push({ title: '🗨段评设置', type: 'button', action: 'getSvgSettings()', style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.25 } });\nlist.push(createFilter('字数', Object.keys(wordNumMap), wordNum, '字数', 0.4));\nlist.push(createFilter('更新', Object.keys(updateMap), update, '更新', 0.4));\nlist.push(createFilter('排序', Object.keys(sortMap), sort, '排序', 0.1));\n\nif (tab == '短剧') {\n list.push({ title: '短剧暂无分类,请使用上方搜索 🔍', url: '', style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 } });\n} else {\n var ctab = tab == '小说' ? (channel == '女频' ? 0 : 1) : (tab == '听书' ? 3 : 5);\n try {\n var res = JSON.parse(java.ajax(base + '/category-front?tab=' + ctab));\n var cells = ((res.data || {}).category_tab_data || {}).cell_data || [];\n cells.forEach(function (cell) {\n (cell.atom_data || []).forEach(function (a) {\n var c = a.category_data;\n if (c && c.category_id != null) {\n list.push({\n title: c.name,\n url: base + '/category-landing?category_id=' + c.category_id + '&offset={{(page-1)*20}}' + q + '&fqtab=' + tabCode,\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.25 }\n });\n }\n });\n });\n } catch (e) {\n java.toast('发现页分类获取失败');\n }\n}\nJSON.stringify(list);\n", + "ruleSearch": { + "bookList": "\nvar d = JSON.parse(result);\nvar isVideo = /tab_type=11/.test(baseUrl);\nvar arr = [];\n(d.data || []).forEach(function (b) {\n var list = isVideo ? (b.video_data || []) : (b.book_data || []);\n list.forEach(function (x) {\n if (isVideo) {\n arr.push({\n book_id: x.series_id,\n book_name: x.title,\n author: '',\n abstract: x.rec_text || '',\n thumb_url: x.horiz_cover || x.cover,\n category: '短剧',\n creation_status: '1',\n score: x.score,\n word_number: String(x.episode_cnt || '')\n });\n } else {\n arr.push(x);\n }\n });\n});\narr;\n", + "name": "$.book_name", + "author": "$.author", + "kind": "{{$.creation_status=='0'?'完结':'连载'}},{{$.category}},{{$.score}}分", + "wordCount": "$.word_number##^0+$", + "intro": "$.abstract", + "coverUrl": "$.thumb_url", + "bookUrl": "\nvar t = /tab_type=11/.test(baseUrl) ? 'd' : /tab_type=2/.test(baseUrl) ? 't' : /tab_type=8/.test(baseUrl) ? 'm' : 'n';\n`/detail?book_id=${result.book_id}&fqtab=${t}`;\n", + "checkKeyWord": "斗罗大陆" }, "ruleExplore": { - "author": "author", "bookList": "$.data.book_info[*]", - "bookUrl": "/detail?book_id={{$.book_id}}", - "coverUrl": "thumb_url", - "intro": "abstract", - "kind": "category", - "name": "book_name", - "wordCount": "word_number" + "name": "$.book_name", + "author": "$.author", + "kind": "{{$.category}},{{$.score}}分", + "wordCount": "$.word_number##^0+$", + "intro": "$.abstract", + "coverUrl": "$.thumb_url", + "bookUrl": "\nvar m = String(baseUrl).match(/fqtab=([ntmd])/);\n`/detail?book_id=${result.book_id}&fqtab=${m ? m[1] : 'n'}`;\n" }, - "ruleSearch": { - "author": "author", - "bookList": "$.data[*].book_data[*]", - "bookUrl": "/detail?book_id={{$.book_id}}", - "checkKeyWord": "十日终焉", - "coverUrl": "thumb_url", - "intro": "abstract", - "kind": "category", - "name": "book_name", - "wordCount": "word_number" + "ruleBookInfo": { + "init": "$.data", + "name": "$.book_name", + "author": "$.author", + "kind": "{{$.creation_status=='0'?'完结':'连载'}},{{$.category}}", + "wordCount": "$.word_number##^0+$", + "lastChapter": "$.last_chapter_title", + "coverUrl": "$.thumb_url", + "intro": "\nconst { book_id, last_chapter_title, last_chapter_update_time, word_number, score, abstract, read_count, category } = result;\nlet m0 = String(baseUrl).match(/fqtab=([ntmd])/);\nconst tab = { n: '小说', t: '听书', m: '漫画', d: '短剧' }[(m0 ? m0[1] : 'n')];\nlet moreSettings = getVariable('更多设置');\nconst lightDivider = \"❇️───────❇️───────❇️\";\nconst heavyDivider = \"‎\\n‎\";\nconst isValid = (v) => String(v).length > 1;\nlet ctitle = book.durChapterTitle || '未开始';\nconst two = (n) => ('0' + n).slice(-2);\nconst fmtTime = (t) => {\n let n = parseInt(t);\n if (!n || n <= 0) return '';\n let d = new Date(n * 1000);\n return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())}`;\n};\nconst formatAbstract = (text) => String(text).split(\"\\n\").map(line => ` ${line}`).join(\"\\n\");\nconst buildBasicInfo = (items) => {\n let info = \"\";\n for (let [value, prefix, icon] of items) {\n if (isValid(value)) info += ` ${icon} ${prefix} ${value}\\n`;\n }\n return info;\n};\nlet info = ` 🏷 数据来源:番茄小说(fanqie-go)\\n 🔄 当前模式:${tab}\\n 📖 阅读至:${ctitle}\\n`;\nconst fullBasicInfo = buildBasicInfo([\n [category, \"书籍分类:\", \"🌈\"],\n [last_chapter_title, \"最新章节:\", \"📚\"],\n [fmtTime(last_chapter_update_time), \"更新时间:\", \"⏳\"],\n [word_number, \"书籍字数:\", \"📝\"],\n [score, \"书籍评分:\", \"⭐\"],\n [read_count, \"今阅读人数:\", \"📃\"]\n]);\nif (fullBasicInfo) info += `${lightDivider}\\n${fullBasicInfo}`;\nif (isValid(abstract)) info += `${heavyDivider}\\n 📖 书籍简介:\\n${formatAbstract(abstract)}\\n`;\ninfo += `${lightDivider}\\n${heavyDivider}\\n 数据更新于 ${new Date().toLocaleString()}\\n`;\nconst abstractMode = moreSettings && moreSettings['完整简介'] || 'true';\nif (abstractMode == \"false\") {\n const simplifiedBasicInfo = buildBasicInfo([\n [last_chapter_title, \"最新章节:\", \"📚\"],\n [fmtTime(last_chapter_update_time), \"更新时间:\", \"⏳\"],\n [word_number, \"书籍字数:\", \"📝\"],\n [score, \"书籍评分:\", \"⭐\"]\n ]);\n info = `\\n‎${isValid(abstract) ? `📖 书籍简介:\\n${formatAbstract(abstract)}\\n${heavyDivider}` : ''}🏷 数据来源:番茄小说\\n${simplifiedBasicInfo}`;\n} else {\n info = info.split(\"\\n\").map(line => line.replace(/^ {4}/, \"\")).join(\"\\n\");\n}\ninfo;\n", + "tocUrl": "\nvar m = String(baseUrl).match(/fqtab=([ntmd])/);\n`/catalog?book_id=${result.book_id}&fqtab=${m ? m[1] : 'n'}`;\n", + "canReName": "1" }, "ruleToc": { - "chapterList": "$.data.item_data_list[*]", - "chapterName": "title", - "chapterUrl": "/content?item_id={{$.item_id}}&text_mode=plain&review_mode=remove" + "chapterList": "\n// 类型随书切换(照光遇 typeMap:安卓用 default,苹果端用其类型体系)\nconst typeMap = {\n \"小说\": { \"苹果\": 0, \"default\": 8 },\n \"听书\": { \"苹果\": 1, \"default\": 32 },\n \"漫画\": { \"苹果\": 2, \"default\": 64 },\n \"短剧\": { \"苹果\": 3, \"default\": 8 }\n};\nconst device_type = checkEnv();\nvar tabName = { n: '小说', t: '听书', m: '漫画', d: '短剧' }[fqTab(baseUrl, 'n')];\nvar currentType = typeMap[tabName] || typeMap[\"小说\"];\nif (device_type == '苹果') {\n book.type = currentType[device_type];\n} else {\n book.type = currentType.default;\n}\ntry {\n book.imageStyle = checkEnv() == '改版' ? 'FULL' : 'TEXT';\n} catch (e) {}\nresult;\n$.data.item_data_list[*]", + "chapterName": "$.title", + "chapterUrl": "\nvar t = fqTab(baseUrl, 'n');\nvar id = result.item_id;\nvar u = {\n n: `/content?item_id=${id}&text_mode=html&review=keep&image_mode=keep&fqtab=n`,\n t: `/audio?item_ids=${id}&fqtab=t`,\n m: `/manga?item_ids=${id}&output=proxy&fqtab=m`,\n d: `/video?item_ids=${id}&fqtab=d`\n}[t];\nu;\n" }, - "exploreUrl": "{\"🔥 男频精选\": [\"玄幻::/category-landing?category_id=7&offset={{(page-1)*20}}\", \"都市::/category-landing?category_id=1&offset={{(page-1)*20}}\", \"历史::/category-landing?category_id=12&offset={{(page-1)*20}}\", \"悬疑::/category-landing?category_id=10&offset={{(page-1)*20}}\", \"衍生::/category-landing?category_id=1079&offset={{(page-1)*20}}\", \"同人::/category-landing?category_id=538&offset={{(page-1)*20}}\", \"科幻末世::/category-landing?category_id=8&offset={{(page-1)*20}}\", \"奇幻仙侠::/category-landing?category_id=259&offset={{(page-1)*20}}\", \"种田::/category-landing?category_id=23&offset={{(page-1)*20}}\", \"游戏体育::/category-landing?category_id=746&offset={{(page-1)*20}}\", \"系统::/category-landing?category_id=19&offset={{(page-1)*20}}\", \"重生::/category-landing?category_id=36&offset={{(page-1)*20}}\", \"穿越::/category-landing?category_id=37&offset={{(page-1)*20}}\", \"多女主::/category-landing?category_id=91&offset={{(page-1)*20}}\", \"修仙::/category-landing?category_id=517&offset={{(page-1)*20}}\", \"二次元::/category-landing?category_id=39&offset={{(page-1)*20}}\", \"搞笑轻松::/category-landing?category_id=778&offset={{(page-1)*20}}\", \"单女主::/category-landing?category_id=389&offset={{(page-1)*20}}\", \"悬疑灵异::/category-landing?category_id=751&offset={{(page-1)*20}}\", \"末日求生::/category-landing?category_id=515&offset={{(page-1)*20}}\", \"娱乐圈::/category-landing?category_id=43&offset={{(page-1)*20}}\", \"末世::/category-landing?category_id=68&offset={{(page-1)*20}}\"], \"🌸 女频精选\": [\"现代言情::/category-landing?category_id=3&offset={{(page-1)*20}}\", \"古代言情::/category-landing?category_id=5&offset={{(page-1)*20}}\", \"纯爱::/category-landing?category_id=1012&offset={{(page-1)*20}}\", \"甜宠::/category-landing?category_id=96&offset={{(page-1)*20}}\", \"总裁::/category-landing?category_id=29&offset={{(page-1)*20}}\", \"幻想言情::/category-landing?category_id=32&offset={{(page-1)*20}}\", \"双男主::/category-landing?category_id=275&offset={{(page-1)*20}}\", \"双女主::/category-landing?category_id=704&offset={{(page-1)*20}}\", \"年代::/category-landing?category_id=79&offset={{(page-1)*20}}\", \"快穿::/category-landing?category_id=24&offset={{(page-1)*20}}\", \"豪门总裁::/category-landing?category_id=748&offset={{(page-1)*20}}\", \"追妻火葬场::/category-landing?category_id=616&offset={{(page-1)*20}}\", \"无CP::/category-landing?category_id=392&offset={{(page-1)*20}}\", \"大女主::/category-landing?category_id=760&offset={{(page-1)*20}}\", \"女强::/category-landing?category_id=86&offset={{(page-1)*20}}\"], \"🏷️ 男频标签\": [\"古代::/category-landing?category_id=758&offset={{(page-1)*20}}\", \"历史古代::/category-landing?category_id=273&offset={{(page-1)*20}}\", \"轻小说::/category-landing?category_id=858&offset={{(page-1)*20}}\", \"都市生活::/category-landing?category_id=2&offset={{(page-1)*20}}\", \"都市高武::/category-landing?category_id=1014&offset={{(page-1)*20}}\", \"游戏动漫::/category-landing?category_id=57&offset={{(page-1)*20}}\", \"抗战谍战::/category-landing?category_id=504&offset={{(page-1)*20}}\", \"体育::/category-landing?category_id=15&offset={{(page-1)*20}}\", \"都市异能::/category-landing?category_id=516&offset={{(page-1)*20}}\", \"武侠::/category-landing?category_id=16&offset={{(page-1)*20}}\", \"东方仙侠::/category-landing?category_id=1140&offset={{(page-1)*20}}\", \"东方玄幻::/category-landing?category_id=511&offset={{(page-1)*20}}\", \"男频衍生::/category-landing?category_id=1016&offset={{(page-1)*20}}\", \"西方奇幻::/category-landing?category_id=1141&offset={{(page-1)*20}}\", \"异世大陆::/category-landing?category_id=512&offset={{(page-1)*20}}\", \"都市种田::/category-landing?category_id=263&offset={{(page-1)*20}}\", \"影视小说::/category-landing?category_id=45&offset={{(page-1)*20}}\", \"仕途::/category-landing?category_id=558&offset={{(page-1)*20}}\", \"规则怪谈::/category-landing?category_id=851&offset={{(page-1)*20}}\", \"国运::/category-landing?category_id=496&offset={{(page-1)*20}}\", \"灵气复苏::/category-landing?category_id=514&offset={{(page-1)*20}}\", \"历史脑洞::/category-landing?category_id=272&offset={{(page-1)*20}}\", \"都市脑洞::/category-landing?category_id=262&offset={{(page-1)*20}}\", \"玄幻脑洞::/category-landing?category_id=257&offset={{(page-1)*20}}\", \"综漫::/category-landing?category_id=465&offset={{(page-1)*20}}\", \"综影视::/category-landing?category_id=1058&offset={{(page-1)*20}}\", \"架空::/category-landing?category_id=452&offset={{(page-1)*20}}\", \"异能::/category-landing?category_id=598&offset={{(page-1)*20}}\", \"克苏鲁::/category-landing?category_id=705&offset={{(page-1)*20}}\", \"高武世界::/category-landing?category_id=513&offset={{(page-1)*20}}\", \"悬疑脑洞::/category-landing?category_id=539&offset={{(page-1)*20}}\", \"宝可梦::/category-landing?category_id=859&offset={{(page-1)*20}}\", \"谍战::/category-landing?category_id=507&offset={{(page-1)*20}}\", \"第四天灾::/category-landing?category_id=855&offset={{(page-1)*20}}\", \"开局::/category-landing?category_id=453&offset={{(page-1)*20}}\", \"天灾::/category-landing?category_id=658&offset={{(page-1)*20}}\", \"宋朝::/category-landing?category_id=501&offset={{(page-1)*20}}\", \"第一人称::/category-landing?category_id=871&offset={{(page-1)*20}}\", \"武将::/category-landing?category_id=497&offset={{(page-1)*20}}\", \"赛博朋克::/category-landing?category_id=873&offset={{(page-1)*20}}\", \"断层::/category-landing?category_id=500&offset={{(page-1)*20}}\", \"清朝::/category-landing?category_id=503&offset={{(page-1)*20}}\", \"扮猪吃虎::/category-landing?category_id=93&offset={{(page-1)*20}}\", \"无女主::/category-landing?category_id=391&offset={{(page-1)*20}}\", \"特种兵::/category-landing?category_id=375&offset={{(page-1)*20}}\", \"校花::/category-landing?category_id=385&offset={{(page-1)*20}}\", \"大小姐::/category-landing?category_id=519&offset={{(page-1)*20}}\", \"神医::/category-landing?category_id=26&offset={{(page-1)*20}}\", \"腹黑::/category-landing?category_id=92&offset={{(page-1)*20}}\", \"反派::/category-landing?category_id=369&offset={{(page-1)*20}}\", \"大佬::/category-landing?category_id=520&offset={{(page-1)*20}}\", \"女帝::/category-landing?category_id=378&offset={{(page-1)*20}}\", \"天才::/category-landing?category_id=90&offset={{(page-1)*20}}\", \"学霸::/category-landing?category_id=82&offset={{(page-1)*20}}\", \"赘婿::/category-landing?category_id=25&offset={{(page-1)*20}}\", \"奶爸::/category-landing?category_id=42&offset={{(page-1)*20}}\", \"战神赘婿::/category-landing?category_id=27&offset={{(page-1)*20}}\", \"皇帝::/category-landing?category_id=498&offset={{(page-1)*20}}\", \"群像::/category-landing?category_id=841&offset={{(page-1)*20}}\", \"特工::/category-landing?category_id=518&offset={{(page-1)*20}}\", \"游戏主播::/category-landing?category_id=509&offset={{(page-1)*20}}\", \"全能::/category-landing?category_id=856&offset={{(page-1)*20}}\", \"贵族::/category-landing?category_id=502&offset={{(page-1)*20}}\", \"神探::/category-landing?category_id=506&offset={{(page-1)*20}}\", \"动漫衍生::/category-landing?category_id=718&offset={{(page-1)*20}}\", \"无脑爽::/category-landing?category_id=850&offset={{(page-1)*20}}\", \"网游::/category-landing?category_id=372&offset={{(page-1)*20}}\", \"风水秘术::/category-landing?category_id=1455&offset={{(page-1)*20}}\", \"灵异::/category-landing?category_id=100&offset={{(page-1)*20}}\", \"无限流::/category-landing?category_id=70&offset={{(page-1)*20}}\", \"仙侠::/category-landing?category_id=9&offset={{(page-1)*20}}\", \"四合院::/category-landing?category_id=495&offset={{(page-1)*20}}\", \"空间::/category-landing?category_id=44&offset={{(page-1)*20}}\", \"乡村::/category-landing?category_id=11&offset={{(page-1)*20}}\", \"诸天万界::/category-landing?category_id=71&offset={{(page-1)*20}}\", \"神豪::/category-landing?category_id=20&offset={{(page-1)*20}}\", \"三国::/category-landing?category_id=67&offset={{(page-1)*20}}\", \"穿书::/category-landing?category_id=382&offset={{(page-1)*20}}\", \"盗墓::/category-landing?category_id=81&offset={{(page-1)*20}}\", \"洪荒::/category-landing?category_id=66&offset={{(page-1)*20}}\", \"无敌::/category-landing?category_id=384&offset={{(page-1)*20}}\", \"都市修真::/category-landing?category_id=124&offset={{(page-1)*20}}\", \"星际::/category-landing?category_id=77&offset={{(page-1)*20}}\", \"民国::/category-landing?category_id=390&offset={{(page-1)*20}}\", \"捉鬼::/category-landing?category_id=867&offset={{(page-1)*20}}\", \"魂穿::/category-landing?category_id=852&offset={{(page-1)*20}}\", \"黑道::/category-landing?category_id=826&offset={{(page-1)*20}}\", \"破案::/category-landing?category_id=505&offset={{(page-1)*20}}\", \"1v1::/category-landing?category_id=834&offset={{(page-1)*20}}\", \"求生::/category-landing?category_id=379&offset={{(page-1)*20}}\", \"囤物资::/category-landing?category_id=494&offset={{(page-1)*20}}\", \"惊悚游戏::/category-landing?category_id=537&offset={{(page-1)*20}}\", \"美食::/category-landing?category_id=78&offset={{(page-1)*20}}\", \"火影::/category-landing?category_id=368&offset={{(page-1)*20}}\", \"直播::/category-landing?category_id=69&offset={{(page-1)*20}}\", \"奇幻::/category-landing?category_id=6&offset={{(page-1)*20}}\", \"发家致富::/category-landing?category_id=840&offset={{(page-1)*20}}\", \"职场::/category-landing?category_id=127&offset={{(page-1)*20}}\", \"宠物::/category-landing?category_id=74&offset={{(page-1)*20}}\", \"双重生::/category-landing?category_id=524&offset={{(page-1)*20}}\", \"山海经::/category-landing?category_id=875&offset={{(page-1)*20}}\", \"双系统::/category-landing?category_id=866&offset={{(page-1)*20}}\", \"海岛::/category-landing?category_id=40&offset={{(page-1)*20}}\", \"打脸::/category-landing?category_id=522&offset={{(page-1)*20}}\", \"电竞::/category-landing?category_id=508&offset={{(page-1)*20}}\", \"大唐::/category-landing?category_id=73&offset={{(page-1)*20}}\", \"推理::/category-landing?category_id=61&offset={{(page-1)*20}}\", \"红楼衍生::/category-landing?category_id=1036&offset={{(page-1)*20}}\", \"奥特同人::/category-landing?category_id=367&offset={{(page-1)*20}}\", \"家庭::/category-landing?category_id=125&offset={{(page-1)*20}}\", \"海贼王::/category-landing?category_id=833&offset={{(page-1)*20}}\", \"升级流::/category-landing?category_id=830&offset={{(page-1)*20}}\", \"基建::/category-landing?category_id=828&offset={{(page-1)*20}}\", \"传统玄幻::/category-landing?category_id=258&offset={{(page-1)*20}}\", \"明朝::/category-landing?category_id=126&offset={{(page-1)*20}}\", \"钓鱼::/category-landing?category_id=493&offset={{(page-1)*20}}\", \"鉴宝::/category-landing?category_id=17&offset={{(page-1)*20}}\", \"黑科技::/category-landing?category_id=854&offset={{(page-1)*20}}\", \"大秦::/category-landing?category_id=377&offset={{(page-1)*20}}\", \"神奇宝贝::/category-landing?category_id=371&offset={{(page-1)*20}}\", \"剑修::/category-landing?category_id=868&offset={{(page-1)*20}}\", \"漫威::/category-landing?category_id=374&offset={{(page-1)*20}}\", \"都市江湖::/category-landing?category_id=1460&offset={{(page-1)*20}}\", \"黑化::/category-landing?category_id=846&offset={{(page-1)*20}}\", \"九叔::/category-landing?category_id=383&offset={{(page-1)*20}}\", \"废土::/category-landing?category_id=869&offset={{(page-1)*20}}\", \"无后宫::/category-landing?category_id=838&offset={{(page-1)*20}}\", \"迪化::/category-landing?category_id=843&offset={{(page-1)*20}}\", \"高手下山::/category-landing?category_id=845&offset={{(page-1)*20}}\", \"灵魂互换::/category-landing?category_id=831&offset={{(page-1)*20}}\", \"西游衍生::/category-landing?category_id=373&offset={{(page-1)*20}}\", \"封神::/category-landing?category_id=731&offset={{(page-1)*20}}\", \"聊天群::/category-landing?category_id=381&offset={{(page-1)*20}}\", \"争霸::/category-landing?category_id=837&offset={{(page-1)*20}}\", \"卡牌::/category-landing?category_id=874&offset={{(page-1)*20}}\", \"科举::/category-landing?category_id=832&offset={{(page-1)*20}}\", \"海贼::/category-landing?category_id=370&offset={{(page-1)*20}}\", \"副本::/category-landing?category_id=864&offset={{(page-1)*20}}\", \"龙珠::/category-landing?category_id=376&offset={{(page-1)*20}}\", \"剑道::/category-landing?category_id=80&offset={{(page-1)*20}}\", \"甄嬛衍生::/category-landing?category_id=1035&offset={{(page-1)*20}}\", \"斩神::/category-landing?category_id=857&offset={{(page-1)*20}}\", \"斩神衍生::/category-landing?category_id=1062&offset={{(page-1)*20}}\", \"外卖::/category-landing?category_id=75&offset={{(page-1)*20}}\", \"公版衍生::/category-landing?category_id=1037&offset={{(page-1)*20}}\", \"如懿衍生::/category-landing?category_id=1034&offset={{(page-1)*20}}\", \"十日衍生::/category-landing?category_id=1060&offset={{(page-1)*20}}\"], \"🏷️ 女频标签\": [\"虐恋情深::/category-landing?category_id=481&offset={{(page-1)*20}}\", \"玄幻言情::/category-landing?category_id=248&offset={{(page-1)*20}}\", \"异世穿越::/category-landing?category_id=464&offset={{(page-1)*20}}\", \"豪门世家::/category-landing?category_id=473&offset={{(page-1)*20}}\", \"女频衍生::/category-landing?category_id=1015&offset={{(page-1)*20}}\", \"宫斗宅斗::/category-landing?category_id=246&offset={{(page-1)*20}}\", \"现言甜宠::/category-landing?category_id=395&offset={{(page-1)*20}}\", \"青春甜宠::/category-landing?category_id=749&offset={{(page-1)*20}}\", \"现言脑洞::/category-landing?category_id=267&offset={{(page-1)*20}}\", \"女频悬疑::/category-landing?category_id=747&offset={{(page-1)*20}}\", \"古言权谋::/category-landing?category_id=1317&offset={{(page-1)*20}}\", \"独宠::/category-landing?category_id=460&offset={{(page-1)*20}}\", \"现代::/category-landing?category_id=757&offset={{(page-1)*20}}\", \"民国言情::/category-landing?category_id=1017&offset={{(page-1)*20}}\", \"悬疑恋爱::/category-landing?category_id=1169&offset={{(page-1)*20}}\", \"日久生情::/category-landing?category_id=474&offset={{(page-1)*20}}\", \"职场商战::/category-landing?category_id=485&offset={{(page-1)*20}}\", \"古言脑洞::/category-landing?category_id=253&offset={{(page-1)*20}}\", \"古风世情::/category-landing?category_id=1139&offset={{(page-1)*20}}\", \"萌宝::/category-landing?category_id=28&offset={{(page-1)*20}}\", \"病娇::/category-landing?category_id=380&offset={{(page-1)*20}}\", \"团宠::/category-landing?category_id=94&offset={{(page-1)*20}}\", \"双病娇::/category-landing?category_id=870&offset={{(page-1)*20}}\", \"明星::/category-landing?category_id=486&offset={{(page-1)*20}}\", \"宠妻::/category-landing?category_id=30&offset={{(page-1)*20}}\", \"白切黑::/category-landing?category_id=853&offset={{(page-1)*20}}\", \"忠犬::/category-landing?category_id=865&offset={{(page-1)*20}}\", \"公主::/category-landing?category_id=83&offset={{(page-1)*20}}\", \"嫡女::/category-landing?category_id=88&offset={{(page-1)*20}}\", \"替身::/category-landing?category_id=470&offset={{(page-1)*20}}\", \"医生::/category-landing?category_id=487&offset={{(page-1)*20}}\", \"王妃::/category-landing?category_id=85&offset={{(page-1)*20}}\", \"位尊权重::/category-landing?category_id=1456&offset={{(page-1)*20}}\", \"厨娘::/category-landing?category_id=490&offset={{(page-1)*20}}\", \"古灵精怪::/category-landing?category_id=459&offset={{(page-1)*20}}\", \"精灵::/category-landing?category_id=89&offset={{(page-1)*20}}\", \"双学霸::/category-landing?category_id=849&offset={{(page-1)*20}}\", \"将军::/category-landing?category_id=492&offset={{(page-1)*20}}\", \"作精::/category-landing?category_id=521&offset={{(page-1)*20}}\", \"皇叔::/category-landing?category_id=87&offset={{(page-1)*20}}\", \"冰山::/category-landing?category_id=468&offset={{(page-1)*20}}\", \"皇后::/category-landing?category_id=84&offset={{(page-1)*20}}\", \"毒医::/category-landing?category_id=491&offset={{(page-1)*20}}\", \"可盐可甜::/category-landing?category_id=454&offset={{(page-1)*20}}\", \"律师::/category-landing?category_id=488&offset={{(page-1)*20}}\", \"戏精::/category-landing?category_id=863&offset={{(page-1)*20}}\", \"双面::/category-landing?category_id=469&offset={{(page-1)*20}}\", \"天作之合::/category-landing?category_id=455&offset={{(page-1)*20}}\", \"虐文::/category-landing?category_id=95&offset={{(page-1)*20}}\", \"强强::/category-landing?category_id=478&offset={{(page-1)*20}}\", \"破镜重圆::/category-landing?category_id=475&offset={{(page-1)*20}}\", \"校园::/category-landing?category_id=4&offset={{(page-1)*20}}\", \"先婚后爱::/category-landing?category_id=265&offset={{(page-1)*20}}\", \"青梅竹马::/category-landing?category_id=387&offset={{(page-1)*20}}\", \"男二上位::/category-landing?category_id=1457&offset={{(page-1)*20}}\", \"兽世::/category-landing?category_id=72&offset={{(page-1)*20}}\", \"虐渣::/category-landing?category_id=457&offset={{(page-1)*20}}\", \"暗恋::/category-landing?category_id=482&offset={{(page-1)*20}}\", \"HE::/category-landing?category_id=484&offset={{(page-1)*20}}\", \"今穿古::/category-landing?category_id=463&offset={{(page-1)*20}}\", \"女扮男装::/category-landing?category_id=388&offset={{(page-1)*20}}\", \"真假千金::/category-landing?category_id=844&offset={{(page-1)*20}}\", \"年下::/category-landing?category_id=829&offset={{(page-1)*20}}\", \"追妻::/category-landing?category_id=842&offset={{(page-1)*20}}\", \"带球跑::/category-landing?category_id=479&offset={{(page-1)*20}}\", \"双洁::/category-landing?category_id=702&offset={{(page-1)*20}}\", \"一见钟情::/category-landing?category_id=477&offset={{(page-1)*20}}\", \"古色古香::/category-landing?category_id=877&offset={{(page-1)*20}}\", \"追夫::/category-landing?category_id=872&offset={{(page-1)*20}}\", \"养崽::/category-landing?category_id=847&offset={{(page-1)*20}}\", \"胎穿::/category-landing?category_id=876&offset={{(page-1)*20}}\", \"年龄差::/category-landing?category_id=848&offset={{(page-1)*20}}\", \"久别重逢::/category-landing?category_id=839&offset={{(page-1)*20}}\", \"古穿今::/category-landing?category_id=462&offset={{(page-1)*20}}\", \"养成::/category-landing?category_id=835&offset={{(page-1)*20}}\", \"前世今生::/category-landing?category_id=523&offset={{(page-1)*20}}\", \"宠夫::/category-landing?category_id=861&offset={{(page-1)*20}}\", \"职场婚恋::/category-landing?category_id=750&offset={{(page-1)*20}}\", \"相爱相杀::/category-landing?category_id=483&offset={{(page-1)*20}}\", \"逃荒::/category-landing?category_id=557&offset={{(page-1)*20}}\", \"攻略反派::/category-landing?category_id=1459&offset={{(page-1)*20}}\", \"清穿::/category-landing?category_id=76&offset={{(page-1)*20}}\", \"家长里短::/category-landing?category_id=862&offset={{(page-1)*20}}\", \"星光璀璨::/category-landing?category_id=745&offset={{(page-1)*20}}\", \"双向奔赴::/category-landing?category_id=476&offset={{(page-1)*20}}\", \"契约婚姻::/category-landing?category_id=471&offset={{(page-1)*20}}\", \"医术::/category-landing?category_id=247&offset={{(page-1)*20}}\", \"相互救赎::/category-landing?category_id=860&offset={{(page-1)*20}}\", \"婚恋::/category-landing?category_id=34&offset={{(page-1)*20}}\", \"群穿::/category-landing?category_id=461&offset={{(page-1)*20}}\", \"隐婚::/category-landing?category_id=467&offset={{(page-1)*20}}\", \"情有独钟::/category-landing?category_id=456&offset={{(page-1)*20}}\", \"代嫁代娶::/category-landing?category_id=1458&offset={{(page-1)*20}}\", \"闪婚::/category-landing?category_id=466&offset={{(page-1)*20}}\", \"互宠::/category-landing?category_id=836&offset={{(page-1)*20}}\", \"护短::/category-landing?category_id=458&offset={{(page-1)*20}}\"]}", - "searchUrl": "/search?query={{key}}&tab_type=3&offset={{(page-1)*10}}", - "weight": 0 + "ruleContent": { + "content": "\nvar t = fqTab(baseUrl, 'n');\nlet moreSettings = getVariable('更多设置');\nlet dpSettings = getVariable('段评设置');\nlet islyc = checkEnv();\n\nif (t == 't') {\n // 听书:直接返回音频地址\n JSON.parse(result).data[0].main_url;\n} else if (t == 'm') {\n // 漫画:拼装图片\n var d = JSON.parse(result);\n var org = String(baseUrl).match(/^https?:\\/\\/[^\\/]+/)[0];\n var imgs = (d.data || {}).images || [];\n var out = [];\n for (var i = 0; i < imgs.length; i++) {\n out.push('');\n }\n out.join('\\n');\n} else if (t == 'd') {\n // 短剧:选最高清晰度,当前章节自动唤起播放\n var d = JSON.parse(result).data || {};\n var vl = d.video_list || {};\n var list = [];\n for (var k in vl) { list.push(vl[k]); }\n list.sort(function (a, b) { return (b.vheight || 0) - (a.vheight || 0); });\n var org = String(baseUrl).match(/^https?:\\/\\/[^\\/]+/)[0];\n var url = list.length ? String(list[0].main_url) : '';\n if (url && url.indexOf('http') != 0) { url = org + url; }\n if (url && book.durChapterIndex == chapter.index) {\n java.startBrowser(url, chapter.title);\n }\n '▶️ 本集已唤起播放(菜单-刷新可重新打开):\\n' + url;\n} else {\n // 小说:正文 + 段评气泡\n var d = JSON.parse(result);\n var content = String((d.data || {}).content || '');\n let show_img = moreSettings && moreSettings['显示图片'] || 'true';\n if (show_img == 'false') {\n content = removeAllImgTags(content);\n }\n let para = dpSettings && dpSettings['段评开关'] || 'true';\n if (para == 'true' && content.indexOf('", + "title": "$.data.title" + } } ] \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index f33e4a6..a8740df 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,6 +7,10 @@ # ./deploy.sh v1.2.0 # 编译 + 部署(指定版本) # ./deploy.sh v1.2.0 --init # 首次初始化(写 systemd/nginx 配置) # ./deploy.sh v1.2.0 --no-pull # 跳过 git pull +# ./deploy.sh v1.2.0 --no-autostart # 部署后不开机自启 +# ./deploy.sh v1.2.0 --no-restart # 只部署,不重启服务 +# ./deploy.sh --stop # 停止服务 +# ./deploy.sh --start # 启动服务 # ./deploy.sh --build-only # 只编译,不部署 # ./deploy.sh --rollback # 交互式回滚到指定版本 # ./deploy.sh --rollback v1.0.0 # 直接回滚到指定版本 @@ -36,9 +40,11 @@ NGINX_CONF="${NGINX_CONF:-/etc/nginx/conf.d/${APP_NAME}.conf}" APP_PORT="${APP_PORT:-40180}" # 程序监听端口 DOMAIN="${DOMAIN:-fq.20010117.xyz}" # 域名 CERT_EMAIL="${CERT_EMAIL:-admin@${DOMAIN}}" # 证书申请邮箱(过期提醒) +CERT_DIR="${CERT_DIR:-/etc/nginx/ssl}" # 证书目录(acme.sh --install-cert 安装位置) BUILD_TARGET="${BUILD_TARGET:-./cmd/fanqie}" # go build 的包路径 ENV_FILE="${ENV_FILE:-${SRC_DIR}/.env}" # 应用环境变量(.env.example 为模板) KEEP_VERSIONS="${KEEP_VERSIONS:-5}" # 保留最近几个版本 +AUTO_START="${AUTO_START:-true}" # 服务是否开机自启(--no-autostart 覆盖) # ==================== 颜色与日志 ==================== C_GREEN='\033[1;32m'; C_YELLOW='\033[1;33m'; C_RED='\033[1;31m'; C_BLUE='\033[1;34m'; C_RESET='\033[0m' @@ -63,6 +69,10 @@ ROLLBACK_VERSION="" LIST=false FORCE=false SYNC_NGINX=false +AUTO_START="${AUTO_START:-true}" +NO_RESTART=false +STOP=false +START=false print_help() { cat </dev/null 2>&1; then - $SUDO systemctl restart "${APP_NAME}" +# 应用开机自启设置(AUTO_START 由 --no-autostart / .deploy.env 决定) +apply_autostart() { + if [ "$AUTO_START" = true ]; then $SUDO systemctl enable "${APP_NAME}" >/dev/null 2>&1 || true else + $SUDO systemctl disable "${APP_NAME}" >/dev/null 2>&1 || true + info "开机自启已禁用(--no-autostart)" + fi +} + +# 重启服务 + 验证 +restart_and_verify() { + # 注意:不要用 "list-unit-files | grep -q"——grep -q 提前退出会让 systemctl 收到 + # SIGPIPE,在 pipefail 下整条管道判假(unit 多的机器必踩),误报"服务不存在" + if ! systemctl cat "${APP_NAME}" >/dev/null 2>&1; then warn "服务 ${APP_NAME}.service 不存在,请先执行 $0 --init" return 0 fi + apply_autostart + + if [ "$NO_RESTART" = true ]; then + info "跳过服务重启(--no-restart),需要时执行: sudo systemctl restart ${APP_NAME}" + return 0 + fi + + log "重启 ${APP_NAME} 服务..." + $SUDO systemctl restart "${APP_NAME}" + if command -v nginx >/dev/null 2>&1; then $SUDO systemctl reload nginx 2>/dev/null || $SUDO nginx -s reload 2>/dev/null || true fi @@ -212,8 +246,8 @@ restart_and_verify() { # 写入 nginx 配置(--init 与 --sync-nginx 共用的唯一模板来源) -# 模板自带完整 443/证书配置(证书存在时启用),不依赖 certbot --nginx 注入—— -# 这样 --sync-nginx 覆盖配置时不会丢失 HTTPS 段(certbot 注入式配置会被覆盖回 80-only) +# 模板自带完整 443/证书配置(证书存在时启用),不依赖签发工具注入—— +# 这样 --sync-nginx 覆盖配置时不会丢失 HTTPS 段(注入式配置会被覆盖回 80-only) write_nginx_conf() { # 反代 location(80 无证书时与 443 共用) local proxy_locations @@ -248,7 +282,7 @@ LOC_EOF ) local ssl_block="" - if [ -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then + if [ -f "${CERT_DIR}/${DOMAIN}/fullchain.pem" ]; then log "检测到 ${DOMAIN} 证书,配置启用 443" ssl_block=$(cat </dev/null 2>&1 || { err "服务 ${APP_NAME}.service 不存在,请先执行 $0 --init"; exit 1; } + if [ "$STOP" = true ]; then + $SUDO systemctl stop "${APP_NAME}" + log "✅ 已停止 ${APP_NAME}(开机自启设置不变)" + else + $SUDO systemctl start "${APP_NAME}" + sleep 1 + $SUDO systemctl status "${APP_NAME}" --no-pager -l | head -n 15 || true + fi + exit 0 +fi + # ==================== --rollback ==================== if [ "$ROLLBACK" = true ]; then if [ ! -d "${BIN_DIR}" ]; then @@ -459,36 +507,31 @@ EOF write_nginx_conf - # ---- Certbot 续期后自动重载 Nginx(deploy-hook 只在续期成功时执行)---- - HOOK_FILE="/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh" - if [ ! -f "${HOOK_FILE}" ]; then - log "创建 Certbot 续期重载 Hook: ${HOOK_FILE}" - $SUDO mkdir -p /etc/letsencrypt/renewal-hooks/deploy/ - $SUDO tee "${HOOK_FILE}" >/dev/null <<'HOOK' -#!/bin/bash -if /usr/sbin/nginx -t 2>/dev/null; then - systemctl reload nginx -fi -HOOK - $SUDO chmod +x "${HOOK_FILE}" - fi - - # ---- 首次初始化自动申请 HTTPS 证书(一次性;续期由 certbot.timer 自动处理)---- - if [ ! -d "/etc/letsencrypt/live/${DOMAIN}" ]; then - if command -v certbot >/dev/null 2>&1; then - log "未检测到证书,尝试自动申请: ${DOMAIN}" - $SUDO mkdir -p /var/www/html - if $SUDO certbot certonly --webroot -w /var/www/html -d "${DOMAIN}" --non-interactive --agree-tos -m "${CERT_EMAIL}"; then - log "证书申请成功,写入 443 配置..." - write_nginx_conf - else - warn "证书申请失败,请检查域名解析与 80 端口;成功后重跑: $0 --sync-nginx" - fi - else - warn "未安装 certbot,跳过证书申请。可手动执行: dnf install certbot python3-certbot-nginx" - fi - else + # ---- 首次初始化自动申请 HTTPS 证书(一次性;续期由 acme.sh cron 自动处理)---- + if [ -f "${CERT_DIR}/${DOMAIN}/fullchain.pem" ]; then log "证书已存在,跳过申请" + else + # 定位 acme.sh:PATH → 当前用户目录 → root 目录(sudo 后 HOME 可能被重置) + ACME_SH="$(command -v acme.sh 2>/dev/null || true)" + for c in "${HOME}/.acme.sh/acme.sh" "/root/.acme.sh/acme.sh"; do + if [ -z "$ACME_SH" ] && [ -x "$c" ]; then ACME_SH="$c"; fi + done + if [ -z "$ACME_SH" ]; then + err "未找到 acme.sh,请先安装: curl https://get.acme.sh | sh -s email=${CERT_EMAIL}" + exit 1 + fi + log "未检测到证书,使用 acme.sh 申请: ${DOMAIN}" + $SUDO mkdir -p /var/www/html "${CERT_DIR}/${DOMAIN}" + if $SUDO "$ACME_SH" --issue -d "${DOMAIN}" -w /var/www/html --server letsencrypt --keylength ec-256 -m "${CERT_EMAIL}" \ + && $SUDO "$ACME_SH" --install-cert -d "${DOMAIN}" --ecc \ + --fullchain-file "${CERT_DIR}/${DOMAIN}/fullchain.pem" \ + --key-file "${CERT_DIR}/${DOMAIN}/privkey.pem" \ + --reloadcmd "systemctl reload nginx"; then + log "证书申请成功,写入 443 配置..." + write_nginx_conf + else + warn "证书申请失败,请检查域名解析与 80 端口;成功后重跑: $0 --sync-nginx" + fi fi if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then diff --git a/internal/handlers/content.go b/internal/handlers/content.go index d35b356..cd01595 100644 --- a/internal/handlers/content.go +++ b/internal/handlers/content.go @@ -124,23 +124,17 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string { } } - // 获取基础 URL + // 获取基础 URL(反代场景识别 X-Forwarded-Proto,避免生成 http:// 被 301) cfg := config.GetConfig() baseURL := "" - if cfg != nil { + if r.Host != "" { + baseURL = externalBaseURL(r) + } else if cfg != nil { host := cfg.Server.Host if host == "0.0.0.0" { host = "localhost" } - port := cfg.Server.Port - scheme := "http" - - // 检查是否来自外部请求 - if r.Host != "" { - baseURL = scheme + "://" + r.Host - } else { - baseURL = scheme + "://" + host + ":" + port - } + baseURL = "http://" + host + ":" + cfg.Server.Port } // 生成带段评标记的内容 diff --git a/internal/handlers/review.go b/internal/handlers/review.go index 9fd00d1..5d3cd9c 100644 --- a/internal/handlers/review.go +++ b/internal/handlers/review.go @@ -12,6 +12,16 @@ import ( "time" ) +// externalBaseURL 取外部可访问的基础 URL(nginx 反代下识别 X-Forwarded-Proto, +// 避免生成 http:// 链接被 301,弹窗 WebView 内 fetch 对 301 处理不稳) +func externalBaseURL(r *http.Request) string { + scheme := "http" + if p := r.Header.Get("X-Forwarded-Proto"); p != "" { + scheme = p + } + return scheme + "://" + r.Host +} + // validateParams 验证必需参数 func validateParams(r *http.Request, required ...string) (map[string]string, error) { params := make(map[string]string) @@ -207,7 +217,7 @@ func HandleReviewHTML(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) - html := generateReviewHTML(data, params["item_id"], bookID, para, page, pageSize, sort) + html := generateReviewHTML(r, data, params["item_id"], bookID, para, page, pageSize, sort) fmt.Fprint(w, html) } @@ -318,6 +328,7 @@ var reviewTmpl = template.Must(template.New("review").Parse(reviewHTML)) // reviewPageData 评论页模板数据 type reviewPageData struct { ItemID, BookID string + BaseURL string ParaPlus1, Para, Sort int Total, NextOffset int PageSize, CommentCount int @@ -327,7 +338,7 @@ type reviewPageData struct { } // generateReviewHTML 生成评论 HTML 页面 -func generateReviewHTML(data map[string]interface{}, itemID, bookID string, para, page, pageSize, sort int) string { +func generateReviewHTML(r *http.Request, data map[string]interface{}, itemID, bookID string, para, page, pageSize, sort int) string { total := 0 hasMore := false nextOffset := 0 @@ -383,6 +394,7 @@ func generateReviewHTML(data map[string]interface{}, itemID, bookID string, para var b strings.Builder if err := reviewTmpl.Execute(&b, reviewPageData{ ItemID: itemID, BookID: bookID, + BaseURL: externalBaseURL(r), ParaPlus1: para + 1, Para: para, Sort: sort, Total: total, NextOffset: nextOffset, PageSize: pageSize, CommentCount: len(pageComments), diff --git a/internal/handlers/templates/review.html b/internal/handlers/templates/review.html index fbe04da..521775e 100644 --- a/internal/handlers/templates/review.html +++ b/internal/handlers/templates/review.html @@ -49,25 +49,50 @@ box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } + html, body { height: 100%; } body { background-color: var(--bg-primary); color: var(--text-primary); - padding: 15px; max-width: 600px; margin: 0 auto; + display: flex; + flex-direction: column; + overflow: hidden; transition: background-color 0.3s ease, color 0.3s ease; } .header { + flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; - padding: 10px 15px; + padding: 10px 15px 12px; background: var(--bg-secondary); - border-radius: 12px; + border-radius: 20px 20px 0 0; + border-bottom: 1px solid var(--border-color); box-shadow: 0 2px 8px var(--shadow); + position: relative; + z-index: 10; transition: background-color 0.3s ease; } + /* BottomSheet 顶部拖拽把手 */ + .header::before { + content: ''; + position: absolute; + top: 6px; + left: 50%; + transform: translateX(-50%); + width: 36px; + height: 4px; + border-radius: 2px; + background: var(--border-color); + } + #comment-list { + flex: 1; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding: 15px; + overscroll-behavior: contain; + } .header h1 { font-size: 18px; color: var(--text-primary); @@ -333,8 +358,8 @@
-
最热
-
最新
+
最热
+
最新
@@ -370,6 +395,8 @@ const bookID = '{{.BookID}}'; const para = {{.Para}}; let currentSort = {{.Sort}}; + // 服务端注入的绝对地址:弹窗 WebView(loadDataWithBaseURL) 中相对路径解析不可靠 + const API_BASE = '{{.BaseURL}}'; const commentList = document.getElementById('comment-list'); const imageViewer = document.getElementById('imageViewer'); @@ -380,7 +407,7 @@ // 已加载回复的评论 const loadedReplies = new Set(); - // URL 构建 + // URL 构建(绝对地址,兼容弹窗 WebView) function buildReviewURL(params) { const baseParams = { item_id: itemID, @@ -388,7 +415,7 @@ para: para }; const query = new URLSearchParams({...baseParams, ...params}); - return '/review?' + query.toString(); + return API_BASE + '/review?' + query.toString(); } // 时间格式化 @@ -429,11 +456,11 @@ }; } - // 滚动信息获取 + // 滚动信息获取(评论区为独立滚动容器) function getScrollInfo() { - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - const scrollHeight = document.documentElement.scrollHeight; - const clientHeight = document.documentElement.clientHeight; + const scrollTop = commentList.scrollTop; + const scrollHeight = commentList.scrollHeight; + const clientHeight = commentList.clientHeight; return { scrollTop, scrollHeight, @@ -443,11 +470,11 @@ } // 切换排序 - async function switchSort(newSort) { + async function switchSort(newSort, tab) { if (currentSort === newSort) return; - - document.querySelectorAll('.sort-tab').forEach(tab => tab.classList.remove('active')); - event.target.closest('.sort-tab').classList.add('active'); + + document.querySelectorAll('.sort-tab').forEach(t => t.classList.remove('active')); + if (tab) tab.classList.add('active'); commentList.innerHTML = LOADING_HTML; @@ -479,7 +506,7 @@ } loadedReplies.clear(); - window.scrollTo({ top: 0, behavior: 'smooth' }); + commentList.scrollTo({ top: 0, behavior: 'smooth' }); } else { commentList.innerHTML = '
加载失败,请重试
'; commentIndex = 0; @@ -751,25 +778,25 @@ // 回到顶部 function scrollToTop() { - window.scrollTo({ + commentList.scrollTo({ top: 0, behavior: 'smooth' }); } - - // 滚动监听 + + // 滚动监听(挂在评论容器上,顶栏固定不参与滚动) const handleScroll = debounce(() => { const scroll = getScrollInfo(); - + // 显示/隐藏回到顶部按钮 backToTopBtn.classList.toggle('show', scroll.scrollTop > 300); - + // 距离底部 200px 时触发加载 if (scroll.distanceFromBottom <= 200) { loadMoreComments(); } }, 100); - - window.addEventListener('scroll', handleScroll, { passive: true }); + + commentList.addEventListener('scroll', handleScroll, { passive: true });