update:修复靶纸贴边问题

This commit is contained in:
2026-05-26 17:58:54 +08:00
parent 8ef64f8f42
commit c5a8100c38
2 changed files with 208 additions and 74 deletions

View File

@@ -34,6 +34,18 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
targetRadius: {
type: Number,
default: 20,
},
hitRadiusPx: {
type: Number,
default: 2,
},
zoomHitRadiusPx: {
type: Number,
default: 5,
},
}); });
const pMode = ref(true); const pMode = ref(true);
@@ -80,13 +92,79 @@ watch(
} }
); );
function calcRealX(num, offset = 3.4) { const safeTargetRadius = computed(() => {
const len = 20.4 + num; const radius = Number(props.targetRadius);
return `calc(${(len / 40.8) * 100 - offset / 2}%)`; return Number.isFinite(radius) && radius > 0 ? radius : 20;
});
const currentHitRadiusPx = computed(() => {
const radius = Number(
pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
);
return Number.isFinite(radius) && radius >= 0 ? radius : 0;
});
function getShotPoint(shot, fallbackCenter = false) {
const x = Number(shot?.x);
const y = Number(shot?.y);
if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
return fallbackCenter ? { x: 0, y: 0 } : null;
} }
function calcRealY(num, offset = 3.4) {
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num; function getPointDirection(point) {
return `calc(${(len / 40.8) * 100 - offset / 2}%)`; if (!point) return null;
const distance = Math.sqrt(point.x * point.x + point.y * point.y);
if (distance === 0) return null;
return {
x: point.x / distance,
y: point.y / distance,
};
}
function formatPxOffset(value) {
if (!value) return "";
const operator = value > 0 ? "+" : "-";
return ` ${operator} ${Math.abs(value)}px`;
}
function formatTargetPosition(percent, offset) {
const pxOffset = formatPxOffset(offset);
return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
}
function getTargetPositionStyle(point, offsetPx = 0) {
if (!point) return { display: "none" };
const radius = safeTargetRadius.value;
const diameter = radius * 2;
const direction = getPointDirection(point);
const xOffset = direction ? direction.x * offsetPx : 0;
const yOffset = direction ? -direction.y * offsetPx : 0;
const leftPercent = ((point.x + radius) / diameter) * 100;
const topPercent = ((radius - point.y) / diameter) * 100;
return {
left: formatTargetPosition(leftPercent, xOffset),
top: formatTargetPosition(topPercent, yOffset),
transform: "translate(-50%, -50%)",
};
}
function getHitStyle(shot) {
const radius = currentHitRadiusPx.value;
const point = getShotPoint(shot);
return {
...getTargetPositionStyle(point, radius),
width: `${radius * 2}px`,
height: `${radius * 2}px`,
};
}
function getTipStyle(shot) {
const point = getShotPoint(shot, true);
return getTargetPositionStyle(point, shot?.ring ? currentHitRadiusPx.value : 0);
} }
const simulShoot = async () => { const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId); if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
@@ -169,20 +247,14 @@ onBeforeUnmount(() => {
<view <view
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId" v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
class="e-value fade-in-out" class="e-value fade-in-out"
:style="{ :style="getTipStyle(latestOne)"
left: calcRealX(latestOne.ring ? latestOne.x : 0, 20),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 40),
}"
> >
经验 +1 经验 +1
</view> </view>
<view <view
v-if="latestOne" v-if="latestOne"
class="round-tip fade-in-out" class="round-tip fade-in-out"
:style="{ :style="getTipStyle(latestOne)"
left: calcRealX(latestOne.ring ? latestOne.x : 0, 28),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 28),
}"
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶" >{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}<text v-if="latestOne.ring"></text> }}<text v-if="latestOne.ring"></text>
</view> </view>
@@ -193,20 +265,14 @@ onBeforeUnmount(() => {
user.id === bluelatestOne.playerId user.id === bluelatestOne.playerId
" "
class="e-value fade-in-out" class="e-value fade-in-out"
:style="{ :style="getTipStyle(bluelatestOne)"
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 20),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 40),
}"
> >
经验 +1 经验 +1
</view> </view>
<view <view
v-if="bluelatestOne" v-if="bluelatestOne"
class="round-tip fade-in-out" class="round-tip fade-in-out"
:style="{ :style="getTipStyle(bluelatestOne)"
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 28),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 28),
}"
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶" >{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}<text v-if="bluelatestOne.ring">环</text></view }}<text v-if="bluelatestOne.ring">环</text></view
> >
@@ -217,8 +283,7 @@ onBeforeUnmount(() => {
index === scores.length - 1 && latestOne ? 'pump-in' : '' index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`" }`"
:style="{ :style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'), ...getHitStyle(bow),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000', backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}" }"
><text v-if="pMode">{{ index + 1 }}</text></view ><text v-if="pMode">{{ index + 1 }}</text></view
@@ -231,8 +296,7 @@ onBeforeUnmount(() => {
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : '' index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`" }`"
:style="{ :style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'), ...getHitStyle(bow),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
backgroundColor: '#1840FF', backgroundColor: '#1840FF',
}" }"
> >
@@ -302,21 +366,11 @@ onBeforeUnmount(() => {
z-index: 1; z-index: 1;
color: #fff; color: #fff;
transition: all 0.3s ease; transition: all 0.3s ease;
} box-sizing: border-box;
.s-point {
width: 4px;
height: 4px;
min-width: 4px;
min-height: 4px;
} }
.b-point { .b-point {
width: 10px;
height: 10px;
min-width: 10px;
min-height: 10px;
border: 1px solid #fff; border: 1px solid #fff;
z-index: 1; z-index: 1;
box-sizing: border-box;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -332,6 +386,19 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/ transform: translate(-50%, -50%);*/
margin-top: 2rpx; margin-top: 2rpx;
} }
@keyframes target-pump-in {
from {
transform: translate(-50%, -50%) scale(2);
}
to {
transform: translate(-50%, -50%) scale(1);
}
}
.hit.pump-in {
animation: target-pump-in 0.3s ease-out forwards;
transform-origin: center center;
}
.header { .header {
width: 100%; width: 100%;
display: flex; display: flex;

View File

@@ -38,6 +38,18 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
targetRadius: {
type: Number,
default: 20,
},
hitRadiusPx: {
type: Number,
default: 2,
},
zoomHitRadiusPx: {
type: Number,
default: 5,
},
}); });
const pMode = ref(true); const pMode = ref(true);
@@ -75,13 +87,79 @@ watch(
{ immediate: true } { immediate: true }
); );
function calcRealX(num, offset = 3.4) { const safeTargetRadius = computed(() => {
const len = 20.4 + num; const radius = Number(props.targetRadius);
return `calc(${(len / 40.8) * 100 - offset / 2}%)`; return Number.isFinite(radius) && radius > 0 ? radius : 20;
});
const currentHitRadiusPx = computed(() => {
const radius = Number(
pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
);
return Number.isFinite(radius) && radius >= 0 ? radius : 0;
});
function getShotPoint(shot, fallbackCenter = false) {
const x = Number(shot?.x);
const y = Number(shot?.y);
if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
return fallbackCenter ? { x: 0, y: 0 } : null;
} }
function calcRealY(num, offset = 3.4) {
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num; function getPointDirection(point) {
return `calc(${(len / 40.8) * 100 - offset / 2}%)`; if (!point) return null;
const distance = Math.sqrt(point.x * point.x + point.y * point.y);
if (distance === 0) return null;
return {
x: point.x / distance,
y: point.y / distance,
};
}
function formatPxOffset(value) {
if (!value) return "";
const operator = value > 0 ? "+" : "-";
return ` ${operator} ${Math.abs(value)}px`;
}
function formatTargetPosition(percent, offset) {
const pxOffset = formatPxOffset(offset);
return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
}
function getTargetPositionStyle(point, offsetPx = 0) {
if (!point) return { display: "none" };
const radius = safeTargetRadius.value;
const diameter = radius * 2;
const direction = getPointDirection(point);
const xOffset = direction ? direction.x * offsetPx : 0;
const yOffset = direction ? -direction.y * offsetPx : 0;
const leftPercent = ((point.x + radius) / diameter) * 100;
const topPercent = ((radius - point.y) / diameter) * 100;
return {
left: formatTargetPosition(leftPercent, xOffset),
top: formatTargetPosition(topPercent, yOffset),
transform: "translate(-50%, -50%)",
};
}
function getHitStyle(shot) {
const radius = currentHitRadiusPx.value;
const point = getShotPoint(shot);
return {
...getTargetPositionStyle(point, radius),
width: `${radius * 2}px`,
height: `${radius * 2}px`,
};
}
function getTipStyle(shot) {
const point = getShotPoint(shot, true);
return getTargetPositionStyle(point, shot?.ring ? currentHitRadiusPx.value : 0);
} }
const simulShoot = async () => { const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId); if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
@@ -164,20 +242,14 @@ onBeforeUnmount(() => {
<view <view
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId" v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
class="e-value fade-in-out" class="e-value fade-in-out"
:style="{ :style="getTipStyle(latestOne)"
left: calcRealX(latestOne.ring ? latestOne.x : 0, 20),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 40),
}"
> >
经验 +1 经验 +1
</view> </view>
<view <view
v-if="latestOne" v-if="latestOne"
class="round-tip fade-in-out" class="round-tip fade-in-out"
:style="{ :style="getTipStyle(latestOne)"
left: calcRealX(latestOne.ring ? latestOne.x : 0, 28),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 28),
}"
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶" >{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}<text v-if="latestOne.ring"></text> }}<text v-if="latestOne.ring"></text>
</view> </view>
@@ -188,20 +260,14 @@ onBeforeUnmount(() => {
user.id === bluelatestOne.playerId user.id === bluelatestOne.playerId
" "
class="e-value fade-in-out" class="e-value fade-in-out"
:style="{ :style="getTipStyle(bluelatestOne)"
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 20),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 40),
}"
> >
经验 +1 经验 +1
</view> </view>
<view <view
v-if="bluelatestOne" v-if="bluelatestOne"
class="round-tip fade-in-out" class="round-tip fade-in-out"
:style="{ :style="getTipStyle(bluelatestOne)"
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 28),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 28),
}"
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶" >{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}<text v-if="bluelatestOne.ring">环</text></view }}<text v-if="bluelatestOne.ring">环</text></view
> >
@@ -212,8 +278,7 @@ onBeforeUnmount(() => {
index === scores.length - 1 && latestOne ? 'pump-in' : '' index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`" }`"
:style="{ :style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'), ...getHitStyle(bow),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000', backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}" }"
><text v-if="pMode">{{ index + 1 }}</text></view ><text v-if="pMode">{{ index + 1 }}</text></view
@@ -226,8 +291,7 @@ onBeforeUnmount(() => {
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : '' index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`" }`"
:style="{ :style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'), ...getHitStyle(bow),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
backgroundColor: '#1840FF', backgroundColor: '#1840FF',
}" }"
> >
@@ -297,21 +361,11 @@ onBeforeUnmount(() => {
z-index: 1; z-index: 1;
color: #fff; color: #fff;
transition: all 0.3s ease; transition: all 0.3s ease;
} box-sizing: border-box;
.s-point {
width: 4px;
height: 4px;
min-width: 4px;
min-height: 4px;
} }
.b-point { .b-point {
width: 10px;
height: 10px;
min-width: 10px;
min-height: 10px;
border: 1px solid #fff; border: 1px solid #fff;
z-index: 1; z-index: 1;
box-sizing: border-box;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -327,6 +381,19 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/ transform: translate(-50%, -50%);*/
margin-top: 2rpx; margin-top: 2rpx;
} }
@keyframes target-pump-in {
from {
transform: translate(-50%, -50%) scale(2);
}
to {
transform: translate(-50%, -50%) scale(1);
}
}
.hit.pump-in {
animation: target-pump-in 0.3s ease-out forwards;
transform-origin: center center;
}
.header { .header {
width: 100%; width: 100%;
display: flex; display: flex;