600 lines
20 KiB
JavaScript
600 lines
20 KiB
JavaScript
(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, ">")
|
||
.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
|
||
? '<img class="play-comment__avatar lazy" src="/static/images/placeholder.png?v=2" data-src="' +
|
||
escapeHtml(avatar) +
|
||
'" alt="" loading="lazy" decoding="async">'
|
||
: '<img class="play-comment__avatar" src="/static/images/placeholder.png?v=2" data-src="/static/images/profile/avatar.png" alt="" loading="lazy" decoding="async">';
|
||
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 (
|
||
'<div class="play-comment__reply">' +
|
||
cName +
|
||
replyTo +
|
||
":" +
|
||
escapeHtml(child.text || "") +
|
||
"</div>"
|
||
);
|
||
})
|
||
.join("");
|
||
var commentId = escapeHtml(item.id || "");
|
||
var rawName = String(item.userName || "匿名用户");
|
||
return (
|
||
'<li class="play-comment" data-comment-id="' +
|
||
commentId +
|
||
'">' +
|
||
avatarHtml +
|
||
'<div class="play-comment__body">' +
|
||
'<div class="play-comment__meta">' +
|
||
'<span class="play-comment__name">' +
|
||
name +
|
||
"</span>" +
|
||
'<span class="play-comment__time">' +
|
||
time +
|
||
"</span>" +
|
||
"</div>" +
|
||
'<p class="play-comment__text">' +
|
||
text +
|
||
"</p>" +
|
||
replies +
|
||
'<div class="play-comment__actions">' +
|
||
'<button class="play-comment__action' +
|
||
(liked ? " is-active" : "") +
|
||
'" type="button" aria-label="点赞" data-comment-like="' +
|
||
commentId +
|
||
'" data-liked="' +
|
||
(liked ? "1" : "2") +
|
||
'" data-like-raw="' +
|
||
escapeHtml(like) +
|
||
'">' +
|
||
'<img class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/heart.svg" alt="" loading="lazy" decoding="async">' +
|
||
'<span data-like-count>' +
|
||
escapeHtml(like) +
|
||
"</span>" +
|
||
"</button>" +
|
||
'<button class="play-comment__action" type="button" data-comment-reply data-comment-id="' +
|
||
commentId +
|
||
'" data-parents-id="' +
|
||
commentId +
|
||
'" data-reply-name="' +
|
||
escapeHtml(rawName) +
|
||
'">回复</button>' +
|
||
"</div></div></li>"
|
||
);
|
||
}
|
||
|
||
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 =
|
||
'<li class="play-comment play-comment--empty"><p class="play-comment__text">暂无评论</p></li>';
|
||
} 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 =
|
||
'<div class="play-comment__inline-hint">' +
|
||
"<span>回复 @" +
|
||
escapeHtml(name) +
|
||
"</span>" +
|
||
'<button type="button" data-comment-reply-cancel>取消</button>' +
|
||
"</div>" +
|
||
'<div class="play-comment__inline-row">' +
|
||
'<input class="play-comment__inline-input" type="text" maxlength="200" placeholder="回复 @' +
|
||
escapeHtml(name) +
|
||
'…" data-comment-inline-input autocomplete="off" />' +
|
||
'<button class="play-comment__inline-send" type="button" data-comment-inline-send>发送</button>' +
|
||
"</div>";
|
||
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);
|
||
});
|
||
}
|
||
})();
|