feat:修改聚合分反馈动画
This commit is contained in:
parent
0d6b7a9e95
commit
1939d35f9e
@ -74,12 +74,12 @@
|
||||
>点击赠送</view
|
||||
>
|
||||
|
||||
<!-- 每个道具独立的反馈动画 -->
|
||||
<!-- 每个道具独立的反馈动画(实时累加:每次点击 +1) -->
|
||||
<view v-if="feedbackItem === item.type" class="feedback-layer">
|
||||
<view
|
||||
class="feedback-icon"
|
||||
:style="{ color: itemFeedbackColor(item.type) }"
|
||||
>+1</view
|
||||
>✖️{{ feedbackCount }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
@ -166,6 +166,11 @@ const items = computed(() => props.actionItems);
|
||||
|
||||
const safeAreaBottom = ref(0);
|
||||
const feedbackItem = ref(null); // 当前触发反馈动画的道具 type
|
||||
const feedbackCount = ref(0); // 当前反馈动画显示的点击次数(实时累加)
|
||||
let feedbackClearTimer = null; // 反馈清除定时器句柄(每次点击重置,让动画可见)
|
||||
const clickCounts = new Map(); // 每个道具 type 的累计点击次数(仅 UI 反馈用)
|
||||
const pendingContribution = ref(0); // 待弹 toast 的累计贡献值(连击期间累加,停止后一次性弹)
|
||||
let contributionToastTimer = null; // 贡献 toast 延迟显示定时器(每次贡献重置)
|
||||
const selectedItem = ref(null); // 当前选中的道具
|
||||
const quantity = ref(1); // 赠送数量
|
||||
const isContributing = ref(false); // 正在贡献中,防止状态被清除
|
||||
@ -177,6 +182,53 @@ function itemFeedbackColor(type) {
|
||||
const idx = items.value.findIndex((i) => i.type === type);
|
||||
return FEEDBACK_COLORS[idx >= 0 ? idx % FEEDBACK_COLORS.length : 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时反馈动画触发器(在 API 返回前立即调用,让连击反馈无延迟)
|
||||
* - 累加 clickCounts[type],feedbackCount 同步显示给 UI
|
||||
* - 每次点击重置清除定时器(800ms 内无新点击才清除,让动画在连击期间持续可见)
|
||||
* - 切换道具时计数自动重置(clickCounts 按 type 隔离)
|
||||
*/
|
||||
function triggerRealtimeFeedback(type) {
|
||||
const nextCount = (clickCounts.get(type) || 0) + 1;
|
||||
clickCounts.set(type, nextCount);
|
||||
feedbackItem.value = type;
|
||||
feedbackCount.value = nextCount;
|
||||
|
||||
// 重置清除定时器:800ms 内无新点击 → 清除反馈并清零计数
|
||||
if (feedbackClearTimer) {
|
||||
clearTimeout(feedbackClearTimer);
|
||||
}
|
||||
feedbackClearTimer = setTimeout(() => {
|
||||
feedbackItem.value = null;
|
||||
feedbackCount.value = 0;
|
||||
clickCounts.delete(type);
|
||||
feedbackClearTimer = null;
|
||||
}, 800);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟弹"贡献值 +N"toast(连击期间累加,停止 800ms 后一次性弹出累计值)
|
||||
* - 每次 API 返回成功调用本函数,累加 pendingContribution
|
||||
* - 重置 800ms 延迟定时器(连续点击会一直延后弹 toast)
|
||||
* - 定时器触发时弹出累计 toast 并清零 pendingContribution
|
||||
* - 用于直接贡献(连击场景);批量/重试路径不走这里(各自处理 toast)
|
||||
*/
|
||||
function scheduleContributionToast(deltaContribution) {
|
||||
pendingContribution.value += deltaContribution;
|
||||
|
||||
if (contributionToastTimer) {
|
||||
clearTimeout(contributionToastTimer);
|
||||
}
|
||||
contributionToastTimer = setTimeout(() => {
|
||||
showResultToast(
|
||||
"✅",
|
||||
`贡献值 +${pendingContribution.value}`,
|
||||
);
|
||||
pendingContribution.value = 0;
|
||||
contributionToastTimer = null;
|
||||
}, 800);
|
||||
}
|
||||
const pendingActions = ref([]);
|
||||
const hasFailedActions = ref(false);
|
||||
const isOnline = ref(true);
|
||||
@ -306,6 +358,8 @@ onUnmounted(() => {
|
||||
if (networkListener) uni.offNetworkStatusChange(networkListener);
|
||||
if (syncDebounceTimer) clearTimeout(syncDebounceTimer);
|
||||
if (resultToastTimer) clearTimeout(resultToastTimer);
|
||||
if (feedbackClearTimer) clearTimeout(feedbackClearTimer);
|
||||
if (contributionToastTimer) clearTimeout(contributionToastTimer);
|
||||
uni.$off("balanceUpdated");
|
||||
});
|
||||
|
||||
@ -374,30 +428,33 @@ function decreaseQuantity() {
|
||||
}
|
||||
|
||||
// 处理直接贡献(点击"点击赠送",不需要确认)
|
||||
// 设计取舍:
|
||||
// - UI 反馈:每次点击立即累加(无延迟),始终显示点击次数 N
|
||||
// - API 行为:每次点击都真正发一次 purchase 请求(连击 N 次 → 服务端 combo 聚合推送 1 条 record,
|
||||
// 但 contribution 表里仍是 N 条;本地水晶扣减 N 次)
|
||||
// - 不做并发限制:服务端是余额/购买数量的唯一权威,并发扣减由服务端控制;
|
||||
// 本地仅做"乐观反馈"——若服务端拒绝(余额不足/活动已结束),toast 会提示
|
||||
// - 失败处理:contributeItem catch 内已加入离线队列,无需重复处理
|
||||
async function handleDirectContribute(item) {
|
||||
// 防止同一道具重复点击
|
||||
if (processingItems.has(item.type) || isContributing.value) return;
|
||||
processingItems.add(item.type);
|
||||
isContributing.value = true;
|
||||
// 0. 余额预校验(避免本地缓存明显不足时还触发反馈 + 发请求)
|
||||
// 真正的余额校验由服务端 purchase 接口完成(contributeItem → purchaseItem)
|
||||
const hasBalance = await validateBalance(item.cost);
|
||||
if (!hasBalance) {
|
||||
showResultToast("", "余额不足");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 立即触发实时反馈(不等任何东西,让连击无延迟)
|
||||
triggerRealtimeFeedback(item.type);
|
||||
|
||||
// 2. 直接发起贡献请求(无并发限制;服务端负责频控与连击聚合)
|
||||
try {
|
||||
// 验证余额
|
||||
const hasBalance = await validateBalance(item.cost);
|
||||
if (!hasBalance) {
|
||||
showResultToast("", "余额不足");
|
||||
// 余额不足时直接返回,不调用API
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接调用贡献API
|
||||
const success = await contributeItem(item);
|
||||
if (!success) {
|
||||
// 贡献失败时不显示额外提示,由 contributeItem 内部处理
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
processingItems.delete(item.type);
|
||||
isContributing.value = false;
|
||||
// skipFeedback=true:UI 反馈已在第 1 步触发,避免 contributeItem 内部重复触发
|
||||
// 其余逻辑(余额更新 / emit / toast / 离线队列)由 contributeItem 内部统一处理
|
||||
await contributeItem(item, false, false, 1, true);
|
||||
} catch (error) {
|
||||
// 失败已在 contributeItem catch 中处理(加入离线队列 + 日志)
|
||||
console.error("贡献失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -467,20 +524,22 @@ async function validateBalance(cost) {
|
||||
}
|
||||
|
||||
// 贡献道具
|
||||
async function contributeItem(item, isRetry = false, silent = false, qty = 1) {
|
||||
async function contributeItem(item, isRetry = false, silent = false, qty = 1, skipFeedback = false) {
|
||||
try {
|
||||
// 使用 activity-config.js 中的 purchaseItem 函数
|
||||
const result = await purchaseItem(props.activityId, item.type, qty);
|
||||
console.log(
|
||||
`[contributeItem] result:`,
|
||||
result,
|
||||
"isRetry:",
|
||||
isRetry,
|
||||
"silent:",
|
||||
silent,
|
||||
"qty:",
|
||||
qty,
|
||||
);
|
||||
// console.log(
|
||||
// `[contributeItem] result:`,
|
||||
// result,
|
||||
// "isRetry:",
|
||||
// isRetry,
|
||||
// "silent:",
|
||||
// silent,
|
||||
// "qty:",
|
||||
// qty,
|
||||
// "skipFeedback:",
|
||||
// skipFeedback,
|
||||
// );
|
||||
|
||||
// 检查购买结果
|
||||
if (!result.success) {
|
||||
@ -489,8 +548,9 @@ async function contributeItem(item, isRetry = false, silent = false, qty = 1) {
|
||||
return { success: false, message: msg };
|
||||
}
|
||||
|
||||
// 成功:触发反馈动画(重试时静默,不触发)
|
||||
if (!isRetry && !silent) {
|
||||
// 成功:触发反馈动画(重试/静默/跳过时不触发)
|
||||
// skipFeedback 用于调用方已在入口触发了实时反馈的场景(如 handleDirectContribute)
|
||||
if (!isRetry && !silent && !skipFeedback) {
|
||||
feedbackItem.value = item.type;
|
||||
setTimeout(() => {
|
||||
feedbackItem.value = null;
|
||||
@ -507,7 +567,9 @@ async function contributeItem(item, isRetry = false, silent = false, qty = 1) {
|
||||
|
||||
// 如果是重试成功或静默模式,不单独弹 toast
|
||||
if (!isRetry && !silent) {
|
||||
showResultToast("✅", `贡献值 +${result.totalContribution}`);
|
||||
// 直接贡献(连击场景):累加贡献值到 pendingContribution,
|
||||
// 停止点击 800ms 后一次性弹累计 toast,避免连击时反复闪烁
|
||||
scheduleContributionToast(result.totalContribution);
|
||||
}
|
||||
|
||||
// 重试时返回完整结果供 syncPendingActions 汇总;正常时返回 true
|
||||
@ -991,10 +1053,13 @@ defineExpose({
|
||||
}
|
||||
|
||||
.feedback-icon {
|
||||
font-size: 60rpx;
|
||||
font-size: 56rpx;
|
||||
color: #ff6b9d;
|
||||
font-weight: bold;
|
||||
animation: feedback-pop 1s ease-out;
|
||||
/* 让 X 和数字视觉分开:X 略小斜体,数字更醒目 */
|
||||
font-style: italic;
|
||||
text-shadow: 0 3rpx 6rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.retry-banner {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user