diff --git a/static/css/style.css b/static/css/style.css index 8a6fc2a..48b3850 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -430,102 +430,117 @@ body { height: 320px; } -/* ===== 读书排行 ===== */ -.top-books-section { - padding: 60px 0; - background: var(--accent-bg); - transition: background-color 0.3s ease; +/* ===== 标签页切换 ===== */ +.tab-panel { + display: none; + animation: fadeInTab 0.3s ease; } -.top-books-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 20px; +.tab-panel.active { + display: block; } -.top-book-card { +@keyframes fadeInTab { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ===== 读书排行(竖向列表) ===== */ +.top-books-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.top-book-row { + display: flex; + align-items: center; + gap: 16px; background: var(--bg-card); border-radius: var(--radius); - overflow: hidden; + padding: 12px 16px; box-shadow: var(--shadow); - transition: transform 0.2s, box-shadow 0.3s, background-color 0.3s, border-color 0.3s; border: 1px solid var(--border); cursor: pointer; + transition: transform 0.2s, box-shadow 0.3s, background-color 0.3s, border-color 0.3s; } -.top-book-card:hover { - transform: translateY(-4px); +.top-book-row:hover { + transform: translateX(4px); box-shadow: var(--shadow-hover); + border-color: var(--accent); } -.top-book-rank { - position: absolute; - top: 8px; - left: 8px; - background: var(--rank-bg); - color: var(--rank-text); - width: 28px; - height: 28px; +.top-book-rank-num { + flex-shrink: 0; + width: 36px; + height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - font-size: 14px; - font-weight: 700; - z-index: 5; + font-size: 16px; + font-weight: 800; + background: var(--accent-bg); + color: var(--accent-dark); } -.top-book-cover-wrapper { - position: relative; - width: 100%; - aspect-ratio: 3 / 4; - border-radius: var(--radius-sm) var(--radius-sm) 0 0; - overflow: hidden; +.top-book-row:nth-child(1) .top-book-rank-num { + background: linear-gradient(135deg, #ffd700, #ffa500); + color: #fff; +} + +.top-book-row:nth-child(2) .top-book-rank-num { + background: linear-gradient(135deg, #c0c0c0, #a8a8a8); + color: #fff; +} + +.top-book-row:nth-child(3) .top-book-rank-num { + background: linear-gradient(135deg, #cd7f32, #b87333); + color: #fff; +} + +.top-book-mini-cover { + flex-shrink: 0; + width: 48px; + height: 64px; + border-radius: 6px; + object-fit: cover; + box-shadow: var(--shadow); background: var(--bg-primary); } -.top-book-cover { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s; +.top-book-row-info { + flex: 1; + min-width: 0; } -.top-book-card:hover .top-book-cover { - transform: scale(1.05); -} - -.top-book-info { - padding: 14px 16px; -} - -.top-book-title { - font-size: 14px; +.top-book-row-title { + font-size: 15px; font-weight: 600; color: var(--text-primary); - margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.top-book-author { +.top-book-row-meta { + display: flex; + align-items: center; + gap: 12px; + margin-top: 4px; font-size: 12px; color: var(--text-light); - margin-bottom: 8px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } -.top-book-time { +.top-book-row-time { font-size: 13px; color: var(--accent); font-weight: 600; + flex-shrink: 0; } -.top-book-tags { - margin-top: 6px; +.top-book-row-tags { display: flex; gap: 4px; flex-wrap: wrap; @@ -1354,6 +1369,324 @@ body { font-size: 14px; } +/* ===== 想法详情独立页面 ===== */ +.thoughts-page { + min-height: 100vh; + background: var(--bg-primary); +} + +.thoughts-page-header { + background: var(--bg-card); + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow); + padding: 16px 0 20px; + position: sticky; + top: 60px; + z-index: 50; +} + +.back-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 16px; + border: 1px solid var(--border); + border-radius: 20px; + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s; + margin-bottom: 16px; +} + +.back-btn:hover { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-bg); +} + +.thoughts-book-header { + display: flex; + gap: 16px; + align-items: center; +} + +.thoughts-book-cover { + width: 64px; + height: 85px; + border-radius: 8px; + object-fit: cover; + flex-shrink: 0; + box-shadow: var(--shadow); +} + +.thoughts-book-title { + font-size: 20px; + font-weight: 700; + color: var(--text-primary); +} + +.thoughts-book-author { + font-size: 13px; + color: var(--text-light); + margin-top: 4px; +} + +.thoughts-book-stats { + display: flex; + gap: 16px; + margin-top: 8px; + font-size: 13px; + color: var(--text-secondary); +} + +.thoughts-book-stats strong { + color: var(--accent-dark); +} + +/* 想法页面分类筛选 */ +.thoughts-filters { + display: flex; + gap: 8px; + margin: 24px 0 20px; + flex-wrap: wrap; +} + +.thought-filter-btn { + padding: 6px 18px; + border: 1px solid var(--border); + border-radius: 20px; + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 6px; +} + +.thought-filter-btn .filter-count { + font-size: 12px; + background: var(--accent-bg); + color: var(--accent-dark); + padding: 1px 8px; + border-radius: 10px; +} + +.thought-filter-btn.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +.thought-filter-btn.active .filter-count { + background: rgba(255,255,255,0.25); + color: #fff; +} + +.thought-filter-btn:hover:not(.active) { + border-color: var(--accent); + color: var(--accent); +} + +/* 分类区块标题 */ +.thoughts-section-title { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + margin: 28px 0 16px; + padding-bottom: 10px; + border-bottom: 2px solid var(--border); + display: flex; + align-items: center; + gap: 8px; +} + +.thoughts-section-title .section-badge { + font-size: 12px; + font-weight: 600; + padding: 2px 10px; + border-radius: 10px; +} + +.badge-highlight { + background: var(--accent-bg-hover); + color: var(--accent-dark); +} + +.badge-review { + background: var(--rating-bg); + color: var(--rating-color); +} + +.badge-thought { + background: rgba(76, 175, 80, 0.12); + color: #4caf50; +} + +[data-theme="dark"] .badge-thought { + background: rgba(76, 175, 80, 0.15); + color: #66bb6a; +} + +/* 划线条目 */ +.thought-highlight-item { + padding: 14px 18px; + background: var(--chart-highlight-bg); + border-radius: var(--radius-sm); + margin-bottom: 10px; + font-size: 15px; + color: var(--text-primary); + line-height: 1.8; + border-left: 4px solid var(--accent-light); + transition: background-color 0.3s; +} + +.thought-highlight-meta { + font-size: 12px; + color: var(--text-light); + margin-top: 8px; + display: flex; + gap: 12px; +} + +/* 书评条目 */ +.thought-review-item { + padding: 16px 20px; + background: var(--chart-review-bg); + border-radius: var(--radius-sm); + margin-bottom: 10px; + font-size: 15px; + color: var(--text-primary); + line-height: 1.8; + border-left: 4px solid var(--rating-star); + transition: background-color 0.3s; +} + +.thought-review-stars { + color: var(--rating-star); + font-size: 14px; + margin-bottom: 6px; +} + +.thought-review-meta { + font-size: 12px; + color: var(--text-light); + margin-top: 8px; + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +/* 想法条目 */ +.thought-thought-item { + padding: 16px 20px; + background: var(--bg-card); + border-radius: var(--radius-sm); + margin-bottom: 10px; + border-left: 4px solid #4caf50; + transition: background-color 0.3s, border-color 0.3s; +} + +[data-theme="dark"] .thought-thought-item { + border-left-color: #66bb6a; +} + +.thought-thought-abstract { + font-size: 13px; + color: var(--text-light); + background: var(--bg-secondary); + padding: 8px 12px; + border-radius: 6px; + margin-bottom: 10px; + line-height: 1.6; + border-left: 3px solid var(--border); +} + +.thought-thought-content { + font-size: 15px; + color: var(--text-primary); + line-height: 1.8; +} + +.thought-thought-meta { + font-size: 12px; + color: var(--text-light); + margin-top: 8px; + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.thoughts-empty { + text-align: center; + padding: 60px 0; + color: var(--text-light); + font-size: 15px; +} + +/* 章节分组 */ +.thoughts-chapter-group { + margin-bottom: 24px; +} + +.thoughts-chapter-title { + font-size: 14px; + font-weight: 600; + color: var(--accent-dark); + margin-bottom: 10px; + padding: 6px 0 6px 12px; + border-left: 4px solid var(--accent); +} + +/* ===== 回到顶部按钮 ===== */ +.back-to-top { + position: fixed; + bottom: 32px; + right: 32px; + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--accent); + color: #fff; + border: none; + cursor: pointer; + font-size: 22px; + line-height: 1; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + z-index: 90; + opacity: 0; + visibility: hidden; + transform: translateY(20px); + transition: opacity 0.3s, visibility 0.3s, transform 0.3s, background-color 0.3s; +} + +.back-to-top.visible { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.back-to-top:hover { + background: var(--accent-dark); + transform: translateY(-2px); +} + +/* 书籍详情弹窗 - 查看想法按钮 */ +.detail-action-thoughts { + background: var(--bg-card); + border: 1px solid var(--accent); + color: var(--accent); +} + +.detail-action-thoughts:hover { + background: var(--accent-bg); +} + /* ===== 响应式 ===== */ @media (max-width: 768px) { .nav-links { @@ -1378,10 +1711,40 @@ body { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 12px; } - .top-books-grid { - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + .top-books-list { + gap: 8px; + } + .top-book-row { + padding: 8px 12px; + gap: 10px; + } + .top-book-mini-cover { + width: 36px; + height: 48px; + } + .top-book-rank-num { + width: 28px; + height: 28px; + font-size: 13px; } .nav-update { display: none; } + .back-to-top { + bottom: 20px; + right: 20px; + width: 40px; + height: 40px; + font-size: 18px; + } + .thoughts-page-header { + top: 0; + } + .thoughts-book-cover { + width: 48px; + height: 64px; + } + .thoughts-book-title { + font-size: 16px; + } } diff --git a/static/js/app.js b/static/js/app.js index 4985cf9..bb73ef7 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -4,18 +4,10 @@ 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); @@ -23,9 +15,6 @@ function toggleTheme() { refreshChartsForTheme(); } -/** - * 从 CSS 变量读取当前主题的颜色值,供 ECharts 使用。 - */ function getChartColors() { const cs = getComputedStyle(document.documentElement); return { @@ -45,13 +34,6 @@ function getChartColors() { // ===== 工具函数 ===== -/** - * 升级微信读书封面图 URL 到高清晰度版本。 - * 微信读书 CDN 的封面图通过文件名前缀区分分辨率: - * t6_ = 低清缩略图(~150px,Retina 屏模糊) - * t8_ = 高清版(~500px,适合 2x/3x 屏幕) - * 如果 URL 不含 t6_ 前缀或已是高清则原样返回。 - */ function hdCover(url) { if (!url) return ""; return url.replace("/t6_", "/t8_"); @@ -68,55 +50,47 @@ function formatHours(seconds) { return (seconds / 3600).toFixed(1); } +function escapeHtml(text) { + if (!text) return ""; + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + // ===== 数据库健康检查 ===== -// 存储所有 ECharts 实例,用于主题切换时重绘 const chartInstances = {}; +let chartsLoaded = false; +let currentThoughtsData = null; -/** - * 主题切换后重绘所有 ECharts 图表。 - */ function refreshChartsForTheme() { + if (!chartsLoaded) return; Object.values(chartInstances).forEach((chart) => { - if (chart && !chart.isDisposed()) { - chart.dispose(); - } + 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"); @@ -124,61 +98,78 @@ function showDBAlert(message) { if (detail) detail.textContent = message; } -// ===== 初始化 ===== -document.addEventListener("DOMContentLoaded", async () => { - // 首先检查数据库连接状态 - const isHealthy = await checkDBHealth(); - if (!isHealthy) { - // 数据库连接失败,不加载后续数据 - return; +// ===== 标签页切换 ===== + +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(); - loadYearlyTrend(); - loadTimeDistribution(); - loadCategories(); - loadAuthors(); loadTopBooks(); loadShelf(1); loadNotebooks(); loadRecommend(0); - initNavScroll(); + initBackToTop(); + + // 搜索和筛选事件 + document.getElementById("shelfSearch").addEventListener("input", debounce(() => loadShelf(1), 300)); + document.getElementById("finishedOnly").addEventListener("change", () => loadShelf(1)); }); -// ===== 导航栏滚动高亮 ===== -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 = `
@@ -202,7 +193,6 @@ async function loadOverview() { document.getElementById("heroSubtitle").textContent = `自 ${data.regist_time} 加入微信读书,已陪伴你 ${data.read_days} 个阅读的日子`; - // 统计卡片 const statsGrid = document.getElementById("statsGrid"); statsGrid.innerHTML = `
@@ -231,7 +221,6 @@ async function loadOverview() {
`; - // 更新时间 const timeStr = `数据更新: ${data.fetch_time}`; document.getElementById("fetchTime").textContent = timeStr; document.getElementById("footerTime").textContent = data.fetch_time; @@ -278,31 +267,20 @@ async function loadYearlyTrend() { 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", - }, + 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) { @@ -338,11 +316,7 @@ async function loadTimeDistribution() { xAxis: { type: "category", data: hours, - axisLabel: { - color: c.axis, - fontSize: 10, - interval: 2, - }, + axisLabel: { color: c.axis, fontSize: 10, interval: 2 }, axisLine: { lineStyle: { color: c.axisLine } }, }, yAxis: { @@ -352,27 +326,22 @@ async function loadTimeDistribution() { 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" }, - ], - }, - }, + 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) { @@ -414,33 +383,19 @@ async function loadCategories() { 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", - ], + 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) { @@ -486,31 +441,20 @@ async function loadAuthors() { 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}本", - }, + 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) { @@ -518,31 +462,31 @@ async function loadAuthors() { } } -// ===== 读书排行 ===== +// ===== 读书排行(竖向列表) ===== async function loadTopBooks() { try { const data = await fetchAPI("/api/top-books"); - const grid = document.getElementById("topBooksGrid"); + const list = document.getElementById("topBooksGrid"); - grid.innerHTML = data + list.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}
` : ""} +
+ ${i + 1} + ${book.title} +
+
${book.title}
+
+ ${book.author} + ${tagsHtml ? `
${tagsHtml}
` : ""} +
+
⏱ ${book.read_time_str}
`; }) .join(""); @@ -558,12 +502,7 @@ 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, - }); + const params = new URLSearchParams({ page: page, per_page: 60, category: search, finished: finished }); try { const data = await fetchAPI(`/api/shelf?${params}`); @@ -591,7 +530,6 @@ async function loadShelf(page) { }) .join(""); - // 分页 renderPagination(page, data.total_pages); } catch (e) { console.error("加载书架失败:", e); @@ -600,13 +538,9 @@ async function loadShelf(page) { function renderPagination(current, total) { const pag = document.getElementById("pagination"); - if (total <= 1) { - pag.innerHTML = ""; - return; - } + 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); @@ -616,31 +550,20 @@ function renderPagination(current, total) { 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); - }; + return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } // ===== 笔记概览 ===== @@ -677,7 +600,7 @@ async function loadNotebooks() { .map((nb) => { const progress = nb.reading_progress || 0; return ` -
+
${nb.title} @@ -701,7 +624,7 @@ async function loadNotebooks() { } } -// ===== 打开书籍详情弹窗 ===== +// ===== 书籍详情弹窗 ===== function openBook(bookId) { if (!bookId) return; showModal("bookDetailModal"); @@ -726,12 +649,12 @@ function openBook(bookId) {
${data.title}
${data.author || ''}${data.translator ? ' / 译: ' + data.translator : ''}
${tags.map(t => `${t}`).join('')}
- ${progress.is_start_reading ? ` + ${p.is_start_reading ? `
- 阅读进度: ${progress.progress}% - ${progress.record_reading_time_str ? ' · 累计 ' + progress.record_reading_time_str : ''} - ${progress.finish_time ? ' · 读完于 ' + progress.finish_time : ''} + 阅读进度: ${p.progress}% + ${p.record_reading_time_str ? ' · 累计 ' + p.record_reading_time_str : ''} + ${p.finish_time ? ' · 读完于 ' + p.finish_time : ''}
@@ -748,6 +671,7 @@ function openBook(bookId) {
+
`; }) @@ -756,88 +680,180 @@ function openBook(bookId) { }); } -// ===== 打开笔记详情弹窗 ===== -function openNotes(bookId, title, author, cover) { +// ===== 想法详情独立页面 ===== + +function openThoughtsPage(bookId, title, author, cover) { if (!bookId) return; - showModal("notesDetailModal"); - const body = document.getElementById("notesDetailBody"); - body.innerHTML = ` -
- ${title} -
-
${title}
-
${author || ''}
-
+ + // 隐藏主视图,显示想法页面 + 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) => { - let html = ` -
- ${title} -
-
${title}
-
${author || ''}
-
- 划线 ${data.highlight_count} - 想法/点评 ${data.review_count} -
-
-
- `; + currentThoughtsData = data; + renderThoughtsPage(data); + }) + .catch((e) => { + content.innerHTML = `
加载失败: ${e.message}
`; + }); +} - // 划线区 - 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 += `
`; +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}
`; } - } - - // 想法/点评区 - 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)}` : ""; + items.forEach((h) => { html += ` -
- ${escapeHtml(r.content)} -
- ${stars} - ${r.chapter_name ? ' · ' + r.chapter_name : ''} - ${r.is_finish ? ' · 书评' : ''} - · ${r.create_time || ''} +
+ ${escapeHtml(h.mark_text)} +
+ ${h.chapter_title ? `${h.chapter_title}` : ""} + ${h.create_time ? `${h.create_time}` : ""}
`; }); + if (category === "highlight") html += `
`; } + } + } - if (data.highlight_count === 0 && data.review_count === 0) { - 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 || ""} +
+
+ `; + }); + } + } - body.innerHTML = html; - }) - .catch((e) => { - body.innerHTML = `
加载失败: ${e.message}
`; - }); + // 想法区 + 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; } // ===== 弹窗控制 ===== @@ -851,22 +867,15 @@ function closeModal(id) { document.body.style.overflow = ""; } -// ESC 关闭弹窗 document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeModal("bookDetailModal"); - closeModal("notesDetailModal"); + if (document.getElementById("thoughtsPage").style.display !== "none") { + closeThoughtsPage(); + } } }); -// ===== HTML 转义 ===== -function escapeHtml(text) { - if (!text) return ""; - const div = document.createElement("div"); - div.textContent = text; - return div.innerHTML; -} - // ===== 推荐发现 ===== let recommendMaxIdx = 0; diff --git a/templates/index.html b/templates/index.html index 885064a..0dedfea 100644 --- a/templates/index.html +++ b/templates/index.html @@ -29,17 +29,17 @@ - -
-
-
-

阅读,是一场漫长的旅行

-

正在加载数据...

-
- -
-
-
+ +
- -
-
-
- + +
+
+
+
+

阅读,是一场漫长的旅行

+

正在加载数据...

+
+
-
-
+
+
+
+
+
+ - -
-
-

📊 阅读数据可视化

-
-
-

年度阅读时长趋势

-
-
-
-

24小时阅读时段分布

-
-
-
-

偏好阅读分类

-
-
-
-

偏好作者 TOP 10

-
+ +
+
+

📊 阅读数据可视化

+
+
+

年度阅读时长趋势

+
+
+
+

24小时阅读时段分布

+
+
+
+

偏好阅读分类

+
+
+
+

偏好作者 TOP 10

+
+
-
-
+ - -
-
-

🏆 阅读时长排行

-
- + +
+
+

🏆 阅读时长排行

+
-
-
+ - -
-
-

📚 我的书架

-
-
- - + +
+
+

📚 我的书架

+
+
+ + +
+
-
+
+
-
- -
- -
-
+ - -
-
-

✍️ 笔记概览

-
- + +
+
+

✍️ 笔记概览

+
+
-
- -
-
-
+ - -
-
-

🔍 推荐发现

-
- - -
- -
-
- - + +
+
+

🔍 推荐发现

+
+ +
-
-
- -
+ +
+ + +
- - + +