import { computed, reactive } from 'vue' import { readerApi } from '../api' // 一篇文章的评论区状态。 // 刻意做成「每个文章实例化一份」而不是像 site.js 那样的单例 —— // 换文章必须重新来过,单例会在路由切换时残留上一条的数据。 // // 后端契约(见 README / 方案文档): // GET /api/comments?post_id=&sort=&page=&size= // -> { items:[Comment], total, page, pages, has_more } // Comment = { id, parent_id, root_id, body_html, state, created_at, edited_at, // is_deleted, reply_count, replies:[Comment], author:{...} } // total / has_more 只统计顶层评论;回复挂在各自的 replies 里。 const PAGE_SIZE = 10 export function useComments(postId) { const s = reactive({ items: [], total: 0, page: 0, sort: 'default', // default | newest | oldest loading: false, loadingMore: false, error: '', replyTo: null, // 正在回复的那条(顶层或回复都可能是) editing: null, // 正在编辑的那条 submitting: false }) const remaining = computed(() => Math.max(0, s.total - s.items.length)) async function load(reset = true) { if (s.loading || s.loadingMore) return if (reset) { s.loading = true s.error = '' } else { s.loadingMore = true } try { const page = reset ? 1 : s.page + 1 const data = await readerApi.comments(postId, { sort: s.sort, page, size: PAGE_SIZE }) const items = (data && data.items) || [] s.items = reset ? items : s.items.concat(items) s.page = page s.total = (data && data.total) || 0 } catch (e) { s.error = e.message || '评论加载失败' } finally { s.loading = false s.loadingMore = false } } function setSort(sort) { if (sort === s.sort) return s.sort = sort load(true) } // SSE 触发的静默刷新:回到第一页按当前排序重拉,不亮加载态—— // 别人发了新评论时页面只是「悄悄长出」新内容,不该闪一下。 // 返回 true 表示列表真的变了(顶层条数有差异)。 async function refresh() { try { const data = await readerApi.comments(postId, { sort: s.sort, page: 1, size: PAGE_SIZE }) if (s.loading || s.loadingMore || s.replyTo || s.editing) return false const items = (data && data.items) || [] const changed = (data && data.total) !== s.total || items.some((c, i) => !s.items[i] || s.items[i].id !== c.id || s.items[i].edited_at !== c.edited_at) s.items = items s.page = 1 s.total = (data && data.total) || 0 return changed } catch { return false } } // 发表 / 回复。成功后直接插进列表,不整页重拉 —— 位置按当前排序决定, // 用户刚发的东西必须马上看得见。 async function submit(bodyMd, parent = null) { s.submitting = true s.error = '' try { const created = await readerApi.create(postId, bodyMd, parent ? parent.id : 0) s.total += 1 if (parent) { const root = rootOf(parent) if (root) { root.replies = (root.replies || []).concat(created) root.reply_count = (root.reply_count || 0) + 1 } } else if (s.sort === 'oldest') { s.items.push(created) } else { s.items.unshift(created) } return created } catch (e) { s.error = e.message || '发表失败' return null } finally { s.submitting = false } } async function saveEdit(comment, bodyMd) { s.submitting = true s.error = '' try { const updated = await readerApi.edit(comment.id, bodyMd) // 后端会把新的 body_html / edited_at 一起回来,就地替换 comment.body_html = updated.body_html comment.edited_at = updated.edited_at s.editing = null return true } catch (e) { s.error = e.message || '保存失败' return false } finally { s.submitting = false } } async function remove(comment) { s.error = '' try { await readerApi.remove(comment.id) comment.is_deleted = true comment.body_html = '' // 顶层评论软删后总数要减;回复只改自身 if (!comment.parent_id) s.total = Math.max(0, s.total - 1) else { const root = rootOf(comment) if (root) root.reply_count = Math.max(0, (root.reply_count || 0) - 1) } return true } catch (e) { s.error = e.message || '删除失败' return false } } // 回复折叠时按需拉取(GET /api/comments/{rootId}/thread) async function loadMoreReplies(root) { if (root.__threadLoading) return root.__threadLoading = true try { const data = await readerApi.thread(root.id, root.__cursor || '') const more = (data && data.items) || [] root.replies = (root.replies || []).concat(more) root.__cursor = (data && data.cursor) || '' root.reply_count = (data && data.reply_count) || (root.replies || []).length } catch (e) { s.error = e.message || '回复加载失败' } finally { root.__threadLoading = false } } // 顶层评论列表里找出某条回复所属的根 function rootOf(comment) { if (comment.root_id) { const byRoot = s.items.find((c) => c.id === comment.root_id) if (byRoot) return byRoot } return s.items.find((c) => (c.replies || []).some((r) => r.id === comment.id)) || null } function startReply(comment) { s.editing = null s.replyTo = comment } function startEdit(comment) { s.replyTo = null s.editing = comment } function cancel() { s.replyTo = null s.editing = null } return { s, remaining, load, refresh, setSort, submit, saveEdit, remove, loadMoreReplies, startReply, startEdit, cancel } }