refactor: posts/show.html 内联 JS 拆分为独立文件

- 拆分 926 行内联 JS 为 5 个独立模块文件
- post-view.js: Vditor 渲染 + 提交审核按钮 (~50 行)
- post-energize.js: 赋能按钮 + 弹窗 (~76 行)
- post-reactions.js: 赞/踩系统 (~71 行)
- post-favorite.js: 收藏按钮 + 收藏夹弹窗 (~218 行)
- post-comments.js: 评论系统 (~456 行)
- show.html 从 1080 行缩减至 159 行
This commit is contained in:
2026-06-02 20:00:06 +08:00
parent a8a52f5fd0
commit 2e6e7fc057
6 changed files with 874 additions and 926 deletions

View File

@ -149,931 +149,10 @@
<script src="/static/vditor/dist/method.min.js?v={{assetV "/static/vditor/dist/method.min.js"}}" nonce="{{.CSPNonce}}"></script> <script src="/static/vditor/dist/method.min.js?v={{assetV "/static/vditor/dist/method.min.js"}}" nonce="{{.CSPNonce}}"></script>
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script> <script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
<script src="/static/js/shortcode.js?v={{assetV "/static/js/shortcode.js"}}" nonce="{{.CSPNonce}}"></script> <script src="/static/js/shortcode.js?v={{assetV "/static/js/shortcode.js"}}" nonce="{{.CSPNonce}}"></script>
<script nonce="{{.CSPNonce}}"> <script src="/static/js/post-view.js?v={{assetV "/static/js/post-view.js"}}" nonce="{{.CSPNonce}}"></script>
(function() { <script src="/static/js/post-energize.js?v={{assetV "/static/js/post-energize.js"}}" nonce="{{.CSPNonce}}"></script>
var el = document.getElementById('postContent'); <script src="/static/js/post-reactions.js?v={{assetV "/static/js/post-reactions.js"}}" nonce="{{.CSPNonce}}"></script>
var mdEl = document.getElementById('postMdContent'); <script src="/static/js/post-favorite.js?v={{assetV "/static/js/post-favorite.js"}}" nonce="{{.CSPNonce}}"></script>
if (!el || !mdEl) return; <script src="/static/js/post-comments.js?v={{assetV "/static/js/post-comments.js"}}" nonce="{{.CSPNonce}}"></script>
var md = mdEl.value;
Vditor.preview(el, md, {
cdn: '/static/vditor',
theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
hljs: { style: 'github-dark', enable: true },
after: function() {
// 外部链接自动加 nofollow + target="_blank",避免权重流失
var host = window.location.host;
var links = el.querySelectorAll('a[href^="http"]');
for (var i = 0; i < links.length; i++) {
var a = links[i];
if (a.host && a.host !== host) {
a.setAttribute('rel', 'nofollow noopener noreferrer');
a.setAttribute('target', '_blank');
}
}
}
});
// Vditor.preview() 渲染 MDshortcode 占位 div 原样保留
// 渲染完成后,将占位 div 替换为对应卡片 UI
renderShortcodes(el);
})();
</script>
<script nonce="{{.CSPNonce}}">
// 提交审核按钮
(function() {
var btn = document.getElementById('submitAuditBtn');
if (!btn) return;
btn.addEventListener('click', function() {
if (!confirm('确认提交审核?审核通过后将公开发布。')) return;
fetch('/api/posts/' + btn.dataset.id + '/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
}
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) { window.location.reload(); }
else { alert(d.message || '提交失败'); }
})
.catch(function() { alert('请求失败'); });
});
})();
</script>
<script nonce="{{.CSPNonce}}">
// 赋能按钮 + 弹窗
(function() {
var btn = document.getElementById('energizeBtn');
var overlay = document.getElementById('energizeOverlay');
var closeBtn = document.getElementById('energizeClose');
var capHint = document.getElementById('energizeCapHint');
var lightBtn = document.getElementById('energizeLight');
var heavyBtn = document.getElementById('energizeHeavy');
if (!btn || !overlay) return;
var postId = btn.dataset.id;
function showOverlay() {
overlay.style.display = 'flex';
}
function hideOverlay() {
overlay.style.display = 'none';
capHint.style.display = 'none';
lightBtn.disabled = false;
heavyBtn.disabled = false;
}
btn.addEventListener('click', function() {
// 先获取域能状态,判断是否需要显示上限提示
fetch('/api/energy/info')
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success && d.data && d.data.daily_exp_cap_reached) {
capHint.style.display = 'block';
}
showOverlay();
})
.catch(function() { showOverlay(); });
});
closeBtn.addEventListener('click', hideOverlay);
overlay.addEventListener('click', function(e) {
if (e.target === overlay) hideOverlay();
});
function doEnergize(amount) {
lightBtn.disabled = true;
heavyBtn.disabled = true;
fetch('/api/energy/energize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
},
body: JSON.stringify({ post_id: parseInt(postId), amount: amount })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
alert(d.data.message || '赋能成功!');
hideOverlay();
} else {
alert(d.message || '赋能失败');
lightBtn.disabled = false;
heavyBtn.disabled = false;
}
})
.catch(function() {
alert('请求失败');
lightBtn.disabled = false;
heavyBtn.disabled = false;
});
}
lightBtn.addEventListener('click', function() {
doEnergize(parseInt(lightBtn.dataset.amount));
});
heavyBtn.addEventListener('click', function() {
doEnergize(parseInt(heavyBtn.dataset.amount));
});
})();
</script>
<script nonce="{{.CSPNonce}}">
// ========== 赞/踩系统 ==========
(function() {
var container = document.getElementById('postReactions');
var likeBtn = document.getElementById('likeBtn');
var dislikeBtn = document.getElementById('dislikeBtn');
var likesCountEl = document.getElementById('likesCount');
if (!container || !likeBtn || !dislikeBtn) return;
var postId = container.dataset.postId;
// 页面加载时获取当前反应状态
fetch('/api/posts/' + postId + '/reaction')
.then(function(r) { return r.json(); })
.then(function(d) {
if (!d.success || !d.data) return;
updateUI(d.data.reaction, d.data.likes_count);
})
.catch(function() {});
function updateUI(reaction, count) {
likeBtn.classList.remove('active');
dislikeBtn.classList.remove('active');
if (reaction === 'liked') likeBtn.classList.add('active');
if (reaction === 'disliked') dislikeBtn.classList.add('active');
if (typeof count === 'number') likesCountEl.textContent = count;
}
likeBtn.addEventListener('click', function() {
likeBtn.disabled = true;
fetch('/api/posts/' + postId + '/like', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCSRFToken() }
})
.then(function(r) {
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; }
return r.json();
})
.then(function(d) {
likeBtn.disabled = false;
if (!d) return;
if (d.success) {
updateUI(d.data.liked ? 'liked' : 'none', d.data.likes_count);
} else {
alert(d.message || '操作失败');
}
})
.catch(function() { likeBtn.disabled = false; });
});
dislikeBtn.addEventListener('click', function() {
dislikeBtn.disabled = true;
fetch('/api/posts/' + postId + '/dislike', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCSRFToken() }
})
.then(function(r) {
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; }
return r.json();
})
.then(function(d) {
dislikeBtn.disabled = false;
if (!d) return;
if (d.success) {
updateUI(d.data.disliked ? 'disliked' : 'none');
} else {
alert(d.message || '操作失败');
}
})
.catch(function() { dislikeBtn.disabled = false; });
});
})();
</script>
<script nonce="{{.CSPNonce}}">
// ========== 收藏按钮(弹窗选择收藏夹)==========
(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 isFavorited = false;
var currentFolderId = null;
var loading = false;
// 更新按钮状态
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 (!getCSRFToken()) {
window.location.href = '/auth/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 = 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() { 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 (!getCSRFToken()) { window.location.href = '/auth/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>
<script nonce="{{.CSPNonce}}">
// ========== 评论系统 ==========
(function() {
var commentsSection = document.getElementById('comments');
if (!commentsSection) return;
var postId = '{{.Post.ID}}';
var commentsList = document.getElementById('commentsList');
var commentsTotal = document.getElementById('commentsTotal');
var loadMoreBtn = document.getElementById('commentsLoadMore');
var commentInput = document.getElementById('commentInput');
var commentSubmitBtn = document.getElementById('commentSubmitBtn');
var mentionDropdown = document.getElementById('mentionDropdown');
var currentPage = 1;
var totalPages = 1;
// escapeHtml / formatTime / showToast / api 由 shared/utils.js 统一提供
function buildCommentCard(c) {
var time = c.created_at ? formatTime(c.created_at) : '';
var author = c.author_name ? escapeHtml(c.author_name) : '用户已注销';
var body = c.body || '';
// 渲染 @ 提及为链接
body = escapeHtml(body);
body = body.replace(/@(\S{1,16})/g, '<span class="reply-to">@$1</span>');
var html = '<div class="comment-card" data-id="' + c.id + '">' +
'<div class="comment-header">' +
'<span class="comment-author">' + author + '</span>' +
'<span class="comment-time">' + time + '</span>' +
'</div>' +
'<div class="comment-body">' + body + '</div>' +
'<div class="comment-actions">';
if (isLoggedIn()) {
html += '<button class="comment-action-btn comment-reply-btn" data-id="'+c.id+'">回复</button>';
}
html += '</div>' +
// 回复区域
'<div class="comment-replies" id="replies-' + c.id + '"></div>';
if (c.replies_count > 0) {
html += '<button class="comment-toggle-replies" id="toggle-' + c.id + '" data-id="'+c.id+'" data-loaded="false">共 ' + c.replies_count + ' 条回复</button>';
}
// 回复输入框(默认隐藏)
html += '<div class="comment-reply-form" id="replyForm-' + c.id + '" style="display:none">' +
'<textarea class="comment-reply-input" id="replyInput-' + c.id + '" rows="2" placeholder="写下回复..." maxlength="5000"></textarea>' +
'<button class="btn btn-primary comment-reply-submit" data-id="' + c.id + '">回复</button>' +
'</div>' +
'</div>';
return html;
}
function buildReplyCard(r) {
var time = r.created_at ? formatTime(r.created_at) : '';
var author = r.author_name ? escapeHtml(r.author_name) : '用户已注销';
var body = r.body || '';
body = escapeHtml(body);
body = body.replace(/@(\S{1,16})/g, '<span class="reply-to">@$1</span>');
var prefix = '';
if (r.reply_to_name) {
prefix = '<span class="comment-reply-to">回复 ' + escapeHtml(r.reply_to_name) + ':</span>';
}
return '<div class="comment-reply-card" data-id="' + r.id + '">' +
'<div class="comment-header">' +
'<span class="comment-author" style="font-size:13px">' + author + '</span>' +
'<span class="comment-time" style="font-size:11px">' + time + '</span>' +
'</div>' +
'<div class="comment-reply-body">' + prefix + body + '</div>' +
'</div>';
}
function isLoggedIn() {
return !!document.getElementById('commentInput');
}
// ====== 加载根评论 ======
function loadComments(page) {
var url = '/api/posts/' + postId + '/comments?page=' + page + '&page_size=20';
fetch(url).then(function(r) { return r.json(); })
.then(function(d) {
if (!d.success) return;
totalPages = d.data.total_pages || 1;
commentsTotal.textContent = '(' + d.data.total + ')';
var items = d.data.items || [];
for (var i = 0; i < items.length; i++) {
commentsList.insertAdjacentHTML('beforeend', buildCommentCard(items[i]));
}
if (page < totalPages) {
loadMoreBtn.style.display = 'block';
loadMoreBtn.textContent = '加载更多';
} else {
loadMoreBtn.style.display = 'none';
}
bindEvents();
})
.catch(function() {
commentsList.innerHTML = '<div class="empty-state">评论加载失败</div>';
});
}
// ====== 绑定事件 ======
function bindEvents() {
// 回复按钮
var replyBtns = commentsList.querySelectorAll('.comment-reply-btn');
for (var i = 0; i < replyBtns.length; i++) {
if (replyBtns[i]._bound) continue;
replyBtns[i]._bound = true;
bindReplyBtn(replyBtns[i]);
}
// 展开回复按钮
var toggleBtns = commentsList.querySelectorAll('.comment-toggle-replies');
for (var j = 0; j < toggleBtns.length; j++) {
if (toggleBtns[j]._bound) continue;
toggleBtns[j]._bound = true;
bindToggleBtn(toggleBtns[j]);
}
// 回复提交按钮
var submitBtns = commentsList.querySelectorAll('.comment-reply-submit');
for (var k = 0; k < submitBtns.length; k++) {
if (submitBtns[k]._bound) continue;
submitBtns[k]._bound = true;
bindReplySubmit(submitBtns[k]);
}
// 回复输入框 @提及
var replyInputs = commentsList.querySelectorAll('.comment-reply-input');
for (var m = 0; m < replyInputs.length; m++) {
if (replyInputs[m]._mentionBound) continue;
replyInputs[m]._mentionBound = true;
bindMention(replyInputs[m]);
}
}
function bindReplyBtn(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
var form = document.getElementById('replyForm-' + id);
if (form) {
form.style.display = form.style.display === 'none' ? 'flex' : 'none';
if (form.style.display !== 'none') {
document.getElementById('replyInput-' + id).focus();
}
}
});
}
function bindToggleBtn(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
var loaded = btn.dataset.loaded === 'true';
if (loaded) {
// 切换显示/隐藏
var replies = document.getElementById('replies-' + id);
if (replies) {
replies.style.display = replies.style.display === 'none' ? 'block' : 'none';
btn.textContent = replies.style.display === 'none' ? '共 ' + (btn.dataset.count || replies.children.length) + ' 条回复' : '收起回复';
}
return;
}
// 首次加载
btn.dataset.loaded = 'true';
btn.textContent = '加载中...';
fetch('/api/posts/' + postId + '/comments/' + id + '/replies')
.then(function(r) { return r.json(); })
.then(function(d) {
if (!d.success) return;
var replies = d.data.replies || [];
var container = document.getElementById('replies-' + id);
if (!container) return;
var html = '';
for (var i = 0; i < replies.length; i++) {
html += buildReplyCard(replies[i]);
}
container.innerHTML = html;
btn.dataset.count = replies.length;
btn.textContent = '收起回复';
})
.catch(function() {
btn.textContent = '加载失败';
});
});
}
function bindReplySubmit(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
var input = document.getElementById('replyInput-' + id);
if (!input) return;
var body = input.value.trim();
if (!body) { alert('请输入回复内容'); return; }
btn.disabled = true;
api('POST', '/api/comments/' + id + '/reply', { body: body })
.then(function(d) {
btn.disabled = false;
if (d.success) {
input.value = '';
document.getElementById('replyForm-' + id).style.display = 'none';
// 刷新回复列表
var toggleBtn = document.getElementById('toggle-' + id);
if (toggleBtn) {
toggleBtn.dataset.loaded = 'false';
if (toggleBtn.dataset.count) {
toggleBtn.dataset.count = parseInt(toggleBtn.dataset.count) + 1;
toggleBtn.textContent = '共 ' + toggleBtn.dataset.count + ' 条回复';
}
toggleBtn.click();
} else {
// 没有展开按钮,新建一个
var replies = document.getElementById('replies-' + id);
if (replies) {
fetch('/api/posts/' + postId + '/comments/' + id + '/replies')
.then(function(r) { return r.json(); })
.then(function(d2) {
if (!d2.success) return;
var items = d2.data.replies || [];
var html = '';
for (var i = 0; i < items.length; i++) {
html += buildReplyCard(items[i]);
}
replies.innerHTML = html;
});
}
}
updateTotalCount(1);
} else {
alert(d.message || '回复失败');
}
})
.catch(function() {
btn.disabled = false;
alert('请求失败');
});
});
}
// ====== @提及自动补全 ======
var mentionTarget = null;
var mentionPos = 0;
var searchTimer = null;
function bindMention(input) {
input.addEventListener('input', function() {
var val = input.value;
var cursor = input.selectionStart;
// 查找光标前的最后一个 @
var atPos = val.lastIndexOf('@', cursor - 1);
if (atPos === -1 || (atPos > 0 && val[atPos - 1] !== ' ' && val[atPos - 1] !== '\n')) {
hideMention();
return;
}
var query = val.substring(atPos + 1, cursor);
if (query.length === 0 || query.includes(' ')) {
hideMention();
return;
}
mentionTarget = input;
mentionPos = atPos;
clearTimeout(searchTimer);
searchTimer = setTimeout(function() {
fetch('/api/users/search?q=' + encodeURIComponent(query))
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success && d.data && d.data.length > 0) {
showMention(input, d.data, query);
} else {
hideMention();
}
})
.catch(function() { hideMention(); });
}, 200);
});
input.addEventListener('blur', function() {
setTimeout(hideMention, 150);
});
}
function showMention(input, users, query) {
var rect = input.getBoundingClientRect();
var dropdown = mentionDropdown;
dropdown.innerHTML = '';
dropdown.style.display = 'block';
dropdown.style.top = (rect.bottom + window.scrollY + 4) + 'px';
dropdown.style.left = (rect.left + window.scrollX) + 'px';
dropdown.style.width = Math.max(rect.width, 200) + 'px';
for (let i = 0; i < users.length; i++) {
let u = users[i];
var item = document.createElement('div');
item.className = 'mention-item';
item.innerHTML = '<span class="mention-username">' + escapeHtml(u.username) + '</span><span class="mention-uid">' + escapeHtml(u.uid) + '</span>';
item.addEventListener('mousedown', function(e) {
e.preventDefault();
selectMention(u);
});
dropdown.appendChild(item);
}
}
function hideMention() {
mentionDropdown.style.display = 'none';
mentionDropdown.innerHTML = '';
mentionTarget = null;
}
function selectMention(user) {
if (!mentionTarget) return;
var val = mentionTarget.value;
var before = val.substring(0, mentionPos);
var after = val.substring(mentionTarget.selectionStart);
var insert = '@' + user.username + ' ';
mentionTarget.value = before + insert + after;
mentionTarget.focus();
var newPos = mentionPos + insert.length;
mentionTarget.setSelectionRange(newPos, newPos);
hideMention();
}
// ====== 发表评论 ======
if (commentSubmitBtn) {
commentSubmitBtn.addEventListener('click', function() {
var body = commentInput.value.trim();
if (!body) { alert('请输入评论内容'); return; }
commentSubmitBtn.disabled = true;
api('POST', '/api/posts/' + postId + '/comments', { body: body })
.then(function(d) {
commentSubmitBtn.disabled = false;
if (d.success) {
commentInput.value = '';
// 刷新列表
commentsList.innerHTML = '';
currentPage = 1;
loadComments(currentPage);
updateTotalCount(1);
} else {
alert(d.message || '发表失败');
}
})
.catch(function() {
commentSubmitBtn.disabled = false;
alert('请求失败');
});
});
// 主输入框 @提及
bindMention(commentInput);
}
// ====== 加载更多 ======
loadMoreBtn.addEventListener('click', function() {
currentPage++;
loadComments(currentPage);
});
function updateTotalCount(delta) {
var el = commentsTotal;
var current = parseInt(el.textContent.replace(/[()]/g, '')) || 0;
el.textContent = '(' + (current + delta) + ')';
}
// ====== 图片上传 ======
var imageUploadInput = document.getElementById('commentImageInput');
var uploadLabel = document.getElementById('commentUploadLabel');
var uploadPreview = document.getElementById('commentUploadPreview');
if (imageUploadInput && uploadLabel) {
uploadLabel.addEventListener('click', function() {
imageUploadInput.click();
});
imageUploadInput.addEventListener('change', function() {
var file = imageUploadInput.files[0];
if (!file) return;
uploadLabel.style.pointerEvents = 'none';
uploadLabel.style.opacity = '0.5';
uploadLabel.textContent = '上传中...';
var formData = new FormData();
formData.append('file', file);
fetch('/api/comments/upload-image', {
method: 'POST',
headers: { 'X-CSRF-Token': getCSRFToken() },
body: formData
})
.then(function(r) { return r.json(); })
.then(function(d) {
uploadLabel.style.pointerEvents = 'auto';
uploadLabel.style.opacity = '1';
uploadLabel.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> 图片';
if (d.success && d.data && d.data.url) {
// 插入图片 URL 到文本框当前光标位置
var imgUrl = '\n![](' + d.data.url + ')\n';
var input = commentInput;
var start = input.selectionStart;
var end = input.selectionEnd;
input.value = input.value.substring(0, start) + imgUrl + input.value.substring(end);
input.focus();
input.selectionStart = input.selectionEnd = start + imgUrl.length;
} else {
alert(d.message || '上传失败');
}
})
.catch(function() {
uploadLabel.style.pointerEvents = 'auto';
uploadLabel.style.opacity = '1';
uploadLabel.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> 图片';
alert('上传失败');
});
imageUploadInput.value = '';
});
}
// ====== 评论高亮 ======
// showToast 由 shared/utils.js 统一提供
function highlightComment(commentId) {
var card = commentsList.querySelector('.comment-card[data-id="' + commentId + '"]') ||
commentsList.querySelector('.comment-reply-card[data-id="' + commentId + '"]');
if (card) {
// 置顶:移动卡片到列表顶部
commentsList.insertBefore(card, commentsList.firstChild);
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
card.style.background = '#fff8e1';
card.style.transition = 'background 1.5s';
setTimeout(function() { card.style.background = ''; }, 2000);
return true;
}
return false;
}
// 解析 hash支持 #comment-{id}、#comments 以及回退)
var hash = window.location.hash;
var commentMatch = hash.match(/^#comment-(\d+)$/);
if (commentMatch) {
var targetCommentId = commentMatch[1];
// 等待评论加载完成后查找
var checkInterval = setInterval(function() {
if (highlightComment(targetCommentId)) {
clearInterval(checkInterval);
}
}, 300);
// 3秒后超时评论可能已被删除
setTimeout(function() {
clearInterval(checkInterval);
if (!commentsList.querySelector('.comment-card[data-id="' + targetCommentId + '"]') &&
!commentsList.querySelector('.comment-reply-card[data-id="' + targetCommentId + '"]')) {
showToast('该评论已不存在');
}
}, 3000);
} else if (hash === '#comments') {
setTimeout(function() {
var section = document.getElementById('comments');
if (section) {
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 400);
}
// ====== 初始化 ======
loadComments(currentPage);
})();
</script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,456 @@
// 评论系统
// 依赖: utils.js (escapeHtml, formatTime, showToast, api), common.js (getCSRFToken)
(function() {
var commentsSection = document.getElementById('comments');
if (!commentsSection) return;
var postId = commentsSection.closest('.container').querySelector('#postReactions').dataset.postId;
var commentsList = document.getElementById('commentsList');
var commentsTotal = document.getElementById('commentsTotal');
var loadMoreBtn = document.getElementById('commentsLoadMore');
var commentInput = document.getElementById('commentInput');
var commentSubmitBtn = document.getElementById('commentSubmitBtn');
var mentionDropdown = document.getElementById('mentionDropdown');
var currentPage = 1;
var totalPages = 1;
function buildCommentCard(c) {
var time = c.created_at ? formatTime(c.created_at) : '';
var author = c.author_name ? escapeHtml(c.author_name) : '用户已注销';
var body = c.body || '';
body = escapeHtml(body);
body = body.replace(/@(\S{1,16})/g, '<span class="reply-to">@$1</span>');
var html = '<div class="comment-card" data-id="' + c.id + '">' +
'<div class="comment-header">' +
'<span class="comment-author">' + author + '</span>' +
'<span class="comment-time">' + time + '</span>' +
'</div>' +
'<div class="comment-body">' + body + '</div>' +
'<div class="comment-actions">';
if (isLoggedIn()) {
html += '<button class="comment-action-btn comment-reply-btn" data-id="'+c.id+'">回复</button>';
}
html += '</div>' +
'<div class="comment-replies" id="replies-' + c.id + '"></div>';
if (c.replies_count > 0) {
html += '<button class="comment-toggle-replies" id="toggle-' + c.id + '" data-id="'+c.id+'" data-loaded="false">共 ' + c.replies_count + ' 条回复</button>';
}
html += '<div class="comment-reply-form" id="replyForm-' + c.id + '" style="display:none">' +
'<textarea class="comment-reply-input" id="replyInput-' + c.id + '" rows="2" placeholder="写下回复..." maxlength="5000"></textarea>' +
'<button class="btn btn-primary comment-reply-submit" data-id="' + c.id + '">回复</button>' +
'</div>' +
'</div>';
return html;
}
function buildReplyCard(r) {
var time = r.created_at ? formatTime(r.created_at) : '';
var author = r.author_name ? escapeHtml(r.author_name) : '用户已注销';
var body = r.body || '';
body = escapeHtml(body);
body = body.replace(/@(\S{1,16})/g, '<span class="reply-to">@$1</span>');
var prefix = '';
if (r.reply_to_name) {
prefix = '<span class="comment-reply-to">回复 ' + escapeHtml(r.reply_to_name) + ':</span>';
}
return '<div class="comment-reply-card" data-id="' + r.id + '">' +
'<div class="comment-header">' +
'<span class="comment-author" style="font-size:13px">' + author + '</span>' +
'<span class="comment-time" style="font-size:11px">' + time + '</span>' +
'</div>' +
'<div class="comment-reply-body">' + prefix + body + '</div>' +
'</div>';
}
function isLoggedIn() {
return !!document.getElementById('commentInput');
}
// ====== 加载根评论 ======
function loadComments(page) {
var url = '/api/posts/' + postId + '/comments?page=' + page + '&page_size=20';
fetch(url).then(function(r) { return r.json(); })
.then(function(d) {
if (!d.success) return;
totalPages = d.data.total_pages || 1;
commentsTotal.textContent = '(' + d.data.total + ')';
var items = d.data.items || [];
for (var i = 0; i < items.length; i++) {
commentsList.insertAdjacentHTML('beforeend', buildCommentCard(items[i]));
}
if (page < totalPages) {
loadMoreBtn.style.display = 'block';
loadMoreBtn.textContent = '加载更多';
} else {
loadMoreBtn.style.display = 'none';
}
bindEvents();
})
.catch(function() {
commentsList.innerHTML = '<div class="empty-state">评论加载失败</div>';
});
}
// ====== 绑定事件 ======
function bindEvents() {
var replyBtns = commentsList.querySelectorAll('.comment-reply-btn');
for (var i = 0; i < replyBtns.length; i++) {
if (replyBtns[i]._bound) continue;
replyBtns[i]._bound = true;
bindReplyBtn(replyBtns[i]);
}
var toggleBtns = commentsList.querySelectorAll('.comment-toggle-replies');
for (var j = 0; j < toggleBtns.length; j++) {
if (toggleBtns[j]._bound) continue;
toggleBtns[j]._bound = true;
bindToggleBtn(toggleBtns[j]);
}
var submitBtns = commentsList.querySelectorAll('.comment-reply-submit');
for (var k = 0; k < submitBtns.length; k++) {
if (submitBtns[k]._bound) continue;
submitBtns[k]._bound = true;
bindReplySubmit(submitBtns[k]);
}
var replyInputs = commentsList.querySelectorAll('.comment-reply-input');
for (var m = 0; m < replyInputs.length; m++) {
if (replyInputs[m]._mentionBound) continue;
replyInputs[m]._mentionBound = true;
bindMention(replyInputs[m]);
}
}
function bindReplyBtn(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
var form = document.getElementById('replyForm-' + id);
if (form) {
form.style.display = form.style.display === 'none' ? 'flex' : 'none';
if (form.style.display !== 'none') {
document.getElementById('replyInput-' + id).focus();
}
}
});
}
function bindToggleBtn(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
var loaded = btn.dataset.loaded === 'true';
if (loaded) {
var replies = document.getElementById('replies-' + id);
if (replies) {
replies.style.display = replies.style.display === 'none' ? 'block' : 'none';
btn.textContent = replies.style.display === 'none' ? '共 ' + (btn.dataset.count || replies.children.length) + ' 条回复' : '收起回复';
}
return;
}
btn.dataset.loaded = 'true';
btn.textContent = '加载中...';
fetch('/api/posts/' + postId + '/comments/' + id + '/replies')
.then(function(r) { return r.json(); })
.then(function(d) {
if (!d.success) return;
var replies = d.data.replies || [];
var container = document.getElementById('replies-' + id);
if (!container) return;
var html = '';
for (var i = 0; i < replies.length; i++) {
html += buildReplyCard(replies[i]);
}
container.innerHTML = html;
btn.dataset.count = replies.length;
btn.textContent = '收起回复';
})
.catch(function() {
btn.textContent = '加载失败';
});
});
}
function bindReplySubmit(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
var input = document.getElementById('replyInput-' + id);
if (!input) return;
var body = input.value.trim();
if (!body) { alert('请输入回复内容'); return; }
btn.disabled = true;
api('POST', '/api/comments/' + id + '/reply', { body: body })
.then(function(d) {
btn.disabled = false;
if (d.success) {
input.value = '';
document.getElementById('replyForm-' + id).style.display = 'none';
var toggleBtn = document.getElementById('toggle-' + id);
if (toggleBtn) {
toggleBtn.dataset.loaded = 'false';
if (toggleBtn.dataset.count) {
toggleBtn.dataset.count = parseInt(toggleBtn.dataset.count) + 1;
toggleBtn.textContent = '共 ' + toggleBtn.dataset.count + ' 条回复';
}
toggleBtn.click();
} else {
var replies = document.getElementById('replies-' + id);
if (replies) {
fetch('/api/posts/' + postId + '/comments/' + id + '/replies')
.then(function(r) { return r.json(); })
.then(function(d2) {
if (!d2.success) return;
var items = d2.data.replies || [];
var html = '';
for (var i = 0; i < items.length; i++) {
html += buildReplyCard(items[i]);
}
replies.innerHTML = html;
});
}
}
updateTotalCount(1);
} else {
alert(d.message || '回复失败');
}
})
.catch(function() {
btn.disabled = false;
alert('请求失败');
});
});
}
// ====== @提及自动补全 ======
var mentionTarget = null;
var mentionPos = 0;
var searchTimer = null;
function bindMention(input) {
input.addEventListener('input', function() {
var val = input.value;
var cursor = input.selectionStart;
var atPos = val.lastIndexOf('@', cursor - 1);
if (atPos === -1 || (atPos > 0 && val[atPos - 1] !== ' ' && val[atPos - 1] !== '\n')) {
hideMention();
return;
}
var query = val.substring(atPos + 1, cursor);
if (query.length === 0 || query.includes(' ')) {
hideMention();
return;
}
mentionTarget = input;
mentionPos = atPos;
clearTimeout(searchTimer);
searchTimer = setTimeout(function() {
fetch('/api/users/search?q=' + encodeURIComponent(query))
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success && d.data && d.data.length > 0) {
showMention(input, d.data, query);
} else {
hideMention();
}
})
.catch(function() { hideMention(); });
}, 200);
});
input.addEventListener('blur', function() {
setTimeout(hideMention, 150);
});
}
function showMention(input, users, query) {
var rect = input.getBoundingClientRect();
var dropdown = mentionDropdown;
dropdown.innerHTML = '';
dropdown.style.display = 'block';
dropdown.style.top = (rect.bottom + window.scrollY + 4) + 'px';
dropdown.style.left = (rect.left + window.scrollX) + 'px';
dropdown.style.width = Math.max(rect.width, 200) + 'px';
for (var i = 0; i < users.length; i++) {
(function(u) {
var item = document.createElement('div');
item.className = 'mention-item';
item.innerHTML = '<span class="mention-username">' + escapeHtml(u.username) + '</span><span class="mention-uid">' + escapeHtml(u.uid) + '</span>';
item.addEventListener('mousedown', function(e) {
e.preventDefault();
selectMention(u);
});
dropdown.appendChild(item);
})(users[i]);
}
}
function hideMention() {
mentionDropdown.style.display = 'none';
mentionDropdown.innerHTML = '';
mentionTarget = null;
}
function selectMention(user) {
if (!mentionTarget) return;
var val = mentionTarget.value;
var before = val.substring(0, mentionPos);
var after = val.substring(mentionTarget.selectionStart);
var insert = '@' + user.username + ' ';
mentionTarget.value = before + insert + after;
mentionTarget.focus();
var newPos = mentionPos + insert.length;
mentionTarget.setSelectionRange(newPos, newPos);
hideMention();
}
// ====== 发表评论 ======
if (commentSubmitBtn) {
commentSubmitBtn.addEventListener('click', function() {
var body = commentInput.value.trim();
if (!body) { alert('请输入评论内容'); return; }
commentSubmitBtn.disabled = true;
api('POST', '/api/posts/' + postId + '/comments', { body: body })
.then(function(d) {
commentSubmitBtn.disabled = false;
if (d.success) {
commentInput.value = '';
commentsList.innerHTML = '';
currentPage = 1;
loadComments(currentPage);
updateTotalCount(1);
} else {
alert(d.message || '发表失败');
}
})
.catch(function() {
commentSubmitBtn.disabled = false;
alert('请求失败');
});
});
bindMention(commentInput);
}
// ====== 加载更多 ======
loadMoreBtn.addEventListener('click', function() {
currentPage++;
loadComments(currentPage);
});
function updateTotalCount(delta) {
var el = commentsTotal;
var current = parseInt(el.textContent.replace(/[()]/g, '')) || 0;
el.textContent = '(' + (current + delta) + ')';
}
// ====== 图片上传 ======
var imageUploadInput = document.getElementById('commentImageInput');
var uploadLabel = document.getElementById('commentUploadLabel');
var uploadPreview = document.getElementById('commentUploadPreview');
if (imageUploadInput && uploadLabel) {
uploadLabel.addEventListener('click', function() {
imageUploadInput.click();
});
imageUploadInput.addEventListener('change', function() {
var file = imageUploadInput.files[0];
if (!file) return;
uploadLabel.style.pointerEvents = 'none';
uploadLabel.style.opacity = '0.5';
uploadLabel.textContent = '上传中...';
var formData = new FormData();
formData.append('file', file);
fetch('/api/comments/upload-image', {
method: 'POST',
headers: { 'X-CSRF-Token': getCSRFToken() },
body: formData
})
.then(function(r) { return r.json(); })
.then(function(d) {
uploadLabel.style.pointerEvents = 'auto';
uploadLabel.style.opacity = '1';
uploadLabel.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> 图片';
if (d.success && d.data && d.data.url) {
var imgUrl = '\n![](' + d.data.url + ')\n';
var input = commentInput;
var start = input.selectionStart;
var end = input.selectionEnd;
input.value = input.value.substring(0, start) + imgUrl + input.value.substring(end);
input.focus();
input.selectionStart = input.selectionEnd = start + imgUrl.length;
} else {
alert(d.message || '上传失败');
}
})
.catch(function() {
uploadLabel.style.pointerEvents = 'auto';
uploadLabel.style.opacity = '1';
uploadLabel.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> 图片';
alert('上传失败');
});
imageUploadInput.value = '';
});
}
// ====== 评论高亮 ======
function highlightComment(commentId) {
var card = commentsList.querySelector('.comment-card[data-id="' + commentId + '"]') ||
commentsList.querySelector('.comment-reply-card[data-id="' + commentId + '"]');
if (card) {
commentsList.insertBefore(card, commentsList.firstChild);
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
card.style.background = '#fff8e1';
card.style.transition = 'background 1.5s';
setTimeout(function() { card.style.background = ''; }, 2000);
return true;
}
return false;
}
var hash = window.location.hash;
var commentMatch = hash.match(/^#comment-(\d+)$/);
if (commentMatch) {
var targetCommentId = commentMatch[1];
var checkInterval = setInterval(function() {
if (highlightComment(targetCommentId)) {
clearInterval(checkInterval);
}
}, 300);
setTimeout(function() {
clearInterval(checkInterval);
if (!commentsList.querySelector('.comment-card[data-id="' + targetCommentId + '"]') &&
!commentsList.querySelector('.comment-reply-card[data-id="' + targetCommentId + '"]')) {
showToast('该评论已不存在');
}
}, 3000);
} else if (hash === '#comments') {
setTimeout(function() {
var section = document.getElementById('comments');
if (section) {
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 400);
}
// ====== 初始化 ======
loadComments(currentPage);
})();

View File

@ -0,0 +1,76 @@
// 赋能按钮 + 弹窗
(function() {
var btn = document.getElementById('energizeBtn');
var overlay = document.getElementById('energizeOverlay');
var closeBtn = document.getElementById('energizeClose');
var capHint = document.getElementById('energizeCapHint');
var lightBtn = document.getElementById('energizeLight');
var heavyBtn = document.getElementById('energizeHeavy');
if (!btn || !overlay) return;
var postId = btn.dataset.id;
function showOverlay() {
overlay.style.display = 'flex';
}
function hideOverlay() {
overlay.style.display = 'none';
capHint.style.display = 'none';
lightBtn.disabled = false;
heavyBtn.disabled = false;
}
btn.addEventListener('click', function() {
fetch('/api/energy/info')
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success && d.data && d.data.daily_exp_cap_reached) {
capHint.style.display = 'block';
}
showOverlay();
})
.catch(function() { showOverlay(); });
});
closeBtn.addEventListener('click', hideOverlay);
overlay.addEventListener('click', function(e) {
if (e.target === overlay) hideOverlay();
});
function doEnergize(amount) {
lightBtn.disabled = true;
heavyBtn.disabled = true;
fetch('/api/energy/energize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
},
body: JSON.stringify({ post_id: parseInt(postId), amount: amount })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
alert(d.data.message || '赋能成功!');
hideOverlay();
} else {
alert(d.message || '赋能失败');
lightBtn.disabled = false;
heavyBtn.disabled = false;
}
})
.catch(function() {
alert('请求失败');
lightBtn.disabled = false;
heavyBtn.disabled = false;
});
}
lightBtn.addEventListener('click', function() {
doEnergize(parseInt(lightBtn.dataset.amount));
});
heavyBtn.addEventListener('click', function() {
doEnergize(parseInt(heavyBtn.dataset.amount));
});
})();

View File

@ -0,0 +1,218 @@
// 收藏按钮(弹窗选择收藏夹)
(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 isFavorited = false;
var currentFolderId = null;
var loading = false;
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() {});
}
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">' + escapeHtml(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 (!getCSRFToken()) {
window.location.href = '/auth/login?redirect=/posts/' + postId;
return;
}
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 = 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() { 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 (!getCSRFToken()) { window.location.href = '/auth/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();
})();

View File

@ -0,0 +1,71 @@
// 赞/踩系统
(function() {
var container = document.getElementById('postReactions');
var likeBtn = document.getElementById('likeBtn');
var dislikeBtn = document.getElementById('dislikeBtn');
var likesCountEl = document.getElementById('likesCount');
if (!container || !likeBtn || !dislikeBtn) return;
var postId = container.dataset.postId;
// 页面加载时获取当前反应状态
fetch('/api/posts/' + postId + '/reaction')
.then(function(r) { return r.json(); })
.then(function(d) {
if (!d.success || !d.data) return;
updateUI(d.data.reaction, d.data.likes_count);
})
.catch(function() {});
function updateUI(reaction, count) {
likeBtn.classList.remove('active');
dislikeBtn.classList.remove('active');
if (reaction === 'liked') likeBtn.classList.add('active');
if (reaction === 'disliked') dislikeBtn.classList.add('active');
if (typeof count === 'number') likesCountEl.textContent = count;
}
likeBtn.addEventListener('click', function() {
likeBtn.disabled = true;
fetch('/api/posts/' + postId + '/like', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCSRFToken() }
})
.then(function(r) {
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; }
return r.json();
})
.then(function(d) {
likeBtn.disabled = false;
if (!d) return;
if (d.success) {
updateUI(d.data.liked ? 'liked' : 'none', d.data.likes_count);
} else {
alert(d.message || '操作失败');
}
})
.catch(function() { likeBtn.disabled = false; });
});
dislikeBtn.addEventListener('click', function() {
dislikeBtn.disabled = true;
fetch('/api/posts/' + postId + '/dislike', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCSRFToken() }
})
.then(function(r) {
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; }
return r.json();
})
.then(function(d) {
dislikeBtn.disabled = false;
if (!d) return;
if (d.success) {
updateUI(d.data.disliked ? 'disliked' : 'none');
} else {
alert(d.message || '操作失败');
}
})
.catch(function() { dislikeBtn.disabled = false; });
});
})();

View File

@ -0,0 +1,48 @@
// Vditor Markdown 渲染 + 外部链接处理 + 提交审核按钮
(function() {
// ====== Vditor 渲染 ======
var el = document.getElementById('postContent');
var mdEl = document.getElementById('postMdContent');
if (el && mdEl) {
var md = mdEl.value;
Vditor.preview(el, md, {
cdn: '/static/vditor',
theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
hljs: { style: 'github-dark', enable: true },
after: function() {
var host = window.location.host;
var links = el.querySelectorAll('a[href^="http"]');
for (var i = 0; i < links.length; i++) {
var a = links[i];
if (a.host && a.host !== host) {
a.setAttribute('rel', 'nofollow noopener noreferrer');
a.setAttribute('target', '_blank');
}
}
}
});
// shortcode 占位 div 替换为对应卡片 UI
renderShortcodes(el);
}
// ====== 提交审核按钮 ======
var submitBtn = document.getElementById('submitAuditBtn');
if (submitBtn) {
submitBtn.addEventListener('click', function() {
if (!confirm('确认提交审核?审核通过后将公开发布。')) return;
fetch('/api/posts/' + submitBtn.dataset.id + '/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
}
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) { window.location.reload(); }
else { alert(d.message || '提交失败'); }
})
.catch(function() { alert('请求失败'); });
});
}
})();