// ==UserScript==
// @name 骨碌碌手机沉浸阅读套件 V1.94
// @namespace Violentmonkey Scripts
// @match *://gululu.world/book/*
// @match *://www.gululu.world/book/*
// @match *://gululu.world/
// @match *://www.gululu.world/
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @version 1.94
// @author 无限悾涧
// @description 手机阅读完美适配:沉浸式阅读 / 楼层&段落评论 / 氛围背景 / 音乐控制 / 骰子解析 / 快捷菜单 / 流量节省。需配合主脚本使用。
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// 0. 启动配置检测
// ==========================================
let config = {};
try {
config = JSON.parse(localStorage.getItem('gululu_global_config_v6') || '{}');
} catch (e) {}
// 容错:防止缩放系数异常导致界面崩溃
if (!config.mobileScale || isNaN(config.mobileScale) || config.mobileScale < 0.1) {
config.mobileScale = 1.0;
}
if (!config.mobileModeEnabled) return;
// ⚡ 全屏自动缩放逻辑 (全局生效 + 跨页面继承)
let preFullscreenScale = null;
const updateScaleUI = () => {
document.documentElement.style.setProperty('--gm-scale', config.mobileScale);
const scaleText = `UI缩放: ${Math.round(config.mobileScale * 100)}%`;
document.querySelectorAll('#gm-act-scale span, #gm-menu-scale').forEach(el => {
if(el.id === 'gm-menu-scale') el.innerText = scaleText;
else el.innerText = scaleText;
});
};
const handleFullscreenChange = () => {
const isFull = !!document.fullscreenElement;
const scales = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5];
if (isFull) {
// 进入全屏:如果之前没保存过比例,则保存当前比例
if (preFullscreenScale === null) {
preFullscreenScale = config.mobileScale;
}
// 计算目标比例 (降2级,例如 1.0 -> 0.5)
let idx = scales.indexOf(preFullscreenScale);
if (idx === -1) idx = 3;
let newIdx = Math.max(0, idx - 2);
// 应用新比例
config.mobileScale = scales[newIdx];
// 更新按钮文本
document.querySelectorAll('#gm-menu-fullscreen, #gm-act-fullscreen span').forEach(el => {
if(el.tagName === 'SPAN') el.innerText = '退出全屏';
else el.innerText = '退出全屏 ✕';
});
} else {
// 退出全屏:恢复比例
if (preFullscreenScale !== null) {
config.mobileScale = preFullscreenScale;
preFullscreenScale = null;
}
// 恢复按钮文本
document.querySelectorAll('#gm-menu-fullscreen, #gm-act-fullscreen span').forEach(el => {
if(el.tagName === 'SPAN') el.innerText = '全屏模式';
else el.innerText = '全屏模式 ⛶';
});
}
updateScaleUI();
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
// 🚀 启动时恢复逻辑 (实现全屏继承)
// 如果 sessionStorage 标记了全屏,但当前未全屏,则监听第一次交互来恢复
if (sessionStorage.getItem('gm_is_fullscreen') === '1') {
if (document.fullscreenElement) {
handleFullscreenChange(); // 已经是全屏 (如刷新),直接应用缩放
} else {
// 等待用户第一次点击/触摸时恢复全屏
const restoreFs = () => {
document.documentElement.requestFullscreen().catch(()=>{});
document.removeEventListener('click', restoreFs);
document.removeEventListener('touchend', restoreFs); // 改为 touchend 兼容性更好
};
document.addEventListener('click', restoreFs);
document.addEventListener('touchend', restoreFs);
}
}
console.log("📱 [Mobile Reforged] v4.5 启动...");
// ==========================================
// 0.5 流量节省拦截器
// ==========================================
const originalFetch = window.fetch;
window.fetch = function(url, options) {
if (typeof url === 'string' && url.includes('paragraph-comment-count')) {
return Promise.resolve(new Response(JSON.stringify({
code: 200,
data: {}
}), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
}));
}
return originalFetch.apply(this, arguments);
};
// ==========================================
// 1. 样式系统
// ==========================================
const appStyle = `
/* 核心:隐藏原网页 */
#__next {
opacity: 0 !important;
pointer-events: none !important;
position: fixed !important;
z-index: -9999 !important;
}
html, body {
background: #f5f6fa !important;
overflow: hidden !important;
overscroll-behavior: none !important;
touch-action: none;
}
/* 手机版主容器 */
#g-mobile-app {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: transparent;
z-index: 999999;
display: flex; flex-direction: column;
font-family: -apple-system, "Microsoft YaHei", sans-serif;
overscroll-behavior: none;
}
/* 背景图层 */
#gm-bg-layer {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 999000;
background-size: cover; background-position: center;
background-repeat: no-repeat;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease-in-out;
filter: brightness(0.9);
}
#gm-bg-backdrop {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 998000;
background: rgba(255, 255, 255, 0.3);
pointer-events: none;
}
/* 顶部导航 */
.gm-navbar {
height: 140px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(0,0,0,0.05); display: flex; align-items: center; justify-content: space-between; padding: 0 30px; flex-shrink: 0;
}
.gm-nav-title { font-weight: 800; font-size: 45px; color: #333; max-width: 80%; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
.gm-nav-btn { font-size: 50px; padding: 20px; color: #666; cursor: pointer; min-width: 60px; text-align: center; }
/* 内容区域 */
.gm-content { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 20px; scroll-behavior: smooth; -webkit-overflow-scrolling: touch; overscroll-behavior: none; }
/* 楼层卡片 */
.gm-floor {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(5px);
border-radius: 24px; padding: 35px; margin-bottom: 25px;
box-shadow: 0 4px 16px rgba(0,0,0,0.05); border: 1px solid rgba(255,255,255,0.4);
transition: background 0.3s;
}
.gm-floor-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid rgba(0,0,0,0.05); }
.gm-user-info { display: flex; align-items: center; gap: 25px; }
.gm-avatar { width: 100px; height: 100px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.5); object-fit: cover; }
.gm-username { font-size: 36px; font-weight: 700; color: #2d3436; }
.gm-floor-meta { font-size: 28px; color: #636e72; margin-top: 8px; }
.gm-floor-right { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
.gm-floor-id { font-size: 30px; color: #b2bec3; font-weight: bold; }
.gm-floor-cmt-btn {
font-size: 34px; color: #6c5ce7; background: rgba(108, 92, 231, 0.1);
padding: 15px 35px; border-radius: 40px; cursor: pointer; font-weight: 700;
}
.gm-floor-cmt-btn:active { background: rgba(108, 92, 231, 0.2); transform: scale(0.95); }
.gm-floor-body { font-size: 42px; line-height: 1.6; color: #2d3436; word-break: break-word; }
.gm-floor-body img { width: 100%; height: auto; border-radius: 16px; margin: 20px 0; display: block; }
.gm-floor-body p { margin-bottom: 25px; min-height: 40px; }
.gm-floor-body h1, .gm-floor-body h2, .gm-floor-body h3 { margin: 30px 0; font-weight: 800; line-height: 1.4; color: #2d3436; }
.gm-floor-body h1 { font-size: 1.4em; }
.gm-floor-body h2 { font-size: 1.25em; }
.gm-floor-body h3 { font-size: 1.1em; }
/* 插件适配 */
.g-dice-mask, .g-dice-revealed { font-size: 38px !important; height: auto !important; line-height: 1.5 !important; padding: 4px 12px !important; border-radius: 12px !important; min-width: 80px !important; margin: 0 6px !important; border-width: 2px !important; }
.g-dice-mask::after { font-size: 32px !important; }
.g-music-key { font-size: 36px !important; padding: 16px 40px !important; border-radius: 60px !important; margin: 15px 0 !important; height: auto !important; max-width: 90% !important; box-shadow: 0 6px 20px rgba(108, 92, 231, 0.3) !important; }
.g-music-key::before { font-size: 30px !important; margin-right: 16px !important; }
.g-fold-controller { font-size: 36px !important; padding: 20px 30px !important; margin: 20px 0 !important; border-radius: 16px !important; border-width: 2px !important; }
.g-folded-content { padding-left: 30px !important; border-left-width: 6px !important; margin-left: 15px !important; }
.g-secret-chest, .g-clue-key { font-size: 34px !important; padding: 16px 24px !important; border-radius: 16px !important; border-width: 3px !important; margin: 10px 0 !important; }
.g-quote-box { padding: 20px 30px !important; border-left-width: 8px !important; margin: 20px 0 !important; }
.g-quote-content { font-size: 36px !important; }
.g-quote-icon { font-size: 60px !important; top: -25px !important; }
.g-quote-info { font-size: 28px !important; margin-top: 10px !important; }
.gm-loader { padding: 40px; text-align: center; color: #999; font-size: 30px; display: none; justify-content: center; align-items: center; }
.gm-loader.active { display: flex; }
.gm-loader::after { content:""; width:40px; height:40px; border:4px solid #ddd; border-top-color:#888; border-radius:50%; margin-left:20px; animation:gm-spin 0.8s linear infinite; }
@keyframes gm-spin { to { transform: rotate(360deg); } }
/* --- 🌙 手机版夜间模式 (深度适配) --- */
body.g-night-mode, body.g-night-mode html { background-color: #121212 !important; }
body.g-night-mode #g-mobile-app { background: transparent; }
body.g-night-mode .gm-navbar { background: rgba(30, 30, 30, 0.95); border-color: rgba(255,255,255,0.1); }
body.g-night-mode .gm-nav-title { color: #dfe6e9; }
body.g-night-mode .gm-nav-btn { color: #b2bec3; }
body.g-night-mode .gm-floor { background: rgba(35, 35, 35, 0.85); box-shadow: none; border: 1px solid rgba(255,255,255,0.08); }
body.g-night-mode .gm-floor-header { border-color: rgba(255,255,255,0.1); }
body.g-night-mode .gm-username { color: #dfe6e9; }
body.g-night-mode .gm-floor-meta { color: #636e72; }
body.g-night-mode .gm-floor-body, body.g-night-mode .gm-floor-body p { color: #b2bec3 !important; }
/* 降低图片亮度,保护眼睛 */
body.g-night-mode .gm-floor-body img { filter: brightness(0.7); transition: filter 0.3s; }
/* 组件适配 */
body.g-night-mode .g-dice-mask { background-color: #2d3436 !important; border-color: #555 !important; }
body.g-night-mode .g-fold-controller { background: #2d2d2d !important; border-color: #555 !important; color: #ddd !important; }
body.g-night-mode .g-quote-box { background: #2d3436 !important; }
body.g-night-mode .g-quote-content { color: #b2bec3 !important; }
/* 选中文字颜色 */
body.g-night-mode ::selection { background: #fdcb6e !important; color: #000 !important; }
/* === 评论系统 === */
.gm-comment-btn {
display: none; /* 默认隐藏 */
position: relative; width: 54px; height: 40px;
margin-left: 2px; cursor: pointer;
vertical-align: -5px;
transition: all 0.2s; -webkit-tap-highlight-color: transparent;
}
.gm-comment-btn svg { width: 100%; height: 100%; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.1)); }
.gm-comment-btn svg path { stroke: #999; stroke-width: 1.2; }
.gm-comment-num { position: absolute; top: 70%; left: 50%; transform: translate(-50%, -50%); font-size: 20px; color: #999; font-weight: bold; font-family: sans-serif; }
.gm-comment-btn:active { transform: scale(1.2); }
.gm-comment-btn.has-comment { display: inline-block; opacity: 1; }
.g-show-comments .gm-comment-btn { display: inline-block !important; }
.g-show-comments .gm-comment-btn:not(.has-comment) { opacity: 0.4; filter: grayscale(1); }
#gm-comment-toggle {
position: fixed; bottom: 170px; right: 20px; width: 80px; height: 80px;
background: rgba(255,255,255,0.95); border-radius: 50%;
box-shadow: 0 4px 16px rgba(0,0,0,0.2); border: 2px solid #dcdde1;
text-align: center; line-height: 80px; cursor: pointer; z-index: 1000001;
font-size: 36px; transition: all 0.2s; backdrop-filter: blur(5px);
}
#gm-comment-toggle:active { transform: scale(0.9); background: #eee; }
#gm-comment-toggle.active { background: #6c5ce7; color: white; border-color: #6c5ce7; box-shadow: 0 4px 20px rgba(108, 92, 231, 0.4); }
#gm-home-btn {
position: fixed; bottom: 70px; right: 20px; width: 80px; height: 80px;
background: rgba(255,255,255,0.95); border-radius: 50%;
box-shadow: 0 4px 16px rgba(0,0,0,0.2); border: 2px solid #dcdde1;
text-align: center; line-height: 80px; cursor: pointer; z-index: 1000001;
font-size: 36px; transition: all 0.2s; backdrop-filter: blur(5px);
}
#gm-home-btn:active { transform: scale(0.9); background: #eee; }
body.g-night-mode #gm-comment-toggle, body.g-night-mode #gm-home-btn { background: rgba(50,50,50,0.9); color: #ccc; border-color: #555; }
body.g-night-mode #gm-comment-toggle.active { background: #6c5ce7; color: white; }
/* === 快捷菜单 === */
#gm-menu-toggle {
position: fixed; bottom: 270px; right: 20px; width: 80px; height: 80px;
background: rgba(255,255,255,0.95); border-radius: 50%;
box-shadow: 0 4px 16px rgba(0,0,0,0.2); border: 2px solid #dcdde1;
text-align: center; line-height: 80px; cursor: pointer; z-index: 1000001;
font-size: 36px; transition: all 0.2s; backdrop-filter: blur(5px);
}
#gm-menu-toggle:active { transform: scale(0.9); background: #eee; }
#gm-menu-toggle.active { transform: rotate(45deg); background: #636e72; color: white; border-color: #636e72; }
.gm-menu-list {
position: fixed; bottom: 370px; right: 20px;
display: flex; flex-direction: column; gap: 20px;
z-index: 1000001; pointer-events: none;
opacity: 0; transform: translateY(20px) scale(0.9); transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform-origin: bottom right;
}
.gm-menu-list.active { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.gm-menu-item {
background: rgba(255,255,255,0.95); padding: 15px 30px; border-radius: 40px;
box-shadow: 0 4px 16px rgba(0,0,0,0.15); font-size: 28px; font-weight: bold;
color: #333; text-align: right; cursor: pointer; border: 1px solid #eee;
display: flex; align-items: center; justify-content: flex-end; gap: 15px;
backdrop-filter: blur(5px);
}
.gm-menu-item:active { transform: scale(0.95); background: #f0f0f0; }
body.g-night-mode #gm-menu-toggle { background: rgba(50,50,50,0.9); color: #ccc; border-color: #555; }
body.g-night-mode #gm-menu-toggle.active { background: #636e72; color: white; }
body.g-night-mode .gm-menu-item { background: rgba(50,50,50,0.95); color: #ccc; border-color: #555; }
/* === 评论抽屉修复 === */
#gm-cmt-drawer {
position: fixed; bottom: 0; left: 0; width: 100%; height: 70vh;
background: #fff; border-radius: 40px 40px 0 0;
/* 提升层级,高于悬浮按钮(1000001) */
z-index: 1000010;
transform: translateY(110%); transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
display: flex; flex-direction: column; box-shadow: 0 -10px 40px rgba(0,0,0,0.1);
}
#gm-cmt-drawer.active { transform: translateY(0); }
#gm-cmt-mask {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
/* 提升遮罩层级,防止误触按钮 */
background: rgba(0,0,0,0.5); z-index: 1000005;
opacity: 0; pointer-events: none; transition: opacity 0.3s;
}
#gm-cmt-mask.active { opacity: 1; pointer-events: auto; }
/* 布局保护:防止 flex 子元素溢出或被挤压 */
.gm-cmt-header {
padding: 25px; font-size: 34px; font-weight: bold;
border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center;
flex-shrink: 0; /* 防止被挤压 */
}
.gm-cmt-close-btn {
color: #999; font-weight: normal; cursor: pointer;
padding: 10px; margin: -10px; /* 增加点击区域 */
font-size: 40px; line-height: 1;
}
.gm-cmt-list {
flex: 1; overflow-y: auto; padding: 25px;
-webkit-overflow-scrolling: touch;
min-height: 0; /* ⚡ 关键:允许 flex 子元素收缩,产生滚动条 */
}
.gm-cmt-item { display: flex; gap: 20px; padding: 25px 0; border-bottom: 1px solid #f5f5f5; }
.gm-cmt-avatar { width: 90px; height: 90px; border-radius: 50%; object-fit: cover; }
.gm-cmt-content { flex: 1; font-size: 34px; line-height: 1.5; color: #333; }
.gm-cmt-meta { font-size: 26px; color: #999; margin-top: 12px; display: flex; justify-content: space-between; align-items: center; }
.gm-cmt-like-btn {
padding: 10px 0 10px 30px; /* 左侧留空,防止误触 */
font-size: 56px; /* 进一步增大图标 */
cursor: pointer;
transition: transform 0.2s, color 0.2s;
user-select: none;
color: #b2bec3;
display: flex; align-items: center; justify-content: center;
align-self: flex-start; /* 顶部对齐 */
flex-shrink: 0; /* 防止被挤压 */
}
.gm-cmt-like-btn:active { transform: scale(1.2); }
.gm-cmt-like-btn.liked { color: #ff4757; }
body.g-night-mode #gm-cmt-drawer { background: #1e1e1e; border-top: 1px solid #333; }
body.g-night-mode .gm-cmt-header { border-color: #333; color: #eee; }
body.g-night-mode .gm-cmt-content { color: #ccc; }
body.g-night-mode .gm-cmt-item { border-color: #333; }
/* 跳转按钮 - 金色版 */
.gm-jump-btn {
display: block; width: 100%; padding: 30px;
margin: 40px 0; border-radius: 24px;
background: linear-gradient(135deg, #f1c40f 0%, #f39c12 100%);
border: 4px solid #e67e22;
color: white;
font-size: 40px; font-weight: 800; text-align: center;
box-shadow: 0 10px 25px rgba(243, 156, 18, 0.4), inset 0 2px 0 rgba(255,255,255,0.2);
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
text-shadow: 0 2px 4px rgba(0,0,0,0.15);
letter-spacing: 2px;
}
.gm-jump-btn:active {
transform: scale(0.96) translateY(2px);
box-shadow: 0 4px 12px rgba(243, 156, 18, 0.3);
filter: brightness(0.95);
border-color: #d35400;
}
body.g-night-mode .gm-jump-btn {
background: linear-gradient(135deg, #b7791f 0%, #d35400 100%);
border-color: #a0522d;
color: #f1f2f6;
box-shadow: 0 10px 25px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.1);
}
.gm-cmt-footer {
padding: 20px; border-top: 1px solid #eee;
display: flex; flex-direction: column; background: #fff;
padding-bottom: calc(20px + env(safe-area-inset-bottom));
flex-shrink: 0;
}
.gm-cmt-input-area { display: flex; gap: 20px; align-items: flex-end; }
.gm-reply-status {
font-size: 26px; color: #666; background: #f0f2f5;
padding: 10px 20px; margin-bottom: 15px; border-radius: 10px;
display: none; justify-content: space-between; align-items: center;
}
.gm-reply-status.active { display: flex; }
.gm-reply-cancel { color: #ff4757; font-weight: bold; cursor: pointer; padding: 5px 15px; }
.gm-cmt-actions { display: flex; align-items: center; gap: 20px; }
.gm-cmt-reply-btn { font-size: 28px; color: #666; font-weight: bold; cursor: pointer; padding: 10px 20px; background: #f5f6fa; border-radius: 30px; }
.gm-cmt-reply-btn:active { background: #e1e1e1; }
.gm-sub-cmt-list { margin-left: 110px; margin-top: 20px; background: #f9f9f9; border-radius: 16px; padding: 0 20px; display: none; }
.gm-sub-cmt-list.active { display: block; }
.gm-sub-cmt-item { padding: 20px 0; border-bottom: 1px solid #eee; font-size: 30px; color: #333; }
.gm-sub-cmt-item:last-child { border-bottom: none; }
.gm-sub-expand-btn { margin-left: 110px; margin-top: 15px; font-size: 26px; color: #6c5ce7; font-weight: bold; cursor: pointer; }
/* 夜间模式子评论适配 */
body.g-night-mode .gm-sub-cmt-list { background: #252525; border: 1px solid #333; }
body.g-night-mode .gm-sub-cmt-item { border-color: #333; color: #ccc; }
body.g-night-mode .gm-sub-cmt-item b { color: #eee; } /* 昵称高亮 */
body.g-night-mode .gm-sub-content { color: #b2bec3; } /* 内容颜色 */
body.g-night-mode .gm-reply-status { background: #333; color: #ccc; }
body.g-night-mode .gm-cmt-reply-btn { background: #333; color: #aaa; }
.gm-cmt-input {
flex: 1; background: #f5f6fa; border: none;
padding: 20px 30px; border-radius: 40px;
font-size: 32px; outline: none; color: #333;
resize: none; overflow-y: hidden; min-height: 80px; max-height: 300px;
font-family: inherit; line-height: 1.4;
}
.gm-cmt-send {
background: #6c5ce7; color: white; border: none;
padding: 0 40px; border-radius: 40px; height: 80px;
font-size: 32px; font-weight: bold; cursor: pointer;
}
.gm-cmt-send:active { transform: scale(0.95); }
body.g-night-mode .gm-cmt-footer { background: #1e1e1e; border-color: #333; }
body.g-night-mode .gm-cmt-input { background: #333; color: #eee; }
/* === 底部标签栏 (App TabBar) === */
.gm-tab-bar {
position: fixed; bottom: 0; left: 0; width: 100%; height: 120px;
background: rgba(255,255,255,0.98); border-top: 1px solid rgba(0,0,0,0.1);
display: flex; justify-content: space-around; align-items: center;
padding-bottom: env(safe-area-inset-bottom);
z-index: 1000000; backdrop-filter: blur(10px);
box-shadow: 0 -4px 20px rgba(0,0,0,0.05);
}
.gm-tab-item {
display: flex; flex-direction: column; align-items: center; justify-content: center;
flex: 1; height: 100%; color: #999;
cursor: pointer; -webkit-tap-highlight-color: transparent;
transition: transform 0.1s;
}
.gm-tab-item:active { transform: scale(0.95); }
.gm-tab-item.active { color: #6c5ce7; }
.gm-tab-icon { width: 48px; height: 48px; margin-bottom: 8px; fill: currentColor; }
.gm-tab-label { font-size: 22px; font-weight: 600; }
/* Tab 内容容器 */
.gm-tab-content {
display: none; width: 100%; height: 100%; padding: 40px; box-sizing: border-box;
font-size: 32px; color: #666; animation: gm-fade-in 0.3s ease;
}
.gm-tab-content.active { display: block; }
@keyframes gm-fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* 夜间模式适配 */
body.g-night-mode .gm-tab-bar { background: rgba(30,30,30,0.98); border-color: rgba(255,255,255,0.1); }
body.g-night-mode .gm-tab-item { color: #636e72; }
body.g-night-mode .gm-tab-item.active { color: #a29bfe; }
body.g-night-mode .gm-tab-content { color: #ccc; }
/* === 自定义模态框 === */
.gm-modal-mask {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); z-index: 1000005;
display: flex; justify-content: center; align-items: center;
opacity: 0; pointer-events: none; transition: opacity 0.2s;
backdrop-filter: blur(5px);
}
.gm-modal-mask.active { opacity: 1; pointer-events: auto; }
.gm-modal {
background: #fff; width: 80%; max-width: 600px;
border-radius: 32px; padding: 50px;
text-align: center; box-shadow: 0 10px 40px rgba(0,0,0,0.2);
transform: scale(0.9); transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.gm-modal-mask.active .gm-modal { transform: scale(1); }
.gm-modal-title { font-size: 40px; font-weight: 800; margin-bottom: 20px; color: #333; }
.gm-modal-desc { font-size: 32px; color: #666; margin-bottom: 50px; line-height: 1.5; }
.gm-modal-btns { display: flex; gap: 30px; justify-content: center; }
.gm-modal-btn {
flex: 1; padding: 25px 0; border-radius: 50px;
font-size: 32px; font-weight: bold; cursor: pointer; transition: transform 0.1s;
}
.gm-modal-btn:active { transform: scale(0.95); }
.gm-btn-cancel { background: #f1f2f6; color: #666; }
.gm-btn-confirm { background: #6c5ce7; color: white; box-shadow: 0 5px 15px rgba(108, 92, 231, 0.3); }
body.g-night-mode .gm-modal { background: #2d2d2d; }
body.g-night-mode .gm-modal-title { color: #eee; }
body.g-night-mode .gm-modal-desc { color: #aaa; }
body.g-night-mode .gm-btn-cancel { background: #444; color: #ccc; }
/* === 消息提示 (Toast) === */
.gm-toast {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(0,0,0,0.8); color: #fff; padding: 25px 50px;
border-radius: 16px; font-size: 32px; z-index: 1000020;
opacity: 0; transition: opacity 0.3s; pointer-events: none;
text-align: center; font-weight: bold;
}
.gm-toast.active { opacity: 1; }
/* === 登录弹窗 === */
.gm-login-tabs { display: flex; border-bottom: 1px solid #eee; margin-bottom: 30px; }
.gm-login-tab { flex: 1; text-align: center; padding: 20px; font-size: 32px; color: #999; font-weight: bold; cursor: pointer; }
.gm-login-tab.active { color: #6c5ce7; border-bottom: 4px solid #6c5ce7; }
.gm-input-group { margin-bottom: 30px; position: relative; }
.gm-login-input { width: 100%; padding: 25px 30px; background: #f5f6fa; border: 1px solid #eee; border-radius: 16px; font-size: 30px; outline: none; box-sizing: border-box; }
.gm-login-input:focus { border-color: #6c5ce7; background: #fff; }
.gm-code-btn { position: absolute; right: 10px; top: 10px; height: calc(100% - 20px); padding: 0 30px; background: #6c5ce7; color: white; border-radius: 12px; border: none; font-size: 26px; font-weight: bold; }
.gm-code-btn:disabled { background: #ccc; }
body.g-night-mode .gm-login-tab { border-color: #333; }
body.g-night-mode .gm-login-input { background: #333; border-color: #444; color: #eee; }
/* === 屏蔽主脚本的桌面端按钮 === */
#g-setting-btn, #g-magic-btn, #g-mobile-btn, #g-mobile-search-btn, #g-stop-bgm-btn, #g-chat-btn {
display: none !important;
}
`;
// 立即注入样式 (处理缩放系数)
// 1. 初始化缩放系数
if (!config.mobileScale) config.mobileScale = 1.0;
// 2. 将所有 px 单位转换为 calc(px * var(--gm-scale))
// 排除 1px 边框 (可选,但为了保持精致感,一般边框不缩放,或者也缩放) -> 这里选择全量缩放以保持比例一致
const scaledStyle = appStyle.replace(/(\d+)px/g, 'calc($1px * var(--gm-scale))');
// 3. 注入变量定义和处理后的样式
const style = document.createElement('style');
style.textContent = `
:root { --gm-scale: ${config.mobileScale}; }
${scaledStyle}
`;
(document.head || document.documentElement).appendChild(style);
// ==========================================
// 2. API 客户端
// ==========================================
class GululuApiClient {
constructor() {
this.token = null;
this.directory = [];
this.floorMap = new Map();
this.loadedFloors = new Set();
this.minLoaded = Infinity;
this.maxLoaded = -Infinity;
this.bookId = null;
this.bookName = "骨碌碌阅读器";
this.bookAuthor = null;
this.totalFloors = 0;
this.readProgress = 0;
this.isCollected = false;
this.isLoading = false;
try {
this.currentUser = JSON.parse(localStorage.getItem('gululu_user_cache') ||
'{"nickName":"我","headPic":""}');
} catch (e) {
this.currentUser = {
nickName: '我',
headPic: ''
};
}
}
initData() {
// 1. 优先从 Cookie 获取 Token (最准确)
const cookieMatch = document.cookie.match(/(?:^|; )token=([^;]*)/);
if (cookieMatch && cookieMatch[1]) {
this.token = decodeURIComponent(cookieMatch[1]);
}
const script = document.getElementById('__NEXT_DATA__');
if (!script) return null;
try {
const data = JSON.parse(script.textContent);
const props = data.props?.pageProps;
if (!props) return null;
// 2. 如果 Cookie 没拿到,尝试从 NEXT_DATA 获取
if (!this.token && props.tokenStore?.token) {
this.token = props.tokenStore.token;
}
// 3. 获取用户信息缓存 (如果有)
if (props.userStore?.userInfo) {
this.currentUser = props.userStore.userInfo;
localStorage.setItem('gululu_user_cache', JSON.stringify(this.currentUser));
}
// 4. 阅读页特有数据 (仅在有书籍数据时解析)
if (props.bookFloorsStore && props.bookDetailStore) {
this.directory = props.bookFloorsStore.directory || [];
this.bookId = props.bookDetailStore.opusId;
this.bookName = props.bookDetailStore.bookDetail?.name || "骨碌碌阅读器";
this.bookAuthor = props.bookDetailStore.bookDetail?.author;
// 修复:优先使用目录最后一项的楼层号,解决"总楼层虚高"问题
if (this.directory && this.directory.length > 0) {
this.totalFloors = this.directory[this.directory.length - 1].floorNum;
} else {
this.totalFloors = props.bookDetailStore.bookDetail?.floorNum || 0;
}
this.readProgress = props.bookFloorsStore.readProgress || 0;
this.isCollected = props.bookDetailStore.bookDetail?.collect || false;
this.directory.forEach(d => this.floorMap.set(d.floorNum, d.floorId));
return props.bookFloorsStore.bookFloors;
}
return null;
} catch (e) {
console.error("解析数据失败", e);
return null;
}
}
fetchComments(floorId, paragraphId, page = 1) {
return new Promise((resolve) => {
if (!this.token) return resolve(null);
let url =
`https://backend.gululu.world/reader/opus/comment/page?opusId=${this.bookId}¤t=${page}&size=20&floorId=${floorId}`;
if (paragraphId) url += `¶graphId=${paragraphId}`;
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"Authorization": `Bearer ${this.token}`,
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/"
},
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.data ? json.data.records : []);
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([])
});
});
}
fetchCommentCounts(paragraphIds) {
return new Promise((resolve) => {
if (!this.token || !paragraphIds || paragraphIds.length === 0) return resolve({});
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/reader/opus/comment/paragraph-comment-count",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/"
},
data: JSON.stringify(paragraphIds),
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.data || {});
} catch (e) {
resolve({});
}
},
onerror: () => resolve({})
});
});
}
sendComment(floorId, pid, content) {
return new Promise((resolve) => {
if (!this.token) return resolve(null);
const payload = {
opusId: this.bookId,
floorId: floorId,
content: content
};
if (pid) payload.paragraphId = pid;
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/reader/opus/comment/create",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/"
},
data: JSON.stringify(payload),
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json);
} catch (e) {
resolve(null);
}
},
onerror: () => resolve(null)
});
});
}
likeComment(commentId) {
return new Promise((resolve) => {
if (!this.token) return resolve(false);
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/reader/opus/comment/like",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"platform": "1"
},
data: JSON.stringify({
opusId: this.bookId,
id: commentId
}),
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
// 201 Created 或 200 OK 都视为成功
resolve(json.code === 200 || json.code === 201);
} catch (e) {
resolve(false);
}
},
onerror: () => resolve(false)
});
});
}
saveHistory(floorNum) {
if (!this.token || !this.bookId) return;
// 简单的防抖:如果进度没变,不发送
if (this.readProgress === floorNum) return;
this.readProgress = floorNum;
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/history/save",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"platform": "1"
},
data: JSON.stringify({
opusId: this.bookId,
type: 1,
floorSort: floorNum,
historyId: this.bookId, // 根据抓包,直接传 opusId
save: 1
}),
onload: () => {}, // 默默保存,不打扰用户
onerror: () => {}
});
}
toggleCollect(isCollect) {
return new Promise((resolve) => {
if (!this.token) return resolve(false);
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/opus/collect",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"platform": "1"
},
data: JSON.stringify({
opusId: this.bookId,
isCollect: isCollect ? 1 : 0
}),
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
if (json.code === 200) {
this.isCollected = isCollect;
resolve(true);
} else {
resolve(false);
}
} catch (e) {
resolve(false);
}
},
onerror: () => resolve(false)
});
});
}
replyComment(content, replyCommentId) {
return new Promise((resolve) => {
if (!this.token) return resolve(null);
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/reader/opus/comment/reply-comment",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"platform": "1"
},
data: JSON.stringify({
opusId: this.bookId,
content: content,
replyCommentId: replyCommentId
}),
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json);
} catch (e) {
resolve(null);
}
},
onerror: () => resolve(null)
});
});
}
fetchChildComments(parentId) {
return new Promise((resolve) => {
if (!this.token) return resolve([]);
GM_xmlhttpRequest({
method: "GET",
url: `https://backend.gululu.world/reader/opus/comment/page-children?opusId=${this.bookId}&parentId=${parentId}¤t=1&size=1000`,
headers: {
"Authorization": `Bearer ${this.token}`,
"Origin": "https://www.gululu.world",
"platform": "1"
},
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.data ? json.data.records : []);
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([])
});
});
}
fetchFloors(floorIds) {
return new Promise((resolve) => {
if (!this.token) return resolve(null);
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/reader/floor/content-by-ids",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/"
},
data: JSON.stringify(floorIds),
onload: function(response) {
try {
const json = JSON.parse(response.responseText);
resolve(json.data);
} catch (e) {
resolve(null);
}
},
onerror: function(e) {
resolve(null);
}
});
});
}
fetchHotSearch() {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: "https://backend.gululu.world/search/hot-week",
headers: {
"Authorization": `Bearer ${this.token}`,
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/"
},
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
if (Array.isArray(json)) resolve(json);
else if (json.data && Array.isArray(json.data)) resolve(json
.data);
else resolve([]);
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([])
});
});
}
fetchSearchHistory() {
return new Promise((resolve) => {
if (!this.token) return resolve([]);
GM_xmlhttpRequest({
method: "GET",
url: "https://backend.gululu.world/search/history",
headers: { "Authorization": `Bearer ${this.token}`, "Origin": "https://www.gululu.world", "Referer": "https://www.gululu.world/" },
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
// 兼容直接返回数组的情况
if (Array.isArray(json)) resolve(json);
else if (json.data && Array.isArray(json.data)) resolve(json.data);
else resolve([]);
} catch(e) { resolve([]); }
},
onerror: () => resolve([])
});
});
}
searchGeneral(keyword, type, page = 1) {
return new Promise((resolve) => {
if (!this.token) return resolve(null);
const encodedKey = encodeURIComponent(keyword);
GM_xmlhttpRequest({
method: "GET",
url: `https://backend.gululu.world/search/generalPageV2?type=${type}&key=${encodedKey}&page=${page}`,
headers: {
"Authorization": `Bearer ${this.token}`,
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/"
},
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.data);
} catch (e) {
resolve(null);
}
},
onerror: () => resolve(null)
});
});
}
// 用户搜索
searchUser(keyword) {
return new Promise((resolve) => {
if (!this.token) return resolve([]);
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/search/opus-author",
headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json", "Origin": "https://www.gululu.world", "Referer": "https://www.gululu.world/" },
data: JSON.stringify({ text: keyword }),
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.searchAuthorRespList || []);
} catch(e) { resolve([]); }
},
onerror: () => resolve([])
});
});
}
// 获取新人榜 (无分页)
fetchNewcomerRank() {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: "https://backend.gululu.world/opus/hot-new",
headers: { "Authorization": `Bearer ${this.token}`, "Origin": "https://www.gululu.world", "Referer": "https://www.gululu.world/" },
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.data || []);
} catch(e) { resolve([]); }
},
onerror: () => resolve([])
});
});
}
// 获取通用榜单 (sort: 0=浏览最高, 1=最新更新, 2=最新发布)
fetchRankList(sort, page = 1) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://backend.gululu.world/opus/list-sort?pageNum=${page}&pageSize=20&isAsc=asc&sort=${sort}`,
headers: { "Authorization": `Bearer ${this.token}`, "Origin": "https://www.gululu.world", "Referer": "https://www.gululu.world/" },
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.rows || []);
} catch(e) { resolve([]); }
},
onerror: () => resolve([])
});
});
}
clearSearchHistory() {
return new Promise((resolve) => {
if (!this.token) return resolve(false);
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/search/delete",
headers: { "Authorization": `Bearer ${this.token}`, "Origin": "https://www.gululu.world", "Referer": "https://www.gululu.world/" },
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.code === 200);
} catch(e) { resolve(false); }
},
onerror: () => resolve(false)
});
});
}
// 获取书架 (分页)
fetchBookshelf(page = 1) {
return new Promise((resolve) => {
if (!this.token) return resolve([]);
GM_xmlhttpRequest({
method: "GET",
url: `https://backend.gululu.world/bookshelf/get-book?pageNum=${page}&pageSize=20&sort=1`,
headers: {
"Authorization": `Bearer ${this.token}`,
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/",
"platform": "1"
},
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(json.code === 200 ? json.data : []);
} catch(e) { resolve([]); }
},
onerror: () => resolve([])
});
});
}
// 获取我的作品
fetchMyWorks() {
return new Promise((resolve) => {
if (!this.token) return resolve([]);
GM_xmlhttpRequest({
method: "GET",
url: "https://backend.gululu.world/app-user/select-user",
headers: {
"Authorization": `Bearer ${this.token}`,
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/",
"platform": "1"
},
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
resolve(Array.isArray(json) ? json : []);
} catch(e) { resolve([]); }
},
onerror: () => resolve([])
});
});
}
// 获取消息未读数
fetchMessageCount() {
return new Promise((resolve) => {
if (!this.token) return resolve({});
GM_xmlhttpRequest({
method: "GET",
url: "https://backend.gululu.world/message/push-get-all",
headers: { "Authorization": `Bearer ${this.token}`, "Origin": "https://www.gululu.world" },
onload: (res) => {
try { resolve(JSON.parse(res.responseText).data || {}); }
catch(e) { resolve({}); }
},
onerror: () => resolve({})
});
});
}
// 获取消息列表 (type: 1=赞, 2=回复, 0=系统)
fetchMessages(type) {
return new Promise((resolve) => {
if (!this.token) return resolve([]);
GM_xmlhttpRequest({
method: "GET",
url: `https://backend.gululu.world/message/push-get-one?type=${type}`,
headers: { "Authorization": `Bearer ${this.token}`, "Origin": "https://www.gululu.world" },
onload: (res) => {
try { resolve(JSON.parse(res.responseText).data || []); }
catch(e) { resolve([]); }
},
onerror: () => resolve([])
});
});
}
// 获取用户信息
fetchUserInfo() {
return new Promise((resolve) => {
if (!this.token) return resolve(null);
GM_xmlhttpRequest({
method: "GET",
url: "https://backend.gululu.world/app-user/list-userinfo",
headers: {
"Authorization": `Bearer ${this.token}`,
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/",
"platform": "1"
},
onload: (response) => {
try {
const json = JSON.parse(response.responseText);
if (json.code === 200 && json.data) {
this.currentUser = json.data;
localStorage.setItem('gululu_user_cache', JSON.stringify(this.currentUser));
resolve(this.currentUser);
} else {
console.error("获取用户信息失败:", json);
resolve(null);
}
} catch(e) {
console.error("解析用户信息失败:", e);
resolve(null);
}
},
onerror: (e) => {
console.error("请求用户信息网络错误:", e);
resolve(null);
}
});
});
}
// 密码登录
loginByPass(phone, pass) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/login/phone-passLogin",
headers: {
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/",
"platform": "1" // 补全平台标识
},
data: JSON.stringify({ phone, pass }),
onload: (res) => resolve(JSON.parse(res.responseText)),
onerror: () => resolve({ code: 500, msg: "网络错误" })
});
});
}
// 发送验证码
sendSmsCode(phone) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/readlogin/send-app-code",
headers: {
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/",
"platform": "1"
},
data: JSON.stringify({ phone }),
onload: (res) => resolve(JSON.parse(res.responseText)),
onerror: () => resolve({ code: 500, msg: "网络错误" })
});
});
}
// 验证码登录
loginBySms(phone, smsCode) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "POST",
url: "https://backend.gululu.world/readlogin/phone",
headers: {
"Content-Type": "application/json",
"Origin": "https://www.gululu.world",
"Referer": "https://www.gululu.world/",
"platform": "1"
},
data: JSON.stringify({ phone, smsCode }),
onload: (res) => resolve(JSON.parse(res.responseText)),
onerror: () => resolve({ code: 500, msg: "网络错误" })
});
});
}
}
// ==========================================
// 3. UI (阅读页)
// ==========================================
class MobileUI {
constructor(client) {
this.client = client;
this.root = null;
this.container = null;
this.parser = new DOMParser();
this.currentBgUrl = null;
this.bgTimer = null;
this.ticking = false;
this.isJumping = false; // 跳转锁
this.renderGeneration = 0; // 渲染代数,用于废弃过期的请求
}
init(initialFloors) {
this.initBgLayer();
this.initCommentDrawer();
this.initFloatingBtn();
this.client.fetchUserInfo(); // 替换原有的 scrapeUserInfo
this.root = document.createElement('div');
this.root.id = 'g-mobile-app';
this.root.innerHTML = `
${this.client.bookName}
⛶
`;
document.body.appendChild(this.root);
this.container = this.root.querySelector('#gm-list-container');
this.renderFloors(initialFloors, 'append');
this.bindEvents();
this.initImageObserver();
this.checkReadProgress();
}
checkReadProgress() {
// 如果有阅读进度,且进度不在当前加载的范围内(比如刚打开是在第一页,但进度在第100页)
if (this.client.readProgress > 20 && !this.client.floorMap.has(this.client.readProgress)) {
// 简单的 Confirm 提示,或者可以用自定义 Modal 优化体验
if(confirm(`检测到上次阅读到 ${this.client.readProgress} 层,是否跳转?`)) {
this.jumpToFloor(this.client.readProgress);
}
}
}
initBgLayer() {
if (!document.getElementById('gm-bg-layer')) {
const layer = document.createElement('div');
layer.id = 'gm-bg-layer';
document.body.appendChild(layer);
const backdrop = document.createElement('div');
backdrop.id = 'gm-bg-backdrop';
document.body.appendChild(backdrop);
}
}
renderFloors(floorsData, direction) {
if (!floorsData || floorsData.length === 0) return;
floorsData.sort((a, b) => a.floorNum - b.floorNum);
const fragment = document.createDocumentFragment();
const pidsToFetch = [];
floorsData.forEach(floor => {
if (this.client.loadedFloors.has(floor.floorNum)) return;
this.client.loadedFloors.add(floor.floorNum);
this.client.minLoaded = Math.min(this.client.minLoaded, floor.floorNum);
this.client.maxLoaded = Math.max(this.client.maxLoaded, floor.floorNum);
const el = document.createElement('div');
el.className = 'gm-floor';
el.dataset.floorNum = floor.floorNum;
let rawHtml = '';
if (floor.paragraphContents) {
floor.paragraphContents.forEach(p => {
if (p.id) pidsToFetch.push(p.id);
});
rawHtml = floor.paragraphContents.map(p => this.jsonToHtml(p)).join('');
}
const processedHtml = this.processContent(rawHtml);
let author = floor.author;
if (!author && floor.authorId === this.client.bookAuthor?.userId) {
author = this.client.bookAuthor;
}
const avatarUrl = author?.headPic || '';
const authorName = author?.nickName || 'Unknown';
const time = floor.createTime || '';
const realFloorId = this.client.floorMap.get(floor.floorNum) || floor.floorId || floor
.id;
el.innerHTML = `
${processedHtml}
`;
fragment.appendChild(el);
});
if (direction === 'append') {
this.container.appendChild(fragment);
} else {
const scrollArea = this.root.querySelector('#gm-scroll-area');
// 1. 临时禁用平滑滚动,确保瞬间修正位置,防止视觉抖动
const originalBehavior = scrollArea.style.scrollBehavior;
scrollArea.style.scrollBehavior = 'auto';
// 2. 关键:在插入前记录旧的高度和位置
const oldHeight = scrollArea.scrollHeight;
const oldTop = scrollArea.scrollTop;
// 3. 插入新内容
this.container.insertBefore(fragment, this.container.firstChild);
// 4. 修正滚动条位置:新位置 = 旧位置 + (新高度 - 旧高度)
// 这样可以保持视口相对于旧内容的相对位置不变,视觉上就像"静止"一样
const newHeight = scrollArea.scrollHeight;
scrollArea.scrollTop = oldTop + (newHeight - oldHeight);
// 5. 恢复平滑滚动
scrollArea.style.scrollBehavior = originalBehavior;
}
this.updateBackground();
if (pidsToFetch.length > 0) {
this.client.fetchCommentCounts(pidsToFetch).then(counts => {
Object.entries(counts).forEach(([pid, count]) => {
if (parseInt(count) > 0) {
const btn = this.root.querySelector(
`.gm-comment-btn[data-pid="${pid}"]`);
if (btn) {
const numEl = btn.querySelector('.gm-comment-num');
if (numEl) numEl.textContent = count;
btn.classList.add('has-comment');
}
}
});
});
}
}
jsonToHtml(p) {
if (p.type === 'image') return `
`;
if (p.type === 'paragraph' || p.type === 'heading') {
const tag = p.type === 'heading' ? `h${p.attrs.level || 1}` : 'p';
let contentHtml = '';
let fullText = '';
if (p.content && p.content.length > 0) {
p.content.forEach(span => {
if (span.type === 'text') {
let style = '';
let text = span.text;
let isTransparent = false;
if (span.marks) {
span.marks.forEach(m => {
if (m.type === 'bold') style += 'font-weight:bold;';
if (m.type === 'textStyle' && m.attrs.color) {
// 检测透明色
if (m.attrs.color === 'transparent' || m.attrs.color.replace(/\s/g,'') === 'rgba(0,0,0,0)') {
isTransparent = true;
}
// 过滤纯黑色,使其跟随主题色
const c = m.attrs.color.replace(/\s/g, '');
if (c !== 'rgb(0,0,0)' && c !== '#000000' && c !== '#000') {
style += `color:${m.attrs.color};`;
}
}
});
}
// 只有非透明文本才计入有效内容 (用于判断是否显示评论气泡)
if (!isTransparent) {
fullText += text;
}
text = text.replace(/&/g, "&").replace(//g,
">");
contentHtml += style ? `${text}` : text;
} else if (span.type === 'hardBreak') contentHtml += '
';
});
}
// 隐藏空行:如果内容为空(且不是超级空行),则不渲染
// 原生界面行为:空段落高度为0或不显示,我们之前强制加了
导致空行显现,现在去掉
if (!contentHtml) return '';
let pHtml = `<${tag} data-pid="${p.id || ''}">${contentHtml}`;
const hasMeaningfulContent = /[a-zA-Z0-9\u4e00-\u9fa5]/.test(fullText);
if (p.id && hasMeaningfulContent) {
pHtml += `
`;
}
pHtml += `${tag}>`;
return pHtml;
}
if (p.type === 'jumpFloorComponent') {
const text = p.attrs.description || '跳转到指定楼层';
const targetFloor = p.attrs.floorNumber;
return ``;
}
return '';
}
processContent(htmlStr) {
const doc = this.parser.parseFromString(htmlStr, 'text/html');
const selectors = ['div[class*="RichTextParagraph_menuBtn"]',
'span[class*="RichTextParagraph_inlineCommentNumber"]'
];
selectors.forEach(s => doc.querySelectorAll(s).forEach(el => el.remove()));
doc.querySelectorAll('img').forEach(img => {
img.removeAttribute('resize');
img.style = "";
if (!img.src && img.dataset.src) img.src = img.dataset.src;
});
// 净化硬编码的黑色字体
doc.querySelectorAll('[style*="color"]').forEach(el => {
if (el.style.color === 'rgb(0, 0, 0)' || el.style.color === '#000000' || el.style.color === 'black') {
el.style.color = '';
}
});
// --- 引用解析 (移植自主脚本) ---
// 1. 块级引用 (跨节点检测)
const children = Array.from(doc.body.children);
for (let i = 0; i < children.length; i++) {
const node = children[i];
if (!node.parentNode) continue;
const text = node.textContent || "";
const startMatch = text.match(/(?:<|<)引用\s+id="(\d+)"\s+floor="(\d+)"(?:>|>)/);
if (startMatch) {
const bid = startMatch[1];
const fid = startMatch[2];
const next1 = node.nextElementSibling;
const next2 = next1 ? next1.nextElementSibling : null;
if (next1 && next2) {
const endText = next2.textContent || "";
if (endText.includes('引用>') || endText.includes('</引用>')) {
const contentHtml = next1.innerHTML;
const quoteDiv = document.createElement('div');
quoteDiv.className = 'g-quote-box';
quoteDiv.dataset.bid = bid;
quoteDiv.dataset.fid = fid;
quoteDiv.innerHTML = `
❝
${contentHtml}
跳转至 #${fid}
`;
node.replaceWith(quoteDiv);
next1.remove();
next2.remove();
continue;
}
}
}
}
// 2. 内联引用 (正则替换)
let html = doc.body.innerHTML;
const inlineRegex = /(?:<|<)引用\s+id="(\d+)"\s+floor="(\d+)"(?:>|>)([\s\S]*?)(?:<|<)\/引用(?:>|>)/gi;
if (html.match(inlineRegex)) {
html = html.replace(inlineRegex, (match, bid, fid, content) => {
return ``;
});
doc.body.innerHTML = html;
}
doc.querySelectorAll('[data-bg-url]').forEach(el => el.classList.add('gm-bg-trigger'));
doc.querySelectorAll('[data-bg-clear]').forEach(el => {
el.classList.add('gm-bg-trigger');
el.setAttribute('data-bg-url', 'CLEAR');
});
const walker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ALL, null, false);
let currentNode, pendingBgUrl = null;
while (currentNode = walker.nextNode()) {
if (currentNode.nodeType === Node.TEXT_NODE) {
const text = currentNode.textContent;
const musicMatch = text.match(/(?:<|<)music:(.*?)(?:>|>)/i);
if (musicMatch && musicMatch[1]) {
const url = musicMatch[1].trim();
const span = document.createElement('span');
span.className = 'g-music-key';
span.dataset.url = url;
span.textContent = ' 点击播放';
if (currentNode.parentNode) currentNode.parentNode.replaceChild(span, currentNode);
continue;
}
const inlineMatch = text.match(/(?:<|<)背景(?:>|>)(?:[\s\S]*?src="([^"]+)")?/i);
if (inlineMatch && inlineMatch[1]) {
const p = currentNode.parentElement;
if (p) {
p.classList.add('gm-bg-trigger');
p.setAttribute('data-bg-url', inlineMatch[1]);
}
} else if (/(?:<|<)背景(?:>|>)/.test(text)) pendingBgUrl = 'WAITING_FOR_IMG';
else if (/(?:<|<)(?:移除背景|清除背景)(?:>|>)/.test(text)) {
const p = currentNode.parentElement;
if (p) {
p.classList.add('gm-bg-trigger');
p.setAttribute('data-bg-url', 'CLEAR');
}
pendingBgUrl = null;
}
} else if (currentNode.nodeType === Node.ELEMENT_NODE && currentNode.tagName === 'IMG') {
if (pendingBgUrl === 'WAITING_FOR_IMG') {
currentNode.classList.add('gm-bg-trigger');
const src = currentNode.getAttribute('src') || currentNode.getAttribute('data-src');
if (src) currentNode.setAttribute('data-bg-url', src);
pendingBgUrl = null;
}
}
}
return doc.body.innerHTML;
}
initImageObserver() {
const container = this.root.querySelector('#gm-list-container');
const observer = new MutationObserver((mutations) => {
let needUpdate = false;
mutations.forEach(m => {
if (m.type === 'attributes' && m.attributeName === 'src') {
const img = m.target;
if (img.classList.contains('gm-bg-trigger') && img.src) {
img.setAttribute('data-bg-url', img.src);
needUpdate = true;
}
}
if (m.type === 'childList') needUpdate = true;
});
if (needUpdate) this.updateBackground();
});
observer.observe(container, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['src']
});
}
initFloatingBtn() {
const btn = document.createElement('div');
btn.id = 'gm-comment-toggle';
btn.innerText = '💬';
document.body.appendChild(btn);
btn.onclick = () => {
const isActive = this.root.classList.contains('g-show-comments');
if (isActive) {
this.root.classList.remove('g-show-comments');
btn.classList.remove('active');
} else {
this.root.classList.add('g-show-comments');
btn.classList.add('active');
}
};
// 1.5 返回首页
const homeBtn = document.createElement('div');
homeBtn.id = 'gm-home-btn';
homeBtn.innerText = '🏠';
document.body.appendChild(homeBtn);
// 注入模态框 HTML
if (!document.getElementById('gm-home-modal')) {
const modalHtml = `
`;
const div = document.createElement('div');
div.innerHTML = modalHtml;
document.body.appendChild(div.firstElementChild);
}
const modal = document.getElementById('gm-home-modal');
homeBtn.onclick = () => modal.classList.add('active');
modal.querySelector('.gm-btn-cancel').onclick = () => modal.classList.remove('active');
modal.querySelector('.gm-btn-confirm').onclick = () => {
window.location.href = '/';
};
// 2. 快捷菜单 (移植主脚本功能)
const menuBtn = document.createElement('div');
menuBtn.id = 'gm-menu-toggle';
menuBtn.innerText = '⚙️';
document.body.appendChild(menuBtn);
const menuList = document.createElement('div');
menuList.className = 'gm-menu-list';
const scalePercent = Math.round(config.mobileScale * 100);
menuList.innerHTML = `
`;
document.body.appendChild(menuList);
menuBtn.onclick = () => {
const isActive = menuList.classList.contains('active');
if (isActive) {
menuList.classList.remove('active');
menuBtn.classList.remove('active');
menuBtn.innerText = '⚙️';
} else {
menuList.classList.add('active');
menuBtn.classList.add('active');
menuBtn.innerText = '✕';
}
};
const callHelper = (method) => {
const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
// 增加重试机制 (最多等待 3 秒)
let attempts = 15;
const tryExecute = () => {
if (targetWindow.GululuHelper && targetWindow.GululuHelper[method]) {
targetWindow.GululuHelper[method]();
menuList.classList.remove('active');
menuBtn.classList.remove('active');
menuBtn.innerText = '⚙️';
} else {
if (attempts > 0) {
attempts--;
setTimeout(tryExecute, 200);
} else {
console.error("GululuHelper not found on", targetWindow);
alert("主脚本接口未就绪或加载过慢!\n请确保“Gululu沉浸助手主文件”已启用。");
}
}
};
tryExecute();
};
menuList.querySelector('#gm-act-stop').onclick = () => callHelper('stopMusic');
menuList.querySelector('#gm-act-dice').onclick = () => callHelper('revealNext10');
menuList.querySelector('#gm-act-setting').onclick = () => callHelper('toggleSettings');
// 全屏切换
menuList.querySelector('#gm-act-fullscreen').onclick = () => {
if (!document.fullscreenElement) {
sessionStorage.setItem('gm_is_fullscreen', '1'); // 标记意图
document.documentElement.requestFullscreen().catch(e => alert("全屏失败: " + e.message));
} else {
sessionStorage.removeItem('gm_is_fullscreen'); // 清除意图
if (document.exitFullscreen) document.exitFullscreen();
}
menuList.classList.remove('active');
menuBtn.classList.remove('active');
menuBtn.innerText = '⚙️';
};
// 退出逻辑
// 1. 注入退出确认框
if (!document.getElementById('gm-exit-modal')) {
const modalHtml = `
`;
const div = document.createElement('div');
div.innerHTML = modalHtml;
document.body.appendChild(div.firstElementChild);
}
const exitModal = document.getElementById('gm-exit-modal');
menuList.querySelector('#gm-act-exit').onclick = () => {
menuList.classList.remove('active');
menuBtn.classList.remove('active');
menuBtn.innerText = '⚙️';
exitModal.classList.add('active');
};
exitModal.querySelector('.gm-btn-cancel').onclick = () => exitModal.classList.remove('active');
exitModal.querySelector('.gm-btn-confirm').onclick = () => {
config.mobileModeEnabled = false;
localStorage.setItem('gululu_global_config_v6', JSON.stringify(config));
location.reload();
};
// 缩放逻辑
menuList.querySelector('#gm-act-scale').onclick = (e) => {
const scales = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5];
let idx = scales.indexOf(config.mobileScale);
idx = (idx + 1) % scales.length;
config.mobileScale = scales[idx];
// 应用
document.documentElement.style.setProperty('--gm-scale', config.mobileScale);
localStorage.setItem('gululu_global_config_v6', JSON.stringify(config));
// 更新按钮文本
const item = e.currentTarget;
item.innerHTML = `UI缩放: ${config.mobileScale * 100}% 📐`;
};
}
initCommentDrawer() {
const drawer = document.createElement('div');
drawer.id = 'gm-cmt-drawer';
drawer.innerHTML = `
`;
document.body.appendChild(drawer);
const mask = document.createElement('div');
mask.id = 'gm-cmt-mask';
document.body.appendChild(mask);
// 状态变量
this.replyTarget = null; // { cid, rootId, uname }
const close = () => {
drawer.classList.remove('active');
mask.classList.remove('active');
document.getElementById('gm-cmt-input').blur();
this.cancelReplyMode();
};
mask.onclick = close;
drawer.querySelector('#gm-cmt-close').onclick = close;
// 事件委托:点赞、回复、展开子评论
const cmtList = document.getElementById('gm-cmt-list');
cmtList.addEventListener('click', async (e) => {
// 1. 点赞
const likeBtn = e.target.closest('.gm-cmt-like-btn');
if (likeBtn) {
const cid = likeBtn.dataset.cid;
if (!cid) return;
if (likeBtn.dataset.liked === "true" || likeBtn.classList.contains('liked')) return;
if (likeBtn.dataset.liking) return;
likeBtn.dataset.liking = "true";
likeBtn.classList.add('liked');
const success = await this.client.likeComment(cid);
if (success) {
const match = likeBtn.innerText.match(/\d+/);
if (match) likeBtn.innerHTML = `♥ ${parseInt(match[0]) + 1}`;
likeBtn.dataset.liked = "true";
} else {
likeBtn.classList.remove('liked');
}
delete likeBtn.dataset.liking;
}
// 2. 回复按钮
const replyBtn = e.target.closest('.gm-cmt-reply-btn');
if (replyBtn) {
const cid = replyBtn.dataset.cid;
const uname = replyBtn.dataset.uname;
const rootId = replyBtn.dataset.root;
this.enterReplyMode(cid, uname, rootId);
}
// 3. 展开/收起子评论
const expandBtn = e.target.closest('.gm-sub-expand-btn');
if (expandBtn) {
const cid = expandBtn.dataset.cid;
const action = expandBtn.dataset.action; // 'collapse' or undefined (expand)
const subList = document.getElementById(`gm-sub-list-${cid}`);
if (action === 'collapse') {
subList.classList.remove('active');
// 既然收起了,我们需要在外面生成一个"展开"按钮,如果之前没有的话
let outerBtn = subList.parentNode.querySelector(`.gm-sub-expand-btn[data-action="expand"]`);
if (!outerBtn) {
outerBtn = document.createElement('div');
outerBtn.className = 'gm-sub-expand-btn';
outerBtn.dataset.cid = cid;
outerBtn.dataset.action = 'expand';
outerBtn.innerText = '查看回复 ▾';
subList.parentNode.appendChild(outerBtn);
}
outerBtn.style.display = 'block';
} else {
// 展开逻辑
if (subList.children.length > 0 && !subList.dataset.autoload) {
// 已经加载过,直接显示
subList.classList.add('active');
expandBtn.style.display = 'none';
} else {
// 需要加载
expandBtn.innerText = "加载中...";
const children = await this.client.fetchChildComments(cid);
if (children && children.length > 0) {
subList.innerHTML = children.map(c => this.renderSubCommentItem(c, cid)).join('');
subList.insertAdjacentHTML('beforeend', `收起回复
`);
subList.classList.add('active');
expandBtn.style.display = 'none';
} else {
expandBtn.innerText = "暂无更多回复";
}
}
}
}
});
// 取消回复模式
document.getElementById('gm-reply-cancel-btn').onclick = () => this.cancelReplyMode();
const sendBtn = document.getElementById('gm-cmt-send');
const input = document.getElementById('gm-cmt-input');
// 自适应高度
input.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
sendBtn.onclick = async () => {
const content = input.value.trim();
if (!content) return;
sendBtn.innerText = '...';
// 分支:回复评论 vs 发送新评论
if (this.replyTarget) {
// === 回复模式 ===
const res = await this.client.replyComment(content, this.replyTarget.cid);
sendBtn.innerText = '发送';
if (res && res.code === 200) {
// 构造假数据插入
const fakeSub = {
id: Date.now(), // 临时ID
content: content,
createTime: "刚刚",
fromUser: this.client.currentUser,
replyUser: { nickName: this.replyTarget.uname }
};
const rootId = this.replyTarget.rootId;
const subList = document.getElementById(`gm-sub-list-${rootId}`);
// 确保子评论容器可见
subList.classList.add('active');
subList.insertAdjacentHTML('beforeend', this.renderSubCommentItem(fakeSub, rootId));
input.value = '';
input.style.height = 'auto'; // 重置高度
this.cancelReplyMode();
} else {
alert('回复失败: ' + (res ? res.msg : '网络错误'));
}
} else {
// === 普通评论模式 ===
if (!this.activeCommentTarget) return;
const { floorId, pid } = this.activeCommentTarget;
const res = await this.client.sendComment(floorId, pid, content);
sendBtn.innerText = '发送';
if (res && res.code === 200) {
const fakeComment = {
content: content,
createTime: "刚刚",
likeCount: 0,
fromUser: this.client.currentUser
};
const list = document.getElementById('gm-cmt-list');
if (list.querySelector('div[style*="text-align:center"]')) list.innerHTML = '';
list.insertAdjacentHTML('beforeend', this.renderCommentItem(fakeComment));
list.scrollTop = list.scrollHeight;
input.value = '';
input.style.height = 'auto'; // 重置高度
// 更新外部计数
if (pid) {
const btn = this.root.querySelector(`.gm-comment-btn[data-pid="${pid}"]`);
if (btn) {
const numEl = btn.querySelector('.gm-comment-num');
if (numEl) numEl.textContent = (parseInt(numEl.textContent) || 0) + 1;
btn.classList.add('has-comment');
}
} else {
const btn = this.root.querySelector(`.gm-floor-cmt-btn[data-fid="${floorId}"]`);
if (btn) {
const match = btn.textContent.match(/(\d+)/);
const count = match ? parseInt(match[1]) : 0;
btn.textContent = `💬 ${count + 1}`;
}
}
} else {
alert('发送失败: ' + (res ? res.msg : '网络错误'));
}
}
};
}
enterReplyMode(cid, uname, rootId) {
this.replyTarget = { cid, uname, rootId };
const cancelBtn = document.getElementById('gm-reply-cancel-btn');
const input = document.getElementById('gm-cmt-input');
if(cancelBtn) cancelBtn.style.display = 'flex';
input.placeholder = `回复 @${uname}...`;
input.focus();
}
cancelReplyMode() {
this.replyTarget = null;
const cancelBtn = document.getElementById('gm-reply-cancel-btn');
const input = document.getElementById('gm-cmt-input');
if(cancelBtn) cancelBtn.style.display = 'none';
if(input) input.placeholder = "说点什么...";
}
renderCommentItem(c) {
const user = c.fromUser || c.userInfo || c.user || {};
const headPic = user.headPic || user.avatar || c.headPic || 'https://www.gululu.world/favicon.ico';
const nickName = user.nickName || user.nickname || c.nickName || c.nickname || '未知用户';
const likeCount = c.likeNum || c.likeCount || 0;
const cid = c.id;
const isLiked = c.liked === true;
const childrenNum = c.childrenNum || 0;
// 默认展开逻辑:如果有子评论,直接显示加载占位符,不显示展开按钮
const subHtml = childrenNum > 0 ?
`
↻ 正在加载 ${childrenNum} 条回复...
`
: ``;
return `
${nickName}
${c.content}
♥ ${likeCount}
${subHtml}
`;
}
renderSubCommentItem(c, rootId) {
const user = c.fromUser || {};
const nickName = user.nickName || '未知用户';
const replyUser = c.replyUser ? `回复 @${c.replyUser.nickName}` : '';
return `
${nickName} ${replyUser}
${c.content}
`;
}
async jumpToFloor(targetNum) {
targetNum = parseInt(targetNum);
if (!this.client.floorMap.has(targetNum)) {
alert("目标楼层不存在或未加载目录");
return;
}
// 1. 上锁并升级代数 (废弃所有正在进行的 loadMore 请求)
this.isJumping = true;
this.renderGeneration++;
this.client.isLoading = true; // 抢占加载锁
// 2. 立即重置状态 (防止滚动事件读取旧数据)
this.client.loadedFloors.clear();
this.client.minLoaded = Infinity;
this.client.maxLoaded = -Infinity;
// 3. 视觉反馈 & 滚动归零
this.container.innerHTML = '正在穿越... 🚀
';
const scrollArea = this.root.querySelector('#gm-scroll-area');
scrollArea.scrollTop = 0;
const idsToLoad = [];
// 加载目标层及后续9层(填满屏幕)
for (let i = 0; i < 10; i++) {
if (this.client.floorMap.has(targetNum + i)) idsToLoad.push(this.client.floorMap.get(targetNum + i));
}
const data = await this.client.fetchFloors(idsToLoad);
// 4. 渲染新数据
if (data) {
this.container.innerHTML = '';
this.renderFloors(data, 'append');
scrollArea.scrollTop = 0;
} else {
this.container.innerHTML = '加载失败,请重试
';
}
// 5. 解锁
this.isJumping = false;
this.client.isLoading = false;
}
async showComments(floorId, pid) {
this.activeCommentTarget = {
floorId,
pid
};
const drawer = document.getElementById('gm-cmt-drawer');
const mask = document.getElementById('gm-cmt-mask');
const list = document.getElementById('gm-cmt-list');
drawer.classList.add('active');
mask.classList.add('active');
list.innerHTML = '加载中...
';
const comments = await this.client.fetchComments(floorId, pid);
if (!comments || comments.length === 0) {
list.innerHTML = '暂无评论
';
return;
}
list.innerHTML = comments.map(c => this.renderCommentItem(c)).join('');
// 自动加载子评论
list.querySelectorAll('.gm-sub-cmt-list[data-autoload="true"]').forEach(async (el) => {
const cid = el.dataset.cid;
const children = await this.client.fetchChildComments(cid);
if (children && children.length > 0) {
el.innerHTML = children.map(c => this.renderSubCommentItem(c, cid)).join('');
// 加载完成后添加收起按钮,并移除 autoload 标记防止重复
el.insertAdjacentHTML('beforeend', `收起回复 ▴
`);
delete el.dataset.autoload;
} else {
el.innerHTML = '';
el.classList.remove('active');
}
});
}
applyBackground(url) {
if (this.currentBgUrl === url) return;
if (!url) return;
if (this.bgTimer) clearTimeout(this.bgTimer);
this.currentBgUrl = url;
const layer = document.getElementById('gm-bg-layer');
if (!layer) return;
layer.style.opacity = '0';
this.bgTimer = setTimeout(() => {
if (url && url !== 'CLEAR') {
const img = new Image();
img.src = url;
img.onload = () => {
if (this.currentBgUrl === url) {
layer.style.backgroundImage = `url("${url}")`;
layer.style.opacity = '1';
}
};
} else {
layer.style.backgroundImage = 'none';
}
}, 500);
}
showToast(msg) {
let toast = document.getElementById('gm-toast-msg');
if (!toast) {
toast = document.createElement('div');
toast.id = 'gm-toast-msg';
toast.className = 'gm-toast';
document.body.appendChild(toast);
}
toast.innerText = msg;
toast.classList.add('active');
if (this.toastTimer) clearTimeout(this.toastTimer);
this.toastTimer = setTimeout(() => {
toast.classList.remove('active');
}, 2000);
}
updateBackground() {
const triggers = this.root.querySelectorAll('.gm-bg-trigger, .gm-floor[data-bg]');
if (triggers.length === 0) return;
const checkLine = window.innerHeight / 2;
let activeBg = null;
for (let i = 0; i < triggers.length; i++) {
const el = triggers[i];
// ⚡ 修复:跳过被隐藏的元素(例如迷雾锁定的图片),防止它们抢占背景
if (el.offsetParent === null) continue;
const rect = el.getBoundingClientRect();
if (rect.top < checkLine) {
let url = el.getAttribute('data-bg-url') || el.getAttribute('data-bg');
if (url && url.length > 4) activeBg = url;
} else {
break;
}
}
this.applyBackground(activeBg);
}
// 检测当前阅读楼层并保存
checkActiveFloor() {
// 我们取屏幕上方 1/3 处作为阅读焦点线
const focusLine = window.innerHeight / 3;
const floors = this.root.querySelectorAll('.gm-floor');
// 倒序查找,找到第一个在该线之上的楼层(或者正好覆盖该线的)
// 由于是顺序排列,只要找到 rect.bottom > focusLine 的第一个元素即可
for (let i = 0; i < floors.length; i++) {
const floor = floors[i];
const rect = floor.getBoundingClientRect();
// 如果楼层底部超过了焦点线,说明它正在被阅读(或者它刚开始)
if (rect.bottom > focusLine) {
const num = parseInt(floor.dataset.floorNum);
if (num && !isNaN(num)) {
this.client.saveHistory(num);
}
break; // 找到即止
}
}
}
bindEvents() {
const scrollArea = this.root.querySelector('#gm-scroll-area');
const topLoader = this.root.querySelector('#gm-loader-top');
const bottomLoader = this.root.querySelector('#gm-loader-bottom');
// 收藏按钮
const collectBtn = this.root.querySelector('#gm-collect');
collectBtn.onclick = () => {
if (!this.client.token) return this.showToast("请先登录");
// 1. 注入收藏确认框 (如果不存在)
if (!document.getElementById('gm-collect-modal')) {
const modalHtml = `
`;
const div = document.createElement('div');
div.innerHTML = modalHtml;
document.body.appendChild(div.firstElementChild);
}
const modal = document.getElementById('gm-collect-modal');
const title = modal.querySelector('.gm-modal-title');
const desc = modal.querySelector('.gm-modal-desc');
const confirmBtn = modal.querySelector('.gm-btn-confirm');
const cancelBtn = modal.querySelector('.gm-btn-cancel');
const isCollected = this.client.isCollected;
title.innerText = isCollected ? "取消收藏" : "加入书架";
desc.innerText = isCollected ? "确定要将本书移出书架吗?" : "确定将本书加入书架吗?";
modal.classList.add('active');
// 绑定事件 (先解绑防止重复)
const close = () => modal.classList.remove('active');
cancelBtn.onclick = close;
// 重新克隆按钮以去除旧监听器
const newConfirm = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirm, confirmBtn);
newConfirm.onclick = async () => {
close();
const newState = !isCollected;
const success = await this.client.toggleCollect(newState);
if (success) {
collectBtn.style.color = newState ? '#0984e3' : '#b2bec3';
this.showToast(newState ? "收藏成功" : "已取消收藏");
} else {
this.showToast("操作失败");
}
};
};
// === 跳页功能 ===
// 1. 注入模态框
if (!document.getElementById('gm-jump-modal')) {
const modalHtml = `
跳转楼层
请输入目标楼层号 (1 - ${this.client.totalFloors || '?'})
`;
const div = document.createElement('div');
div.innerHTML = modalHtml;
document.body.appendChild(div.firstElementChild);
}
const jumpBtn = this.root.querySelector('#gm-jump-toggle');
const jumpModal = document.getElementById('gm-jump-modal');
const jumpInput = document.getElementById('gm-jump-input');
// 打开模态框
jumpBtn.onclick = () => {
jumpModal.querySelector('.gm-modal-desc').innerText = `请输入目标楼层号 (1 - ${this.client.totalFloors || '未知'})`;
jumpModal.classList.add('active');
jumpInput.value = '';
setTimeout(() => jumpInput.focus(), 100);
};
// 取消
jumpModal.querySelector('.gm-btn-cancel').onclick = () => jumpModal.classList.remove('active');
// 确认跳转
const doJump = () => {
const val = parseInt(jumpInput.value);
if (!val || val < 1) return;
jumpModal.classList.remove('active');
this.jumpToFloor(val);
};
jumpModal.querySelector('.gm-btn-confirm').onclick = doJump;
jumpInput.onkeydown = (e) => { if(e.key === 'Enter') doJump(); };
let loadTimer = null;
const handleScroll = () => {
// ⚡ 如果正在跳转中,忽略一切滚动事件,防止触发旧楼层的加载逻辑
if (this.isJumping) return;
if (!this.ticking) {
window.requestAnimationFrame(() => {
this.updateBackground();
this.ticking = false;
});
this.ticking = true;
}
if (loadTimer) clearTimeout(loadTimer);
loadTimer = setTimeout(() => {
// 1. 记录阅读进度 (500ms 延迟保存,即停止滚动后半秒保存)
this.checkActiveFloor();
if (this.client.isLoading) return;
const scrollTop = scrollArea.scrollTop;
const scrollHeight = scrollArea.scrollHeight;
const clientHeight = scrollArea.clientHeight;
if (scrollTop < 200) this.loadMore('prev', topLoader);
else if (scrollTop + clientHeight >= scrollHeight - 800) this.loadMore('next',
bottomLoader);
}, 500); // 将防抖时间从 100ms 改为 500ms,对性能更好,也符合阅读习惯
};
scrollArea.addEventListener('scroll', handleScroll, {
passive: true
});
this.root.addEventListener('click', (e) => {
const btn = e.target.closest('.gm-comment-btn');
if (btn) {
e.stopPropagation();
const pid = btn.dataset.pid;
const floorEl = btn.closest('.gm-floor');
if (floorEl && pid) {
const floorNum = parseInt(floorEl.dataset.floorNum);
const floorId = this.client.floorMap.get(floorNum);
if (floorId) this.showComments(floorId, pid);
}
}
if (e.target.classList.contains('gm-jump-btn')) {
const target = e.target.dataset.target;
if (target) this.jumpToFloor(target);
}
if (e.target.classList.contains('gm-floor-cmt-btn')) {
const fid = e.target.dataset.fid;
if (fid) this.showComments(fid, null);
}
// 引用跳转
const quoteBox = e.target.closest('.g-quote-box');
if (quoteBox) {
const bid = quoteBox.dataset.bid;
const fid = quoteBox.dataset.fid;
if (bid && fid) this.showQuoteModal(bid, fid);
}
});
}
showQuoteModal(bid, fid) {
if (!document.getElementById('gm-quote-modal')) {
const modalHtml = `
`;
const div = document.createElement('div');
div.innerHTML = modalHtml;
document.body.appendChild(div.firstElementChild);
const m = document.getElementById('gm-quote-modal');
m.querySelector('.gm-btn-cancel').onclick = () => m.classList.remove('active');
}
const modal = document.getElementById('gm-quote-modal');
const desc = modal.querySelector('.gm-modal-desc');
const confirmBtn = modal.querySelector('.gm-btn-confirm');
// 检查是否为本书
const isCurrentBook = String(bid) === String(this.client.bookId);
const bookText = isCurrentBook ? "当前书籍" : `Book: ${bid}`;
desc.innerHTML = `即将跳转到
${bookText}
第 ${fid} 层`;
// 重新克隆按钮以去除旧监听器
const newConfirm = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirm, confirmBtn);
newConfirm.onclick = () => {
modal.classList.remove('active');
if (isCurrentBook) {
this.jumpToFloor(fid);
} else {
window.location.href = `/book/${bid}?floorSort=${fid}`;
}
};
modal.classList.add('active');
}
async loadMore(direction, loaderEl) {
// ⚡ 记录发起请求时的代数
const currentGen = this.renderGeneration;
let targetFloorNum;
if (direction === 'prev') {
targetFloorNum = this.client.minLoaded - 1;
if (targetFloorNum < 1) return;
} else {
targetFloorNum = this.client.maxLoaded + 1;
if (!this.client.floorMap.has(targetFloorNum)) return;
}
this.client.isLoading = true;
loaderEl.style.display = 'flex';
const idsToLoad = [];
for (let i = 0; i < 10; i++) {
let num = direction === 'prev' ? targetFloorNum - i : targetFloorNum + i;
if (this.client.floorMap.has(num)) idsToLoad.push(this.client.floorMap.get(num));
}
if (idsToLoad.length > 0) {
console.log(`📡 [API] 请求 ${direction} 加载`);
const data = await this.client.fetchFloors(idsToLoad);
// ⚡ 核心检查:如果数据返回时,代数已经变了(说明中间发生了跳转),则丢弃数据
if (this.renderGeneration === currentGen) {
if (data) this.renderFloors(data, direction === 'prev' ? 'prepend' : 'append');
} else {
console.log("⚠️ [LoadMore] 检测到跳转发生,丢弃过期数据");
}
}
loaderEl.style.display = 'none';
this.client.isLoading = false;
}
}
// ==========================================
// 3.5 首页 App 模式 UI
// ==========================================
class MobileHomeUI {
constructor(client) {
this.client = client;
this.root = null;
this.tabs = [
{ id: 'search', label: '搜索', icon: 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z' },
{ id: 'msg', label: '消息', icon: 'M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z' },
{ id: 'rank', label: '排行榜', icon: 'M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6z' },
{ id: 'shelf', label: '书架', icon: 'M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12zM10 9h8v2h-8zm0 3h4v2h-4zm0-6h8v2h-8z' },
{ id: 'works', label: '我的作品', icon: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z' }
];
this.activeTab = 'rank';
// 搜索相关状态
this.searchOptions = { sort: 'default', minWords: 0 };
// 排行榜相关状态
this.rankState = {
type: 'NEW', // NEW, UPDATE, RELEASE, VIEW
page: 1,
loading: false,
hasMore: true
};
// 书架相关状态
this.shelfState = {
page: 1,
loading: false,
hasMore: true,
mode: 'list', // 'list' or 'grid'
data: [] // 缓存已加载的书籍
};
// 消息轮询定时器
this.msgPollTimer = null;
// 图片懒加载观察器
this.imageObserver = null;
}
init() {
this.initImageObserver();
this.injectHomeStyles();
this.root = document.createElement('div');
this.root.id = 'g-mobile-app';
const user = this.client.currentUser || {};
const isLogin = !!this.client.token;
// 统一逻辑:无论登录与否都显示头像框,未登录显示默认头像和"请登录"
const headPic = (isLogin && user.headPic) ? user.headPic : 'https://www.gululu.world/favicon.ico';
const displayName = isLogin ? (user.nickName || '我') : '请登录';
this.root.innerHTML = `
退出
骨碌碌
🌗
${displayName}
${this.tabs.map(tab => `
${this.getTabInitialHtml(tab)}
`).join('')}
${this.tabs.map(tab => `
${tab.label}
${tab.id === 'msg' ? '
' : ''}
`).join('')}
`;
document.body.appendChild(this.root);
// 绑定 Tab 点击事件
const tabItems = this.root.querySelectorAll('.gm-tab-item');
tabItems.forEach(item => {
item.onclick = () => {
const tabId = item.dataset.tab;
this.switchTab(tabId);
};
});
// 左上角退出手机模式
this.root.querySelector('#gm-home-exit').onclick = () => {
if(confirm("退出手机模式?")) {
config.mobileModeEnabled = false;
localStorage.setItem('gululu_global_config_v6', JSON.stringify(config));
location.reload();
}
};
// 夜间模式初始化与切换
if (config.nightMode) document.body.classList.add('g-night-mode');
this.root.querySelector('#gm-toggle-night').onclick = () => {
document.body.classList.toggle('g-night-mode');
config.nightMode = document.body.classList.contains('g-night-mode');
localStorage.setItem('gululu_global_config_v6', JSON.stringify(config));
};
// 右上角用户区域点击事件 (切换菜单)
const menu = this.root.querySelector('#gm-user-menu');
this.root.querySelector('#gm-user-area').onclick = (e) => {
e.stopPropagation();
menu.classList.toggle('active');
};
// 点击外部关闭菜单
document.addEventListener('click', (e) => {
if (!e.target.closest('#gm-user-menu') && !e.target.closest('#gm-user-area')) {
menu.classList.remove('active');
}
});
// 菜单:UI缩放切换
this.root.querySelector('#gm-menu-scale').onclick = (e) => {
const scales = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5];
let current = config.mobileScale || 1.0;
let idx = scales.findIndex(s => Math.abs(s - current) < 0.01);
if (idx === -1) idx = 3;
idx = (idx + 1) % scales.length;
config.mobileScale = scales[idx];
document.documentElement.style.setProperty('--gm-scale', config.mobileScale);
localStorage.setItem('gululu_global_config_v6', JSON.stringify(config));
e.target.innerText = `UI缩放: ${config.mobileScale * 100}%`;
};
// 菜单:全屏切换
const fsBtn = this.root.querySelector('#gm-menu-fullscreen');
fsBtn.onclick = () => {
if (!document.fullscreenElement) {
sessionStorage.setItem('gm_is_fullscreen', '1'); // 标记意图
document.documentElement.requestFullscreen().catch(e => alert("全屏失败: " + e.message));
} else {
sessionStorage.removeItem('gm_is_fullscreen'); // 清除意图
if (document.exitFullscreen) document.exitFullscreen();
}
menu.classList.remove('active');
};
document.addEventListener('fullscreenchange', () => {
fsBtn.innerHTML = document.fullscreenElement ? '退出全屏 ✕' : '全屏模式 ⛶';
});
// 菜单:登录/退出
this.root.querySelector('#gm-menu-auth').onclick = () => {
menu.classList.remove('active');
if (isLogin) {
this.initLogoutModal();
} else {
this.initLoginModal();
}
};
// ⚡ 异步更新用户信息 (修复头像显示为"我"的问题)
if (isLogin) {
this.client.fetchUserInfo().then(u => {
if (u) {
const nameEl = this.root.querySelector('.gm-user-name');
const imgEl = this.root.querySelector('#gm-user-area img');
if (nameEl) nameEl.textContent = u.nickName;
if (imgEl && u.headPic) imgEl.src = u.headPic;
}
});
}
// 初始化搜索页逻辑
this.initSearchTab();
// 初始化消息页逻辑
this.initMsgTab();
// 初始化排行榜逻辑
this.initRankTab();
// 初始化作品页逻辑
this.initWorksTab();
// 初始化书架页逻辑
this.initShelfTab();
// 全局滚动监听 (用于排行榜/书架无限加载)
const scrollContent = this.root.querySelector('.gm-content');
scrollContent.addEventListener('scroll', () => {
const { scrollTop, scrollHeight, clientHeight } = scrollContent;
if (scrollTop + clientHeight < scrollHeight - 200) return;
if (this.activeTab === 'rank') {
this.loadRankData(true);
} else if (this.activeTab === 'shelf') {
this.loadShelfData(true);
}
}, { passive: true });
}
initLogoutModal() {
if (document.getElementById('gm-logout-modal')) {
document.getElementById('gm-logout-modal').classList.add('active');
return;
}
const modalHtml = `
`;
const div = document.createElement('div');
div.innerHTML = modalHtml;
document.body.appendChild(div.firstElementChild);
const modal = document.getElementById('gm-logout-modal');
modal.querySelector('#gm-logout-cancel').onclick = () => modal.classList.remove('active');
modal.querySelector('#gm-logout-confirm').onclick = () => {
// 强力清除 Cookie (覆盖根域名和当前域名)
const keys = document.cookie.match(/([^ =;]+)=/g);
if (keys) {
for (let i = keys.length; i--;) {
const key = keys[i].substring(0, keys[i].length - 1);
document.cookie = key + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
document.cookie = key + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.gululu.world;';
document.cookie = key + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=gululu.world;';
}
}
localStorage.removeItem('gululu_user_cache');
location.reload();
};
}
initLoginModal() {
if (document.getElementById('gm-login-modal')) {
document.getElementById('gm-login-modal').classList.add('active');
return;
}
const modalHtml = `
`;
const div = document.createElement('div');
div.innerHTML = modalHtml;
document.body.appendChild(div.firstElementChild);
const modal = document.getElementById('gm-login-modal');
const tabs = modal.querySelectorAll('.gm-login-tab');
let loginType = 'pass';
// 切换 Tab
tabs.forEach(tab => tab.onclick = () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
loginType = tab.dataset.type;
document.getElementById('gm-login-form-pass').style.display = loginType === 'pass' ? 'block' : 'none';
document.getElementById('gm-login-form-sms').style.display = loginType === 'sms' ? 'block' : 'none';
});
// 取消
modal.querySelector('#gm-login-cancel').onclick = () => modal.classList.remove('active');
// 获取验证码
modal.querySelector('#gm-get-code').onclick = async (e) => {
const btn = e.target;
const phone = document.getElementById('gm-login-phone-sms').value;
if (!phone) return alert("请输入手机号");
btn.disabled = true;
const res = await this.client.sendSmsCode(phone);
if (res.code === 200) {
let sec = 60;
const timer = setInterval(() => {
btn.innerText = `${sec--}s`;
if (sec < 0) {
clearInterval(timer);
btn.disabled = false;
btn.innerText = "获取验证码";
}
}, 1000);
} else {
alert(res.msg);
btn.disabled = false;
}
};
// 提交登录
modal.querySelector('#gm-login-submit').onclick = async () => {
let res;
if (loginType === 'pass') {
const phone = document.getElementById('gm-login-phone').value;
const pass = document.getElementById('gm-login-pass').value;
if (!phone || !pass) return alert("请填写完整");
res = await this.client.loginByPass(phone, pass);
} else {
const phone = document.getElementById('gm-login-phone-sms').value;
const code = document.getElementById('gm-login-code').value;
if (!phone || !code) return alert("请填写完整");
res = await this.client.loginBySms(phone, code);
}
if (res.code === 200 && res.data && res.data.token) {
// 写入 Cookie 并刷新
const token = res.data.token;
document.cookie = `token=${token}; path=/; max-age=2592000`; // 30天
document.cookie = `Admin-Token=${token}; path=/; max-age=2592000`;
location.reload();
} else {
alert(res.msg || "登录失败");
}
};
}
injectHomeStyles() {
const style = document.createElement('style');
const css = `
.gm-search-container { padding: 40px 20px; margin-top: 40px; }
.gm-search-container.compact { margin-top: 0; padding-bottom: 20px; }
.gm-search-box {
position: relative; width: 100%; height: 110px;
background: #fff; border-radius: 60px;
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
display: flex; align-items: center; padding: 0 40px;
box-sizing: border-box; border: 3px solid transparent;
transition: all 0.3s;
}
.gm-search-box.focused { border-color: #6c5ce7; box-shadow: 0 10px 50px rgba(108, 92, 231, 0.2); }
.gm-search-box.compact { height: 90px; }
.gm-search-icon { width: 48px; height: 48px; fill: #999; margin-right: 25px; flex-shrink: 0; }
.gm-search-input {
flex: 1; border: none; background: transparent;
font-size: 36px; color: #333; outline: none; height: 100%;
font-weight: 500;
}
.gm-search-input::placeholder { color: #ccc; }
.gm-tags-section { margin-top: 80px; padding: 0 20px; }
.gm-tags-title { font-size: 34px; font-weight: 800; color: #333; margin-bottom: 30px; display: flex; justify-content: space-between; align-items: center; }
.gm-tags-list { display: flex; flex-wrap: wrap; gap: 20px; }
.gm-tag-item {
padding: 16px 36px; background: #fff; border-radius: 40px;
font-size: 30px; color: #555; cursor: pointer; font-weight: 500;
box-shadow: 0 4px 15px rgba(0,0,0,0.05); transition: all 0.2s; border: 1px solid #f0f0f0;
}
.gm-tag-item:active { transform: scale(0.95); background: #f0f0f0; }
/* 夜间模式适配 */
body.g-night-mode .gm-search-box { background: #2d2d2d; box-shadow: none; border-color: #444; }
body.g-night-mode .gm-search-box.focused { border-color: #6c5ce7; }
body.g-night-mode .gm-search-input { color: #eee; }
body.g-night-mode .gm-tags-title { color: #eee; }
body.g-night-mode .gm-tag-item { background: #2d2d2d; color: #ccc; border-color: #444; }
body.g-night-mode .gm-user-name { color: #ccc !important; }
/* === 搜索结果页样式 === */
.gm-sub-tabs { display: flex; border-bottom: 1px solid #eee; background: #fff; position: sticky; top: 0; z-index: 10; }
.gm-sub-tab { flex: 1; text-align: center; padding: 25px 0; font-size: 30px; color: #999; font-weight: 600; position: relative; }
.gm-sub-tab.active { color: #6c5ce7; }
.gm-sub-tab.active::after { content: ""; position: absolute; bottom: 0; left: 25%; width: 50%; height: 6px; background: #6c5ce7; border-radius: 6px; }
.gm-result-list { padding: 20px; }
/* 通用卡片基础 */
.gm-res-card { background: #fff; border-radius: 20px; padding: 25px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); display: flex; gap: 25px; }
/* 书籍卡片 (安科) */
.gm-res-book-cover { width: 140px; height: 186px; border-radius: 12px; object-fit: cover; flex-shrink: 0; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
.gm-res-book-info { flex: 1; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; }
.gm-res-title { font-size: 32px; font-weight: bold; color: #333; margin-bottom: 10px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.gm-res-author { font-size: 24px; color: #999; display: flex; align-items: center; gap: 10px; }
.gm-res-tags { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
.gm-res-tag { font-size: 20px; padding: 4px 12px; background: #f0f2f5; color: #666; border-radius: 8px; }
/* 综合卡片 (闲聊/楼层) */
.gm-res-general { flex-direction: column; gap: 15px; }
.gm-res-ctx { font-size: 28px; color: #555; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.gm-res-meta { font-size: 22px; color: #aaa; display: flex; justify-content: space-between; border-top: 1px solid #f9f9f9; padding-top: 15px; }
/* 用户卡片 */
.gm-res-user { align-items: center; }
.gm-res-avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; }
.gm-res-uinfo { flex: 1; }
.gm-res-uname { font-size: 32px; font-weight: bold; color: #333; margin-bottom: 8px; }
.gm-res-ustats { font-size: 24px; color: #999; }
/* 夜间模式 */
body.g-night-mode .gm-sub-tabs { background: #1e1e1e; border-color: #333; }
body.g-night-mode .gm-res-card { background: #2d2d2d; }
body.g-night-mode .gm-res-title, body.g-night-mode .gm-res-uname { color: #eee; }
body.g-night-mode .gm-res-ctx { color: #bbb; }
body.g-night-mode .gm-res-tag { background: #333; color: #aaa; }
/* === 搜索筛选栏 === */
.gm-filter-bar { display: flex; gap: 20px; padding: 0 20px; margin-bottom: 20px; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.gm-select-wrapper { position: relative; flex-shrink: 0; }
.gm-select {
appearance: none; -webkit-appearance: none;
background: #f5f6fa; border: 1px solid #eee;
padding: 12px 60px 12px 30px; border-radius: 30px;
font-size: 26px; font-weight: bold; color: #555;
outline: none; transition: all 0.2s;
}
.gm-select:focus { border-color: #6c5ce7; background: #fff; }
.gm-select-arrow {
position: absolute; right: 20px; top: 50%; transform: translateY(-50%);
width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 10px solid #999;
pointer-events: none;
}
body.g-night-mode .gm-select { background: #2d2d2d; border-color: #444; color: #ccc; }
body.g-night-mode .gm-select:focus { border-color: #6c5ce7; background: #333; }
/* 我的作品卡片 - 大图模式 */
.gm-work-card { flex-direction: column; padding: 30px; }
.gm-work-header { display: flex; gap: 30px; margin-bottom: 25px; }
.gm-work-cover { width: 180px; height: 240px; border-radius: 16px; object-fit: cover; box-shadow: 0 5px 15px rgba(0,0,0,0.1); flex-shrink: 0; }
.gm-work-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.gm-work-title { font-size: 36px; font-weight: 800; color: #333; margin-bottom: 15px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.gm-work-intro { font-size: 26px; color: #888; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.gm-work-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; background: #f8f9fa; padding: 25px; border-radius: 20px; }
.gm-stat-item { text-align: center; }
.gm-stat-num { font-size: 32px; font-weight: bold; color: #333; margin-bottom: 5px; }
.gm-stat-label { font-size: 22px; color: #999; }
.gm-work-footer { margin-top: 25px; padding-top: 20px; border-top: 1px solid #f0f0f0; font-size: 24px; color: #aaa; text-align: right; }
body.g-night-mode .gm-work-title { color: #eee; }
body.g-night-mode .gm-work-intro { color: #aaa; }
body.g-night-mode .gm-work-stats { background: #333; }
body.g-night-mode .gm-stat-num { color: #ddd; }
body.g-night-mode .gm-work-footer { border-color: #333; }
/* === 书架样式 === */
.gm-shelf-toolbar { display: flex; justify-content: space-between; align-items: center; padding: 20px 30px; background: #fff; margin-bottom: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.03); position: sticky; top: 0; z-index: 10; }
.gm-view-toggle { font-size: 28px; padding: 10px 24px; background: #f0f2f5; border-radius: 30px; color: #666; cursor: pointer; display: flex; align-items: center; gap: 10px; font-weight: bold; }
.gm-view-toggle:active { transform: scale(0.95); }
/* 列表模式进度条 */
.gm-shelf-progress { margin-top: 15px; font-size: 24px; color: #6c5ce7; font-weight: bold; display: flex; align-items: center; gap: 15px; }
.gm-progress-bar { flex: 1; height: 10px; background: #eee; border-radius: 5px; overflow: hidden; }
.gm-progress-fill { height: 100%; background: #6c5ce7; border-radius: 5px; }
/* 网格模式 */
.gm-shelf-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; padding: 0 20px 40px 20px; }
.gm-grid-item { position: relative; aspect-ratio: 0.7; border-radius: 16px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.1); background: #eee; cursor: pointer; }
.gm-grid-cover { width: 100%; height: 100%; object-fit: cover; }
.gm-grid-overlay { position: absolute; bottom: 0; left: 0; width: 100%; background: linear-gradient(to top, rgba(0,0,0,0.8), transparent); padding: 20px 10px 10px; color: white; box-sizing: border-box; }
.gm-grid-title { font-size: 22px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gm-grid-info { font-size: 18px; color: #ddd; margin-top: 4px; display: flex; justify-content: space-between; }
.gm-grid-badge { position: absolute; top: 10px; right: 10px; background: #ff4757; color: white; font-size: 16px; padding: 4px 8px; border-radius: 8px; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
body.g-night-mode .gm-shelf-toolbar { background: #1e1e1e; box-shadow: none; border-bottom: 1px solid #333; }
body.g-night-mode .gm-view-toggle { background: #333; color: #aaa; }
body.g-night-mode .gm-grid-item { border: 1px solid #333; }
/* === 消息页样式 === */
.gm-msg-card {
background: #fff; padding: 25px; margin-bottom: 20px;
border-radius: 20px; display: flex; gap: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.03); cursor: pointer;
}
.gm-msg-card:active { background: #f9f9f9; }
.gm-msg-avatar { width: 90px; height: 90px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.gm-msg-body { flex: 1; overflow: hidden; display: flex; flex-direction: column; justify-content: center; }
.gm-msg-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.gm-msg-user { font-size: 30px; font-weight: bold; color: #333; }
.gm-msg-time { font-size: 22px; color: #aaa; }
.gm-msg-content { font-size: 28px; color: #555; line-height: 1.4; margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.gm-msg-source {
font-size: 22px; color: #999; background: #f5f6fa;
padding: 8px 16px; border-radius: 10px; align-self: flex-start;
max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 消息红点 */
.gm-tab-badge {
position: absolute; top: 15px; right: 20%;
background: #ff4757; color: white;
font-size: 20px; height: 32px; min-width: 32px; padding: 0 8px;
border-radius: 16px; display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 5px rgba(255, 71, 87, 0.3); z-index: 20;
}
/* 底部导航红点 */
.gm-tab-badge-main {
position: absolute; top: 5px; left: 50%; margin-left: 10px;
background: #ff4757; color: white;
font-size: 20px; height: 32px; min-width: 32px; padding: 0 6px;
border-radius: 16px; display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 5px rgba(255, 71, 87, 0.3); z-index: 25;
font-weight: bold; border: 2px solid #fff;
}
body.g-night-mode .gm-tab-badge-main { border-color: #2d2d2d; }
body.g-night-mode .gm-msg-card { background: #2d2d2d; }
body.g-night-mode .gm-msg-user { color: #eee; }
body.g-night-mode .gm-msg-content { color: #ccc; }
body.g-night-mode .gm-msg-source { background: #333; color: #888; }
/* 图片懒加载动画 */
img.lazy-load { opacity: 0; transition: opacity 0.3s ease-in; }
img.lazy-loaded { opacity: 1; }
/* 强制覆盖首页背景 */
body.g-night-mode, body.g-night-mode #g-mobile-app {
background-color: #121212 !important;
}
/* === 用户下拉菜单 === */
.gm-user-menu {
position: absolute; top: 120px; right: 20px;
background: #fff; border-radius: 24px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
display: none; flex-direction: column;
z-index: 10000; overflow: hidden;
min-width: 280px; border: 1px solid #eee;
}
.gm-user-menu.active { display: flex; animation: gm-fade-in 0.2s ease; }
.gm-user-menu-item {
padding: 30px 40px; font-size: 30px; font-weight: bold;
color: #333; text-align: center; border-bottom: 1px solid #f5f5f5;
cursor: pointer; transition: background 0.1s;
}
.gm-user-menu-item:active { background: #f5f6fa; }
.gm-user-menu-item:last-child { border-bottom: none; }
body.g-night-mode .gm-user-menu { background: #2d2d2d; border-color: #444; }
body.g-night-mode .gm-user-menu-item { color: #ccc; border-bottom-color: #444; }
body.g-night-mode .gm-user-menu-item:active { background: #333; }
/* === 缩放适配补丁 === */
.gm-navbar-exit { font-size: 30px; }
.gm-user-name { font-size: 26px; font-weight: bold; color: #666; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gm-res-meta-bottom { font-size: 22px; color: #aaa; margin-top: 10px; }
.gm-res-meta-shelf { font-size: 22px; color: #aaa; margin-top: 10px; display: flex; justify-content: space-between; }
.gm-work-id { font-size: 26px; line-height: 1.4; color: #aaa; }
.gm-res-title-general { font-size: 30px; }
/* 补漏:头像、夜间按钮、阅读进度 */
.gm-navbar-avatar { width: 70px !important; height: 70px !important; border-radius: 50% !important; border: 2px solid rgba(0,0,0,0.1); object-fit: cover; display: block; }
.gm-navbar-night { font-size: 36px; margin-right: 15px; }
.gm-shelf-progress-num { font-size: 20px; color: #999; }
body.g-night-mode .gm-user-name { color: #ccc; }
`;
style.textContent = css.replace(/(\d+)px/g, 'calc($1px * var(--gm-scale))');
document.head.appendChild(style);
}
initImageObserver() {
if ('IntersectionObserver' in window) {
this.imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src) {
img.src = src;
img.onload = () => img.classList.add('lazy-loaded');
img.removeAttribute('data-src');
img.classList.remove('lazy-load');
observer.unobserve(img);
}
}
});
}, { rootMargin: '300px 0px' }); // 提前300px加载,保证流畅
}
}
observeNewImages(container) {
if (!this.imageObserver || !container) return;
container.querySelectorAll('img.lazy-load').forEach(img => this.imageObserver.observe(img));
}
getTabInitialHtml(tab) {
if (tab.id === 'rank') {
return `
没有更多了
`;
}
if (tab.id === 'msg') {
return `
`;
}
if (tab.id === 'search') {
return `
${this.renderFilterBar()}
`;
}
if (tab.id === 'works') {
return ``;
}
if (tab.id === 'shelf') {
return `
没有更多了
`;
}
return `
`;
}
initSearchTab() {
const input = this.root.querySelector('.gm-search-input');
const box = this.root.querySelector('.gm-search-box');
const form = this.root.querySelector('.gm-search-form');
if (!input) return;
// 聚焦特效
input.onfocus = () => box.classList.add('focused');
input.onblur = () => box.classList.remove('focused');
// 回车搜索 (表单提交)
if (form) {
form.onsubmit = (e) => {
e.preventDefault(); // 阻止页面刷新
this.doSearch(input.value);
input.blur(); // 提交后收起键盘
};
} else {
// 兜底逻辑
input.onkeydown = (e) => {
if(e.key === 'Enter') this.doSearch(input.value);
};
}
// 绑定筛选/排序变化
this.bindFilterEvents(this.root);
// 加载数据
this.loadSearchData();
}
async loadSearchData() {
const [hot, history] = await Promise.all([
this.client.fetchHotSearch(),
this.client.fetchSearchHistory()
]);
this.renderTags('history', history);
this.renderTags('hot', hot);
}
renderTags(type, data) {
const container = this.root.querySelector(`#gm-tags-${type}`);
if(!container) return;
const section = container.closest('.gm-tags-section');
if(!data || data.length === 0) {
section.style.display = 'none';
return;
}
section.style.display = 'block';
container.innerHTML = data.map(t => `${t}
`).join('');
// 绑定点击
container.querySelectorAll('.gm-tag-item').forEach(el => {
el.onclick = () => this.doSearch(el.innerText);
});
// 绑定清空按钮 (仅针对历史记录)
if (type === 'history') {
const clearBtn = section.querySelector('#gm-clear-history');
if (clearBtn) {
clearBtn.onclick = async () => {
if (confirm('确定要清空搜索历史吗?')) {
const success = await this.client.clearSearchHistory();
if (success) {
section.style.display = 'none';
container.innerHTML = '';
} else {
alert('清空失败,请重试');
}
}
};
}
}
}
renderFilterBar() {
return `
`;
}
bindFilterEvents(container) {
const sortSelect = container.querySelector('#gm-filter-sort');
const wordsSelect = container.querySelector('#gm-filter-words');
if(sortSelect) {
sortSelect.onchange = (e) => {
this.searchOptions.sort = e.target.value;
// 如果在结果页且当前是OPUS,刷新结果
if(document.getElementById('gm-res-list') && document.querySelector('.gm-sub-tab[data-type="OPUS"].active')) {
const keyword = container.querySelector('.gm-search-input').value;
this.loadSubTab('OPUS', keyword);
}
};
}
if(wordsSelect) {
wordsSelect.onchange = (e) => {
this.searchOptions.minWords = parseInt(e.target.value);
if(document.getElementById('gm-res-list') && document.querySelector('.gm-sub-tab[data-type="OPUS"].active')) {
const keyword = container.querySelector('.gm-search-input').value;
this.loadSubTab('OPUS', keyword);
}
};
}
}
doSearch(keyword) {
if(!keyword || !keyword.trim()) return;
keyword = keyword.trim();
// 1. 隐藏标签页,显示结果容器
const container = this.root.querySelector('#gm-tab-content-search');
container.innerHTML = ''; // 清空旧内容
// 2. 调整搜索框样式 (变紧凑)
const searchBox = this.root.querySelector('.gm-search-container');
if(searchBox) searchBox.style.marginTop = '0';
// 3. 渲染结果页框架
this.renderResultPage(container, keyword);
}
renderResultPage(container, keyword) {
// 顶部:紧凑的搜索框
const html = `
${this.renderFilterBar()}
`;
container.innerHTML = html;
// 重新绑定搜索框事件
const form = container.querySelector('.gm-search-form');
const input = container.querySelector('.gm-search-input');
if (form) {
form.onsubmit = (e) => {
e.preventDefault();
this.doSearch(input.value);
input.blur();
};
}
// 重新绑定筛选栏事件 (因为 renderResultPage 重新渲染了筛选栏)
this.bindFilterEvents(container);
// 绑定 Tab 切换
const tabs = container.querySelectorAll('.gm-sub-tab');
tabs.forEach(tab => {
tab.onclick = () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
this.loadSubTab(tab.dataset.type, keyword);
};
});
// 默认加载安科
this.loadSubTab('OPUS', keyword);
}
async loadSubTab(type, keyword) {
const list = document.getElementById('gm-res-list');
list.innerHTML = '加载中...
';
let html = '';
// 只有在 OPUS 模式下才显示/使用筛选栏
const filterBar = this.root.querySelector('.gm-filter-bar');
if (filterBar) {
filterBar.style.display = (type === 'OPUS') ? 'flex' : 'none';
}
const PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
if (type === 'USER') {
const users = await this.client.searchUser(keyword);
if (!users || users.length === 0) {
list.innerHTML = '未找到相关用户
';
return;
}
html = users.map(u => `
${this.highlight(u.nickName, keyword)}
作品: ${u.opusNum} · 粉丝: ${u.fansNum}
`).join('');
}
else {
if (type === 'OPUS') {
// 2. 聚合搜索 (前10页)
const items = await this.fetchOpusAggregated(keyword);
if (!items || items.length === 0) {
list.innerHTML = '未找到相关安科
';
return;
}
html = items.map(item => `
${this.renderHl(item.opusNameHighlight, item.opusName)}
${this.renderHl(item.authorNameHighlight, item.authorName)}
${this.renderTagsHtml(item.tagNamesHighlight, item.tagNames)}
${item.words}字 · ${item.readNum}热度 · ${item.updateTime}
`).join('');
} else {
// ALL (综合)
const res = await this.client.searchGeneral(keyword, type, 1);
const items = res ? res.items : [];
if (!items || items.length === 0) {
list.innerHTML = '未找到相关内容
';
return;
}
html = items.map(item => `
${item.title || '无标题'}
${this.renderHl(item.contextHighlight, item.context)}
${item.createTime}
回复: ${item.commentNum}
`).join('');
}
}
list.innerHTML = html;
this.observeNewImages(list); // 启动懒加载监听
}
// === 消息逻辑 ===
initMsgTab() {
const tabsContainer = this.root.querySelector('#gm-msg-tabs');
if(!tabsContainer) return;
// 更新红点
this.updateMsgBadges();
this.startMsgPolling();
// 绑定 Tab 切换
const tabs = tabsContainer.querySelectorAll('.gm-sub-tab');
tabs.forEach(tab => {
tab.onclick = () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// 1. 清除当前Tab红点
const badge = tab.querySelector('.gm-tab-badge');
if (badge) {
badge.style.display = 'none';
badge.innerText = '0'; // 归零以便后续计算
}
// 2. 重新计算底部总未读数
let total = 0;
tabsContainer.querySelectorAll('.gm-tab-badge').forEach(b => {
if (b.style.display !== 'none') {
total += (parseInt(b.innerText) || 0);
}
});
// 3. 更新底部导航红点
const mainBadge = document.getElementById('gm-tab-badge-msg');
if (mainBadge) {
if (total > 0) {
mainBadge.style.display = 'flex';
mainBadge.innerText = total > 99 ? '99+' : total;
} else {
mainBadge.style.display = 'none';
}
}
this.loadMsgData(tab.dataset.type);
};
});
// 默认加载回复
this.loadMsgData(2);
}
startMsgPolling() {
if (this.msgPollTimer) clearInterval(this.msgPollTimer);
// 60秒轮询一次,避免过于频繁请求造成服务器压力
this.msgPollTimer = setInterval(() => {
this.updateMsgBad
ges();
}, 60000);
}
async updateMsgBadges() {
if (!this.client.token) return;
const data = await this.client.fetchMessageCount();
let total = 0;
// getReplyNum -> type 2
if(data.getReplyNum > 0) {
total += data.getReplyNum;
const el = document.getElementById('gm-badge-reply');
if(el) { el.style.display = 'flex'; el.innerText = data.getReplyNum; }
}
// getGoodNum -> type 1
if(data.getGoodNum > 0) {
total += data.getGoodNum;
const el = document.getElementById('gm-badge-good');
if(el) { el.style.display = 'flex'; el.innerText = data.getGoodNum; }
}
// getAdviceNum -> type 0
if(data.getAdviceNum > 0) {
total += data.getAdviceNum;
const el = document.getElementById('gm-badge-sys');
if(el) { el.style.display = 'flex'; el.innerText = data.getAdviceNum; }
}
// 更新底部导航栏红点
const mainBadge = document.getElementById('gm-tab-badge-msg');
if(mainBadge) {
if(total > 0) {
mainBadge.style.display = 'flex';
mainBadge.innerText = total > 99 ? '99+' : total;
} else {
mainBadge.style.display = 'none';
}
}
}
async loadMsgData(type) {
const list = document.getElementById('gm-msg-list');
if(!list) return;
if (!this.client.token) {
list.innerHTML = '请先登录
';
return;
}
list.innerHTML = '加载中...
';
const msgs = await this.client.fetchMessages(type);
if (!msgs || msgs.length === 0) {
list.innerHTML = '暂无消息
';
return;
}
list.innerHTML = msgs.map(msg => {
const user = {
nickName: msg.nickName || '系统',
headPic: msg.headPic || 'https://www.gululu.world/favicon.ico'
};
// 时间格式化
const date = new Date(msg.createTime);
const timeStr = `${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2,'0')}`;
// 构建跳转链接
let jumpUrl = '';
if(msg.opusId) {
jumpUrl = `/book/${msg.opusId}`;
if(msg.floorId) jumpUrl += `?floorId=${msg.floorId}`;
}
return `
${user.nickName}
${timeStr}
${msg.content || '(无内容)'}
${msg.title ? `
来源: ${msg.title}
` : ''}
`;
}).join('');
}
// === 书架逻辑 ===
initShelfTab() {
// 绑定视图切换
const toggleBtn = this.root.querySelector('#gm-shelf-toggle');
if (toggleBtn) {
toggleBtn.onclick = () => {
this.shelfState.mode = this.shelfState.mode === 'list' ? 'grid' : 'list';
toggleBtn.innerHTML = this.shelfState.mode === 'list' ? '列表 ☰' : '网格 ☷';
this.renderShelf(); // 重新渲染,不重新请求
};
}
this.loadShelfData();
}
async loadShelfData(isMore = false) {
const container = document.getElementById('gm-shelf-container');
if (!container) return;
if (!this.client.token) {
container.innerHTML = `
`;
setTimeout(() => {
const btn = container.querySelector('#gm-shelf-login-btn');
if(btn) btn.onclick = () => this.initLoginModal();
}, 0);
return;
}
if (this.shelfState.loading || (!isMore && !this.shelfState.hasMore)) return;
if (isMore && !this.shelfState.hasMore) return;
this.shelfState.loading = true;
const newItems = await this.client.fetchBookshelf(this.shelfState.page);
if (newItems && newItems.length > 0) {
this.shelfState.data = [...this.shelfState.data, ...newItems];
this.shelfState.page++;
this.renderShelf();
} else {
this.shelfState.hasMore = false;
const noMoreEl = document.getElementById('gm-shelf-nomore');
if(noMoreEl) noMoreEl.style.display = 'block';
if(!isMore && this.shelfState.data.length === 0) {
container.innerHTML = '书架空空如也
';
}
}
this.shelfState.loading = false;
}
renderShelf() {
const container = document.getElementById('gm-shelf-container');
const data = this.shelfState.data;
if(!container || data.length === 0) return;
const PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
if (this.shelfState.mode === 'list') {
container.className = 'gm-result-list'; // 复用列表容器样式
container.innerHTML = data.map(book => {
const progress = book.readInFloorInt || 0;
const total = book.floorNum || 1;
const percent = Math.min(100, Math.round((progress / total) * 100));
return `
${book.opusName}
${book.nickName}
${book.updateTime.split(' ')[0]} 更新
${book.enReadFloorInt > book.readInFloorInt ? '● 有新内容' : ''}
`;
}).join('');
} else {
container.className = 'gm-shelf-grid';
container.innerHTML = data.map(book => {
const hasNew = book.enReadFloorInt > book.readInFloorInt;
return `
${book.opusName}
${book.readInFloorInt}/${book.floorNum}
${hasNew ? '
NEW
' : ''}
`;
}).join('');
}
// 更新统计
const statEl = document.getElementById('gm-shelf-stat');
if(statEl) statEl.innerText = `我的书架 (${data.length})`;
this.observeNewImages(container);
}
// === 我的作品逻辑 ===
initWorksTab() {
this.loadWorksData();
}
async loadWorksData() {
const container = document.getElementById('gm-works-list');
if (!container) return;
if (!this.client.token) {
container.innerHTML = `
`;
setTimeout(() => {
const btn = container.querySelector('#gm-works-login-btn');
if (btn) btn.onclick = () => this.initLoginModal();
}, 0);
return;
}
container.innerHTML = '加载中...
';
const works = await this.client.fetchMyWorks();
if (!works || works.length === 0) {
container.innerHTML = '暂无作品
';
return;
}
// 按更新时间降序排序
works.sort((a, b) => {
const tA = new Date(a.updateTime.replace(/-/g, '/')).getTime();
const tB = new Date(b.updateTime.replace(/-/g, '/')).getTime();
return tB - tA;
});
const PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
container.innerHTML = works.map(w => `
${this.formatNum(w.words)}
字数
${this.formatNum(w.readNum)}
热度
${this.formatNum(w.collectNum)}
收藏
${this.formatNum(w.commentNum)}
评论
`).join('');
this.observeNewImages(container);
}
formatNum(num) {
if (!num) return '0';
if (num > 10000) return (num / 10000).toFixed(1) + 'w';
return num;
}
// === 排行榜逻辑 ===
initRankTab() {
const tabsContainer = this.root.querySelector('#gm-rank-tabs');
if(!tabsContainer) return; // 尚未渲染
// 绑定 Tab 点击
const tabs = tabsContainer.querySelectorAll('.gm-sub-tab');
tabs.forEach(tab => {
tab.onclick = () => {
const type = tab.dataset.type;
if(this.rankState.type === type) return;
// 切换高亮
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// 重置状态
this.rankState.type = type;
this.rankState.page = 1;
this.rankState.hasMore = true;
this.rankState.loading = false;
// 清空列表
document.getElementById('gm-rank-list').innerHTML = '加载中...
';
document.getElementById('gm-rank-nomore').style.display = 'none';
this.loadRankData();
};
});
// 初始加载
this.loadRankData();
}
async loadRankData(isMore = false) {
if (this.rankState.loading || (!isMore && !this.rankState.hasMore)) return;
if (isMore && !this.rankState.hasMore) return;
this.rankState.loading = true;
const list = document.getElementById('gm-rank-list');
const type = this.rankState.type;
// 如果是第一页,清空列表 (防止初始的"加载中"残留)
if (!isMore && this.rankState.page === 1) {
// list.innerHTML = ''; // 保持"加载中"提示直到数据回来
}
let items = [];
try {
if (type === 'NEW') {
// 新人榜无分页,一次性加载
if (this.rankState.page > 1) {
this.rankState.hasMore = false;
} else {
items = await this.client.fetchNewcomerRank();
this.rankState.hasMore = false; // 新人榜只有一页
}
} else {
// 其他榜单
let sortId = 0;
if (type === 'UPDATE') sortId = 1;
else if (type === 'RELEASE') sortId = 2;
else if (type === 'VIEW') sortId = 0;
items = await this.client.fetchRankList(sortId, this.rankState.page);
if (!items || items.length === 0) {
this.rankState.hasMore = false;
} else {
this.rankState.page++;
}
}
} catch(e) {
console.error(e);
this.rankState.hasMore = false;
}
// 渲染
if (!isMore && list.querySelector('div[style*="text-align:center"]')) {
list.innerHTML = '';
}
if (items && items.length > 0) {
const PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
const html = items.map(item => `
${item.opusName}
${item.nickName || item.authorName}
${item.tagNames ? item.tagNames.split(' ').map(t => `${t}`).join('') : ''}
${item.words}字 · ${item.readNum}热度 · ${item.updateTime}
`).join('');
list.insertAdjacentHTML('beforeend', html);
this.observeNewImages(list); // 监听新加入的图片
} else if (!isMore) {
list.innerHTML = '暂无数据
';
}
if (!this.rankState.hasMore) {
document.getElementById('gm-rank-nomore').style.display = 'block';
}
this.rankState.loading = false;
}
async fetchOpusAggregated(keyword) {
const list = document.getElementById('gm-res-list');
list.innerHTML = '正在聚合全站数据...
请求前10页
';
// 1. 并发请求前10页
const promises = [];
for (let i = 1; i <= 10; i++) {
promises.push(this.client.searchGeneral(keyword, 'OPUS', i));
}
const results = await Promise.all(promises);
// 2. 合并结果
let allItems = [];
results.forEach(res => {
if (res && res.items) allItems.push(...res.items);
});
// 3. 客户端筛选 (字数)
const minWords = this.searchOptions.minWords;
if (minWords > 0) {
allItems = allItems.filter(item => (item.words || 0) >= minWords);
}
// 4. 客户端排序
if (this.searchOptions.sort === 'time') {
allItems.sort((a, b) => {
const tA = new Date(a.updateTime.replace(/-/g, '/')).getTime();
const tB = new Date(b.updateTime.replace(/-/g, '/')).getTime();
return tB - tA; // 降序
});
} else {
// 默认排序
}
return allItems;
}
// 辅助:渲染高亮数组 [{text, color}, ...]
renderHl(hlArr, defaultText) {
if (!hlArr || hlArr.length === 0) return defaultText;
return hlArr.map(h => `${h.text}`).join('');
}
// 辅助:简单高亮 (用于用户搜索)
highlight(text, key) {
if(!key) return text;
return text.replace(new RegExp(key, 'gi'), match => `${match}`);
}
// 辅助:渲染标签 (标签也是高亮数组格式)
renderTagsHtml(hlArr, rawStr) {
if (!hlArr || hlArr.length === 0) {
return rawStr ? rawStr.split(' ').map(t => `${t}`).join('') : '';
}
return rawStr.split(' ').map(t => `${t}`).join('');
}
switchTab(tabId) {
if (this.activeTab === tabId) return;
this.activeTab = tabId;
// 如果切换到消息页,立即刷新一下未读数
if (tabId === 'msg') {
this.updateMsgBadges();
}
// 更新底部高亮
this.root.querySelectorAll('.gm-tab-item').forEach(el => {
el.classList.toggle('active', el.dataset.tab === tabId);
});
// 更新内容显示
this.root.querySelectorAll('.gm-tab-content').forEach(el => {
el.classList.toggle('active', el.id === `gm-tab-content-${tabId}`);
});
}
}
// ==========================================
// 4. 稳健启动
// ==========================================
const client = new GululuApiClient();
const startApp = () => {
const path = window.location.pathname;
// 🏠 场景 A:首页 App 模式
if (path === '/' || path === '' || path === '/index.html') {
// 尝试初始化数据以获取 Token
client.initData();
const ui = new MobileHomeUI(client);
ui.init();
return;
}
// 📖 场景 B:阅读器模式
if (path.startsWith('/book/')) {
const initialFloors = client.initData();
if (initialFloors) {
const ui = new MobileUI(client);
ui.init(initialFloors);
} else {
setTimeout(() => {
const retryFloors = client.initData();
if (retryFloors) {
const ui = new MobileUI(client);
ui.init(retryFloors);
} else {
let attempts = 0;
const poller = setInterval(() => {
const f = client.initData();
if (f) {
clearInterval(poller);
const ui = new MobileUI(client);
ui.init(f);
}
if (++attempts > 20) clearInterval(poller);
}, 500);
}
}, 500);
}
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startApp);
} else {
startApp();
}
})();