1078 lines
28 KiB
Vue
1078 lines
28 KiB
Vue
<script setup>
|
||
import { ref, computed, onBeforeUnmount } from "vue";
|
||
import { onLoad } from "@dcloudio/uni-app";
|
||
import Container from "@/components/Container.vue";
|
||
import Avatar from "@/components/Avatar.vue";
|
||
import UserUpgrade from "@/components/UserUpgrade.vue";
|
||
import { getBattleAPI } from "@/apis";
|
||
import { getBattleResultTips } from "@/constants";
|
||
import audioManager from "@/audioManager";
|
||
import useStore from "@/store";
|
||
import { storeToRefs } from "pinia";
|
||
|
||
const store = useStore();
|
||
const { user } = storeToRefs(store);
|
||
/** 通过 store action 获取段位名称(rank_lvl → 显示文字) */
|
||
const { getLvlName } = store;
|
||
|
||
/** 结算图片资源 CDN 前缀 */
|
||
const RESULT_CDN = "https://static.shelingxingqiu.com/shootmini/static/friend-battle-result";
|
||
|
||
// ---- 数据状态 ----
|
||
/** 当前用户是否属于胜利方 */
|
||
const ifWin = ref(false);
|
||
/** 后端返回的对战数据 */
|
||
const data = ref({});
|
||
/** 进场覆盖动效层是否显示 */
|
||
const showOverlay = ref(false);
|
||
/** 自动关闭覆盖层的计时器 */
|
||
const overlayTimer = ref(null);
|
||
/** 控制经验条是否执行进场动画(弹窗关闭后才置 true,避免动画被遮挡) */
|
||
const showExpAnim = ref(false);
|
||
|
||
// ---- 经验进度条数据(onLoad 中从 teams.players 解析填充)----
|
||
/** 本局获得的经验值 */
|
||
const expGained = ref(0);
|
||
/** 对战结束后当前经验进度值 */
|
||
const expCurrent = ref(0);
|
||
/** 升级到下一级所需的总经验(兜底 100,防止分母为 0) */
|
||
const expTotal = ref(100);
|
||
/** 用户当前等级 */
|
||
const userLvl = ref(0);
|
||
|
||
// ---- 计算属性 ----
|
||
|
||
/**
|
||
* 计算当前用户所在队伍编号
|
||
* @returns {number} 1=蓝队,2=红队
|
||
*/
|
||
const myTeam = computed(() => {
|
||
const teams = data.value.teams;
|
||
if (teams && teams[1]) {
|
||
if (teams[1].players.some((p) => p.id === user.value.id)) return 1;
|
||
}
|
||
return 2;
|
||
});
|
||
|
||
/** 蓝队(队伍1)玩家列表 */
|
||
const blueTeamPlayers = computed(() => data.value.teams?.[1]?.players || []);
|
||
|
||
/** 红队(队伍2)玩家列表 */
|
||
const redTeamPlayers = computed(() => data.value.teams?.[2]?.players || []);
|
||
|
||
/**
|
||
* 蓝队得分(直接取队伍级 score 字段,与 team-battle.vue 保持一致)
|
||
*/
|
||
const blueScore = computed(() => data.value.teams?.[1]?.score ?? 0);
|
||
|
||
/**
|
||
* 红队得分(直接取队伍级 score 字段)
|
||
*/
|
||
const redScore = computed(() => data.value.teams?.[2]?.score ?? 0);
|
||
|
||
/**
|
||
* MVP 玩家对象(仅 mode=2/3 时有值)
|
||
* 兼容 mvp 为对象或数组两种格式
|
||
*/
|
||
const mvpPlayer = computed(() => {
|
||
if (data.value.mode > 1) {
|
||
const mvp = data.value.mvp;
|
||
if (mvp && mvp.id) return mvp;
|
||
if (Array.isArray(mvp) && mvp.length) return mvp[0];
|
||
}
|
||
return null;
|
||
});
|
||
|
||
/**
|
||
* MVP 玩家所在队伍编号(1=蓝队,2=红队)
|
||
* 通过比对 mvpPlayer.id 与 blueTeamPlayers 确定,用于选择背景图(mvp-blue / mvp-red)
|
||
*/
|
||
const mvpTeam = computed(() => {
|
||
if (!mvpPlayer.value) return 0;
|
||
return blueTeamPlayers.value.some((p) => p.id === mvpPlayer.value.id) ? 1 : 2;
|
||
});
|
||
|
||
/**
|
||
* 激励语图片 URL(在 onLoad 中确定 ifWin 后赋值,避免 Math.random 放在 computed 里产生缓存不一致问题)
|
||
*/
|
||
const tipImage = ref("");
|
||
|
||
/**
|
||
* 经验进度条宽度百分比(0~100)
|
||
*/
|
||
const expPercent = computed(() =>
|
||
Math.min((expCurrent.value / expTotal.value) * 100, 100)
|
||
);
|
||
|
||
/**
|
||
* 是否为大乱斗模式(mode > 3)
|
||
*/
|
||
const isMeleeMode = computed(() => data.value.mode > 3);
|
||
|
||
/**
|
||
* 大乱斗排行榜列表(从接口 teams["0"].players + resultList 构建)
|
||
* - 以 teams["0"].players 为全量玩家基础(包含未射击的玩家)
|
||
* - 从 resultList 中按 userId 匹配获取得分;未命中则 totalRing 为 0
|
||
* - 按 totalRing 降序排列后注入 rank
|
||
* @returns {{ id, rank, avatar, name, lvlName, totalRing, isSelf }[]}
|
||
*/
|
||
const meleeRankList = computed(() => {
|
||
const resultList = data.value.resultList || [];
|
||
const teamPlayers = data.value.teams?.[0]?.players || [];
|
||
if (!teamPlayers.length) return [];
|
||
|
||
// 以 players 为基础,merge resultList 得分数据
|
||
const list = teamPlayers.map((p) => {
|
||
const resultItem = resultList.find((r) => r.userId === p.id) || {};
|
||
return {
|
||
id: p.id,
|
||
avatar: p.avatar || "",
|
||
name: p.name || "",
|
||
// rank_lvl 字段可能缺失,缺失时显示空字符串,避免 getLvlName(undefined) 返回错误段位名
|
||
lvlName: p.rank_lvl != null ? getLvlName(p.rank_lvl) : "",
|
||
totalRing: resultItem.totalRing ?? 0,
|
||
isSelf: String(p.id) === String(user.value.id),
|
||
};
|
||
});
|
||
|
||
// 按总环数降序排列,环数相同时按 id 升序保证顺序稳定
|
||
list.sort((a, b) => b.totalRing - a.totalRing || a.id - b.id);
|
||
|
||
// 注入名次(从1开始)
|
||
return list.map((item, index) => ({ ...item, rank: index + 1 }));
|
||
});
|
||
|
||
/**
|
||
* 获取前三名行背景装饰 SVG 路径
|
||
* @param {number} rank 名次(1/2/3)
|
||
* @returns {string} 静态资源路径
|
||
*/
|
||
function getMeleeRankBgSrc(rank) {
|
||
const names = ["one", "two", "three"];
|
||
return `../static/friend-battle-result/rank-${names[rank - 1]}.svg`;
|
||
}
|
||
|
||
/**
|
||
* 获取前三名头像光环颜色
|
||
* @param {number} rank 名次(1=金,2=银,3=铜)
|
||
* @returns {string|undefined} 边框颜色,4+名返回 undefined
|
||
*/
|
||
function getMeleeAvatarBorderColor(rank) {
|
||
const colors = ['#FFD947', '#D2D2D2', '#FFA515'];
|
||
return colors[rank - 1];
|
||
}
|
||
|
||
// ---- 生命周期 ----
|
||
|
||
onLoad(async (options) => {
|
||
if (!options.battleId) return;
|
||
|
||
const result = await getBattleAPI(options.battleId);
|
||
data.value = result;
|
||
|
||
// 从 teams 各队伍的 players 中找到当前用户,解析经验进度条所需字段
|
||
// 后端字段:exp(本局经验)/ currentExp(当前经验)/ upgradeExp(升级所需经验)/ level(当前等级)
|
||
const myIdStr = String(user.value.id);
|
||
for (const team of Object.values(result.teams || {})) {
|
||
const p = (team.players || []).find((pl) => String(pl.id) === myIdStr);
|
||
if (p) {
|
||
expGained.value = p.exp ?? 0;
|
||
expCurrent.value = p.currentExp ?? 0;
|
||
expTotal.value = p.upgradeExp ?? 100;
|
||
userLvl.value = p.level ?? 0;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 判断当前用户是否在胜利队伍中
|
||
// 优先使用接口返回的 winTeam 字段;若缺失则以队伍得分高低作为兜底判断
|
||
const myId = String(user.value.id);
|
||
if (result.winTeam != null && result.teams) {
|
||
// 使用 String() 类型统一,防止后端返回字符串 ID 与 store 中数字 ID 严格不等
|
||
const winPlayers = result.teams[result.winTeam]?.players || [];
|
||
ifWin.value = winPlayers.some((p) => String(p.id) === myId);
|
||
} else if (result.teams) {
|
||
// 兜底:winTeam 未返回时,比较双方分数;当前用户所在队分数 >= 对方则视为胜利
|
||
const team1Players = result.teams[1]?.players || [];
|
||
const inTeam1 = team1Players.some((p) => String(p.id) === myId);
|
||
const score1 = result.teams[1]?.score ?? 0;
|
||
const score2 = result.teams[2]?.score ?? 0;
|
||
ifWin.value = inTeam1 ? score1 > score2 : score2 > score1;
|
||
}
|
||
|
||
// 确保 ifWin 已定后再生成激励语图片,防止 Math.random 在 computed 缓存时用到旧的 ifWin 值
|
||
tipImage.value = getBattleResultTips(result.way, result.mode, { win: ifWin.value });
|
||
|
||
// 播放胜利/失败音效
|
||
if (result.mode <= 3) {
|
||
audioManager.play(ifWin.value ? "胜利" : "失败");
|
||
}
|
||
|
||
// 数据加载完成后显示覆盖动效层
|
||
showOverlay.value = true;
|
||
|
||
// 2.5 秒后自动关闭覆盖层
|
||
overlayTimer.value = setTimeout(() => {
|
||
closeOverlay();
|
||
}, 2500);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (overlayTimer.value) clearTimeout(overlayTimer.value);
|
||
});
|
||
|
||
// ---- 方法 ----
|
||
|
||
/**
|
||
* 关闭进场覆盖动效层(点击任意位置或超时均可触发)
|
||
*/
|
||
function closeOverlay() {
|
||
showOverlay.value = false;
|
||
if (overlayTimer.value) {
|
||
clearTimeout(overlayTimer.value);
|
||
overlayTimer.value = null;
|
||
}
|
||
// 弹窗关闭后启动经验条进场动画
|
||
showExpAnim.value = true;
|
||
}
|
||
|
||
/**
|
||
* 底部按钮文案:好友约战显示“返回房间”,排位赛等其他模式显示“返回”
|
||
*/
|
||
const exitBtnText = computed(() => data.value.way === 1 ? '返回房间' : '返回');
|
||
|
||
/**
|
||
* 点击底部按钮跳转
|
||
* - 好友约战(way=1):返回对战房间
|
||
* - 其他模式(排位赛等):跳转到排位赛首页
|
||
*/
|
||
function exit() {
|
||
if (data.value.way === 1) {
|
||
uni.redirectTo({
|
||
url: `/pages/battle-room?roomNumber=${data.value.roomId}`,
|
||
});
|
||
} else {
|
||
uni.redirectTo({
|
||
url: '/pages/ranking',
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 跳转到完整弓箭数据页(match-detail)
|
||
*/
|
||
function checkBowData() {
|
||
uni.navigateTo({
|
||
url: `/pages/match-detail?battleId=${data.value.matchId}`,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 点击导航栏返回按钮
|
||
*/
|
||
function goBack() {
|
||
uni.navigateBack();
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<view class="page">
|
||
<!-- ===== 主体可滚动内容区(由 Container 接管)===== -->
|
||
<Container
|
||
:bgType="data.mode > 3 ? -1 : 0"
|
||
bgColor="#000000"
|
||
:onBack="goBack"
|
||
>
|
||
|
||
<!-- ----- Banner 区:game 胜负展示图(仅 NvN 对抗模式)----- -->
|
||
<view v-if="!isMeleeMode" class="banner-section">
|
||
<!-- 根据胜负动态展示 game-win / game-lose(切图尺寸不同,用 style 分别设宽和叠层距) -->
|
||
<image
|
||
class="banner-result scale-in"
|
||
:src="ifWin ? `${RESULT_CDN}/game-win.png` : `${RESULT_CDN}/game-lose.png`"
|
||
:style="ifWin
|
||
? { width: '362rpx', marginBottom: '-32rpx' }
|
||
: { width: '378rpx', marginBottom: '-40rpx' }"
|
||
mode="widthFix"
|
||
/>
|
||
</view>
|
||
|
||
<!-- ----- VS 对战信息区(仅 NvN 对抗模式)----- -->
|
||
<view v-if="!isMeleeMode" class="vs-section">
|
||
<!-- 红蓝背景切图 -->
|
||
<image class="vs-bg-img" :src="`${RESULT_CDN}/vs-bg.png`" mode="aspectFill" />
|
||
|
||
<!-- 双方玩家头像行 -->
|
||
<view class="vs-players-row">
|
||
<!-- 蓝队 -->
|
||
<view class="team-players team-players-blue">
|
||
<view v-for="p in blueTeamPlayers" :key="p.id" class="player-item">
|
||
<Avatar :src="p.avatar" :size="34" borderColor="#8FB4FD" />
|
||
<text class="player-name player-name-blue">{{ p.name }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 红队 -->
|
||
<view class="team-players team-players-red">
|
||
<view v-for="p in redTeamPlayers" :key="p.id" class="player-item">
|
||
<Avatar :src="p.avatar" :size="34" borderColor="#E67470" />
|
||
<text class="player-name player-name-red">{{ p.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 得分行(蓝队得分 + 红队得分) -->
|
||
<view class="vs-scores-row">
|
||
<view class="score-tag-blue">
|
||
<text class="score-label">蓝方得分</text>
|
||
<text class="score-num score-num-blue">{{ blueScore }}</text>
|
||
</view>
|
||
<!-- 红队得分:分数在内侧(左),标签在外侧(右),配合 justify-content: flex-end 靠右对齐,与蓝队形成镜像对称 -->
|
||
<view class="score-tag-red">
|
||
<text class="score-num score-num-red">{{ redScore }}</text>
|
||
<text class="score-label">红方得分</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ----- MVP 卡片(仅 NvN mode = 2/3)----- -->
|
||
<view
|
||
v-if="mvpPlayer && !isMeleeMode"
|
||
class="mvp-card"
|
||
:class="mvpTeam === 1 ? 'mvp-card-blue' : 'mvp-card-red'"
|
||
>
|
||
<!-- 背景图:蓝队用 mvp-blue.png,红队用 mvp-red.png(平行四边形渐变切图) -->
|
||
<image
|
||
class="mvp-bg"
|
||
:src="mvpTeam === 1 ? '../static/mvp-blue.png' : '../static/mvp-red.png'"
|
||
mode="scaleToFill"
|
||
/>
|
||
<!-- 左:MVP 标题图 + 斩获环数 -->
|
||
<view class="mvp-info">
|
||
<image class="mvp-badge" src="../static/mvp-tip.png" mode="widthFix" />
|
||
<view class="mvp-rings">
|
||
斩获<text class="mvp-rings-num">{{ mvpPlayer.totalRing }}</text>环
|
||
</view>
|
||
</view>
|
||
<!-- 右:MVP 头像 + 名字,边框颜色跟随 MVP 所在队伍 -->
|
||
<view class="mvp-player">
|
||
<Avatar
|
||
:src="mvpPlayer.avatar"
|
||
:size="53"
|
||
:borderColor="mvpTeam === 1 ? '#5FADFF' : '#FF6060'"
|
||
/>
|
||
<text class="mvp-name">{{ mvpPlayer.name }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ----- 大乱斗战绩排行(mode > 3)----- -->
|
||
<view v-if="isMeleeMode" class="melee-section">
|
||
<!-- 本场战绩标题:装饰横线 + 标题图 + 装饰横线 -->
|
||
<view class="melee-title">
|
||
<view class="melee-title-line" />
|
||
<image class="melee-title-img" src="../static/battle-result.png" mode="scaleToFill" />
|
||
<view class="melee-title-line" />
|
||
</view>
|
||
|
||
<!-- 排行榜滚动列表(显示 6.5 条,第 7 条半显示提示可滚动)-->
|
||
<scroll-view class="melee-rank-scroll" scroll-y>
|
||
<view
|
||
v-for="(item, index) in meleeRankList"
|
||
:key="item.id"
|
||
:class="['rank-item', item.isSelf ? 'rank-item-self' : '', index % 2 !== 0 ? 'rank-item-light' : '']"
|
||
>
|
||
<!-- 前三名左侧渐变背景装饰(绝对定位于内容层之下)-->
|
||
<image
|
||
v-if="item.rank <= 3"
|
||
class="rank-bg-deco"
|
||
:src="getMeleeRankBgSrc(item.rank)"
|
||
mode="scaleToFill"
|
||
/>
|
||
|
||
<!-- 行内容区域(z-index 高于背景装饰)-->
|
||
<view class="rank-item-content">
|
||
<!-- 名次徽章:1-3 使用 champ 图标,4+ 灰色圆形数字 -->
|
||
<view class="rank-badge-wrap">
|
||
<image
|
||
v-if="item.rank <= 3"
|
||
class="rank-badge-img"
|
||
:src="`../static/champ${item.rank}.png`"
|
||
mode="aspectFit"
|
||
/>
|
||
<view v-else class="rank-badge-default">
|
||
<text class="rank-badge-num">{{ item.rank }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 圆形头像(前三名根据名次展示对应光环颜色)-->
|
||
<Avatar :src="item.avatar" :size="44" :borderColor="getMeleeAvatarBorderColor(item.rank)" />
|
||
|
||
<!-- 昵称 + 段位 -->
|
||
<view class="rank-player-info">
|
||
<text class="rank-player-name">{{ item.name }}</text>
|
||
<text class="rank-player-lvl">{{ item.lvlName }}</text>
|
||
</view>
|
||
|
||
<!-- 总环数 -->
|
||
<view class="rank-score">
|
||
<text class="rank-score-num">{{ item.totalRing }}</text>
|
||
<text class="rank-score-unit">环</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- ----- 查看完整成绩链接 ----- -->
|
||
<text class="view-full-btn" @click="checkBowData">查看完整成绩</text>
|
||
|
||
<!-- ----- 经验进度条区域 ----- -->
|
||
<view class="exp-section">
|
||
<text class="exp-gained">+{{ expGained }}经验</text>
|
||
<view class="exp-bar-row">
|
||
<text class="exp-lv">LV.{{ userLvl }}</text>
|
||
<view class="exp-bar-bg">
|
||
<view
|
||
class="exp-bar-fg"
|
||
:style="{ width: showExpAnim ? expPercent + '%' : '0%' }"
|
||
/>
|
||
</view>
|
||
<text class="exp-progress">{{ expCurrent }} / {{ expTotal }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部安全区占位 -->
|
||
<view class="safe-area-bottom" />
|
||
</Container>
|
||
|
||
<!-- ===== 底部:返回房间按鈕(对抗模式与大乱斗模式样式不同)===== -->
|
||
<view class="bottom-bar">
|
||
<view
|
||
:class="['btn-return', data.mode > 3 ? 'btn-return-melee' : 'btn-return-battle']"
|
||
@click="exit"
|
||
>{{ exitBtnText }}</view>
|
||
</view>
|
||
|
||
<!-- ===== 进场覆盖动效层(初始显示,点击或2.5s后消失)===== -->
|
||
<view
|
||
v-if="showOverlay"
|
||
class="overlay"
|
||
@click="closeOverlay"
|
||
>
|
||
<!-- 胜负/game-over 被摭图:大乱斗模式展示 game-over.png,对抗模式展示 you-win.png/you-lost.png -->
|
||
<image
|
||
:class="['overlay-trophy', isMeleeMode ? 'overlay-trophy-melee' : '']"
|
||
:src="isMeleeMode ? `${RESULT_CDN}/game-over.png` : (ifWin ? `${RESULT_CDN}/you-win.png` : `${RESULT_CDN}/you-lost.png`)"
|
||
mode="widthFix"
|
||
/>
|
||
|
||
<!-- 激励语图片(仅 NvN 对抗模式展示,大乱斗模式不展示)-->
|
||
<image
|
||
v-if="tipImage && !isMeleeMode"
|
||
class="overlay-tip"
|
||
:src="tipImage"
|
||
mode="widthFix"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 升级/段位提升动效组件 -->
|
||
<UserUpgrade />
|
||
</view>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* ============================
|
||
全局页面容器
|
||
============================ */
|
||
.page {
|
||
width: 750rpx;
|
||
position: relative;
|
||
}
|
||
|
||
|
||
|
||
/* ============================
|
||
Banner 区:game 胜负图
|
||
============================ */
|
||
.banner-section {
|
||
position: relative;
|
||
z-index: 10; /* 高于 vs-section,确保 game 图层级在上 */
|
||
width: 750rpx;
|
||
height: 300rpx;
|
||
display: flex;
|
||
align-items: flex-end; /* 图片裴底对齐 vs-section 顶边 */
|
||
justify-content: center;
|
||
overflow: visible; /* 允许 6rpx 叠入 vs-section */
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
/* game 胜负展示图(宽度和叠层距由 :style 动态赋値:胜 362rpx/-32rpx,负 378rpx/-40rpx) */
|
||
.banner-result {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 弹入缩放动画 */
|
||
@keyframes scale-in {
|
||
0% { transform: scale(0.3); opacity: 0; }
|
||
70% { transform: scale(1.08); opacity: 1; }
|
||
100% { transform: scale(1); }
|
||
}
|
||
|
||
.scale-in {
|
||
animation: scale-in 0.5s ease-out forwards;
|
||
}
|
||
|
||
/* ============================
|
||
VS 对战信息区
|
||
全宽背景条 246rpx 高,参考设计稿 375×123pt
|
||
============================ */
|
||
.vs-section {
|
||
position: relative;
|
||
width: 750rpx;
|
||
height: 246rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 红蓝背景切图 */
|
||
.vs-bg-img {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 750rpx;
|
||
height: 246rpx;
|
||
}
|
||
|
||
/* 玩家头像行:padding-top 补偿 banner 图叠入 vs-section 导致的视觉偏上 */
|
||
.vs-players-row {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 750rpx;
|
||
height: 180rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 30rpx 24rpx 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.team-players {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12rpx;
|
||
flex: 1;
|
||
}
|
||
|
||
.team-players-blue {
|
||
justify-content: center;
|
||
}
|
||
|
||
.team-players-red {
|
||
justify-content: center;
|
||
}
|
||
|
||
.player-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.player-name {
|
||
font-size: 22rpx;
|
||
font-weight: 400;
|
||
color: #BBCBFF;
|
||
width: 74rpx;
|
||
height: 30rpx;
|
||
text-align: center;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ---- 得分行 ---- */
|
||
.vs-scores-row {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 750rpx;
|
||
height: 66rpx; /* 33px → 66rpx */
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.score-tag-blue {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
width: 216rpx;
|
||
height: 54rpx;
|
||
background: linear-gradient(270deg, rgba(116,197,255,0) 0%, rgba(0,92,159,0.64) 100%);
|
||
padding: 0 20rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.score-tag-red {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end; /* 内容靠右对齐,使标签贴近外侧渐变区 */
|
||
gap: 12rpx;
|
||
width: 216rpx;
|
||
height: 54rpx;
|
||
/* 从中心侧(左)透明渐变到外侧(右)红色,与蓝队镜像对称 */
|
||
background: linear-gradient(90deg, rgba(193,4,1,0) 0%, rgba(193,4,1,0.64) 100%);
|
||
padding: 0 20rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.score-label {
|
||
font-size: 20rpx;
|
||
font-weight: 400;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.score-num {
|
||
font-size: 48rpx;
|
||
font-weight: 400;
|
||
line-height: 1;
|
||
}
|
||
|
||
.score-num-blue {
|
||
color: #62CBFF;
|
||
}
|
||
|
||
.score-num-red {
|
||
color: #E86B72;
|
||
}
|
||
|
||
/* ============================
|
||
MVP 卡片
|
||
设计尺寸:700×230rpx(横幅风格,背景由切图决定)
|
||
============================ */
|
||
.mvp-card {
|
||
width: 574rpx;
|
||
height: 264rpx;
|
||
margin: 40rpx auto 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 56rpx;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 蓝队 MVP(背景色透明,颜色由 mvp-blue.png 决定)*/
|
||
.mvp-card-blue,
|
||
/* 红队 MVP(背景色透明,颜色由 mvp-red.png 决定)*/
|
||
.mvp-card-red {
|
||
background: transparent;
|
||
}
|
||
|
||
/* 背景切图:绝对定位全铺,产生平行四边形渐变效果 */
|
||
.mvp-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.mvp-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4rpx;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.mvp-badge {
|
||
width: 228rpx; /* 114px → 228rpx */
|
||
}
|
||
|
||
.mvp-rings {
|
||
font-size: 30rpx;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: baseline;
|
||
align-self: flex-end;
|
||
transform: rotate(-5deg);
|
||
transform-origin: right center;
|
||
}
|
||
|
||
/* 环数数值:40rpx 加粗,金色,由父级统一倾斜 */
|
||
.mvp-rings-num {
|
||
color: #fed847;
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
display: inline-block;
|
||
line-height: 1;
|
||
margin: 0 4rpx;
|
||
}
|
||
|
||
.mvp-player {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
position: relative;
|
||
z-index: 1;
|
||
transform: translateY(-22rpx);
|
||
}
|
||
|
||
.mvp-name {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
max-width: 120rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ============================
|
||
查看完整成绩链接
|
||
============================ */
|
||
.view-full-btn {
|
||
display: block;
|
||
width: 100%;
|
||
text-align: center;
|
||
color: #4DA6FF;
|
||
font-size: 28rpx;
|
||
margin-top: 48rpx;
|
||
padding: 12rpx 0;
|
||
}
|
||
|
||
/* ============================
|
||
经验进度条区域
|
||
============================ */
|
||
.exp-section {
|
||
width: 100%;
|
||
padding: 32rpx 48rpx 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.exp-gained {
|
||
display: block;
|
||
text-align: center;
|
||
color: #FAE6BC;
|
||
font-size: 20rpx;
|
||
font-weight: 400;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.exp-bar-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.exp-lv {
|
||
font-size: 22rpx;
|
||
color: #ffffff;
|
||
font-weight: 400;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.exp-bar-bg {
|
||
flex: 1;
|
||
height: 10rpx;
|
||
background: #454545;
|
||
border-radius: 5rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.exp-bar-fg {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #DD7B13 0%, #DD7B13 75%, rgba(244, 255, 176, 0.84) 100%);
|
||
border-radius: 5rpx;
|
||
transition: width 0.8s ease-out;
|
||
/* 发光效果:近似 rgba(244,255,176,0.84) + blur 7.7px 叠加层 */
|
||
box-shadow: 0 0 14rpx rgba(244, 255, 176, 0.84);
|
||
}
|
||
|
||
.exp-progress {
|
||
font-size: 18rpx;
|
||
font-weight: 400;
|
||
color: #C3C3C3;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ============================
|
||
底部安全区占位
|
||
iPhone X 底部约 34pt → 68rpx
|
||
============================ */
|
||
.safe-area-bottom {
|
||
height: 68rpx;
|
||
}
|
||
|
||
/* ============================
|
||
底部返回房间按钮(固定定位)
|
||
设计:全宽减32px边距=343pt → 686rpx,高50pt → 100rpx
|
||
============================ */
|
||
.bottom-bar {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 750rpx;
|
||
padding: 20rpx 0;
|
||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, transparent 100%);
|
||
display: flex;
|
||
justify-content: center;
|
||
z-index: 50;
|
||
}
|
||
|
||
/* 返回房间按鈕公共样式 */
|
||
.btn-return {
|
||
background: #FED847;
|
||
font-weight: 500;
|
||
color: #000000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 对抗模式(mode <= 3)按鈕 */
|
||
.btn-return-battle {
|
||
width: 694rpx;
|
||
height: 88rpx;
|
||
border-radius: 16rpx;
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
/* 大乱斗模式(mode > 3)按鈕 */
|
||
.btn-return-melee {
|
||
width: 232rpx;
|
||
height: 70rpx;
|
||
border-radius: 44rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
/* ============================
|
||
进场覆盖动效层
|
||
============================ */
|
||
.overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 200;
|
||
/* rgba 写法等价于 background:#000 + opacity:0.7,且不影响子元素不透明度 */
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
animation: overlay-in 0.3s ease-out;
|
||
/* 两类被摭图片都是 750×666/668rpx 全宽,去除水平 padding */
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
@keyframes overlay-in {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
/* 胜利/失败奖杯图片(750×666rpx 全宽展示)*/
|
||
.overlay-trophy {
|
||
width: 750rpx;
|
||
height: 666rpx;
|
||
animation: trophy-bounce-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
/* 大乱斗结束图:高度覆盖为 668rpx */
|
||
.overlay-trophy-melee {
|
||
height: 668rpx;
|
||
height: 668rpx;
|
||
}
|
||
|
||
@keyframes trophy-bounce-in {
|
||
0% { transform: scale(0.3) translateY(-80rpx); opacity: 0; }
|
||
60% { transform: scale(1.1) translateY(10rpx); opacity: 1; }
|
||
100% { transform: scale(1) translateY(0); }
|
||
}
|
||
|
||
/* 覆盖层激励语图片(向上移入奖杯图底部空白区) */
|
||
.overlay-tip {
|
||
width: 600rpx;
|
||
margin-top: -145rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
/* 提示文字 */
|
||
.overlay-hint {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
/* ============================
|
||
大乱斗战绩区域
|
||
============================ */
|
||
.melee-section {
|
||
width: 750rpx;
|
||
/* 左右各 60rpx,使列表项宽度 = 750 - 120 = 630rpx */
|
||
padding: 0 60rpx;
|
||
box-sizing: border-box;
|
||
margin-top: 48rpx;
|
||
/* view-full-btn 有 margin-top: 48rpx,叠加后共 50rpx 间距 */
|
||
margin-bottom: 2rpx;
|
||
}
|
||
|
||
/* 标题行:装饰横线 + 标题图 + 装饰横线 */
|
||
.melee-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16rpx;
|
||
margin-bottom: 50rpx;
|
||
}
|
||
|
||
/* 两侧装饰横线 */
|
||
.melee-title-line {
|
||
width: 120rpx;
|
||
height: 2rpx;
|
||
background: #ffffff;
|
||
opacity: 0.2;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 本场战绩标题图(180×44rpx)*/
|
||
.melee-title-img {
|
||
width: 180rpx;
|
||
height: 44rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/*
|
||
排行榜滚动容器高度计算:
|
||
条目无间距,6.5 条 × 116rpx = 754rpx,第 7 条上半部分可见提示用户可滚动
|
||
*/
|
||
.melee-rank-scroll {
|
||
height: 754rpx;
|
||
}
|
||
|
||
/* 单条排行记录 */
|
||
.rank-item {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 116rpx;
|
||
background: rgba(35, 33, 30, 0.85);
|
||
/* 相对定位,使背景装饰图可绝对定位于行内 */
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 当前用户所在行:内嵌阴影高亮,不影响尺寸与相邻条目间距 */
|
||
.rank-item-self {
|
||
box-shadow: inset 0 0 0 2rpx #ff4d4d;
|
||
}
|
||
|
||
/* 斑马色块侸数行(rank 1/3/5...):白色 5% 透明底 */
|
||
.rank-item-light {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
/* 前三名行背景装饰图(SVG 设计尺寸 156×58@2x = 312×116rpx,左对齐绝对定位)*/
|
||
.rank-bg-deco {
|
||
display: block; /* 消除 image 默认 inline-block 行高撑开导致的条目间缝隙 */
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 312rpx;
|
||
height: 116rpx;
|
||
z-index: 0;
|
||
}
|
||
|
||
/* 行内容区域:flex 水平排布,层叠高于背景装饰 */
|
||
.rank-item-content {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
padding: 0 20rpx;
|
||
box-sizing: border-box;
|
||
gap: 16rpx;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 名次徽章容器(统一尺寸,内部放 champ 图标或灰色圆形)*/
|
||
.rank-badge-wrap {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
/* 徽章右侧叠加 gap(16) + margin(18) = 34rpx 到头像 */
|
||
margin-right: 18rpx;
|
||
}
|
||
|
||
/* 1-3 名:champ 图标,与容器等尺寸 */
|
||
.rank-badge-img {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
}
|
||
|
||
/* 4+ 名:灰色圆形底托 */
|
||
.rank-badge-default {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.rank-badge-num {
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
/* 玩家信息:昵称 + 段位(flex 自适应伸展)*/
|
||
.rank-player-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4rpx;
|
||
min-width: 0;
|
||
}
|
||
|
||
.rank-player-name {
|
||
font-size: 28rpx;
|
||
color: #ffffff;
|
||
font-weight: 500;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.rank-player-lvl {
|
||
font-size: 22rpx;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
}
|
||
|
||
/* 总环数(靠右对齐,与用户信息区对比)*/
|
||
.rank-score {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.rank-score-num {
|
||
font-size: 26rpx;
|
||
font-weight: 400;
|
||
color: #ffffff;
|
||
line-height: 1;
|
||
}
|
||
|
||
.rank-score-unit {
|
||
font-size: 22rpx;
|
||
font-weight: 400;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
line-height: 1;
|
||
}
|
||
</style>
|