feat: 评论系统补充功能完成——图片上传、后台管理、通知跳转高亮

- 评论图片上传(LV4+,经验≥1500),JPEG/PNG 自动转 WebP,二进制搜索质量优化
- 后台评论管理页面,支持关键词搜索、分页、删除,侧边栏新增入口
- 通知跳转高亮:评论通知 RelatedID 改为 postID,消息页链接到 #comments,前端自动滚动
- 新增 adminCommentUseCase/commentStore 接口(遵循 ISP),AdminCommentRow 移至 model 层
- 前端 @mention 联想/图片上传光标插入/评论展开收起/内联回复交互完善
This commit is contained in:
2026-06-01 13:35:41 +08:00
parent b7acc91f8c
commit 36c571b6bb
17 changed files with 532 additions and 11 deletions

View File

@ -41,6 +41,8 @@
{{$link := ""}}
{{if or (eq $m.NotifyType "post_approved") (eq $m.NotifyType "post_rejected")}}
{{$link = printf "/posts/%d" (derefUint $m.RelatedID)}}
{{else if or (eq $m.NotifyType "comment") (eq $m.NotifyType "comment_reply")}}
{{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}}
{{else if or (eq $m.NotifyType "audit_approved") (eq $m.NotifyType "audit_rejected")}}
{{$link = "/settings"}}
{{end}}

View File

@ -80,7 +80,15 @@
{{if .IsLoggedIn}}
<div class="comment-form">
<textarea class="comment-input" id="commentInput" placeholder="写下你的评论..." rows="3" maxlength="5000"></textarea>
<button class="btn btn-primary comment-submit-btn" id="commentSubmitBtn">发表评论</button>
<div class="comment-form-actions">
<label class="comment-upload-btn" id="commentUploadLabel">
<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>
图片
</label>
<input type="file" id="commentImageInput" accept="image/*" style="display:none" />
<button class="btn btn-primary comment-submit-btn" id="commentSubmitBtn">发表评论</button>
</div>
<div class="comment-upload-preview" id="commentUploadPreview"></div>
</div>
{{else}}
<div class="comment-login-hint">
@ -640,6 +648,71 @@
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': csrfToken },
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 = '';
});
}
// ====== Hash 定位:从消息通知跳转时滚动到评论区 ======
if (window.location.hash === '#comments') {
setTimeout(function() {
var section = document.getElementById('comments');
if (section) {
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 400);
}
// ====== 初始化 ======
loadComments(currentPage);
})();