Files
go_blog/templates/admin/comment_list.html
T
kevin a0061b14e3 feat: 评论接口 JSON 化——/api/article/:slug/comments 与 admin 审核操作
- comment.go:commentForm 加 json tag;PostComment 校验分支改 APIError
  (404 article_not_found、403 comments_disabled/comments_guests_disabled、
  400 校验码、500 article_error),保留 guest 令牌与 flash 机制,成功返回
  {ok,redirect:/article/:slug#comment-N,comment_id}
- api.go:新增 APIErrorf(支持 %d/%s 占位符键如 comments_too_long)
- admin_comment.go:approve/reject/delete 改 JSON(parseUintParam 拒绝非
  数值 id 400),成功带原 ?saved=1&msg= 查询串 redirect
- main.go:PostComment 迁入 /api;评论审核三操作迁入 /api/admin/comments
- article.html:评论表单改 blogAPI 提交,错误内联 commentError div
- comment_list.html:审核操作改 to commentAct() 委托(confirm 在函数内,
  取消不发请求),成功 reload 保持筛选状态
- 测试:security_test env 路由同步 /api;session_upload 评论用例改 JSON
- main_test 冒烟补评论 API 路由断言;go build/vet/test 全绿
2026-08-27 19:36:09 +08:00

114 lines
6.1 KiB
HTML

{{define "comment_list"}}
{{template "header" .}}
{{template "markdown_assets" .}}
<section class="max-w-6xl mx-auto px-4 py-12">
<div class="flex items-center justify-between mb-6">
<h2 class="text-3xl font-bold text-gray-900">{{index .Tr "admin_comments_title"}}</h2>
</div>
<!-- Status tabs -->
<div class="flex gap-2 mb-6 text-sm">
<a href="/admin/comments?status=pending"
class="px-4 py-2 rounded-lg font-medium {{if eq .Status "pending"}}bg-blue-600 text-white{{else}}bg-gray-200 text-gray-700 hover:bg-gray-300{{end}} transition-colors">
{{index .Tr "comment_status_pending"}}
{{if gt .PendingCount 0}}<span class="ml-1 inline-flex items-center justify-center bg-red-500 text-white text-xs rounded-full px-2">{{.PendingCount}}</span>{{end}}
</a>
<a href="/admin/comments?status=approved"
class="px-4 py-2 rounded-lg font-medium {{if eq .Status "approved"}}bg-blue-600 text-white{{else}}bg-gray-200 text-gray-700 hover:bg-gray-300{{end}} transition-colors">
{{index .Tr "comment_status_approved"}}
</a>
<a href="/admin/comments?status=rejected"
class="px-4 py-2 rounded-lg font-medium {{if eq .Status "rejected"}}bg-blue-600 text-white{{else}}bg-gray-200 text-gray-700 hover:bg-gray-300{{end}} transition-colors">
{{index .Tr "comment_status_rejected"}}
</a>
<a href="/admin/comments?status=all"
class="px-4 py-2 rounded-lg font-medium {{if eq .Status "all"}}bg-blue-600 text-white{{else}}bg-gray-200 text-gray-700 hover:bg-gray-300{{end}} transition-colors">
{{index .Tr "comment_status_all"}}
</a>
</div>
{{if .Success}}
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg mb-6">{{.Success}}</div>
{{end}}
<div class="space-y-4">
{{range .Comments}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div class="flex items-start gap-3">
{{if .GravatarURL}}
<img src="{{.GravatarURL}}" alt="" class="w-10 h-10 rounded-full bg-gray-100">
{{else}}
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold" style="background:{{.AvatarColor}}">{{.Initial}}</div>
{{end}}
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2 text-sm">
<span class="font-semibold text-gray-800">{{.AuthorName}}</span>
{{if .Website}}<a href="{{.Website}}" target="_blank" rel="nofollow ugc noopener" class="text-blue-500 hover:underline text-xs">{{.Website}}</a>{{end}}
<span class="text-gray-400 text-xs">{{.MaskedEmail}}</span>
<span class="text-gray-300">·</span>
<span class="text-gray-400 text-xs">{{.CreatedAt.Format "2006-01-02 15:04"}}</span>
{{if .IPAddress}}<span class="text-gray-400 text-xs">IP: {{.IPAddress}}</span>{{end}}
{{if .IsPrivate}}<span class="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded">{{index $.Tr "comments_private_badge"}}</span>{{end}}
<span class="text-xs {{.StatusBadge}} px-2 py-0.5 rounded">{{.StatusLabel}}</span>
</div>
{{if .ArticleTitle}}
<a href="/article/{{.ArticleSlug}}#comment-{{.ID}}" target="_blank" class="text-xs text-gray-500 hover:text-blue-600 mt-1 inline-block">
↗ {{.ArticleTitle}}
</a>
{{end}}
<div class="comment-body mt-2 text-sm text-gray-700 prose prose-sm max-w-none md-body" data-md="{{.Content}}"></div>
</div>
</div>
<div class="flex justify-end gap-3 mt-3 text-sm">
{{if eq .Status 0}}
<form class="comment-act inline" data-id="{{.ID}}" data-act="approve" onsubmit="return commentAct(this)">
<button type="submit" class="text-green-600 hover:text-green-800 font-medium cursor-pointer bg-transparent border-none">{{index $.Tr "comment_approve"}}</button>
</form>
{{end}}
{{if ne .Status 2}}
<form class="comment-act inline" data-id="{{.ID}}" data-act="reject" onsubmit="return commentAct(this)">
<button type="submit" class="text-orange-600 hover:text-orange-800 font-medium cursor-pointer bg-transparent border-none">{{index $.Tr "comment_reject"}}</button>
</form>
{{end}}
<form class="comment-act inline" data-id="{{.ID}}" data-act="delete"
data-confirm="{{index $.Tr "comment_delete_confirm"}}" onsubmit="return commentAct(this)">
<button type="submit" class="text-red-600 hover:text-red-800 font-medium cursor-pointer bg-transparent border-none">{{index $.Tr "comment_delete"}}</button>
</form>
</div>
</div>
{{else}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center text-gray-500">
{{index .Tr "comment_empty"}}
</div>
{{end}}
</div>
</section>
<script>
(function () {
document.querySelectorAll('.comment-body[data-md]').forEach(function (el) {
var raw = el.getAttribute('data-md') || '';
if (window.BlogMD) {
BlogMD.renderInto(el, raw);
} else {
el.textContent = raw;
}
});
})();
// 评论审核操作(approve/reject/delete)走 JSON API;成功 reload 保持筛选状态。
window.commentAct = function (form) {
var act = form.getAttribute('data-act');
var id = form.getAttribute('data-id');
var confirmText = form.getAttribute('data-confirm');
if (confirmText && !confirm(confirmText)) return false;
blogAPI('POST', '/api/admin/comments/' + id + '/' + act).then(function (r) {
if (r.ok) { window.location.reload(); }
else { alert(r.error || 'Failed'); }
});
return false;
};
</script>
{{template "footer" .}}
{{end}}