feat(Claude): 左右翻页

This commit is contained in:
irony 2025-12-06 20:11:54 +08:00
parent b8377cbb80
commit 3a4ef41428
3 changed files with 370 additions and 40 deletions

View File

@ -9,11 +9,24 @@ const Reader = (function() {
let toolbarVisible = false; let toolbarVisible = false;
let toolbarTimer = null; let toolbarTimer = null;
// 翻页模式相关状态
let currentReadingMode = 'page'; // 'scroll' 或 'page'
let currentPage = 0;
let totalPages = 0;
let pageContents = []; // 分页后的内容
let touchStartX = 0;
let touchStartY = 0;
let touchEndX = 0;
let touchEndY = 0;
let isSwiping = false;
// ========== 初始化阅读器 ========== // ========== 初始化阅读器 ==========
async function init(novelId, chapterIndex = 0) { async function init(novelId, chapterIndex = 0) {
currentNovelId = novelId; currentNovelId = novelId;
currentChapterIndex = chapterIndex; currentChapterIndex = chapterIndex;
currentReadingMode = Store.getReadingMode();
currentPage = 0;
// 隐藏导航栏 // 隐藏导航栏
$('#app-header').addClass('header-hidden'); $('#app-header').addClass('header-hidden');
@ -98,8 +111,11 @@ const Reader = (function() {
} }
const $content = $('#app-content'); const $content = $('#app-content');
if (currentReadingMode === 'scroll') {
// 上下滚动模式
$content.html(` $content.html(`
<div class="reader-page"> <div class="reader-page scroll-mode">
<div class="reader-content"> <div class="reader-content">
<h1 class="reader-chapter-title">${chapter.title}</h1> <h1 class="reader-chapter-title">${chapter.title}</h1>
<div class="reader-text">${content}</div> <div class="reader-text">${content}</div>
@ -116,11 +132,148 @@ const Reader = (function() {
</div> </div>
</div> </div>
`); `);
} else {
// 左右翻页模式
$content.html(`
<div class="reader-page page-mode">
<div class="page-container">
<div class="page-content">
<h1 class="reader-chapter-title">${chapter.title}</h1>
<div class="reader-text">${content}</div>
</div>
</div>
<div class="page-indicator">
<span class="page-current">1</span> / <span class="page-total">1</span>
</div>
<div class="page-tap-zones">
<div class="tap-zone tap-prev"></div>
<div class="tap-zone tap-center"></div>
<div class="tap-zone tap-next"></div>
</div>
</div>
`);
// 等待DOM渲染后进行分页计算
setTimeout(() => {
calculatePages();
showPage(currentPage);
}, 100);
}
// 应用设置 // 应用设置
applySettings(); applySettings();
} }
// ========== 翻页模式相关函数 ==========
function calculatePages() {
if (currentReadingMode !== 'page') return;
const $container = $('.page-container');
const $content = $('.page-content');
if (!$container.length || !$content.length) return;
const containerHeight = $container.height();
const contentHeight = $content[0].scrollHeight;
// 计算总页数
totalPages = Math.max(1, Math.ceil(contentHeight / containerHeight));
pageContents = [];
// 更新页码显示
$('.page-total').text(totalPages);
// 恢复之前的阅读位置
const position = Store.getReadingPosition(currentNovelId);
if (position && position.chapterIndex === currentChapterIndex && position.pageIndex !== undefined) {
currentPage = Math.min(position.pageIndex, totalPages - 1);
} else {
currentPage = 0;
}
}
function showPage(pageIndex) {
if (currentReadingMode !== 'page') return;
const $container = $('.page-container');
const $content = $('.page-content');
if (!$container.length || !$content.length) return;
// 确保页码在有效范围内
pageIndex = Math.max(0, Math.min(pageIndex, totalPages - 1));
currentPage = pageIndex;
const containerHeight = $container.height();
const scrollTop = pageIndex * containerHeight;
// 使用transform进行平滑翻页
$content.css('transform', `translateY(-${scrollTop}px)`);
// 更新页码显示
$('.page-current').text(currentPage + 1);
// 保存阅读位置
saveReadingProgress();
// 更新进度条
updateProgressIndicator();
}
function nextPage() {
if (currentReadingMode !== 'page') return;
if (currentPage < totalPages - 1) {
showPage(currentPage + 1);
} else {
// 已是最后一页,跳转下一章
nextChapter();
}
}
function prevPage() {
if (currentReadingMode !== 'page') return;
if (currentPage > 0) {
showPage(currentPage - 1);
} else {
// 已是第一页,跳转上一章
if (currentChapterIndex > 0) {
// 跳转上一章并显示最后一页
goToPrevChapterLastPage();
} else {
Toast.show('已是第一章');
}
}
}
async function goToPrevChapterLastPage() {
hideToolbar();
try {
const chapter = await API.getPrevChapter(currentNovelId, currentChapterIndex);
if (chapter) {
currentChapter = chapter;
currentChapterIndex = chapter.index;
renderChapter(chapter);
saveReadingProgress();
updateHistory(currentNovelId, chapter.id);
$('#reader-title').text(chapter.title);
// 等待分页计算完成后跳到最后一页
setTimeout(() => {
if (totalPages > 0) {
showPage(totalPages - 1);
}
}, 150);
} else {
Toast.show('已是第一章');
}
} catch (error) {
Toast.show('加载失败');
}
}
// ========== 章节导航 ========== // ========== 章节导航 ==========
async function nextChapter() { async function nextChapter() {
@ -130,6 +283,7 @@ const Reader = (function() {
if (chapter) { if (chapter) {
currentChapter = chapter; currentChapter = chapter;
currentChapterIndex = chapter.index; currentChapterIndex = chapter.index;
currentPage = 0; // 重置页码
renderChapter(chapter); renderChapter(chapter);
scrollToTop(); scrollToTop();
saveReadingProgress(); saveReadingProgress();
@ -154,6 +308,7 @@ const Reader = (function() {
if (chapter) { if (chapter) {
currentChapter = chapter; currentChapter = chapter;
currentChapterIndex = chapter.index; currentChapterIndex = chapter.index;
currentPage = 0; // 重置页码
renderChapter(chapter); renderChapter(chapter);
scrollToTop(); scrollToTop();
saveReadingProgress(); saveReadingProgress();
@ -170,11 +325,13 @@ const Reader = (function() {
async function goToChapter(index) { async function goToChapter(index) {
hideToolbar(); hideToolbar();
hideCatalog(); hideCatalog();
currentPage = 0; // 重置页码
await loadChapter(currentNovelId, index); await loadChapter(currentNovelId, index);
scrollToTop(); scrollToTop();
} }
function reload() { function reload() {
currentPage = 0;
loadChapter(currentNovelId, currentChapterIndex); loadChapter(currentNovelId, currentChapterIndex);
} }
@ -221,6 +378,7 @@ const Reader = (function() {
function applySettings() { function applySettings() {
const fontSize = Store.getFontSize(); const fontSize = Store.getFontSize();
const lineHeight = Store.getLineHeight(); const lineHeight = Store.getLineHeight();
const readingMode = Store.getReadingMode();
$('.reader-text').css({ $('.reader-text').css({
'--reader-font-size': fontSize + 'px', '--reader-font-size': fontSize + 'px',
@ -239,6 +397,26 @@ const Reader = (function() {
const theme = Store.getTheme(); const theme = Store.getTheme();
$('.theme-btn').removeClass('active'); $('.theme-btn').removeClass('active');
$(`.theme-btn[data-theme="${theme}"]`).addClass('active'); $(`.theme-btn[data-theme="${theme}"]`).addClass('active');
// 更新阅读模式按钮状态
$('.reading-mode-btn').removeClass('active');
$(`.reading-mode-btn[data-mode="${readingMode}"]`).addClass('active');
}
function changeReadingMode(mode) {
if (mode === currentReadingMode) return;
Store.setReadingMode(mode);
currentReadingMode = mode;
currentPage = 0;
// 重新渲染当前章节
if (currentChapter) {
renderChapter(currentChapter);
}
applySettings();
Toast.show(mode === 'page' ? '已切换为翻页模式' : '已切换为滚动模式');
} }
function changeFontSize(delta) { function changeFontSize(delta) {
@ -365,19 +543,31 @@ const Reader = (function() {
function saveReadingProgress() { function saveReadingProgress() {
if (!currentNovelId) return; if (!currentNovelId) return;
if (currentReadingMode === 'page') {
Store.setReadingPosition(currentNovelId, { Store.setReadingPosition(currentNovelId, {
chapterIndex: currentChapterIndex, chapterIndex: currentChapterIndex,
scrollTop: window.scrollY pageIndex: currentPage,
readingMode: 'page'
}); });
} else {
Store.setReadingPosition(currentNovelId, {
chapterIndex: currentChapterIndex,
scrollTop: window.scrollY,
readingMode: 'scroll'
});
}
} }
function restoreScrollPosition() { function restoreScrollPosition() {
const position = Store.getReadingPosition(currentNovelId); const position = Store.getReadingPosition(currentNovelId);
if (position && position.chapterIndex === currentChapterIndex && position.scrollTop) { if (!position || position.chapterIndex !== currentChapterIndex) return;
if (currentReadingMode === 'scroll' && position.scrollTop) {
setTimeout(() => { setTimeout(() => {
window.scrollTo(0, position.scrollTop); window.scrollTo(0, position.scrollTop);
}, 100); }, 100);
} }
// 翻页模式的位置恢复在 calculatePages 中处理
} }
async function updateHistory(novelId, chapterId) { async function updateHistory(novelId, chapterId) {
@ -400,25 +590,99 @@ const Reader = (function() {
} }
function updateProgressIndicator() { function updateProgressIndicator() {
let progress = 0;
if (currentReadingMode === 'page') {
// 翻页模式:基于页码计算进度
progress = totalPages > 1 ? (currentPage / (totalPages - 1)) * 100 : 100;
} else {
// 滚动模式:基于滚动位置计算进度
const scrollTop = window.scrollY; const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight; const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
}
$('.reading-progress-indicator .progress-bar').css('width', progress + '%'); $('.reading-progress-indicator .progress-bar').css('width', progress + '%');
} }
// ========== 事件绑定 ========== // ========== 事件绑定 ==========
function bindEvents() { function bindEvents() {
// 点击内容区域切换工具栏 // 滚动模式:点击内容区域切换工具栏
$(document).off('click.reader').on('click.reader', '.reader-page', function(e) { $(document).off('click.reader').on('click.reader', '.reader-page.scroll-mode', function(e) {
// 排除按钮点击 // 排除按钮点击
if ($(e.target).closest('button, a').length) return; if ($(e.target).closest('button, a').length) return;
toggleToolbar(); toggleToolbar();
}); });
// 滚动事件 // 翻页模式:点击区域处理
$(document).off('click.tapprev').on('click.tapprev', '.tap-prev', function(e) {
e.stopPropagation();
prevPage();
});
$(document).off('click.tapcenter').on('click.tapcenter', '.tap-center', function(e) {
e.stopPropagation();
toggleToolbar();
});
$(document).off('click.tapnext').on('click.tapnext', '.tap-next', function(e) {
e.stopPropagation();
nextPage();
});
// 翻页模式:触摸滑动事件
$(document).off('touchstart.reader').on('touchstart.reader', '.reader-page.page-mode', function(e) {
if ($(e.target).closest('button, a, .tap-zone').length) return;
touchStartX = e.originalEvent.touches[0].clientX;
touchStartY = e.originalEvent.touches[0].clientY;
isSwiping = false;
});
$(document).off('touchmove.reader').on('touchmove.reader', '.reader-page.page-mode', function(e) {
if (!touchStartX) return;
touchEndX = e.originalEvent.touches[0].clientX;
touchEndY = e.originalEvent.touches[0].clientY;
const diffX = touchEndX - touchStartX;
const diffY = touchEndY - touchStartY;
// 判断是否为水平滑动
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 30) {
isSwiping = true;
}
});
$(document).off('touchend.reader').on('touchend.reader', '.reader-page.page-mode', function(e) {
if (!isSwiping) {
touchStartX = 0;
touchStartY = 0;
return;
}
const diffX = touchEndX - touchStartX;
const threshold = 50; // 滑动阈值
if (diffX > threshold) {
// 向右滑动 - 上一页
prevPage();
} else if (diffX < -threshold) {
// 向左滑动 - 下一页
nextPage();
}
touchStartX = 0;
touchStartY = 0;
touchEndX = 0;
touchEndY = 0;
isSwiping = false;
});
// 滚动模式:滚动事件
let scrollTimer; let scrollTimer;
$(window).off('scroll.reader').on('scroll.reader', function() { $(window).off('scroll.reader').on('scroll.reader', function() {
if (currentReadingMode !== 'scroll') return;
// 更新进度条 // 更新进度条
updateProgressIndicator(); updateProgressIndicator();
@ -437,7 +701,7 @@ const Reader = (function() {
$('#reader-settings').off('click').on('click', showSettings); $('#reader-settings').off('click').on('click', showSettings);
$('#reader-bookmark').off('click').on('click', showBookmarkModal); $('#reader-bookmark').off('click').on('click', showBookmarkModal);
// 章节内导航按钮 // 章节内导航按钮(滚动模式)
$(document).off('click.navprev').on('click.navprev', '#nav-prev', prevChapter); $(document).off('click.navprev').on('click.navprev', '#nav-prev', prevChapter);
$(document).off('click.navnext').on('click.navnext', '#nav-next', nextChapter); $(document).off('click.navnext').on('click.navnext', '#nav-next', nextChapter);
@ -445,17 +709,48 @@ const Reader = (function() {
$('#close-settings').off('click').on('click', hideSettings); $('#close-settings').off('click').on('click', hideSettings);
$('.settings-overlay').off('click').on('click', hideSettings); $('.settings-overlay').off('click').on('click', hideSettings);
$('#font-decrease').off('click').on('click', () => changeFontSize(-2)); $('#font-decrease').off('click').on('click', () => {
$('#font-increase').off('click').on('click', () => changeFontSize(2)); changeFontSize(-2);
// 翻页模式下重新计算分页
if (currentReadingMode === 'page') {
setTimeout(() => {
calculatePages();
showPage(0);
}, 100);
}
});
$('#font-increase').off('click').on('click', () => {
changeFontSize(2);
// 翻页模式下重新计算分页
if (currentReadingMode === 'page') {
setTimeout(() => {
calculatePages();
showPage(0);
}, 100);
}
});
$('.line-height-btn').off('click').on('click', function() { $('.line-height-btn').off('click').on('click', function() {
changeLineHeight($(this).data('value')); changeLineHeight($(this).data('value'));
// 翻页模式下重新计算分页
if (currentReadingMode === 'page') {
setTimeout(() => {
calculatePages();
showPage(0);
}, 100);
}
}); });
$('.theme-btn').off('click').on('click', function() { $('.theme-btn').off('click').on('click', function() {
changeTheme($(this).data('theme')); changeTheme($(this).data('theme'));
}); });
// 阅读模式切换按钮
$(document).off('click.readingmode').on('click.readingmode', '.reading-mode-btn', function() {
const mode = $(this).data('mode');
changeReadingMode(mode);
});
// 目录面板 // 目录面板
$('#close-catalog').off('click').on('click', hideCatalog); $('#close-catalog').off('click').on('click', hideCatalog);
$('.catalog-overlay').off('click').on('click', hideCatalog); $('.catalog-overlay').off('click').on('click', hideCatalog);
@ -477,14 +772,32 @@ const Reader = (function() {
const note = $('#bookmark-note').val(); const note = $('#bookmark-note').val();
addBookmark(note); addBookmark(note);
}); });
// 窗口大小变化时重新计算分页
$(window).off('resize.reader').on('resize.reader', function() {
if (currentReadingMode === 'page') {
setTimeout(() => {
calculatePages();
showPage(currentPage);
}, 100);
}
});
} }
function unbindEvents() { function unbindEvents() {
$(document).off('click.reader'); $(document).off('click.reader');
$(document).off('click.tapprev');
$(document).off('click.tapcenter');
$(document).off('click.tapnext');
$(document).off('touchstart.reader');
$(document).off('touchmove.reader');
$(document).off('touchend.reader');
$(window).off('scroll.reader'); $(window).off('scroll.reader');
$(window).off('resize.reader');
$(document).off('click.navprev'); $(document).off('click.navprev');
$(document).off('click.navnext'); $(document).off('click.navnext');
$(document).off('click.catalogitem'); $(document).off('click.catalogitem');
$(document).off('click.readingmode');
} }
// ========== 退出阅读器 ========== // ========== 退出阅读器 ==========
@ -534,7 +847,10 @@ const Reader = (function() {
goToChapter, goToChapter,
showCatalog, showCatalog,
showSettings, showSettings,
showBookmarkModal showBookmarkModal,
changeReadingMode,
nextPage,
prevPage
}; };
})(); })();

View File

@ -8,7 +8,7 @@ const Store = (function() {
THEME: 'novel_reader_theme', THEME: 'novel_reader_theme',
FONT_SIZE: 'novel_reader_font_size', FONT_SIZE: 'novel_reader_font_size',
LINE_HEIGHT: 'novel_reader_line_height', LINE_HEIGHT: 'novel_reader_line_height',
READ_MODE: 'novel_reader_read_mode', // 阅读模式: 'page'(翻页) | 'scroll'(滚动) READING_MODE: 'novel_reader_reading_mode', // 阅读模式scroll(上下滚动) / page(左右翻页)
READING_POSITION: 'novel_reader_position_', READING_POSITION: 'novel_reader_position_',
SHELF_CACHE: 'novel_reader_shelf_cache', SHELF_CACHE: 'novel_reader_shelf_cache',
HISTORY_CACHE: 'novel_reader_history_cache', HISTORY_CACHE: 'novel_reader_history_cache',
@ -23,7 +23,7 @@ const Store = (function() {
theme: 'dark', theme: 'dark',
fontSize: 18, fontSize: 18,
lineHeight: 1.8, lineHeight: 1.8,
readMode: 'page' // 默认左右翻页模式 readingMode: 'page' // 默认左右翻页模式
}; };
// ========== 基础存储方法 ========== // ========== 基础存储方法 ==========
@ -102,16 +102,15 @@ const Store = (function() {
// ========== 阅读模式设置 ========== // ========== 阅读模式设置 ==========
function getReadMode() { function getReadingMode() {
return get(KEYS.READ_MODE, DEFAULTS.readMode); return get(KEYS.READING_MODE, DEFAULTS.readingMode);
} }
function setReadMode(mode) { function setReadingMode(mode) {
// mode: 'page' (左右翻页) | 'scroll' (上下滚动) // mode: 'scroll' (上下滚动) 或 'page' (左右翻页)
if (mode === 'page' || mode === 'scroll') { if (mode === 'scroll' || mode === 'page') {
set(KEYS.READ_MODE, mode); set(KEYS.READING_MODE, mode);
} }
return getReadMode();
} }
// ========== 阅读位置缓存 ========== // ========== 阅读位置缓存 ==========
@ -227,6 +226,8 @@ const Store = (function() {
setFontSize, setFontSize,
getLineHeight, getLineHeight,
setLineHeight, setLineHeight,
getReadingMode,
setReadingMode,
// 阅读位置 // 阅读位置
getReadingPosition, getReadingPosition,

View File

@ -198,6 +198,19 @@
<button id="font-increase">A+</button> <button id="font-increase">A+</button>
</div> </div>
</div> </div>
<div class="settings-group">
<label>阅读模式</label>
<div class="reading-mode-control">
<button class="reading-mode-btn active" data-mode="page">
<svg viewBox="0 0 24 24"><path d="M18 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 4h5v8l-2.5-1.5L6 12V4z"/></svg>
<span>左右翻页</span>
</button>
<button class="reading-mode-btn" data-mode="scroll">
<svg viewBox="0 0 24 24"><path d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
<span>上下滚动</span>
</button>
</div>
</div>
<div class="settings-group"> <div class="settings-group">
<label>行间距</label> <label>行间距</label>
<div class="line-height-control"> <div class="line-height-control">