topfans/frontend/pages/support-activity/components/ActionBar.vue
2026-06-24 20:07:42 +08:00

1140 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="action-bar" :style="{ paddingBottom: safeAreaBottom + 'px' }">
<!-- 钻石消耗确认弹窗 -->
<DiamondConfirmModal
:visible="confirmModal.visible"
:itemLabel="confirmModal.itemLabel"
:itemCost="confirmModal.itemCost"
@confirm="handleModalConfirm"
@cancel="handleModalCancel"
/>
<!-- 主容器 -->
<view class="bar-container">
<!-- 用户信息区域 -->
<!-- <view class="user-info">
<image :src="userInfo?.avatar_url || '/static/icon/avatar-default.png'" class="user-avatar" mode="aspectFill" />
<view class="user-balance">
<image src="/static/icon/crystal.png" class="balance-icon" mode="aspectFit" />
<text class="balance-text">{{ crystalBalance || 0 }}</text>
</view>
</view> -->
<view class="items-row">
<view
v-for="item in items"
:key="item.type"
class="action-item"
:class="{ active: selectedItem === item.type }"
@click="handleItemSelect(item)"
>
<!-- 选中状态的背景 -->
<image
v-if="selectedItem === item.type"
src="/static/rank/activity-support-icon/djbeij.jpg"
class="item-bg"
mode="aspectFill"
/>
<!-- 图标区域:添加白色背景和阴影 -->
<view class="icon-wrapper">
<image
:src="item.icon"
class="item-icon"
mode="aspectFill"
lazy-load
/>
</view>
<!-- 名称区域 -->
<view class="name-wrapper">
<text class="item-name">{{ item.name || item.label }}</text>
</view>
<!-- 价格区域:钻石图标 + 数字 -->
<view class="cost-wrapper">
<image
src="/static/icon/crystal.png"
class="diamond-icon"
mode="aspectFit"
lazy-load
/>
<text class="item-cost">{{ item.cost }}</text>
</view>
<!-- 选中状态显示"点击赠送" -->
<view
v-if="selectedItem === item.type"
class="gift-text"
@click="
(e) => {
e.stopPropagation();
handleDirectContribute(item);
}
"
>点击赠送</view
>
<!-- 每个道具独立的反馈动画(实时累加:每次点击 +1 -->
<view v-if="feedbackItem === item.type" class="feedback-layer">
<view
class="feedback-icon"
:style="{ color: itemFeedbackColor(item.type) }"
>✖️{{ feedbackCount }}</view
>
</view>
</view>
</view>
<!-- 数量选择器和确认按钮 -->
<view class="quantity-control">
<view class="quantity-selector">
<view class="quantity-btn minus" @click.stop="decreaseQuantity"
>-</view
>
<input type="number" v-model="quantity" class="quantity-input" />
<view class="quantity-btn plus" @click.stop="increaseQuantity"
>+</view
>
</view>
<view class="confirm-btn" @click.stop="handleConfirmContribute">
<text>确认赠送</text>
</view>
</view>
</view>
<!-- 重试按钮 (当有失败操作时显示) -->
<view
v-if="hasFailedActions"
class="retry-banner"
@click="retryFailedActions"
>
<text class="retry-text">有操作失败,点击重试</text>
</view>
<!-- 自定义结果提示(替代系统 toast/modal避免截断 -->
<view
v-show="resultToast.visible"
class="result-toast"
:class="{ 'result-toast--visible': resultToast.visible }"
>
<text class="result-toast-icon">{{ resultToast.icon }}</text>
<text class="result-toast-text">{{ resultToast.text }}</text>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
import { purchaseItem } from "@/utils/activity-config";
import { getEarningsSummaryApi } from "@/utils/api";
import DiamondConfirmModal from "./DiamondConfirmModal.vue";
const props = defineProps({
activityId: {
type: String,
required: true,
},
actionItems: {
type: Array,
default: () => [
{
type: "firework",
label: "烟花",
icon: "/static/rank/spark.png",
cost: 1,
},
{
type: "megaphone",
label: "喇叭",
icon: "/static/icon/task.png",
cost: 5,
},
{
type: "love",
label: "LOVE",
icon: "/static/icon/castlove.png",
cost: 10,
},
],
},
});
const emit = defineEmits(["contribute"]);
// 动态响应父组件传入的道具配置
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); // 正在贡献中,防止状态被清除
// 按索引分配颜色,不依赖后端 type 字符串
const FEEDBACK_COLORS = ["#FFD700", "#00CFFF", "#FF6B9D", "#A78BFA", "#34D399"];
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);
const processingItems = new Set(); // 防止重复点击
const resultToast = ref({ visible: false, icon: "", text: "" });
let resultToastTimer = null;
function showResultToast(icon, text) {
if (resultToastTimer) clearTimeout(resultToastTimer);
resultToast.value = { visible: true, icon, text };
resultToastTimer = setTimeout(() => {
resultToast.value.visible = false;
}, 3000);
}
// 自定义确认弹窗状态
const confirmModal = ref({ visible: false, itemLabel: "", itemCost: 0 });
let confirmResolve = null;
function showConfirmModal(item) {
confirmModal.value = {
visible: true,
itemLabel: item.label,
itemCost: item.cost,
};
return new Promise((resolve) => {
confirmResolve = resolve;
});
}
function handleModalConfirm() {
confirmModal.value.visible = false;
confirmResolve?.(true);
}
function handleModalCancel() {
confirmModal.value.visible = false;
confirmResolve?.(false);
}
const userInfo = ref(null);
const crystalBalance = ref(0);
async function loadUserInfo() {
try {
const userStr = uni.getStorageSync("user");
if (userStr) {
userInfo.value =
typeof userStr === "string" ? JSON.parse(userStr) : userStr;
}
// 从API获取真实余额
const res = await getEarningsSummaryApi();
if (res.code === 0 && res.data) {
crystalBalance.value = res.data.crystal_balance || 0;
if (userInfo.value) {
userInfo.value.crystal_balance = crystalBalance.value;
uni.setStorageSync("user", JSON.stringify(userInfo.value));
}
// 通知其他组件(如 Header余额已更新
uni.$emit("balanceUpdated", { crystal_balance: crystalBalance.value });
} else {
crystalBalance.value = userInfo.value?.crystal_balance || 0;
}
} catch (error) {
console.error("加载用户信息失败:", error);
crystalBalance.value = userInfo.value?.crystal_balance || 0;
}
}
// 网络状态监听
let networkListener = null;
let syncDebounceTimer = null;
function debouncedSync() {
if (syncDebounceTimer) clearTimeout(syncDebounceTimer);
if (resultToastTimer) clearTimeout(resultToastTimer);
syncDebounceTimer = setTimeout(() => {
syncDebounceTimer = null;
syncPendingActions();
}, 1000);
}
onMounted(() => {
// 获取安全区域信息
const systemInfo = uni.getSystemInfoSync();
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0;
// 加载用户信息
loadUserInfo();
// 恢复离线队列
loadPendingActions();
// 默认选中第一个道具
if (items.value && items.value.length > 0) {
selectedItem.value = items.value[0].type;
}
// 监听网络状态
networkListener = uni.onNetworkStatusChange((res) => {
const wasOffline = !isOnline.value;
isOnline.value = res.isConnected;
// 如果从离线恢复到在线,自动同步(防抖避免网络抖动重复触发)
if (wasOffline && isOnline.value && pendingActions.value.length > 0) {
debouncedSync();
}
});
// 检查当前网络状态
uni.getNetworkType({
success: (res) => {
isOnline.value = res.networkType !== "none";
},
});
// 监听余额更新事件
uni.$on("balanceUpdated", (data) => {
if (userInfo.value) {
userInfo.value.crystal_balance = data.crystal_balance;
}
});
});
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");
});
// 存储键前缀
const STORAGE_PREFIX = "topfans_";
// 加载离线队列
async function loadPendingActions() {
try {
// 先检查 key 是否存在
const info = uni.getStorageInfoSync();
const key = `${STORAGE_PREFIX}pending_actions_${props.activityId}`;
if (!info.keys.includes(key)) {
return; // key 不存在,直接返回
}
const cached = await uni.getStorage({
key: `${STORAGE_PREFIX}pending_actions_${props.activityId}`,
});
if (cached && cached[1]?.data && Array.isArray(cached[1].data)) {
pendingActions.value = cached[1].data;
hasFailedActions.value = cached[1].data.length > 0;
// 如果在线,尝试同步
if (isOnline.value && cached[1].data.length > 0) {
syncPendingActions();
}
}
} catch (error) {
console.error("加载离线队列失败:", error);
}
}
// 保存离线队列
async function savePendingActions() {
try {
await uni.setStorage({
key: `${STORAGE_PREFIX}pending_actions_${props.activityId}`,
data: pendingActions.value,
});
hasFailedActions.value = pendingActions.value.length > 0;
} catch (error) {
console.error("保存离线队列失败:", error);
}
}
// 处理道具选择(第一次点击)
function handleItemSelect(item) {
selectedItem.value = item.type;
quantity.value = 1;
isContributing.value = false;
}
// 增加数量
function increaseQuantity() {
quantity.value++;
}
// 减少数量
function decreaseQuantity() {
if (quantity.value > 1) {
quantity.value--;
}
}
// 处理直接贡献(点击"点击赠送",不需要确认)
// 设计取舍:
// - UI 反馈:每次点击立即累加(无延迟),始终显示点击次数 N
// - API 行为:每次点击都真正发一次 purchase 请求(连击 N 次 → 服务端 combo 聚合推送 1 条 record
// 但 contribution 表里仍是 N 条;本地水晶扣减 N 次)
// - 不做并发限制:服务端是余额/购买数量的唯一权威,并发扣减由服务端控制;
// 本地仅做"乐观反馈"——若服务端拒绝(余额不足/活动已结束toast 会提示
// - 失败处理contributeItem catch 内已加入离线队列,无需重复处理
async function handleDirectContribute(item) {
// 0. 余额预校验(避免本地缓存明显不足时还触发反馈 + 发请求)
// 真正的余额校验由服务端 purchase 接口完成contributeItem → purchaseItem
const hasBalance = await validateBalance(item.cost);
if (!hasBalance) {
showResultToast("", "余额不足");
return;
}
// 1. 立即触发实时反馈(不等任何东西,让连击无延迟)
triggerRealtimeFeedback(item.type);
// 2. 直接发起贡献请求(无并发限制;服务端负责频控与连击聚合)
try {
// skipFeedback=trueUI 反馈已在第 1 步触发,避免 contributeItem 内部重复触发
// 其余逻辑(余额更新 / emit / toast / 离线队列)由 contributeItem 内部统一处理
await contributeItem(item, false, false, 1, true);
} catch (error) {
// 失败已在 contributeItem catch 中处理(加入离线队列 + 日志)
console.error("贡献失败:", error);
}
}
// 处理确认贡献(点击"确认赠送"
async function handleConfirmContribute() {
const item = items.value.find((i) => i.type === selectedItem.value);
if (!item || isContributing.value) return;
// 防止同一道具重复点击
if (processingItems.has(item.type)) return;
processingItems.add(item.type);
isContributing.value = true;
try {
// 验证余额(乘以数量)
const totalCost = item.cost * Number(quantity.value);
const hasBalance = await validateBalance(totalCost);
if (!hasBalance) {
showResultToast("", "余额不足");
return;
}
// 弹出确认框
const confirmed = await showConfirmModal({ ...item, cost: totalCost });
if (!confirmed) {
return;
}
// 调用贡献API一次传入总数量确保为 number 类型)
const res = await contributeItem(item, true, true, Number(quantity.value));
console.log(`批量购买返回值:`, res);
// 更新本地余额
if (res && res.remainingBalance !== null) {
await updateLocalBalanceFromResult(res.remainingBalance);
}
// 显示自定义结果提示
if (res && res.success) {
showResultToast(
"✅",
`贡献成功\n+${item.cost * Number(quantity.value)} 贡献值已到账`,
);
// 通知父组件更新进度
emit("contribute", item.type, res.remainingBalance);
} else {
showResultToast("❌", res?.message || "贡献失败");
}
} finally {
processingItems.delete(item.type);
isContributing.value = false;
}
}
// 验证用户余额(优先用缓存,避免重复读取存储)
async function validateBalance(cost) {
try {
// 确保余额已加载
if (crystalBalance.value === 0 && userInfo.value?.crystal_balance) {
crystalBalance.value = userInfo.value.crystal_balance;
}
return crystalBalance.value >= cost;
} catch (error) {
console.error("验证余额失败:", error);
return false;
}
}
// 贡献道具
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,
// "skipFeedback:",
// skipFeedback,
// );
// 检查购买结果
if (!result.success) {
const msg = result.message || "活动不在进行中,无法购买";
if (!silent) showResultToast("", msg);
return { success: false, message: msg };
}
// 成功:触发反馈动画(重试/静默/跳过时不触发)
// skipFeedback 用于调用方已在入口触发了实时反馈的场景(如 handleDirectContribute
if (!isRetry && !silent && !skipFeedback) {
feedbackItem.value = item.type;
setTimeout(() => {
feedbackItem.value = null;
}, 800);
}
// 更新本地用户余额:非重试时直接用服务端余额;重试时由 syncPendingActions 统一处理
if (!isRetry && !silent) {
await updateLocalBalanceFromResult(result.remainingBalance);
}
// 通知父组件更新水晶余额(使用返回的剩余余额)
emit("contribute", item.type, result.remainingBalance);
// 如果是重试成功或静默模式,不单独弹 toast
if (!isRetry && !silent) {
// 直接贡献(连击场景):累加贡献值到 pendingContribution
// 停止点击 800ms 后一次性弹累计 toast避免连击时反复闪烁
scheduleContributionToast(result.totalContribution);
}
// 重试时返回完整结果供 syncPendingActions 汇总;正常时返回 true
return isRetry
? {
contribution: result.totalContribution,
remainingBalance: result.remainingBalance,
success: true,
}
: { success: true };
} catch (error) {
console.error("贡献失败:", error);
// 如果不是重试操作,先乐观扣除本地余额再入队
if (!isRetry && !silent) {
deductLocalBalance(item.cost);
const queued = addToPendingQueue(item);
if (!queued) {
// 队列已满,退还刚扣除的余额
refundLocalBalance(item.cost);
return { success: false, message: "网络异常,已加入队列" };
}
return { success: false, message: "网络异常,已加入队列" };
}
return { success: false, message: error.message || "贡献失败" };
}
}
// 添加到离线队列(上限 10 个)
const QUEUE_LIMIT = 10;
function addToPendingQueue(item) {
if (pendingActions.value.length >= QUEUE_LIMIT) {
showResultToast("⚠️", "离线队列已满\n请联网后再操作");
return false;
}
const action = {
type: item.type,
cost: item.cost,
label: item.label,
icon: item.icon,
timestamp: Date.now(),
retryCount: 0,
};
pendingActions.value.push(action);
savePendingActions();
return true;
}
// 乐观扣除本地余额(离线入队时使用)
function deductLocalBalance(cost) {
try {
const userStr = uni.getStorageSync("user");
if (userStr) {
// 确保正确解析用户对象
let user;
if (typeof userStr === "string") {
user = JSON.parse(userStr);
} else {
user = { ...userStr };
}
// 扣除余额,确保不为负数
user.crystal_balance = Math.max(
0,
(Number(user.crystal_balance) || 0) - cost,
);
// 保存回存储
uni.setStorageSync("user", JSON.stringify(user));
userInfo.value = user;
crystalBalance.value = user.crystal_balance;
uni.$emit("balanceUpdated", { crystal_balance: user.crystal_balance });
}
} catch (error) {
console.error("扣除本地余额失败:", error);
}
}
// 退还本地余额(同步彻底失败时使用)
function refundLocalBalance(cost) {
try {
const userStr = uni.getStorageSync("user");
if (userStr) {
// 确保正确解析用户对象
let user;
if (typeof userStr === "string") {
user = JSON.parse(userStr);
} else {
user = { ...userStr };
}
// 退还余额
user.crystal_balance = (Number(user.crystal_balance) || 0) + cost;
// 保存回存储
uni.setStorageSync("user", JSON.stringify(user));
userInfo.value = user;
crystalBalance.value = user.crystal_balance;
uni.$emit("balanceUpdated", { crystal_balance: user.crystal_balance });
}
} catch (error) {
console.error("退还本地余额失败:", error);
}
}
async function updateLocalBalanceFromResult(newBalance) {
try {
const userStr = uni.getStorageSync("user");
if (userStr) {
// 确保正确解析用户对象
let user;
if (typeof userStr === "string") {
user = JSON.parse(userStr);
} else {
// 如果已经是对象,创建一个新副本避免引用问题
user = { ...userStr };
}
// 更新余额,确保是数字类型
user.crystal_balance = Number(newBalance) || 0;
// 保存回存储,确保是字符串格式
uni.setStorageSync("user", JSON.stringify(user));
userInfo.value = user;
crystalBalance.value = user.crystal_balance;
uni.$emit("balanceUpdated", { crystal_balance: user.crystal_balance });
}
} catch (error) {
console.error("更新本地余额失败:", error);
}
}
// 同步离线队列
async function syncPendingActions() {
if (pendingActions.value.length === 0) {
return;
}
uni.showLoading({
title: "同步中...",
mask: true,
});
const actions = [...pendingActions.value];
const failedActions = [];
const CONCURRENCY = 3;
let totalContribution = 0;
let minRemainingBalance = Infinity; // 并发时取最小余额,避免覆盖问题
try {
// 分批并发执行,每批最多 CONCURRENCY 个
for (let i = 0; i < actions.length; i += CONCURRENCY) {
const batch = actions.slice(i, i + CONCURRENCY);
const results = await Promise.all(
batch.map(async (action) => {
if (action.retryCount >= 3) {
// 重试超限,退还之前乐观扣除的余额
refundLocalBalance(action.cost);
return {
action,
contribution: 0,
remainingBalance: null,
success: false,
};
}
action.retryCount = (action.retryCount || 0) + 1;
const item = {
type: action.type,
cost: action.cost,
label: action.label,
icon: action.icon,
};
const res = await contributeItem(item, true);
const success = res !== false && res !== null;
return {
action,
contribution: success ? res.contribution : 0,
remainingBalance: success ? res.remainingBalance : null,
success,
};
}),
);
results.forEach(({ action, contribution, remainingBalance, success }) => {
if (success) {
totalContribution += contribution;
if (
remainingBalance !== null &&
remainingBalance < minRemainingBalance
) {
minRemainingBalance = remainingBalance;
}
} else {
failedActions.push(action);
}
});
}
} finally {
uni.hideLoading();
// 批次全部完成后,用最小余额做一次写入,避免并发覆盖
if (minRemainingBalance !== Infinity) {
await updateLocalBalanceFromResult(minRemainingBalance);
}
}
// 更新队列
pendingActions.value = failedActions;
savePendingActions();
// 显示结果(自定义 toast完全控制尺寸避免系统截断
const successCount = actions.length - failedActions.length;
if (successCount > 0 && failedActions.length === 0) {
showResultToast("✅", `贡献成功\n+${totalContribution} 贡献值已到账`);
} else if (successCount > 0 && failedActions.length > 0) {
showResultToast(
"⚠️",
`成功 ${successCount} 个,+${totalContribution} 贡献值\n失败 ${failedActions.length} 个,可点击重试`,
);
} else {
showResultToast("❌", "贡献失败\n钻石已退还");
}
}
// 手动重试失败操作
function retryFailedActions() {
syncPendingActions();
}
// 暴露方法供父组件调用
defineExpose({
syncPendingActions,
getPendingActionsCount: () => pendingActions.value.length,
});
</script>
<style scoped>
/* 整体定位 */
.action-bar {
width: 100%;
min-height: 13rem;
display: flex;
justify-content: center;
}
/* 核心容器:模仿图中的粉色渐变长条 */
.bar-container {
width: 100%;
background-image: url("@/static/rank/activity-support-icon/beijingkuang.png");
background-size: 105% 130%;
background-position: center;
border-radius: 40rpx 40rpx 0 0;
padding: 20rpx 40rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
/* 用户信息区域 */
.user-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
}
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 3rpx solid #fff;
}
.user-balance {
display: flex;
align-items: center;
gap: 8rpx;
/* background: rgba(0, 0, 0, 0.2); */
padding: 8rpx 20rpx;
background: linear-gradient(
to bottom right,
#f0e4b1 0%,
#f08399 50%,
#b94e73 100%
);
border-radius: 24rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
border-radius: 30rpx;
}
.balance-icon {
width: 40rpx;
height: 40rpx;
}
.balance-text {
font-size: 28rpx;
color: #fff;
font-weight: bold;
}
/* 道具行容器 */
.items-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
padding: 16rpx 0;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
cursor: pointer;
padding: 16rpx;
border-radius: 20rpx;
}
/* 选中状态 */
.action-item.active .icon-wrapper,
.action-item.active .name-wrapper,
.action-item.active .cost-wrapper {
opacity: 0.6;
}
/* 选中背景 */
.item-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.gift-text {
font-size: 22rpx;
color: #fff;
white-space: nowrap;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
background: linear-gradient(
to bottom right,
#f0e4b1 0%,
#f08399 50%,
#b94e73 100%
);
border-radius: 24rpx;
padding: 6rpx 16rpx;
margin-top: 8rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
position: relative;
}
/* 数量选择器和确认按钮 */
.quantity-control {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.3);
}
.quantity-selector {
display: flex;
align-items: center;
gap: 16rpx;
background: rgba(0, 0, 0, 0.2);
border-radius: 40rpx;
padding: 8rpx 16rpx;
}
.quantity-btn {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: linear-gradient(
to bottom right,
#f0e4b1 0%,
#f08399 50%,
#b94e73 100%
);
color: #fff;
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
.quantity-input {
width: 60rpx;
text-align: center;
font-size: 28rpx;
color: #fff;
background: transparent;
}
.confirm-btn {
background-image: url("@/static/rank/activity-support-icon/beijingkuang.png");
background-size: cover;
background-position: center;
border-radius: 40rpx;
padding: 16rpx 40rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 143, 158, 0.3);
}
.confirm-btn text {
font-size: 28rpx;
color: #fff;
font-weight: bold;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
}
/* 图标包装器 */
.icon-wrapper {
width: 112rpx;
height: 112rpx;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 8rpx;
overflow: hidden;
}
.item-icon {
width: 100%;
height: 100%;
}
/* 名称包装器 */
.name-wrapper {
margin-bottom: 4rpx;
}
.item-name {
font-size: 22rpx;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
text-align: center;
}
/* 价格包装器:横向排列钻石和数字 */
.cost-wrapper {
display: flex;
align-items: center;
gap: 4rpx;
}
/* 钻石图标样式 */
.diamond-icon {
width: 40rpx;
height: 40rpx;
}
.item-cost {
font-size: 24rpx;
color: #fff;
/* 橙色,接近图中 LISA 的颜色 */
text-shadow:
0 3rpx 6rpx rgba(0, 0, 0, 0.4),
0 1rpx 3rpx rgba(0, 0, 0, 0.3);
font-family: "yt", sans-serif;
}
/* --- 以下保持原有逻辑样式 --- */
.feedback-layer {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
pointer-events: none;
z-index: 10;
}
.feedback-icon {
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 {
position: absolute;
top: -80rpx;
/* 调整位置以免遮挡渐变条 */
left: 50%;
transform: translateX(-50%);
background: rgba(255, 107, 157, 0.9);
padding: 10rpx 30rpx;
border-radius: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
cursor: pointer;
white-space: nowrap;
}
.retry-text {
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
@keyframes feedback-pop {
0% {
opacity: 0;
transform: scale(0.5) translateY(0);
}
50% {
opacity: 1;
transform: scale(1.2) translateY(-40rpx);
}
100% {
opacity: 0;
transform: scale(1) translateY(-80rpx);
}
}
.result-toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
background: rgba(0, 0, 0, 0.75);
border-radius: 20rpx;
padding: 40rpx 48rpx;
min-width: 320rpx;
max-width: 560rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.result-toast--visible {
opacity: 1;
pointer-events: auto;
}
.result-toast-icon {
font-size: 56rpx;
line-height: 1;
}
.result-toast-text {
font-size: 28rpx;
color: #fff;
text-align: center;
line-height: 1.6;
white-space: pre-line;
}
</style>