feat: 补全收藏与分类功能
- 后台编辑区新增★收藏切换按钮(即时生效) - 后台侧栏新增快速筛选: 全部/★收藏/分类 - 前台快速筛选新增分类chips(按分类过滤) - 两种模式均含暗色样式适配
This commit is contained in:
+168
-1
@@ -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');
|
||||
@@ -916,12 +967,81 @@
|
||||
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">暂无内容,点击 + 新建</div>';
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user