2025-12-06 20:47:45 +08:00

608 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* App模块 - 主应用入口和路由
*/
const App = (function() {
// ========== 路由处理 ==========
function initRouter() {
// 监听hash变化
$(window).on('hashchange', handleRoute);
// 初始路由
handleRoute();
}
// 记录上一个路由,用于返回
let lastHash = '#/shelf';
let lastNonReadHash = '#/shelf';
function handleRoute() {
const hash = window.location.hash || '#/shelf';
const prevHash = lastHash;
lastHash = hash;
if (!hash.includes('#/read/')) {
lastNonReadHash = hash;
}
const [path, ...params] = hash.slice(2).split('/');
// 更新导航状态
updateNavState(path);
// 路由匹配
switch (path) {
case 'shelf':
showPage('shelf');
Pages.renderShelf();
break;
case 'explore':
showPage('explore');
Pages.renderExplore();
break;
case 'history':
showPage('history');
Pages.renderHistory();
break;
case 'bookmarks':
showPage('bookmarks');
Pages.renderBookmarks();
break;
case 'novel':
if (params[0]) {
showPage('detail');
Pages.renderDetail(params[0]);
}
break;
case 'read':
if (params[0]) {
const chapterIndex = parseInt(params[1]) || 0;
// 记录进入阅读页前的路由,用于返回时回到详情页或来源页
const backRoute = prevHash && !prevHash.includes('#/read/') ? prevHash : lastNonReadHash;
Store.setState('prevRouteBeforeRead', backRoute || '#/shelf');
showPage('reader');
Reader.init(params[0], chapterIndex);
}
break;
default:
window.location.hash = '#/shelf';
}
}
function showPage(page) {
// 重置UI状态
if (page === 'reader') {
// 阅读器模式:隐藏所有导航
$('#app-header').addClass('header-hidden');
$('#app-nav').addClass('nav-hidden');
$('#app-content').addClass('reader-mode').removeClass('detail-mode');
$('#reader-toolbar').removeClass('hidden');
} else if (page === 'detail') {
// 详情页:隐藏底部导航(有自己的操作栏)
$('#app-header').removeClass('header-hidden');
$('#app-nav').addClass('nav-hidden');
$('#app-content').removeClass('reader-mode').addClass('detail-mode');
$('#reader-toolbar').addClass('hidden');
} else {
// 普通页面:显示所有导航
$('#app-header').removeClass('header-hidden');
$('#app-nav').removeClass('nav-hidden');
$('#app-content').removeClass('reader-mode').removeClass('detail-mode');
$('#reader-toolbar').addClass('hidden');
}
// 更新页面标题
const titles = {
'shelf': '墨香阁',
'explore': '发现',
'history': '阅读历史',
'bookmarks': '我的书签',
'detail': '小说详情',
'reader': ''
};
$('#page-title').text(titles[page] || '墨香阁');
// 显示/隐藏返回按钮
if (['detail'].includes(page)) {
$('#btn-back').removeClass('hidden');
$('#btn-search').addClass('hidden');
} else if (page === 'reader') {
// 阅读器有自己的返回按钮
} else {
$('#btn-back').addClass('hidden');
$('#btn-search').removeClass('hidden');
}
// 滚动到顶部
if (page !== 'reader') {
window.scrollTo(0, 0);
}
}
function updateNavState(page) {
$('.nav-item').removeClass('active');
$(`.nav-item[data-page="${page}"]`).addClass('active');
}
function navigate(hash) {
window.location.hash = hash;
}
// ========== 事件绑定 ==========
function bindEvents() {
// 导航点击
$('.nav-item').on('click', function(e) {
e.preventDefault();
const page = $(this).data('page');
navigate('#/' + page);
});
// 返回按钮 - 使用浏览器历史返回
$('#btn-back').on('click', function() {
window.history.back();
});
// 搜索按钮
$('#btn-search').on('click', function() {
showSearch();
});
// 菜单按钮(暂时不使用)
$('#btn-menu').on('click', function() {
Toast.show('更多功能开发中...');
});
// 搜索面板
$('#search-back').on('click', hideSearch);
// 搜索输入 - 实时搜索
$('#search-input').on('input', debounce(function() {
const keyword = $(this).val().trim();
if (keyword) {
$('#search-clear').removeClass('hidden');
doSearch(keyword);
} else {
$('#search-clear').addClass('hidden');
$('#search-results').html('<div class="search-hint">输入关键词搜索小说</div>');
}
}, 500));
// 搜索输入 - 回车确认搜索
$('#search-input').on('keypress', function(e) {
if (e.which === 13) {
const keyword = $(this).val().trim();
if (keyword) {
doSearch(keyword);
}
}
});
$('#search-clear').on('click', function() {
$('#search-input').val('').focus();
$(this).addClass('hidden');
$('#search-results').html('<div class="search-hint">输入关键词搜索小说</div>');
renderSearchTags(); // 重新显示搜索历史和标签
});
// 搜索历史点击
$(document).on('click', '#search-tags .history-tag', function(e) {
// 检查是否点击了删除按钮
if ($(e.target).hasClass('remove-history') || $(e.target).closest('.remove-history').length) {
return;
}
const keyword = $(this).data('keyword');
$('#search-input').val(keyword);
doSearch(keyword);
});
// 删除单条搜索历史
$(document).on('click', '.remove-history', function(e) {
e.stopPropagation();
const keyword = $(this).data('keyword');
Store.removeSearchHistory(keyword);
renderSearchTags();
});
// 清空搜索历史
$(document).on('click', '#clear-search-history', function() {
Store.clearSearchHistory();
renderSearchTags();
Toast.show('搜索历史已清空');
});
// 搜索页热门标签点击
$(document).on('click', '#search-tags .tag-btn:not(.history-tag)', function() {
const tag = $(this).data('tag');
if (tag) {
$('#search-input').val('');
Pages.renderSearchByTag(tag);
}
});
// 发现页标签点击
$(document).on('click', '#tags-cloud .tag-btn', function() {
$('.tag-btn').removeClass('active');
$(this).addClass('active');
const tag = $(this).data('tag');
Pages.loadNovels(0, tag, null); // 清除作者筛选,只按标签筛选
});
// 加载更多(追加模式,保持当前筛选条件)
$(document).on('click', '.load-more-btn', function() {
if (Pages.hasMore()) {
Pages.loadNovels(Pages.currentPage() + 1, null, true); // append=true 会使用已保存的标签
}
});
// 搜索结果中的小说卡片点击(优先处理)
$(document).on('click', '#search-results .novel-card', function(e) {
e.preventDefault();
e.stopPropagation();
const novelId = $(this).data('novel-id');
if (novelId) {
hideSearch();
// 使用setTimeout确保搜索面板完全隐藏后再跳转
setTimeout(() => {
navigate('#/novel/' + novelId);
}, 100);
}
});
// 点击作者名字,筛选该作者的作品
$(document).on('click', '.clickable-author', function(e) {
e.preventDefault();
e.stopPropagation();
const author = $(this).data('author');
if (author) {
Pages.searchByAuthor(author);
}
});
// 其他页面的小说卡片点击
$(document).on('click', '.novel-card, .novel-grid-card, .shelf-item', function(e) {
// 如果是搜索结果中的,上面的事件已处理,这里跳过
if ($(this).closest('#search-results').length) return;
// 如果点击的是作者名字,跳过(上面的事件已处理)
if ($(e.target).hasClass('clickable-author') || $(e.target).closest('.clickable-author').length) return;
e.preventDefault();
if ($(e.target).closest('button').length) return;
const novelId = $(this).data('novel-id') || $(this).closest('[data-novel-id]').data('novel-id');
if (novelId) {
navigate('#/novel/' + novelId);
}
});
// 章节点击
$(document).on('click', '.chapters-preview .chapter-item', function() {
const novelId = $(this).data('novel-id');
const index = $(this).data('chapter-index');
navigate('#/read/' + novelId + '/' + index);
});
// 查看全部章节
$(document).on('click', '#view-all-chapters', function() {
// 显示目录面板
const novelId = $(this).data('novel-id');
showAllChapters(novelId);
});
// 历史记录点击
$(document).on('click', '.history-item', function(e) {
if ($(e.target).closest('button').length) return;
const novelId = $(this).data('novel-id');
const index = $(this).data('chapter-index');
navigate('#/read/' + novelId + '/' + index);
});
// 书签点击
$(document).on('click', '.bookmark-item', function(e) {
if ($(e.target).closest('button').length) return;
const novelId = $(this).data('novel-id');
const index = $(this).data('chapter-index');
navigate('#/read/' + novelId + '/' + index);
});
// 删除历史
$(document).on('click', '.delete-history-btn', async function(e) {
e.stopPropagation();
const novelId = $(this).data('novel-id');
try {
await API.deleteHistory(novelId, Store.getUserId());
$(this).closest('.history-item').fadeOut(300, function() {
$(this).remove();
});
Toast.show('已删除');
} catch (error) {
Toast.show('删除失败');
}
});
// 删除书签
$(document).on('click', '.delete-bookmark-btn', async function(e) {
e.stopPropagation();
const bookmarkId = $(this).data('bookmark-id');
try {
await API.deleteBookmark(bookmarkId, Store.getUserId());
$(this).closest('.bookmark-item').fadeOut(300, function() {
$(this).remove();
});
Toast.show('已删除');
} catch (error) {
Toast.show('删除失败');
}
});
// 清空历史
$(document).on('click', '#clear-history', function() {
if (confirm('确定要清空所有阅读历史吗?')) {
// 这里需要后端支持批量删除,暂时提示
Toast.show('功能开发中...');
}
});
// 加入/移出书架
$(document).on('click', '#btn-shelf', async function() {
const $btn = $(this);
const novelId = $btn.data('novel-id');
const inShelf = $btn.hasClass('in-shelf');
try {
if (inShelf) {
await API.removeFromShelf(novelId, Store.getUserId());
Store.removeFromShelfCache(novelId);
$btn.removeClass('in-shelf');
$btn.find('svg path').attr('d', 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z');
$btn.contents().last()[0].textContent = '加入书架';
Toast.show('已从书架移除');
// 更新当前页面所有相关书籍卡片的书架标识
$(`.novel-card[data-novel-id="${novelId}"] .shelf-badge, .novel-grid-card[data-novel-id="${novelId}"] .shelf-badge`).remove();
} else {
await API.addToShelf({ userId: Store.getUserId(), novelId });
Store.addToShelfCache(novelId);
$btn.addClass('in-shelf');
$btn.find('svg path').attr('d', 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z');
$btn.contents().last()[0].textContent = '已在书架';
Toast.show('已加入书架');
// 更新当前页面所有相关书籍卡片的书架标识
// 列表视图显示文字,网格视图只显示图标
const badgeHtmlList = '<div class="shelf-badge" title="已在书架"><svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg><span class="shelf-badge-text">已收藏</span></div>';
const badgeHtmlGrid = '<div class="shelf-badge" title="已在书架"><svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg></div>';
$(`.novel-card[data-novel-id="${novelId}"] .novel-cover`).append(badgeHtmlList);
$(`.novel-grid-card[data-novel-id="${novelId}"] .novel-cover`).append(badgeHtmlGrid);
}
} catch (error) {
Toast.show('操作失败');
}
});
// 开始/继续阅读
$(document).on('click', '#btn-read', function() {
const novelId = $(this).data('novel-id');
const index = $(this).data('chapter-index') || 0;
navigate('#/read/' + novelId + '/' + index);
});
// 展开简介
$(document).on('click', '#expand-synopsis', function() {
const $text = $('#synopsis-text');
$text.toggleClass('text-clamp-3');
$(this).text($text.hasClass('text-clamp-3') ? '展开' : '收起');
});
// 加载更多按钮使用事件委托在pages.js中也有绑定这里作为备用
$(document).on('click', '.load-more-btn', function() {
if (Pages.hasMore()) {
Pages.loadNovels(Pages.currentPage() + 1);
}
});
}
// ========== 搜索功能 ==========
async function showSearch() {
$('#search-panel').removeClass('hidden');
$('#search-input').val('').focus();
$('#search-results').html('<div class="search-hint">输入关键词搜索小说</div>');
// 渲染搜索历史和热门标签
renderSearchTags();
}
async function renderSearchTags() {
let html = '';
// 搜索历史
const history = Store.getSearchHistory();
if (history.length > 0) {
html += '<div class="search-section">';
html += '<div class="search-tags-header"><span class="search-tags-title">搜索历史</span>';
html += '<button class="clear-history-btn" id="clear-search-history">清空</button></div>';
html += '<div class="tags-cloud">';
history.forEach(keyword => {
html += `<button class="tag-btn history-tag" data-keyword="${keyword}">
<span>${keyword}</span>
<svg class="remove-history" data-keyword="${keyword}" viewBox="0 0 24 24" width="14" height="14">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>`;
});
html += '</div></div>';
}
// 热门标签
try {
const tags = await API.getPopularTags(10);
if (tags.length > 0) {
html += '<div class="search-section">';
html += '<div class="search-tags-title">热门标签</div><div class="tags-cloud">';
tags.forEach(tag => {
html += `<button class="tag-btn" data-tag="${tag}">${tag}</button>`;
});
html += '</div></div>';
}
} catch (error) {
console.error('加载标签失败');
}
$('#search-tags').html(html);
}
function doSearch(keyword) {
if (!keyword || !keyword.trim()) return;
keyword = keyword.trim();
// 保存到搜索历史
Store.addSearchHistory(keyword);
// 执行搜索
Pages.renderSearchResults(keyword);
}
function hideSearch() {
$('#search-panel').addClass('hidden');
}
// ========== 全部章节面板 ==========
async function showAllChapters(novelId) {
$('#catalog-panel').removeClass('hidden');
const $list = $('#catalog-list');
$list.html('<div class="chapter-loading"><div class="loading-spinner"></div></div>');
try {
let allChapters = [];
let page = 0;
let hasMore = true;
while (hasMore) {
const data = await API.getChapters(novelId, { page, size: 100 });
allChapters = allChapters.concat(data.content || []);
hasMore = !data.last;
page++;
if (page > 50) break;
}
let html = '';
allChapters.forEach(chapter => {
html += `
<div class="chapter-item" data-novel-id="${novelId}" data-chapter-index="${chapter.index}">
<span class="chapter-index">${chapter.index + 1}</span>
<span class="chapter-title text-ellipsis">${chapter.title}</span>
</div>
`;
});
$list.html(html);
// 绑定点击事件
$list.find('.chapter-item').on('click', function() {
const idx = $(this).data('chapter-index');
$('#catalog-panel').addClass('hidden');
navigate('#/read/' + novelId + '/' + idx);
});
} catch (error) {
$list.html('<p style="padding:20px;text-align:center;">加载失败</p>');
}
// 关闭按钮
$('#close-catalog').off('click').on('click', () => $('#catalog-panel').addClass('hidden'));
$('.catalog-overlay').off('click').on('click', () => $('#catalog-panel').addClass('hidden'));
// 搜索过滤
$('#catalog-search-input').off('input').on('input', function() {
const keyword = $(this).val().toLowerCase();
$list.find('.chapter-item').each(function() {
const title = $(this).find('.chapter-title').text().toLowerCase();
$(this).toggle(title.includes(keyword));
});
});
}
// ========== 工具方法 ==========
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// ========== 初始化 ==========
function init() {
// 初始化主题
Store.initTheme();
// 绑定事件
bindEvents();
// 初始化路由
initRouter();
// 隐藏加载动画
setTimeout(() => {
$('#app-loader').addClass('fade-out');
setTimeout(() => {
$('#app-loader').remove();
}, 400);
}, 500);
// 注册Service Worker
registerServiceWorker();
}
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
console.log('Service Worker 注册成功');
})
.catch(err => {
console.log('Service Worker 注册失败:', err);
});
}
}
// 公开API
return {
init,
navigate
};
})();
/**
* Toast提示组件
*/
const Toast = (function() {
let timer = null;
function show(message, duration = 2000) {
const $toast = $('#toast');
$toast.text(message).addClass('show');
clearTimeout(timer);
timer = setTimeout(() => {
$toast.removeClass('show');
}, duration);
}
return { show };
})();
// 页面加载完成后初始化
$(document).ready(function() {
App.init();
});