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:
admin committed 2026-07-17 12:16:19 +08:00
1 parent d276608538
commit 90b9614b5e
2 files changed
+176 -9

No files matched your search

+72 -2
View File
@@ -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;
}
+104 -7
View File
@@ -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,6 +843,7 @@ function renderThoughtsPage(data) {
}
let html = `
<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')">
@@ -852,10 +856,24 @@ function renderThoughtsPage(data) {
💭 想法 <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,12 +956,17 @@ 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 || {};
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)) {
if (category === "highlight") {
html += `<div class="thoughts-chapter-group">`;
html += `<div class="thoughts-chapter-title">${chTitle}</div>`;
}
items.forEach((h) => {
html += `
<div class="thought-highlight-item">
@@ -899,7 +978,21 @@ function renderThoughtsList(category) {
</div>
`;
});
if (category === "highlight") html += `</div>`;
html += `</div>`;
}
} else {
// 按时间排序,平铺展示(标注章节)
highlights.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>
`;
});
}
}
}
@@ -946,8 +1039,12 @@ function renderThoughtsList(category) {
}
if (!html) {
if (currentThoughtsSearch) {
html = `<div class="thoughts-empty">未找到包含"${escapeHtml(currentThoughtsSearch)}"的内容</div>`;
} else {
html = `<div class="thoughts-empty">该分类暂无内容</div>`;
}
}
list.innerHTML = html;
}