feat(discovery): 完善书籍搜索推荐与日志记录

This commit is contained in:
admin committed 2026-09-12 08:39:02 +08:00
1 parent aee8c58b04
commit 61d3951905
4 files changed
+263 -27

No files matched your search

+70 -16
View File
@@ -19,6 +19,8 @@ const SHELF_PAGE = 48;
/* ------------------------------- 运行时状态 ------------------------------ */
let page = 'overview', shelfTab = 'all', discoverTab = 'books', searchQuery = '', period = 'week';
let searchComposing = false, discoverSearchTimer = null;
let recommendOffset = 0;
let noteBook = 'all', noteType = 'all', goalFilter = 'all';
let detailBook = null, bookTab = 'info', reviewFilter = 0, selectedChapter = 0;
let shelfLimit = SHELF_PAGE, chapterLimit = 30, searchScope = 'store', notesLimit = 30;
@@ -81,7 +83,8 @@ $('#theme').onclick = () => {
/* 封面:优先显示微信读书返回的真实封面,加载失败时回退到示意封面 */
function cover(book, mini = false) {
const short = book.short || book.title || '';
const coverUrl = book.cover_url || (typeof book.cover === 'string' && /^https?:\/\//i.test(book.cover) ? book.cover : '');
const rawCoverUrl = book.cover_url || (typeof book.cover === 'string' && /^https?:\/\//i.test(book.cover) ? book.cover : '');
const coverUrl = rawCoverUrl.replace(/\/(?:t6_|s_)/, '/t8_');
const coverClass = typeof book.cover === 'string' && !/^https?:\/\//i.test(book.cover) ? book.cover : '';
const fallback = mini
? `<div class="mini-cover ${coverClass}" aria-hidden="true"><b>${esc(book.title)}</b><small>${esc(book.author)}</small></div>`
@@ -91,7 +94,7 @@ function cover(book, mini = false) {
return `<img class="${className}" src="${esc(coverUrl)}" alt="${esc(book.title)}" loading="lazy" onerror='this.outerHTML=${JSON.stringify(fallback)}'>`;
}
function scoreText(book) {
return book.score ? `评分 ${Math.round(Number(book.score) * 10)}/100` : '暂无评分';
return book.score ? `评分 ${(Number(book.score) * 10).toFixed(2)}%` : '暂无评分';
}
function stateText(book) {
if (book.status === 'done') return '已读完';
@@ -143,13 +146,35 @@ async function ensureShelf(force = false) {
if (loadState.shelf === 'loading') return;
if (shelfData && !force) return;
loadState.shelf = 'loading'; loadError.shelf = '';
if (['shelf', 'overview', 'goals'].includes(page)) renderPage();
if (['shelf', 'overview', 'goals', 'discover'].includes(page)) renderPage();
try {
const data = await WeApi.shelf('all', { progress: true });
// 首屏只读取书架基础数据,避免等待逐本补拉进度和评分。
const data = await WeApi.shelf('all', { progress: false });
shelfData = data;
books = data.books || [];
booklists = (data.lists || []).map(list => ({ ...list, description: list.description || `我的书单 · ${list.book_count} 本` }));
loadState.shelf = 'ready';
const localDiscoverKey = `${searchScope}:books:${searchQuery}`;
if (!searchQuery) {
discoverData[localDiscoverKey] = { local: true, books, authors: [], lists: booklists };
loadState.discover[localDiscoverKey] = 'ready';
}
if (['shelf', 'overview', 'goals', 'discover'].includes(page)) renderPage();
// 后台补充评分和阅读进度,完成后静默刷新,不阻塞首屏。
WeApi.shelf('all', { progress: true }).then(enriched => {
shelfData = enriched;
books = enriched.books || books;
if (enriched.lists) {
booklists = enriched.lists.map(list => ({ ...list, description: list.description || `我的书单 · ${list.book_count} 本` }));
}
if (!searchQuery) {
discoverData[localDiscoverKey] = { local: true, books, authors: [], lists: booklists };
}
if (['shelf', 'overview', 'goals', 'discover'].includes(page)) renderPage();
}).catch(() => {
// 评分/进度补充失败时保留已展示的基础书架数据。
});
} catch (error) {
loadError.shelf = error.message; loadState.shelf = 'error';
}
@@ -207,17 +232,17 @@ function discoverKey() {
if (discoverTab === 'lists') return `${searchScope}:lists`;
return `${searchScope}:${discoverTab}:${searchQuery}`;
}
async function ensureDiscover(force = false) {
async function ensureDiscover(force = false, refresh = false) {
const key = discoverKey();
if (loadState.discover[key] === 'loading') return;
if (discoverData[key] && !force) return;
loadState.discover[key] = 'loading'; loadError.discover[key] = '';
if (page === 'discover') renderPage();
try {
if (searchScope === 'shelf' || (!searchQuery && (discoverTab === 'books' || discoverTab === 'all'))) {
if (searchScope === 'shelf') {
discoverData[key] = { local: true, books: books, authors: [], lists: [] };
} else if (discoverTab === 'recommend') {
discoverData[key] = await WeApi.recommend(0);
discoverData[key] = await WeApi.recommend(recommendOffset, refresh);
} else {
discoverData[key] = await WeApi.search(searchQuery || '文学', discoverTab);
}
@@ -458,6 +483,11 @@ function renderDiscover() {
const options = [['books', '书籍'], ['lists', '书单'], ['recommend', '为你推荐'], ['all', '综合'], ['author', '作者'], ['novel', '网文'], ['audio', '听书'], ['fulltext', '全文'], ['account', '公众号'], ['article', '文章']];
if (!shelfData) ensureShelf();
const key = discoverKey();
// 默认书籍页直接使用已加载的本地书架,不等待远程书城搜索。
if (!discoverData[key] && !searchQuery && searchScope === 'shelf') {
discoverData[key] = { local: true, books, authors: [], lists: booklists };
loadState.discover[key] = 'ready';
}
const payload = discoverData[key];
let content = '';
if (loadError.discover[key]) content = statusBox('error', '暂时无法获取数据', loadError.discover[key], 'retry-discover');
@@ -465,10 +495,10 @@ function renderDiscover() {
else if (discoverTab === 'recommend') {
const list = payload.books || [];
const prefer = (dashboardData[period] && dashboardData[period].prefer_words.category) || '你的阅读偏好';
content = `<div class="discovery-hero"><div class="eyebrow">PICKED FOR YOUR NEXT CHAPTER</div><h2>读过的书,会带你遇见下一本。</h2><p>${esc(prefer)} · 微信读书实时推荐</p></div><div class="card panel"><div class="books full-books">${list.map(recommendCard).join('') || '<div class="empty">接口暂时没有返回推荐书籍。</div>'}</div></div>`;
content = `<div class="discovery-hero"><div class="section-head"><div><div class="eyebrow">PICKED FOR YOUR NEXT CHAPTER</div><h2>读过的书,会带你遇见下一本。</h2><p>${esc(prefer)} · 微信读书实时推荐</p></div><button class="secondary" data-action="refresh-recommend">换一批</button></div></div><div class="card panel"><div class="books full-books">${list.map(recommendCard).join('') || '<div class="empty">接口暂时没有返回推荐书籍。</div>'}</div></div>`;
} else if (discoverTab === 'lists') {
const lists = (payload.lists && payload.lists.length) ? payload.lists : booklists;
content = renderLists(lists.map(list => ({ ...list, description: list.description || `收录 ${list.book_count || (list.ids || []).length} 本`, source: list.source || '我的书单' }))) + (payload.lists && payload.lists.length ? '' : '<p class="hint section-space">书城书单搜索接口暂未返回结果,以上为你的书架书单。</p>');
const lists = searchScope === 'shelf' ? booklists : (payload.lists || []);
content = renderLists(lists.map(list => ({ ...list, description: list.description || `收录 ${list.book_count || (list.ids || []).length} 本`, source: list.source || (searchScope === 'shelf' ? '我的书单' : '腾讯书城') }))) + (!lists.length && searchScope !== 'shelf' ? '<p class="hint section-space">腾讯书城返回了书单数量,但当前接口没有返回具体书单条目。</p>' : '');
} else if (discoverTab === 'author') {
const authors = payload.authors || [];
content = `<div class="card panel">${authors.map(author => `<div class="book-row"><div class="avatar">${esc(author.name.slice(0, 1))}</div><div class="book-row-text"><b>${esc(author.name)}</b><p>作者 · 书城检索</p></div><button class="text-button" data-action="author-books" data-name="${esc(author.name)}">查看书籍 →</button></div>`).join('') || '<div class="empty">没有匹配的作者,换个关键词试试。</div>'}</div>`;
@@ -480,7 +510,8 @@ function renderDiscover() {
: `<div class="card empty">「${esc(names[discoverTab])}」搜索:接口当前没有返回条目,可切换其他类型或关键词。</div>`;
} else if (discoverTab === 'all') {
const list = payload.books || [];
content = `<div class="section-head"><h2>书籍</h2></div><div class="card panel"><div class="books">${list.slice(0, 8).map(searchCard).join('') || '<div class="empty">没有匹配的书籍。</div>'}</div></div><div class="section-head section-space"><h2>书单</h2></div>${renderLists(booklists.slice(0, 3))}`;
const remoteLists = payload.lists || [];
content = `<div class="section-head"><h2>书籍</h2></div><div class="card panel"><div class="books">${list.slice(0, 8).map(searchCard).join('') || '<div class="empty">没有匹配的书籍。</div>'}</div></div><div class="section-head section-space"><h2>书单</h2></div>${renderLists(remoteLists)}${!remoteLists.length ? '<p class="hint section-space">腾讯书城未返回具体书单条目。</p>' : ''}`;
} else {
const list = payload.books || [];
const local = list.filter(book => !searchQuery || `${book.title}${book.author}${book.category}`.toLowerCase().includes(searchQuery.toLowerCase()));
@@ -514,7 +545,7 @@ function renderBookDetail() {
const progress = data.progress.progress;
const state = progress >= 100 ? '已读完' : progress > 0 ? `已读 ${progress}%` : '尚未开始阅读';
const readTimeText = data.progress.record_reading_time ? ` · 累计 ${esc(data.progress.record_reading_time_str)}` : '';
showDetail('书籍详情', `<div class="detail-body">${cover(info)}<div class="detail-info"><div class="eyebrow">READING SPACE / BOOK</div><h3>${esc(info.title)}</h3><p>${esc(info.author)}${info.translator ? ' · 译者 ' + esc(info.translator) : ''}</p><p style="color:var(--green)">${info.rating ? `评分 ${Math.round(info.rating)}/100 · ${info.rating_count} 人评价` : '暂无评分'}</p><p>${state}${readTimeText}</p><button class="primary" data-action="add-book" data-id="${esc(info.id)}">+ 加入阅读目标</button>${info.deep_link ? ` <a class="secondary" style="text-decoration:none;display:inline-block;margin-top:8px" href="${esc(info.deep_link)}" target="_blank" rel="noreferrer">在微信读书中打开 ↗</a>` : ''}</div></div><div class="detail-tabs">${[['info', '简介'], ['chapters', '目录'], ['notes', '我的笔记'], ['popular', '热门划线'], ['reviews', '读者点评'], ['similar', '相似推荐']].map(([tab, label]) => `<button data-action="book-tab" data-id="${tab}" class="${bookTab === tab ? 'active' : ''}">${label}</button>`).join('')}</div><div id="book-tab-content">${renderBookTab(data)}</div>`);
showDetail('书籍详情', `<div class="detail-body">${cover(info)}<div class="detail-info"><div class="eyebrow">READING SPACE / BOOK</div><h3>${esc(info.title)}</h3><p>${esc(info.author)}${info.translator ? ' · 译者 ' + esc(info.translator) : ''}</p><p style="color:var(--green)">${info.rating ? `评分 ${(Number(info.rating) / 10).toFixed(2)}% · ${info.rating_count} 人评价` : '暂无评分'}</p><p>${state}${readTimeText}</p><button class="primary" data-action="add-book" data-id="${esc(info.id)}">+ 加入阅读目标</button>${info.deep_link ? ` <a class="secondary" style="text-decoration:none;display:inline-block;margin-top:8px" href="${esc(info.deep_link)}" target="_blank" rel="noreferrer">在微信读书中打开 ↗</a>` : ''}</div></div><div class="detail-tabs">${[['info', '简介'], ['chapters', '目录'], ['notes', '我的笔记'], ['popular', '热门划线'], ['reviews', '读者点评'], ['similar', '相似推荐']].map(([tab, label]) => `<button data-action="book-tab" data-id="${tab}" class="${bookTab === tab ? 'active' : ''}">${label}</button>`).join('')}</div><div id="book-tab-content">${renderBookTab(data)}</div>`);
}
function renderBookTab(data) {
const info = data.book;
@@ -592,6 +623,7 @@ function handleAction(button) {
case 'shelf-more': shelfLimit += SHELF_PAGE; renderPage(); break;
case 'period': period = id; renderPage(); break;
case 'discover-tab': discoverTab = id; renderPage(); break;
case 'refresh-recommend': recommendOffset += 12; ensureDiscover(true, true); break;
case 'retry-overview': ensureDashboard(period, true); break;
case 'retry-shelf': ensureShelf(true); break;
case 'retry-notes': if (!notebooksData) ensureNotebooks(true); else ensureNotes(noteBook, true); break;
@@ -642,15 +674,37 @@ $('#goal-start').onchange = () => { $('#goal-end').min = $('#goal-start').value;
$('#goal-name').oninput = () => $('#goal-name').setCustomValidity('');
$('#goal-form').onsubmit = saveGoal;
$('#picker-search').oninput = renderPicker;
$('#global-search').oninput = event => {
searchQuery = event.target.value.trim();
function applyGlobalSearch(value) {
searchQuery = value;
if (!['books', 'all', 'author', 'novel', 'audio', 'fulltext', 'account', 'article'].includes(discoverTab)) discoverTab = 'books';
navigate('discover');
}
$('#global-search').addEventListener('compositionstart', () => { searchComposing = true; });
$('#global-search').addEventListener('compositionend', event => {
searchComposing = false;
applyGlobalSearch(event.target.value.trim());
});
$('#global-search').oninput = event => {
if (searchComposing || event.isComposing) return;
clearTimeout(discoverSearchTimer);
discoverSearchTimer = setTimeout(() => applyGlobalSearch(event.target.value.trim()), 300);
};
document.addEventListener('input', event => {
if (event.target.id === 'discover-search') {
const position = event.target.selectionStart; searchQuery = event.target.value; $('#global-search').value = searchQuery; renderPage();
$('#discover-search').focus(); $('#discover-search').setSelectionRange(position, position);
if (event.isComposing || searchComposing) return;
const value = event.target.value;
$('#global-search').value = value;
clearTimeout(discoverSearchTimer);
discoverSearchTimer = setTimeout(() => applyGlobalSearch(value), 300);
}
});
document.addEventListener('compositionstart', event => {
if (event.target.id === 'discover-search') searchComposing = true;
});
document.addEventListener('compositionend', event => {
if (event.target.id === 'discover-search') {
searchComposing = false;
applyGlobalSearch(event.target.value.trim());
}
});
document.addEventListener('keydown', event => { if ((event.metaKey || event.ctrlKey) && event.key === 'k') { event.preventDefault(); $('#global-search').focus(); } });