diff --git a/handlers/backend/comment.go b/handlers/backend/comment.go index 1f64519..204bd4a 100644 --- a/handlers/backend/comment.go +++ b/handlers/backend/comment.go @@ -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") diff --git a/routers/router.go b/routers/router.go index b305075..78d06b4 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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) diff --git a/static/css/admin.css b/static/css/admin.css index 8c1c846..3cef987 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -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; diff --git a/static/js/admin.js b/static/js/admin.js index b89a2fd..4b4c162 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -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 => ` ${c.author} ${c.content.substring(0, 50)}${c.content.length > 50 ? '...' : ''} ${c.post_title || '文章 #' + c.post_id} - ${c.status} + ${getStatusText(c.status)} ${new Date(c.created_at).toLocaleString()} ${c.status === 'pending' ? `` : ''} @@ -1076,11 +1088,28 @@ async function loadComments(postId = '') { `).join('') || '暂无评论'; + + 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(``); + for (let i = 1; i <= totalPages; i++) { + buttons.push(``); + } + buttons.push(``); + + box.innerHTML = buttons.join('') + `共 ${total} 条,第 ${commentsCurrentPage}/${totalPages} 页`; +} + // 显示分类表单 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 = ''; + (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 = ''; - 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); } // 加载基础设置 diff --git a/templates/admin/index.html b/templates/admin/index.html index f6a04a8..a372c6d 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -5,7 +5,7 @@ 后台管理 - GoBlog - +
@@ -58,7 +58,7 @@ {{template "admin_modal_password" .}} - + {{end}} @@ -353,9 +353,17 @@ {{end}}