update:对接个人训练改版
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
<script setup>
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import BowShotEffect from "@/components/BowShotEffect.vue";
|
||||
import PointSwitcher from "@/components/PointSwitcher.vue";
|
||||
import TargetCanvas from "@/components/TargetCanvas.vue";
|
||||
|
||||
@@ -33,6 +36,14 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isSvip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shotEffectToken: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: "solo", // solo 单排,team 双排
|
||||
@@ -57,23 +68,22 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showQuadrantLabels: {
|
||||
sectorCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
activeSector: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
activeRing: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showSectorLabels: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
quadrantLabels: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
1: "1",
|
||||
2: "2",
|
||||
3: "3",
|
||||
4: "4",
|
||||
}),
|
||||
},
|
||||
highlightAreas: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const pMode = ref(true);
|
||||
@@ -85,6 +95,12 @@ const timer = ref(null);
|
||||
const dirTimer = ref(null);
|
||||
const angle = ref(null);
|
||||
const circleColor = ref("");
|
||||
const shotEffect = ref(null);
|
||||
const hiddenLatestKey = ref("");
|
||||
const targetShaking = ref(false);
|
||||
const targetSize = ref({ width: 0, height: 0 });
|
||||
const shakeTimer = ref(null);
|
||||
const instance = getCurrentInstance();
|
||||
const ROUND_TIP_OFFSET_Y = -32;
|
||||
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
||||
|
||||
@@ -162,6 +178,13 @@ function getHitStyle(shot) {
|
||||
};
|
||||
}
|
||||
|
||||
function getSvipHitBgStyle(shot) {
|
||||
const radius = currentHitRadiusPx.value;
|
||||
const point = getShotPoint(shot);
|
||||
|
||||
return getTargetPositionStyle(point, radius);
|
||||
}
|
||||
|
||||
function getRoundTipStyle(shot) {
|
||||
const point = getShotPoint(shot, true);
|
||||
return getTargetPositionStyle(
|
||||
@@ -180,15 +203,116 @@ function getExperienceTipStyle(shot) {
|
||||
);
|
||||
}
|
||||
|
||||
function clearTipTimer() {
|
||||
if (!timer.value) return;
|
||||
clearTimeout(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
|
||||
function showShotTip(shot) {
|
||||
clearTipTimer();
|
||||
latestOne.value = shot;
|
||||
timer.value = setTimeout(() => {
|
||||
latestOne.value = null;
|
||||
timer.value = null;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function hasShotPoint(shot) {
|
||||
return !!getShotPoint(shot);
|
||||
}
|
||||
|
||||
function shouldPlayShotEffect(shot) {
|
||||
return (
|
||||
props.isSvip &&
|
||||
!!shot &&
|
||||
Number(shot.ring) > 0 &&
|
||||
hasShotPoint(shot)
|
||||
);
|
||||
}
|
||||
|
||||
function buildShotEffectKey(shot, index) {
|
||||
return [
|
||||
props.shotEffectToken,
|
||||
index,
|
||||
shot?.playerId ?? "",
|
||||
shot?.x ?? "",
|
||||
shot?.y ?? "",
|
||||
shot?.ring ?? "",
|
||||
shot?.ringX ? 1 : 0,
|
||||
].join("-");
|
||||
}
|
||||
|
||||
function triggerShotEffect(shot, index) {
|
||||
const key = buildShotEffectKey(shot, index);
|
||||
clearTipTimer();
|
||||
latestOne.value = null;
|
||||
hiddenLatestKey.value = key;
|
||||
shotEffect.value = { key, shot };
|
||||
}
|
||||
|
||||
function completeShotEffect(key) {
|
||||
if (!shotEffect.value || shotEffect.value.key !== key) return;
|
||||
|
||||
const shot = shotEffect.value.shot;
|
||||
hiddenLatestKey.value = "";
|
||||
shotEffect.value = null;
|
||||
showShotTip(shot);
|
||||
}
|
||||
|
||||
function shouldHideLatestHit(index) {
|
||||
return !!hiddenLatestKey.value && index === props.scores.length - 1;
|
||||
}
|
||||
|
||||
function shakeTarget() {
|
||||
targetShaking.value = false;
|
||||
if (shakeTimer.value) {
|
||||
clearTimeout(shakeTimer.value);
|
||||
shakeTimer.value = null;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
targetShaking.value = true;
|
||||
shakeTimer.value = setTimeout(() => {
|
||||
targetShaking.value = false;
|
||||
shakeTimer.value = null;
|
||||
}, 260);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTargetSize() {
|
||||
nextTick(() => {
|
||||
const query = instance?.proxy
|
||||
? uni.createSelectorQuery().in(instance.proxy)
|
||||
: uni.createSelectorQuery();
|
||||
|
||||
query
|
||||
.select(".target")
|
||||
.boundingClientRect((rect) => {
|
||||
const width = Number(rect?.width);
|
||||
const height = Number(rect?.height);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) return;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
targetSize.value = { width, height };
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
}
|
||||
|
||||
function handleWindowResize() {
|
||||
updateTargetSize();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.scores,
|
||||
(newVal) => {
|
||||
if (newVal.length - prevScores.value.length === 1) {
|
||||
latestOne.value = newVal[newVal.length - 1];
|
||||
if (timer.value) clearTimeout(timer.value);
|
||||
timer.value = setTimeout(() => {
|
||||
latestOne.value = null;
|
||||
}, 1000);
|
||||
showShotTip(newVal[newVal.length - 1]);
|
||||
} else if (newVal.length < prevScores.value.length) {
|
||||
clearTipTimer();
|
||||
latestOne.value = null;
|
||||
hiddenLatestKey.value = "";
|
||||
shotEffect.value = null;
|
||||
}
|
||||
prevScores.value = [...newVal];
|
||||
},
|
||||
@@ -197,6 +321,19 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.shotEffectToken,
|
||||
(token) => {
|
||||
// token 只由实时 ShootResult 推进,同步快照不会重播飞箭。
|
||||
if (!token || props.scores.length === 0) return;
|
||||
const latestIndex = props.scores.length - 1;
|
||||
const latestShot = props.scores[latestIndex];
|
||||
if (shouldPlayShotEffect(latestShot)) {
|
||||
triggerShotEffect(latestShot, latestIndex);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.blueScores,
|
||||
(newVal) => {
|
||||
@@ -237,42 +374,9 @@ const arrowStyle = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const currentArrowIndex = computed(() => {
|
||||
return props.scores.length + props.blueScores.length + 1;
|
||||
});
|
||||
|
||||
const getHighlightArrowIndex = (area = {}) => {
|
||||
const arrowIndex = Number(area.arrowIndex ?? area.arrowNo ?? area.arrow);
|
||||
return Number.isInteger(arrowIndex) && arrowIndex > 0 ? arrowIndex : null;
|
||||
};
|
||||
|
||||
const currentHighlightAreas = computed(() => {
|
||||
if (!Array.isArray(props.highlightAreas) || props.highlightAreas.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hasExplicitArrowIndex = props.highlightAreas.some((area = {}) => {
|
||||
return getHighlightArrowIndex(area) !== null;
|
||||
});
|
||||
|
||||
const matchedAreas = props.highlightAreas.filter((area = {}) => {
|
||||
return getHighlightArrowIndex(area) === currentArrowIndex.value;
|
||||
});
|
||||
|
||||
if (hasExplicitArrowIndex) {
|
||||
return matchedAreas;
|
||||
}
|
||||
|
||||
if (props.highlightAreas.length === 1) {
|
||||
return props.highlightAreas.slice(0, 1);
|
||||
}
|
||||
|
||||
const currentArea = props.highlightAreas[currentArrowIndex.value - 1];
|
||||
return currentArea ? [currentArea] : [];
|
||||
});
|
||||
|
||||
const showHighlightCanvas = computed(() => {
|
||||
return props.totalRound > 0 && currentHighlightAreas.value.length > 0;
|
||||
const showSectorCanvas = computed(() => {
|
||||
const count = Number(props.sectorCount);
|
||||
return props.totalRound > 0 && Number.isInteger(count) && count > 0;
|
||||
});
|
||||
|
||||
async function onReceiveMessage(message) {
|
||||
@@ -299,23 +403,27 @@ async function onReceiveMessage(message) {
|
||||
|
||||
onMounted(() => {
|
||||
uni.$on("socket-inbox", onReceiveMessage);
|
||||
updateTargetSize();
|
||||
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
clearTipTimer();
|
||||
if (dirTimer.value) {
|
||||
clearTimeout(dirTimer.value);
|
||||
dirTimer.value = null;
|
||||
}
|
||||
if (shakeTimer.value) {
|
||||
clearTimeout(shakeTimer.value);
|
||||
shakeTimer.value = null;
|
||||
}
|
||||
uni.$off("socket-inbox", onReceiveMessage);
|
||||
if (uni.offWindowResize) uni.offWindowResize(handleWindowResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="container">
|
||||
<view :class="['container', { 'container--effecting': shotEffect }]">
|
||||
<!-- <view class="header" v-if="totalRound > 0">
|
||||
<text v-if="totalRound > 0" class="round-count">{{
|
||||
(currentRound > totalRound ? totalRound : currentRound) +
|
||||
@@ -323,37 +431,44 @@ onBeforeUnmount(() => {
|
||||
totalRound
|
||||
}}</text>
|
||||
</view> -->
|
||||
<view class="target">
|
||||
<view :class="['target', { 'target--shake': targetShaking }]">
|
||||
<image
|
||||
class="target-image"
|
||||
src="../../../static/bow-target.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<TargetCanvas
|
||||
v-if="showHighlightCanvas"
|
||||
v-if="showSectorCanvas"
|
||||
class="target-highlight-layer"
|
||||
:coordinateRadius="coordinateRadius"
|
||||
:showCrosshair="false"
|
||||
:showQuadrantLabels="false"
|
||||
:showRingLabels="false"
|
||||
:highlightOnly="true"
|
||||
:highlightAreas="currentHighlightAreas"
|
||||
:sectorCount="sectorCount"
|
||||
:activeSector="activeSector"
|
||||
:activeRing="activeRing"
|
||||
:showSectorLabels="showSectorLabels"
|
||||
/>
|
||||
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
|
||||
<view :style="{ background: circleColor }">
|
||||
<image src="../../../static/dot-circle.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/dot-circle.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="stop" class="stop-sign">中场休息</view>
|
||||
<view
|
||||
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
|
||||
v-if="
|
||||
!shotEffect &&
|
||||
latestOne &&
|
||||
latestOne.ring &&
|
||||
user.id === latestOne.playerId
|
||||
"
|
||||
class="e-value fade-in-out"
|
||||
:style="getExperienceTipStyle(latestOne)"
|
||||
>
|
||||
经验 +1
|
||||
</view>
|
||||
<view
|
||||
v-if="latestOne"
|
||||
v-if="!shotEffect && latestOne"
|
||||
class="round-tip fade-in-out"
|
||||
:style="getRoundTipStyle(latestOne)"
|
||||
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
|
||||
@@ -378,8 +493,15 @@ onBeforeUnmount(() => {
|
||||
}}<text v-if="bluelatestOne.ring">环</text></view
|
||||
>
|
||||
<block v-for="(bow, index) in scores" :key="index">
|
||||
<image
|
||||
v-if="pMode && isSvip && bow.ring > 0 && !shouldHideLatestHit(index)"
|
||||
class="svip-hit-bg"
|
||||
src="../../../static/vip/svip-xuan.png"
|
||||
:style="getSvipHitBgStyle(bow)"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view
|
||||
v-if="bow.ring > 0"
|
||||
v-if="bow.ring > 0 && !shouldHideLatestHit(index)"
|
||||
:class="`hit ${pMode ? 'b' : 's'}-point ${
|
||||
index === scores.length - 1 && latestOne ? 'pump-in' : ''
|
||||
}`"
|
||||
@@ -404,6 +526,16 @@ onBeforeUnmount(() => {
|
||||
<text v-if="pMode">{{ index + 1 }}</text>
|
||||
</view>
|
||||
</block>
|
||||
<BowShotEffect
|
||||
:shot="shotEffect && shotEffect.shot"
|
||||
:playKey="shotEffect ? shotEffect.key : ''"
|
||||
:targetRadius="safeTargetRadius"
|
||||
:targetWidth="targetSize.width"
|
||||
:targetHeight="targetSize.height"
|
||||
:hitOffsetPx="currentHitRadiusPx"
|
||||
@impact="shakeTarget"
|
||||
@complete="completeShotEffect"
|
||||
/>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<PointSwitcher
|
||||
@@ -424,13 +556,22 @@ onBeforeUnmount(() => {
|
||||
height: calc(100vw - 30px);
|
||||
padding: 0px 15px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.container--effecting {
|
||||
z-index: 10000;
|
||||
}
|
||||
.target {
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 20px);
|
||||
z-index: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
transform-origin: center center;
|
||||
}
|
||||
.target--shake {
|
||||
animation: target-shake 0.26s ease-out;
|
||||
}
|
||||
.target-image {
|
||||
position: absolute;
|
||||
@@ -499,6 +640,15 @@ onBeforeUnmount(() => {
|
||||
.e-value.fade-in-out {
|
||||
animation: target-tip-fade-in-out 1.2s ease forwards;
|
||||
}
|
||||
.svip-hit-bg {
|
||||
position: absolute;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
transform-origin: center center;
|
||||
animation: svip-hit-xuan 1.2s linear infinite;
|
||||
}
|
||||
.hit {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
@@ -527,6 +677,20 @@ onBeforeUnmount(() => {
|
||||
transform: translate(-50%, -50%);*/
|
||||
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 {
|
||||
from {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
@@ -536,6 +700,29 @@ onBeforeUnmount(() => {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes target-shake {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
14% {
|
||||
transform: translate(-20rpx, 8rpx);
|
||||
}
|
||||
28% {
|
||||
transform: translate(16rpx, -8rpx);
|
||||
}
|
||||
44% {
|
||||
transform: translate(-12rpx, 6rpx);
|
||||
}
|
||||
64% {
|
||||
transform: translate(8rpx, -4rpx);
|
||||
}
|
||||
82% {
|
||||
transform: translate(-4rpx, 2rpx);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
.hit.pump-in {
|
||||
animation: target-pump-in 0.3s ease-out forwards;
|
||||
transform-origin: center center;
|
||||
|
||||
Reference in New Issue
Block a user