评论分页
This commit is contained in:
+48
-15
@@ -17,15 +17,15 @@ func GetComments(c *gin.Context) {
|
||||
postSlug := c.Query("post_slug") // 前台使用 slug
|
||||
status := c.Query("status")
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "1000")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "10")
|
||||
|
||||
// 解析分页参数
|
||||
page := 1
|
||||
pageSize := 1000
|
||||
pageSize := 10
|
||||
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||
page = p
|
||||
}
|
||||
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 1000 {
|
||||
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 10 {
|
||||
pageSize = ps
|
||||
}
|
||||
|
||||
@@ -34,22 +34,20 @@ func GetComments(c *gin.Context) {
|
||||
return db.Select("id", "title", "slug")
|
||||
})
|
||||
|
||||
// 非管理员只能查看自己文章上的评论(后台)或已审核的评论(前台)
|
||||
if isAdmin(c) {
|
||||
// 管理员可以查看所有评论
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
} else {
|
||||
// 状态过滤(管理员和普通用户均适用)
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 非管理员只能查看自己文章上的评论
|
||||
if !isAdmin(c) {
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
// 如果是后台接口调用,限制为自己的文章
|
||||
// 未指定具体文章时,限制为自己名下的文章
|
||||
if postID == "" && postSlug == "" {
|
||||
db = db.Where("post_id IN (?)",
|
||||
database.DB.Model(&models.Post{}).Select("id").Where("author_id = ?", userID))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 按文章 ID 或 slug 过滤
|
||||
@@ -64,14 +62,16 @@ func GetComments(c *gin.Context) {
|
||||
db = db.Where("post_id = ?", post.ID)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
// 只统计并查询顶级评论(parent_id IS NULL),子评论在 Preload 中加载
|
||||
// Count 必须与列表使用相同过滤条件,否则分页总数会包含子评论而不准确
|
||||
db = db.Where("parent_id IS NULL")
|
||||
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
// 只查询顶级评论(parent_id IS NULL),子评论在 Preload 中加载
|
||||
var comments []models.Comment
|
||||
offset := (page - 1) * pageSize
|
||||
db.Where("parent_id IS NULL").Order("created_at ASC").Offset(offset).Limit(pageSize).Find(&comments)
|
||||
db.Order("created_at ASC").Offset(offset).Limit(pageSize).Find(&comments)
|
||||
|
||||
// 预加载子评论
|
||||
for i := range comments {
|
||||
@@ -102,6 +102,39 @@ func GetComments(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetCommentPosts 获取所有有评论的文章列表(用于评论管理页的文章筛选下拉框)
|
||||
// 与评论列表分页无关,始终返回权限范围内的全部文章
|
||||
func GetCommentPosts(c *gin.Context) {
|
||||
// 有评论的文章 ID 子查询
|
||||
postIDsWithComments := database.DB.Model(&models.Comment{}).Select("DISTINCT post_id")
|
||||
|
||||
query := database.DB.Model(&models.Post{}).
|
||||
Select("id", "title", "slug").
|
||||
Where("id IN (?)", postIDsWithComments)
|
||||
|
||||
// 非管理员只能看到自己名下的文章
|
||||
if !isAdmin(c) {
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
query = query.Where("author_id = ?", userID)
|
||||
}
|
||||
}
|
||||
|
||||
var posts []models.Post
|
||||
query.Order("id DESC").Find(&posts)
|
||||
|
||||
type postOption struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
list := make([]postOption, 0, len(posts))
|
||||
for _, p := range posts {
|
||||
list = append(list, postOption{ID: p.ID, Title: p.Title, Slug: p.Slug})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": list})
|
||||
}
|
||||
|
||||
// loadAndVerifyComment 加载评论并校验权限,成功返回评论,失败直接写响应并返回 nil
|
||||
func loadAndVerifyComment(c *gin.Context) *models.Comment {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -81,6 +81,7 @@ func setupAuthenticatedAPIRoutes(api *gin.RouterGroup) {
|
||||
|
||||
// 评论管理(查看所有评论,需登录)
|
||||
auth.GET("/comments", backend.GetComments)
|
||||
auth.GET("/comments/posts", backend.GetCommentPosts) // 有评论的文章列表(筛选下拉框)
|
||||
auth.PUT("/comments/:id/approve", backend.ApproveComment)
|
||||
auth.PUT("/comments/:id/spam", backend.MarkSpamComment)
|
||||
auth.DELETE("/comments/:id", backend.DeleteComment)
|
||||
|
||||
@@ -1092,6 +1092,22 @@ a.stat-card:hover {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 评论管理页顶部筛选区(文章 + 状态) */
|
||||
.comment-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.comment-status-filter {
|
||||
width: 140px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 基础设置表单 */
|
||||
.settings-form {
|
||||
max-width: 560px;
|
||||
|
||||
+72
-31
@@ -272,6 +272,7 @@ function showPage(pageName) {
|
||||
loadPages();
|
||||
break;
|
||||
case 'comments':
|
||||
loadCommentPostFilter();
|
||||
loadComments();
|
||||
break;
|
||||
case 'users':
|
||||
@@ -1050,25 +1051,36 @@ async function loadPages() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载评论(postId 为空时加载全部)
|
||||
async function loadComments(postId = '') {
|
||||
// 评论列表分页与筛选状态(每页 10 条)
|
||||
const COMMENTS_PAGE_SIZE = 10;
|
||||
let commentsCurrentPage = 1;
|
||||
let commentsCurrentPostId = ''; // 当前按文章筛选的 post_id
|
||||
let commentsCurrentStatus = ''; // 当前按状态筛选的 status
|
||||
|
||||
// 加载评论列表(分页 + 文章/状态筛选)
|
||||
async function loadComments(page = commentsCurrentPage) {
|
||||
try {
|
||||
const url = postId ? `${API_BASE}/comments?post_id=${postId}` : `${API_BASE}/comments`;
|
||||
const res = await fetch(url, { headers: getHeaders() });
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', page);
|
||||
params.set('page_size', COMMENTS_PAGE_SIZE);
|
||||
if (commentsCurrentPostId) params.set('post_id', commentsCurrentPostId);
|
||||
if (commentsCurrentStatus) params.set('status', commentsCurrentStatus);
|
||||
|
||||
const res = await fetch(`${API_BASE}/comments?${params.toString()}`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
|
||||
// 加载全部评论时刷新文章筛选下拉框
|
||||
if (!postId) {
|
||||
populateCommentPostFilter(data.data || []);
|
||||
}
|
||||
|
||||
const list = data.data || [];
|
||||
const total = data.total || 0;
|
||||
const totalPages = Math.max(1, data.total_pages || Math.ceil(total / COMMENTS_PAGE_SIZE));
|
||||
commentsCurrentPage = Math.min(Math.max(1, data.page || page), totalPages);
|
||||
|
||||
const tbody = document.getElementById('comments-list');
|
||||
tbody.innerHTML = data.data?.map(c => `
|
||||
tbody.innerHTML = list.map(c => `
|
||||
<tr>
|
||||
<td>${c.author}</td>
|
||||
<td>${c.content.substring(0, 50)}${c.content.length > 50 ? '...' : ''}</td>
|
||||
<td><a href="/post/${encodeURIComponent(c.post_slug || c.post_id)}" target="_blank">${c.post_title || '文章 #' + c.post_id}</a></td>
|
||||
<td><span class="status-badge status-${c.status}">${c.status}</span></td>
|
||||
<td><span class="status-badge status-${c.status}">${getStatusText(c.status)}</span></td>
|
||||
<td>${new Date(c.created_at).toLocaleString()}</td>
|
||||
<td>
|
||||
${c.status === 'pending' ? `<button class="btn-success" onclick="approveComment(${c.id})">通过</button>` : ''}
|
||||
@@ -1076,11 +1088,28 @@ async function loadComments(postId = '') {
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="6" style="text-align:center">暂无评论</td></tr>';
|
||||
|
||||
renderCommentsPagination(total, totalPages);
|
||||
} catch (err) {
|
||||
console.error('加载评论失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染评论列表分页条
|
||||
function renderCommentsPagination(total, totalPages) {
|
||||
const box = document.getElementById('comments-pagination');
|
||||
if (!box) return;
|
||||
|
||||
const buttons = [];
|
||||
buttons.push(`<button ${commentsCurrentPage <= 1 ? 'disabled' : ''} onclick="loadComments(${commentsCurrentPage - 1})">上一页</button>`);
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
buttons.push(`<button class="${i === commentsCurrentPage ? 'active' : ''}" onclick="loadComments(${i})">${i}</button>`);
|
||||
}
|
||||
buttons.push(`<button ${commentsCurrentPage >= totalPages ? 'disabled' : ''} onclick="loadComments(${commentsCurrentPage + 1})">下一页</button>`);
|
||||
|
||||
box.innerHTML = buttons.join('') + `<span class="pagination-info">共 ${total} 条,第 ${commentsCurrentPage}/${totalPages} 页</span>`;
|
||||
}
|
||||
|
||||
// 显示分类表单
|
||||
function showCategoryForm() {
|
||||
showModal('category', '新建分类');
|
||||
@@ -1280,7 +1309,9 @@ function getStatusText(status) {
|
||||
'published': '已发布',
|
||||
'draft': '草稿',
|
||||
'private': '私密',
|
||||
'pending': '待审核'
|
||||
'pending': '待审核',
|
||||
'approved': '已通过',
|
||||
'spam': '垃圾'
|
||||
};
|
||||
return map[status] || status;
|
||||
}
|
||||
@@ -1322,33 +1353,43 @@ async function loadUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
// 填充评论筛选下拉框(所有有评论的文章)
|
||||
function populateCommentPostFilter(comments) {
|
||||
// 加载评论筛选下拉框(所有有评论的文章,来自后端接口而非当前页)
|
||||
async function loadCommentPostFilter() {
|
||||
const select = document.getElementById('comment-post-filter');
|
||||
if (!select) return;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/comments/posts`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
|
||||
// 按文章去重
|
||||
const posts = new Map();
|
||||
comments.forEach(c => {
|
||||
if (!posts.has(c.post_id)) {
|
||||
posts.set(c.post_id, c.post_title || ('文章 #' + c.post_id));
|
||||
select.innerHTML = '<option value="">全部文章</option>';
|
||||
(data.data || []).forEach(p => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.title;
|
||||
opt.title = p.title; // 悬停显示完整标题
|
||||
select.appendChild(opt);
|
||||
});
|
||||
// 恢复当前选中的文章;若已不存在则重置为全部
|
||||
select.value = commentsCurrentPostId;
|
||||
if (select.value !== commentsCurrentPostId) {
|
||||
commentsCurrentPostId = '';
|
||||
select.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
select.innerHTML = '<option value="">全部文章</option>';
|
||||
posts.forEach((title, id) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = id;
|
||||
opt.textContent = title;
|
||||
opt.title = title; // 悬停显示完整标题
|
||||
select.appendChild(opt);
|
||||
});
|
||||
select.value = '';
|
||||
} catch (err) {
|
||||
console.error('加载文章筛选失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 按选中文章过滤评论列表
|
||||
function filterCommentsByPost() {
|
||||
loadComments(document.getElementById('comment-post-filter').value);
|
||||
commentsCurrentPostId = document.getElementById('comment-post-filter').value;
|
||||
loadComments(1);
|
||||
}
|
||||
|
||||
// 按选中状态过滤评论列表
|
||||
function filterCommentsByStatus() {
|
||||
commentsCurrentStatus = document.getElementById('comment-status-filter').value;
|
||||
loadComments(1);
|
||||
}
|
||||
|
||||
// 加载基础设置
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>后台管理 - GoBlog</title>
|
||||
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/static/css/admin.css?v=4">
|
||||
<link rel="stylesheet" href="/static/css/admin.css?v=5">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-header">
|
||||
@@ -58,7 +58,7 @@
|
||||
{{template "admin_modal_password" .}}
|
||||
|
||||
<script>window.__currentUser = {{.CurrentUser }}</script>
|
||||
<script src="/static/js/admin.js?v=22"></script>
|
||||
<script src="/static/js/admin.js?v=23"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -353,9 +353,17 @@
|
||||
<div id="comments-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1>评论管理</h1>
|
||||
<select id="comment-post-filter" class="comment-post-filter" onchange="filterCommentsByPost()">
|
||||
<option value="">全部文章</option>
|
||||
</select>
|
||||
<div class="comment-filters">
|
||||
<select id="comment-post-filter" class="comment-post-filter" onchange="filterCommentsByPost()">
|
||||
<option value="">全部文章</option>
|
||||
</select>
|
||||
<select id="comment-status-filter" class="comment-status-filter" onchange="filterCommentsByStatus()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待审核</option>
|
||||
<option value="approved">已通过</option>
|
||||
<option value="spam">垃圾</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
@@ -370,6 +378,7 @@
|
||||
</thead>
|
||||
<tbody id="comments-list"></tbody>
|
||||
</table>
|
||||
<div id="comments-pagination" class="pagination"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user