824 lines
33 KiB
JavaScript
824 lines
33 KiB
JavaScript
/**
|
||
* Pages模块 - 页面渲染逻辑
|
||
*/
|
||
const Pages = (function() {
|
||
|
||
// ========== 工具方法 ==========
|
||
|
||
// 格式化时间
|
||
function formatTime(timestamp) {
|
||
if (!timestamp) return '';
|
||
const date = new Date(timestamp);
|
||
const now = new Date();
|
||
const diff = now - date;
|
||
|
||
if (diff < 60000) return '刚刚';
|
||
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
|
||
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
|
||
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
|
||
|
||
return date.toLocaleDateString('zh-CN');
|
||
}
|
||
|
||
// 处理封面图片错误
|
||
function handleCoverError(img) {
|
||
img.onerror = null;
|
||
img.src = 'data:image/svg+xml,' + encodeURIComponent(`
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" viewBox="0 0 120 160">
|
||
<rect fill="#2d2d4a" width="120" height="160"/>
|
||
<text fill="#6a6a7a" font-family="serif" font-size="14" x="50%" y="50%" text-anchor="middle" dy=".3em">暂无封面</text>
|
||
</svg>
|
||
`);
|
||
}
|
||
|
||
// 清理HTML内容中的换行标签
|
||
function cleanContent(content) {
|
||
if (!content) return '';
|
||
return content
|
||
.replace(/<br\s*\/?>/gi, '\n')
|
||
.replace(/ /g, ' ')
|
||
.replace(/<[^>]+>/g, '')
|
||
.trim();
|
||
}
|
||
|
||
// ========== 书架页面 ==========
|
||
|
||
async function renderShelf() {
|
||
const $content = $('#app-content');
|
||
$content.html(`
|
||
<div class="shelf-page page">
|
||
<div class="shelf-header">
|
||
<h2 class="shelf-title">我的书架</h2>
|
||
</div>
|
||
<div class="shelf-grid" id="shelf-grid">
|
||
${renderSkeletonGrid(6)}
|
||
</div>
|
||
</div>
|
||
`);
|
||
|
||
try {
|
||
const userId = Store.getUserId();
|
||
const data = await API.getShelf({ userId, page: 0, size: 50 });
|
||
const items = data.content || [];
|
||
|
||
if (items.length === 0) {
|
||
$('#shelf-grid').html(`
|
||
<div class="empty-state" style="grid-column: 1/-1;">
|
||
<svg viewBox="0 0 24 24"><path d="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-2zm-1 9H9V9h10v2zm-4 4H9v-2h6v2zm4-8H9V5h10v2z"/></svg>
|
||
<h3>书架空空如也</h3>
|
||
<p>去发现页面找本好书吧</p>
|
||
</div>
|
||
`);
|
||
return;
|
||
}
|
||
|
||
// 获取每本书的详情
|
||
const novels = await Promise.all(
|
||
items.map(item => API.getNovelDetail(item.novelId).catch(() => null))
|
||
);
|
||
|
||
// 更新书架缓存
|
||
Store.setShelfCache(items.map(i => i.novelId));
|
||
|
||
let html = '';
|
||
novels.forEach((novel, index) => {
|
||
if (!novel) return;
|
||
const position = Store.getReadingPosition(novel.id);
|
||
html += renderShelfItem(novel, position);
|
||
});
|
||
|
||
$('#shelf-grid').html(html);
|
||
|
||
// 绑定图片错误处理
|
||
$('#shelf-grid img').on('error', function() {
|
||
handleCoverError(this);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载书架失败:', error);
|
||
Toast.show('加载失败,请重试');
|
||
}
|
||
}
|
||
|
||
function renderShelfItem(novel, position) {
|
||
const progress = position ? Math.min(100, Math.round((position.chapterIndex / (novel.lastChapterIndex || 1)) * 100)) : 0;
|
||
return `
|
||
<div class="shelf-item" data-novel-id="${novel.id}">
|
||
<div class="novel-grid-card">
|
||
<div class="novel-cover">
|
||
<img src="${novel.cover || ''}" alt="${novel.name}" loading="lazy">
|
||
</div>
|
||
<div class="novel-info">
|
||
<div class="novel-title text-ellipsis">${novel.name}</div>
|
||
<div class="novel-author text-ellipsis">${novel.author}</div>
|
||
</div>
|
||
</div>
|
||
${progress > 0 ? `
|
||
<div class="reading-progress">
|
||
<div class="reading-progress-bar" style="width: ${progress}%"></div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ========== 发现页面 ==========
|
||
|
||
async function renderExplore() {
|
||
const $content = $('#app-content');
|
||
$content.html(`
|
||
<div class="explore-page page">
|
||
<div class="explore-banner">
|
||
<div class="explore-banner-content">
|
||
<h2>发现好书</h2>
|
||
<p>探索精彩小说世界</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="page-section">
|
||
<div class="section-header">
|
||
<h3 class="section-title">🎲 随机推荐</h3>
|
||
<button class="section-more" id="refresh-random">换一批</button>
|
||
</div>
|
||
<div class="novel-scroll" id="random-novels">
|
||
${renderSkeletonScroll(5)}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="page-section">
|
||
<div class="section-header">
|
||
<h3 class="section-title">热门标签</h3>
|
||
</div>
|
||
<div class="tags-cloud" id="tags-cloud">
|
||
<div class="skeleton" style="width:60px;height:32px;border-radius:16px;"></div>
|
||
<div class="skeleton" style="width:80px;height:32px;border-radius:16px;"></div>
|
||
<div class="skeleton" style="width:50px;height:32px;border-radius:16px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="page-section">
|
||
<div class="section-header">
|
||
<h3 class="section-title">全部小说</h3>
|
||
</div>
|
||
<div class="novel-list" id="novel-list">
|
||
${renderSkeletonList(5)}
|
||
</div>
|
||
<div class="load-more" id="load-more">
|
||
<button class="load-more-btn">加载更多</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`);
|
||
|
||
// 加载随机推荐
|
||
loadRandomNovels();
|
||
|
||
// 加载标签
|
||
loadTags();
|
||
|
||
// 加载小说列表
|
||
loadNovels(0);
|
||
|
||
// 绑定刷新随机推荐按钮
|
||
$('#refresh-random').on('click', function() {
|
||
loadRandomNovels();
|
||
});
|
||
}
|
||
|
||
// 加载随机推荐
|
||
async function loadRandomNovels() {
|
||
$('#random-novels').html(renderSkeletonScroll(6));
|
||
|
||
try {
|
||
const novels = await API.getRandomNovels(12);
|
||
|
||
if (novels.length === 0) {
|
||
$('#random-novels').html('<p style="padding:20px;color:var(--text-muted);">暂无推荐</p>');
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
novels.forEach(novel => {
|
||
const isInShelf = Store.isInShelfCache(novel.id);
|
||
html += `
|
||
<div class="novel-grid-card" data-novel-id="${novel.id}">
|
||
<div class="novel-cover">
|
||
<img src="${novel.cover || ''}" alt="${novel.name}" loading="lazy">
|
||
${isInShelf ? '<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>' : ''}
|
||
</div>
|
||
<div class="novel-info">
|
||
<div class="novel-title text-ellipsis">${novel.name}</div>
|
||
<div class="novel-author text-ellipsis clickable-author" data-author="${novel.author || ''}">${novel.author || '未知'}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
$('#random-novels').html(html);
|
||
|
||
// 绑定图片错误处理
|
||
$('#random-novels img').on('error', function() {
|
||
handleCoverError(this);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载随机推荐失败:', error);
|
||
$('#random-novels').html('<p style="padding:20px;color:var(--text-muted);">加载失败</p>');
|
||
}
|
||
}
|
||
|
||
// 渲染横向滚动骨架屏
|
||
function renderSkeletonScroll(count) {
|
||
let html = '';
|
||
for (let i = 0; i < count; i++) {
|
||
html += `
|
||
<div class="novel-grid-card" style="flex-shrink:0;width:100px;">
|
||
<div class="novel-cover skeleton" style="width:100px;height:140px;"></div>
|
||
<div style="padding:8px;">
|
||
<div class="skeleton" style="height:14px;width:80%;margin-bottom:4px;"></div>
|
||
<div class="skeleton" style="height:12px;width:50%;"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
return html;
|
||
}
|
||
|
||
let currentPage = 0;
|
||
let isLoadingMore = false;
|
||
let hasMore = true;
|
||
let currentTag = null; // 保存当前选择的标签
|
||
|
||
async function loadTags() {
|
||
try {
|
||
const tags = await API.getPopularTags(15);
|
||
let html = '';
|
||
tags.forEach(tag => {
|
||
html += `<button class="tag-btn" data-tag="${tag}">${tag}</button>`;
|
||
});
|
||
$('#tags-cloud').html(html);
|
||
} catch (error) {
|
||
console.error('加载标签失败:', error);
|
||
}
|
||
}
|
||
|
||
let currentAuthor = null; // 保存当前选择的作者
|
||
|
||
async function loadNovels(page, tag = null, author = null, append = false) {
|
||
if (isLoadingMore) return;
|
||
isLoadingMore = true;
|
||
|
||
// 如果不是追加加载,保存当前筛选条件
|
||
if (!append) {
|
||
currentPage = 0;
|
||
hasMore = true;
|
||
currentTag = tag; // 保存当前选择的标签
|
||
currentAuthor = author; // 保存当前选择的作者
|
||
$('#novel-list').html(renderSkeletonList(5));
|
||
}
|
||
|
||
// 追加加载时使用已保存的筛选条件
|
||
const useTag = append ? currentTag : tag;
|
||
const useAuthor = append ? currentAuthor : author;
|
||
|
||
try {
|
||
const params = { page, size: 20 };
|
||
if (useTag) params.tag = useTag;
|
||
if (useAuthor) params.keyword = useAuthor; // 使用keyword参数搜索作者
|
||
|
||
const data = await API.searchNovels(params);
|
||
const novels = data.content || [];
|
||
hasMore = !data.last;
|
||
|
||
// 如果有作者筛选,只保留该作者的作品
|
||
let filteredNovels = novels;
|
||
if (useAuthor) {
|
||
filteredNovels = novels.filter(novel => novel.author === useAuthor);
|
||
}
|
||
|
||
let html = '';
|
||
filteredNovels.forEach(novel => {
|
||
const isInShelf = Store.isInShelfCache(novel.id);
|
||
html += renderNovelCard(novel, isInShelf);
|
||
});
|
||
|
||
if (append) {
|
||
$('#novel-list').append(html);
|
||
} else {
|
||
// 显示筛选信息
|
||
let filterInfo = '';
|
||
if (useAuthor) {
|
||
filterInfo = `<div class="filter-info"><span class="filter-label">作者:</span><span class="filter-value">${useAuthor}</span><button class="clear-filter-btn" data-type="author">×</button></div>`;
|
||
} else if (useTag) {
|
||
filterInfo = `<div class="filter-info"><span class="filter-label">标签:</span><span class="filter-value">${useTag}</span><button class="clear-filter-btn" data-type="tag">×</button></div>`;
|
||
}
|
||
const listHtml = html || '<div class="empty-state"><p>暂无小说</p></div>';
|
||
$('#novel-list').html(filterInfo ? (filterInfo + listHtml) : listHtml);
|
||
}
|
||
|
||
// 更新加载更多按钮
|
||
if (hasMore && filteredNovels.length > 0) {
|
||
$('#load-more').html('<button class="load-more-btn">加载更多</button>');
|
||
} else {
|
||
$('#load-more').html('<span style="color:var(--text-muted);font-size:14px;">没有更多了</span>');
|
||
}
|
||
|
||
currentPage = page;
|
||
|
||
// 绑定图片错误处理
|
||
$('#novel-list img').on('error', function() {
|
||
handleCoverError(this);
|
||
});
|
||
|
||
// 重新绑定清除筛选按钮事件(只在非追加模式下绑定,避免重复绑定)
|
||
if (!append) {
|
||
$(document).off('click', '.clear-filter-btn').on('click', '.clear-filter-btn', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
const filterType = $(this).data('type');
|
||
if (filterType === 'author') {
|
||
currentAuthor = null;
|
||
loadNovels(0, currentTag, null);
|
||
} else if (filterType === 'tag') {
|
||
currentTag = null;
|
||
$('.tag-btn').removeClass('active');
|
||
loadNovels(0, null, currentAuthor);
|
||
}
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('加载小说失败:', error);
|
||
Toast.show('加载失败,请重试');
|
||
} finally {
|
||
isLoadingMore = false;
|
||
}
|
||
}
|
||
|
||
function renderNovelCard(novel, inShelf = false) {
|
||
const synopsis = cleanContent(novel.synopsis).substring(0, 80);
|
||
const tags = (novel.tags || []).slice(0, 2);
|
||
// 检查书架状态(如果没有传入,则从缓存中检查)
|
||
const isInShelf = inShelf !== undefined ? inShelf : Store.isInShelfCache(novel.id);
|
||
return `
|
||
<div class="novel-card" data-novel-id="${novel.id}">
|
||
<div class="novel-cover">
|
||
<img src="${novel.cover || ''}" alt="${novel.name}" loading="lazy">
|
||
${isInShelf ? '<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>' : ''}
|
||
</div>
|
||
<div class="novel-info">
|
||
<div class="novel-title text-ellipsis">${novel.name}</div>
|
||
<div class="novel-author clickable-author" data-author="${novel.author || ''}">${novel.author || '未知'}</div>
|
||
<div class="novel-synopsis text-clamp-2">${synopsis}...</div>
|
||
<div class="novel-tags">
|
||
${tags.map(t => `<span class="tag">${t}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ========== 历史页面 ==========
|
||
|
||
async function renderHistory() {
|
||
const $content = $('#app-content');
|
||
$content.html(`
|
||
<div class="history-page page">
|
||
<div class="history-header">
|
||
<h2 class="history-title">阅读历史</h2>
|
||
<button class="clear-history-btn" id="clear-history">清空</button>
|
||
</div>
|
||
<div class="history-list" id="history-list">
|
||
${renderSkeletonList(5)}
|
||
</div>
|
||
</div>
|
||
`);
|
||
|
||
try {
|
||
const userId = Store.getUserId();
|
||
const data = await API.getHistory({ userId, page: 0, size: 50 });
|
||
const items = data.content || [];
|
||
|
||
if (items.length === 0) {
|
||
$('#history-list').html(`
|
||
<div class="empty-state">
|
||
<svg viewBox="0 0 24 24"><path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>
|
||
<h3>暂无阅读记录</h3>
|
||
<p>开始阅读后会在这里显示</p>
|
||
</div>
|
||
`);
|
||
return;
|
||
}
|
||
|
||
// 获取小说详情和章节信息
|
||
const historyWithDetails = await Promise.all(
|
||
items.map(async (item) => {
|
||
try {
|
||
const [novel, chapter] = await Promise.all([
|
||
API.getNovelDetail(item.novelId),
|
||
API.getChapterById(item.chapterId)
|
||
]);
|
||
return { ...item, novel, chapter };
|
||
} catch {
|
||
return null;
|
||
}
|
||
})
|
||
);
|
||
|
||
let html = '';
|
||
historyWithDetails.filter(Boolean).forEach(item => {
|
||
html += renderHistoryItem(item);
|
||
});
|
||
|
||
$('#history-list').html(html || '<div class="empty-state"><p>暂无记录</p></div>');
|
||
|
||
// 绑定图片错误处理
|
||
$('#history-list img').on('error', function() {
|
||
handleCoverError(this);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载历史失败:', error);
|
||
Toast.show('加载失败,请重试');
|
||
}
|
||
}
|
||
|
||
function renderHistoryItem(item) {
|
||
return `
|
||
<div class="history-item" data-novel-id="${item.novelId}" data-chapter-index="${item.chapter?.index || 0}">
|
||
<div class="novel-cover">
|
||
<img src="${item.novel?.cover || ''}" alt="${item.novel?.name || ''}" loading="lazy">
|
||
</div>
|
||
<div class="history-info">
|
||
<div class="novel-title text-ellipsis">${item.novel?.name || '未知'}</div>
|
||
<div class="history-chapter text-ellipsis">${item.chapter?.title || '未知章节'}</div>
|
||
<div class="history-time">${formatTime(item.lastReadAt)}</div>
|
||
</div>
|
||
<div class="item-actions">
|
||
<button class="delete-history-btn" data-novel-id="${item.novelId}">
|
||
<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ========== 书签页面 ==========
|
||
|
||
async function renderBookmarks() {
|
||
const $content = $('#app-content');
|
||
$content.html(`
|
||
<div class="bookmarks-page page">
|
||
<div class="bookmarks-header">
|
||
<h2 class="bookmarks-title">我的书签</h2>
|
||
</div>
|
||
<div class="bookmarks-list" id="bookmarks-list">
|
||
${renderSkeletonList(5)}
|
||
</div>
|
||
</div>
|
||
`);
|
||
|
||
try {
|
||
const userId = Store.getUserId();
|
||
const data = await API.getBookmarks({ userId, page: 0, size: 50 });
|
||
const items = data.content || [];
|
||
|
||
if (items.length === 0) {
|
||
$('#bookmarks-list').html(`
|
||
<div class="empty-state">
|
||
<svg viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>
|
||
<h3>暂无书签</h3>
|
||
<p>阅读时点击书签按钮添加</p>
|
||
</div>
|
||
`);
|
||
return;
|
||
}
|
||
|
||
// 获取小说和章节详情
|
||
const bookmarksWithDetails = await Promise.all(
|
||
items.map(async (item) => {
|
||
try {
|
||
const [novel, chapter] = await Promise.all([
|
||
API.getNovelDetail(item.novelId),
|
||
API.getChapterById(item.chapterId)
|
||
]);
|
||
return { ...item, novel, chapter };
|
||
} catch {
|
||
return null;
|
||
}
|
||
})
|
||
);
|
||
|
||
let html = '';
|
||
bookmarksWithDetails.filter(Boolean).forEach(item => {
|
||
html += renderBookmarkItem(item);
|
||
});
|
||
|
||
$('#bookmarks-list').html(html || '<div class="empty-state"><p>暂无书签</p></div>');
|
||
|
||
// 绑定图片错误处理
|
||
$('#bookmarks-list img').on('error', function() {
|
||
handleCoverError(this);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载书签失败:', error);
|
||
Toast.show('加载失败,请重试');
|
||
}
|
||
}
|
||
|
||
function renderBookmarkItem(item) {
|
||
return `
|
||
<div class="bookmark-item" data-novel-id="${item.novelId}" data-chapter-index="${item.chapter?.index || 0}">
|
||
<div class="novel-cover">
|
||
<img src="${item.novel?.cover || ''}" alt="${item.novel?.name || ''}" loading="lazy">
|
||
</div>
|
||
<div class="bookmark-info">
|
||
<div class="novel-title text-ellipsis">${item.novel?.name || '未知'}</div>
|
||
<div class="bookmark-chapter text-ellipsis">${item.chapter?.title || '未知章节'}</div>
|
||
${item.note ? `<div class="bookmark-note">${item.note}</div>` : ''}
|
||
<div class="history-time">${formatTime(item.createdAt)}</div>
|
||
</div>
|
||
<div class="item-actions">
|
||
<button class="delete-bookmark-btn" data-bookmark-id="${item.id}">
|
||
<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ========== 小说详情页面 ==========
|
||
|
||
async function renderDetail(novelId) {
|
||
const $content = $('#app-content');
|
||
$content.html(`
|
||
<div class="detail-page">
|
||
<div class="detail-header">
|
||
<div class="detail-cover-wrap">
|
||
<div class="novel-cover detail-cover skeleton"></div>
|
||
<div class="detail-meta">
|
||
<div class="skeleton" style="height:24px;width:80%;"></div>
|
||
<div class="skeleton" style="height:18px;width:50%;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`);
|
||
|
||
try {
|
||
const novel = await API.getNovelDetail(novelId);
|
||
Store.setState('currentNovel', novel);
|
||
|
||
// 检查是否在书架
|
||
const inShelf = Store.isInShelfCache(novelId);
|
||
|
||
// 获取阅读位置
|
||
const position = Store.getReadingPosition(novelId);
|
||
|
||
$content.html(renderDetailPage(novel, inShelf, position));
|
||
|
||
// 加载前几章节
|
||
loadPreviewChapters(novelId);
|
||
|
||
// 绑定图片错误处理
|
||
$('.detail-cover img').on('error', function() {
|
||
handleCoverError(this);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载详情失败:', error);
|
||
Toast.show('加载失败,请重试');
|
||
}
|
||
}
|
||
|
||
function renderDetailPage(novel, inShelf, position) {
|
||
const synopsis = cleanContent(novel.synopsis);
|
||
const tags = novel.tags || [];
|
||
|
||
return `
|
||
<div class="detail-page">
|
||
<div class="detail-header">
|
||
<div class="detail-cover-wrap">
|
||
<div class="novel-cover detail-cover">
|
||
<img src="${novel.cover || ''}" alt="${novel.name}">
|
||
</div>
|
||
<div class="detail-meta">
|
||
<h1 class="detail-title">${novel.name}</h1>
|
||
<div class="detail-author clickable-author" data-author="${novel.author || ''}">${novel.author || '未知'}</div>
|
||
<div class="detail-stats">
|
||
<div class="detail-stat">
|
||
<div class="detail-stat-value">${novel.chapterCount || 0}</div>
|
||
<div class="detail-stat-label">章节</div>
|
||
</div>
|
||
<div class="detail-stat">
|
||
<div class="detail-stat-value">${novel.status === 1 ? '完结' : '连载'}</div>
|
||
<div class="detail-stat-label">状态</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-tags">
|
||
${tags.map(t => `<span class="tag">${t}</span>`).join('')}
|
||
</div>
|
||
|
||
<div class="detail-synopsis">
|
||
<h3>简介</h3>
|
||
<p class="text-clamp-3" id="synopsis-text">${synopsis}</p>
|
||
<button class="expand-btn" id="expand-synopsis">展开</button>
|
||
</div>
|
||
|
||
<div class="detail-chapters">
|
||
<h3>
|
||
目录
|
||
<span class="chapter-count">共${novel.chapterCount || 0}章</span>
|
||
</h3>
|
||
<div class="chapters-preview" id="chapters-preview">
|
||
<div class="chapter-loading">
|
||
<div class="loading-spinner"></div>
|
||
<span>加载中...</span>
|
||
</div>
|
||
</div>
|
||
<button class="view-all-chapters" id="view-all-chapters" data-novel-id="${novel.id}">
|
||
查看全部章节
|
||
<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="detail-actions">
|
||
<button class="btn-shelf ${inShelf ? 'in-shelf' : ''}" id="btn-shelf" data-novel-id="${novel.id}">
|
||
<svg viewBox="0 0 24 24"><path d="${inShelf ? 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z' : 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'}"/></svg>
|
||
${inShelf ? '已在书架' : '加入书架'}
|
||
</button>
|
||
<button class="btn-read" id="btn-read" data-novel-id="${novel.id}" data-chapter-index="${position?.chapterIndex || 0}">
|
||
${position ? '继续阅读' : '开始阅读'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadPreviewChapters(novelId) {
|
||
try {
|
||
const data = await API.getChapters(novelId, { page: 0, size: 5 });
|
||
const chapters = data.content || [];
|
||
|
||
let html = '';
|
||
chapters.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>
|
||
`;
|
||
});
|
||
|
||
$('#chapters-preview').html(html || '<p style="padding:16px;color:var(--text-muted);">暂无章节</p>');
|
||
} catch (error) {
|
||
console.error('加载章节失败:', error);
|
||
}
|
||
}
|
||
|
||
// ========== 搜索功能 ==========
|
||
|
||
async function renderSearchResults(keyword) {
|
||
const $results = $('#search-results');
|
||
$results.html('<div class="chapter-loading"><div class="loading-spinner"></div><span>搜索中...</span></div>');
|
||
|
||
// 保存当前搜索关键词用于高亮
|
||
currentSearchKeyword = keyword;
|
||
|
||
try {
|
||
const data = await API.searchNovels({ keyword, page: 0, size: 30 });
|
||
const novels = data.content || [];
|
||
|
||
if (novels.length === 0) {
|
||
$results.html('<div class="search-hint">未找到相关小说</div>');
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="novel-list">';
|
||
novels.forEach(novel => {
|
||
const isInShelf = Store.isInShelfCache(novel.id);
|
||
html += renderNovelCard(novel, isInShelf, keyword);
|
||
});
|
||
html += '</div>';
|
||
|
||
$results.html(html);
|
||
|
||
// 绑定图片错误处理
|
||
$results.find('img').on('error', function() {
|
||
handleCoverError(this);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('搜索失败:', error);
|
||
$results.html('<div class="search-hint">搜索失败,请重试</div>');
|
||
}
|
||
}
|
||
|
||
async function renderSearchByTag(tag) {
|
||
const $results = $('#search-results');
|
||
$results.html('<div class="chapter-loading"><div class="loading-spinner"></div><span>加载中...</span></div>');
|
||
|
||
try {
|
||
const data = await API.searchNovels({ tag, page: 0, size: 30 });
|
||
const novels = data.content || [];
|
||
|
||
if (novels.length === 0) {
|
||
$results.html('<div class="search-hint">该标签下暂无小说</div>');
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="novel-list">';
|
||
novels.forEach(novel => {
|
||
const isInShelf = Store.isInShelfCache(novel.id);
|
||
html += renderNovelCard(novel, isInShelf);
|
||
});
|
||
html += '</div>';
|
||
|
||
$results.html(html);
|
||
|
||
// 绑定图片错误处理
|
||
$results.find('img').on('error', function() {
|
||
handleCoverError(this);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载失败:', error);
|
||
$results.html('<div class="search-hint">加载失败,请重试</div>');
|
||
}
|
||
}
|
||
|
||
// ========== 骨架屏 ==========
|
||
|
||
function renderSkeletonGrid(count) {
|
||
let html = '';
|
||
for (let i = 0; i < count; i++) {
|
||
html += `
|
||
<div class="shelf-item">
|
||
<div class="novel-grid-card">
|
||
<div class="novel-cover skeleton"></div>
|
||
<div class="novel-info">
|
||
<div class="skeleton" style="height:14px;width:80%;margin-bottom:4px;"></div>
|
||
<div class="skeleton" style="height:12px;width:50%;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function renderSkeletonList(count) {
|
||
let html = '';
|
||
for (let i = 0; i < count; i++) {
|
||
html += `
|
||
<div class="skeleton-card">
|
||
<div class="skeleton skeleton-cover"></div>
|
||
<div class="skeleton-info">
|
||
<div class="skeleton skeleton-title"></div>
|
||
<div class="skeleton skeleton-text"></div>
|
||
<div class="skeleton skeleton-desc"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
return html;
|
||
}
|
||
|
||
// 按作者搜索(公开函数)
|
||
function searchByAuthor(author) {
|
||
if (!author) return;
|
||
// 切换到发现页面
|
||
if (window.location.hash !== '#/explore') {
|
||
window.location.hash = '#/explore';
|
||
// 等待页面渲染完成后加载该作者的作品
|
||
setTimeout(() => {
|
||
loadNovels(0, null, author);
|
||
}, 300);
|
||
} else {
|
||
// 如果已经在发现页面,直接加载
|
||
loadNovels(0, null, author);
|
||
}
|
||
}
|
||
|
||
// 公开API
|
||
return {
|
||
renderShelf,
|
||
renderExplore,
|
||
renderHistory,
|
||
renderBookmarks,
|
||
renderDetail,
|
||
renderSearchResults,
|
||
renderSearchByTag,
|
||
loadNovels,
|
||
searchByAuthor,
|
||
currentPage: () => currentPage,
|
||
hasMore: () => hasMore
|
||
};
|
||
})();
|
||
|