From 25293a1d50b06ca815027d24c64ca43f1cdfacfd Mon Sep 17 00:00:00 2001 From: kinghwj Date: Fri, 11 Sep 2026 18:21:13 +0800 Subject: [PATCH] =?UTF-8?q?chore(legacy):=20=E5=BD=92=E6=A1=A3=20v1=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E8=87=B3=20legacy/v1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v2 成为默认前端后,把 v1 的模板与静态资源原样归档保留, 便于对照回滚。v1 后端接口 /api/* 未做改动。 --- legacy/v1/static/css/style.css | 2056 ++++++++++++++++++++++++++++++++ legacy/v1/static/js/app.js | 1267 ++++++++++++++++++++ legacy/v1/templates/index.html | 211 ++++ 3 files changed, 3534 insertions(+) create mode 100644 legacy/v1/static/css/style.css create mode 100644 legacy/v1/static/js/app.js create mode 100644 legacy/v1/templates/index.html diff --git a/legacy/v1/static/css/style.css b/legacy/v1/static/css/style.css new file mode 100644 index 0000000..92e4e86 --- /dev/null +++ b/legacy/v1/static/css/style.css @@ -0,0 +1,2056 @@ +/* ===== 主题变量 ===== */ +:root { + /* 明亮模式 - 暖纸色文学风 */ + --bg-primary: #f8f5f0; + --bg-card: #ffffff; + --bg-secondary: #f0ebe3; + --bg-hero: linear-gradient(135deg, #8B6914 0%, #c17b4a 50%, #a05c2c 100%); + --bg-hero-overlay: rgba(255, 255, 255, 0.04); + + --text-primary: #2c2416; + --text-secondary: #6b5d4f; + --text-light: #9b8e80; + + --accent: #c17b4a; + --accent-light: #e8a87c; + --accent-dark: #a05c2c; + --accent-bg: rgba(193, 123, 74, 0.08); + --accent-bg-hover: rgba(193, 123, 74, 0.12); + + --border: #e8e0d4; + --border-light: #f0ebe3; + + --shadow: 0 2px 12px rgba(44, 36, 22, 0.08); + --shadow-hover: 0 8px 24px rgba(44, 36, 22, 0.15); + --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.3); + + --radius: 12px; + --radius-sm: 8px; + --radius-xs: 4px; + + --alert-bg: #fff3cd; + --alert-border: #ffc107; + --alert-text: #856404; + + /* ECharts 专用 */ + --chart-axis: #6b5d4f; + --chart-axis-line: #e8e0d4; + --chart-split-line: #f0ebe3; + --chart-label: #6b5d4f; + --chart-border-color: #fff; + --chart-tooltip-bg: rgba(255, 255, 255, 0.95); + --chart-tooltip-border: #e8e0d4; + --chart-tooltip-text: #2c2416; + --chart-review-bg: #fff8e1; + --chart-review-border: #ffc107; + --chart-highlight-bg: #f8f5f0; + + /* 导航栏 */ + --nav-bg: rgba(255, 255, 255, 0.92); + --nav-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); + + /* 模态遮罩 */ + --modal-overlay: rgba(0, 0, 0, 0.5); + + /* 主题切换按钮 */ + --toggle-bg: #f0ebe3; + --toggle-icon-sun: #f59e0b; + --toggle-icon-moon: #6b5d4f; + + /* 评分标签 */ + --rating-color: #e65100; + --rating-bg: #fff3e0; + --rating-star: #ff9800; + + /* 徽章 */ + --badge-finished-bg: #4caf50; + --badge-top-bg: rgba(255, 215, 0, 0.9); + --badge-top-text: #5d4037; + --rank-bg: rgba(0, 0, 0, 0.7); + --rank-text: #ffd700; +} + +[data-theme="dark"] { + /* 黑暗模式 - 暖色深调,保持文学质感 */ + --bg-primary: #1a1612; + --bg-card: #252019; + --bg-secondary: #2d2820; + --bg-hero: linear-gradient(135deg, #3d2e1a 0%, #5c3f1e 50%, #4a3015 100%); + --bg-hero-overlay: rgba(255, 255, 255, 0.03); + + --text-primary: #e8e0d4; + --text-secondary: #b0a698; + --text-light: #8a7f70; + + --accent: #d4925a; + --accent-light: #e8a87c; + --accent-dark: #b87436; + --accent-bg: rgba(212, 146, 90, 0.12); + --accent-bg-hover: rgba(212, 146, 90, 0.18); + + --border: #3a342c; + --border-light: #2d2820; + + --shadow: 0 2px 12px rgba(0, 0, 0, 0.3); + --shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.4); + --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.6); + + --alert-bg: #3d2f15; + --alert-border: #8a6d1e; + --alert-text: #f0d77a; + + /* ECharts 专用 */ + --chart-axis: #b0a698; + --chart-axis-line: #3a342c; + --chart-split-line: #2d2820; + --chart-label: #b0a698; + --chart-border-color: #252019; + --chart-tooltip-bg: rgba(37, 32, 25, 0.95); + --chart-tooltip-border: #3a342c; + --chart-tooltip-text: #e8e0d4; + --chart-review-bg: #2d2515; + --chart-review-border: #66521e; + --chart-highlight-bg: #2d2820; + + /* 导航栏 */ + --nav-bg: rgba(26, 22, 18, 0.92); + --nav-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + + /* 模态遮罩 */ + --modal-overlay: rgba(0, 0, 0, 0.7); + + /* 主题切换按钮 */ + --toggle-bg: #2d2820; + --toggle-icon-sun: #f59e0b; + --toggle-icon-moon: #d4925a; + + /* 评分标签 */ + --rating-color: #ff9800; + --rating-bg: #3d2e15; + --rating-star: #ffa726; + + /* 徽章 */ + --badge-finished-bg: #2e7d32; + --badge-top-bg: rgba(255, 215, 0, 0.85); + --badge-top-text: #3d2e15; + --rank-bg: rgba(0, 0, 0, 0.8); + --rank-text: #ffd700; +} + +/* ===== 全局过渡 ===== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* 所有主题相关元素平滑过渡 */ +.navbar, .nav-link, .stat-card, .chart-card, .top-book-card, .book-card, +.book-cover-wrapper, .notebook-card, .note-stat, .discover-card, .discover-tab, +.modal-content, .modal-overlay, .footer, .db-alert-banner, .page-btn, +.btn-primary, .btn-secondary, .search-input, .shelf-search, .highlight-item, +.review-item, .detail-progress-info, .top-book-cover-wrapper, +.theme-toggle, .loading-spinner { + transition: background-color 0.3s ease, border-color 0.3s ease, + color 0.3s ease, box-shadow 0.3s ease, transform 0.2s ease; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; +} + +/* ===== 导航栏 ===== */ +.navbar { + position: sticky; + top: 0; + z-index: 100; + background: var(--nav-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); + box-shadow: var(--nav-shadow); +} + +.nav-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + height: 60px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.nav-logo { + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + font-weight: 700; + color: var(--text-primary); +} + +.logo-icon { + font-size: 24px; +} + +.nav-links { + display: flex; + gap: 24px; + align-items: center; +} + +.nav-link { + text-decoration: none; + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; + padding: 6px 12px; + border-radius: var(--radius-sm); + transition: all 0.2s; +} + +.nav-link:hover { + color: var(--accent); + background: var(--accent-bg); +} + +.nav-link.active { + color: var(--accent); + background: var(--accent-bg-hover); +} + +.nav-right { + display: flex; + align-items: center; + gap: 16px; +} + +.nav-update { + font-size: 12px; + color: var(--text-light); +} + +/* ===== 主题切换按钮 ===== */ +.theme-toggle { + width: 40px; + height: 40px; + border: 1px solid var(--border); + border-radius: 50%; + background: var(--toggle-bg); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + line-height: 1; + color: var(--toggle-icon-moon); + flex-shrink: 0; +} + +.theme-toggle:hover { + border-color: var(--accent); + transform: scale(1.08); +} + +.theme-toggle .icon-sun { display: none; } +.theme-toggle .icon-moon { display: block; } + +[data-theme="dark"] .theme-toggle .icon-sun { display: block; } +[data-theme="dark"] .theme-toggle .icon-moon { display: none; } +[data-theme="dark"] .theme-toggle { color: var(--toggle-icon-sun); } + +/* ===== 顶部常驻搜索框 ===== */ +.global-search-bar { + position: sticky; + top: 60px; + z-index: 90; + background: var(--nav-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); + padding: 10px 0; + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +.global-search-wrapper { + display: flex; + align-items: center; + gap: 0; + max-width: 600px; + margin: 0 auto; + position: relative; +} + +.global-search-icon { + position: absolute; + left: 14px; + font-size: 15px; + opacity: 0.5; + pointer-events: none; +} + +.global-search-input { + flex: 1; + padding: 9px 16px 9px 40px; + border: 1px solid var(--border); + border-right: none; + border-radius: var(--radius-sm) 0 0 var(--radius-sm); + font-size: 14px; + background: var(--bg-card); + color: var(--text-primary); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.global-search-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-bg); +} + +.global-search-btn { + padding: 9px 20px; + border: 1px solid var(--accent); + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + background: var(--accent); + color: #fff; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.global-search-btn:hover { + background: var(--accent-dark); +} + +/* ===== 书架分类标签 ===== */ +/* 层级折叠分类标签 */ +.shelf-categories { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 20px; + align-items: center; +} + +.shelf-cat-parent { + padding: 5px 14px; + border: 1px solid var(--border); + border-radius: 20px; + font-size: 13px; + color: var(--text-secondary); + background: var(--bg-card); + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 5px; +} + +.shelf-cat-parent:hover { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-bg); +} + +.shelf-cat-parent.active { + border-color: var(--accent); + color: #fff; + background: var(--accent); + font-weight: 500; +} + +.shelf-cat-count { + font-size: 11px; + padding: 1px 7px; + border-radius: 10px; + background: var(--accent-bg); + color: var(--accent-dark); +} + +.shelf-cat-parent.active .shelf-cat-count { + background: rgba(255,255,255,0.25); + color: #fff; +} + +.shelf-cat-arrow { + font-size: 9px; + margin-left: 2px; + transition: transform 0.2s; +} + +.shelf-cat-subs { + display: flex; + flex-wrap: wrap; + gap: 6px; + width: 100%; + padding: 4px 0 4px 20px; + margin-bottom: 4px; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} + +.shelf-cat-sub { + padding: 3px 12px; + border: 1px solid var(--border); + border-radius: 16px; + font-size: 12px; + color: var(--text-light); + background: var(--bg-primary); + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.shelf-cat-sub:hover { + border-color: var(--accent); + color: var(--accent); +} + +.shelf-cat-sub.active { + border-color: var(--accent); + color: #fff; + background: var(--accent); + font-weight: 500; +} + +.shelf-cat-count-sm { + font-size: 10px; + padding: 0 5px; + border-radius: 8px; + background: var(--accent-bg); + color: var(--accent-dark); +} + +.shelf-cat-sub.active .shelf-cat-count-sm { + background: rgba(255,255,255,0.25); + color: #fff; +} + +/* ===== Hero 区 ===== */ +.hero-section { + position: relative; + padding: 80px 0 60px; + overflow: hidden; + background: var(--bg-hero); + transition: background 0.5s ease; +} + +.hero-bg { + position: absolute; + inset: 0; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M50 25 L55 40 L70 40 L58 50 L63 65 L50 55 L37 65 L42 50 L30 40 L45 40 Z'/%3E%3C/g%3E%3C/svg%3E"); + opacity: 0.6; +} + +.hero-content { + position: relative; + text-align: center; + color: #fff; + max-width: 800px; + margin: 0 auto; + padding: 0 24px; +} + +.hero-title { + font-size: 36px; + font-weight: 700; + margin-bottom: 12px; + letter-spacing: 2px; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.hero-subtitle { + font-size: 16px; + opacity: 0.9; + margin-bottom: 40px; +} + +.hero-stats { + display: flex; + justify-content: center; + gap: 48px; + flex-wrap: wrap; +} + +.hero-stat { + text-align: center; +} + +.hero-stat-value { + font-size: 42px; + font-weight: 800; + line-height: 1.2; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.hero-stat-label { + font-size: 14px; + opacity: 0.85; + margin-top: 4px; +} + +/* ===== 统计卡片 ===== */ +.stats-cards-section { + padding: 40px 0 20px; + margin-top: -40px; + position: relative; + z-index: 10; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; +} + +.stat-card { + background: var(--bg-card); + border-radius: var(--radius); + padding: 24px; + box-shadow: var(--shadow); + transition: transform 0.2s, box-shadow 0.3s, background-color 0.3s, border-color 0.3s; + text-align: center; + border: 1px solid var(--border); +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-hover); +} + +.stat-card-icon { + font-size: 32px; + margin-bottom: 12px; +} + +.stat-card-value { + font-size: 28px; + font-weight: 800; + color: var(--accent-dark); + margin-bottom: 4px; +} + +.stat-card-label { + font-size: 13px; + color: var(--text-secondary); +} + +.stat-card-sub { + font-size: 12px; + color: var(--text-light); + margin-top: 4px; +} + +/* ===== Section 标题 ===== */ +.section-title { + font-size: 24px; + font-weight: 700; + margin-bottom: 32px; + color: var(--text-primary); + text-align: center; +} + +/* ===== 图表区 ===== */ +.charts-section { + padding: 60px 0; +} + +.charts-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; +} + +.chart-card { + background: var(--bg-card); + border-radius: var(--radius); + padding: 24px; + box-shadow: var(--shadow); + border: 1px solid var(--border); +} + +.chart-card.large { + grid-column: 1 / -1; +} + +.chart-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16px; +} + +.chart-container { + width: 100%; + height: 320px; +} + +/* ===== 标签页切换 ===== */ +.tab-panel { + display: none; + animation: fadeInTab 0.3s ease; +} + +.tab-panel.active { + display: block; +} + +@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); + padding: 12px 16px; + box-shadow: var(--shadow); + 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-row:hover { + transform: translateX(4px); + box-shadow: var(--shadow-hover); + border-color: var(--accent); +} + +.top-book-rank-num { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 800; + background: var(--accent-bg); + color: var(--accent-dark); +} + +.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-row-info { + flex: 1; + min-width: 0; +} + +.top-book-row-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.top-book-row-meta { + display: flex; + align-items: center; + gap: 12px; + margin-top: 4px; + font-size: 12px; + color: var(--text-light); +} + +.top-book-row-time { + font-size: 13px; + color: var(--accent); + font-weight: 600; + flex-shrink: 0; +} + +.top-book-row-tags { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.top-book-tag { + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--accent-bg-hover); + color: var(--accent-dark); +} + +/* ===== 书架 ===== */ +.shelf-section { + padding: 60px 0; +} + +.shelf-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + flex-wrap: wrap; + gap: 12px; +} + +.shelf-filters { + display: flex; + align-items: center; + gap: 16px; +} + +.shelf-search { + padding: 8px 16px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 14px; + width: 200px; + background: var(--bg-card); + color: var(--text-primary); +} + +.shelf-search:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-bg); +} + +.shelf-refresh-btn { + padding: 0 16px; + height: 36px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 13px; + color: var(--text-secondary); + background: var(--bg-card); + cursor: pointer; + transition: color 0.2s, border-color 0.2s, opacity 0.2s; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + flex-shrink: 0; + white-space: nowrap; +} + +.shelf-refresh-btn .btn-icon { + display: inline-block; + font-size: 14px; + line-height: 1; +} + +.shelf-refresh-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} + +.shelf-refresh-btn:disabled { + opacity: 0.6; + cursor: wait; +} + +.shelf-refresh-btn.loading { + border-color: var(--accent); + color: var(--accent); +} + +.filter-checkbox { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + color: var(--text-secondary); + cursor: pointer; +} + +.shelf-info { + font-size: 13px; + color: var(--text-light); +} + +.book-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 20px; + margin-bottom: 32px; +} + +.book-card { + cursor: pointer; + transition: transform 0.2s; +} + +.book-card:hover { + transform: translateY(-4px); +} + +.book-cover-wrapper { + position: relative; + width: 100%; + aspect-ratio: 3 / 4; + border-radius: var(--radius-sm); + overflow: hidden; + box-shadow: var(--shadow); + background: var(--bg-primary); +} + +.book-cover-img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s; +} + +.book-card:hover .book-cover-img { + transform: scale(1.05); +} + +.book-finished-badge { + position: absolute; + top: 6px; + right: 6px; + background: var(--badge-finished-bg); + color: white; + font-size: 10px; + padding: 2px 6px; + border-radius: var(--radius-xs); + font-weight: 600; +} + +.book-top-badge { + position: absolute; + top: 6px; + left: 6px; + background: var(--badge-top-bg); + color: var(--badge-top-text); + font-size: 10px; + padding: 2px 6px; + border-radius: var(--radius-xs); + font-weight: 600; +} + +.book-title { + font-size: 13px; + font-weight: 500; + margin-top: 8px; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.book-author { + font-size: 11px; + color: var(--text-light); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ===== 分页 ===== */ +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + margin-top: 32px; +} + +.page-btn { + padding: 8px 14px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.page-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} + +.page-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.page-btn.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +.page-info { + font-size: 13px; + color: var(--text-light); + margin: 0 8px; +} + +/* ===== 笔记区 ===== */ +.notes-section { + padding: 60px 0; + background: var(--accent-bg); + transition: background-color 0.3s ease; +} + +.notes-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; + margin-bottom: 32px; +} + +.note-stat { + background: var(--bg-card); + border-radius: var(--radius); + padding: 20px; + text-align: center; + box-shadow: var(--shadow); + border: 1px solid var(--border); +} + +.note-stat-value { + font-size: 28px; + font-weight: 800; + color: var(--accent-dark); +} + +.note-stat-label { + font-size: 13px; + color: var(--text-secondary); + margin-top: 4px; +} + +.notebooks-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; +} + +.notebook-card { + background: var(--bg-card); + border-radius: var(--radius); + padding: 16px; + display: flex; + gap: 14px; + box-shadow: var(--shadow); + border: 1px solid var(--border); + transition: transform 0.2s, box-shadow 0.3s, background-color 0.3s, border-color 0.3s; +} + +.notebook-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-hover); +} + +.notebook-cover { + width: 60px; + height: 80px; + border-radius: 6px; + object-fit: cover; + flex-shrink: 0; + box-shadow: var(--shadow); +} + +.notebook-info { + flex: 1; + min-width: 0; +} + +.notebook-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.notebook-author { + font-size: 12px; + color: var(--text-light); + margin-bottom: 8px; +} + +.notebook-stats { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.notebook-stat-item { + font-size: 12px; + color: var(--text-secondary); +} + +.notebook-stat-item strong { + color: var(--accent-dark); +} + +.notebook-progress { + margin-top: 8px; + height: 4px; + background: var(--border); + border-radius: 2px; + overflow: hidden; +} + +.notebook-progress-bar { + height: 100%; + background: var(--accent); + border-radius: 2px; + transition: width 0.3s; +} + +/* ===== 页脚 ===== */ +.footer { + padding: 40px 0; + text-align: center; + color: var(--text-light); + font-size: 13px; + border-top: 1px solid var(--border); + background: var(--bg-card); +} + +.footer-hint { + margin-top: 8px; + font-size: 12px; + opacity: 0.7; +} + +/* ===== 数据库连接告警横幅 ===== */ +.db-alert-banner { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9999; + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: var(--alert-bg); + border-bottom: 2px solid var(--alert-border); + color: var(--alert-text); + font-size: 14px; + font-weight: 500; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + animation: slideDown 0.3s ease-out; +} + +.db-alert-icon { + font-size: 20px; +} + +.db-alert-text { + flex-shrink: 0; +} + +.db-alert-detail { + font-size: 12px; + opacity: 0.8; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@keyframes slideDown { + from { transform: translateY(-100%); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* ===== 加载动画 ===== */ +.loading { + display: flex; + justify-content: center; + align-items: center; + padding: 60px 0; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ===== 推荐发现区 ===== */ +.discover-section { + padding: 60px 0; +} + +.discover-tabs { + display: flex; + justify-content: center; + gap: 12px; + margin-bottom: 24px; +} + +.discover-tab { + padding: 8px 24px; + 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; +} + +.discover-tab.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +.discover-controls { + display: flex; + justify-content: center; + gap: 12px; + margin-bottom: 24px; +} + +.btn-primary { + padding: 8px 20px; + border: none; + border-radius: var(--radius-sm); + background: var(--accent); + color: #fff; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background 0.2s; +} + +.btn-primary:hover { + background: var(--accent-dark); +} + +.btn-secondary { + padding: 8px 20px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.btn-secondary:hover { + border-color: var(--accent); + color: var(--accent); +} + +.search-box { + display: flex; + gap: 12px; + max-width: 500px; + margin: 0 auto 24px; +} + +.search-input { + flex: 1; + padding: 10px 16px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 14px; + background: var(--bg-card); + color: var(--text-primary); +} + +.search-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-bg); +} + +.discover-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 20px; +} + +.discover-card { + background: var(--bg-card); + border-radius: var(--radius); + overflow: hidden; + box-shadow: var(--shadow); + border: 1px solid var(--border); + transition: transform 0.2s, box-shadow 0.3s, background-color 0.3s, border-color 0.3s; + cursor: pointer; +} + +.discover-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-hover); +} + +.discover-cover { + width: 100%; + aspect-ratio: 3 / 4; + object-fit: cover; + background: var(--bg-primary); +} + +.discover-info { + padding: 14px 16px; +} + +.discover-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.discover-author { + font-size: 12px; + color: var(--text-light); + margin-bottom: 6px; +} + +.discover-meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.discover-rating { + font-size: 13px; + color: var(--rating-color); + font-weight: 600; +} + +.discover-rating-label { + font-size: 11px; + padding: 2px 6px; + border-radius: var(--radius-xs); + background: var(--rating-bg); + color: var(--rating-color); +} + +.discover-reading-count { + font-size: 11px; + color: var(--text-light); +} + +.discover-reason { + margin-top: 6px; + font-size: 12px; + color: var(--accent); + background: var(--accent-bg); + padding: 4px 8px; + border-radius: var(--radius-xs); +} + +.discover-category { + font-size: 11px; + color: var(--text-light); + margin-top: 4px; +} + +/* ===== 弹窗通用 ===== */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--modal-overlay); + z-index: 1000; + justify-content: center; + align-items: flex-start; + padding: 40px 20px; + overflow-y: auto; +} + +.modal-overlay.active { + display: flex; +} + +.modal-content { + background: var(--bg-card); + border-radius: var(--radius); + max-width: 700px; + width: 100%; + position: relative; + box-shadow: var(--shadow-modal); + margin-bottom: 40px; + border: 1px solid var(--border); +} + +.modal-close { + position: absolute; + top: 12px; + right: 16px; + font-size: 28px; + color: var(--text-light); + cursor: pointer; + z-index: 10; + line-height: 1; +} + +.modal-close:hover { + color: var(--text-primary); +} + +.modal-body { + padding: 32px; +} + +/* ===== 书籍详情弹窗 ===== */ +.book-detail-modal .detail-header { + display: flex; + gap: 20px; + margin-bottom: 24px; +} + +.book-detail-modal .detail-cover { + width: 120px; + height: 160px; + border-radius: 8px; + object-fit: cover; + box-shadow: var(--shadow); + flex-shrink: 0; +} + +.book-detail-modal .detail-meta { + flex: 1; +} + +.book-detail-modal .detail-title { + font-size: 20px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; + line-height: 1.4; +} + +.book-detail-modal .detail-author { + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.book-detail-modal .detail-tags { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.book-detail-modal .detail-tag { + font-size: 12px; + padding: 2px 10px; + border-radius: 10px; + background: var(--accent-bg); + color: var(--accent-dark); +} + +.book-detail-modal .detail-progress-info { + margin-top: 12px; + padding: 12px; + background: var(--bg-primary); + border-radius: var(--radius-sm); +} + +.book-detail-modal .detail-progress-bar { + height: 8px; + background: var(--border); + border-radius: 4px; + overflow: hidden; + margin-top: 8px; +} + +.book-detail-modal .detail-progress-fill { + height: 100%; + background: var(--accent); + border-radius: 4px; + transition: width 0.3s; +} + +.book-detail-modal .detail-intro { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.8; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--border); +} + +.book-detail-modal .detail-info-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 16px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--border); +} + +.book-detail-modal .detail-info-item { + font-size: 13px; + color: var(--text-secondary); +} + +.book-detail-modal .detail-info-item strong { + color: var(--text-primary); +} + +.book-detail-modal .detail-actions { + margin-top: 20px; + display: flex; + gap: 12px; +} + +/* ===== 笔记详情弹窗 ===== */ +.notes-detail-modal { + max-width: 800px; +} + +.notes-detail-modal .notes-header { + display: flex; + gap: 16px; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border); +} + +.notes-detail-modal .notes-cover { + width: 60px; + height: 80px; + border-radius: 6px; + object-fit: cover; + flex-shrink: 0; + box-shadow: var(--shadow); +} + +.notes-detail-modal .notes-title { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); +} + +.notes-detail-modal .notes-author { + font-size: 13px; + color: var(--text-light); + margin-top: 4px; +} + +.notes-detail-modal .notes-counts { + display: flex; + gap: 16px; + margin-top: 8px; + font-size: 13px; + color: var(--text-secondary); +} + +.notes-detail-modal .notes-counts strong { + color: var(--accent-dark); +} + +.notes-detail-modal .notes-section-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + margin: 20px 0 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); +} + +.notes-detail-modal .chapter-group { + margin-bottom: 20px; +} + +.notes-detail-modal .chapter-title { + font-size: 13px; + font-weight: 600; + color: var(--accent-dark); + margin-bottom: 8px; + padding: 4px 0; + border-left: 3px solid var(--accent); + padding-left: 10px; +} + +.notes-detail-modal .highlight-item { + padding: 10px 14px; + background: var(--chart-highlight-bg); + border-radius: var(--radius-sm); + margin-bottom: 8px; + font-size: 14px; + color: var(--text-primary); + line-height: 1.7; + border-left: 3px solid var(--accent-light); +} + +.notes-detail-modal .highlight-date { + font-size: 11px; + color: var(--text-light); + margin-top: 4px; +} + +.notes-detail-modal .review-item { + padding: 10px 14px; + background: var(--chart-review-bg); + border-radius: var(--radius-sm); + margin-bottom: 8px; + font-size: 14px; + color: var(--text-primary); + line-height: 1.7; + border-left: 3px solid var(--chart-review-border); +} + +.notes-detail-modal .review-meta { + font-size: 11px; + color: var(--text-light); + margin-top: 4px; +} + +.notes-detail-modal .review-star { + color: var(--rating-star); +} + +.notes-detail-modal .empty-state { + text-align: center; + padding: 40px; + color: var(--text-light); + 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-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin: 24px 0 20px; + flex-wrap: wrap; +} + +.thoughts-filters { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.thoughts-controls { + display: flex; + gap: 10px; + align-items: center; +} + +.thoughts-search-input { + padding: 7px 14px; + border: 1px solid var(--border); + border-radius: 20px; + background: var(--bg-card); + color: var(--text-primary); + font-size: 14px; + width: 240px; + transition: border-color 0.2s, box-shadow 0.2s; + outline: none; +} + +.thoughts-search-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-bg); +} + +.thoughts-search-input::placeholder { + color: var(--text-light); +} + +.thoughts-sort-select { + padding: 7px 14px; + border: 1px solid var(--border); + border-radius: 20px; + background: var(--bg-card); + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + outline: none; + transition: border-color 0.2s; +} + +.thoughts-sort-select:hover { + border-color: var(--accent); +} + +.thoughts-sort-select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-bg); +} + +.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 { + display: none; + } + .hero-title { + font-size: 24px; + } + .hero-stats { + gap: 24px; + } + .hero-stat-value { + font-size: 32px; + } + .charts-grid { + grid-template-columns: 1fr; + } + .chart-card.large { + grid-column: auto; + } + .book-grid { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 12px; + } + .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; + } + .global-search-wrapper { + max-width: 100%; + } + .thoughts-toolbar { + flex-direction: column; + align-items: stretch; + } + .thoughts-controls { + flex-direction: column; + gap: 8px; + } + .thoughts-search-input { + width: 100%; + } + .thoughts-sort-select { + width: 100%; + } + .shelf-search { + width: 140px; + } + .shelf-categories { + gap: 6px; + } + .shelf-cat-parent { + font-size: 12px; + padding: 4px 10px; + } + .shelf-cat-sub { + font-size: 11px; + padding: 3px 9px; + } + .shelf-cat-subs { + padding-left: 10px; + } + .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/legacy/v1/static/js/app.js b/legacy/v1/static/js/app.js new file mode 100644 index 0000000..daf6e68 --- /dev/null +++ b/legacy/v1/static/js/app.js @@ -0,0 +1,1267 @@ +/* ===== 微信读书数据展示 - 前端逻辑 ===== */ + +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(), + }; +} + +// ===== 工具函数 ===== + +/** + * 升级微信读书封面图 URL 到高清晰度版本。 + * 微信读书 CDN 的封面图通过文件名前缀区分分辨率: + * t6_ = 低清缩略图(~150px,Retina 屏模糊) + * s_ = 超低清缩略图(~90px,搜索结果常用,更模糊) + * t8_ = 高清版(~500px,适合 2x/3x 屏幕) + * 将 t6_ 和 s_ 前缀统一升级为 t8_ 高清版。 + */ +function hdCover(url) { + if (!url) return ""; + return url.replace(/\/(t6_|s_)/, "/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; +let currentThoughtsCategory = "all"; +let currentThoughtsSort = "time_desc"; +let currentThoughtsSearch = ""; + +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) { + // 如果想法详情页正在显示,先关闭它回到主视图 + const thoughtsPage = document.getElementById("thoughtsPage"); + if (thoughtsPage && thoughtsPage.style.display !== "none") { + thoughtsPage.style.display = "none"; + document.getElementById("mainView").style.display = ""; + currentThoughtsData = null; + } + + // 关闭可能打开的弹窗 + closeModal("bookDetailModal"); + + 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" }); +} + +/** + * 在微信读书中打开书籍。 + * 优先使用 API 返回的 deep_link(https://weread.qq.com/... 网页链接), + * 没有则用 book_id 构造网页链接。先尝试 weread:// 深链接唤起 App, + * 1.5 秒后无响应则回退到网页版。 + */ +function openInWeRead(bookId, deepLink) { + const webUrl = deepLink || `https://weread.qq.com/#book/${bookId}`; + const schemeUrl = `weread://reading?bId=${bookId}`; + + // 尝试唤起 App(移动端),同时设置定时器回退到网页版 + let triggered = false; + const fallbackTimer = setTimeout(() => { + if (!triggered) window.open(webUrl, "_blank"); + }, 1500); + + // 监听页面可见性变化(App 唤起成功会切到后台) + const onVisChange = () => { + if (document.hidden) { + triggered = true; + clearTimeout(fallbackTimer); + } + }; + document.addEventListener("visibilitychange", onVisChange); + + // 尝试打开深链接 + const iframe = document.createElement("iframe"); + iframe.style.display = "none"; + iframe.src = schemeUrl; + document.body.appendChild(iframe); + setTimeout(() => document.body.removeChild(iframe), 2000); + + // 桌面端不会有 visibilitychange,确保 fallback 一定执行 + setTimeout(() => { + document.removeEventListener("visibilitychange", onVisChange); + }, 3000); +} + +// ===== 全局搜索(顶部常驻搜索框) ===== + +function globalSearch() { + const keyword = document.getElementById("globalSearchInput").value.trim(); + if (!keyword) return; + + // 切换到推荐标签页 + switchTab("discover"); + // 切换到搜索子标签 + switchDiscoverTab("search"); + // 填入搜索词并执行搜索 + document.getElementById("searchInput").value = keyword; + doSearch(); +} + +// ===== 书架分类标签(可折叠层级结构) ===== + +let shelfCategoryTree = {}; // { "历史": {count:114, subs:{"世界史":5,...}}, ... } +let shelfActiveCategory = ""; // 完整分类名,如 "历史-中国古代" +let shelfActiveParent = ""; // 父分类名,如 "历史";空表示"全部" +let shelfExpandedParents = new Set(); // 当前展开的父分类 +let shelfSortMode = "category"; // 始终按分类排序,前端不显示切换按钮 + +async function loadShelfCategories() { + try { + const data = await fetchAPI("/api/shelf?page=1&per_page=1"); + shelfCategoryTree = data.category_tree || {}; + renderShelfCategories(); + } catch (e) { + console.error("加载分类失败:", e); + } +} + +function renderShelfCategories() { + const container = document.getElementById("shelfCategories"); + if (!container) return; + + let html = ""; + + // "全部" 按钮 + const allActive = !shelfActiveParent && !shelfActiveCategory; + html += ``; + + // 父分类按钮(按书籍数量降序) + const parents = Object.entries(shelfCategoryTree) + .sort((a, b) => b[1].count - a[1].count); + + for (const [parent, info] of parents) { + const isActive = shelfActiveParent === parent && !shelfActiveCategory; + const isExpanded = shelfExpandedParents.has(parent); + const arrow = Object.keys(info.subs).length > 0 + ? `${isExpanded ? "▼" : "▶"}` + : ""; + + html += ``; + + // 展开的子分类 + if (isExpanded && Object.keys(info.subs).length > 0) { + html += `
`; + for (const [sub, count] of Object.entries(info.subs).sort((a, b) => b[1] - a[1])) { + const fullCat = `${parent}-${sub}`; + const subActive = shelfActiveCategory === fullCat; + html += ``; + } + html += `
`; + } + } + + container.innerHTML = html; +} + +function filterByParent(parent) { + if (parent) { + // 点击父分类:展开/折叠子分类,同时筛选该父分类下的所有书 + if (shelfExpandedParents.has(parent) && shelfActiveParent === parent && !shelfActiveCategory) { + // 已展开且已选中 → 折叠 + shelfExpandedParents.delete(parent); + shelfActiveParent = ""; + shelfActiveCategory = ""; + } else { + // 展开 + 筛选 + shelfExpandedParents.add(parent); + shelfActiveParent = parent; + shelfActiveCategory = ""; + } + } else { + // 点击"全部" + shelfExpandedParents.clear(); + shelfActiveParent = ""; + shelfActiveCategory = ""; + } + renderShelfCategories(); + loadShelf(1); +} + +function filterByCategory(fullCategory) { + shelfActiveCategory = fullCategory; + // 确保父分类已展开 + const parent = fullCategory.split("-")[0]; + shelfExpandedParents.add(parent); + shelfActiveParent = parent; + renderShelfCategories(); + loadShelf(1); +} + +// ===== 书架刷新 ===== + +async function refreshShelf() { + const btn = document.getElementById("shelfRefreshBtn"); + const textEl = btn.querySelector(".btn-text"); + btn.disabled = true; + btn.classList.add("loading"); + textEl.textContent = "同步中..."; + + try { + // Step 1: 从微信读书 API 同步数据到 MySQL + const resp = await fetch(API_BASE + "/api/sync", { method: "POST" }); + const syncResult = await resp.json(); + if (!syncResult.ok) { + throw new Error(syncResult.error || "同步失败"); + } + + // Step 2: 重新加载分类和书架 + textEl.textContent = "加载中..."; + await loadShelfCategories(); + await loadShelf(1); + } catch (e) { + console.error("刷新失败:", e); + alert("刷新失败: " + (e.message || "未知错误")); + } finally { + btn.disabled = false; + btn.classList.remove("loading"); + textEl.textContent = "刷新"; + } +} + +// ===== 初始化 ===== + +document.addEventListener("DOMContentLoaded", async () => { + const isHealthy = await checkDBHealth(); + if (!isHealthy) return; + + loadOverview(); + loadTopBooks(); + loadShelfCategories(); + 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 keyword = document.getElementById("shelfSearch").value.trim(); + const finished = document.getElementById("finishedOnly").checked ? "1" : "0"; + const params = new URLSearchParams({ page: page, per_page: 60, finished: finished }); + if (keyword) params.set("keyword", keyword); + if (shelfActiveCategory) { + params.set("category", shelfActiveCategory); + } else if (shelfActiveParent) { + params.set("parent_category", shelfActiveParent); + } + if (shelfSortMode === "category") params.set("sort", "category"); + + 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) => renderBookCard(book)).join(""); + + renderPagination(page, data.total_pages); + } catch (e) { + console.error("加载书架失败:", e); + } +} + +function renderBookCard(book) { + const badges = []; + if (book.is_top) badges.push(`置顶`); + if (book.finish_reading) badges.push(`已读完`); + return ` +
+
+ ${book.title} + ${badges.join("")} +
+
${book.title}
+
${book.author}
+
`; +} + +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; + currentThoughtsCategory = "all"; + currentThoughtsSort = "time_desc"; + currentThoughtsSearch = ""; + 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"); + currentThoughtsCategory = category; + renderThoughtsList(category); +} + +function onThoughtsSearch(val) { + currentThoughtsSearch = val.trim().toLowerCase(); + renderThoughtsList(currentThoughtsCategory); +} + +function onThoughtsSort(val) { + currentThoughtsSort = val; + renderThoughtsList(currentThoughtsCategory); +} + +/** 解析日期字符串为时间戳,无效则返回 0 */ +function parseDate(str) { + if (!str) return 0; + const d = new Date(str.replace(/-/g, "/")); + return isNaN(d.getTime()) ? 0 : d.getTime(); +} + +/** 对数组按排序模式排序,返回新数组 */ +function sortByMode(arr) { + const sorted = [...arr]; + if (currentThoughtsSort === "time_desc") { + sorted.sort((a, b) => parseDate(b.create_time) - parseDate(a.create_time)); + } else if (currentThoughtsSort === "time_asc") { + sorted.sort((a, b) => parseDate(a.create_time) - parseDate(b.create_time)); + } else if (currentThoughtsSort === "chapter") { + sorted.sort((a, b) => { + const ua = parseInt(a.chapter_uid) || 0; + const ub = parseInt(b.chapter_uid) || 0; + if (ua !== ub) return ua - ub; + // 同章节内按 range 起始位置排序 + const ra = a.range ? parseInt(a.range.split("-")[0]) : 0; + const rb = b.range ? parseInt(b.range.split("-")[0]) : 0; + return ra - rb; + }); + } + return sorted; +} + +/** 过滤搜索关键词 */ +function matchSearch(text) { + if (!currentThoughtsSearch) return true; + return (text || "").toLowerCase().includes(currentThoughtsSearch); +} + +function renderThoughtsList(category) { + const data = currentThoughtsData; + if (!data) return; + + // 原始分类 + let highlights = data.highlights || []; + let bookReviews = (data.reviews || []).filter((r) => r.is_finish); + let thoughts = (data.reviews || []).filter((r) => !r.is_finish); + + // 搜索过滤 + highlights = highlights.filter((h) => matchSearch(h.mark_text)); + bookReviews = bookReviews.filter((r) => matchSearch(r.content) || matchSearch(r.abstract)); + thoughts = thoughts.filter((r) => matchSearch(r.content) || matchSearch(r.abstract)); + + // 排序 + highlights = sortByMode(highlights); + bookReviews = sortByMode(bookReviews); + thoughts = sortByMode(thoughts); + + const list = document.getElementById("thoughtsList"); + let html = ""; + + // 划线区 + if (category === "all" || category === "highlight") { + if (highlights.length > 0) { + html += `
📌 划线 ${highlights.length}
`; + if (currentThoughtsSort === "chapter") { + // 按章节分组展示 + const grouped = {}; + highlights.forEach((h) => { + const ch = h.chapter_title || "未知章节"; + if (!grouped[ch]) grouped[ch] = []; + grouped[ch].push(h); + }); + for (const [chTitle, items] of Object.entries(grouped)) { + html += `
`; + html += `
${chTitle}
`; + items.forEach((h) => { + html += ` +
+ ${escapeHtml(h.mark_text)} +
+ ${h.chapter_title ? `${h.chapter_title}` : ""} + ${h.create_time ? `${h.create_time}` : ""} +
+
+ `; + }); + html += `
`; + } + } else { + // 按时间排序,平铺展示(标注章节) + highlights.forEach((h) => { + html += ` +
+ ${escapeHtml(h.mark_text)} +
+ ${h.chapter_title ? `${h.chapter_title}` : ""} + ${h.create_time ? `${h.create_time}` : ""} +
+
+ `; + }); + } + } + } + + // 书评区 + 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) { + if (currentThoughtsSearch) { + html = `
未找到包含"${escapeHtml(currentThoughtsSearch)}"的内容
`; + } else { + 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(""); +} diff --git a/legacy/v1/templates/index.html b/legacy/v1/templates/index.html new file mode 100644 index 0000000..803e317 --- /dev/null +++ b/legacy/v1/templates/index.html @@ -0,0 +1,211 @@ + + + + + + 我的读书数据 + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+

阅读,是一场漫长的旅行

+

正在加载数据...

+
+
+
+
+
+
+
+
+
+ + +
+
+

📊 阅读数据可视化

+
+
+

年度阅读时长趋势

+
+
+
+

24小时阅读时段分布

+
+
+
+

偏好阅读分类

+
+
+
+

偏好作者 TOP 10

+
+
+
+
+
+ + +
+
+

🏆 阅读时长排行

+
+
+
+ + +
+
+

📚 我的书架

+
+
+ + + +
+
+
+
+
+ +
+
+ + +
+
+

✍️ 笔记概览

+
+
+
+
+ + +
+
+

🔍 推荐发现

+
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + +