feat: 补全收藏与分类功能
- 后台编辑区新增★收藏切换按钮(即时生效) - 后台侧栏新增快速筛选: 全部/★收藏/分类 - 前台快速筛选新增分类chips(按分类过滤) - 两种模式均含暗色样式适配
This commit is contained in:
@@ -518,6 +518,41 @@
|
||||
.backlink-item:hover { background: #f1f3f4; text-decoration: underline; }
|
||||
.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 链接 */
|
||||
.preview-content .wiki-link {
|
||||
color: #1a73e8;
|
||||
@@ -583,6 +618,12 @@
|
||||
body.dark .backlink-item { color: #7aa2f7; }
|
||||
body.dark .backlink-item:hover { background: #1e232d; }
|
||||
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:hover { background: #1b2a44; }
|
||||
body.dark .graph-node circle { fill: #6aa1f7; stroke: #0f1115; }
|
||||
@@ -627,6 +668,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-filter" id="sideFilter" style="display:none;"></div>
|
||||
<div class="tree-container" id="treeContainer">
|
||||
<div class="loading">加载中...</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>
|
||||
公开
|
||||
</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;">
|
||||
<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;">
|
||||
@@ -822,6 +870,8 @@
|
||||
let searchTimer = null; // 搜索防抖定时器
|
||||
let searchMode = false; // 是否处于搜索模式(隐藏树形)
|
||||
let autosaveInlineOpen = false; // 草稿已加载标志
|
||||
let filterMode = { type: 'all', value: '' }; // 侧栏筛选:all / fav / category:xxx
|
||||
let categoryList = []; // 分类列表(用于筛选 chips)
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
@@ -903,6 +953,7 @@
|
||||
if (data.code === 0) {
|
||||
treeData = data.data;
|
||||
renderTree();
|
||||
renderSideFilter();
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('加载目录失败', 'error');
|
||||
@@ -917,11 +968,80 @@
|
||||
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);
|
||||
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) {
|
||||
return items.map(item => {
|
||||
const children = treeData.filter(c => c.parent_id === item.id);
|
||||
@@ -1022,6 +1142,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
updateFavUI();
|
||||
renderTree();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1116,9 +1237,54 @@
|
||||
document.getElementById('noteTags').value = '';
|
||||
document.getElementById('noteContent').value = '';
|
||||
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() {
|
||||
const title = document.getElementById('noteTitle').value.trim();
|
||||
const content = document.getElementById('noteContent').value;
|
||||
@@ -1144,6 +1310,7 @@
|
||||
category,
|
||||
tags,
|
||||
is_public: isPublic,
|
||||
is_favorite: document.getElementById('favBtn').dataset.fav === '1',
|
||||
parent_id: currentItem && currentItem.parent_id ? currentItem.parent_id : 0
|
||||
};
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@
|
||||
body.dark .filter-item:hover { background: #1e232d; }
|
||||
body.dark .filter-item.active { background: #1b2a44; color: #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-item:hover { background: #1e232d; }
|
||||
@@ -286,6 +289,21 @@
|
||||
.filter-item.active { background: #e3f2fd; color: #1976d2; }
|
||||
.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 {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 8px;
|
||||
@@ -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>
|
||||
收藏笔记
|
||||
</div>
|
||||
<div class="cat-filters" id="catFilters" style="padding: 4px 16px 6px; display: flex; flex-wrap: wrap; gap: 6px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -677,6 +696,52 @@
|
||||
|
||||
async function init() {
|
||||
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() {
|
||||
@@ -895,12 +960,14 @@
|
||||
async function filterAll() {
|
||||
currentFilter = { type: 'all' };
|
||||
updateFilterUI();
|
||||
renderCatFilters();
|
||||
await loadTree();
|
||||
}
|
||||
|
||||
async function filterFavorites() {
|
||||
currentFilter = { type: 'favorites' };
|
||||
updateFilterUI();
|
||||
renderCatFilters();
|
||||
try {
|
||||
const res = await fetch(`${API}/notes?favorite=true&page_size=100`);
|
||||
const data = await res.json();
|
||||
|
||||
Reference in New Issue
Block a user