精确搜索
晋江精确搜索
分享者: guaner001125 (317)发布时间: 11/04 23:12
本订阅源仅支持阅读Sigma及其继承版本使用 旧版精确搜索数据迁移到本订阅源: 复制旧版源变量数据粘贴到本源的源变量保存即可
{
"articleStyle": 0,
"cacheFirst": false,
"customOrder": 0,
"enableJs": true,
"enabled": true,
"enabledCookieJar": true,
"header": "@js:\ncache.put(\"jqss\",\"1\",\"5\");\nJSON.stringify({\n\t\"User-Agent\":String(java.getWebViewUA()).replace(\/\\\/$\/,'')+\"\/JINJIANG-Android\",\n\t\"versiontype\":\"reading\"\n\t});",
"lastUpdateTime": 1793804960604,
"loadWithBaseUrl": true,
"preload": false,
"preloadJs": "window.java = java;\nwindow.ajaxAwait = ajaxAwait;\nwindow.source = source;\nwindow.cache = cache;",
"showWebLog": false,
"singleUrl": false,
"sourceComment": "本订阅源仅支持阅读Sigma及其继承版本使用\n\n\n旧版精确搜索数据迁移到本订阅源:\n复制旧版源变量数据粘贴到本源的源变量保存即可",
"sourceIcon": "https:\/\/m-static.jjwxc.net\/images\/wap\/logo.png",
"sourceName": "精确搜索",
"sourceUrl": "晋江精确搜索",
"startHtml": "<!DOCTYPE html>\r\n<html lang=\"zh-CN\">\r\n<head>\r\n<meta charset=\"UTF-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover\">\r\n<title>晋江精确搜索<\/title>\r\n<link rel=\"stylesheet\" href=\"styles.css\">\r\n<\/head>\r\n<body>\r\n <div class=\"app\">\r\n <!-- 顶部常驻:完成状态 + 排序 -->\r\n <header class=\"topbar\" id=\"topFilters\">\n <div class=\"result-title\" id=\"resultTitle\">\n <div class=\"result-info\" id=\"resultInfo\" style=\"display:none;\"><\/div>\n <\/div>\n\n <\/header>\r\n\r\n\r\n <!-- 书单 -->\r\n <div class=\"loading\" id=\"loadPrev\">加载上一页…<\/div>\r\n <div id=\"list\"><\/div>\r\n <div class=\"loading\" id=\"loading\">加载中…<\/div>\r\n\r\n <!-- 空状态 -->\r\n <div id=\"emptyState\">\r\n <div class=\"em-ico\">🔍<\/div>\r\n <p>点左下角 ☰ 打开筛选抽屉<br>设置条件后点击「搜索」查看书籍<\/p>\r\n <\/div>\r\n <\/div>\r\n\r\n <!-- 悬浮按钮:左下打开筛选抽屉 -->\r\n <button class=\"fab\" id=\"fabFilter\" title=\"筛选\">☰<\/button>\r\n\r\n <!-- 右下工具按钮组 -->\r\n <div class=\"tools\" id=\"tools\">\r\n <button class=\"pagenav\" id=\"pageNav\" data-act=\"toggle\" title=\"展开 \/ 收起\">\r\n <span class=\"cur\" id=\"navCur\">1<\/span>\r\n <span class=\"tot\" id=\"navTot\">1<\/span>\r\n <\/button>\r\n <div class=\"tools-extra\" id=\"toolsExtra\">\r\n <button class=\"rbtn\" data-act=\"top\" title=\"置顶\">↑<\/button>\r\n <button class=\"rbtn\" data-act=\"bottom\" title=\"置底\">↓<\/button>\r\n <div class=\"pagepill\">\r\n <input type=\"number\" id=\"pageInput\" value=\"1\" min=\"1\">\r\n <span class=\"tt\" id=\"totalPages\">1<\/span>\r\n <\/div>\r\n <button class=\"rbtn go\" data-act=\"jump\" title=\"跳页\">GO<\/button>\r\n <\/div>\r\n <\/div>\r\n\r\n <!-- 遮罩 -->\r\n <div class=\"overlay\" id=\"overlay\"><\/div>\r\n\r\n <!-- 左侧抽屉 -->\r\n <aside class=\"drawer\" id=\"drawer\">\r\n <div class=\"drawer-head\">\r\n <h2>筛选<\/h2>\r\n <div class=\"mode-switch\" id=\"modeSwitch\">\r\n <button type=\"button\" data-mode=\"web\">网页模式<\/button>\r\n <button type=\"button\" data-mode=\"discovery\">发现模式<\/button>\r\n <\/div>\r\n <button class=\"drawer-close\" id=\"drawerClose\">×<\/button>\r\n <\/div>\r\n <div class=\"drawer-body\">\r\n <!-- 关键词 -->\r\n <section class=\"d-card\">\r\n <div class=\"d-title\">关键词<\/div>\r\n <div class=\"sub-desc\">文章名 \/ 作者名 \/ 主角配角名,可用 , 分隔<\/div>\r\n <input class=\"kw-input\" type=\"text\" id=\"kw\" placeholder=\"例如:穿书, 甜文, 都市\">\r\n <\/section>\r\n\r\n <!-- 多选分类 -->\r\n <div id=\"drawerGroups\"><\/div>\r\n\r\n <!-- 文学标签 -->\r\n <section class=\"d-card\">\r\n <div class=\"d-title\">文学标签 <span class=\"req\">点击切换:未选 → 包含 → 排除<\/span><\/div>\r\n <div class=\"sel-head\" id=\"selHead\"><\/div>\r\n <div class=\"legend\">\r\n <span><span class=\"sw i\"><\/span>包含 <b id=\"incCount\">0<\/b><\/span>\r\n <span><span class=\"sw e\"><\/span>排除 <b id=\"excCount\">0<\/b><\/span>\r\n <span class=\"tip\">共 <b id=\"tagcount\"><\/b> 个<\/span>\r\n <\/div>\r\n <div class=\"methods\">\r\n <div class=\"method\">\r\n <span class=\"mlabel\">包含方式<\/span>\r\n <span class=\"seg\" id=\"incSeg\">\r\n <button type=\"button\" data-v=\"ors\" class=\"on\">任一<\/button>\r\n <button type=\"button\" data-v=\"ands\">同时<\/button>\r\n <\/span>\r\n <\/div>\r\n <div class=\"method\">\r\n <span class=\"mlabel\">排除方式<\/span>\r\n <span class=\"seg\" id=\"excSeg\">\r\n <button type=\"button\" data-v=\"ors\" class=\"on\">任一<\/button>\r\n <button type=\"button\" data-v=\"ands\">同时<\/button>\r\n <\/span>\r\n <\/div>\r\n <\/div>\r\n <div class=\"taggrid\" id=\"drawerTags\"><\/div>\r\n <button type=\"button\" id=\"toggleTags\" style=\"margin-top:10px;border:1px dashed var(--line);background:#fff;color:var(--accent);padding:6px 15px;border-radius:8px;font-size:12px;cursor:pointer;\">展开全部<\/button>\r\n <\/section>\r\n\r\n <details class=\"req-details\">\r\n <summary>本次请求地址<\/summary>\r\n <a id=\"reqUrl\" class=\"req-url\" target=\"_blank\" rel=\"noopener\"><\/a>\r\n <\/details>\r\n <\/div>\r\n <div class=\"drawer-foot\">\r\n <button class=\"btn-foot\" id=\"drawerReset\">重置<\/button>\r\n <button class=\"btn-foot\" id=\"drawerSave\">保存<\/button>\r\n <button class=\"btn-foot\" id=\"drawerTagsBtn\">标签<\/button>\r\n <button class=\"btn-apply\" id=\"drawerApply\">搜索<\/button>\r\n <\/div>\r\n <\/aside>\r\n\r\n <!-- 名称输入弹窗(保存 \/ 重命名共用) -->\r\n <div class=\"modal\" id=\"nameModal\">\r\n <div class=\"modal-card\">\r\n <div class=\"modal-title\" id=\"nameTitle\">保存筛选<\/div>\r\n <input class=\"kw-input\" id=\"nameInput\" placeholder=\"请输入标签名称\">\r\n <div class=\"modal-actions\">\r\n <button class=\"btn-foot\" id=\"nameCancel\">取消<\/button>\r\n <button class=\"btn-apply\" id=\"nameOk\">确定<\/button>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n\r\n <!-- 标签管理弹窗 -->\r\n <div class=\"modal\" id=\"tagModal\">\r\n <div class=\"modal-card\">\r\n <div class=\"modal-head\"><span>标签管理<\/span>\r\n <button class=\"mini-btn\" id=\"tagEditBtn\">编辑<\/button>\r\n <button class=\"drawer-close\" id=\"tagModalClose\">×<\/button><\/div>\r\n <div class=\"tag-list\" id=\"tagList\"><\/div>\r\n <div class=\"modal-empty\" id=\"tagEmpty\">暂无保存的标签<\/div>\r\n <\/div>\r\n <\/div>\r\n\r\n <!-- 删除确认弹窗 -->\r\n <div class=\"modal\" id=\"delConfirmModal\">\r\n <div class=\"modal-card\">\r\n <div class=\"modal-title\">确认删除该筛选?<\/div>\r\n <div class=\"cf-line\"><span class=\"k\">序号<\/span><span class=\"v\" id=\"cfNo\"><\/span><\/div>\r\n <div class=\"cf-line\"><span class=\"k\">名称<\/span><span class=\"v\" id=\"cfName\"><\/span><\/div>\r\n <div class=\"cf-line\"><span class=\"k\">URL<\/span><span class=\"v url\" id=\"cfUrl\"><\/span><\/div>\r\n <div class=\"modal-actions\">\r\n <button class=\"btn-foot\" id=\"cfCancel\">取消<\/button>\r\n <button class=\"btn-apply\" id=\"cfOk\" style=\"background:#c0392b;border-color:#c0392b;\">确认删除<\/button>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n\r\n<script src=\"app.js\"><\/script>\r\n<\/body>\r\n<\/html>\r\n",
"startJs": "\r\nconst DATA = {\"groups\": [{\"label\": \"范围\", \"single\": false, \"options\": [{\"name\": \"fw0\", \"value\": \"0\", \"text\": \"全站\"}, {\"name\": \"fw2\", \"value\": \"2\", \"text\": \"完结半价\"}, {\"name\": \"fw3\", \"value\": \"3\", \"text\": \"VIP库\"}, {\"name\": \"fw4\", \"value\": \"4\", \"text\": \"短篇包月\"}, {\"name\": \"fw6\", \"value\": \"6\", \"text\": \"免费\"}, {\"name\": \"fw7\", \"value\": \"7\", \"text\": \"阅文衍生\"}]}, {\"label\": \"发表时间\", \"single\": false, \"options\": [{\"name\": \"fbsj0\", \"value\": \"0\", \"text\": \"不限\"}, {\"name\": \"fbsj2\", \"value\": \"2\", \"text\": \"两个月内\"}, {\"name\": \"fbsj3\", \"value\": \"3\", \"text\": \"一季度内\"}, {\"name\": \"fbsj6\", \"value\": \"6\", \"text\": \"半年内\"}, {\"name\": \"fbsj12\", \"value\": \"12\", \"text\": \"一年内\"}, {\"name\": \"fbsj24\", \"value\": \"24\", \"text\": \"两年内\"}, {\"name\": \"fbsj2026\", \"value\": \"2026\", \"text\": \"2026年\"}, {\"name\": \"fbsj2025\", \"value\": \"2025\", \"text\": \"2025年\"}, {\"name\": \"fbsj2024\", \"value\": \"2024\", \"text\": \"2024年\"}, {\"name\": \"fbsj2023\", \"value\": \"2023\", \"text\": \"2023年\"}, {\"name\": \"fbsj2022\", \"value\": \"2022\", \"text\": \"2022年\"}, {\"name\": \"fbsj2021\", \"value\": \"2021\", \"text\": \"2021年\"}, {\"name\": \"fbsj2020\", \"value\": \"2020\", \"text\": \"2020年\"}, {\"name\": \"fbsj2019\", \"value\": \"2019\", \"text\": \"2019年\"}, {\"name\": \"fbsj2018\", \"value\": \"2018\", \"text\": \"2018年\"}, {\"name\": \"fbsj2017\", \"value\": \"2017\", \"text\": \"2017年\"}, {\"name\": \"fbsj2016\", \"value\": \"2016\", \"text\": \"2016年\"}, {\"name\": \"fbsj2015\", \"value\": \"2015\", \"text\": \"2015年\"}, {\"name\": \"fbsj2014\", \"value\": \"2014\", \"text\": \"2014年\"}, {\"name\": \"fbsj2013\", \"value\": \"2013\", \"text\": \"2013年\"}, {\"name\": \"fbsj2012\", \"value\": \"2012\", \"text\": \"2012年\"}, {\"name\": \"fbsj2011\", \"value\": \"2011\", \"text\": \"2011年\"}, {\"name\": \"fbsj2010\", \"value\": \"2010\", \"text\": \"2010年\"}, {\"name\": \"fbsj2009\", \"value\": \"2009\", \"text\": \"2009年\"}, {\"name\": \"fbsj2008\", \"value\": \"2008\", \"text\": \"2008年\"}, {\"name\": \"fbsj2007\", \"value\": \"2007\", \"text\": \"2007年\"}, {\"name\": \"fbsj2006\", \"value\": \"2006\", \"text\": \"2006年\"}, {\"name\": \"fbsj2005\", \"value\": \"2005\", \"text\": \"2005年\"}, {\"name\": \"fbsj2004\", \"value\": \"2004\", \"text\": \"2004年\"}, {\"name\": \"fbsj2003\", \"value\": \"2003\", \"text\": \"2003年\"}]}, {\"label\": \"收藏数\", \"single\": false, \"options\": [{\"name\": \"novelbefavoritedcount0\", \"value\": \"0\", \"text\": \"不限\"}, {\"name\": \"novelbefavoritedcount6\", \"value\": \"6\", \"text\": \"100万收以上\"}, {\"name\": \"novelbefavoritedcount5\", \"value\": \"5\", \"text\": \"50~100万收\"}, {\"name\": \"novelbefavoritedcount4\", \"value\": \"4\", \"text\": \"10~50万收\"}, {\"name\": \"novelbefavoritedcount3\", \"value\": \"3\", \"text\": \"5~10万收\"}, {\"name\": \"novelbefavoritedcount2\", \"value\": \"2\", \"text\": \"1~5万收\"}, {\"name\": \"novelbefavoritedcount1\", \"value\": \"1\", \"text\": \"1万收及以下\"}]}, {\"label\": \"原创性\", \"single\": false, \"options\": [{\"name\": \"yc0\", \"value\": \"0\", \"text\": \"不限\"}, {\"name\": \"yc1\", \"value\": \"1\", \"text\": \"原创\"}, {\"name\": \"yc2\", \"value\": \"2\", \"text\": \"衍生\"}]}, {\"label\": \"性向\", \"single\": false, \"options\": [{\"name\": \"xx0\", \"value\": \"0\", \"text\": \"不限\"}, {\"name\": \"xx1\", \"value\": \"1\", \"text\": \"言情\"}, {\"name\": \"xx2\", \"value\": \"2\", \"text\": \"纯爱\"}, {\"name\": \"xx3\", \"value\": \"3\", \"text\": \"百合\"}, {\"name\": \"xx5\", \"value\": \"5\", \"text\": \"无CP\"}, {\"name\": \"xx6\", \"value\": \"6\", \"text\": \"多元\"}]}, {\"label\": \"视角\", \"single\": false, \"options\": [{\"name\": \"mainview0\", \"value\": \"0\", \"text\": \"不限\"}, {\"name\": \"mainview1\", \"value\": \"1\", \"text\": \"男主\"}, {\"name\": \"mainview2\", \"value\": \"2\", \"text\": \"女主\"}, {\"name\": \"mainview3\", \"value\": \"3\", \"text\": \"主攻\"}, {\"name\": \"mainview4\", \"value\": \"4\", \"text\": \"主受\"}, {\"name\": \"mainview5\", \"value\": \"5\", \"text\": \"互攻\"}, {\"name\": \"mainview8\", \"value\": \"8\", \"text\": \"不明\"}, {\"name\": \"mainview9\", \"value\": \"9\", \"text\": \"其他\"}, {\"name\": \"mainview12\", \"value\": \"12\", \"text\": \"双视角\"}, {\"name\": \"mainview13\", \"value\": \"13\", \"text\": \"多视角\"}]}, {\"label\": \"时代\", \"single\": false, \"options\": [{\"name\": \"sd0\", \"value\": \"0\", \"text\": \"不限\"}, {\"name\": \"sd1\", \"value\": \"1\", \"text\": \"近代现代\"}, {\"name\": \"sd2\", \"value\": \"2\", \"text\": \"古色古香\"}, {\"name\": \"sd4\", \"value\": \"4\", \"text\": \"架空历史\"}, {\"name\": \"sd5\", \"value\": \"5\", \"text\": \"幻想未来\"}]}, {\"label\": \"类型\", \"single\": false, \"options\": [{\"name\": \"lx0\", \"value\": \"0\", \"text\": \"不限\"}, {\"name\": \"lx1\", \"value\": \"1\", \"text\": \"爱情\"}, {\"name\": \"lx2\", \"value\": \"2\", \"text\": \"武侠\"}, {\"name\": \"lx3\", \"value\": \"3\", \"text\": \"奇幻\"}, {\"name\": \"lx4\", \"value\": \"4\", \"text\": \"仙侠\"}, {\"name\": \"lx5\", \"value\": \"5\", \"text\": \"游戏\"}, {\"name\": \"lx6\", \"value\": \"6\", \"text\": \"传奇\"}, {\"name\": \"lx7\", \"value\": \"7\", \"text\": \"科幻\"}, {\"name\": \"lx8\", \"value\": \"8\", \"text\": \"童话\"}, {\"name\": \"lx9\", \"value\": \"9\", \"text\": \"惊悚\"}, {\"name\": \"lx10\", \"value\": \"10\", \"text\": \"悬疑\"}, {\"name\": \"lx16\", \"value\": \"16\", \"text\": \"剧情\"}, {\"name\": \"lx17\", \"value\": \"17\", \"text\": \"轻小说\"}, {\"name\": \"lx20\", \"value\": \"20\", \"text\": \"古典衍生\"}, {\"name\": \"lx18\", \"value\": \"18\", \"text\": \"东方衍生\"}, {\"name\": \"lx19\", \"value\": \"19\", \"text\": \"西方衍生\"}, {\"name\": \"lx21\", \"value\": \"21\", \"text\": \"其他衍生\"}]}, {\"label\": \"完成状态\", \"single\": true, \"options\": [{\"name\": \"isfinish\", \"value\": \"0\", \"text\": \"无限制\", \"checked\": true}, {\"name\": \"isfinish\", \"value\": \"1\", \"text\": \"连载\", \"checked\": false}, {\"name\": \"isfinish\", \"value\": \"2\", \"text\": \"完结\", \"checked\": false}]}, {\"label\": \"排序方式\", \"single\": true, \"options\": [{\"name\": \"sorttype\", \"value\": \"2\", \"text\": \"积分\", \"checked\": true}, {\"name\": \"sorttype\", \"value\": \"1\", \"text\": \"最近更新\", \"checked\": false}, {\"name\": \"sorttype\", \"value\": \"3\", \"text\": \"最新发表\", \"checked\": false}, {\"name\": \"sorttype\", \"value\": \"5\", \"text\": \"字数\", \"checked\": false}, {\"name\": \"sorttype\", \"value\": \"4\", \"text\": \"收藏数\", \"checked\": false}, {\"name\": \"sorttype\", \"value\": \"10\", \"text\": \"完结高分\", \"checked\": false}]}], \"tags\": [{\"id\": \"39\", \"name\": \"情有独钟\"}, {\"id\": \"124\", \"name\": \"甜文\"}, {\"id\": \"137\", \"name\": \"爽文\"}, {\"id\": \"19\", \"name\": \"强强\"}, {\"id\": \"60\", \"name\": \"穿越时空\"}, {\"id\": \"52\", \"name\": \"天作之合\"}, {\"id\": \"54\", \"name\": \"天之骄子\"}, {\"id\": \"33\", \"name\": \"豪门世家\"}, {\"id\": \"122\", \"name\": \"系统\"}, {\"id\": \"30\", \"name\": \"都市\"}, {\"id\": \"268\", \"name\": \"日常\"}, {\"id\": \"134\", \"name\": \"穿书\"}, {\"id\": \"32\", \"name\": \"宫廷侯爵\"}, {\"id\": \"183\", \"name\": \"成长\"}, {\"id\": \"66\", \"name\": \"种田文\"}, {\"id\": \"68\", \"name\": \"仙侠修真\"}, {\"id\": \"47\", \"name\": \"破镜重圆\"}, {\"id\": \"75\", \"name\": \"重生\"}, {\"id\": \"139\", \"name\": \"升级流\"}, {\"id\": \"101\", \"name\": \"业界精英\"}, {\"id\": \"173\", \"name\": \"年代文\"}, {\"id\": \"26\", \"name\": \"灵异神怪\"}, {\"id\": \"315\", \"name\": \"先婚后爱\"}, {\"id\": \"295\", \"name\": \"万人迷\"}, {\"id\": \"64\", \"name\": \"娱乐圈\"}, {\"id\": \"25\", \"name\": \"幻想空间\"}, {\"id\": \"125\", \"name\": \"快穿\"}, {\"id\": \"237\", \"name\": \"基建\"}, {\"id\": \"259\", \"name\": \"ABO\"}, {\"id\": \"318\", \"name\": \"HE\"}, {\"id\": \"185\", \"name\": \"校园\"}, {\"id\": \"21\", \"name\": \"年下\"}, {\"id\": \"266\", \"name\": \"沙雕\"}, {\"id\": \"99\", \"name\": \"美食\"}, {\"id\": \"41\", \"name\": \"欢喜冤家\"}, {\"id\": \"83\", \"name\": \"无限流\"}, {\"id\": \"275\", \"name\": \"狗血\"}, {\"id\": \"20\", \"name\": \"生子\"}, {\"id\": \"121\", \"name\": \"励志\"}, {\"id\": \"321\", \"name\": \"追爱火葬场\"}, {\"id\": \"329\", \"name\": \"救赎\"}, {\"id\": \"72\", \"name\": \"异能\"}, {\"id\": \"265\", \"name\": \"治愈\"}, {\"id\": \"356\", \"name\": \"群像\"}, {\"id\": \"103\", \"name\": \"相爱相杀\"}, {\"id\": \"263\", \"name\": \"正剧\"}, {\"id\": \"126\", \"name\": \"现代架空\"}, {\"id\": \"6\", \"name\": \"综漫\"}, {\"id\": \"178\", \"name\": \"朝堂\"}, {\"id\": \"81\", \"name\": \"末世\"}, {\"id\": \"262\", \"name\": \"轻松\"}, {\"id\": \"287\", \"name\": \"团宠\"}, {\"id\": \"314\", \"name\": \"暗恋\"}, {\"id\": \"240\", \"name\": \"咒回\"}, {\"id\": \"135\", \"name\": \"星际\"}, {\"id\": \"205\", \"name\": \"萌宠\"}, {\"id\": \"291\", \"name\": \"美强惨\"}, {\"id\": \"180\", \"name\": \"经营\"}, {\"id\": \"62\", \"name\": \"青梅竹马\"}, {\"id\": \"142\", \"name\": \"直播\"}, {\"id\": \"128\", \"name\": \"悬疑推理\"}, {\"id\": \"17\", \"name\": \"英美衍生\"}, {\"id\": \"59\", \"name\": \"西方罗曼\"}, {\"id\": \"175\", \"name\": \"市井生活\"}, {\"id\": \"184\", \"name\": \"逆袭\"}, {\"id\": \"45\", \"name\": \"因缘邂逅\"}, {\"id\": \"90\", \"name\": \"少年漫\"}, {\"id\": \"301\", \"name\": \"高岭之花\"}, {\"id\": \"91\", \"name\": \"历史衍生\"}, {\"id\": \"332\", \"name\": \"日久生情\"}, {\"id\": \"132\", \"name\": \"超级英雄\"}, {\"id\": \"138\", \"name\": \"打脸\"}, {\"id\": \"70\", \"name\": \"体育竞技\"}, {\"id\": \"127\", \"name\": \"未来架空\"}, {\"id\": \"322\", \"name\": \"囤货\"}, {\"id\": \"46\", \"name\": \"近水楼台\"}, {\"id\": \"144\", \"name\": \"东方玄幻\"}, {\"id\": \"243\", \"name\": \"马甲文\"}, {\"id\": \"208\", \"name\": \"文野\"}, {\"id\": \"282\", \"name\": \"港风\"}, {\"id\": \"298\", \"name\": \"钓系\"}, {\"id\": \"241\", \"name\": \"柯南\"}, {\"id\": \"143\", \"name\": \"西幻\"}, {\"id\": \"145\", \"name\": \"复仇虐渣\"}, {\"id\": \"276\", \"name\": \"全息\"}, {\"id\": \"82\", \"name\": \"女强\"}, {\"id\": \"44\", \"name\": \"边缘恋歌\"}, {\"id\": \"206\", \"name\": \"玄学\"}, {\"id\": \"310\", \"name\": \"御姐\"}, {\"id\": \"92\", \"name\": \"游戏网游\"}, {\"id\": \"273\", \"name\": \"脑洞\"}, {\"id\": \"260\", \"name\": \"虫族\"}, {\"id\": \"355\", \"name\": \"单元文\"}, {\"id\": \"31\", \"name\": \"花季雨季\"}, {\"id\": \"96\", \"name\": \"女配\"}, {\"id\": \"319\", \"name\": \"求生\"}, {\"id\": \"29\", \"name\": \"惊悚\"}, {\"id\": \"326\", \"name\": \"权谋\"}, {\"id\": \"344\", \"name\": \"开挂\"}, {\"id\": \"370\", \"name\": \"迪化流\"}, {\"id\": \"350\", \"name\": \"位面\"}, {\"id\": \"313\", \"name\": \"白月光\"}, {\"id\": \"53\", \"name\": \"阴差阳错\"}, {\"id\": \"48\", \"name\": \"恋爱合约\"}, {\"id\": \"261\", \"name\": \"爆笑\"}, {\"id\": \"174\", \"name\": \"科举\"}, {\"id\": \"28\", \"name\": \"科幻\"}, {\"id\": \"328\", \"name\": \"电竞\"}, {\"id\": \"136\", \"name\": \"女扮男装\"}, {\"id\": \"78\", \"name\": \"婚恋\"}, {\"id\": \"368\", \"name\": \"足球\"}, {\"id\": \"4\", \"name\": \"火影\"}, {\"id\": \"285\", \"name\": \"第四天灾\"}, {\"id\": \"73\", \"name\": \"宅斗\"}, {\"id\": \"360\", \"name\": \"乙女向\"}, {\"id\": \"320\", \"name\": \"失忆\"}, {\"id\": \"181\", \"name\": \"都市异闻\"}, {\"id\": \"369\", \"name\": \"哨向\"}, {\"id\": \"35\", \"name\": \"江湖\"}, {\"id\": \"86\", \"name\": \"日韩泰\"}, {\"id\": \"38\", \"name\": \"布衣生活\"}, {\"id\": \"289\", \"name\": \"龙傲天\"}, {\"id\": \"302\", \"name\": \"萌娃\"}, {\"id\": \"57\", \"name\": \"异世大陆\"}, {\"id\": \"354\", \"name\": \"论坛体\"}, {\"id\": \"36\", \"name\": \"三教九流\"}, {\"id\": \"290\", \"name\": \"腹黑\"}, {\"id\": \"335\", \"name\": \"综艺\"}, {\"id\": \"56\", \"name\": \"随身空间\"}, {\"id\": \"380\", \"name\": \"唐穿\"}, {\"id\": \"49\", \"name\": \"前世今生\"}, {\"id\": \"23\", \"name\": \"灵魂转换\"}, {\"id\": \"97\", \"name\": \"机甲\"}, {\"id\": \"55\", \"name\": \"平步青云\"}, {\"id\": \"1\", \"name\": \"网王\"}, {\"id\": \"283\", \"name\": \"克苏鲁\"}, {\"id\": \"27\", \"name\": \"魔幻\"}, {\"id\": \"58\", \"name\": \"田园\"}, {\"id\": \"339\", \"name\": \"抽奖抽卡\"}, {\"id\": \"253\", \"name\": \"排球少年\"}, {\"id\": \"312\", \"name\": \"忠犬\"}, {\"id\": \"393\", \"name\": \"咸鱼\"}, {\"id\": \"189\", \"name\": \"古代幻想\"}, {\"id\": \"12\", \"name\": \"家教\"}, {\"id\": \"281\", \"name\": \"废土\"}, {\"id\": \"51\", \"name\": \"乔装改扮\"}, {\"id\": \"85\", \"name\": \"制服情缘\"}, {\"id\": \"18\", \"name\": \"清穿\"}, {\"id\": \"343\", \"name\": \"剧透\"}, {\"id\": \"362\", \"name\": \"转生\"}, {\"id\": \"361\", \"name\": \"规则怪谈\"}, {\"id\": \"13\", \"name\": \"海贼王\"}, {\"id\": \"358\", \"name\": \"模拟器\"}, {\"id\": \"305\", \"name\": \"炮灰\"}, {\"id\": \"42\", \"name\": \"虐文\"}, {\"id\": \"277\", \"name\": \"赛博朋克\"}, {\"id\": \"381\", \"name\": \"宋穿\"}, {\"id\": \"387\", \"name\": \"真假少爷\"}, {\"id\": \"74\", \"name\": \"宫斗\"}, {\"id\": \"292\", \"name\": \"师徒\"}, {\"id\": \"311\", \"name\": \"高智商\"}, {\"id\": \"267\", \"name\": \"热血\"}, {\"id\": \"348\", \"name\": \"神豪流\"}, {\"id\": \"258\", \"name\": \"刀剑乱舞\"}, {\"id\": \"363\", \"name\": \"萌\"}, {\"id\": \"364\", \"name\": \"燃\"}, {\"id\": \"372\", \"name\": \"星穹铁道\"}, {\"id\": \"196\", \"name\": \"异闻传说\"}, {\"id\": \"379\", \"name\": \"明穿\"}, {\"id\": \"309\", \"name\": \"学霸\"}, {\"id\": \"22\", \"name\": \"时代奇缘\"}, {\"id\": \"378\", \"name\": \"秦穿\"}, {\"id\": \"271\", \"name\": \"现实\"}, {\"id\": \"307\", \"name\": \"天选之子\"}, {\"id\": \"210\", \"name\": \"时代新风\"}, {\"id\": \"274\", \"name\": \"反套路\"}, {\"id\": \"190\", \"name\": \"异想天开\"}, {\"id\": \"65\", \"name\": \"古穿今\"}, {\"id\": \"5\", \"name\": \"猎人\"}, {\"id\": \"331\", \"name\": \"开荒\"}, {\"id\": \"334\", \"name\": \"真假千金\"}, {\"id\": \"254\", \"name\": \"鬼灭\"}, {\"id\": \"383\", \"name\": \"三国穿越\"}, {\"id\": \"394\", \"name\": \"弹幕\"}, {\"id\": \"130\", \"name\": \"古典名著\"}, {\"id\": \"330\", \"name\": \"创业\"}, {\"id\": \"270\", \"name\": \"吐槽\"}, {\"id\": \"186\", \"name\": \"姐弟恋\"}, {\"id\": \"299\", \"name\": \"毒舌\"}, {\"id\": \"367\", \"name\": \"神话传说\"}, {\"id\": \"352\", \"name\": \"群穿\"}, {\"id\": \"182\", \"name\": \"时尚圈\"}, {\"id\": \"385\", \"name\": \"公路文\"}, {\"id\": \"371\", \"name\": \"原神\"}, {\"id\": \"98\", \"name\": \"职场\"}, {\"id\": \"199\", \"name\": \"史诗奇幻\"}, {\"id\": \"316\", \"name\": \"对照组\"}, {\"id\": \"382\", \"name\": \"汉穿\"}, {\"id\": \"366\", \"name\": \"吐槽役\"}, {\"id\": \"384\", \"name\": \"总裁\"}, {\"id\": \"294\", \"name\": \"傲娇\"}, {\"id\": \"337\", \"name\": \"读心术\"}, {\"id\": \"11\", \"name\": \"武侠\"}, {\"id\": \"61\", \"name\": \"民国\"}, {\"id\": \"303\", \"name\": \"男配\"}, {\"id\": \"95\", \"name\": \"黑篮\"}, {\"id\": \"24\", \"name\": \"性别转换\"}, {\"id\": \"284\", \"name\": \"灵气复苏\"}, {\"id\": \"16\", \"name\": \"西方名著\"}, {\"id\": \"325\", \"name\": \"赶山赶海\"}, {\"id\": \"336\", \"name\": \"极品亲戚\"}, {\"id\": \"257\", \"name\": \"犬夜叉\"}, {\"id\": \"377\", \"name\": \"女尊\"}, {\"id\": \"340\", \"name\": \"读档流\"}, {\"id\": \"324\", \"name\": \"乡村\"}, {\"id\": \"351\", \"name\": \"签到流\"}, {\"id\": \"288\", \"name\": \"纸片人\"}, {\"id\": \"15\", \"name\": \"红楼梦\"}, {\"id\": \"198\", \"name\": \"大冒险\"}, {\"id\": \"390\", \"name\": \"国风幻想\"}, {\"id\": \"286\", \"name\": \"替身\"}, {\"id\": \"293\", \"name\": \"NPC\"}, {\"id\": \"389\", \"name\": \"特摄\"}, {\"id\": \"14\", \"name\": \"少女漫\"}, {\"id\": \"357\", \"name\": \"卡牌\"}, {\"id\": \"296\", \"name\": \"冰山\"}, {\"id\": \"269\", \"name\": \"烧脑\"}, {\"id\": \"140\", \"name\": \"美娱\"}, {\"id\": \"123\", \"name\": \"商战\"}, {\"id\": \"392\", \"name\": \"魔王勇者\"}, {\"id\": \"376\", \"name\": \"FGO\"}, {\"id\": \"67\", \"name\": \"洪荒\"}, {\"id\": \"323\", \"name\": \"非遗\"}, {\"id\": \"338\", \"name\": \"app\"}, {\"id\": \"391\", \"name\": \"漫穿\"}, {\"id\": \"317\", \"name\": \"BE\"}, {\"id\": \"346\", \"name\": \"预知\"}, {\"id\": \"300\", \"name\": \"多重人格\"}, {\"id\": \"395\", \"name\": \"剑网3\"}, {\"id\": \"215\", \"name\": \"封神\"}, {\"id\": \"8\", \"name\": \"七五\"}, {\"id\": \"347\", \"name\": \"召唤流\"}, {\"id\": \"280\", \"name\": \"中世纪\"}, {\"id\": \"256\", \"name\": \"魔法少女\"}, {\"id\": \"192\", \"name\": \"亡灵异族\"}, {\"id\": \"279\", \"name\": \"原始社会\"}, {\"id\": \"375\", \"name\": \"中二\"}, {\"id\": \"87\", \"name\": \"港台\"}, {\"id\": \"308\", \"name\": \"天降\"}, {\"id\": \"197\", \"name\": \"奇谭\"}, {\"id\": \"278\", \"name\": \"蒸汽朋克\"}, {\"id\": \"304\", \"name\": \"暖男\"}, {\"id\": \"297\", \"name\": \"吃货\"}, {\"id\": \"388\", \"name\": \"蓝锁\"}, {\"id\": \"242\", \"name\": \"JOJO\"}, {\"id\": \"201\", \"name\": \"齐神\"}, {\"id\": \"141\", \"name\": \"网红\"}, {\"id\": \"69\", \"name\": \"血族\"}, {\"id\": \"63\", \"name\": \"骑士与剑\"}, {\"id\": \"188\", \"name\": \"聊斋\"}, {\"id\": \"94\", \"name\": \"银魂\"}, {\"id\": \"345\", \"name\": \"盲盒\"}, {\"id\": \"342\", \"name\": \"交换人生\"}, {\"id\": \"359\", \"name\": \"恶役\"}, {\"id\": \"79\", \"name\": \"七年之痒\"}, {\"id\": \"349\", \"name\": \"锦鲤\"}, {\"id\": \"2\", \"name\": \"死神\"}, {\"id\": \"365\", \"name\": \"御兽\"}, {\"id\": \"10\", \"name\": \"圣斗士\"}, {\"id\": \"353\", \"name\": \"对话体\"}, {\"id\": \"264\", \"name\": \"悲剧\"}, {\"id\": \"80\", \"name\": \"婆媳\"}, {\"id\": \"255\", \"name\": \"亚人\"}, {\"id\": \"7\", \"name\": \"SD\"}, {\"id\": \"84\", \"name\": \"网配\"}, {\"id\": \"341\", \"name\": \"红包群\"}, {\"id\": \"306\", \"name\": \"曲艺\"}, {\"id\": \"386\", \"name\": \"绝区零\"}, {\"id\": \"396\", \"name\": \"倩女游戏\"}]};\r\nconst PAGE_SIZE = 20;\r\nconst optKey = o => o.name + ':' + o.value;\r\nconst FIRST = 0;\r\nconst groupSel = {};\r\nlet currentPage = 1, searched = false;\r\nconst STATE_PREFIX = 'https:\/\/android.jjwxc.net\/search\/getSearchForKeyWords?offset=0&limit=20&bq=';\r\nlet currentTagName = null;\r\n\r\nDATA.groups.forEach(g => {\r\n const chk = g.options.find(o => o.checked);\r\n groupSel[g.label] = new Set([optKey(chk || g.options[FIRST])]);\r\n});\r\nconst topGroups = DATA.groups.filter(g => g.single);\r\nconst drawerGroups = DATA.groups; \r\nconst topSelects = {}; \r\nconst groupChips = {}; \r\n\r\nconst topbar = document.getElementById('topFilters');\nconst topbarDiv = document.createElement(\"div\");\ntopbarDiv.className = \"result-sub\"\r\nconst ordered = [...topGroups].sort((a,b) => (a.label==='排序方式'?0:1) - (b.label==='排序方式'?0:1));\r\nordered.forEach(g => {\r\n const pill = document.createElement('span');\n pill.className = 'pill';\r\n const sel = document.createElement('select');\n \r\n g.options.forEach(o => {\r\n const op = document.createElement('option');\r\n op.value = optKey(o); op.textContent = o.text;\r\n if (groupSel[g.label].has(optKey(o))) op.selected = true;\r\n sel.appendChild(op);\r\n });\r\n sel.onchange = () => { groupSel[g.label] = new Set([sel.value]); syncGroup(g); currentTagName = null; updateTagBtn(); applySearch(); };\r\n pill.appendChild(sel);\n topbarDiv.appendChild(pill)\r\n topSelects[g.label] = sel;\r\n \r\n topbar.appendChild(topbarDiv);\r\n});\n\n\n\nconst resultTitle = document.getElementById(\"resultTitle\");\n\r\nconst tagBtn = document.createElement('span');\r\ntagBtn.className = 'pilltag-btn';\ntagBtn.textContent = '标签';\r\ntagBtn.onclick = () => { renderTagList(); openModal('tagModal'); };\nresultTitle.prepend(tagBtn);\n\n\r\nfunction updateTagBtn(){ tagBtn.textContent = currentTagName || '标签'; }\r\n\n\r\n\r\nconst groupsEl = document.getElementById('drawerGroups');\r\nfunction buildGroupCard(g){\r\n const card = document.createElement('section');\r\n card.className = 'd-card';\r\n const title = document.createElement('div');\r\n title.className = 'd-title';\r\n title.innerHTML = `${g.label} <span class=\"req\" id=\"cnt_${g.label}\"><\/span>`;\r\n card.appendChild(title);\r\n const chips = document.createElement('div');\r\n chips.className = 'd-chips';\r\n g.options.forEach(o => {\r\n const c = document.createElement('span');\r\n c.className = 'd-chip' + (groupSel[g.label].has(optKey(o)) ? ' on' : '');\r\n c.textContent = o.text;\r\n c.onclick = () => {\r\n if (g.single){\r\n groupSel[g.label] = new Set([optKey(o)]);\r\n } else {\r\n groupSel[g.label].delete(optKey(g.options[FIRST]));\r\n if (groupSel[g.label].has(optKey(o))) groupSel[g.label].delete(optKey(o));\r\n else groupSel[g.label].add(optKey(o));\r\n if (groupSel[g.label].size === 0) groupSel[g.label].add(optKey(g.options[FIRST]));\r\n }\r\n syncGroup(g);\r\n updateGroupCount(g);\r\n updateReqUrl();\r\n };\r\n chips.appendChild(c);\r\n });\r\n groupChips[g.label] = [...chips.children];\r\n card.appendChild(chips);\r\n groupsEl.appendChild(card);\r\n updateGroupCount(g);\r\n}\r\n\r\nfunction syncGroup(g){\r\n if (g.single){\r\n const sel = topSelects[g.label];\r\n if (sel) sel.value = [...groupSel[g.label]][0];\r\n }\r\n (groupChips[g.label] || []).forEach((x, idx) => {\r\n const o2 = g.options[idx];\r\n x.classList.toggle('on', groupSel[g.label].has(optKey(o2)));\r\n });\r\n}\r\nfunction updateGroupCount(g){\r\n const el = document.getElementById('cnt_' + g.label);\r\n if (!el) return;\r\n if (g.single){ el.textContent = ''; return; }\r\n const n = groupSel[g.label].size - (groupSel[g.label].has(optKey(g.options[FIRST])) ? 1 : 0);\r\n el.textContent = n > 0 ? `已选 ${n}` : '';\r\n}\r\ndrawerGroups.forEach(buildGroupCard);\r\n\r\n\r\nconst tagState = {};\r\nconst taggrid = document.getElementById('drawerTags');\r\ndocument.getElementById('tagcount').textContent = DATA.tags.length;\r\nDATA.tags.forEach(t => {\r\n tagState[t.id] = 0;\r\n const el = document.createElement('span');\r\n el.className = 'tag'; el.textContent = t.name; el.dataset.id = t.id;\r\n el.onclick = () => setTag(t.id, (tagState[t.id] + 1) % 3);\r\n taggrid.appendChild(el);\r\n});\r\nfunction setTag(id, state){\r\n tagState[id] = state;\r\n const el = taggrid.querySelector(`[data-id=\"${id}\"]`);\r\n el.classList.remove('inc','exc');\r\n if (state === 1) el.classList.add('inc');\r\n if (state === 2) el.classList.add('exc');\r\n updateSummary();\r\n updateReqUrl();\r\n}\r\ndocument.getElementById('toggleTags').onclick = (e) => {\r\n const ex = taggrid.classList.toggle('expanded');\r\n e.target.textContent = ex ? '收起' : '展开全部';\r\n};\r\n\r\nlet incMethod = 'ors', excMethod = 'ors';\r\nfunction bindSeg(id, cb){\r\n const seg = document.getElementById(id);\r\n seg.querySelectorAll('button').forEach(b => {\r\n b.onclick = () => {\r\n seg.querySelectorAll('button').forEach(x => x.classList.remove('on'));\r\n b.classList.add('on'); cb(b.dataset.v);\r\n };\r\n });\r\n}\r\nbindSeg('incSeg', v => { incMethod = v; updateReqUrl(); });\r\nbindSeg('excSeg', v => { excMethod = v; updateReqUrl(); });\r\ndocument.getElementById('kw').addEventListener('input', updateReqUrl);\r\n\r\n\r\nfunction updateSummary(){\r\n const inc = [], exc = [];\r\n DATA.tags.forEach(t => { if (tagState[t.id] === 1) inc.push(t); if (tagState[t.id] === 2) exc.push(t); });\r\n document.getElementById('incCount').textContent = inc.length;\r\n document.getElementById('excCount').textContent = exc.length;\r\n const selHead = document.getElementById('selHead');\r\n selHead.innerHTML = '';\r\n if (inc.length === 0 && exc.length === 0){\r\n selHead.innerHTML = '<div class=\"sel-empty\">尚未选择任何包含 \/ 排除标签<\/div>';\r\n return;\r\n }\r\n if (inc.length) selHead.appendChild(buildSelRow('包含', inc));\r\n if (exc.length) selHead.appendChild(buildSelRow('排除', exc));\r\n}\r\nfunction buildSelRow(label, arr){\r\n const row = document.createElement('div'); row.className = 'sel-row';\r\n const gl = document.createElement('span'); gl.className = 'gl'; gl.textContent = label;\r\n row.appendChild(gl);\r\n const chips = document.createElement('div'); chips.className = 'sel-chips';\r\n arr.forEach(t => {\r\n const c = document.createElement('span'); c.className = 'schip' + (label === '排除' ? ' exc' : '');\r\n c.textContent = t.name; c.title = '点击移除';\r\n c.onclick = () => setTag(t.id, 0);\r\n chips.appendChild(c);\r\n });\r\n row.appendChild(chips);\r\n return row;\r\n}\r\nupdateSummary();\r\n\r\n\/\/ ---------- 构建最终请求 URL(对齐晋江 android 接口) ----------\r\nfunction buildSearchUrl(offset){\r\n const vars = {};\r\n const add = (key, val) => { const k = key.replace(\/\\d+\/g, ''); (vars[k] = vars[k] || []).push(val); };\r\n DATA.groups.forEach(g => g.options.forEach(o => { if (groupSel[g.label].has(optKey(o))) add(o.name, o.value); }));\r\n const inc = [], exc = [];\r\n DATA.tags.forEach(t => { if (tagState[t.id] === 1) inc.push(t.id); if (tagState[t.id] === 2) exc.push(t.id); });\r\n inc.forEach(id => add('bq', id));\r\n exc.forEach(id => add('removebq', id));\r\n add('collectiontypes', incMethod);\r\n add('notlikecollectiontypes', excMethod);\r\n const kw = document.getElementById('kw').value.trim();\r\n if (kw) add('searchkeywords', kw);\r\n for (const k in vars) vars[k] = vars[k].join(',');\r\n const enc = encodeURIComponent;\r\n return 'https:\/\/android.jjwxc.net\/search\/getSearchForKeyWords?offset=' + ((offset|0)) + '&limit=' + PAGE_SIZE\r\n + '&bq=' + enc(vars.bq || '')\r\n + '&removetags=' + enc(vars.removebq || '')\r\n + '¬likecollectionTypes=' + enc(vars.notlikecollectiontypes || '')\r\n + '&fw=' + enc(vars.fw || '')\r\n + '&yc=' + enc(vars.yc || '')\r\n + '&xx=' + enc(vars.xx || '')\r\n + '&sd=' + enc(vars.sd || '')\r\n + '&lx=' + enc(vars.lx || '')\r\n + '&mainview=' + enc(vars.mainview || '')\r\n + '&fbsj=' + enc(vars.fbsj || '')\r\n + '&novelbefavoritedcount=' + enc(vars.novelbefavoritedcount || '')\r\n + '&isfinish=' + enc(vars.isfinish || '')\r\n + '&collectionTypes=' + enc(vars.collectiontypes || '')\r\n + '&searchkeyWords=' + enc(vars.searchkeywords || '')\r\n + '&sortType=' + enc(vars.sorttype || '');\r\n}\r\n\r\n\r\nconst STEP_MAP = { '2': '完结', '1': '连载', '0': '暂停' };\r\nfunction esc(s){ return String(s == null ? '' : s)\n\/\/.replace(\/[&<>\"]\/g, c => ({'&':'&','<':'<','>':'>','\"':'"'}[c])); \n}\r\n\r\n\/\/ 标签随机色:基于标签名做稳定哈希,保证同名标签颜色一致、不会每次渲染都变\r\nfunction tagColor(name){\r\n let h = 0;\r\n for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff;\r\n const hue = Math.abs(h) % 360;\r\n return { bg: `hsl(${hue}, 70%, 90%)`, fg: `hsl(${hue}, 65%, 32%)` };\r\n}\r\n\r\nfunction bookHTML(b, page){\r\n const cover2 = 'https:\/\/i9-static.jjwxc.net\/novelimage.php?novelid=' + b.novelid;\r\n const raw = String(b.cover || '').replace(\/https?\/, 'https').replace(\/_\\d+_\\d+\/, '');\r\n const src = raw || cover2;\r\n const cover = `<img src=\"${esc(src)}\" alt=\"封面\" loading=\"lazy\" referrerpolicy=\"no-referrer\" onerror=\"if(this.src!=='${cover2}')this.src='${cover2}'\">`;\r\n const step = STEP_MAP[String(b.novelstep)] || '未知';\r\n const tags = String(b.tags || '').split(\/\\s+\/).filter(Boolean).map(t => {\r\n const c = tagColor(t);\r\n return `<span style=\"background:${c.bg};color:${c.fg}\">${esc(t)}<\/span>`;\r\n }).join('');\r\n const id = esc(b.novelid || '');\r\n return `<article class=\"book\" data-novelid=\"${id}\" data-page=\"${page || 1}\">\r\n <div class=\"book-cover\">${cover}<\/div>\r\n <div class=\"book-main\">\r\n <div class=\"book-title\" data-novelid=\"${id}\">${esc(b.novelname)}<\/div>\r\n <div class=\"book-meta\">\r\n <span class=\"bk-author\" data-author=\"${esc(b.authorid)}\">${esc(b.authorname)}<\/span> ·\r\n <span class=\"st st-${esc(b.novelstep)}\">${step}<\/span> ·\r\n <span class=\"bk-size\">${esc(b.novelSizeformat)}<\/span>\r\n <\/div>\r\n <div class=\"book-class\" data-class=\"${esc(b.novelClass)}\">${esc(b.novelClass)}<\/div>\r\n <div class=\"book-tags\">${tags}<\/div>\r\n <div class=\"book-intro\">${esc(b.novelintroshort)}<\/div>\r\n <\/div>\r\n <\/article>`;\r\n}\r\n\r\nfunction renderBooks(items){\r\n const listNode = document.getElementById('list');\r\n if (!listNode) return;\r\n if (!items || !items.length){\r\n listNode.innerHTML = '<div class=\"list-empty\">没有符合条件的作品~<\/div>';\r\n showListArea();\r\n return;\r\n }\r\n listNode.innerHTML = pageBlockHTML(items, 1);\r\n showListArea();\r\n setDisplay('resultInfo', true, 'block');\r\n}\r\n\r\n\r\nfunction onAuthorClick(authorId){\r\n java.addBook(`https:\/\/app.jjwxc.org\/androidapi\/authorColumn?authorid=${authorId},{\"origin\":\"https:\/\/m.jjwxc.net\/channel\/\"}`)\r\n}\r\n\r\n\r\nfunction onBookClick(novelId){\n java.addBook(`https:\/\/app-cdn.jjwxc.net\/androidapi\/novelbasicinfo?novelId=${novelId},{\"origin\":\"https:\/\/m.jjwxc.net\/channel\/\"}`)\r\n}\r\n\/\/ 「发现模式」下点击搜索后触发,参数为当前请求的完整 URL(不加载书单)\r\nfunction onDiscovery(url,name){\n url = url.replace(\/offset=\\d+\/,'offset={{(page-1)*20}}');\r\n java.open(\"explore\",url,name??\"搜索结果\",\"https:\/\/m.jjwxc.net\/channel\/\")\r\n}\r\n\r\n\/\/ 书卡点击委托:作者 \/ 书名优先,其余区域算整卡点击\r\ndocument.getElementById('list').addEventListener('click', e => {\r\n const authorEl = e.target.closest('[data-author]');\r\n if (authorEl){ onAuthorClick(authorEl.dataset.author); return; }\r\n const bookEl = e.target.closest('.book');\r\n if (bookEl){ onBookClick(bookEl.dataset.novelid); return; }\r\n});\r\n\r\n\/\/ ---------- 渲染书单 \/ 翻页(按页分块:每页一个 .page-block,窗口连续,页码按块判定) ----------\r\nlet totalCount = 0, totalPages = 0, loading = false;\r\nlet minPage = 0, maxPage = 0; \/\/ 已加载的「连续页窗口」[minPage, maxPage]\r\nlet reachStart = false, reachEnd = false; \/\/ 已确证没有上一页 \/ 下一页\r\nlet adjustingScroll = false; \/\/ 程序化定位期间暂停翻页(时间兜底解锁,定时器被节流也不会永久锁死)\r\nlet lockedUntil = 0; \/\/ 程序化滚动锁的到期时间戳(硬兜底,见 lockScroll)\r\nlet jumpSeq = 0; \/\/ 跳转令牌:新跳转会让旧请求的迟到结果失效\r\nconst pageCache = new Map(); \/\/ page -> items[],避免重复请求同一页\r\nconst listEl = document.getElementById('list');\r\n\r\n\/\/ 每页渲染为一个 section.page-block:定位「第 N 页」和判定当前页都靠它,不再依赖单本书卡\r\nfunction pageBlockHTML(items, page){\r\n return `<section class=\"page-block\" data-page=\"${page}\">` +\r\n items.map(b => bookHTML(b, page)).join('') +\r\n `<\/section>`;\r\n}\r\n\r\n\/\/ ---------- 顶部 \/ 底部「哨兵」:用 IntersectionObserver 判定是否滚到了边缘 ----------\r\n\/\/ 为什么不用 scrollY \/ scrollHeight \/ innerHeight 算?这些量在 WebView、iframe、\r\n\/\/ 内部滚动容器里经常不准;而 scroll 事件也可能被节流甚至漏发(停止滚动后不再补发)。\r\n\/\/ 哨兵元素进入视野就触发加载,不依赖上述任何一项,是最稳的一层。\r\nconst SEN_MARGIN = 500; \/\/ 提前 500px 进入触发区\r\nlet sentinelIO = null;\r\n\r\n\/\/ 确保列表头尾各有一个哨兵(jumpTo \/ loadFirstPage 会整体替换 innerHTML,需要重建)\r\nfunction ensureSentinels(){\r\n let top = document.getElementById('senTop');\r\n if (!top){ top = document.createElement('div'); top.id = 'senTop'; top.className = 'sen'; }\r\n let bot = document.getElementById('senBot');\r\n if (!bot){ bot = document.createElement('div'); bot.id = 'senBot'; bot.className = 'sen'; }\r\n if (listEl.firstChild !== top) listEl.insertBefore(top, listEl.firstChild);\r\n if (listEl.lastChild !== bot) listEl.appendChild(bot);\r\n observeSentinels();\r\n}\r\n\r\nfunction observeSentinels(){\r\n if (typeof IntersectionObserver !== 'function') return;\r\n if (!sentinelIO){\r\n sentinelIO = new IntersectionObserver(entries => {\r\n for (const en of entries){\r\n if (!en.isIntersecting) continue;\r\n if (en.target.id === 'senBot') tryLoadNext('io-bottom');\r\n else tryLoadPrev('io-top');\r\n }\r\n }, { root: null, rootMargin: SEN_MARGIN + 'px 0px ' + SEN_MARGIN + 'px 0px', threshold: 0 });\r\n }\r\n sentinelIO.disconnect();\r\n const t = document.getElementById('senTop'), b = document.getElementById('senBot');\r\n if (t) sentinelIO.observe(t);\r\n if (b) sentinelIO.observe(b);\r\n}\r\n\r\n\/\/ 哨兵现在是否仍处在触发区(用 getBoundingClientRect,和滚动容器无关)\r\nfunction nearEdge(which){\r\n const el = document.getElementById(which === 'bot' ? 'senBot' : 'senTop');\r\n if (!el) return false;\r\n if (!el.getClientRects().length) return false;\r\n const r = el.getBoundingClientRect();\r\n const vh = window.innerHeight || document.documentElement.clientHeight || 0;\r\n if (!vh) return false;\r\n return which === 'bot' ? (r.top <= vh + SEN_MARGIN) : (r.bottom >= -SEN_MARGIN);\r\n}\r\n\r\n\/\/ 统一的两个方向入口:所有触发源(scroll \/ IO \/ 轮询 \/ scrollend)都走这里\r\nfunction tryLoadNext(from){\r\n if (loading || adjustingScroll || !searched) return;\r\n if (reachEnd) return;\r\n if (!listEl.querySelector('.page-block')) return;\r\n dbg('next←' + from);\r\n appendPage(maxPage + 1);\r\n}\r\nfunction tryLoadPrev(from){\r\n if (loading || adjustingScroll || !searched) return;\r\n if (minPage <= 1){ if (minPage === 1) reachStart = true; return; }\r\n if (reachStart) return;\r\n if (!listEl.querySelector('.page-block')) return;\r\n dbg('prev←' + from);\r\n prependPage(minPage - 1);\r\n}\r\n\r\n\/\/ 轻量调试记录(网址加 ?debug=1 时会显示在右下角面板里)\r\nlet dbgLog = [];\r\nfunction dbg(s){\r\n dbgLog.push(String(s));\r\n if (dbgLog.length > 6) dbgLog.shift();\r\n}\r\n\r\n\r\n\/\/ 安全地显示 \/ 隐藏某个节点:节点不存在时静默跳过,绝不抛错\r\nfunction setDisplay(id, on, val){\r\n const el = document.getElementById(id);\r\n if (!el) return null;\r\n el.style.display = on ? (val || 'block') : 'none';\r\n return el;\r\n}\r\n\r\n\/\/ 一次性把「书单区」切到可见状态(节点缺失也不会抛错)\r\nfunction showListArea(){\r\n setDisplay('list', true, 'block');\r\n setDisplay('emptyState', false);\r\n const tools = document.getElementById('tools');\r\n if (tools) tools.classList.add('show');\r\n}\r\n\r\n\r\nfunction ensureNode(id, cls, text, place){\r\n let el = document.getElementById(id);\r\n if (el) return el;\r\n el = document.createElement('div');\r\n el.id = id;\r\n if (cls) el.className = cls;\r\n if (text) el.textContent = text;\r\n const host = (listEl && listEl.parentNode) || document.body;\r\n if (place === 'before' && listEl && listEl.parentNode) host.insertBefore(el, listEl);\r\n else if (listEl && listEl.parentNode) host.insertBefore(el, listEl.nextSibling);\r\n else host.appendChild(el);\r\n console.warn('[jjwxc] 页面缺少 #' + id + ',已在 #list 外侧自动补上');\r\n return el;\r\n}\r\n\r\nfunction setLoadingNext(on){ \/\/ 底部 \/ 下一页加载\r\n ensureNode('loading', 'loading', '加载中…', 'after').style.display = on ? 'flex' : 'none';\r\n}\r\nfunction setLoadingPrev(on){ \/\/ 顶部 \/ 上一页加载\r\n ensureNode('loadPrev', 'loading', '加载上一页…', 'before').style.display = on ? 'flex' : 'none';\r\n}\r\n\r\n\/\/ 非破坏性顶部提示:加载失败时只提示,不清空已加载的书单(避免一次失败就让整个列表消失)\r\nfunction showBanner(msg){\r\n let b = document.getElementById('errBanner');\r\n if (!b){ b = document.createElement('div'); b.id = 'errBanner'; b.className = 'err-banner';\r\n const app = document.querySelector('.app'); (app || document.body).prepend(b); }\r\n b.textContent = msg; b.style.display = 'block';\r\n clearTimeout(b._t); b._t = setTimeout(() => { b.style.display = 'none'; }, 4000);\r\n}\r\n\r\n\/\/ 顶部「共 N 本」信息(居中)\r\nfunction updateResultInfo(){\r\n const el = document.getElementById('resultInfo');\r\n if (!el) return;\r\n el.textContent = totalCount ? `共 ${totalCount} 本` : '';\r\n el.style.display = totalCount ? 'flex' : 'none';\r\n}\r\n\r\n\/\/ 仅更新当前页码指示(右下角 + 输入框),不改 totalPages(未知时显示 ?)\r\nfunction setCurrentPage(page){\r\n currentPage = page;\r\n document.getElementById('navCur').textContent = page;\r\n document.getElementById('navTot').textContent = totalPages || '?';\r\n document.getElementById('totalPages').textContent = totalPages || '?';\r\n const pi = document.getElementById('pageInput');\r\n if (document.activeElement !== pi) pi.value = page;\r\n}\r\n\r\n\/\/ 根据滚动位置判断当前正在看的是第几页:取视口上部一条参考线,落在哪个 .page-block 就是第几页\r\n\/\/ 关键点:参考线落在所有块之外时,兜底取「最后一块」而不是 minPage —— 避免页码在 1 和当前页之间乱跳\r\nfunction updateCurrentPageFromScroll(){\r\n const blocks = listEl.querySelectorAll('.page-block');\r\n if (!blocks.length) return;\r\n const y = window.scrollY || window.pageYOffset || 0;\r\n const ref = y + Math.min(window.innerHeight * 0.35, 220);\r\n let page = null;\r\n for (const b of blocks){\r\n const rect = b.getBoundingClientRect();\r\n const top = rect.top + y, bottom = rect.bottom + y;\r\n if (ref >= top && ref < bottom){ page = parseInt(b.dataset.page, 10); break; }\r\n if (ref < top){ page = parseInt(b.dataset.page, 10); break; } \/\/ 参考线落在该块上方的空隙:归下一块\r\n }\r\n if (page == null) page = parseInt(blocks[blocks.length - 1].dataset.page, 10);\r\n if (page && page !== currentPage) setCurrentPage(page);\r\n}\r\n\r\n\/\/ 统一的数据请求入口:优先用宿主提供的 ajaxAwait(url)(返回 JSON 字符串)\r\n\/\/ 若当前环境没有该函数(例如直接用浏览器打开调试),退回原生 fetch,避免整页无数据\r\nfunction requestText(url){\r\n if (typeof ajaxAwait === 'function') return Promise.resolve(ajaxAwait(url));\r\n return fetch(url).then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); });\r\n}\r\n\r\n\/\/ 真实请求某一页(offset 驱动),并更新总数 \/ 总页数 \/ 抽屉请求地址\r\nfunction fetchPage(page){\r\n const offset = (page - 1) * PAGE_SIZE;\r\n const url = buildSearchUrl(offset);\r\n const reqEl = document.getElementById('reqUrl');\r\n if (reqEl){ reqEl.textContent = url; reqEl.href = url; }\r\n\r\n let req;\r\n try { req = requestText(url); }\r\n catch (e) { return Promise.reject(e); }\r\n\r\n \/\/ 超时兜底:请求一直不返回时也要把 loading 放开,否则整套翻页会永久卡死\r\n let timer;\r\n const timeout = new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('请求超时')), 15000); });\r\n return Promise.race([req, timeout])\r\n .then(res => {\r\n const data = typeof res === 'string' ? JSON.parse(res) : res;\r\n const items = Array.isArray(data.items) ? data.items : [];\r\n \/\/ 空结果不覆盖已有总数\/总页数:接口对越界 offset 会返回 total=0、total_page=0,\r\n \/\/ 若让它覆盖,顶部「共 N 本」和总页数会被一次性冲掉\r\n if (items.length){\r\n if (data.total != null && data.total > 0) totalCount = data.total;\r\n else if (data.count != null) totalCount = data.count;\r\n if (data.total_page != null && data.total_page > 0) totalPages = data.total_page;\r\n else if (totalCount) totalPages = Math.ceil(totalCount \/ PAGE_SIZE);\r\n }\r\n updateResultInfo();\r\n return items;\r\n })\r\n .finally(() => clearTimeout(timer));\r\n}\r\n\r\n\/\/ 初次搜索:加载第 1 页并铺满首屏\r\nfunction loadFirstPage(){\r\n loading = true; searched = true; setLoadingNext(true);\r\n jumpSeq++; \/\/ 让仍在飞行中的旧跳转结果失效\r\n let okRender = false;\r\n fetchPage(1)\r\n .then(items => {\r\n items = items || [];\r\n if (!items.length){\r\n listEl.innerHTML = '<div class=\"list-empty\">没有符合条件的作品~<\/div>';\r\n showListArea();\r\n minPage = maxPage = 0; reachStart = reachEnd = true; \/\/ 确证空结果\r\n return;\r\n }\r\n pageCache.set(1, items);\r\n listEl.innerHTML = pageBlockHTML(items, 1);\r\n ensureSentinels(); \/\/ 重建头尾哨兵(innerHTML 被整体替换后会丢失)\r\n showListArea();\r\n minPage = maxPage = 1;\r\n reachStart = true; reachEnd = false;\r\n setCurrentPage(1);\r\n lockScroll();\r\n window.scrollTo(0, 0);\r\n unlockScroll(260); \/\/ 解锁:锁定期内若有滚动,会补跑一次\r\n dbg('first-page');\r\n updateCurrentPageFromScroll();\r\n okRender = true;\r\n })\r\n .catch(err => { showBanner('加载失败:' + err.message + '(可能需登录签名或存在跨域限制)'); })\r\n .finally(() => {\r\n setLoadingNext(false); loading = false;\r\n if (okRender) ensureFill(); \/\/ 状态复位后再补充,否则会被 ensureFill 的 loading 守卫挡掉\r\n });\r\n}\r\n\r\n\/\/ 方向边界标记:置位后 6 秒自动解除,允许重试\r\n\/\/ 接口对越界\/异常 offset 也可能返回空,若一次空结果就永久锁死,表现就是「跳转后滚动再也不加载」\r\nlet endTimer = 0, startTimer = 0;\r\nfunction markEnd(){\r\n reachEnd = true;\r\n clearTimeout(endTimer);\r\n endTimer = setTimeout(() => { reachEnd = false; }, 6000);\r\n}\r\nfunction markStart(){\r\n reachStart = true;\r\n clearTimeout(startTimer);\r\n startTimer = setTimeout(() => { reachStart = false; }, 6000);\r\n}\r\n\r\n\/\/ 下一页:以 .page-block 追加在列表最下方(插到尾部哨兵之前),不滚动、不打断阅读\r\nfunction appendPage(page){\r\n if (loading || page <= maxPage || reachEnd) return Promise.resolve(false);\r\n const cached = pageCache.get(page);\r\n const run = (cached && cached.length)\r\n ? Promise.resolve(cached)\r\n : fetchPage(page).then(items => { if (items && items.length) pageCache.set(page, items); return items; });\r\n loading = true; setLoadingNext(true);\r\n let added = false;\r\n return run.then(items => {\r\n items = items || [];\r\n if (!items.length){ \/\/ 空页:不写缓存,只做「暂时到底」,6 秒后可重试\r\n pageCache.delete(page);\r\n markEnd();\r\n dbg('next-empty:' + page);\r\n return false;\r\n }\r\n const html = pageBlockHTML(items, page);\r\n const bot = document.getElementById('senBot');\r\n if (bot && bot.parentNode === listEl) bot.insertAdjacentHTML('beforebegin', html);\r\n else listEl.insertAdjacentHTML('beforeend', html);\r\n maxPage = Math.max(maxPage, page);\r\n clearTimeout(endTimer); reachEnd = false;\r\n dbg('next+:' + page + '(' + items.length + ')');\r\n if (!adjustingScroll) updateCurrentPageFromScroll();\r\n added = true;\r\n return true;\r\n }).catch(err => { showBanner('下一页加载失败:' + err.message); markEnd(); return false; })\r\n .finally(() => {\r\n setLoadingNext(false); loading = false;\r\n if (added) ensureFill(); \/\/ 短页(如末页只有几条)继续向下铺满\r\n });\r\n}\r\n\r\n\/\/ 上一页:以 .page-block 插入列表最上方(插到头部哨兵之后),插入后修正滚动位置,视口内容不跳动\r\nfunction prependPage(page){\r\n if (loading || page >= minPage || page < 1 || reachStart) return Promise.resolve(false);\r\n const cached = pageCache.get(page);\r\n const run = (cached && cached.length)\r\n ? Promise.resolve(cached)\r\n : fetchPage(page).then(items => { if (items && items.length) pageCache.set(page, items); return items; });\r\n loading = true; setLoadingPrev(true);\r\n return run.then(items => {\r\n items = items || [];\r\n if (!items.length){ pageCache.delete(page); markStart(); dbg('prev-empty:' + page); return false; }\r\n \/\/ 视口锚定:记录插入前第一块的位置,插入后用位置差修正滚动,不依赖 scrollHeight\r\n const firstBlock = listEl.querySelector('.page-block');\r\n const y0 = window.scrollY || window.pageYOffset || 0;\r\n const before = firstBlock ? firstBlock.getBoundingClientRect().top : 0;\r\n const tpl = document.createElement('template');\r\n tpl.innerHTML = pageBlockHTML(items, page);\r\n const topSen = document.getElementById('senTop');\r\n const ref = (topSen && topSen.parentNode === listEl) ? topSen.nextSibling : listEl.firstChild;\r\n listEl.insertBefore(tpl.content, ref);\r\n minPage = minPage ? Math.min(minPage, page) : page;\r\n if (firstBlock){\r\n const delta = firstBlock.getBoundingClientRect().top - before;\r\n if (delta) { lockScroll(); window.scrollTo(0, Math.max(0, y0 + delta)); unlockScroll(260); }\r\n }\r\n clearTimeout(startTimer); reachStart = false;\r\n if (page <= 1) markStart();\r\n dbg('prev+:' + page + '(' + items.length + ')');\r\n return true;\r\n }).catch(err => { showBanner('上一页加载失败:' + err.message); markStart(); return false; })\r\n .finally(() => { setLoadingPrev(false); loading = false; });\r\n}\r\n\r\n\/\/ 向下补充:最后一块的底部还没离开「视口 + 缓冲」时,继续加载下一页(铺满首屏 \/ 触底续接)\r\n\/\/ 用 getBoundingClientRect 判定,不用 scrollHeight(WebView\/iframe 里经常不准)\r\nfunction ensureFill(){\r\n if (loading || reachEnd) return;\r\n const blocks = listEl.querySelectorAll('.page-block');\r\n if (!blocks.length || blocks.length > 60) return; \/\/ 上限保护:页面高度异常时不无限串行请求\r\n const last = blocks[blocks.length - 1];\r\n const vh = window.innerHeight || document.documentElement.clientHeight || 0;\r\n if (!vh) return;\r\n if (last.getBoundingClientRect().bottom <= vh + 600) appendPage(maxPage + 1);\r\n}\r\n\r\n\r\nfunction jumpTo(page){\r\n page = parseInt(page, 10);\r\n if (isNaN(page) || page < 1) return;\r\n\r\n const block = listEl.querySelector('.page-block[data-page=\"' + page + '\"]');\r\n if (block){\r\n scrollToBlock(page);\r\n reachEnd = false; \/\/ 已加载页附近仍可能补充下一页\r\n if (page <= 1) reachStart = true;\r\n setCurrentPage(page);\r\n dbg('jump-cached:' + page);\r\n return;\r\n }\r\n\r\n const seq = ++jumpSeq;\r\n const want = [page - 1, page, page + 1].filter(p => p >= 1);\r\n const oldHTML = listEl.innerHTML, oldMin = minPage, oldMax = maxPage;\r\n const oldRS = reachStart, oldRE = reachEnd;\r\n let okRender = false;\r\n\r\n loading = true; searched = true; setLoadingNext(true);\r\n Promise.all(want.map(p =>\r\n pageCache.has(p) ? Promise.resolve(pageCache.get(p))\r\n : fetchPage(p).then(items => {\r\n if (items && items.length) pageCache.set(p, items); \/\/ 空页不写缓存,避免之后滚动直接读到空数组\r\n return items || [];\r\n })\r\n ))\r\n .then(results => {\r\n if (seq !== jumpSeq) return; \/\/ 已被更新的跳转取代,丢弃本次结果\r\n let html = '', got = 0, mn = Infinity, mx = -Infinity;\r\n want.forEach((p, i) => {\r\n const items = results[i] || [];\r\n if (!items.length) return;\r\n html += pageBlockHTML(items, p);\r\n got += items.length;\r\n mn = Math.min(mn, p); mx = Math.max(mx, p);\r\n });\r\n if (!got){ \/\/ 目标页无数据:保留原列表,仅提示\r\n listEl.innerHTML = oldHTML;\r\n ensureSentinels();\r\n minPage = oldMin; maxPage = oldMax; reachStart = oldRS; reachEnd = oldRE;\r\n showBanner('第 ' + page + ' 页没有数据');\r\n dbg('jump-empty:' + page);\r\n return;\r\n }\r\n listEl.innerHTML = html;\r\n ensureSentinels(); \/\/ 重建头尾哨兵\r\n showListArea();\r\n minPage = mn; maxPage = mx;\r\n reachStart = (mn <= 1);\r\n reachEnd = false; \/\/ 跳转后一律允许两侧继续滚动加载(真实末页由空结果判定)\r\n setCurrentPage(page);\r\n scrollToBlock(page);\r\n updateCurrentPageFromScroll();\r\n dbg('jump:' + page + ' [' + mn + '-' + mx + ']');\r\n okRender = true;\r\n })\r\n .catch(err => { \/\/ 失败:回滚到跳转前的列表\r\n if (seq !== jumpSeq) return;\r\n listEl.innerHTML = oldHTML;\r\n ensureSentinels();\r\n minPage = oldMin; maxPage = oldMax; reachStart = oldRS; reachEnd = oldRE;\r\n showBanner('跳转失败:' + err.message);\r\n })\r\n .finally(() => {\r\n if (seq !== jumpSeq) return; \/\/ 已被更新的跳转接管,不要动 loading 状态\r\n setLoadingNext(false); loading = false;\r\n if (okRender) ensureFill(); \/\/ 复位后再补充,避免被 loading 守卫挡掉\r\n });\r\n}\r\n\r\n\r\nlet searchMode = String(source.get(\"mode\"))==\"discovery\"?\"discovery\":'web'; \n \r\ndocument.getElementById('modeSwitch').addEventListener('click', e => {\r\n const btn = e.target.closest('[data-mode]');\r\n if (!btn) return;\r\n searchMode = btn.dataset.mode;\n source.put(\"mode\",searchMode)\r\n document.querySelectorAll('#modeSwitch button').forEach(b => b.classList.toggle('on', b.dataset.mode === searchMode));\r\n});\r\n\r\n\r\nfunction updateReqUrl(){\r\n const url = buildSearchUrl(0);\r\n const reqEl = document.getElementById('reqUrl');\r\n if (reqEl){ reqEl.textContent = url; reqEl.href = url; }\r\n}\r\n\r\nfunction applySearch(name){\r\n updateReqUrl();\r\n closeDrawer();\r\n const url = buildSearchUrl(0);\r\n if (searchMode === 'discovery'){\r\n \/\/ 发现模式:不加载书单,把当前请求地址交给你的自定义函数 onDiscovery(url)\r\n onDiscovery(url,name);\r\n return;\r\n }\r\n loadFirstPage(); \/\/ 网页模式:主页面加载数据(从第 1 页开始累加)\r\n}\r\n\r\n\/\/ ---------- 抽屉 \/ 遮罩 ----------\r\nconst overlay = document.getElementById('overlay');\r\nconst drawer = document.getElementById('drawer');\r\nfunction openDrawer(){ drawer.classList.add('open'); overlay.classList.add('show'); }\r\nfunction closeDrawer(){ drawer.classList.remove('open'); overlay.classList.remove('show'); }\r\ndocument.getElementById('fabFilter').onclick = openDrawer;\r\ndocument.getElementById('drawerClose').onclick = closeDrawer;\r\noverlay.onclick = closeDrawer;\r\n\r\n\/\/ ---------- 抽屉底部按钮 ----------\r\ndocument.getElementById('drawerApply').onclick = () => { currentTagName = null; updateTagBtn(); applySearch(); };\r\n\/\/ ---------- 状态重置 \/ 刷新 ----------\r\nfunction resetStateValues(){\r\n DATA.groups.forEach(g => {\r\n const chk = g.options.find(o => o.checked);\r\n groupSel[g.label] = new Set([optKey(chk || g.options[FIRST])]);\r\n });\r\n DATA.tags.forEach(t => { tagState[t.id] = 0; });\r\n document.querySelectorAll('#drawerTags .tag').forEach(el => el.classList.remove('inc','exc'));\r\n document.getElementById('kw').value = '';\r\n incMethod = 'ors'; excMethod = 'ors';\r\n}\r\nfunction syncSegUI(){\r\n document.querySelectorAll('#incSeg button').forEach(b => b.classList.toggle('on', b.dataset.v === incMethod));\r\n document.querySelectorAll('#excSeg button').forEach(b => b.classList.toggle('on', b.dataset.v === excMethod));\r\n}\r\nfunction refreshUI(){\r\n groupsEl.innerHTML = ''; drawerGroups.forEach(buildGroupCard);\r\n topGroups.forEach(g => { const sel = topSelects[g.label]; if (sel) sel.value = [...groupSel[g.label]][0]; });\r\n syncSegUI();\r\n updateSummary();\r\n}\r\ndocument.getElementById('drawerReset').onclick = () => { resetStateValues(); refreshUI(); updateReqUrl(); };\r\n\r\n\/\/ ---------- Base64 状态编码 \/ 还原(公共前缀 + 仅余下部分编码) ----------\r\nfunction getCurrentToken(){\r\n const full = buildSearchUrl();\r\n const prefix = 'https:\/\/android.jjwxc.net\/search\/getSearchForKeyWords?offset=0&limit=20&bq=';\r\n const rem = full.indexOf(prefix) === 0 ? full.slice(prefix.length) : full;\r\n return btoa(rem);\r\n}\r\nfunction setTagRaw(id, state){\r\n tagState[id] = state;\r\n const el = taggrid.querySelector(`[data-id=\"${id}\"]`);\r\n if (el){ el.classList.remove('inc','exc'); if (state === 1) el.classList.add('inc'); if (state === 2) el.classList.add('exc'); }\r\n}\r\nfunction restoreState(token){\r\n let rem;\r\n try { rem = atob(token); } catch(e){ alert('保存信息已损坏,无法还原'); return false; }\r\n const p = new URLSearchParams('bq=' + rem);\r\n resetStateValues();\r\n (p.get('bq') || '').split(',').forEach(id => { if (id) setTagRaw(id, 1); });\r\n (p.get('removetags') || '').split(',').forEach(id => { if (id) setTagRaw(id, 2); });\r\n incMethod = p.get('collectionTypes') === 'ands' ? 'ands' : 'ors';\r\n excMethod = p.get('notlikecollectionTypes') === 'ands' ? 'ands' : 'ors';\r\n document.getElementById('kw').value = p.get('searchkeyWords') || '';\r\n DATA.groups.forEach(g => {\r\n const urlKey = g.options[0].name === 'sorttype' ? 'sortType' : g.options[0].name.replace(\/\\d+\/g, '');\r\n const raw = p.get(urlKey);\r\n if (raw === null) return;\r\n const set = new Set();\r\n raw.split(',').forEach(val => { const op = g.options.find(x => String(x.value) === val); if (op) set.add(optKey(op)); });\r\n if (set.size === 0) set.add(optKey(g.options[FIRST]));\r\n groupSel[g.label] = set;\r\n });\r\n refreshUI();\r\n updateReqUrl();\r\n return true;\r\n}\r\n\r\n\/\/ ---------- 标签(保存筛选)本地存储(localStorage 不可用时降级到内存) ----------\r\nconst LS_KEY = 'jjwxc_saved_tags';\r\nlet _memTags = null;\r\n\r\nconst LS_DEF = 'jjwxc_default_tag';\r\nlet _memDefault = null;\r\nfunction loadTags(){\r\n try {\n let raw = String(source.getVariable()); \n if(raw.trim().match(\/#$\/)){\n let r = [];\n let tagList = raw.replace(\/#$\/,'').split(\"#\");\n tagList.forEach((x,i)=>{\n let tag = x.split(\"@\");\n r.push({\n id:i,\n name:tag[0],\n token:tag[1]\n });\n cache.put(\"saveTag\",JSON.stringify(r))\n source.putVariable(JSON.stringify(r));\n return r\n })\n }\n \n if (raw.includes(\"{\"))\n return JSON.parse(raw); \n \n } catch(e){} \r\n return _memTags ? _memTags : [];\r\n}\n\n\n\r\nfunction saveTags(arr){\r\n _memTags = arr.slice();\r\n try { \n cache.put(\"saveTag\",JSON.stringify(arr));\n source.putVariable(JSON.stringify(arr)); \n } catch(e){\n } \r\n}\n\r\nfunction loadDefaultId(){\n \r\n try { return String(source.get(LS_DEF)); } catch(e){\n }\r\n return _memDefault;\r\n}\r\nfunction saveDefaultId(id){\r\n _memDefault = String(id);\r\n try { if (id) source.put(LS_DEF, _memDefault); else source.put(LS_DEF,\"\"); } catch(e){}\r\n}\r\nfunction decodeToken(token){ try { return STATE_PREFIX + atob(token); } catch(e){ return '(解码失败)'; } }\r\nfunction openModal(id){ document.getElementById(id).classList.add('show'); }\r\nfunction closeModal(id){ document.getElementById(id).classList.remove('show'); }\r\n\r\n\/\/ 名称输入弹窗(保存 \/ 重命名共用)\r\nlet nameCb = null;\r\nfunction askName(title, defaultVal, cb){\r\n document.getElementById('nameTitle').textContent = title;\r\n document.getElementById('nameInput').value = defaultVal || '';\r\n nameCb = cb;\r\n openModal('nameModal');\r\n setTimeout(() => document.getElementById('nameInput').focus(), 260);\r\n}\r\ndocument.getElementById('nameCancel').onclick = () => { nameCb = null; closeModal('nameModal'); };\r\ndocument.getElementById('nameOk').onclick = () => {\r\n const v = document.getElementById('nameInput').value.trim() || '未命名';\r\n const cb = nameCb; nameCb = null; closeModal('nameModal');\r\n if (cb){\n cb(v);\n java.toast(\"保存成功\")\n } \r\n};\r\ndocument.getElementById('nameInput').addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('nameOk').click(); });\r\n\r\n\/\/ 保存当前筛选\r\ndocument.getElementById('drawerSave').onclick = () => {\r\n const token = getCurrentToken();\r\n askName('保存筛选', '', name => {\r\n const arr = loadTags();\r\n arr.push({ id: Date.now(), name, token });\r\n saveTags(arr);\r\n });\r\n};\r\n\r\n\/\/ 标签管理弹窗\r\nlet tagEditMode = false;\r\nfunction renderTagList(){\r\n const arr = loadTags();\r\n const list = document.getElementById('tagList');\r\n list.innerHTML = '';\r\n setDisplay('tagEmpty', !arr.length, 'block');\r\n const defId = loadDefaultId();\r\n arr.forEach((t, i) => {\r\n const pill = document.createElement('div'); pill.className = 'tag-pill';\r\n const no = document.createElement('span'); no.className = 'tp-no'; no.textContent = (i + 1);\r\n const name = document.createElement('span'); name.className = 'tp-name';\r\n name.textContent = t.name;\r\n if (!tagEditMode){\r\n name.title = '点击应用此筛选';\r\n name.onclick = () => {\r\n if (restoreState(t.token)) { currentTagName = t.name; updateTagBtn(); closeModal('tagModal'); applySearch(currentTagName); }\r\n };\r\n }\r\n pill.appendChild(no); pill.appendChild(name);\r\n if (tagEditMode){\r\n const edit = document.createElement('button'); edit.textContent = '✎'; edit.title = '重命名';\r\n edit.onclick = () => askName('重命名', t.name, newName => {\r\n const a = loadTags(); const it = a.find(x => x.id === t.id); if (it) it.name = newName; saveTags(a); renderTagList();\r\n });\r\n const del = document.createElement('button'); del.className = 'del'; del.textContent = '×'; del.title = '删除';\r\n del.onclick = () => openDelConfirm(t, i);\r\n const def = document.createElement('button'); def.className = 'def' + (String(t.id) === String(defId) ? ' on' : '');\r\n def.textContent = (String(t.id) === String(defId)) ? '默认' : '设默认';\r\n def.title = '设为初始加载';\r\n def.onclick = () => { saveDefaultId(String(t.id) === String(defId) ? null : t.id); renderTagList(); };\r\n pill.appendChild(edit); pill.appendChild(del); pill.appendChild(def);\r\n }\r\n list.appendChild(pill);\r\n });\r\n}\r\nlet pendingDel = null;\r\nfunction openDelConfirm(t, i){\r\n pendingDel = t.id;\r\n document.getElementById('cfNo').textContent = (i + 1);\r\n document.getElementById('cfName').textContent = t.name;\r\n document.getElementById('cfUrl').textContent = decodeToken(t.token);\r\n openModal('delConfirmModal');\r\n}\r\ndocument.getElementById('cfCancel').onclick = () => { pendingDel = null; closeModal('delConfirmModal'); };\r\ndocument.getElementById('cfOk').onclick = () => {\r\n if (pendingDel !== null){\r\n const a = loadTags().filter(x => x.id !== pendingDel); saveTags(a);\r\n if (String(loadDefaultId()) === String(pendingDel)) saveDefaultId(null);\r\n }\r\n pendingDel = null; closeModal('delConfirmModal'); renderTagList();\r\n};\r\ndocument.getElementById('drawerTagsBtn').onclick = () => { renderTagList(); openModal('tagModal'); };\r\ndocument.getElementById('tagModalClose').onclick = () => closeModal('tagModal');\r\ndocument.getElementById('tagEditBtn').onclick = () => {\r\n tagEditMode = !tagEditMode;\r\n const b = document.getElementById('tagEditBtn');\r\n b.classList.toggle('on', tagEditMode);\r\n b.textContent = tagEditMode ? '完成' : '编辑';\r\n renderTagList();\r\n};\r\ndocument.querySelectorAll('.modal').forEach(m => m.addEventListener('click', e => { if (e.target === m) m.classList.remove('show'); }));\r\n\r\n\/\/ 启动时:若有默认标签,则将其筛选作为初始主页面加载内容\r\n(function initDefault(){\r\n const defId = loadDefaultId();\r\n if (defId){\r\n const t = loadTags().find(x => String(x.id) === String(defId));\r\n if (t && restoreState(t.token)){ currentTagName = t.name; applySearch(); }\r\n }\r\n updateTagBtn();\r\n updateReqUrl(); \/\/ 初始即展示当前筛选对应的请求地址\r\n})();\r\n\ndocument.querySelectorAll('#modeSwitch button').forEach(b => b.classList.toggle('on', b.dataset.mode === String(source.get(\"mode\"))));\n\r\n\/\/ ---------- 右下工具按钮动作 ----------\r\ndocument.getElementById('pageNav').onclick = (e) => {\r\n e.stopPropagation();\r\n document.getElementById('tools').classList.toggle('expanded');\r\n};\r\ndocument.getElementById('tools').addEventListener('click', e => {\r\n const btn = e.target.closest('[data-act]');\r\n if (!btn) return;\r\n const act = btn.dataset.act;\r\n if (act === 'top') window.scrollTo({top:0,behavior:'smooth'});\r\n else if (act === 'bottom') window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'});\r\n else if (act === 'jump'){\r\n const v = parseInt(document.getElementById('pageInput').value, 10);\r\n if (!isNaN(v) && v >= 1) jumpTo(v);\r\n }\r\n});\r\n\r\ndocument.addEventListener('click', e => {\r\n const tools = document.getElementById('tools');\r\n if (tools && !tools.contains(e.target)) tools.classList.remove('expanded');\r\n});\r\ndocument.getElementById('pageInput').addEventListener('keydown', e => {\r\n if (e.key === 'Enter'){ const v = parseInt(e.target.value,10); if(!isNaN(v)&&v>=1) jumpTo(v); }\r\n});\r\n\r\n\r\nlet lastScrollAt = 0;\r\nlet scrollDuringLock = false; \r\n\r\nfunction lockScroll(){ adjustingScroll = true; lockedUntil = Date.now() + 800; }\r\n\r\n\r\nfunction unlockScroll(delay){\r\n setTimeout(() => {\r\n if (!adjustingScroll) return;\r\n adjustingScroll = false; lockedUntil = 0;\r\n scrollDuringLock = false;\r\n recheckScroll();\r\n }, delay);\r\n}\r\n\r\nfunction scrollToBlock(page){\r\n const b = listEl.querySelector('.page-block[data-page=\"' + page + '\"]');\r\n if (!b){ window.scrollTo(0, 0); return; }\r\n const y = window.scrollY || window.pageYOffset || 0;\r\n const top = b.getBoundingClientRect().top + y;\r\n lockScroll();\r\n window.scrollTo(0, Math.max(0, top - 52));\r\n unlockScroll(260);\r\n}\r\n\r\nfunction onScrollFrame(){\r\n if (!searched) return;\r\n try { updateCurrentPageFromScroll(); } catch (e) { }\r\n if (loading) return;\r\n if (nearEdge('bot')) tryLoadNext('scroll');\r\n else if (nearEdge('top')) tryLoadPrev('scroll');\r\n}\r\n\r\n\r\nfunction recheckScroll(){\r\n if (loading || adjustingScroll || !searched) return;\r\n onScrollFrame();\r\n}\r\n\r\nfunction handleScroll(){\r\n if (adjustingScroll){\r\n if (Date.now() < lockedUntil){ scrollDuringLock = true; return; }\r\n adjustingScroll = false;\r\n }\r\n const now = Date.now();\r\n if (now - lastScrollAt < 50) return;\r\n lastScrollAt = now;\r\n onScrollFrame();\r\n}\r\nwindow.addEventListener('scroll', handleScroll, { passive: true });\r\ndocument.addEventListener('scroll', handleScroll, { passive: true, capture: true }); \r\n['scrollend', 'touchend', 'touchcancel', 'mouseup'].forEach(ev =>\r\n window.addEventListener(ev, () => { setTimeout(recheckScroll, 60); }, { passive: true }));\r\n\r\nsetInterval(() => {\r\n if (document.visibilityState !== 'visible') return;\r\n recheckScroll();\r\n}, 1200);\r\n\r\n\r\n",
"startStyle": "\r\n :root{\r\n --accent:#7F7770;\r\n --accent-dark:#000;\r\n --accent-tint:#F3F1EB;\r\n --ink:#2a2e33;\r\n --muted:#9aa0a6;\r\n --line:#eeeeee;\r\n --bg:#f6f7f6;\r\n --card:#ffffff;\r\n }\r\n *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\r\n html,body{margin:0;padding:0;}\r\n body{\r\n font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",sans-serif;\r\n background:var(--bg); color:var(--ink); font-size:13px; line-height:1.5;\r\n -webkit-font-smoothing:antialiased;\r\n }\r\n .app{max-width:480px;margin:0 auto;min-height:100vh;position:relative;background:var(--bg);\r\n box-shadow:0 0 0 1px var(--line);}\r\n\r\n \/* ---------- 顶部常驻筛选栏(药丸下拉,居中占满一行) ---------- *\/\n.topbar {\n position: sticky;\n top: 0;\n z-index: 15;\n background: rgba(255,255,255,.96);\n backdrop-filter: saturate(180%) blur(8px);\n border-bottom: 1px solid var(--line);\n display: flex;\n flex-direction: column;\n padding: 8px 2px;\n}\n\n\/* 第1排 *\/\n.result-title {\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n padding:0px\n}\n\n.result-info {\n font-size: 11.5px;\n color: var(--muted);\n margin-left:10px\n}\n\nspan.pilltag-btn{\n font-size:18px;\n font-weight:bolder;\n color:var(--accent)\n}\n\n\/* 第2排 *\/\n.result-sub {\n display: flex;\n justify-content: center; \/* 水平居中 *\/\n align-items: center;\n width: 100%;\n margin-top: 4px; \/* 跟第1排留点间距,可选 *\/\n}\n\n\r\n .pill{flex:1 1;display:inline-flex;align-items:center;min-width:0;justify-content:center;margin:8px}\r\n .pill select{\n appearance:none;-moz-appearance: none;-webkit-appearance:none;border:none;outline:none;cursor:pointer;\n width:100%;\n background:#F3F1EB;\r\n color:var(--accent);font-size:12.5px;font-weight:600;border-radius:999px;text-align:center;text-align-last:center;\r\n padding:7px 24px;}\r\n .pill select:focus{box-shadow:0 0 0 2px var(--accent)}\r\n .pill-btn{flex:1 1;width:100%;border:none;cursor:pointer;background:var(--accent-tint);color:var(--accent);\r\n font-size:12.5px;font-weight:600;border-radius:999px;padding:7px 14px;}\r\n .pill-btn:active{background:var(--accent);}\r\n\r\n \/* ---------- 书单 ---------- *\/\r\n #list{display:none;padding:6px 12px 96px;}\r\n \/* 每页一个分块:无额外间距;滚动定位时避让吸顶筛选栏 *\/\r\n .page-block{display:block;margin:0;padding:0;scroll-margin-top:58px;}\r\n \/* 列表头尾哨兵:只给 IntersectionObserver 判定用,不占视觉高度 *\/\r\n .sen{height:1px;width:100%;margin:0;padding:0;pointer-events:none;}\r\n .book{display:flex;gap:10px;background:var(--card);border:1px solid var(--line);\r\n border-radius:11px;padding:11px;margin-bottom:9px;cursor:pointer;}\r\n .book-cover{width:25vw;max-width:120px;border-radius:7px;overflow:hidden;\r\n display:flex;align-items:center;justify-content:center;flex-shrink:0;}\r\n .book-cover img{\n border-radius:8px;\r\n border: 1px solid #C4C4C4;\r\n border-radius: 5px;\r\n box-shadow: 1px 2px 2px black;\r\n width: 97%;\r\n height: auto;\r\n object-fit:cover;display:block;\n min-height:120px\n }\r\n .book-cover-ph{width:100%;height:100%;min-height:72px;display:flex;align-items:center;justify-content:center;}\r\n .book-main{flex:1;min-width:0;}\r\n .book-title{font-size:14px;font-weight:600;line-height:1.3;margin-bottom:3px;\r\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink);}\r\n .book-meta{font-size:11px;color:var(--muted);margin-bottom:3px;}\r\n .book-meta .st{font-weight:600;}\r\n .book-meta .st-2{color:#C62828;} \/* 完结:红色调 *\/\r\n .book-meta .st-1{color:#2E7D32;} \/* 连载:绿色调 *\/\r\n .book-meta .st-0{color:#9e9e9e;} \/* 暂停:灰色 *\/\r\n .book-meta .bk-author{color:#1565C0;cursor:pointer;} \/* 作者:蓝色,可点击 *\/\r\n .book-meta .bk-author:hover{text-decoration:underline;}\r\n .book-meta .bk-size{color:#E67E22;font-weight:600;} \/* 字数:橙色调,加粗区分 *\/\r\n .book-class{font-size:11px;color:#8E44AD;margin-bottom:4px;} \/* 分类:紫色,单独成行 *\/\r\n .book-intro{font-size:11.5px;color:#8a9099;line-height:1.45;display:-webkit-box; \/* 简介:灰 *\/\r\n -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}\r\n .list-empty{text-align:center;color:var(--muted);padding:50px 20px;font-size:13px;}\r\n\r\n \/* ---------- 加载指示 ---------- *\/\r\n .loading{display:none;align-items:center;justify-content:center;gap:8px;\r\n padding:14px;color:var(--accent);font-size:12.5px;}\r\n .loading::before{content:\"\";width:15px;height:15px;border:2px solid var(--accent-tint);\r\n border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite;}\r\n .err-banner{display:none;position:sticky;top:0;z-index:40;background:#fdecea;color:#c62828;\r\n font-size:12px;padding:9px 14px;text-align:center;border-bottom:1px solid #f5c6c0;}\r\n @keyframes spin{to{transform:rotate(360deg);}}\r\n .req-url{color:var(--accent);text-decoration:underline;word-break:break-all;cursor:pointer;}\r\n .book-tags{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:5px;}\r\n .book-tags span{font-size:10.5px;color:var(--accent);background:var(--accent-tint);\r\n padding:1px 7px;border-radius:999px;}\r\n .book-intro{font-size:11.5px;color:#7a8088;line-height:1.45;display:-webkit-box;\r\n -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}\r\n\r\n \/* ---------- 空状态 ---------- *\/\r\n #emptyState{display:flex;flex-direction:column;align-items:center;justify-content:center;\r\n padding:80px 30px;color:var(--muted);text-align:center;gap:10px;}\r\n #emptyState .em-ico{width:54px;height:54px;border-radius:16px;background:var(--accent-tint);\r\n color:var(--accent);display:flex;align-items:center;justify-content:center;font-size:26px;}\r\n #emptyState p{margin:0;font-size:13px;line-height:1.6;}\r\n\r\n \/* ---------- 悬浮按钮(左下:筛选抽屉) ---------- *\/\r\n .fab{position:fixed;bottom:18px;left:max(12px,calc(50vw - 240px + 12px));z-index:18;\r\n width:46px;height:46px;border-radius:50%;border:none;background:var(--accent);\r\n color:#fff;font-size:19px;box-shadow:0 4px 8px #000);cursor:pointer;\r\n display:flex;align-items:center;justify-content:center;}\r\n \r\n \/* ---------- 右下工具按钮组(先显页码,点击展开) ---------- *\/\r\n .tools{position:fixed;right:max(12px,calc(50vw - 240px + 12px));bottom:18px;z-index:18;\r\n display:none;flex-direction:column;align-items:center;gap:8px;}\r\n .tools.show{display:flex;}\r\n .pagenav{width:48px;height:48px;border-radius:50%;border:none;cursor:pointer;background:#fff;\r\n box-shadow:0 3px 10px rgba(0,0,0,.14);display:flex;flex-direction:column;align-items:center;\r\n justify-content:center;line-height:1.15;padding:0;}\r\n .pagenav .cur{font-size:16px;font-weight:700;color:var(--accent);}\r\n .pagenav .tot{font-size:10px;font-weight:400;color:var(--muted);border-top:1px solid #e6e6e6;\r\n margin-top:2px;padding-top:2px;width:64%;text-align:center;}\r\n .pagenav:active{background:var(--accent-tint);}\r\n .tools.expanded .pagenav{display:none;}\r\n .tools-extra{display:none;flex-direction:column;align-items:center;gap:8px;}\r\n .tools.expanded .tools-extra{display:flex;}\r\n .rbtn{width:42px;height:42px;border-radius:50%;border:none;cursor:pointer;\r\n background:#fff;color:var(--accent);font-size:17px;font-weight:700;line-height:1;\r\n box-shadow:0 3px 10px rgba(0,0,0,.14);display:flex;align-items:center;justify-content:center;}\r\n .rbtn:active{background:var(--accent-tint);}\r\n .rbtn.go{background:var(--accent);color:#fff;font-size:12.5px;letter-spacing:.5px;}\r\n .rbtn.go:active{background:var(--accent-dark);}\r\n .pagepill{display:flex;flex-direction:column;align-items:center;background:#fff;border-radius:14px;\r\n box-shadow:0 3px 10px rgba(0,0,0,.12);padding:5px 4px 4px;min-width:44px;}\r\n .pagepill input{width:36px;border:none;background:transparent;text-align:center;outline:none;\r\n font-size:15px;font-weight:700;color:var(--accent);-moz-appearance:textfield;padding:0;}\r\n .pagepill input::-webkit-outer-spin-button,.pagepill input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0;}\r\n .pagepill .tt{font-size:10px;color:var(--muted);border-top:1px solid var(--line);\r\n align-self:stretch;text-align:center;padding-top:2px;}\r\n\r\n \/* ---------- 抽屉底部四按钮 ---------- *\/\r\n .btn-foot{flex:1;padding:11px;border-radius:10px;font-size:13px;cursor:pointer;border:1px solid var(--line);background:#fff;color:var(--muted);}\r\n .btn-foot:active{background:var(--bg);}\r\n\r\n \/* ---------- 弹窗(名称 \/ 标签管理) ---------- *\/\r\n .modal{position:fixed;inset:0;z-index:40;background:rgba(0,0,0,.35);opacity:0;visibility:hidden;transition:opacity .2s;}\r\n .modal.show{opacity:1;visibility:visible;}\r\n .modal-card{position:absolute;left:50%;bottom:0;width:100%;max-width:480px;transform:translateX(-50%) translateY(100%);\r\n background:#fff;border-top-left-radius:16px;border-top-right-radius:16px;padding:16px 16px calc(16px + env(safe-area-inset-bottom));\r\n transition:transform .26s ease;box-shadow:0 -4px 20px rgba(0,0,0,.12);}\r\n .modal.show .modal-card{transform:translateX(-50%) translateY(0);}\r\n #nameModal{z-index:60;}\r\n .modal-title{font-size:15px;font-weight:700;margin-bottom:12px;}\r\n .modal-head{display:flex;align-items:center;justify-content:space-between;font-size:15px;font-weight:700;margin-bottom:12px;}\r\n .modal-actions{display:flex;gap:8px;margin-top:14px;}\r\n .modal-actions button{flex:1;padding:11px;border-radius:10px;font-size:14px;cursor:pointer;border:1px solid transparent;}\r\n .tag-list{max-height:50vh;overflow:auto;display:flex;flex-wrap:wrap;gap:8px;align-content:flex-start;}\r\n .tag-pill{display:inline-flex;align-items:center;gap:3px;background:var(--accent-tint);color:var(--accent);\r\n border-radius:999px;padding:5px 5px 5px 8px;max-width:100%;}\r\n .tag-pill .tp-no{font-size:11px;font-weight:700;color:#fff;background:var(--accent);border-radius:50%;\r\n width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}\r\n .tag-pill .tp-name{font-size:13px;cursor:pointer;padding:3px 6px;border-radius:999px;white-space:nowrap;\r\n overflow:hidden;text-overflow:ellipsis;max-width:150px;}\r\n .tag-pill .tp-name:active{background:rgba(46,125,50,.16);}\r\n .tag-pill button{border:none;background:transparent;cursor:pointer;color:var(--accent);font-size:13px;line-height:1;\r\n width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;}\r\n .tag-pill button:hover{background:rgba(46,125,50,.12);}\r\n .tag-pill button.del{color:#c0392b;}\r\n .tag-pill button.del.armed{background:#c0392b;color:#fff;}\r\n .tag-pill button.def{color:var(--accent);font-size:11px;width:auto;padding:0 8px;border-radius:999px;}\r\n .tag-pill button.def.on{background:var(--accent);color:#fff;}\r\n .mini-btn{border:none;cursor:pointer;background:var(--accent-tint);color:var(--accent);font-size:12px;font-weight:600;\r\n border-radius:999px;padding:5px 12px;flex-shrink:0;}\r\n .mini-btn.on{background:var(--accent);color:#fff;}\r\n .cf-line{display:flex;gap:8px;font-size:13px;margin-bottom:8px;line-height:1.5;}\r\n .cf-line .k{color:var(--muted);flex-shrink:0;width:40px;}\r\n .cf-line .v{color:var(--ink);word-break:break-all;}\r\n .cf-line .v.url{font-size:11px;color:var(--muted);}\r\n .modal-empty{font-size:13px;color:var(--muted);text-align:center;padding:30px 0;}\r\n\r\n \/* ---------- 遮罩 ---------- *\/\r\n .overlay{position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:25;opacity:0;\r\n visibility:hidden;transition:opacity .22s;}\r\n .overlay.show{opacity:1;visibility:visible;}\r\n\r\n \/* ---------- 左侧抽屉 ---------- *\/\r\n .drawer{position:fixed;top:0;left:0;height:100%;width:84%;max-width:340px;background:var(--bg);\r\n z-index:30;transform:translateX(-100%);transition:transform .26s ease;display:flex;flex-direction:column;}\r\n .drawer.open{transform:translateX(0);}\r\n .drawer-head{padding:14px 16px 10px;border-bottom:1px solid var(--line);\r\n display:flex;align-items:center;justify-content:space-between;background:#fff;position:relative;}\r\n .drawer-head h2{margin:0;font-size:15px;font-weight:700;}\r\n .drawer-close{border:none;background:transparent;font-size:22px;color:var(--muted);cursor:pointer;line-height:1;}\r\n \/* 抽屉标题行居中的模式切换:网页模式 \/ 发现模式 *\/\r\n .mode-switch{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);\r\n display:flex;background:#eef2f0;border-radius:999px;padding:3px;gap:2px;}\r\n .mode-switch button{border:none;background:transparent;font-size:11.5px;color:var(--muted);\r\n padding:4px 11px;border-radius:999px;cursor:pointer;white-space:nowrap;transition:all .15s;}\r\n .mode-switch button.on{background:#fff;color:var(--accent);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}\r\n .drawer-body{flex:1;overflow:auto;padding:12px 14px 16px;-webkit-overflow-scrolling:touch;}\r\n .drawer-foot{display:flex;gap:8px;padding:10px 14px;background:#fff;border-top:1px solid var(--line);}\r\n .drawer-foot button{flex:1;padding:11px;border-radius:10px;font-size:14px;cursor:pointer;border:1px solid transparent;}\r\n .btn-apply{background:var(--accent);color:#fff;font-weight:600;}\r\n .btn-apply:active{background:var(--accent-dark);}\r\n .btn-reset{background:#fff;border-color:var(--line);color:var(--muted);}\r\n\r\n \/* 抽屉内:卡片\/分组\/标签 紧凑样式 *\/\r\n .d-card{background:#fff;border:1px solid var(--line);border-radius:11px;padding:12px;margin-bottom:11px;}\r\n .d-card > .d-title{font-size:12.5px;font-weight:600;margin:0 0 10px;display:flex;align-items:center;gap:6px;}\r\n .d-card > .d-title .req{color:var(--muted);font-weight:400;font-size:11px;}\r\n .kw-input{width:100%;padding:9px 11px;font-size:14px;border:1px solid var(--line);border-radius:9px;outline:none;}\r\n .kw-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-tint);}\r\n .sub-desc{font-size:11px;color:var(--muted);margin:-4px 0 10px;}\r\n\r\n .d-chips{display:flex;flex-wrap:wrap;gap:6px;}\r\n .d-chip{border:1px solid var(--line);background:#fff;color:var(--ink);padding:6px 11px;border-radius:8px;\r\n font-size:12px;cursor:pointer;user-select:none;transition:all .12s;}\r\n .d-chip:hover{border-color:#d8d8d8;}\r\n .d-chip.on{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600;}\r\n\r\n .sel-head{border:1px solid var(--line);background:#fbfcfb;border-radius:9px;padding:8px 10px;margin-bottom:10px;\r\n display:flex;flex-direction:column;gap:6px;max-height:96px;overflow:auto;-webkit-overflow-scrolling:touch;}\r\n .sel-empty{font-size:11px;color:var(--muted);}\r\n .sel-row{display:flex;align-items:flex-start;gap:7px;}\r\n .sel-row .gl{font-size:11px;font-weight:600;color:var(--accent);flex-shrink:0;width:28px;line-height:20px;}\r\n .sel-chips{display:flex;gap:5px;flex-wrap:wrap;}\r\n .schip{font-size:11px;padding:2px 9px;border-radius:999px;cursor:pointer;user-select:none;background:var(--accent);\r\n color:#fff;font-weight:600;border:1px solid var(--accent);display:inline-flex;align-items:center;gap:3px;}\r\n .schip.exc{background:#fff;color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);}\r\n .schip::after{content:\"×\";opacity:.65;font-size:10px;}\r\n .schip:hover{opacity:.85;}\r\n\r\n .legend{display:flex;gap:12px;font-size:11px;color:var(--muted);margin-bottom:10px;align-items:center;flex-wrap:wrap;}\r\n .legend .sw{display:inline-block;width:12px;height:12px;border-radius:99px;vertical-align:-2px;margin-right:5px;}\r\n .legend .sw.i{background:var(--accent);}\r\n .legend .sw.e{background:#fff;border:1.5px solid var(--accent);}\r\n .legend .tip{margin-left:auto;}\r\n\r\n .methods{display:flex;flex-wrap:wrap;gap:16px;margin-bottom:12px;}\r\n .method{display:flex;align-items:center;gap:8px;}\r\n .mlabel{font-size:12px;color:var(--muted);}\r\n .seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;}\r\n .seg button{border:none;background:#fff;color:var(--muted);padding:5px 13px;font-size:12px;cursor:pointer;}\r\n .seg button + button{border-left:1px solid var(--line);}\r\n .seg button.on{background:var(--accent);color:#fff;font-weight:600;}\r\n\r\n .taggrid{display:flex;flex-wrap:wrap;gap:6px;max-height:300px;overflow:auto;padding-right:2px;}\r\n .taggrid.expanded{max-height:none;}\r\n .tag{border:1px solid var(--line);background:#fff;color:var(--ink);padding:6px 10px;border-radius:8px;\r\n font-size:11.5px;cursor:pointer;user-select:none;transition:all .12s;}\r\n .tag:hover{border-color:#d8d8d8;}\r\n .tag.inc{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600;}\r\n .tag.exc{background:#fff;border-color:var(--accent);color:var(--accent);font-weight:600;box-shadow:inset 0 0 0 1px var(--accent);}\r\n\r\n .req-details{margin-top:4px;font-size:11px;color:var(--muted);}\r\n .req-details summary{cursor:pointer;color:var(--accent);}\r\n .req-details code{display:block;margin-top:6px;background:#0f1115;color:#d6e9d6;border-radius:8px;\r\n padding:8px;font-size:10.5px;word-break:break-all;white-space:pre-wrap;}\r\n",
"type": 0
}