mirror of
https://github.com/KingHWJ/WeReadBoard.git
synced 2026-11-03 18:24:58 +08:00
feat: add search and sort to thoughts detail page
- Search box: real-time filter across highlights, book reviews, and thoughts content (mark_text, content, abstract fields) - Sort dropdown: 3 modes - newest first, oldest first, by chapter order (chapter_uid + range position) - Chapter sort mode groups highlights by chapter with titles - Time sort mode flattens highlights with chapter labels - Empty state shows search keyword when no results match - Mobile responsive: toolbar stacks vertically on narrow screens
This commit is contained in:
1 parent
d276608538
commit
90b9614b5e
2 files changed
+189
-22
No files matched your search
+72
-2
@@ -1543,14 +1543,70 @@ body {
|
||||
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;
|
||||
margin: 24px 0 20px;
|
||||
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);
|
||||
@@ -1830,6 +1886,20 @@ body {
|
||||
.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;
|
||||
}
|
||||
|
||||
+117
-20
@@ -70,6 +70,9 @@ function escapeHtml(text) {
|
||||
const chartInstances = {};
|
||||
let chartsLoaded = false;
|
||||
let currentThoughtsData = null;
|
||||
let currentThoughtsCategory = "all";
|
||||
let currentThoughtsSort = "time_desc";
|
||||
let currentThoughtsSearch = "";
|
||||
|
||||
function refreshChartsForTheme() {
|
||||
if (!chartsLoaded) return;
|
||||
@@ -840,22 +843,37 @@ function renderThoughtsPage(data) {
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="thoughts-filters">
|
||||
<button class="thought-filter-btn active" onclick="filterThoughts('all')">全部</button>
|
||||
<button class="thought-filter-btn" onclick="filterThoughts('highlight')">
|
||||
📌 划线 <span class="filter-count">${highlights.length}</span>
|
||||
</button>
|
||||
<button class="thought-filter-btn" onclick="filterThoughts('review')">
|
||||
⭐ 书评 <span class="filter-count">${bookReviews.length}</span>
|
||||
</button>
|
||||
<button class="thought-filter-btn" onclick="filterThoughts('thought')">
|
||||
💭 想法 <span class="filter-count">${thoughts.length}</span>
|
||||
</button>
|
||||
<div class="thoughts-toolbar">
|
||||
<div class="thoughts-filters">
|
||||
<button class="thought-filter-btn active" onclick="filterThoughts('all')">全部</button>
|
||||
<button class="thought-filter-btn" onclick="filterThoughts('highlight')">
|
||||
📌 划线 <span class="filter-count">${highlights.length}</span>
|
||||
</button>
|
||||
<button class="thought-filter-btn" onclick="filterThoughts('review')">
|
||||
⭐ 书评 <span class="filter-count">${bookReviews.length}</span>
|
||||
</button>
|
||||
<button class="thought-filter-btn" onclick="filterThoughts('thought')">
|
||||
💭 想法 <span class="filter-count">${thoughts.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="thoughts-controls">
|
||||
<input type="text" id="thoughtsSearchInput" class="thoughts-search-input"
|
||||
placeholder="搜索划线、书评、想法内容..."
|
||||
oninput="onThoughtsSearch(this.value)">
|
||||
<select id="thoughtsSortSelect" class="thoughts-sort-select" onchange="onThoughtsSort(this.value)">
|
||||
<option value="time_desc">最新优先</option>
|
||||
<option value="time_asc">最早优先</option>
|
||||
<option value="chapter">按章节顺序</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="thoughtsList"></div>
|
||||
`;
|
||||
|
||||
content.innerHTML = html;
|
||||
currentThoughtsCategory = "all";
|
||||
currentThoughtsSort = "time_desc";
|
||||
currentThoughtsSearch = "";
|
||||
renderThoughtsList("all");
|
||||
}
|
||||
|
||||
@@ -864,16 +882,72 @@ function filterThoughts(category) {
|
||||
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;
|
||||
|
||||
const highlights = data.highlights || [];
|
||||
const bookReviews = (data.reviews || []).filter((r) => r.is_finish);
|
||||
const thoughts = (data.reviews || []).filter((r) => !r.is_finish);
|
||||
// 原始分类
|
||||
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 = "";
|
||||
@@ -882,13 +956,33 @@ function renderThoughtsList(category) {
|
||||
if (category === "all" || category === "highlight") {
|
||||
if (highlights.length > 0) {
|
||||
html += `<div class="thoughts-section-title">📌 划线 <span class="section-badge badge-highlight">${highlights.length}</span></div>`;
|
||||
const grouped = data.grouped_highlights || {};
|
||||
for (const [chTitle, items] of Object.entries(grouped)) {
|
||||
if (category === "highlight") {
|
||||
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 += `<div class="thoughts-chapter-group">`;
|
||||
html += `<div class="thoughts-chapter-title">${chTitle}</div>`;
|
||||
items.forEach((h) => {
|
||||
html += `
|
||||
<div class="thought-highlight-item">
|
||||
${escapeHtml(h.mark_text)}
|
||||
<div class="thought-highlight-meta">
|
||||
${h.chapter_title ? `<span>${h.chapter_title}</span>` : ""}
|
||||
${h.create_time ? `<span>${h.create_time}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
items.forEach((h) => {
|
||||
} else {
|
||||
// 按时间排序,平铺展示(标注章节)
|
||||
highlights.forEach((h) => {
|
||||
html += `
|
||||
<div class="thought-highlight-item">
|
||||
${escapeHtml(h.mark_text)}
|
||||
@@ -899,7 +993,6 @@ function renderThoughtsList(category) {
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (category === "highlight") html += `</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -946,7 +1039,11 @@ function renderThoughtsList(category) {
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = `<div class="thoughts-empty">该分类暂无内容</div>`;
|
||||
if (currentThoughtsSearch) {
|
||||
html = `<div class="thoughts-empty">未找到包含"${escapeHtml(currentThoughtsSearch)}"的内容</div>`;
|
||||
} else {
|
||||
html = `<div class="thoughts-empty">该分类暂无内容</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
list.innerHTML = html;
|
||||
|
||||
Reference in new issue
Block a user