/* ===== 微信读书数据展示 - 前端逻辑 ===== */ const API_BASE = ""; // ===== 主题管理 ===== function getCurrentTheme() { return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light"; } function toggleTheme() { const newTheme = getCurrentTheme() === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", newTheme); localStorage.setItem("theme", newTheme); refreshChartsForTheme(); } function getChartColors() { const cs = getComputedStyle(document.documentElement); return { axis: cs.getPropertyValue("--chart-axis").trim(), axisLine: cs.getPropertyValue("--chart-axis-line").trim(), splitLine: cs.getPropertyValue("--chart-split-line").trim(), label: cs.getPropertyValue("--chart-label").trim(), borderColor: cs.getPropertyValue("--chart-border-color").trim(), tooltipBg: cs.getPropertyValue("--chart-tooltip-bg").trim(), tooltipBorder: cs.getPropertyValue("--chart-tooltip-border").trim(), tooltipText: cs.getPropertyValue("--chart-tooltip-text").trim(), accent: cs.getPropertyValue("--accent").trim(), accentLight: cs.getPropertyValue("--accent-light").trim(), accentDark: cs.getPropertyValue("--accent-dark").trim(), }; } // ===== 工具函数 ===== function hdCover(url) { if (!url) return ""; return url.replace("/t6_", "/t8_"); } async function fetchAPI(path) { const resp = await fetch(API_BASE + path); if (!resp.ok) throw new Error(`API error: ${resp.status}`); return resp.json(); } function formatHours(seconds) { if (!seconds) return "0"; return (seconds / 3600).toFixed(1); } function escapeHtml(text) { if (!text) return ""; const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } // ===== 数据库健康检查 ===== const chartInstances = {}; let chartsLoaded = false; let currentThoughtsData = null; function refreshChartsForTheme() { if (!chartsLoaded) return; Object.values(chartInstances).forEach((chart) => { if (chart && !chart.isDisposed()) chart.dispose(); }); loadYearlyTrend(); loadTimeDistribution(); loadCategories(); loadAuthors(); } async function checkDBHealth() { try { const resp = await fetch(API_BASE + "/api/health"); const data = await resp.json(); if (resp.ok && data.database === "connected") { document.getElementById("dbAlertBanner").style.display = "none"; return true; } else { showDBAlert(data.message || "数据库连接失败"); return false; } } catch (error) { showDBAlert("无法连接到服务器,请检查后端服务是否运行。"); return false; } } function showDBAlert(message) { const banner = document.getElementById("dbAlertBanner"); const detail = document.getElementById("dbAlertDetail"); if (banner) banner.style.display = "flex"; if (detail) detail.textContent = message; } // ===== 标签页切换 ===== function switchTab(tabName) { document.querySelectorAll(".tab-panel").forEach((panel) => { panel.classList.remove("active"); }); document.querySelectorAll(".nav-link").forEach((link) => { link.classList.toggle("active", link.dataset.tab === tabName); }); const target = document.getElementById(`tab-${tabName}`); if (target) { target.classList.add("active"); } window.scrollTo({ top: 0, behavior: "smooth" }); // 懒加载图表 if (tabName === "charts" && !chartsLoaded) { chartsLoaded = true; loadYearlyTrend(); loadTimeDistribution(); loadCategories(); loadAuthors(); } // 切换到图表时重绘尺寸 if (tabName === "charts" && chartsLoaded) { setTimeout(() => { Object.values(chartInstances).forEach((chart) => { if (chart && !chart.isDisposed()) chart.resize(); }); }, 100); } } // ===== 回到顶部按钮 ===== function initBackToTop() { const btn = document.getElementById("backToTop"); window.addEventListener("scroll", () => { btn.classList.toggle("visible", window.scrollY > 400); }); } function scrollToTop() { window.scrollTo({ top: 0, behavior: "smooth" }); } // ===== 初始化 ===== document.addEventListener("DOMContentLoaded", async () => { const isHealthy = await checkDBHealth(); if (!isHealthy) return; loadOverview(); loadTopBooks(); loadShelf(1); loadNotebooks(); loadRecommend(0); initBackToTop(); // 搜索和筛选事件 document.getElementById("shelfSearch").addEventListener("input", debounce(() => loadShelf(1), 300)); document.getElementById("finishedOnly").addEventListener("change", () => loadShelf(1)); }); // ===== 总览数据 ===== async function loadOverview() { try { const data = await fetchAPI("/api/overview"); const heroStats = document.getElementById("heroStats"); heroStats.innerHTML = `
${formatHours(data.total_read_time)}
累计阅读 (小时)
${data.read_days}
阅读天数
${data.shelf_count}
书架藏书
${data.total_notes}
读书笔记
`; document.getElementById("heroSubtitle").textContent = `自 ${data.regist_time} 加入微信读书,已陪伴你 ${data.read_days} 个阅读的日子`; const statsGrid = document.getElementById("statsGrid"); statsGrid.innerHTML = `
⏱️
${data.total_read_time_str}
累计阅读时长
今年: ${data.year_read_time_str}
📅
${data.read_days}
累计阅读天数
今年: ${data.year_read_days} 天
📚
${data.shelf_count}
书架书籍
已读完: ${data.finished_count} 本
✍️
${data.total_notes}
读书笔记
来自 ${data.notebook_count} 本书
`; const timeStr = `数据更新: ${data.fetch_time}`; document.getElementById("fetchTime").textContent = timeStr; document.getElementById("footerTime").textContent = data.fetch_time; } catch (e) { console.error("加载总览失败:", e); } } // ===== 年度趋势图 ===== async function loadYearlyTrend() { try { const data = await fetchAPI("/api/yearly-trend"); const el = document.getElementById("chart-yearly"); if (chartInstances.yearly) chartInstances.yearly.dispose(); const chart = echarts.init(el); chartInstances.yearly = chart; const c = getChartColors(); const years = data.map((d) => d.year); const hours = data.map((d) => d.hours); chart.setOption({ tooltip: { trigger: "axis", backgroundColor: c.tooltipBg, borderColor: c.tooltipBorder, textStyle: { color: c.tooltipText }, formatter: (params) => { const p = params[0]; return `${p.name}年
阅读: ${p.value} 小时`; }, }, grid: { left: 50, right: 30, top: 30, bottom: 40 }, xAxis: { type: "category", data: years, axisLabel: { color: c.axis }, axisLine: { lineStyle: { color: c.axisLine } }, }, yAxis: { type: "value", name: "小时", nameTextStyle: { color: c.axis }, axisLabel: { color: c.axis }, splitLine: { lineStyle: { color: c.splitLine } }, }, series: [{ data: hours, type: "bar", barWidth: "50%", itemStyle: { borderRadius: [8, 8, 0, 0], color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: c.accentLight }, { offset: 1, color: c.accent }, ] }, }, label: { show: true, position: "top", color: c.label, fontSize: 12, formatter: "{c}h" }, }], }); window.addEventListener("resize", () => chart.resize()); } catch (e) { console.error("加载年度趋势失败:", e); } } // ===== 24小时时段分布 ===== async function loadTimeDistribution() { try { const data = await fetchAPI("/api/time-distribution"); const el = document.getElementById("chart-hours"); if (chartInstances.hours) chartInstances.hours.dispose(); const chart = echarts.init(el); chartInstances.hours = chart; const c = getChartColors(); const hours = data.map((d) => `${d.hour}时`); const minutes = data.map((d) => d.duration_min); chart.setOption({ tooltip: { trigger: "axis", backgroundColor: c.tooltipBg, borderColor: c.tooltipBorder, textStyle: { color: c.tooltipText }, formatter: (params) => { const p = params[0]; return `${p.name}
阅读: ${p.value} 分钟`; }, }, grid: { left: 50, right: 20, top: 20, bottom: 40 }, xAxis: { type: "category", data: hours, axisLabel: { color: c.axis, fontSize: 10, interval: 2 }, axisLine: { lineStyle: { color: c.axisLine } }, }, yAxis: { type: "value", name: "分钟", nameTextStyle: { color: c.axis }, axisLabel: { color: c.axis }, splitLine: { lineStyle: { color: c.splitLine } }, }, series: [{ data: minutes, type: "line", smooth: true, symbol: "circle", symbolSize: 4, lineStyle: { width: 2, color: c.accent }, itemStyle: { color: c.accent }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: c.accent + "4D" }, { offset: 1, color: c.accent + "05" }, ] }, }, }], }); window.addEventListener("resize", () => chart.resize()); } catch (e) { console.error("加载时段分布失败:", e); } } // ===== 偏好分类 ===== async function loadCategories() { try { const data = await fetchAPI("/api/categories"); const el = document.getElementById("chart-categories"); if (chartInstances.categories) chartInstances.categories.dispose(); const chart = echarts.init(el); chartInstances.categories = chart; const c = getChartColors(); const items = data.slice(0, 8).map((d) => ({ name: d.category_title, value: d.reading_time, })); chart.setOption({ tooltip: { trigger: "item", backgroundColor: c.tooltipBg, borderColor: c.tooltipBorder, textStyle: { color: c.tooltipText }, formatter: (p) => { const hours = (p.value / 3600).toFixed(1); return `${p.name}
阅读: ${hours} 小时 (${p.percent}%)`; }, }, legend: { type: "scroll", orient: "vertical", right: 10, top: 20, bottom: 20, textStyle: { color: c.label, fontSize: 11 }, }, series: [{ type: "pie", radius: ["40%", "70%"], center: ["35%", "50%"], avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderColor: c.borderColor, borderWidth: 2 }, label: { show: false }, emphasis: { label: { show: true, fontSize: 14, fontWeight: "bold", color: c.tooltipText }, }, data: items, color: ["#c17b4a", "#e8a87c", "#d4a574", "#b8860b", "#cd853f", "#daa520", "#bc8f8f", "#bdb76b"], }], }); window.addEventListener("resize", () => chart.resize()); } catch (e) { console.error("加载分类失败:", e); } } // ===== 偏好作者 ===== async function loadAuthors() { try { const data = await fetchAPI("/api/authors"); const el = document.getElementById("chart-authors"); if (chartInstances.authors) chartInstances.authors.dispose(); const chart = echarts.init(el); chartInstances.authors = chart; const c = getChartColors(); const top10 = data.slice(0, 10); const names = top10.map((d) => d.name); const counts = top10.map((d) => d.count); chart.setOption({ tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: c.tooltipBg, borderColor: c.tooltipBorder, textStyle: { color: c.tooltipText }, formatter: (params) => { const p = params[0]; return `${p.name}
阅读: ${p.value} 本`; }, }, grid: { left: 100, right: 40, top: 20, bottom: 20 }, xAxis: { type: "value", axisLabel: { color: c.axis }, splitLine: { lineStyle: { color: c.splitLine } }, }, yAxis: { type: "category", data: names.reverse(), axisLabel: { color: c.axis, fontSize: 12 }, axisLine: { lineStyle: { color: c.axisLine } }, }, series: [{ type: "bar", data: counts.reverse(), barWidth: "50%", itemStyle: { borderRadius: [0, 6, 6, 0], color: { type: "linear", x: 0, y: 0, x2: 1, y2: 0, colorStops: [ { offset: 0, color: c.accentLight }, { offset: 1, color: c.accent }, ] }, }, label: { show: true, position: "right", color: c.label, fontSize: 12, formatter: "{c}本" }, }], }); window.addEventListener("resize", () => chart.resize()); } catch (e) { console.error("加载作者失败:", e); } } // ===== 读书排行(竖向列表) ===== async function loadTopBooks() { try { const data = await fetchAPI("/api/top-books"); const list = document.getElementById("topBooksGrid"); list.innerHTML = data .map((book, i) => { const tagsHtml = book.tags .map((t) => `${t}`) .join(""); return `
${i + 1} ${book.title}
${book.title}
${book.author} ${tagsHtml ? `
${tagsHtml}
` : ""}
⏱ ${book.read_time_str}
`; }) .join(""); } catch (e) { console.error("加载排行失败:", e); } } // ===== 书架 ===== let shelfCurrentPage = 1; async function loadShelf(page) { shelfCurrentPage = page; const search = document.getElementById("shelfSearch").value; const finished = document.getElementById("finishedOnly").checked ? "1" : "0"; const params = new URLSearchParams({ page: page, per_page: 60, category: search, finished: finished }); try { const data = await fetchAPI(`/api/shelf?${params}`); const grid = document.getElementById("bookGrid"); document.getElementById("shelfInfo").textContent = `共 ${data.total} 本 · 第 ${page}/${data.total_pages} 页`; grid.innerHTML = data.books .map((book) => { const badges = []; if (book.is_top) badges.push(`置顶`); if (book.finish_reading) badges.push(`已读完`); return `
${book.title} ${badges.join("")}
${book.title}
${book.author}
`; }) .join(""); renderPagination(page, data.total_pages); } catch (e) { console.error("加载书架失败:", e); } } function renderPagination(current, total) { const pag = document.getElementById("pagination"); if (total <= 1) { pag.innerHTML = ""; return; } let html = ``; const maxButtons = 7; let start = Math.max(1, current - 3); let end = Math.min(total, start + maxButtons - 1); start = Math.max(1, end - maxButtons + 1); if (start > 1) { html += ``; if (start > 2) html += `...`; } for (let i = start; i <= end; i++) { html += ``; } if (end < total) { if (end < total - 1) html += `...`; html += ``; } html += ``; pag.innerHTML = html; } function debounce(fn, delay) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } // ===== 笔记概览 ===== async function loadNotebooks() { try { const data = await fetchAPI("/api/notebooks"); const summary = document.getElementById("notesSummary"); const totalHighlights = data.reduce((s, d) => s + d.note_count, 0); const totalReviews = data.reduce((s, d) => s + d.review_count, 0); const totalBookmarks = data.reduce((s, d) => s + d.bookmark_count, 0); summary.innerHTML = `
${data.length}
有笔记的书
${totalHighlights}
划线
${totalReviews}
想法/点评
${totalBookmarks}
书签
`; const grid = document.getElementById("notebooksGrid"); grid.innerHTML = data .map((nb) => { const progress = nb.reading_progress || 0; return `
${nb.title}
${nb.title}
${nb.author}
划线 ${nb.note_count} 想法 ${nb.review_count} 书签 ${nb.bookmark_count}
`; }) .join(""); } catch (e) { console.error("加载笔记失败:", e); } } // ===== 书籍详情弹窗 ===== function openBook(bookId) { if (!bookId) return; showModal("bookDetailModal"); const body = document.getElementById("bookDetailBody"); body.innerHTML = `
`; fetchAPI(`/api/book/detail?book_id=${bookId}`) .then((data) => { const p = data.progress || {}; const progress = p.progress || 0; const rating = data.new_rating > 0 ? (data.new_rating / 10).toFixed(1) : null; const tags = []; if (data.category) tags.push(data.category); if (rating) tags.push(`${rating}分`); if (data.new_rating_count) tags.push(`${data.new_rating_count}人评分`); body.innerHTML = `
${data.title}
${data.title}
${data.author || ''}${data.translator ? ' / 译: ' + data.translator : ''}
${tags.map(t => `${t}`).join('')}
${p.is_start_reading ? `
阅读进度: ${p.progress}% ${p.record_reading_time_str ? ' · 累计 ' + p.record_reading_time_str : ''} ${p.finish_time ? ' · 读完于 ' + p.finish_time : ''}
` : '
尚未开始阅读
'}
${data.intro ? `
${data.intro}
` : ''}
${data.publisher ? `
出版社: ${data.publisher}
` : ''} ${data.publish_time ? `
出版时间: ${data.publish_time}
` : ''} ${data.isbn ? `
ISBN: ${data.isbn}
` : ''} ${data.word_count ? `
字数: ${(data.word_count / 10000).toFixed(1)}万字
` : ''}
`; }) .catch((e) => { body.innerHTML = `
加载失败: ${e.message}
`; }); } // ===== 想法详情独立页面 ===== function openThoughtsPage(bookId, title, author, cover) { if (!bookId) return; // 隐藏主视图,显示想法页面 document.getElementById("mainView").style.display = "none"; document.getElementById("thoughtsPage").style.display = "block"; window.scrollTo({ top: 0, behavior: "auto" }); // 渲染头部 const header = document.getElementById("thoughtsBookHeader"); header.innerHTML = ` ${title}
${title}
${author || ''}
加载中...
`; const content = document.getElementById("thoughtsContent"); content.innerHTML = `
`; fetchAPI(`/api/book/notes?book_id=${bookId}`) .then((data) => { currentThoughtsData = data; renderThoughtsPage(data); }) .catch((e) => { content.innerHTML = `
加载失败: ${e.message}
`; }); } function closeThoughtsPage() { document.getElementById("thoughtsPage").style.display = "none"; document.getElementById("mainView").style.display = ""; currentThoughtsData = null; } function renderThoughtsPage(data) { // 更新统计 const stats = document.getElementById("thoughtsBookStats"); // 分类:划线 / 书评 / 想法 const highlights = data.highlights || []; const bookReviews = (data.reviews || []).filter((r) => r.is_finish); const thoughts = (data.reviews || []).filter((r) => !r.is_finish); stats.innerHTML = ` 划线 ${highlights.length} 书评 ${bookReviews.length} 想法 ${thoughts.length} `; const content = document.getElementById("thoughtsContent"); if (highlights.length === 0 && bookReviews.length === 0 && thoughts.length === 0) { content.innerHTML = `
暂无笔记数据
`; return; } let html = `
`; content.innerHTML = html; renderThoughtsList("all"); } function filterThoughts(category) { document.querySelectorAll(".thought-filter-btn").forEach((btn) => { btn.classList.remove("active"); }); event.target.closest(".thought-filter-btn").classList.add("active"); renderThoughtsList(category); } function renderThoughtsList(category) { const data = currentThoughtsData; if (!data) return; const highlights = data.highlights || []; const bookReviews = (data.reviews || []).filter((r) => r.is_finish); const thoughts = (data.reviews || []).filter((r) => !r.is_finish); const list = document.getElementById("thoughtsList"); let html = ""; // 划线区 if (category === "all" || category === "highlight") { if (highlights.length > 0) { html += `
📌 划线 ${highlights.length}
`; const grouped = data.grouped_highlights || {}; for (const [chTitle, items] of Object.entries(grouped)) { if (category === "highlight") { html += `
`; html += `
${chTitle}
`; } items.forEach((h) => { html += `
${escapeHtml(h.mark_text)}
${h.chapter_title ? `${h.chapter_title}` : ""} ${h.create_time ? `${h.create_time}` : ""}
`; }); if (category === "highlight") html += `
`; } } } // 书评区 if (category === "all" || category === "review") { if (bookReviews.length > 0) { html += `
⭐ 书评 ${bookReviews.length}
`; bookReviews.forEach((r) => { const starVal = Math.max(0, Math.min(5, parseInt(r.star) || 0)); const stars = starVal > 0 ? `
${"★".repeat(starVal)}${"☆".repeat(5 - starVal)}
` : ""; html += `
${stars} ${escapeHtml(r.content)}
${r.chapter_name ? `${r.chapter_name}` : ""} ${r.create_time || ""}
`; }); } } // 想法区 if (category === "all" || category === "thought") { if (thoughts.length > 0) { html += `
💭 想法 ${thoughts.length}
`; thoughts.forEach((r) => { const abstract = r.abstract ? `
${escapeHtml(r.abstract)}
` : ""; html += `
${abstract}
${escapeHtml(r.content)}
${r.chapter_name ? `${r.chapter_name}` : ""} ${r.create_time || ""}
`; }); } } if (!html) { html = `
该分类暂无内容
`; } list.innerHTML = html; } // ===== 弹窗控制 ===== function showModal(id) { document.getElementById(id).classList.add("active"); document.body.style.overflow = "hidden"; } function closeModal(id) { document.getElementById(id).classList.remove("active"); document.body.style.overflow = ""; } document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeModal("bookDetailModal"); if (document.getElementById("thoughtsPage").style.display !== "none") { closeThoughtsPage(); } } }); // ===== 推荐发现 ===== let recommendMaxIdx = 0; function switchDiscoverTab(tab) { const recPanel = document.getElementById("recommendPanel"); const searchPanel = document.getElementById("searchPanel"); const tabRec = document.getElementById("tabRecommend"); const tabSearch = document.getElementById("tabSearch"); if (tab === "recommend") { recPanel.style.display = ""; searchPanel.style.display = "none"; tabRec.classList.add("active"); tabSearch.classList.remove("active"); } else { recPanel.style.display = "none"; searchPanel.style.display = ""; tabRec.classList.remove("active"); tabSearch.classList.add("active"); } } async function loadRecommend(maxIdx) { const grid = document.getElementById("recommendGrid"); grid.innerHTML = `
`; try { const data = await fetchAPI(`/api/recommend?maxIdx=${maxIdx}`); recommendMaxIdx = data.last_search_idx || 0; renderDiscoverGrid(grid, data.books, true); } catch (e) { grid.innerHTML = `
加载推荐失败: ${e.message}
`; } } async function loadMoreRecommend() { loadRecommend(recommendMaxIdx); } async function doSearch() { const keyword = document.getElementById("searchInput").value.trim(); if (!keyword) return; const grid = document.getElementById("searchGrid"); grid.innerHTML = `
`; try { const data = await fetchAPI(`/api/search?keyword=${encodeURIComponent(keyword)}`); if (data.books.length === 0) { grid.innerHTML = `
未找到相关书籍
`; return; } renderDiscoverGrid(grid, data.books, false); } catch (e) { grid.innerHTML = `
搜索失败: ${e.message}
`; } } function renderDiscoverGrid(grid, books, showReason) { grid.innerHTML = books .map((book) => { const rating = book.new_rating > 0 ? (book.new_rating / 10).toFixed(1) : null; const ratingLabel = book.rating_title ? `${book.rating_title}` : ""; const ratingHtml = rating ? `${rating}` : ""; const readingHtml = book.reading_count ? `${book.reading_count}人在读` : ""; const reasonHtml = (showReason && book.reason) ? `
${book.reason}
` : ""; const categoryHtml = book.category ? `
${book.category}
` : ""; return `
${book.title}
${book.title}
${book.author || ''}
${ratingHtml}${ratingLabel}${readingHtml}
${categoryHtml} ${reasonHtml}
`; }) .join(""); }