fix: 修复收藏夹列表加载失败 + 收藏按钮改为弹窗选择收藏夹
- settings/favorites 收藏夹列表加载失败:escapeHTML 在 energy Tab 闭包内,收藏夹 Tab 无法访问导致 ReferenceError,修复为本地 esc() 函数 - settings 创建/删除收藏夹请求缺少 CSRF Token(403),统一使用 fetch + X-CSRF-Token 头 - API 错误时显示实际错误信息而非吞掉显示"暂无收藏夹" - 文章详情页收藏按钮:点击弹出选择收藏夹弹窗,列出所有收藏夹,已收藏的文章高亮显示 - 弹窗支持原地新建收藏夹并立即收藏(一键创建+收藏) - 新增收藏夹弹窗 CSS 样式(.fav-overlay/.fav-modal/.fav-folder-option 等)
This commit is contained in:
@ -85,6 +85,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择收藏夹弹窗 -->
|
||||
<div class="fav-overlay" id="favOverlay" style="display:none">
|
||||
<div class="fav-modal">
|
||||
<div class="fav-modal-header">
|
||||
<h3>选择收藏夹</h3>
|
||||
<button class="fav-close" id="favClose">×</button>
|
||||
</div>
|
||||
<div class="fav-modal-body">
|
||||
<div class="fav-folder-list" id="favFolderCheckList">
|
||||
<div class="fav-loading">加载中...</div>
|
||||
</div>
|
||||
<div class="fav-new-folder">
|
||||
<input type="text" class="fav-new-input" id="favNewInput" placeholder="新建收藏夹..." maxlength="50">
|
||||
<button class="fav-new-btn" id="favNewConfirm">创建并收藏</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- 评论区 -->
|
||||
@ -345,59 +364,253 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// ========== 收藏按钮 ==========
|
||||
// ========== 收藏按钮(弹窗选择收藏夹)==========
|
||||
(function() {
|
||||
var favBtn = document.getElementById('favBtn');
|
||||
var favCountEl = document.getElementById('favCount');
|
||||
var container = document.getElementById('postReactions');
|
||||
var overlay = document.getElementById('favOverlay');
|
||||
var closeBtn = document.getElementById('favClose');
|
||||
var folderListEl = document.getElementById('favFolderCheckList');
|
||||
var newInput = document.getElementById('favNewInput');
|
||||
var newBtn = document.getElementById('favNewConfirm');
|
||||
if (!favBtn || !container) return;
|
||||
|
||||
var postId = container.dataset.postId;
|
||||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||||
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
||||
var isFavorited = false;
|
||||
var currentFolderId = null;
|
||||
var loading = false;
|
||||
|
||||
// 页面加载时获取收藏状态
|
||||
fetch('/api/posts/' + postId + '/folder-status')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.success && d.data && d.data.favorited) {
|
||||
favBtn.classList.add('active');
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
|
||||
favBtn.addEventListener('click', function() {
|
||||
favBtn.disabled = true;
|
||||
fetch('/api/posts/' + postId + '/favorite', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }
|
||||
})
|
||||
.then(function(r) {
|
||||
if (r.status === 401) { window.location.href = '/login?redirect=/posts/' + postId; return null; }
|
||||
function api(method, url, data) {
|
||||
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
|
||||
if (csrfToken) opts.headers['X-CSRF-Token'] = csrfToken;
|
||||
if (data) opts.body = JSON.stringify(data);
|
||||
return fetch(url, opts).then(function(r) {
|
||||
if (r.status === 401) { window.location.href = '/login?redirect=/posts/' + postId; throw new Error('unauthorized'); }
|
||||
return r.json();
|
||||
})
|
||||
.then(function(d) {
|
||||
favBtn.disabled = false;
|
||||
if (!d) return;
|
||||
if (d.success) {
|
||||
var isFav = d.data && d.data.favorited;
|
||||
if (isFav) {
|
||||
});
|
||||
}
|
||||
|
||||
// 更新按钮状态
|
||||
function updateBtn(favorited) {
|
||||
isFavorited = favorited;
|
||||
if (favorited) {
|
||||
favBtn.classList.add('active');
|
||||
} else {
|
||||
favBtn.classList.remove('active');
|
||||
currentFolderId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载收藏状态
|
||||
function loadStatus() {
|
||||
return fetch('/api/posts/' + postId + '/folder-status')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.success && d.data && d.data.favorited) {
|
||||
currentFolderId = d.data.folder_id;
|
||||
updateBtn(true);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
// HTML 转义
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// 渲染收藏夹列表
|
||||
function renderFolderList(folders) {
|
||||
if (!folders || folders.length === 0) {
|
||||
folderListEl.innerHTML = '<div class="fav-empty">暂无收藏夹,请在下方创建</div>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
folders.forEach(function(f) {
|
||||
var isActive = currentFolderId === f.id;
|
||||
html += '<div class="fav-folder-option' + (isActive ? ' active' : '') + '" data-id="' + f.id + '">'
|
||||
+ '<span class="fav-folder-opt-name">' + esc(f.name) + (f.is_default ? ' <small>(默认)</small>' : '') + '</span>'
|
||||
+ '<span class="fav-folder-opt-count">' + (f.item_count || 0) + ' 篇</span>'
|
||||
+ '</div>';
|
||||
});
|
||||
folderListEl.innerHTML = html;
|
||||
}
|
||||
|
||||
// 显示弹窗
|
||||
function showModal() {
|
||||
if (!overlay) { handleNoModal(); return; }
|
||||
loading = true;
|
||||
overlay.style.display = 'flex';
|
||||
folderListEl.innerHTML = '<div class="fav-loading">加载中...</div>';
|
||||
|
||||
api('GET', '/api/folders')
|
||||
.then(function(d) {
|
||||
var folders = (d.success && d.data && d.data.folders) ? d.data.folders : [];
|
||||
renderFolderList(folders);
|
||||
loading = false;
|
||||
})
|
||||
.catch(function() {
|
||||
folderListEl.innerHTML = '<div class="fav-loading fav-error">加载失败,请重试</div>';
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function hideModal() {
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
loading = false;
|
||||
}
|
||||
|
||||
// 无弹窗回退(未登录跳转)
|
||||
function handleNoModal() {
|
||||
if (!csrfToken) {
|
||||
window.location.href = '/login?redirect=/posts/' + postId;
|
||||
return;
|
||||
}
|
||||
// 无弹窗兜底:直接 toggle
|
||||
favBtn.disabled = true;
|
||||
api('POST', '/api/posts/' + postId + '/favorite')
|
||||
.then(function(d) {
|
||||
favBtn.disabled = false;
|
||||
if (d.success) {
|
||||
var f = d.data && d.data.favorited;
|
||||
updateBtn(f);
|
||||
if (favCountEl && d.data && typeof d.data.favorites_count === 'number') {
|
||||
favCountEl.textContent = d.data.favorites_count;
|
||||
} else if (favCountEl) {
|
||||
var cur = parseInt(favCountEl.textContent) || 0;
|
||||
favCountEl.textContent = isFav ? cur + 1 : Math.max(0, cur - 1);
|
||||
favCountEl.textContent = f ? cur + 1 : Math.max(0, cur - 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function() { favBtn.disabled = false; });
|
||||
}
|
||||
|
||||
// 收藏到指定收藏夹
|
||||
function addToFolder(folderId) {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
api('POST', '/api/folders/' + folderId + '/posts', { post_id: parseInt(postId) })
|
||||
.then(function(d) {
|
||||
loading = false;
|
||||
if (d.success) {
|
||||
currentFolderId = folderId;
|
||||
updateBtn(true);
|
||||
if (favCountEl) favCountEl.textContent = (parseInt(favCountEl.textContent) || 0) + 1;
|
||||
hideModal();
|
||||
} else {
|
||||
alert(d.message || '操作失败');
|
||||
}
|
||||
})
|
||||
.catch(function() { favBtn.disabled = false; });
|
||||
.catch(function() { loading = false; });
|
||||
}
|
||||
|
||||
// 取消收藏
|
||||
function removeFromFolder(folderId) {
|
||||
if (loading) return;
|
||||
if (!confirm('确定取消收藏吗?')) return;
|
||||
loading = true;
|
||||
api('DELETE', '/api/folders/' + folderId + '/posts/' + postId)
|
||||
.then(function(d) {
|
||||
loading = false;
|
||||
if (d.success) {
|
||||
currentFolderId = null;
|
||||
updateBtn(false);
|
||||
if (favCountEl) favCountEl.textContent = Math.max(0, (parseInt(favCountEl.textContent) || 0) - 1);
|
||||
hideModal();
|
||||
} else {
|
||||
alert(d.message || '操作失败');
|
||||
}
|
||||
})
|
||||
.catch(function() { loading = false; });
|
||||
}
|
||||
|
||||
// 创建收藏夹并收藏
|
||||
function createAndAdd(name) {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
newBtn.disabled = true;
|
||||
newBtn.textContent = '创建中...';
|
||||
api('POST', '/api/folders', { name: name, description: '', is_public: false })
|
||||
.then(function(d) {
|
||||
if (!d.success) {
|
||||
loading = false;
|
||||
newBtn.disabled = false;
|
||||
newBtn.textContent = '创建并收藏';
|
||||
alert(d.message || '创建失败');
|
||||
return;
|
||||
}
|
||||
var newId = d.data && d.data.id;
|
||||
return api('POST', '/api/folders/' + newId + '/posts', { post_id: parseInt(postId) })
|
||||
.then(function(d2) {
|
||||
loading = false;
|
||||
newBtn.disabled = false;
|
||||
newBtn.textContent = '创建并收藏';
|
||||
if (d2.success) {
|
||||
currentFolderId = newId;
|
||||
updateBtn(true);
|
||||
if (favCountEl) favCountEl.textContent = (parseInt(favCountEl.textContent) || 0) + 1;
|
||||
hideModal();
|
||||
} else {
|
||||
alert(d2.message || '操作失败');
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
loading = false;
|
||||
newBtn.disabled = false;
|
||||
newBtn.textContent = '创建并收藏';
|
||||
});
|
||||
}
|
||||
|
||||
// 事件:点击收藏按钮 → 打开弹窗
|
||||
favBtn.addEventListener('click', function() {
|
||||
if (!csrfToken) { window.location.href = '/login?redirect=/posts/' + postId; return; }
|
||||
showModal();
|
||||
});
|
||||
|
||||
// 事件:关闭弹窗
|
||||
if (closeBtn) closeBtn.addEventListener('click', hideModal);
|
||||
if (overlay) overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) hideModal();
|
||||
});
|
||||
|
||||
// 事件:点击收藏夹选项
|
||||
if (folderListEl) {
|
||||
folderListEl.addEventListener('click', function(e) {
|
||||
var option = e.target.closest('.fav-folder-option');
|
||||
if (!option) return;
|
||||
var folderId = parseInt(option.getAttribute('data-id'));
|
||||
if (isNaN(folderId)) return;
|
||||
if (currentFolderId === folderId) {
|
||||
removeFromFolder(folderId);
|
||||
} else {
|
||||
addToFolder(folderId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 事件:新建收藏夹
|
||||
if (newBtn && newInput) {
|
||||
newBtn.addEventListener('click', function() {
|
||||
var name = newInput.value.trim();
|
||||
if (!name) { alert('请输入收藏夹名称'); return; }
|
||||
if (name.length > 50) { alert('收藏夹名称不能超过 50 个字符'); return; }
|
||||
createAndAdd(name);
|
||||
});
|
||||
newInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') newBtn.click();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadStatus();
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@ -1032,11 +1032,31 @@
|
||||
var createBtn = document.getElementById('createFolderBtn');
|
||||
if (!listEl) return;
|
||||
|
||||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||||
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
||||
|
||||
function esc(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s || '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function api(method, url, data) {
|
||||
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
|
||||
if (csrfToken) opts.headers['X-CSRF-Token'] = csrfToken;
|
||||
if (data) opts.body = JSON.stringify(data);
|
||||
return fetch(url, opts).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
function loadFolders() {
|
||||
fetch('/api/folders')
|
||||
.then(function (r) { return r.json(); })
|
||||
listEl.innerHTML = '<div class="energy-log-empty">加载中...</div>';
|
||||
api('GET', '/api/folders')
|
||||
.then(function (d) {
|
||||
if (!d.success || !d.data || !d.data.folders || d.data.folders.length === 0) {
|
||||
if (!d.success) {
|
||||
listEl.innerHTML = '<div class="energy-log-empty">' + esc(d.message || '加载失败') + '</div>';
|
||||
return;
|
||||
}
|
||||
if (!d.data || !d.data.folders || d.data.folders.length === 0) {
|
||||
listEl.innerHTML = '<div class="energy-log-empty">暂无收藏夹</div>';
|
||||
return;
|
||||
}
|
||||
@ -1046,7 +1066,7 @@
|
||||
var pub = f.is_public ? '公开' : '私密';
|
||||
html += '<div class="fav-folder-card">'
|
||||
+ '<div class="fav-folder-info">'
|
||||
+ '<span class="fav-folder-name">' + escapeHTML(f.name) + isDefault + '</span>'
|
||||
+ '<span class="fav-folder-name">' + esc(f.name) + isDefault + '</span>'
|
||||
+ '<span class="fav-folder-meta">' + (f.item_count || 0) + ' 篇文章 · ' + pub + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div class="fav-folder-actions">'
|
||||
@ -1056,33 +1076,27 @@
|
||||
});
|
||||
listEl.innerHTML = html;
|
||||
|
||||
// 绑定删除按钮
|
||||
listEl.querySelectorAll('.fav-folder-del').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var id = btn.getAttribute('data-id');
|
||||
if (!confirm('确定要删除此收藏夹?其中的收藏内容也将被清除。')) return;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('DELETE', '/api/folders/' + id, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState !== 4) return;
|
||||
try {
|
||||
var r = JSON.parse(xhr.responseText);
|
||||
api('DELETE', '/api/folders/' + id)
|
||||
.then(function (r) {
|
||||
if (r.success) {
|
||||
showSettingsToast('收藏夹已删除');
|
||||
loadFolders();
|
||||
} else {
|
||||
showSettingsToast(r.message || '删除失败', true);
|
||||
}
|
||||
} catch (e) {
|
||||
})
|
||||
.catch(function () {
|
||||
showSettingsToast('删除失败', true);
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
listEl.innerHTML = '<div class="energy-log-empty">加载失败</div>';
|
||||
listEl.innerHTML = '<div class="energy-log-empty">加载失败,请刷新页面重试</div>';
|
||||
});
|
||||
}
|
||||
|
||||
@ -1094,24 +1108,18 @@
|
||||
showSettingsToast('收藏夹名称不能超过 50 个字符', true);
|
||||
return;
|
||||
}
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/folders', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState !== 4) return;
|
||||
try {
|
||||
var r = JSON.parse(xhr.responseText);
|
||||
api('POST', '/api/folders', { name: name.trim(), description: '', is_public: false })
|
||||
.then(function (r) {
|
||||
if (r.success) {
|
||||
showSettingsToast('收藏夹已创建');
|
||||
loadFolders();
|
||||
} else {
|
||||
showSettingsToast(r.message || '创建失败', true);
|
||||
}
|
||||
} catch (e) {
|
||||
})
|
||||
.catch(function () {
|
||||
showSettingsToast('创建失败', true);
|
||||
}
|
||||
};
|
||||
xhr.send(JSON.stringify({ name: name.trim(), description: '', is_public: false }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -790,6 +790,184 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ========== 选择收藏夹弹窗 ========== */
|
||||
.fav-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.fav-modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
width: 400px;
|
||||
max-width: 92vw;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fav-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fav-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.fav-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.fav-close:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.fav-modal-body {
|
||||
padding: 16px 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fav-loading {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.fav-loading.fav-error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.fav-empty {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.fav-folder-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.fav-folder-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.fav-folder-option:hover {
|
||||
border-color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.fav-folder-option.active {
|
||||
border-color: #f59e0b;
|
||||
background: #fef3c7;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fav-folder-option.active::after {
|
||||
content: "✓";
|
||||
color: #f59e0b;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fav-folder-opt-name {
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.fav-folder-opt-name small {
|
||||
font-weight: 400;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.fav-folder-opt-count {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fav-new-folder {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fav-new-input {
|
||||
flex: 1;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.fav-new-input:focus {
|
||||
border-color: #f59e0b;
|
||||
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.15);
|
||||
}
|
||||
|
||||
.fav-new-btn {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #f59e0b;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fav-new-btn:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.fav-new-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ========== 评论区 ========== */
|
||||
.comments-section {
|
||||
max-width: 800px;
|
||||
|
||||
Reference in New Issue
Block a user