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);
|
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 {
|
.thoughts-filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 24px 0 20px;
|
|
||||||
flex-wrap: wrap;
|
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 {
|
.thought-filter-btn {
|
||||||
padding: 6px 18px;
|
padding: 6px 18px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -1830,6 +1886,20 @@ body {
|
|||||||
.global-search-wrapper {
|
.global-search-wrapper {
|
||||||
max-width: 100%;
|
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 {
|
.shelf-search {
|
||||||
width: 140px;
|
width: 140px;
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-20
@@ -70,6 +70,9 @@ function escapeHtml(text) {
|
|||||||
const chartInstances = {};
|
const chartInstances = {};
|
||||||
let chartsLoaded = false;
|
let chartsLoaded = false;
|
||||||
let currentThoughtsData = null;
|
let currentThoughtsData = null;
|
||||||
|
let currentThoughtsCategory = "all";
|
||||||
|
let currentThoughtsSort = "time_desc";
|
||||||
|
let currentThoughtsSearch = "";
|
||||||
|
|
||||||
function refreshChartsForTheme() {
|
function refreshChartsForTheme() {
|
||||||
if (!chartsLoaded) return;
|
if (!chartsLoaded) return;
|
||||||
@@ -840,22 +843,37 @@ function renderThoughtsPage(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<div class="thoughts-filters">
|
<div class="thoughts-toolbar">
|
||||||
<button class="thought-filter-btn active" onclick="filterThoughts('all')">全部</button>
|
<div class="thoughts-filters">
|
||||||
<button class="thought-filter-btn" onclick="filterThoughts('highlight')">
|
<button class="thought-filter-btn active" onclick="filterThoughts('all')">全部</button>
|
||||||
📌 划线 <span class="filter-count">${highlights.length}</span>
|
<button class="thought-filter-btn" onclick="filterThoughts('highlight')">
|
||||||
</button>
|
📌 划线 <span class="filter-count">${highlights.length}</span>
|
||||||
<button class="thought-filter-btn" onclick="filterThoughts('review')">
|
</button>
|
||||||
⭐ 书评 <span class="filter-count">${bookReviews.length}</span>
|
<button class="thought-filter-btn" onclick="filterThoughts('review')">
|
||||||
</button>
|
⭐ 书评 <span class="filter-count">${bookReviews.length}</span>
|
||||||
<button class="thought-filter-btn" onclick="filterThoughts('thought')">
|
</button>
|
||||||
💭 想法 <span class="filter-count">${thoughts.length}</span>
|
<button class="thought-filter-btn" onclick="filterThoughts('thought')">
|
||||||
</button>
|
💭 想法 <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>
|
||||||
<div id="thoughtsList"></div>
|
<div id="thoughtsList"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
content.innerHTML = html;
|
content.innerHTML = html;
|
||||||
|
currentThoughtsCategory = "all";
|
||||||
|
currentThoughtsSort = "time_desc";
|
||||||
|
currentThoughtsSearch = "";
|
||||||
renderThoughtsList("all");
|
renderThoughtsList("all");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -864,16 +882,72 @@ function filterThoughts(category) {
|
|||||||
btn.classList.remove("active");
|
btn.classList.remove("active");
|
||||||
});
|
});
|
||||||
event.target.closest(".thought-filter-btn").classList.add("active");
|
event.target.closest(".thought-filter-btn").classList.add("active");
|
||||||
|
currentThoughtsCategory = category;
|
||||||
renderThoughtsList(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) {
|
function renderThoughtsList(category) {
|
||||||
const data = currentThoughtsData;
|
const data = currentThoughtsData;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const highlights = data.highlights || [];
|
// 原始分类
|
||||||
const bookReviews = (data.reviews || []).filter((r) => r.is_finish);
|
let highlights = data.highlights || [];
|
||||||
const thoughts = (data.reviews || []).filter((r) => !r.is_finish);
|
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");
|
const list = document.getElementById("thoughtsList");
|
||||||
let html = "";
|
let html = "";
|
||||||
@@ -882,13 +956,33 @@ function renderThoughtsList(category) {
|
|||||||
if (category === "all" || category === "highlight") {
|
if (category === "all" || category === "highlight") {
|
||||||
if (highlights.length > 0) {
|
if (highlights.length > 0) {
|
||||||
html += `<div class="thoughts-section-title">📌 划线 <span class="section-badge badge-highlight">${highlights.length}</span></div>`;
|
html += `<div class="thoughts-section-title">📌 划线 <span class="section-badge badge-highlight">${highlights.length}</span></div>`;
|
||||||
const grouped = data.grouped_highlights || {};
|
if (currentThoughtsSort === "chapter") {
|
||||||
for (const [chTitle, items] of Object.entries(grouped)) {
|
// 按章节分组展示
|
||||||
if (category === "highlight") {
|
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-group">`;
|
||||||
html += `<div class="thoughts-chapter-title">${chTitle}</div>`;
|
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 += `
|
html += `
|
||||||
<div class="thought-highlight-item">
|
<div class="thought-highlight-item">
|
||||||
${escapeHtml(h.mark_text)}
|
${escapeHtml(h.mark_text)}
|
||||||
@@ -899,7 +993,6 @@ function renderThoughtsList(category) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
if (category === "highlight") html += `</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -946,7 +1039,11 @@ function renderThoughtsList(category) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!html) {
|
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;
|
list.innerHTML = html;
|
||||||
|
|||||||
Reference in new issue
Block a user