update:vip完成

This commit is contained in:
2026-06-18 16:18:55 +08:00
parent 68f13910a3
commit 8d8ede5397
41 changed files with 1054 additions and 107 deletions

View File

@@ -250,6 +250,69 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.member-nickname {
position: relative;
display: inline-flex;
max-width: 100%;
overflow: hidden;
}
.member-nickname__text,
.member-nickname__shine {
display: block;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.member-nickname--vip .member-nickname__text {
color: #E7BA80;
}
.member-nickname--svip .member-nickname__text {
background: linear-gradient(90deg, #ffb86c, #ff4fd8, #7c5cff, #35d6ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.member-nickname__shine {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: linear-gradient(
110deg,
transparent 0%,
transparent 38%,
rgba(255, 255, 255, 0.15) 45%,
rgba(255, 255, 255, 1) 50%,
rgba(255, 255, 255, 0.15) 55%,
transparent 62%,
transparent 100%
);
background-size: 220% 100%;
background-position: 120% 0;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
pointer-events: none;
animation: memberNicknameShine 3.5s infinite ease-in-out;
}
@keyframes memberNicknameShine {
0%,
50% {
background-position: 120% 0;
}
100% {
background-position: -200% 0;
}
}
.modal { .modal {
height: 100%; height: 100%;
display: flex; display: flex;

View File

@@ -60,7 +60,7 @@ const props = defineProps({
<image <image
class="bg-image" class="bg-image"
v-if="type === 10" v-if="type === 10"
src="@/static/vip/vip-bg.png" src="https://static.shelingxingqiu.com/shootmini/static/vip/vip-bg.png"
mode="widthFix" mode="widthFix"
/> />
<view class="bg-overlay" v-if="type === 0"></view> <view class="bg-overlay" v-if="type === 0"></view>

View File

@@ -18,12 +18,14 @@ const props = defineProps({
}, },
}); });
const loading = ref(false); const loading = ref(false);
const navigating = ref(false);
/** 统一获取当前环境 token用于守卫无有效 token 时不发起接口请求 */ /** 统一获取当前环境 token用于守卫无有效 token 时不发起接口请求 */
const getToken = () => const getToken = () =>
uni.getStorageSync(`${uni.getAccountInfoSync().miniProgram.envVersion}_token`); uni.getStorageSync(`${uni.getAccountInfoSync().miniProgram.envVersion}_token`);
onShow(async () => { onShow(async () => {
navigating.value = false;
if (user.value.id && getToken()) { if (user.value.id && getToken()) {
setTimeout(async () => { setTimeout(async () => {
const state = await getUserGameState(); const state = await getUserGameState();
@@ -45,28 +47,35 @@ watch(
} }
); );
const navigateOnce = (url) =>
new Promise((resolve, reject) => {
navigating.value = true;
uni.navigateTo({
url,
success: resolve,
fail: (error) => {
navigating.value = false;
reject(error);
},
});
});
const onClick = debounce(async () => { const onClick = debounce(async () => {
if (loading.value) return; if (loading.value || navigating.value) return;
try { try {
loading.value = true; loading.value = true;
const result = await getBattleAPI(); const result = await getBattleAPI();
if (result && result.matchId) { if (result && result.matchId) {
await uni.$checkAudio(); await uni.$checkAudio();
if (result.mode <= 3) { if (result.mode <= 3) {
uni.navigateTo({ await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`);
url: `/pages/team-battle/index?battleId=${result.matchId}`,
});
} else { } else {
uni.navigateTo({ await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`);
url: `/pages/melee-battle?battleId=${result.matchId}`,
});
} }
return; return;
} }
if (game.value.roomID) { if (game.value.roomID) {
uni.navigateTo({ await navigateOnce("/pages/battle-room?roomNumber=" + game.value.roomID);
url: "/pages/battle-room?roomNumber=" + game.value.roomID,
});
} else { } else {
updateGame(false, ""); updateGame(false, "");
} }

View File

@@ -27,6 +27,14 @@ defineProps({
default: true, default: true,
}, },
}); });
const getMemberNicknameClass = (player = {}) => [
"member-nickname",
player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
player.sVip === true ? "member-nickname--svip" : "",
];
const isMember = (player = {}) => player.vip === true || player.sVip === true;
</script> </script>
<template> <template>
@@ -51,7 +59,16 @@ defineProps({
}" }"
> >
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" /> <Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" />
<text class="player-name">{{ player.name }}</text> <view
v-if="isMember(player)"
:class="['player-name', ...getMemberNicknameClass(player)]"
>
<text class="member-nickname__text">{{ player.name }}</text>
<text v-if="player.sVip === true" class="member-nickname__shine">
{{ player.name }}
</text>
</view>
<text v-else class="player-name">{{ player.name }}</text>
</view> </view>
<image <image
v-if="winner === 1" v-if="winner === 1"
@@ -70,7 +87,16 @@ defineProps({
}" }"
> >
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" /> <Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" />
<text class="player-name">{{ player.name }}</text> <view
v-if="isMember(player)"
:class="['player-name', ...getMemberNicknameClass(player)]"
>
<text class="member-nickname__text">{{ player.name }}</text>
<text v-if="player.sVip === true" class="member-nickname__shine">
{{ player.name }}
</text>
</view>
<text v-else class="player-name">{{ player.name }}</text>
</view> </view>
<image <image
v-if="winner === 2" v-if="winner === 2"
@@ -105,7 +131,16 @@ defineProps({
:size="40" :size="40"
:rank="showRank ? index + 1 : 0" :rank="showRank ? index + 1 : 0"
/> />
<text class="player-name">{{ player.name }}</text> <view
v-if="isMember(player)"
:class="['player-name', ...getMemberNicknameClass(player)]"
>
<text class="member-nickname__text">{{ player.name }}</text>
<text v-if="player.sVip === true" class="member-nickname__shine">
{{ player.name }}
</text>
</view>
<text v-else class="player-name">{{ player.name }}</text>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@@ -183,6 +218,13 @@ defineProps({
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: center; text-align: center;
} }
view.player-name {
justify-content: center;
}
.player-name .member-nickname__text,
.player-name .member-nickname__shine {
font-size: 12px;
}
.left-winner-badge { .left-winner-badge {
position: absolute; position: absolute;
width: 50px; width: 50px;

View File

@@ -1,4 +1,5 @@
<script setup> <script setup>
import { computed } from "vue";
import AppBackground from "@/components/AppBackground.vue"; import AppBackground from "@/components/AppBackground.vue";
import Avatar from "@/components/Avatar.vue"; import Avatar from "@/components/Avatar.vue";
import BowTarget from "@/components/BowTarget.vue"; import BowTarget from "@/components/BowTarget.vue";
@@ -8,6 +9,9 @@ import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user } = storeToRefs(store); const { user } = storeToRefs(store);
const isSVip = computed(() => user.value.sVip === true);
const isVip = computed(() => user.value.vip === true && user.value.sVip !== true);
const props = defineProps({ const props = defineProps({
show: { show: {
type: Boolean, type: Boolean,
@@ -35,7 +39,21 @@ const props = defineProps({
<view> <view>
<Avatar :src="user.avatar" :rankLvl="user.rankLvl" :size="45" /> <Avatar :src="user.avatar" :rankLvl="user.rankLvl" :size="45" />
<view> <view>
<text>{{ user.nickName }}</text> <view
v-if="isVip || isSVip"
:class="[
'bow-data-user-name',
'member-nickname',
isVip ? 'member-nickname--vip' : '',
isSVip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">{{ user.nickName }}</text>
<text v-if="isSVip" class="member-nickname__shine">
{{ user.nickName }}
</text>
</view>
<text v-else>{{ user.nickName }}</text>
<text>{{ user.lvlName }}</text> <text>{{ user.lvlName }}</text>
</view> </view>
</view> </view>
@@ -44,7 +62,7 @@ const props = defineProps({
</view> </view>
</view> </view>
<view :style="{ width: '100%', marginBottom: '20px' }"> <view :style="{ width: '100%', marginBottom: '20px' }">
<BowTarget :scores="arrows" /> <BowTarget :scores="arrows" :isSvip="isSVip" />
</view> </view>
<view class="desc"> <view class="desc">
<text>{{ arrows.length }}</text> <text>{{ arrows.length }}</text>
@@ -95,6 +113,13 @@ const props = defineProps({
margin-left: 10px; margin-left: 10px;
color: #fff; color: #fff;
} }
.bow-data-user-name {
max-width: 300rpx;
}
.bow-data-user-name .member-nickname__text,
.bow-data-user-name .member-nickname__shine {
max-width: 300rpx;
}
.header > view:first-child > view:last-child > text:last-child { .header > view:first-child > view:last-child > text:last-child {
font-size: 10px; font-size: 10px;
background-color: #5f51ff; background-color: #5f51ff;

View File

@@ -27,6 +27,10 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
isSvip: {
type: Boolean,
default: false,
},
mode: { mode: {
type: String, type: String,
default: "solo", // solo 单排team 双排 default: "solo", // solo 单排team 双排
@@ -79,7 +83,7 @@ function buildShotEffectKey(team, shot, index) {
} }
function shouldPlayShotEffect(shot) { function shouldPlayShotEffect(shot) {
return !!shot && Number(shot.ring) > 0; return props.isSvip && !!shot && Number(shot.ring) > 0;
} }
function clearTipTimer() { function clearTipTimer() {
@@ -272,6 +276,15 @@ function getHitStyle(shot) {
}; };
} }
function getSvipHitBgStyle(shot) {
const radius = currentHitRadiusPx.value;
const point = getShotPoint(shot);
return {
...getTargetPositionStyle(point, radius),
};
}
function getRoundTipStyle(shot) { function getRoundTipStyle(shot) {
const point = getShotPoint(shot, true); const point = getShotPoint(shot, true);
return getTargetPositionStyle( return getTargetPositionStyle(
@@ -404,6 +417,13 @@ onBeforeUnmount(() => {
}}<text v-if="bluelatestOne.ring">环</text></view }}<text v-if="bluelatestOne.ring">环</text></view
> >
<block v-for="(bow, index) in scores" :key="index"> <block v-for="(bow, index) in scores" :key="index">
<image
v-if="pMode && isSvip && bow.ring > 0 && !shouldHideRedHit(index)"
class="svip-hit-bg"
src="../static/vip/svip-xuan.png"
:style="getSvipHitBgStyle(bow)"
mode="aspectFit"
/>
<view <view
v-if="bow.ring > 0 && !shouldHideRedHit(index)" v-if="bow.ring > 0 && !shouldHideRedHit(index)"
:class="`hit ${pMode ? 'b' : 's'}-point ${ :class="`hit ${pMode ? 'b' : 's'}-point ${
@@ -417,6 +437,13 @@ onBeforeUnmount(() => {
> >
</block> </block>
<block v-for="(bow, index) in blueScores" :key="index"> <block v-for="(bow, index) in blueScores" :key="index">
<image
v-if="pMode && isSvip && bow.ring > 0 && !shouldHideBlueHit(index)"
class="svip-hit-bg"
src="../static/vip/svip-xuan.png"
:style="getSvipHitBgStyle(bow)"
mode="aspectFit"
/>
<view <view
v-if="bow.ring > 0 && !shouldHideBlueHit(index)" v-if="bow.ring > 0 && !shouldHideBlueHit(index)"
:class="`hit ${pMode ? 'b' : 's'}-point ${ :class="`hit ${pMode ? 'b' : 's'}-point ${
@@ -528,17 +555,26 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.svip-hit-bg {
position: absolute;
width: 48rpx;
height: 48rpx;
z-index: 1;
pointer-events: none;
transform-origin: center center;
animation: svip-hit-xuan 1.2s linear infinite;
}
.hit { .hit {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
z-index: 1; z-index: 2;
color: #fff; color: #fff;
transition: transform 0.2s ease, opacity 0.2s ease; transition: transform 0.2s ease, opacity 0.2s ease;
box-sizing: border-box; box-sizing: border-box;
} }
.b-point { .b-point {
border: 1px solid #fff; border: 1px solid #fff;
z-index: 1; z-index: 2;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -554,6 +590,20 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/ transform: translate(-50%, -50%);*/
margin-top: 2rpx; margin-top: 2rpx;
} }
@keyframes svip-hit-xuan {
0% {
opacity: 0.9;
transform: translate(-50%, -50%) rotate(0deg) scale(0.92);
}
50% {
opacity: 1;
transform: translate(-50%, -50%) rotate(180deg) scale(1.08);
}
100% {
opacity: 0.9;
transform: translate(-50%, -50%) rotate(360deg) scale(0.92);
}
}
@keyframes target-pump-in { @keyframes target-pump-in {
from { from {
transform: translate(-50%, -50%) scale(2); transform: translate(-50%, -50%) scale(2);

View File

@@ -55,6 +55,9 @@ const signin = () => {
} }
}; };
const isSVip = computed(() => user.value.sVip === true);
const isVip = computed(() => user.value.vip === true && user.value.sVip !== true);
const loading = ref(false); const loading = ref(false);
const pointBook = ref(null); const pointBook = ref(null);
const heat = ref(0); const heat = ref(0);
@@ -128,7 +131,21 @@ onBeforeUnmount(() => {
:size="40" :size="40"
borderColor="#333" borderColor="#333"
/> />
<text class="truncate">{{ user.nickName }}</text> <view
v-if="isVip || isSVip"
:class="[
'point-book-user-name',
'member-nickname',
isVip ? 'member-nickname--vip' : '',
isSVip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">{{ user.nickName }}</text>
<text v-if="isSVip" class="member-nickname__shine">
{{ user.nickName }}
</text>
</view>
<text v-else class="truncate">{{ user.nickName }}</text>
<image <image
v-if="heat" v-if="heat"
:src="`../static/hot${heat}.png`" :src="`../static/hot${heat}.png`"
@@ -285,7 +302,8 @@ onBeforeUnmount(() => {
width: 36rpx; width: 36rpx;
height: 36rpx; height: 36rpx;
} }
.user-header > text:nth-child(2) { .user-header > text:nth-child(2),
.user-header > .point-book-user-name {
font-weight: 500; font-weight: 500;
font-size: 30rpx; font-size: 30rpx;
color: #333333; color: #333333;

View File

@@ -15,6 +15,14 @@ const props = defineProps({
}); });
const rowCount = new Array(6).fill(0); const rowCount = new Array(6).fill(0);
const isMember = (player = {}) => player.vip === true || player.sVip === true;
const getMemberNicknameClass = (player = {}) => [
"member-nickname",
player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
player.sVip === true ? "member-nickname--svip" : "",
];
</script> </script>
<template> <template>
@@ -31,7 +39,16 @@ const rowCount = new Array(6).fill(0);
mode="widthFix" mode="widthFix"
/> />
<image :src="player.avatar || '../static/user-icon.png'" mode="widthFix" /> <image :src="player.avatar || '../static/user-icon.png'" mode="widthFix" />
<text>{{ player.name }}</text> <view
v-if="isMember(player)"
:class="['player-score-name', ...getMemberNicknameClass(player)]"
>
<text class="member-nickname__text">{{ player.name }}</text>
<text v-if="player.sVip === true" class="member-nickname__shine">
{{ player.name }}
</text>
</view>
<text v-else>{{ player.name }}</text>
<view> <view>
<view> <view>
<view v-for="(_, index) in rowCount" :key="index"> <view v-for="(_, index) in rowCount" :key="index">
@@ -96,6 +113,13 @@ const rowCount = new Array(6).fill(0);
text-overflow: ellipsis; text-overflow: ellipsis;
width: 20%; width: 20%;
} }
.player-score-name {
width: 20%;
}
.player-score-name .member-nickname__text,
.player-score-name .member-nickname__shine {
font-size: 14px;
}
.container > view:nth-child(4) { .container > view:nth-child(4) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -22,6 +22,15 @@ const props = defineProps({
const like = ref(props.data.ifLike); const like = ref(props.data.ifLike);
const likeCount = ref(props.data.likeTotal || 0); const likeCount = ref(props.data.likeTotal || 0);
const isMember = (data = {}) => data.vip === true || data.sVip === true;
const getMemberNicknameClass = (data = {}) => [
"point-rank-name",
"member-nickname",
data.vip === true && data.sVip !== true ? "member-nickname--vip" : "",
data.sVip === true ? "member-nickname--svip" : "",
];
watch( watch(
() => props.data, () => props.data,
(newVal) => { (newVal) => {
@@ -53,7 +62,13 @@ const onClick = async () => {
<view> <view>
<Avatar :src="data.avatar || '../static/user-icon.png'" :size="36" /> <Avatar :src="data.avatar || '../static/user-icon.png'" :size="36" />
<view> <view>
<text class="truncate">{{ data.name }}</text> <view v-if="isMember(data)" :class="getMemberNicknameClass(data)">
<text class="member-nickname__text">{{ data.name }}</text>
<text v-if="data.sVip === true" class="member-nickname__shine">
{{ data.name }}
</text>
</view>
<text v-else class="truncate">{{ data.name }}</text>
<view> <view>
<text>{{ data.totalDay }}</text> <text>{{ data.totalDay }}</text>
<view /> <view />
@@ -118,6 +133,14 @@ const onClick = async () => {
color: #333333; color: #333333;
margin-bottom: 5rpx; margin-bottom: 5rpx;
} }
.rank-item > view:nth-child(2) > view:last-child > .point-rank-name {
width: 200rpx;
margin-bottom: 5rpx;
}
.point-rank-name .member-nickname__text,
.point-rank-name .member-nickname__shine {
font-size: 28rpx;
}
.rank-item > view:nth-child(2) > view:last-child > view { .rank-item > view:nth-child(2) > view:last-child > view {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -19,6 +19,8 @@ const nextLvlPoints = ref(0);
const containerWidth = computed(() => const containerWidth = computed(() =>
props.showRank ? "72%" : "calc(100% - 15px)" props.showRank ? "72%" : "calc(100% - 15px)"
); );
const isSVip = computed(() => user.value.sVip === true);
const isVip = computed(() => user.value.vip === true && !isSVip.value);
const toUserPage = () => { const toUserPage = () => {
// 获取当前页面路径 // 获取当前页面路径
const pages = getCurrentPages(); const pages = getCurrentPages();
@@ -69,7 +71,18 @@ watch(
/> />
<view class="user-details" @click="toUserPage"> <view class="user-details" @click="toUserPage">
<view class="user-name"> <view class="user-name">
<text>{{ user.nickName }}</text> <view
:class="[
'member-nickname',
isVip ? 'member-nickname--vip' : '',
isSVip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">{{ user.nickName }}</text>
<text v-if="isSVip" class="member-nickname__shine">{{
user.nickName
}}</text>
</view>
<image <image
class="user-name-image" class="user-name-image"
src="../static/vip1.png" src="../static/vip1.png"
@@ -148,12 +161,13 @@ watch(
margin-bottom: 5px; margin-bottom: 5px;
} }
.user-name > text:first-child { .user-name .member-nickname {
font-size: 13px;
max-width: 180rpx; max-width: 180rpx;
white-space: nowrap; }
overflow: hidden;
text-overflow: ellipsis; .user-name .member-nickname__text,
.user-name .member-nickname__shine {
font-size: 13px;
} }
.user-name-image { .user-name-image {

View File

@@ -114,9 +114,9 @@
{ {
"path": "pages/match-detail" "path": "pages/match-detail"
}, },
{ {
"path": "pages/team-bow-data" "path": "pages/team-battle/team-bow-data"
}, },
{ {
"path": "pages/melee-bow-data" "path": "pages/melee-bow-data"
}, },

View File

@@ -92,6 +92,14 @@ const mvpTeam = computed(() => {
return blueTeamPlayers.value.some((p) => p.id === mvpPlayer.value.id) ? 1 : 2; return blueTeamPlayers.value.some((p) => p.id === mvpPlayer.value.id) ? 1 : 2;
}); });
const isMember = (player = {}) => player.vip === true || player.sVip === true;
const getMemberNicknameClass = (player = {}) => [
"member-nickname",
player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
player.sVip === true ? "member-nickname--svip" : "",
];
/** /**
* 激励语图片 URL在 onLoad 中确定 ifWin 后赋值,避免 Math.random 放在 computed 里产生缓存不一致问题) * 激励语图片 URL在 onLoad 中确定 ifWin 后赋值,避免 Math.random 放在 computed 里产生缓存不一致问题)
*/ */
@@ -128,6 +136,8 @@ const meleeRankList = computed(() => {
id: p.id, id: p.id,
avatar: p.avatar || "", avatar: p.avatar || "",
name: p.name || "", name: p.name || "",
vip: p.vip,
sVip: p.sVip,
// rank_lvl 字段可能缺失,缺失时显示空字符串,避免 getLvlName(undefined) 返回错误段位名 // rank_lvl 字段可能缺失,缺失时显示空字符串,避免 getLvlName(undefined) 返回错误段位名
lvlName: p.rank_lvl != null ? getLvlName(p.rank_lvl) : "", lvlName: p.rank_lvl != null ? getLvlName(p.rank_lvl) : "",
totalRing: resultItem.totalRing ?? 0, totalRing: resultItem.totalRing ?? 0,
@@ -332,7 +342,20 @@ function goBack() {
<view class="team-players team-players-blue"> <view class="team-players team-players-blue">
<view v-for="p in blueTeamPlayers" :key="p.id" class="player-item"> <view v-for="p in blueTeamPlayers" :key="p.id" class="player-item">
<Avatar :src="p.avatar" :size="34" borderColor="#8FB4FD" /> <Avatar :src="p.avatar" :size="34" borderColor="#8FB4FD" />
<text class="player-name player-name-blue">{{ p.name }}</text> <view
v-if="isMember(p)"
:class="[
'player-name',
'player-name-blue',
...getMemberNicknameClass(p),
]"
>
<text class="member-nickname__text">{{ p.name }}</text>
<text v-if="p.sVip === true" class="member-nickname__shine">
{{ p.name }}
</text>
</view>
<text v-else class="player-name player-name-blue">{{ p.name }}</text>
</view> </view>
</view> </view>
@@ -340,7 +363,20 @@ function goBack() {
<view class="team-players team-players-red"> <view class="team-players team-players-red">
<view v-for="p in redTeamPlayers" :key="p.id" class="player-item"> <view v-for="p in redTeamPlayers" :key="p.id" class="player-item">
<Avatar :src="p.avatar" :size="34" borderColor="#E67470" /> <Avatar :src="p.avatar" :size="34" borderColor="#E67470" />
<text class="player-name player-name-red">{{ p.name }}</text> <view
v-if="isMember(p)"
:class="[
'player-name',
'player-name-red',
...getMemberNicknameClass(p),
]"
>
<text class="member-nickname__text">{{ p.name }}</text>
<text v-if="p.sVip === true" class="member-nickname__shine">
{{ p.name }}
</text>
</view>
<text v-else class="player-name player-name-red">{{ p.name }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -385,7 +421,16 @@ function goBack() {
:size="53" :size="53"
:borderColor="mvpTeam === 1 ? '#5FADFF' : '#FF6060'" :borderColor="mvpTeam === 1 ? '#5FADFF' : '#FF6060'"
/> />
<text class="mvp-name">{{ mvpPlayer.name }}</text> <view
v-if="isMember(mvpPlayer)"
:class="['mvp-name', ...getMemberNicknameClass(mvpPlayer)]"
>
<text class="member-nickname__text">{{ mvpPlayer.name }}</text>
<text v-if="mvpPlayer.sVip === true" class="member-nickname__shine">
{{ mvpPlayer.name }}
</text>
</view>
<text v-else class="mvp-name">{{ mvpPlayer.name }}</text>
</view> </view>
</view> </view>
@@ -433,7 +478,16 @@ function goBack() {
<!-- 昵称 + 段位 --> <!-- 昵称 + 段位 -->
<view class="rank-player-info"> <view class="rank-player-info">
<text class="rank-player-name">{{ item.name }}</text> <view
v-if="isMember(item)"
:class="['rank-player-name', ...getMemberNicknameClass(item)]"
>
<text class="member-nickname__text">{{ item.name }}</text>
<text v-if="item.sVip === true" class="member-nickname__shine">
{{ item.name }}
</text>
</view>
<text v-else class="rank-player-name">{{ item.name }}</text>
<text class="rank-player-lvl">{{ item.lvlName }}</text> <text class="rank-player-lvl">{{ item.lvlName }}</text>
</view> </view>
@@ -613,6 +667,12 @@ function goBack() {
white-space: nowrap; white-space: nowrap;
} }
.player-name .member-nickname__text,
.player-name .member-nickname__shine {
font-size: 22rpx;
font-weight: 400;
}
/* ---- 得分行 ---- */ /* ---- 得分行 ---- */
.vs-scores-row { .vs-scores-row {
position: relative; position: relative;
@@ -752,6 +812,11 @@ function goBack() {
white-space: nowrap; white-space: nowrap;
} }
.mvp-name .member-nickname__text,
.mvp-name .member-nickname__shine {
font-size: 24rpx;
}
/* ============================ /* ============================
查看完整成绩链接 查看完整成绩链接
============================ */ ============================ */
@@ -1072,6 +1137,12 @@ function goBack() {
white-space: nowrap; white-space: nowrap;
} }
.rank-player-name .member-nickname__text,
.rank-player-name .member-nickname__shine {
font-size: 28rpx;
font-weight: 500;
}
.rank-player-lvl { .rank-player-lvl {
font-size: 22rpx; font-size: 22rpx;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from "vue"; import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app"; import { onLoad, onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import GuideTwo from "@/components/GuideTwo.vue"; import GuideTwo from "@/components/GuideTwo.vue";
@@ -24,6 +24,8 @@ const roomNumber = ref("");
const data = ref({}); const data = ref({});
const roomID = ref(""); const roomID = ref("");
const loading = ref(false); const loading = ref(false);
const isSVip = computed(() => user.value.sVip === true);
const isVip = computed(() => user.value.vip === true && !isSVip.value);
const enterRoom = debounce(async (number) => { const enterRoom = debounce(async (number) => {
if (loading.value) return; if (loading.value) return;
@@ -106,7 +108,18 @@ onLoad(async (options) => {
<view class="my-data"> <view class="my-data">
<view> <view>
<Avatar :rankLvl="user.rankLvl" :src="user.avatar" :size="30" /> <Avatar :rankLvl="user.rankLvl" :src="user.avatar" :size="30" />
<text class="truncate">{{ user.nickName }}</text> <view
:class="[
'member-nickname',
isVip ? 'member-nickname--vip' : '',
isSVip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">{{ user.nickName }}</text>
<text v-if="isSVip" class="member-nickname__shine">{{
user.nickName
}}</text>
</view>
<text class="my-record-btn" @click="goMyRecord">我的战绩</text> <text class="my-record-btn" @click="goMyRecord">我的战绩</text>
</view> </view>
<view> <view>
@@ -335,13 +348,17 @@ onLoad(async (options) => {
margin-left: auto; margin-left: auto;
} }
.my-data>view:first-child>text { .my-data>view:first-child>.member-nickname {
color: #fff; color: #fff;
font-size: 17px;
margin-left: 10px; margin-left: 10px;
width: 120px; width: 120px;
} }
.my-data>view:first-child>.member-nickname__text,
.my-data>view:first-child>.member-nickname__shine {
font-size: 17px;
}
.my-data>view:last-child { .my-data>view:last-child {
margin-bottom: 15px; margin-bottom: 15px;
} }

View File

@@ -67,7 +67,7 @@ onLoad(async (options) => {
const checkBowData = (selected) => { const checkBowData = (selected) => {
if (data.value.mode <= 3) { if (data.value.mode <= 3) {
uni.navigateTo({ uni.navigateTo({
url: `/pages/team-bow-data?battleId=${battleId.value}&selected=${selected}`, url: `/pages/team-battle/team-bow-data?battleId=${battleId.value}&selected=${selected}`,
}); });
} else { } else {
uni.navigateTo({ uni.navigateTo({

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue"; import { ref, onMounted, onBeforeUnmount, watch, nextTick, computed } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app"; import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue"; import BowTarget from "@/components/BowTarget.vue";
@@ -35,6 +35,11 @@ const showOfflineModal = ref(false);
/** 记录每位玩家当前半场连续 X 环数key 为 playerId用于触发 tententen 音效 */ /** 记录每位玩家当前半场连续 X 环数key 为 playerId用于触发 tententen 音效 */
const xRingStreaks = ref({}); const xRingStreaks = ref({});
const currentPlayer = computed(() =>
players.value.find((player) => String(player?.id) === String(user.value.id))
);
const isCurrentUserSvip = computed(() => currentPlayer.value?.sVip === true);
/** /**
* 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户 * 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户
*/ */
@@ -244,6 +249,7 @@ onShow(async () => {
" "
:totalRound="12" :totalRound="12"
:scores="playersScores.map((r) => r[user.id]).flat()" :scores="playersScores.map((r) => r[user.id]).flat()"
:isSvip="isCurrentUserSvip"
:stop="halfRest" :stop="halfRest"
/> />
<view :style="{ paddingBottom: '20px' }"> <view :style="{ paddingBottom: '20px' }">

View File

@@ -13,13 +13,31 @@ const currentUser = ref({
}); });
const players = ref([]); const players = ref([]);
function getRingTotal(arrows = []) {
return arrows.reduce((last, next) => last + (Number(next?.ring) || 0), 0);
}
function getScoreLabel(score) {
if (!score) return "";
return score.ringX ? "X" : score.ring || "";
}
const isMember = (player = {}) => player.vip === true || player.sVip === true;
const getMemberNicknameClass = (player = {}) => [
"player-name",
"member-nickname",
player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
player.sVip === true ? "member-nickname--svip" : "",
];
onLoad(async (options) => { onLoad(async (options) => {
if (!options.battleId) return; if (!options.battleId) return;
const result = await getBattleAPI(options.battleId || "59348111700660224"); const result = await getBattleAPI(options.battleId || "59348111700660224");
const plist = result.teams?.[0]?.players || [];
players.value = result.resultList.map((item, index) => { players.value = result.resultList.map((item, index) => {
const plist = result.teams[0] ? result.teams[0].players : []; const p = plist.find((p) => String(p.id) === String(item.userId));
const p = plist.find((p) => p.id === item.userId); const arrows = Array.from({ length: 12 }, () => ({}));
const arrows = new Array(12);
result.rounds.forEach((r, index) => { result.rounds.forEach((r, index) => {
if (r.shoots[item.userId]) { if (r.shoots[item.userId]) {
r.shoots[item.userId].forEach((s, index2) => { r.shoots[item.userId].forEach((s, index2) => {
@@ -29,9 +47,11 @@ onLoad(async (options) => {
}); });
return { return {
...item, ...item,
...p,
userId: item.userId,
rank: index + 1, rank: index + 1,
name: p.name, name: p?.name || item.name,
avatar: p.avatar || "", avatar: p?.avatar || item.avatar || "",
arrows, arrows,
}; };
}); });
@@ -68,18 +88,27 @@ onLoad(async (options) => {
class="player-bg" class="player-bg"
/> />
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" /> <Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" />
<text>{{ player.name }}</text> <view v-if="isMember(player)" :class="getMemberNicknameClass(player)">
<text class="member-nickname__text">{{ player.name }}</text>
<text v-if="player.sVip === true" class="member-nickname__shine">
{{ player.name }}
</text>
</view>
<text v-else>{{ player.name }}</text>
</view> </view>
</view> </view>
<view :style="{ marginTop: '10px' }"> <view :style="{ marginTop: '10px' }">
<BowTarget :scores="currentUser.arrows" /> <BowTarget
:scores="currentUser.arrows"
:isSvip="currentUser.sVip === true"
/>
</view> </view>
<view class="score-text" <view class="score-text"
><text :style="{ color: '#fed847' }">{{ ><text :style="{ color: '#fed847' }">{{
currentUser.arrows.length currentUser.arrows.length
}}</text }}</text
>支箭<text :style="{ color: '#fed847' }">{{ >支箭<text :style="{ color: '#fed847' }">{{
currentUser.arrows.reduce((last, next) => last + next.ring, 0) getRingTotal(currentUser.arrows)
}}</text }}</text
></view ></view
> >
@@ -90,7 +119,7 @@ onLoad(async (options) => {
class="score-item" class="score-item"
:style="{ width: '13vw', height: '13vw' }" :style="{ width: '13vw', height: '13vw' }"
> >
{{ score.ringX ? "X" : score.ring }} {{ getScoreLabel(score) }}
</view> </view>
</view> </view>
</view> </view>
@@ -149,6 +178,16 @@ onLoad(async (options) => {
text-align: center; text-align: center;
position: relative; position: relative;
} }
.players > view > .player-name {
margin: 5px 0;
width: 80%;
position: relative;
justify-content: center;
}
.player-name .member-nickname__text,
.player-name .member-nickname__shine {
font-size: 12px;
}
.score-text { .score-text {
width: 100%; width: 100%;
color: #fff; color: #fff;

View File

@@ -32,8 +32,8 @@ const memberTypes = [
desc: "特享约战竞技次数包、专属会员标识", desc: "特享约战竞技次数包、专属会员标识",
benefitTitle: "普通会员专属权益", benefitTitle: "普通会员专属权益",
themeClass: "vip-page--normal", themeClass: "vip-page--normal",
heroCard: "../../static/vip/vip-title.png", heroCard: "https://static.shelingxingqiu.com/shootmini/static/vip/vip-title.png",
activeHeroCard: "../../static/vip/vip-title2.png", activeHeroCard: "https://static.shelingxingqiu.com/shootmini/static/vip/vip-title2.png",
orderIcon: "../../static/vip/vip-order.png", orderIcon: "../../static/vip/vip-order.png",
heroBadge: "../../static/vip/normal-hero-badge.png", heroBadge: "../../static/vip/normal-hero-badge.png",
buttonClass: "activate-btn--normal", buttonClass: "activate-btn--normal",
@@ -58,8 +58,8 @@ const memberTypes = [
desc: "尊享专属特效、无限制约战竞技、专属会员标识", desc: "尊享专属特效、无限制约战竞技、专属会员标识",
benefitTitle: "超级会员专属权益", benefitTitle: "超级会员专属权益",
themeClass: "vip-page--super", themeClass: "vip-page--super",
heroCard: "../../static/vip/svip-title.png", heroCard: "https://static.shelingxingqiu.com/shootmini/static/vip/svip-title.png",
activeHeroCard: "../../static/vip/svip-title2.png", activeHeroCard: "https://static.shelingxingqiu.com/shootmini/static/vip/svip-title2.png",
orderIcon: "../../static/vip/svip-order.png", orderIcon: "../../static/vip/svip-order.png",
heroBadge: "../../static/vip/super-hero-badge.png", heroBadge: "../../static/vip/super-hero-badge.png",
buttonClass: "activate-btn--super", buttonClass: "activate-btn--super",
@@ -115,7 +115,7 @@ const isVipActive = (type) => {
return toTimestamp(getVipExpiredValue(type)) > Date.now(); return toTimestamp(getVipExpiredValue(type)) > Date.now();
}; };
// 会员卡片展示日期,不展示具体时分秒 // 会员卡片展示完整到期时间
const formatVipDate = (value) => { const formatVipDate = (value) => {
const timestamp = toTimestamp(value); const timestamp = toTimestamp(value);
if (!timestamp) return ""; if (!timestamp) return "";
@@ -123,7 +123,10 @@ const formatVipDate = (value) => {
const year = date.getFullYear(); const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`; const hour = String(date.getHours()).padStart(2, "0");
const minute = String(date.getMinutes()).padStart(2, "0");
const second = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}; };
// 会员生效时使用 title2 切图,未生效时沿用原来的开通引导图。 // 会员生效时使用 title2 切图,未生效时沿用原来的开通引导图。

View File

@@ -61,6 +61,18 @@ const goPay = async () => {
} }
}; };
const copyOrderId = (orderId) => {
uni.setClipboardData({
data: String(orderId),
success: () => {
uni.showToast({
title: "复制成功",
icon: "success",
});
},
});
};
const cancelOrder = async () => { const cancelOrder = async () => {
const result = await cancelOrderListAPI(data.value.orderId); const result = await cancelOrderListAPI(data.value.orderId);
data.value = result; data.value = result;
@@ -78,8 +90,13 @@ const cancelOrder = async () => {
> >
<view class="order"> <view class="order">
<view> <view>
<text>商品名{{ data.vipName }}</text> <text>{{ data.vipName }}</text>
<text>订单号{{ data.orderId }}</text> <view class="order-number">
<text>订单号{{ data.orderId }}</text>
<text class="copy-action" @click.stop="copyOrderId(data.orderId)"
>复制</text
>
</view>
<text>下单时间{{ data.vipCreateAt }}</text> <text>下单时间{{ data.vipCreateAt }}</text>
<text <text
>支付时间{{ >支付时间{{
@@ -141,4 +158,25 @@ const cancelOrder = async () => {
text-align: center; text-align: center;
font-size: 11px; font-size: 11px;
} }
.order-number {
display: flex;
align-items: center;
color: #666666;
font-size: 26rpx;
margin-top: 10rpx;
}
.order-number > text:first-child {
flex: 1;
min-width: 0;
word-break: break-all;
}
.copy-action {
flex-shrink: 0;
margin-left: 16rpx;
padding: 2rpx 14rpx;
color: #1f6ed4;
font-size: 24rpx;
line-height: 34rpx;
}
</style> </style>

View File

@@ -80,15 +80,15 @@ onShow(() => {
>{{ orderStatusNames[item.orderStatus] }}</view >{{ orderStatusNames[item.orderStatus] }}</view
> >
<text>{{ item.vipName }}</text> <text>{{ item.vipName }}</text>
<!-- <text>订单号{{ item.orderId }}</text> --> <text>订单号{{ item.orderId }}</text>
<!-- <text>创建时间{{ item.vipCreateAt }}</text> --> <text>创建时间{{ item.orderCreateAt }}</text>
<text <!-- <text
>支付时间{{ >支付时间{{
item.orderStatus === 4 ? item.paymentTime : "" item.orderStatus === 4 ? item.paymentTime : ""
}}</text }}</text
> > -->
<text>金额{{ item.total }} </text> <text>金额{{ item.total }} </text>
<text>支付方式微信</text> <!-- <text>支付方式微信</text> -->
<!-- <text class="renew-action" @click.stop="openAutoRenewDialog(item)"> <!-- <text class="renew-action" @click.stop="openAutoRenewDialog(item)">
自动续费 自动续费
</text> --> </text> -->
@@ -135,6 +135,7 @@ onShow(() => {
.order-item > text:nth-child(2) { .order-item > text:nth-child(2) {
color: #333333; color: #333333;
font-size: 30rpx; font-size: 30rpx;
font-weight: bold;
} }
.order-item > text { .order-item > text {
color: #666666; color: #666666;

View File

@@ -11,12 +11,14 @@ import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user } = storeToRefs(store); const { user } = storeToRefs(store);
const arrows = ref([]); const arrows = ref([]);
const isSvip = ref(false);
const total = ref(0); const total = ref(0);
onLoad(async (options) => { onLoad(async (options) => {
if (!options.id) return; if (!options.id) return;
const result = await getPractiseAPI(options.id || 176); const result = await getPractiseAPI(options.id || 176);
arrows.value = result.details; arrows.value = result.details;
isSvip.value = result.sVip === true;
total.value = result.details.length; total.value = result.details.length;
}); });
</script> </script>
@@ -34,7 +36,7 @@ onLoad(async (options) => {
</view> </view>
</view> --> </view> -->
<view :style="{ marginBottom: '20px' }"> <view :style="{ marginBottom: '20px' }">
<BowTarget :scores="arrows" /> <BowTarget :scores="arrows" :isSvip="isSvip" />
</view> </view>
<view class="desc"> <view class="desc">
<text>{{ arrows.length }}</text> <text>{{ arrows.length }}</text>

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from "vue"; import { computed, ref } from "vue";
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import ScreenHint from "@/components/ScreenHint.vue"; import ScreenHint from "@/components/ScreenHint.vue";
@@ -21,6 +21,8 @@ const { user, device } = storeToRefs(store);
const justBind = ref(false); const justBind = ref(false);
const calibration = ref(false); const calibration = ref(false);
const token = ref(null); const token = ref(null);
const isSVip = computed(() => user.value.sVip === true);
const isVip = computed(() => user.value.vip === true && !isSVip.value);
// 扫描二维码方法 // 扫描二维码方法
const handleScan = () => { const handleScan = () => {
@@ -209,7 +211,18 @@ onShow(() => {
mode="widthFix" mode="widthFix"
:style="{ borderRadius: '50%' }" :style="{ borderRadius: '50%' }"
/> />
<text>{{ user.nickName }}</text> <view
:class="[
'member-nickname',
isVip ? 'member-nickname--vip' : '',
isSVip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">{{ user.nickName }}</text>
<text v-if="isSVip" class="member-nickname__shine">{{
user.nickName
}}</text>
</view>
</view> </view>
</view> </view>
<!-- <block v-if="calibration"> --> <!-- <block v-if="calibration"> -->
@@ -267,7 +280,18 @@ onShow(() => {
mode="widthFix" mode="widthFix"
:style="{ borderRadius: '50%' }" :style="{ borderRadius: '50%' }"
/> />
<text>{{ user.nickName }}</text> <view
:class="[
'member-nickname',
isVip ? 'member-nickname--vip' : '',
isSVip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">{{ user.nickName }}</text>
<text v-if="isSVip" class="member-nickname__shine">{{
user.nickName
}}</text>
</view>
</view> </view>
</view> </view>
<view :style="{ marginTop: '240rpx' }"> <view :style="{ marginTop: '240rpx' }">
@@ -405,6 +429,15 @@ onShow(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: center; text-align: center;
} }
.device-binded .member-nickname {
justify-content: center;
width: 120px;
}
.device-binded .member-nickname__text,
.device-binded .member-nickname__shine {
font-size: 26rpx;
text-align: center;
}
.device-binded > image { .device-binded > image {
width: 100rpx; width: 100rpx;
margin: 0 20px; margin: 0 20px;

View File

@@ -30,6 +30,7 @@ const { user } = storeToRefs(store);
const start = ref(false); const start = ref(false);
const scores = ref([]); const scores = ref([]);
const isSvip = ref(false);
const total = 12; const total = 12;
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */ /** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
const xRingStreak = ref(0); const xRingStreak = ref(0);
@@ -48,6 +49,7 @@ onLoad((options) => {
const onReady = async () => { const onReady = async () => {
await startPractiseAPI(); await startPractiseAPI();
scores.value = []; scores.value = [];
isSvip.value = false;
xRingStreak.value = 0; // 新一局开始,重置 X 环连续计数 xRingStreak.value = 0; // 新一局开始,重置 X 环连续计数
start.value = true; start.value = true;
audioManager.play("练习开始"); audioManager.play("练习开始");
@@ -79,6 +81,7 @@ function checkAndPlayTententen(isXRing) {
async function onReceiveMessage(msg) { async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) { if (msg.type === MESSAGETYPESV2.ShootResult) {
const prevLen = scores.value.length; const prevLen = scores.value.length;
isSvip.value = msg.sVip === true;
scores.value = msg.details; scores.value = msg.details;
// 有新箭时取最后一箭判断是否 X 环并检测连续计数 // 有新箭时取最后一箭判断是否 X 环并检测连续计数
if (scores.value.length > prevLen) { if (scores.value.length > prevLen) {
@@ -101,6 +104,7 @@ async function onComplete() {
practiseResult.value = {}; practiseResult.value = {};
start.value = false; start.value = false;
scores.value = []; scores.value = [];
isSvip.value = false;
xRingStreak.value = 0; // 重新开始练习,重置 X 环连续计数 xRingStreak.value = 0; // 重新开始练习,重置 X 环连续计数
const result = await createPractiseAPI(total, 120); const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id; if (result) practiseId.value = result.id;
@@ -174,6 +178,7 @@ onBeforeUnmount(() => {
:totalRound="start ? total / 4 : 0" :totalRound="start ? total / 4 : 0"
:currentRound="scores.length % 3" :currentRound="scores.length % 3"
:scores="scores" :scores="scores"
:isSvip="isSvip"
/> />
<ScorePanel2 :arrows="scores" /> <ScorePanel2 :arrows="scores" />
<ScoreResult <ScoreResult

View File

@@ -30,6 +30,7 @@ const { user } = storeToRefs(store);
const start = ref(false); const start = ref(false);
const scores = ref([]); const scores = ref([]);
const isSvip = ref(false);
const total = 36; const total = 36;
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */ /** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
const xRingStreak = ref(0); const xRingStreak = ref(0);
@@ -47,6 +48,7 @@ onLoad((options) => {
const onReady = async () => { const onReady = async () => {
await startPractiseAPI(); await startPractiseAPI();
scores.value = []; scores.value = [];
isSvip.value = false;
xRingStreak.value = 0; // 新一局开始,重置 X 环连续计数 xRingStreak.value = 0; // 新一局开始,重置 X 环连续计数
start.value = true; start.value = true;
audioManager.play("练习开始"); audioManager.play("练习开始");
@@ -78,6 +80,7 @@ function checkAndPlayTententen(isXRing) {
async function onReceiveMessage(msg) { async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) { if (msg.type === MESSAGETYPESV2.ShootResult) {
const prevLen = scores.value.length; const prevLen = scores.value.length;
isSvip.value = msg.sVip === true;
scores.value = msg.details; scores.value = msg.details;
// 有新箭时取最后一箭判断是否 X 环并检测连续计数 // 有新箭时取最后一箭判断是否 X 环并检测连续计数
if (scores.value.length > prevLen) { if (scores.value.length > prevLen) {
@@ -116,6 +119,7 @@ async function onComplete() {
practiseResult.value = {}; practiseResult.value = {};
start.value = false; start.value = false;
scores.value = []; scores.value = [];
isSvip.value = false;
xRingStreak.value = 0; // 重新开始练习,重置 X 环连续计数 xRingStreak.value = 0; // 重新开始练习,重置 X 环连续计数
const result = await createPractiseAPI(total, 3600); const result = await createPractiseAPI(total, 3600);
if (result) practiseId.value = result.id; if (result) practiseId.value = result.id;
@@ -175,6 +179,7 @@ onBeforeUnmount(() => {
:currentRound="scores.length" :currentRound="scores.length"
:totalRound="start ? total : 0" :totalRound="start ? total : 0"
:scores="scores" :scores="scores"
:isSvip="isSvip"
/> />
<ScorePanel <ScorePanel
v-if="start" v-if="start"

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from "vue"; import { computed, ref } from "vue";
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import Guide from "@/components/Guide.vue"; import Guide from "@/components/Guide.vue";
@@ -15,6 +15,8 @@ const { user, device, online } = storeToRefs(store);
const data = ref({}); const data = ref({});
const showTargetPicker = ref(false); const showTargetPicker = ref(false);
const pendingPractiseType = ref(""); const pendingPractiseType = ref("");
const isSVip = computed(() => user.value.sVip === true);
const isVip = computed(() => user.value.vip === true && !isSVip.value);
const goPractise = async (type) => { const goPractise = async (type) => {
if (!canEenter(user.value, device.value, online.value)) return; if (!canEenter(user.value, device.value, online.value)) return;
@@ -49,7 +51,18 @@ onShow(async () => {
<view> <view>
<view> <view>
<Avatar :rankLvl="user.rankLvl" :src="user.avatar" :size="30" /> <Avatar :rankLvl="user.rankLvl" :src="user.avatar" :size="30" />
<text class="truncate">{{ user.nickName }}</text> <view
:class="[
'member-nickname',
isVip ? 'member-nickname--vip' : '',
isSVip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">{{ user.nickName }}</text>
<text v-if="isSVip" class="member-nickname__shine">{{
user.nickName
}}</text>
</view>
</view> </view>
<view> <view>
<text>已练习打卡</text> <text>已练习打卡</text>
@@ -132,12 +145,15 @@ onShow(async () => {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
} }
.practise-data > view:first-child > view:first-child > text { .practise-data > view:first-child > view:first-child .member-nickname {
color: #fff; color: #fff;
margin-left: 10px; margin-left: 10px;
font-size: 16px;
width: 120px; width: 120px;
} }
.practise-data > view:first-child > view:first-child .member-nickname__text,
.practise-data > view:first-child > view:first-child .member-nickname__shine {
font-size: 16px;
}
.practise-data > view:first-child > view:last-child > text:nth-child(2) { .practise-data > view:first-child > view:last-child > text:nth-child(2) {
color: #f7d247; color: #f7d247;
margin: 0 3px; margin: 0 3px;

View File

@@ -102,6 +102,8 @@ const buildDefaultMyData = () => ({
userId: user.value.id, userId: user.value.id,
name: user.value.nickName, name: user.value.nickName,
avatar: user.value.avatar, avatar: user.value.avatar,
vip: user.value.vip,
sVip: user.value.sVip,
totalScore: 0, totalScore: 0,
mvpCount: 0, mvpCount: 0,
tenRings: 0, tenRings: 0,
@@ -154,6 +156,15 @@ const getRankUnit = (index = selectedIndex.value) => {
return "次"; return "次";
}; };
const isMember = (item = {}) => item.vip === true || item.sVip === true;
const getMemberNicknameClass = (item = {}) => [
"rank-list-player-name",
"member-nickname",
item.vip === true && item.sVip !== true ? "member-nickname--vip" : "",
item.sVip === true ? "member-nickname--svip" : "",
];
// 统一设置页面当前的视觉滚动状态,避免吸顶和顶部背景不同步。 // 统一设置页面当前的视觉滚动状态,避免吸顶和顶部背景不同步。
const syncScrollVisualState = (scrollTop = 0) => { const syncScrollVisualState = (scrollTop = 0) => {
currentScrollTop.value = scrollTop; currentScrollTop.value = scrollTop;
@@ -441,7 +452,13 @@ const measureTabsMetrics = () => {
</view> </view>
<Avatar :src="item.avatar" /> <Avatar :src="item.avatar" />
<view class="rank-item-content"> <view class="rank-item-content">
<text class="truncate">{{ item.name }}</text> <view v-if="isMember(item)" :class="getMemberNicknameClass(item)">
<text class="member-nickname__text">{{ item.name }}</text>
<text v-if="item.sVip === true" class="member-nickname__shine">
{{ item.name }}
</text>
</view>
<text v-else class="rank-list-player-name truncate">{{ item.name }}</text>
<text>{{ formatLevelText(item) }}</text> <text>{{ formatLevelText(item) }}</text>
</view> </view>
<text class="rank-item-integral"> <text class="rank-item-integral">
@@ -479,7 +496,23 @@ const measureTabsMetrics = () => {
<text>{{ getDisplayMyRank(currentMyData) }}</text> <text>{{ getDisplayMyRank(currentMyData) }}</text>
<Avatar :src="currentMyData.avatar || user.avatar" /> <Avatar :src="currentMyData.avatar || user.avatar" />
<view class="rank-item-content"> <view class="rank-item-content">
<text class="truncate">{{ currentMyData.name || user.nickName }}</text> <view
v-if="isMember(currentMyData)"
:class="getMemberNicknameClass(currentMyData)"
>
<text class="member-nickname__text">
{{ currentMyData.name || user.nickName }}
</text>
<text
v-if="currentMyData.sVip === true"
class="member-nickname__shine"
>
{{ currentMyData.name || user.nickName }}
</text>
</view>
<text v-else class="rank-list-player-name truncate">
{{ currentMyData.name || user.nickName }}
</text>
<text>{{ formatLevelText(currentMyData) }}</text> <text>{{ formatLevelText(currentMyData) }}</text>
</view> </view>
<text class="rank-item-integral"> <text class="rank-item-integral">
@@ -653,6 +686,18 @@ const measureTabsMetrics = () => {
width: 120px; width: 120px;
} }
.rank-list-player-name {
color: #fff;
font-size: 14px;
margin-bottom: 3px;
width: 120px;
}
.rank-list-player-name .member-nickname__text,
.rank-list-player-name .member-nickname__shine {
font-size: 14px;
}
.rank-list-item > text:last-child { .rank-list-item > text:last-child {
margin-right: 10px; margin-right: 10px;
width: 56px; width: 56px;

View File

@@ -55,6 +55,17 @@ const rankLoading = ref(false);
const scoreRankList = ref([]); const scoreRankList = ref([]);
const mvpRankList = ref([]); const mvpRankList = ref([]);
const tenRingRankList = ref([]); const tenRingRankList = ref([]);
const isSVip = computed(() => user.value.sVip === true);
const isVip = computed(() => user.value.vip === true && !isSVip.value);
const isMember = (item = {}) => item.vip === true || item.sVip === true;
const getMemberNicknameClass = (item = {}) => [
"rank-preview-name",
"member-nickname",
item.vip === true && item.sVip !== true ? "member-nickname--vip" : "",
item.sVip === true ? "member-nickname--svip" : "",
];
// 根据接口返回结构提取榜单数组,兼容数组和对象两种返回形式。 // 根据接口返回结构提取榜单数组,兼容数组和对象两种返回形式。
const getRankListFromResponse = (result) => { const getRankListFromResponse = (result) => {
@@ -314,7 +325,20 @@ onShow(async () => {
:rankLvl="seasonStats.rankLvl" :rankLvl="seasonStats.rankLvl"
:size="30" :size="30"
/> />
<text>{{ seasonStats.nickName || user.nickName }}</text> <view
:class="[
'member-nickname',
isVip ? 'member-nickname--vip' : '',
isSVip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">
{{ seasonStats.nickName || user.nickName }}
</text>
<text v-if="isSVip" class="member-nickname__shine">
{{ seasonStats.nickName || user.nickName }}
</text>
</view>
</view> </view>
<view <view
class="ranking-season" class="ranking-season"
@@ -476,7 +500,13 @@ onShow(async () => {
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }" :style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/> />
<view> <view>
<text class="truncate">{{ item.name }}</text> <view v-if="isMember(item)" :class="getMemberNicknameClass(item)">
<text class="member-nickname__text">{{ item.name }}</text>
<text v-if="item.sVip === true" class="member-nickname__shine">
{{ item.name }}
</text>
</view>
<text v-else class="rank-preview-name truncate">{{ item.name }}</text>
<text>{{ formatRankSubTitle(item) }}</text> <text>{{ formatRankSubTitle(item) }}</text>
</view> </view>
<text>{{ item.totalScore || 0 }}<text></text></text> <text>{{ item.totalScore || 0 }}<text></text></text>
@@ -499,7 +529,13 @@ onShow(async () => {
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }" :style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/> />
<view> <view>
<text class="truncate">{{ item.name }}</text> <view v-if="isMember(item)" :class="getMemberNicknameClass(item)">
<text class="member-nickname__text">{{ item.name }}</text>
<text v-if="item.sVip === true" class="member-nickname__shine">
{{ item.name }}
</text>
</view>
<text v-else class="rank-preview-name truncate">{{ item.name }}</text>
<text>{{ formatRankSubTitle(item) }}</text> <text>{{ formatRankSubTitle(item) }}</text>
</view> </view>
<text>{{ item.mvpCount || 0 }}<text></text></text> <text>{{ item.mvpCount || 0 }}<text></text></text>
@@ -522,7 +558,13 @@ onShow(async () => {
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }" :style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/> />
<view> <view>
<text class="truncate">{{ item.name }}</text> <view v-if="isMember(item)" :class="getMemberNicknameClass(item)">
<text class="member-nickname__text">{{ item.name }}</text>
<text v-if="item.sVip === true" class="member-nickname__shine">
{{ item.name }}
</text>
</view>
<text v-else class="rank-preview-name truncate">{{ item.name }}</text>
<text>{{ formatRankSubTitle(item) }}</text> <text>{{ formatRankSubTitle(item) }}</text>
</view> </view>
<text>{{ item.tenRings ?? item.TenRings ?? 0 }}<text></text></text> <text>{{ item.tenRings ?? item.TenRings ?? 0 }}<text></text></text>
@@ -579,8 +621,14 @@ onShow(async () => {
font-size: 14px; font-size: 14px;
} }
.user-info > text { .user-info > .member-nickname {
margin-left: 15px; margin-left: 15px;
max-width: 220rpx;
}
.user-info .member-nickname__text,
.user-info .member-nickname__shine {
font-size: 14px;
} }
.ranking-season { .ranking-season {
@@ -773,6 +821,17 @@ onShow(async () => {
width: 120px; width: 120px;
} }
.rank-preview-name {
color: #fff9;
font-size: 14px;
width: 120px;
}
.rank-preview-name .member-nickname__text,
.rank-preview-name .member-nickname__shine {
font-size: 14px;
}
.rank-item > view:nth-child(3) > text:last-child { .rank-item > view:nth-child(3) > text:last-child {
color: #fff4; color: #fff4;
font-size: 13px; font-size: 13px;

View File

@@ -18,12 +18,14 @@ const props = defineProps({
}, },
}); });
const loading = ref(false); const loading = ref(false);
const navigating = ref(false);
/** 统一获取当前环境 token用于守卫无有效 token 时不发起接口请求 */ /** 统一获取当前环境 token用于守卫无有效 token 时不发起接口请求 */
const getToken = () => const getToken = () =>
uni.getStorageSync(`${uni.getAccountInfoSync().miniProgram.envVersion}_token`); uni.getStorageSync(`${uni.getAccountInfoSync().miniProgram.envVersion}_token`);
onShow(async () => { onShow(async () => {
navigating.value = false;
if (user.value.id && getToken()) { if (user.value.id && getToken()) {
setTimeout(async () => { setTimeout(async () => {
const state = await getUserGameState(); const state = await getUserGameState();
@@ -45,28 +47,35 @@ watch(
} }
); );
const navigateOnce = (url) =>
new Promise((resolve, reject) => {
navigating.value = true;
uni.navigateTo({
url,
success: resolve,
fail: (error) => {
navigating.value = false;
reject(error);
},
});
});
const onClick = debounce(async () => { const onClick = debounce(async () => {
if (loading.value) return; if (loading.value || navigating.value) return;
try { try {
loading.value = true; loading.value = true;
const result = await getBattleAPI(); const result = await getBattleAPI();
if (result && result.matchId) { if (result && result.matchId) {
await uni.$checkAudio(); await uni.$checkAudio();
if (result.mode <= 3) { if (result.mode <= 3) {
uni.navigateTo({ await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`);
url: `/pages/team-battle/index?battleId=${result.matchId}`,
});
} else { } else {
uni.navigateTo({ await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`);
url: `/pages/melee-battle?battleId=${result.matchId}`,
});
} }
return; return;
} }
if (game.value.roomID) { if (game.value.roomID) {
uni.navigateTo({ await navigateOnce("/pages/battle-room?roomNumber=" + game.value.roomID);
url: "/pages/battle-room?roomNumber=" + game.value.roomID,
});
} else { } else {
updateGame(false, ""); updateGame(false, "");
} }

View File

@@ -51,7 +51,7 @@ const normalRounds = computed(() => {
<view v-for="(result, index) in roundResults" :key="index"> <view v-for="(result, index) in roundResults" :key="index">
<block v-if="index + 1 > normalRounds"> <block v-if="index + 1 > normalRounds">
<image <image
:src="RoundImages[`gold${index + 1 - normalRounds}`]" :src="RoundImages[`gold${result.goldRound || index + 1 - normalRounds}`]"
mode="widthFix" mode="widthFix"
/> />
</block> </block>
@@ -86,7 +86,7 @@ const normalRounds = computed(() => {
<view v-for="(result, index) in roundResults" :key="index"> <view v-for="(result, index) in roundResults" :key="index">
<block v-if="index + 1 > normalRounds"> <block v-if="index + 1 > normalRounds">
<image <image
:src="RoundImages[`gold${index + 1 - normalRounds}`]" :src="RoundImages[`gold${result.goldRound || index + 1 - normalRounds}`]"
mode="widthFix" mode="widthFix"
/> />
</block> </block>

View File

@@ -27,6 +27,14 @@ defineProps({
default: true, default: true,
}, },
}); });
const getMemberNicknameClass = (player = {}) => [
"member-nickname",
player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
player.sVip === true ? "member-nickname--svip" : "",
];
const isMember = (player = {}) => player.vip === true || player.sVip === true;
</script> </script>
<template> <template>
@@ -51,7 +59,16 @@ defineProps({
}" }"
> >
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" /> <Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" />
<text class="player-name">{{ player.name }}</text> <view
v-if="isMember(player)"
:class="['player-name', ...getMemberNicknameClass(player)]"
>
<text class="member-nickname__text">{{ player.name }}</text>
<text v-if="player.sVip === true" class="member-nickname__shine">
{{ player.name }}
</text>
</view>
<text v-else class="player-name">{{ player.name }}</text>
</view> </view>
<image <image
v-if="winner === 1" v-if="winner === 1"
@@ -70,7 +87,16 @@ defineProps({
}" }"
> >
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" /> <Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" />
<text class="player-name">{{ player.name }}</text> <view
v-if="isMember(player)"
:class="['player-name', ...getMemberNicknameClass(player)]"
>
<text class="member-nickname__text">{{ player.name }}</text>
<text v-if="player.sVip === true" class="member-nickname__shine">
{{ player.name }}
</text>
</view>
<text v-else class="player-name">{{ player.name }}</text>
</view> </view>
<image <image
v-if="winner === 2" v-if="winner === 2"
@@ -105,7 +131,16 @@ defineProps({
:size="40" :size="40"
:rank="showRank ? index + 1 : 0" :rank="showRank ? index + 1 : 0"
/> />
<text class="player-name">{{ player.name }}</text> <view
v-if="isMember(player)"
:class="['player-name', ...getMemberNicknameClass(player)]"
>
<text class="member-nickname__text">{{ player.name }}</text>
<text v-if="player.sVip === true" class="member-nickname__shine">
{{ player.name }}
</text>
</view>
<text v-else class="player-name">{{ player.name }}</text>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@@ -183,6 +218,13 @@ defineProps({
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: center; text-align: center;
} }
view.player-name {
justify-content: center;
}
.player-name .member-nickname__text,
.player-name .member-nickname__shine {
font-size: 12px;
}
.left-winner-badge { .left-winner-badge {
position: absolute; position: absolute;
width: 50px; width: 50px;

View File

@@ -35,6 +35,14 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
redTeam: {
type: Array,
default: () => [],
},
blueTeam: {
type: Array,
default: () => [],
},
latestShotFlash: { latestShotFlash: {
type: Object, type: Object,
default: null, default: null,
@@ -92,8 +100,17 @@ function buildShotEffectKey(team, shot, fallbackKey = "") {
); );
} }
function shouldPlayShotEffect(shot) { function findShotPlayer(shot, team) {
return !!shot && Number(shot.ring) > 0; const players = team === "red" ? props.redTeam : props.blueTeam;
return players.find((player) => String(player?.id) === String(shot?.playerId));
}
function isSvipShot(shot, team) {
return findShotPlayer(shot, team)?.sVip === true;
}
function shouldPlayShotEffect(shot, team) {
return !!shot && Number(shot.ring) > 0 && isSvipShot(shot, team);
} }
function clearTipTimer() { function clearTipTimer() {
@@ -204,7 +221,7 @@ function showShotFlash(flash) {
} }
const team = flash.team === "red" ? "red" : "blue"; const team = flash.team === "red" ? "red" : "blue";
if (shouldPlayShotEffect(shootData)) { if (shouldPlayShotEffect(shootData, team)) {
triggerShotEffect(team, shootData, flash.key); triggerShotEffect(team, shootData, flash.key);
return; return;
} }
@@ -310,6 +327,15 @@ function getHitStyle(shot) {
}; };
} }
function getSvipHitBgStyle(shot) {
const radius = currentHitRadiusPx.value;
const point = getShotPoint(shot);
return {
...getTargetPositionStyle(point, radius),
};
}
function getRoundTipStyle(shot) { function getRoundTipStyle(shot) {
const point = getShotPoint(shot, true); const point = getShotPoint(shot, true);
return getTargetPositionStyle( return getTargetPositionStyle(
@@ -445,6 +471,18 @@ onBeforeUnmount(() => {
}}<text v-if="bluelatestOne.ring">环</text></view }}<text v-if="bluelatestOne.ring">环</text></view
> >
<block v-for="(bow, index) in scores" :key="index"> <block v-for="(bow, index) in scores" :key="index">
<image
v-if="
pMode &&
bow.ring > 0 &&
isSvipShot(bow, 'red') &&
!shouldHideRedHit(index)
"
class="svip-hit-bg"
src="../../../static/vip/svip-xuan.png"
:style="getSvipHitBgStyle(bow)"
mode="aspectFit"
/>
<view <view
v-if="bow.ring > 0 && !shouldHideRedHit(index)" v-if="bow.ring > 0 && !shouldHideRedHit(index)"
:class="`hit ${pMode ? 'b' : 's'}-point ${ :class="`hit ${pMode ? 'b' : 's'}-point ${
@@ -458,6 +496,18 @@ onBeforeUnmount(() => {
> >
</block> </block>
<block v-for="(bow, index) in blueScores" :key="index"> <block v-for="(bow, index) in blueScores" :key="index">
<image
v-if="
pMode &&
bow.ring > 0 &&
isSvipShot(bow, 'blue') &&
!shouldHideBlueHit(index)
"
class="svip-hit-bg"
src="../../../static/vip/svip-xuan.png"
:style="getSvipHitBgStyle(bow)"
mode="aspectFit"
/>
<view <view
v-if="bow.ring > 0 && !shouldHideBlueHit(index)" v-if="bow.ring > 0 && !shouldHideBlueHit(index)"
:class="`hit ${pMode ? 'b' : 's'}-point ${ :class="`hit ${pMode ? 'b' : 's'}-point ${
@@ -572,17 +622,26 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.svip-hit-bg {
position: absolute;
width: 48rpx;
height: 48rpx;
z-index: 1;
pointer-events: none;
transform-origin: center center;
animation: svip-hit-xuan 1.2s linear infinite;
}
.hit { .hit {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
z-index: 1; z-index: 2;
color: #fff; color: #fff;
transition: transform 0.2s ease, opacity 0.2s ease; transition: transform 0.2s ease, opacity 0.2s ease;
box-sizing: border-box; box-sizing: border-box;
} }
.b-point { .b-point {
border: 1px solid #fff; border: 1px solid #fff;
z-index: 1; z-index: 2;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -598,6 +657,20 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/ transform: translate(-50%, -50%);*/
margin-top: 2rpx; margin-top: 2rpx;
} }
@keyframes svip-hit-xuan {
0% {
opacity: 0.9;
transform: translate(-50%, -50%) rotate(0deg) scale(0.92);
}
50% {
opacity: 1;
transform: translate(-50%, -50%) rotate(180deg) scale(1.08);
}
100% {
opacity: 0.9;
transform: translate(-50%, -50%) rotate(360deg) scale(0.92);
}
}
@keyframes target-pump-in { @keyframes target-pump-in {
from { from {
transform: translate(-50%, -50%) scale(2); transform: translate(-50%, -50%) scale(2);

View File

@@ -17,6 +17,7 @@ const props = defineProps({
const players = ref({}); const players = ref({});
const currentTeam = ref(false); const currentTeam = ref(false);
const firstName = ref(""); const firstName = ref("");
const currentPlayer = ref(null);
// 抽出判断:当前队伍且该玩家排序为 0队伍首位 // 抽出判断:当前队伍且该玩家排序为 0队伍首位
const isFirst = (id) => const isFirst = (id) =>
@@ -30,6 +31,18 @@ const getPos = (id) => {
return sort * 40; return sort * 40;
}; };
const getMemberNicknameClass = () => [
"current-shooter-name",
"member-nickname",
currentPlayer.value?.vip === true && currentPlayer.value?.sVip !== true
? "member-nickname--vip"
: "",
currentPlayer.value?.sVip === true ? "member-nickname--svip" : "",
];
const isCurrentPlayerMember = () =>
currentPlayer.value?.vip === true || currentPlayer.value?.sVip === true;
const syncPlayers = () => { const syncPlayers = () => {
const nextPlayers = {}; const nextPlayers = {};
const shooterId = props.currentShooterId; const shooterId = props.currentShooterId;
@@ -40,12 +53,14 @@ const syncPlayers = () => {
currentTeam.value = !!shooterId && shooterIndex >= 0; currentTeam.value = !!shooterId && shooterIndex >= 0;
firstName.value = ""; firstName.value = "";
currentPlayer.value = null;
if (currentTeam.value) { if (currentTeam.value) {
const target = nextTeam.splice(shooterIndex, 1)[0]; const target = nextTeam.splice(shooterIndex, 1)[0];
if (target) { if (target) {
nextTeam.unshift(target); nextTeam.unshift(target);
firstName.value = target.name || ""; firstName.value = target.name || "";
currentPlayer.value = target;
} }
} }
@@ -93,8 +108,20 @@ watch(
>{{ isRed ? "红队" : "蓝队" }}</text >{{ isRed ? "红队" : "蓝队" }}</text
> >
</view> </view>
<view
v-if="currentTeam && isCurrentPlayerMember()"
:class="getMemberNicknameClass()"
:style="{
[isRed ? 'left' : 'right']: '-4rpx',
}"
>
<text class="member-nickname__text">{{ firstName }}</text>
<text v-if="currentPlayer?.sVip === true" class="member-nickname__shine">
{{ firstName }}
</text>
</view>
<text <text
v-if="currentTeam" v-else-if="currentTeam"
class="truncate" class="truncate"
:style="{ :style="{
color: isRed ? '#ff6060' : '#5fadff', color: isRed ? '#ff6060' : '#5fadff',
@@ -114,6 +141,17 @@ watch(
height: 10rpx; height: 10rpx;
margin: 0 20rpx; margin: 0 20rpx;
} }
.current-shooter-name {
position: absolute;
width: 80rpx;
bottom: -100rpx;
justify-content: center;
}
.current-shooter-name .member-nickname__text,
.current-shooter-name .member-nickname__shine {
font-size: 20rpx;
text-align: center;
}
.container > text { .container > text {
position: absolute; position: absolute;
font-size: 20rpx; font-size: 20rpx;

View File

@@ -473,13 +473,17 @@ function updateTeams(battleInfo) {
} }
function updateGoldenRound(battleInfo) { function updateGoldenRound(battleInfo) {
if (!battleInfo?.current?.goldRound) { const rounds = Array.isArray(battleInfo?.rounds) ? battleInfo.rounds : [];
goldenRound.value = 0; const currentRoundNo = Number(battleInfo?.current?.round || 0);
return; const currentRoundInfo = rounds.find((round) => Number(round?.round) === currentRoundNo);
} const activeGoldRoundInfo = rounds.find(
const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : []; (round) => Number(round?.goldRound || 0) > 0 && round?.status === 1
const finishedGoldCount = rounds.filter((round) => !!round?.ifGold).length; );
goldenRound.value = Math.max(1, finishedGoldCount + (battleInfo.current?.playerId ? 1 : 0)); const roundGoldRound = Number(currentRoundInfo?.goldRound || 0);
const activeGoldRound = Number(activeGoldRoundInfo?.goldRound || 0);
const currentGoldRound = Number(battleInfo?.current?.goldRound || 0);
const nextGoldRound = roundGoldRound || activeGoldRound || currentGoldRound;
goldenRound.value = nextGoldRound > 0 ? nextGoldRound : 0;
} }
// Restore an info snapshot whose eventType points at the NewRound phase. // Restore an info snapshot whose eventType points at the NewRound phase.
@@ -1214,6 +1218,8 @@ onShow(() => {
:scores="scores" :scores="scores"
:blueScores="blueScores" :blueScores="blueScores"
:latestShotFlash="latestShotFlash" :latestShotFlash="latestShotFlash"
:redTeam="redTeam"
:blueTeam="blueTeam"
/> />
<BattleFooter <BattleFooter
v-if="start" v-if="start"

View File

@@ -0,0 +1,171 @@
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "./components/Container.vue";
import BowTarget from "./components/BowTarget.vue";
import Avatar from "./components/Avatar.vue";
import { roundsName } from "@/constants";
import { getBattleAPI } from "@/apis";
const selected = ref(0);
const redScores = ref([]);
const blueScores = ref([]);
const redTeam = ref([]);
const blueTeam = ref([]);
const tabs = ref([]);
const players = ref([]);
const data = ref({});
const loadArrows = (round) => {
round.shoots[1].forEach((arrow) => {
blueScores.value.push(arrow);
});
round.shoots[2].forEach((arrow) => {
redScores.value.push(arrow);
});
};
onLoad(async (options) => {
if (!options.battleId) return;
const result = await getBattleAPI(options.battleId || "57943107462893568");
data.value = result;
blueTeam.value = data.value.teams?.[1]?.players || [];
redTeam.value = data.value.teams?.[2]?.players || [];
blueTeam.value.forEach((p, index) => {
players.value.push(p);
players.value.push(redTeam.value[index]);
});
Object.values(data.value.rounds).forEach((round, index) => {
if (round.ifGold) tabs.value.push(`决金箭`);
else tabs.value.push(`${roundsName[index + 1]}`);
});
selected.value = Number(options.selected || 0);
onClickTab(selected.value);
});
const onClickTab = (index) => {
selected.value = index;
redScores.value = [];
blueScores.value = [];
loadArrows(data.value.rounds[index]);
};
</script>
<template>
<Container title="靶纸">
<view class="container">
<view>
<view
v-for="(tab, index) in tabs"
:key="index"
@click="() => onClickTab(index)"
:class="selected === index ? 'selected-tab' : ''"
>
{{ tab }}
</view>
</view>
<view :style="{ margin: '20px 0' }">
<BowTarget
:scores="redScores"
:blueScores="blueScores"
:redTeam="redTeam"
:blueTeam="blueTeam"
mode="team"
/>
</view>
<view class="score-container">
<view
class="score-row"
v-for="(player, index) in players"
:key="index"
:style="{
justifyContent: index % 2 === 0 ? 'flex-end' : 'flex-start',
}"
>
<Avatar
:src="player.avatar"
:borderColor="index % 2 === 0 ? '#64BAFF' : '#FF6767'"
:size="36"
/>
<view>
<view
v-for="(score, index) in data.rounds[selected].shoots[
index % 2 === 0 ? 1 : 2
]"
:key="index"
class="score-item"
>
{{ score.ringX ? "X" : score.ring }}
</view>
</view>
</view>
</view>
</view>
</Container>
</template>
<style scoped>
.container {
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container > view:nth-child(1) {
display: flex;
align-items: center;
justify-content: flex-start;
width: calc(100% - 20px);
color: #fff9;
padding: 10px;
overflow-x: auto;
}
.container > view:nth-child(1)::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.container > view:nth-child(1) > view {
border: 1px solid #fff9;
border-radius: 20px;
padding: 7px 10px;
margin: 0 5px;
font-size: 14px;
flex: 0 0 auto;
}
.selected-tab {
background-color: #fed847;
border-color: #fed847 !important;
color: #000;
}
.score-row {
display: flex;
align-items: flex-start;
margin-bottom: 5px;
width: calc(50% - 5px);
padding-left: 5px;
}
.score-row > view:last-child {
margin-left: 10px;
display: grid;
grid-template-columns: repeat(3, auto);
gap: 5px;
margin-right: 5px;
min-width: 26%;
}
.score-item {
background-image: url("../../static/score-bg.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
color: #fed847;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
width: 10vw;
height: 10vw;
}
.score-container {
display: flex;
flex-wrap: wrap;
width: 100%;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB