fix(single): align article like/favorite interaction UX with reference
The like and favorite buttons gave no in-flight feedback: the label just sat there until the request returned, the lovestar animation never replayed on a repeat like, duplicate clicks reported "success", and guest favorites silently wrote a cookie instead of asking the user to sign in. - pending state: replace the count with the reference inline spinner (`<i class="loading zts">`) and disable the button until the response lands - animation: strip `action-animation` before re-adding it (forced reflow), so every successful like replays the reference lovestar keyframes - duplicate: when the server reports the same state and count, skip the animation and surface the reference warning copy (已赞过此文章了!) - failures: restore the original count and toast `danger`; guest-only actions now open the site login modal instead of dead-ending on an error - favorite requires login server-side, matching the reference; the guest favourite cookie branch and its reader are removed as dead code - unify like/favorite through one bindToggle path and keep every mirror button (article row + footer tabbar) in sync Tests: new article-actions-ux suite (8 cases) drives assets/js/core/article.js against a DOM/fetch shim — pending spinner, replay, duplicate no-op, concurrency, rollback, guest login, mirror sync. Widget suite 828/829; the one failure (frontend-lifecycle theme-init stub) predates this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3c07ef7a8d
commit
93e83e1835
7 files changed
+470
-92
No files matched your search
+103
-76
@@ -163,16 +163,55 @@
|
||||
});
|
||||
}).then(function (payload) {
|
||||
if (!payload || !payload.success) {
|
||||
var message = payload && payload.data && payload.data.message ? payload.data.message : "请求失败";
|
||||
throw new Error(message);
|
||||
var data = (payload && payload.data) || {};
|
||||
var error = new Error(data.message || "请求失败");
|
||||
// 参考主题:游客操作受限时直接拉起登录弹窗,而不是只弹一条错误提示。
|
||||
error.needsLogin = !!data.login;
|
||||
throw error;
|
||||
}
|
||||
return payload.data || {};
|
||||
});
|
||||
}
|
||||
|
||||
function setButtonBusy(button, busy) {
|
||||
button.disabled = !!busy;
|
||||
button.classList.toggle("is-loading", !!busy);
|
||||
/** 游客被拦下时,复用站点的登录弹窗触发器。 */
|
||||
function openLoginModal() {
|
||||
var trigger = document.querySelector(".signin-loader") ||
|
||||
document.querySelector("[data-jio-auth-required]") ||
|
||||
document.querySelector("#nav-login-btn");
|
||||
if (trigger) {
|
||||
trigger.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 参考主题的待发送态:计数被行内加载点替换,按钮不可重复提交。
|
||||
* @returns {Function} 还原计数内容的函数
|
||||
*/
|
||||
function showPending(button) {
|
||||
var counts = button.querySelectorAll("count");
|
||||
var previous = [];
|
||||
for (var i = 0; i < counts.length; i++) {
|
||||
previous.push(counts[i].innerHTML);
|
||||
counts[i].innerHTML = '<i class="loading zts"></i>';
|
||||
}
|
||||
button.disabled = true;
|
||||
return function restore() {
|
||||
for (var j = 0; j < counts.length; j++) {
|
||||
if (previous[j] === undefined) continue;
|
||||
counts[j].innerHTML = previous[j];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 参考主题只在点亮时播放 lovestar;先摘类再于下一帧加回,重复点赞也会重放。 */
|
||||
function paintActive(button, isActive, animate) {
|
||||
button.dataset.initialActive = isActive ? "1" : "0";
|
||||
button.classList.toggle("actived", isActive);
|
||||
button.classList.remove("action-animation");
|
||||
if (!isActive || !animate) return;
|
||||
// 读取 offsetWidth 强制重排,使动画类移除→添加之间重新触发关键帧。
|
||||
void button.offsetWidth;
|
||||
button.classList.add("action-animation");
|
||||
}
|
||||
|
||||
function findInScope(scope, id) {
|
||||
@@ -219,87 +258,75 @@
|
||||
var likeBtns = collectActionButtons(scope, ".action-like[data-post-id]");
|
||||
var favBtns = collectActionButtons(scope, ".action-favorite[data-post-id]");
|
||||
|
||||
function paintLike(btn, isActive, animate) {
|
||||
btn.dataset.initialActive = isActive ? "1" : "0";
|
||||
// actived 为参考主题的选中态类名;action-animation 只在点亮瞬间附加,
|
||||
// 触发 main.css 中的 lovestar 关键帧。
|
||||
btn.classList.toggle("actived", isActive);
|
||||
btn.classList.toggle("action-animation", !!(isActive && animate));
|
||||
}
|
||||
|
||||
function paintFavorite(btn, isActive, animate) {
|
||||
btn.dataset.initialActive = isActive ? "1" : "0";
|
||||
btn.classList.toggle("actived", isActive);
|
||||
btn.classList.toggle("action-animation", !!(isActive && animate));
|
||||
}
|
||||
|
||||
function applyLikeState(isActive, count, animate) {
|
||||
for (var i = 0; i < likeBtns.length; i++) paintLike(likeBtns[i], isActive, animate);
|
||||
function applyState(buttons, statName, isActive, count, animate) {
|
||||
for (var i = 0; i < buttons.length; i++) paintActive(buttons[i], isActive, animate);
|
||||
if ("number" === typeof count && !isNaN(count)) {
|
||||
setBaseStatCount("likes", count);
|
||||
setBaseStatCount(statName, count);
|
||||
} else {
|
||||
updateStatCounts("likes", getBaseStatCount("likes"));
|
||||
updateStatCounts(statName, getBaseStatCount(statName));
|
||||
}
|
||||
}
|
||||
|
||||
function applyFavoriteState(isActive, count, animate) {
|
||||
for (var i = 0; i < favBtns.length; i++) paintFavorite(favBtns[i], isActive, animate);
|
||||
if ("number" === typeof count && !isNaN(count)) {
|
||||
setBaseStatCount("favorites", count);
|
||||
} else {
|
||||
updateStatCounts("favorites", getBaseStatCount("favorites"));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 参考主题的点击流程:立即还原计数 → 提交 → 按服务端结果重绘。
|
||||
* 服务端返回未变化(例如访客重复记录)时不播放动画,只提示。
|
||||
*/
|
||||
function bindToggle(buttons, statName, actionName, messages) {
|
||||
if (!buttons.length) return;
|
||||
if (buttons[0].dataset.jioInteractionBound) return;
|
||||
|
||||
if (likeBtns.length) {
|
||||
var seedLike = likeBtns[0];
|
||||
if (!seedLike.dataset.jioInteractionBound) {
|
||||
applyLikeState(seedLike.dataset.initialActive === "1", getBaseStatCount("likes"));
|
||||
likeBtns.forEach(function (likeBtn) {
|
||||
if (likeBtn.dataset.jioInteractionBound) return;
|
||||
likeBtn.dataset.jioInteractionBound = "1";
|
||||
likeBtn.addEventListener("click", function () {
|
||||
var postId = likeBtn.dataset.postId ? String(likeBtn.dataset.postId) : "";
|
||||
if (!postId || likeBtn.disabled) return;
|
||||
var nextState = likeBtn.dataset.initialActive !== "1";
|
||||
setButtonBusy(likeBtn, true);
|
||||
requestInteraction("jio_toggle_like", postId, nextState).then(function (data) {
|
||||
applyLikeState(!!data.active, parseInt(data.count, 10) || 0, true);
|
||||
notyf(data.active ? "感谢你的点赞" : "已取消点赞", "success");
|
||||
}).catch(function (error) {
|
||||
notyf(error && error.message ? error.message : "点赞失败", "danger");
|
||||
}).finally(function () {
|
||||
setButtonBusy(likeBtn, false);
|
||||
});
|
||||
var seed = buttons[0];
|
||||
applyState(buttons, statName, seed.dataset.initialActive === "1", getBaseStatCount(statName), false);
|
||||
|
||||
buttons.forEach(function (button) {
|
||||
if (button.dataset.jioInteractionBound) return;
|
||||
button.dataset.jioInteractionBound = "1";
|
||||
button.addEventListener("click", function () {
|
||||
var postId = button.dataset.postId ? String(button.dataset.postId) : "";
|
||||
if (!postId || button.disabled) return;
|
||||
|
||||
// 点击前的快照:用来判断服务端是否真的改变了状态。
|
||||
var wasActive = button.dataset.initialActive === "1";
|
||||
var previousCount = getBaseStatCount(statName);
|
||||
var restoreCount = showPending(button);
|
||||
|
||||
requestInteraction(actionName, postId, !wasActive).then(function (data) {
|
||||
var active = !!data.active;
|
||||
var count = parseInt(data.count, 10);
|
||||
if (isNaN(count)) count = previousCount;
|
||||
restoreCount();
|
||||
|
||||
var unchanged = active === wasActive && count === previousCount;
|
||||
applyState(buttons, statName, active, count, !unchanged);
|
||||
if (unchanged) {
|
||||
notyf(messages.duplicate, "warning");
|
||||
} else {
|
||||
notyf(active ? messages.added : messages.removed, active ? "success" : "warning");
|
||||
}
|
||||
}).catch(function (error) {
|
||||
restoreCount();
|
||||
notyf(error && error.message ? error.message : messages.failed, "danger");
|
||||
if (error && error.needsLogin) openLoginModal();
|
||||
}).finally(function () {
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (favBtns.length) {
|
||||
var seedFav = favBtns[0];
|
||||
if (!seedFav.dataset.jioInteractionBound) {
|
||||
applyFavoriteState(seedFav.dataset.initialActive === "1", getBaseStatCount("favorites"));
|
||||
favBtns.forEach(function (favBtn) {
|
||||
if (favBtn.dataset.jioInteractionBound) return;
|
||||
favBtn.dataset.jioInteractionBound = "1";
|
||||
favBtn.addEventListener("click", function () {
|
||||
var postId = favBtn.dataset.postId ? String(favBtn.dataset.postId) : "";
|
||||
if (!postId || favBtn.disabled) return;
|
||||
var nextState = favBtn.dataset.initialActive !== "1";
|
||||
setButtonBusy(favBtn, true);
|
||||
requestInteraction("jio_toggle_favorite", postId, nextState).then(function (data) {
|
||||
applyFavoriteState(!!data.active, parseInt(data.count, 10) || 0, true);
|
||||
notyf(data.active ? "收藏成功" : "已取消收藏", "success");
|
||||
}).catch(function (error) {
|
||||
notyf(error && error.message ? error.message : "收藏失败", "danger");
|
||||
}).finally(function () {
|
||||
setButtonBusy(favBtn, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
bindToggle(likeBtns, "likes", "jio_toggle_like", {
|
||||
added: "已赞!感谢您的支持",
|
||||
removed: "点赞已取消",
|
||||
duplicate: "已赞过此文章了!",
|
||||
failed: "点赞失败"
|
||||
});
|
||||
|
||||
bindToggle(favBtns, "favorites", "jio_toggle_favorite", {
|
||||
added: "已收藏此文章",
|
||||
removed: "已取消收藏",
|
||||
duplicate: "已经收藏过此文章了!",
|
||||
failed: "收藏失败"
|
||||
});
|
||||
}
|
||||
|
||||
function init(root) {
|
||||
|
||||
Reference in new issue
Block a user