feat: 补全收藏与分类功能

- 后台编辑区新增★收藏切换按钮(即时生效)
- 后台侧栏新增快速筛选: 全部/★收藏/分类
- 前台快速筛选新增分类chips(按分类过滤)
- 两种模式均含暗色样式适配
This commit is contained in:
Your Name
2026-08-11 12:43:10 +08:00
parent b93c6f1e16
commit 2196189791
2 changed files with 235 additions and 1 deletions
+168 -1
View File
@@ -518,6 +518,41 @@
.backlink-item:hover { background: #f1f3f4; text-decoration: underline; } .backlink-item:hover { background: #f1f3f4; text-decoration: underline; }
.backlink-empty { padding: 10px; font-size: 12px; color: #999; } .backlink-empty { padding: 10px; font-size: 12px; color: #999; }
/* 侧栏筛选(收藏/分类) */
.side-filter {
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
max-height: 45%;
overflow-y: auto;
background: #fff;
}
.side-filter-header {
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
color: #666;
}
.filter-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 4px 12px 10px;
}
.filter-chip {
padding: 4px 10px;
border: 1px solid #e0e0e0;
border-radius: 14px;
background: #fff;
color: #555;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
}
.filter-chip:hover { background: #f1f3f4; }
.filter-chip.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
.filter-chip.star { color: #e5a00d; }
.filter-chip.star.active { background: #e5a00d; color: #fff; border-color: #e5a00d; }
/* 预览中的 wiki 链接 */ /* 预览中的 wiki 链接 */
.preview-content .wiki-link { .preview-content .wiki-link {
color: #1a73e8; color: #1a73e8;
@@ -583,6 +618,12 @@
body.dark .backlink-item { color: #7aa2f7; } body.dark .backlink-item { color: #7aa2f7; }
body.dark .backlink-item:hover { background: #1e232d; } body.dark .backlink-item:hover { background: #1e232d; }
body.dark .backlink-empty { color: #9aa0aa; } body.dark .backlink-empty { color: #9aa0aa; }
body.dark .side-filter { background: #15181e; border-bottom-color: #232733; }
body.dark .side-filter-header { color: #9aa0aa; }
body.dark .filter-chip { background: #1a1e26; border-color: #2a3040; color: #c9ced8; }
body.dark .filter-chip:hover { background: #2a3040; }
body.dark .filter-chip.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
body.dark .filter-chip.star.active { background: #e5a00d; color: #fff; border-color: #e5a00d; }
body.dark .preview-content .wiki-link { color: #7aa2f7; border-bottom-color: #7aa2f7; } body.dark .preview-content .wiki-link { color: #7aa2f7; border-bottom-color: #7aa2f7; }
body.dark .preview-content .wiki-link:hover { background: #1b2a44; } body.dark .preview-content .wiki-link:hover { background: #1b2a44; }
body.dark .graph-node circle { fill: #6aa1f7; stroke: #0f1115; } body.dark .graph-node circle { fill: #6aa1f7; stroke: #0f1115; }
@@ -627,6 +668,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="side-filter" id="sideFilter" style="display:none;"></div>
<div class="tree-container" id="treeContainer"> <div class="tree-container" id="treeContainer">
<div class="loading">加载中...</div> <div class="loading">加载中...</div>
</div> </div>
@@ -668,6 +710,12 @@
<svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: #666;"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg> <svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: #666;"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
公开 公开
</label> </label>
<!-- 收藏切换按钮 -->
<button type="button" id="favBtn" class="btn btn-icon" onclick="toggleFavorite()" title="收藏/取消收藏"
style="display:none; align-items:center; gap:6px; font-size:13px; color:#666; white-space:nowrap; padding:4px 10px;">
<svg id="favStar" viewBox="0 0 24 24" style="width:16px; height:16px; fill:#ccc;"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
<span id="favText">收藏</span>
</button>
<div style="display: flex; align-items: center; gap: 6px; margin-left: 12px;"> <div style="display: flex; align-items: center; gap: 6px; margin-left: 12px;">
<svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: #666;"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg> <svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: #666;"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
<input type="password" id="notePassword" placeholder="访问密码(可选)" style="width: 120px; padding: 6px 10px; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 12px;"> <input type="password" id="notePassword" placeholder="访问密码(可选)" style="width: 120px; padding: 6px 10px; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 12px;">
@@ -822,6 +870,8 @@
let searchTimer = null; // 搜索防抖定时器 let searchTimer = null; // 搜索防抖定时器
let searchMode = false; // 是否处于搜索模式(隐藏树形) let searchMode = false; // 是否处于搜索模式(隐藏树形)
let autosaveInlineOpen = false; // 草稿已加载标志 let autosaveInlineOpen = false; // 草稿已加载标志
let filterMode = { type: 'all', value: '' }; // 侧栏筛选:all / fav / category:xxx
let categoryList = []; // 分类列表(用于筛选 chips)
// 初始化 // 初始化
async function init() { async function init() {
@@ -903,6 +953,7 @@
if (data.code === 0) { if (data.code === 0) {
treeData = data.data; treeData = data.data;
renderTree(); renderTree();
renderSideFilter();
} }
} catch (err) { } catch (err) {
showToast('加载目录失败', 'error'); showToast('加载目录失败', 'error');
@@ -916,12 +967,81 @@
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">暂无内容,点击 + 新建</div>'; container.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">暂无内容,点击 + 新建</div>';
return; return;
} }
// 若处于筛选模式(收藏/分类),渲染扁平结果列表
if (filterMode.type !== 'all') {
const list = getFilteredList();
if (list.length === 0) {
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">' +
(filterMode.type === 'fav' ? '暂无收藏笔记' : '该分类暂无笔记') + '</div>';
} else {
container.innerHTML = list.map(item => {
const isActive = currentItem && currentItem.id === item.id;
return `
<div class="tree-item ${isActive ? 'active' : ''}" onclick="selectItem(${item.id})">
<span class="icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="#666"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg></span>
<span class="title">${escapeHtml(item.title)}</span>
${item.is_favorite ? '<span style="color:#e5a00d;font-size:11px;">★</span>' : ''}
</div>
`;
}).join('');
}
return;
}
// 构建树形结构 // 构建树形结构
const rootItems = treeData.filter(item => item.parent_id === 0); const rootItems = treeData.filter(item => item.parent_id === 0);
container.innerHTML = renderTreeItems(rootItems); container.innerHTML = renderTreeItems(rootItems);
} }
// 根据当前筛选模式返回扁平笔记列表
function getFilteredList() {
if (filterMode.type === 'fav') {
return treeData.filter(i => !i.is_folder && i.is_favorite);
}
if (filterMode.type === 'category' && filterMode.value) {
return treeData.filter(i => !i.is_folder && i.category === filterMode.value);
}
return treeData.filter(i => !i.is_folder);
}
// 渲染侧栏筛选条(收藏 + 分类)
function renderSideFilter() {
const box = document.getElementById('sideFilter');
if (!box) return;
// 汇总分类
const catSet = new Set();
treeData.forEach(i => { if (!i.is_folder && i.category) catSet.add(i.category); });
categoryList = Array.from(catSet);
let html = '<div class="side-filter-header">快速筛选</div><div class="filter-chip-row">';
html += `<span class="filter-chip ${filterMode.type === 'all' ? 'active' : ''}" onclick="setFilter('all','')">全部</span>`;
html += `<span class="filter-chip star ${filterMode.type === 'fav' ? 'active' : ''}" onclick="setFilter('fav','')">★ 收藏</span>`;
categoryList.forEach(cat => {
const hot = filterMode.type === 'category' && filterMode.value === cat;
html += `<span class="filter-chip ${hot ? 'active' : ''}" onclick="setFilter('category','${escapeHtml(cat)}')">${escapeHtml(cat)}</span>`;
});
// 清理没有笔记的分类(含已删除引用)
if (categoryList.length === 0) {
html += '<span style="font-size:12px;color:#999;">暂无分类</span>';
}
html += '</div>';
box.innerHTML = html;
box.style.display = 'block';
}
// 设置筛选模式并重绘
function setFilter(type, value) {
filterMode = { type, value };
// 筛选时退出目录模式(避免编辑目录)
if (type !== 'all') {
searchMode = false;
}
renderSideFilter();
renderTree();
}
function renderTreeItems(items) { function renderTreeItems(items) {
return items.map(item => { return items.map(item => {
const children = treeData.filter(c => c.parent_id === item.id); const children = treeData.filter(c => c.parent_id === item.id);
@@ -1022,6 +1142,7 @@
} }
} }
updateFavUI();
renderTree(); renderTree();
} }
} catch (err) { } catch (err) {
@@ -1116,9 +1237,54 @@
document.getElementById('noteTags').value = ''; document.getElementById('noteTags').value = '';
document.getElementById('noteContent').value = ''; document.getElementById('noteContent').value = '';
updatePreview(); updatePreview();
updateFavUI();
} }
// 保存笔记 // 保存笔记
// 收藏切换按钮(点击直接调接口即时生效)
async function toggleFavorite() {
const btn = document.getElementById('favBtn');
if (!currentItem || !currentItem.id || currentItem.is_folder) {
showToast('请先打开一篇笔记', 'error');
return;
}
const newVal = btn.dataset.fav !== '1';
try {
const res = await fetch(`${ADMIN_API}/notes/${currentItem.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_favorite: newVal })
});
const data = await res.json();
if (data.code === 0) {
currentItem.is_favorite = newVal;
btn.dataset.fav = newVal ? '1' : '0';
document.getElementById('favText').textContent = newVal ? '已收藏' : '收藏';
document.getElementById('favStar').style.fill = newVal ? '#e5a00d' : '#ccc';
showToast(newVal ? '已收藏' : '已取消收藏', 'success');
// 若当前正处于收藏筛选,刷新列表
if (filterMode.type === 'fav' || filterMode.type === 'all') {
await loadTree();
}
} else {
showToast(data.message || '操作失败', 'error');
}
} catch (err) {
showToast('操作失败', 'error');
}
}
// 根据 currentItem 刷新收藏按钮状态
function updateFavUI() {
const btn = document.getElementById('favBtn');
if (!btn) return;
const fav = currentItem && currentItem.id && currentItem.is_favorite;
btn.dataset.fav = fav ? '1' : '0';
document.getElementById('favText').textContent = fav ? '已收藏' : '收藏';
document.getElementById('favStar').style.fill = fav ? '#e5a00d' : '#ccc';
btn.style.display = (!currentItem || currentItem.is_folder) ? 'none' : 'flex';
}
async function saveNote() { async function saveNote() {
const title = document.getElementById('noteTitle').value.trim(); const title = document.getElementById('noteTitle').value.trim();
const content = document.getElementById('noteContent').value; const content = document.getElementById('noteContent').value;
@@ -1144,6 +1310,7 @@
category, category,
tags, tags,
is_public: isPublic, is_public: isPublic,
is_favorite: document.getElementById('favBtn').dataset.fav === '1',
parent_id: currentItem && currentItem.parent_id ? currentItem.parent_id : 0 parent_id: currentItem && currentItem.parent_id ? currentItem.parent_id : 0
}; };
+67
View File
@@ -72,6 +72,9 @@
body.dark .filter-item:hover { background: #1e232d; } body.dark .filter-item:hover { background: #1e232d; }
body.dark .filter-item.active { background: #1b2a44; color: #7aa2f7; } body.dark .filter-item.active { background: #1b2a44; color: #7aa2f7; }
body.dark .filter-item.active svg { fill: #7aa2f7; } body.dark .filter-item.active svg { fill: #7aa2f7; }
body.dark .filter-chip { background: #1a1e26; border-color: #2a3040; color: #c9ced8; }
body.dark .filter-chip:hover { background: #2a3040; }
body.dark .filter-chip.active { background: #1976d2; color: #fff; border-color: #1976d2; }
body.dark .note-list { border-top: 1px solid #232733; } body.dark .note-list { border-top: 1px solid #232733; }
body.dark .note-item:hover { background: #1e232d; } body.dark .note-item:hover { background: #1e232d; }
@@ -285,6 +288,21 @@
.filter-item:hover { background: #f1f3f4; } .filter-item:hover { background: #f1f3f4; }
.filter-item.active { background: #e3f2fd; color: #1976d2; } .filter-item.active { background: #e3f2fd; color: #1976d2; }
.filter-item svg { width: 16px; height: 16px; } .filter-item svg { width: 16px; height: 16px; }
/* 分类筛选 chips */
.filter-chip {
padding: 4px 11px;
border: 1px solid #e0e0e0;
border-radius: 14px;
background: #fff;
color: #555;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.filter-chip:hover { background: #f1f3f4; }
.filter-chip.active { background: #1976d2; color: #fff; border-color: #1976d2; }
.note-list { .note-list {
border-top: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0;
@@ -631,6 +649,7 @@
<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> <svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
收藏笔记 收藏笔记
</div> </div>
<div class="cat-filters" id="catFilters" style="padding: 4px 16px 6px; display: flex; flex-wrap: wrap; gap: 6px;"></div>
</div> </div>
</div> </div>
@@ -677,6 +696,52 @@
async function init() { async function init() {
await loadTree(); await loadTree();
await loadCategories();
}
// 加载分类并在筛选区渲染 chips
async function loadCategories() {
try {
const res = await fetch(`${API}/categories`);
const data = await res.json();
const cats = data.data || [];
const box = document.getElementById('catFilters');
if (!box) return;
if (cats.length === 0) {
box.innerHTML = '';
return;
}
box.innerHTML = cats.map(cat =>
`<span class="filter-chip ${currentFilter.type === 'category' && currentFilter.value === cat ? 'active' : ''}" onclick="filterCategory('${escapeHtml(cat)}')">${escapeHtml(cat)}</span>`
).join('');
} catch (e) { /* 忽略 */ }
}
// 按分类筛选
async function filterCategory(cat) {
currentFilter = { type: 'category', value: cat };
updateFilterUI();
renderCatFilters();
try {
const res = await fetch(`${API}/notes?category=${encodeURIComponent(cat)}&page_size=100`);
const data = await res.json();
if (data.code === 0) {
flatNotes = data.data || [];
renderFlatNotes();
}
} catch (err) {
showToast('加载失败', 'error');
}
}
function renderCatFilters() {
const box = document.getElementById('catFilters');
if (!box) return;
const chips = box.querySelectorAll('.filter-chip');
chips.forEach(chip => {
chip.classList.toggle('active',
currentFilter.type === 'category' && chip.textContent === currentFilter.value);
});
} }
async function loadTree() { async function loadTree() {
@@ -895,12 +960,14 @@
async function filterAll() { async function filterAll() {
currentFilter = { type: 'all' }; currentFilter = { type: 'all' };
updateFilterUI(); updateFilterUI();
renderCatFilters();
await loadTree(); await loadTree();
} }
async function filterFavorites() { async function filterFavorites() {
currentFilter = { type: 'favorites' }; currentFilter = { type: 'favorites' };
updateFilterUI(); updateFilterUI();
renderCatFilters();
try { try {
const res = await fetch(`${API}/notes?favorite=true&page_size=100`); const res = await fetch(`${API}/notes?favorite=true&page_size=100`);
const data = await res.json(); const data = await res.json();