/* ===== 微信读书数据展示 - 前端逻辑 ===== */ const API_BASE = ""; // ===== 主题管理 ===== /** * 获取当前主题。 * @returns {"light"|"dark"} */ function getCurrentTheme() { return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light"; } /** * 切换明亮/黑暗主题。 * 持久化到 localStorage,并触发图表重绘。 */ function toggleTheme() { const newTheme = getCurrentTheme() === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", newTheme); localStorage.setItem("theme", newTheme); refreshChartsForTheme(); } /** * 从 CSS 变量读取当前主题的颜色值,供 ECharts 使用。 */ 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(), }; } // ===== 工具函数 ===== /** * 升级微信读书封面图 URL 到高清晰度版本。 * 微信读书 CDN 的封面图通过文件名前缀区分分辨率: * t6_ = 低清缩略图(~150px,Retina 屏模糊) * t8_ = 高清版(~500px,适合 2x/3x 屏幕) * 如果 URL 不含 t6_ 前缀或已是高清则原样返回。 */ 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); } // ===== 数据库健康检查 ===== // 存储所有 ECharts 实例,用于主题切换时重绘 const chartInstances = {}; /** * 主题切换后重绘所有 ECharts 图表。 */ function refreshChartsForTheme() { Object.values(chartInstances).forEach((chart) => { if (chart && !chart.isDisposed()) { chart.dispose(); } }); // 重新加载所有图表 loadYearlyTrend(); loadTimeDistribution(); loadCategories(); loadAuthors(); } /** * 页面加载时检查 MySQL 数据库连接状态。 * 如果连接失败,显示告警横幅并阻止后续数据加载。 */ 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; } } /** * 显示数据库连接告警横幅。 * @param {string} message - 告警详情信息 */ function showDBAlert(message) { const banner = document.getElementById("dbAlertBanner"); const detail = document.getElementById("dbAlertDetail"); if (banner) banner.style.display = "flex"; if (detail) detail.textContent = message; } // ===== 初始化 ===== document.addEventListener("DOMContentLoaded", async () => { // 首先检查数据库连接状态 const isHealthy = await checkDBHealth(); if (!isHealthy) { // 数据库连接失败,不加载后续数据 return; } // 数据库连接正常,加载所有数据模块 loadOverview(); loadYearlyTrend(); loadTimeDistribution(); loadCategories(); loadAuthors(); loadTopBooks(); loadShelf(1); loadNotebooks(); loadRecommend(0); initNavScroll(); }); // ===== 导航栏滚动高亮 ===== function initNavScroll() { const links = document.querySelectorAll(".nav-link"); const sections = document.querySelectorAll("section[id]"); window.addEventListener("scroll", () => { let current = ""; sections.forEach((sec) => { const top = sec.offsetTop - 100; if (window.scrollY >= top) current = sec.id; }); links.forEach((link) => { link.classList.toggle("active", link.getAttribute("href") === `#${current}`); }); }); links.forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const target = document.querySelector(link.getAttribute("href")); if (target) { window.scrollTo({ top: target.offsetTop - 60, behavior: "smooth" }); } }); }); } // ===== 总览数据 ===== async function loadOverview() { try { const data = await fetchAPI("/api/overview"); // Hero 统计 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 grid = document.getElementById("topBooksGrid"); grid.innerHTML = data .map((book, i) => { const tagsHtml = book.tags .map((t) => `${t}`) .join(""); return `
${i + 1} ${book.title}
${book.title}
${book.author}
⏱ ${book.read_time_str}
${tagsHtml ? `
${tagsHtml}
` : ""}
`; }) .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; } // 搜索和筛选事件 document.getElementById("shelfSearch").addEventListener("input", debounce(() => loadShelf(1), 300)); document.getElementById("finishedOnly").addEventListener("change", () => loadShelf(1)); 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('')}
${progress.is_start_reading ? `
阅读进度: ${progress.progress}% ${progress.record_reading_time_str ? ' · 累计 ' + progress.record_reading_time_str : ''} ${progress.finish_time ? ' · 读完于 ' + progress.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 openNotes(bookId, title, author, cover) { if (!bookId) return; showModal("notesDetailModal"); const body = document.getElementById("notesDetailBody"); body.innerHTML = `
${title}
${title}
${author || ''}
`; fetchAPI(`/api/book/notes?book_id=${bookId}`) .then((data) => { let html = `
${title}
${title}
${author || ''}
划线 ${data.highlight_count} 想法/点评 ${data.review_count}
`; // 划线区 if (data.highlight_count > 0) { html += `
📌 划线 (${data.highlight_count})
`; const grouped = data.grouped_highlights; for (const [chTitle, items] of Object.entries(grouped)) { html += `
`; html += `
${chTitle}
`; items.forEach((h) => { html += `
${escapeHtml(h.mark_text)}
${h.create_time || ''}
`; }); html += `
`; } } // 想法/点评区 if (data.review_count > 0) { html += `
💭 想法/点评 (${data.review_count})
`; data.reviews.forEach((r) => { const starVal = Math.max(0, Math.min(5, parseInt(r.star) || 0)); const stars = starVal > 0 ? `${"★".repeat(starVal)}${"☆".repeat(5 - starVal)}` : ""; html += `
${escapeHtml(r.content)}
${stars} ${r.chapter_name ? ' · ' + r.chapter_name : ''} ${r.is_finish ? ' · 书评' : ''} · ${r.create_time || ''}
`; }); } if (data.highlight_count === 0 && data.review_count === 0) { html += `
暂无笔记数据
`; } body.innerHTML = html; }) .catch((e) => { body.innerHTML = `
加载失败: ${e.message}
`; }); } // ===== 弹窗控制 ===== 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 = ""; } // ESC 关闭弹窗 document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeModal("bookDetailModal"); closeModal("notesDetailModal"); } }); // ===== HTML 转义 ===== function escapeHtml(text) { if (!text) return ""; const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } // ===== 推荐发现 ===== 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(""); }