(function () { var root = document.querySelector("[data-play-composer]"); if (!root) return; var commentsRoot = document.querySelector("[data-play-comments]") || root.closest(".play-comments"); var input = root.querySelector("[data-comment-input]"); var countEl = root.querySelector("[data-comment-count]"); var sendBtn = root.querySelector("[data-comment-send]"); var listEl = document.querySelector("[data-comment-list]"); var totalEl = document.querySelector("[data-comment-total-label]"); var moreBtn = document.querySelector("[data-comment-more]"); var sending = false; var inlineSending = false; var loadingMore = false; var MAX_LEN = 200; var PAGE_SIZE = 4; var replyTarget = null; function toast(message, type) { if (typeof window.showToast === "function") { window.showToast(message, type); return; } if (window.AuthAPI && typeof window.AuthAPI.toast === "function") { window.AuthAPI.toast(message, type || "info"); return; } } function toInt(v, fallback) { var n = parseInt(v, 10); return isNaN(n) ? fallback || 0 : n; } function escapeHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function getPageSize() { return Math.max( 1, toInt(commentsRoot && commentsRoot.getAttribute("data-comment-size"), PAGE_SIZE) ); } function getPage() { return Math.max( 1, toInt(commentsRoot && commentsRoot.getAttribute("data-comment-page"), 1) ); } function getLoaded() { if (!listEl) return 0; return listEl.querySelectorAll(".play-comment:not(.play-comment--empty)").length; } function setPagerState(page, loaded, total) { if (!commentsRoot) return; if (page != null) commentsRoot.setAttribute("data-comment-page", String(page)); if (loaded != null) commentsRoot.setAttribute("data-comment-loaded", String(loaded)); if (total != null) commentsRoot.setAttribute("data-comment-total", String(total)); syncMoreButton(total != null ? total : toInt(commentsRoot.getAttribute("data-comment-total"), 0)); } function syncMoreButton(total) { if (!moreBtn) return; var loaded = getLoaded(); var hasMore = total > loaded; if (hasMore) { moreBtn.hidden = false; moreBtn.removeAttribute("hidden"); } else { moreBtn.hidden = true; moreBtn.setAttribute("hidden", ""); } } function syncCount() { if (!input || !countEl) return; var len = Array.from(input.value || "").length; countEl.textContent = len + " / " + MAX_LEN; } function formatTime(raw) { var s = String(raw || "").trim(); if (!s) return "刚刚"; var d = new Date(s); if (isNaN(d.getTime())) return s; return ( d.getFullYear() + "/" + (d.getMonth() + 1) + "/" + d.getDate() + " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0") ); } function renderCommentItem(item) { item = item || {}; var avatar = String(item.userAvatar || "").trim(); var name = escapeHtml(item.userName || "匿名用户"); var text = escapeHtml(item.text || ""); var time = escapeHtml(formatTime(item.createdAt)); var like = toInt(item.countLike, 0); var liked = Number(item.isLike) === 1; var avatarHtml = avatar ? '' : ''; var children = Array.isArray(item.children) ? item.children : []; var replies = children .map(function (child) { var cName = escapeHtml(child.userName || "用户"); var replyTo = child.replyUserName ? " 回复 " + escapeHtml(child.replyUserName) : ""; return ( '
' + cName + replyTo + ":" + escapeHtml(child.text || "") + "
" ); }) .join(""); var commentId = escapeHtml(item.id || ""); var rawName = String(item.userName || "匿名用户"); return ( '
  • ' + avatarHtml + '
    ' + '
    ' + '' + name + "" + '' + time + "" + "
    " + '

    ' + text + "

    " + replies + '
    ' + '" + '' + "
  • " ); } function getLoadedIds() { var ids = Object.create(null); if (!listEl) return ids; listEl.querySelectorAll(".play-comment[data-comment-id]").forEach(function (el) { var id = String(el.getAttribute("data-comment-id") || "").trim(); if (id) ids[id] = true; }); return ids; } function updateTotal(n) { if (!totalEl) return; totalEl.textContent = n > 0 ? n + " 条" : "暂无评论"; } function renderList(list, totalCount) { if (!listEl) return; list = Array.isArray(list) ? list : []; clearReplyTarget(); if (!list.length) { listEl.innerHTML = '
  • 暂无评论

  • '; } else { listEl.innerHTML = list.map(renderCommentItem).join(""); if (typeof window.initLazyImages === "function") { window.initLazyImages(listEl); } } var total = typeof totalCount === "number" ? totalCount : toInt(commentsRoot && commentsRoot.getAttribute("data-comment-total"), 0); if (typeof totalCount === "number") { updateTotal(totalCount); } setPagerState(1, list.length, total); } // 下一页追加到现有列表底部,不替换已展示评论。 function appendList(list, totalCount) { if (!listEl) return; list = Array.isArray(list) ? list : []; var total = typeof totalCount === "number" ? totalCount : toInt(commentsRoot && commentsRoot.getAttribute("data-comment-total"), 0); if (typeof totalCount === "number") { updateTotal(totalCount); } if (!list.length) { setPagerState(getPage(), getLoaded(), total); return; } var empty = listEl.querySelector(".play-comment--empty"); if (empty) empty.remove(); var existing = getLoadedIds(); var fresh = list.filter(function (item) { var id = String((item && item.id) || "").trim(); return id && !existing[id]; }); if (!fresh.length) { setPagerState(getPage(), getLoaded(), total); return; } var wrap = document.createElement("ul"); wrap.innerHTML = fresh.map(renderCommentItem).join(""); var nodes = []; while (wrap.firstChild) { nodes.push(wrap.firstChild); listEl.appendChild(wrap.firstChild); } if (typeof window.initLazyImages === "function") { nodes.forEach(function (node) { if (node && node.nodeType === 1) window.initLazyImages(node); }); window.initLazyImages(listEl); } setPagerState(getPage(), getLoaded(), total); } function fetchCommentPage(pageNum) { if (!window.HttpRequest || typeof window.HttpRequest.get !== "function") { return Promise.reject(new Error("评论服务未就绪")); } var objectId = toInt(root.getAttribute("data-object-id"), 0); var objectType = toInt(root.getAttribute("data-object-type"), 10); if (objectId <= 0) return Promise.reject(new Error("缺少评论对象")); var pageSize = getPageSize(); return window.HttpRequest.get("/api/comment/list", { params: { objectId: objectId, objectType: objectType, pageNum: pageNum, pageSize: pageSize, sortOrder: 1, }, }).then(function (res) { if (!res || Number(res.code) !== 200 || !res.data) { throw new Error((res && (res.tip || res.msg)) || "加载失败"); } return res.data; }); } function refreshList() { return fetchCommentPage(1) .then(function (data) { renderList(data.list || [], toInt(data.totalCount, 0)); }) .catch(function () {}); } function loadMoreComments() { if (loadingMore || !moreBtn) return; var nextPage = getPage() + 1; loadingMore = true; moreBtn.disabled = true; fetchCommentPage(nextPage) .then(function (data) { var list = data.list || []; var total = toInt(data.totalCount, 0); if (commentsRoot) { commentsRoot.setAttribute("data-comment-page", String(nextPage)); } // 只追加,绝不 renderList 整表替换 appendList(list, total); }) .catch(function (err) { toast((err && err.message) || "加载失败,请稍后重试", "error"); }) .finally(function () { loadingMore = false; if (moreBtn) moreBtn.disabled = false; }); } function removeInlineComposer() { if (!listEl) return; listEl.querySelectorAll("[data-comment-inline-composer]").forEach(function (el) { el.remove(); }); } function clearReplyTarget() { replyTarget = null; removeInlineComposer(); } function applyLikeUI(btn, isLike, countLike) { if (!btn) return; var liked = Number(isLike) === 1; btn.classList.toggle("is-active", liked); btn.setAttribute("data-liked", liked ? "1" : "2"); if (countLike != null) { btn.setAttribute("data-like-raw", String(countLike)); var countEl = btn.querySelector("[data-like-count]"); if (countEl) countEl.textContent = String(countLike); } } function toggleCommentLike(btn) { if (!btn || btn.getAttribute("data-liking") === "1") return; if (root.getAttribute("data-logged-in") !== "1") { var loginBtn = document.querySelector("[data-login-open]"); if (loginBtn) loginBtn.click(); return; } if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { toast("点赞服务未就绪", "error"); return; } var commentId = toInt(btn.getAttribute("data-comment-like"), 0); if (commentId <= 0) return; var likedNow = Number(btn.getAttribute("data-liked") || 2) === 1; var nextLike = !likedNow; var prevCount = toInt(btn.getAttribute("data-like-raw"), 0); var nextCount = Math.max(0, prevCount + (nextLike ? 1 : -1)); btn.setAttribute("data-liking", "1"); btn.disabled = true; applyLikeUI(btn, nextLike ? 1 : 2, nextCount); var payload = { commentId: commentId, isLike: nextLike }; window.HttpRequest.post("/api/comment/like/set", payload) .then(function (res) { if (!res || Number(res.code) !== 200) { throw new Error((res && (res.tip || res.msg)) || "操作失败"); } var data = res.data || {}; applyLikeUI( btn, data.isLike != null ? data.isLike : nextLike ? 1 : 2, data.countLike != null ? data.countLike : nextCount ); }) .catch(function (err) { applyLikeUI(btn, likedNow ? 1 : 2, prevCount); toast((err && err.message) || "操作失败,请稍后重试", "error"); }) .finally(function () { btn.removeAttribute("data-liking"); btn.disabled = false; }); } function openInlineComposer(commentEl, target) { if (!commentEl || !target) return; removeInlineComposer(); replyTarget = target; var body = commentEl.querySelector(".play-comment__body") || commentEl; var actions = body.querySelector(".play-comment__actions"); var box = document.createElement("div"); box.className = "play-comment__inline"; box.setAttribute("data-comment-inline-composer", ""); var name = target.replyName || "用户"; box.innerHTML = '
    ' + "回复 @" + escapeHtml(name) + "" + '' + "
    " + '
    ' + '' + '' + "
    "; if (actions && actions.parentNode === body) { if (actions.nextSibling) body.insertBefore(box, actions.nextSibling); else body.appendChild(box); } else { body.appendChild(box); } var inlineInput = box.querySelector("[data-comment-inline-input]"); if (inlineInput) inlineInput.focus(); } function setSending(on) { sending = !!on; if (sendBtn) sendBtn.disabled = !!on; if (input) input.disabled = !!on; } function submitTopComment() { if (sending) return; if (root.getAttribute("data-logged-in") !== "1") { var loginBtn = document.querySelector("[data-login-open]"); if (loginBtn) loginBtn.click(); return; } if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { toast("评论服务未就绪", "error"); return; } var text = String((input && input.value) || "").trim(); if (!text) { toast("请输入评论内容", "error"); return; } if (Array.from(text).length > MAX_LEN) { toast("评论最多 200 字", "error"); return; } var objectId = toInt(root.getAttribute("data-object-id"), 0); var objectType = toInt(root.getAttribute("data-object-type"), 10); if (objectId <= 0) { toast("缺少评论对象", "error"); return; } var payload = { objectId: objectId, objectType: objectType, text: text, parentsId: 0, replyCommentId: 0, }; setSending(true); window.HttpRequest.post("/api/comment/send", payload) .then(function (res) { if (!res || Number(res.code) !== 200) { throw new Error((res && (res.tip || res.msg)) || "发表失败"); } if (input) input.value = ""; syncCount(); clearReplyTarget(); toast("发布成功,评论审核后才会展示", "success"); return refreshList(); }) .catch(function (err) { toast((err && err.message) || "发表失败,请稍后重试", "error"); }) .finally(function () { setSending(false); }); } function sendInlineReply(box) { if (!box || inlineSending || !replyTarget) return; if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { toast("评论服务未就绪", "error"); return; } var inlineInput = box.querySelector("[data-comment-inline-input]"); var btn = box.querySelector("[data-comment-inline-send]"); if (!inlineInput) return; var text = String(inlineInput.value || "").trim(); if (!text) { toast("请输入评论内容", "error"); return; } if (Array.from(text).length > MAX_LEN) { toast("评论最多 200 字", "error"); return; } var objectId = toInt(root.getAttribute("data-object-id"), 0); var objectType = toInt(root.getAttribute("data-object-type"), 10); if (objectId <= 0) { toast("缺少评论对象", "error"); return; } inlineSending = true; if (btn) btn.disabled = true; inlineInput.disabled = true; var payload = { objectId: objectId, objectType: objectType, text: text, parentsId: toInt(replyTarget.parentsId, 0), replyCommentId: toInt( replyTarget.replyCommentId || replyTarget.parentsId, 0 ), }; window.HttpRequest.post("/api/comment/send", payload) .then(function (res) { if (!res || Number(res.code) !== 200) { throw new Error((res && (res.tip || res.msg)) || "发表失败"); } clearReplyTarget(); toast("发布成功,评论审核后才会展示", "success"); return refreshList(); }) .catch(function (err) { toast((err && err.message) || "发表失败,请稍后重试", "error"); }) .finally(function () { inlineSending = false; if (btn) btn.disabled = false; if (inlineInput) inlineInput.disabled = false; }); } if (input) { input.addEventListener("input", syncCount); input.addEventListener("keydown", function (event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); submitTopComment(); } }); syncCount(); } if (sendBtn) { sendBtn.addEventListener("click", submitTopComment); } if (moreBtn) { moreBtn.addEventListener("click", function (event) { event.preventDefault(); loadMoreComments(); }); } if (listEl) { listEl.addEventListener("click", function (event) { var likeBtn = window.domClosest(event.target, "[data-comment-like]"); if (likeBtn && listEl.contains(likeBtn)) { event.preventDefault(); toggleCommentLike(likeBtn); return; } var cancelBtn = window.domClosest(event.target, "[data-comment-reply-cancel]"); if (cancelBtn && listEl.contains(cancelBtn)) { clearReplyTarget(); return; } var sendInlineBtn = window.domClosest(event.target, "[data-comment-inline-send]"); if (sendInlineBtn && listEl.contains(sendInlineBtn)) { var box = sendInlineBtn.closest("[data-comment-inline-composer]"); sendInlineReply(box); return; } var replyBtn = window.domClosest(event.target, "[data-comment-reply]"); if (!replyBtn || !listEl.contains(replyBtn)) return; if (root.getAttribute("data-logged-in") !== "1") { var loginBtn = document.querySelector("[data-login-open]"); if (loginBtn) loginBtn.click(); return; } var parentsId = toInt(replyBtn.getAttribute("data-parents-id"), 0); var commentId = toInt(replyBtn.getAttribute("data-comment-id"), parentsId); if (parentsId <= 0) return; var commentEl = replyBtn.closest(".play-comment"); openInlineComposer(commentEl, { parentsId: parentsId, replyCommentId: commentId, replyName: replyBtn.getAttribute("data-reply-name") || "用户", }); }); listEl.addEventListener("keydown", function (event) { if (event.key !== "Enter" || event.shiftKey) return; var inlineInput = window.domClosest(event.target, "[data-comment-inline-input]"); if (!inlineInput || !listEl.contains(inlineInput)) return; event.preventDefault(); var box = inlineInput.closest("[data-comment-inline-composer]"); sendInlineReply(box); }); } })();