608 lines
22 KiB
JavaScript
608 lines
22 KiB
JavaScript
/**
|
||
* 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();
|
||
});
|
||
|