/* ===== 微信读书数据展示 - 前端逻辑 ===== */
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");
btn.disabled = true;
btn.classList.add("loading");
btn.textContent = "↻ 刷新中...";
try {
await loadShelfCategories();
await loadShelf(1);
} finally {
btn.disabled = false;
btn.classList.remove("loading");
btn.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)}
累计阅读 (小时)
`;
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.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 `
})
${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 = `
`;
const grid = document.getElementById("notebooksGrid");
grid.innerHTML = data
.map((nb) => {
const progress = nb.reading_progress || 0;
return `
${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.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}
${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.author || ''}
${ratingHtml}${ratingLabel}${readingHtml}
${categoryHtml}
${reasonHtml}
`;
})
.join("");
}