update:代码备份

This commit is contained in:
2026-05-29 17:46:52 +08:00
parent 8b25a10d4c
commit b3fc11f1b1
8 changed files with 1322 additions and 108 deletions

View File

@@ -1,6 +1,15 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import {
computed,
getCurrentInstance,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch,
} from "vue";
import PointSwitcher from "@/components/PointSwitcher.vue";
import TargetCanvas from "@/components/TargetCanvas.vue";
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
import { simulShootAPI } from "@/apis";
@@ -8,6 +17,7 @@ import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, device } = storeToRefs(store);
const instance = getCurrentInstance();
const props = defineProps({
currentRound: {
@@ -34,6 +44,39 @@ const props = defineProps({
type: Boolean,
default: false,
},
coordinateRadius: {
type: Number,
default: 20,
},
hitRadiusPx: {
type: Number,
default: 2,
},
zoomHitRadiusPx: {
type: Number,
default: 5,
},
showCrosshair: {
type: Boolean,
default: false,
},
showQuadrantLabels: {
type: Boolean,
default: false,
},
quadrantLabels: {
type: Object,
default: () => ({
1: "1",
2: "2",
3: "3",
4: "4",
}),
},
highlightAreas: {
type: Array,
default: () => [],
},
});
const pMode = ref(true);
@@ -45,6 +88,100 @@ const timer = ref(null);
const dirTimer = ref(null);
const angle = ref(null);
const circleColor = ref("");
const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68;
const getNumber = (value, fallback = 0) => {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : fallback;
};
const safeTargetRadius = computed(() => {
return Math.max(getNumber(props.coordinateRadius, 20), 1);
});
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 getPointDirection(point) {
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, extraOffset = {}) {
if (!point) return { display: "none" };
const radius = safeTargetRadius.value;
const diameter = radius * 2;
const direction = getPointDirection(point);
const xOffset = (direction ? direction.x * offsetPx : 0) + (extraOffset.x || 0);
const yOffset = (direction ? -direction.y * offsetPx : 0) + (extraOffset.y || 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 getRoundTipStyle(shot) {
const point = getShotPoint(shot, true);
return getTargetPositionStyle(
point,
shot?.ring ? currentHitRadiusPx.value : 0,
{ y: ROUND_TIP_OFFSET_Y }
);
}
function getExperienceTipStyle(shot) {
const point = getShotPoint(shot, true);
return getTargetPositionStyle(
point,
shot?.ring ? currentHitRadiusPx.value : 0,
{ y: EXPERIENCE_TIP_OFFSET_Y }
);
}
watch(
() => props.scores,
@@ -80,14 +217,6 @@ watch(
}
);
function calcRealX(num, offset = 3.4) {
const len = 20.4 + num;
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
}
function calcRealY(num, offset = 3.4) {
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
}
const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
};
@@ -111,6 +240,160 @@ 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 targetImageNaturalSize = ref({
width: 0,
height: 0,
});
const targetLayerRect = ref({
left: 0,
top: 0,
width: 0,
height: 0,
ready: false,
});
const targetHighlightLayerStyle = computed(() => ({
left: `${targetLayerRect.value.left}px`,
top: `${targetLayerRect.value.top}px`,
width: `${targetLayerRect.value.width}px`,
height: `${targetLayerRect.value.height}px`,
}));
const getRectNumber = (value, fallback = 0) => {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : fallback;
};
const getImageContentRect = (imageRect) => {
const naturalWidth = getRectNumber(targetImageNaturalSize.value.width);
const naturalHeight = getRectNumber(targetImageNaturalSize.value.height);
const imageWidth = getRectNumber(imageRect?.width);
const imageHeight = getRectNumber(imageRect?.height);
if (naturalWidth <= 0 || naturalHeight <= 0 || imageWidth <= 0 || imageHeight <= 0) {
return imageRect;
}
const naturalRatio = naturalWidth / naturalHeight;
const boxRatio = imageWidth / imageHeight;
if (naturalRatio > boxRatio) {
const contentHeight = imageWidth / naturalRatio;
return {
left: imageRect.left,
top: imageRect.top + (imageHeight - contentHeight) / 2,
width: imageWidth,
height: contentHeight,
};
}
const contentWidth = imageHeight * naturalRatio;
return {
left: imageRect.left + (imageWidth - contentWidth) / 2,
top: imageRect.top,
width: contentWidth,
height: imageHeight,
};
};
const updateTargetLayerRect = async () => {
await nextTick();
const query = uni.createSelectorQuery().in(instance?.proxy);
query.select(".target").boundingClientRect();
query.select(".target-image").boundingClientRect();
query.exec((rects = []) => {
const stageRect = rects[0];
const imageRect = rects[1];
if (!stageRect || !imageRect || !imageRect.width || !imageRect.height) {
targetLayerRect.value = {
...targetLayerRect.value,
ready: false,
};
return;
}
const contentRect = getImageContentRect(imageRect);
const width = Math.round(getRectNumber(contentRect?.width));
const height = Math.round(getRectNumber(contentRect?.height));
targetLayerRect.value = {
left: getRectNumber(contentRect?.left) - getRectNumber(stageRect.left),
top: getRectNumber(contentRect?.top) - getRectNumber(stageRect.top),
width,
height,
ready: width > 0 && height > 0,
};
});
};
const onTargetImageLoad = (event) => {
const width = getRectNumber(event?.detail?.width);
const height = getRectNumber(event?.detail?.height);
if (width > 0 && height > 0) {
targetImageNaturalSize.value = {
width,
height,
};
}
updateTargetLayerRect();
};
const onWindowResize = () => {
updateTargetLayerRect();
};
watch(
showHighlightCanvas,
(newVal) => {
if (newVal) {
updateTargetLayerRect();
}
}
);
async function onReceiveMessage(message) {
if (Array.isArray(message)) return;
if (message.type === MESSAGETYPESV2.ShootResult && message.shootData) {
@@ -135,6 +418,10 @@ async function onReceiveMessage(message) {
onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage);
setTimeout(updateTargetLayerRect, 30);
if (uni.onWindowResize) {
uni.onWindowResize(onWindowResize);
}
});
onBeforeUnmount(() => {
@@ -147,6 +434,9 @@ onBeforeUnmount(() => {
dirTimer.value = null;
}
uni.$off("socket-inbox", onReceiveMessage);
if (uni.offWindowResize) {
uni.offWindowResize(onWindowResize);
}
});
</script>
@@ -160,6 +450,25 @@ onBeforeUnmount(() => {
}}</text>
</view> -->
<view class="target">
<image
class="target-image"
src="../../../static/bow-target.png"
mode="aspectFit"
@load="onTargetImageLoad"
/>
<TargetCanvas
v-if="showHighlightCanvas && targetLayerRect.ready"
class="target-highlight-layer"
:style="targetHighlightLayerStyle"
:canvasWidth="targetLayerRect.width"
:canvasHeight="targetLayerRect.height"
:coordinateRadius="coordinateRadius"
:showCrosshair="false"
:showQuadrantLabels="false"
:showRingLabels="false"
:highlightOnly="true"
:highlightAreas="currentHighlightAreas"
/>
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
<view :style="{ background: circleColor }">
<image src="../../../static/dot-circle.png" mode="widthFix" />
@@ -169,20 +478,14 @@ onBeforeUnmount(() => {
<view
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
class="e-value fade-in-out"
:style="{
left: calcRealX(latestOne.ring ? latestOne.x : 0, 20),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 40),
}"
:style="getExperienceTipStyle(latestOne)"
>
经验 +1
</view>
<view
v-if="latestOne"
class="round-tip fade-in-out"
:style="{
left: calcRealX(latestOne.ring ? latestOne.x : 0, 28),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 28),
}"
:style="getRoundTipStyle(latestOne)"
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}<text v-if="latestOne.ring"></text>
</view>
@@ -193,20 +496,14 @@ onBeforeUnmount(() => {
user.id === bluelatestOne.playerId
"
class="e-value fade-in-out"
:style="{
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 20),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 40),
}"
:style="getExperienceTipStyle(bluelatestOne)"
>
经验 +1
</view>
<view
v-if="bluelatestOne"
class="round-tip fade-in-out"
:style="{
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 28),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 28),
}"
:style="getRoundTipStyle(bluelatestOne)"
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}<text v-if="bluelatestOne.ring">环</text></view
>
@@ -217,8 +514,7 @@ onBeforeUnmount(() => {
index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
...getHitStyle(bow),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}"
><text v-if="pMode">{{ index + 1 }}</text></view
@@ -231,15 +527,13 @@ onBeforeUnmount(() => {
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
...getHitStyle(bow),
backgroundColor: '#1840FF',
}"
>
<text v-if="pMode">{{ index + 1 }}</text>
</view>
</block>
<image src="../../../static/bow-target.png" mode="widthFix" />
</view>
<view class="footer">
<PointSwitcher
@@ -266,7 +560,21 @@ onBeforeUnmount(() => {
margin: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
z-index: -1;
z-index: 0;
}
.target-image {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.target-highlight-layer {
position: absolute;
z-index: 1;
pointer-events: none;
}
.e-value {
position: absolute;
@@ -275,7 +583,7 @@ onBeforeUnmount(() => {
font-size: 12px;
padding: 4px 7px;
border-radius: 5px;
z-index: 2;
z-index: 4;
width: 50px;
text-align: center;
}
@@ -284,7 +592,7 @@ onBeforeUnmount(() => {
color: #fff;
font-size: 30px;
font-weight: bold;
z-index: 2;
z-index: 4;
width: 100px;
text-align: center;
}
@@ -292,31 +600,44 @@ onBeforeUnmount(() => {
font-size: 24px;
margin-left: 5px;
}
.target > image:last-child {
width: 100%;
height: 100%;
@keyframes target-tip-fade-in-out {
0% {
transform: translate(-50%, -50%) translateY(20px);
opacity: 0;
}
30% {
transform: translate(-50%, -50%);
opacity: 1;
}
80% {
transform: translate(-50%, -50%);
opacity: 1;
}
100% {
transform: translate(-50%, -50%);
opacity: 0;
}
}
.round-tip.fade-in-out,
.e-value.fade-in-out {
animation: target-tip-fade-in-out 1.2s ease forwards;
}
.hit {
position: absolute;
border-radius: 50%;
z-index: 1;
z-index: 3;
color: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
}
.s-point {
width: 4px;
height: 4px;
min-width: 4px;
min-height: 4px;
}
.b-point {
width: 10px;
height: 10px;
min-width: 10px;
min-height: 10px;
border: 1px solid #fff;
z-index: 1;
box-sizing: border-box;
z-index: 3;
display: flex;
justify-content: center;
align-items: center;
@@ -332,6 +653,19 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/
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 {
width: 100%;
display: flex;
@@ -382,7 +716,7 @@ onBeforeUnmount(() => {
height: 60px;
left: calc(50% - 100px);
top: calc(50% - 30px);
z-index: 99;
z-index: 5;
font-weight: bold;
}
.arrow-dir {
@@ -391,6 +725,7 @@ onBeforeUnmount(() => {
height: 52%;
left: 50%;
bottom: 50%;
z-index: 4;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -0,0 +1,124 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
const props = defineProps({
rowCount: {
type: Number,
default: 0,
},
total: {
type: Number,
default: 0,
},
arrows: {
type: Array,
default: () => [],
},
fontSize: {
type: Number,
default: 25,
},
completeEffect: {
type: Boolean,
default: true,
},
});
const items = ref(new Array(props.total).fill(9));
const width = ref(92);
const itemWidth = ref(0);
const bgImages = [
"../static/complete-light1.png",
"../static/complete-light2.png",
];
const bgIndex = ref(0);
watch(
() => props.total,
(newValue) => {
items.value = new Array(newValue).fill(9);
}
);
const timer = ref(null);
onMounted(() => {
timer.value = setInterval(() => {
bgIndex.value = bgIndex.value === 0 ? 1 : 0;
}, 200);
});
onBeforeUnmount(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
</script>
<template>
<view class="container">
<image
v-if="total > 0 && arrows.length === total && completeEffect"
:src="bgImages[bgIndex]"
class="complete-light"
:style="{
width: `calc(${(100 / (rowCount + 2)) * rowCount}vw + ${
(100 / (total * 2)) * (rowCount * 2 + (total === 12 ? 8 : 24))
}px)`,
height: `calc(${(100 / (rowCount + 2)) * (total / rowCount)}vw + ${
(100 / (total * 2)) *
((total / rowCount) * 2 + (total === 12 ? 7 : 24))
}px)`,
top: `${total === 12 ? -2 : -3}vw`,
}"
/>
<view
v-for="(_, index) in items"
:key="index"
class="score-item"
:style="{
width: 100 / (rowCount + 2) + 'vw',
height: 100 / (rowCount + 2) + 'vw',
lineHeight: 100 / (rowCount + 2) + 'vw',
fontSize: fontSize + 'px',
margin: 100 / (total * 2) + 'px',
}"
>
<image src="/static/score-bg.png" mode="widthFix" />
<text
:style="{ fontWeight: arrows[index] !== undefined ? 'bold' : 'normal' }"
>{{
!arrows[index] ? "-" : arrows[index].ringX ? "X" : arrows[index].ring
}}</text
>
</view>
</view>
</template>
<style scoped>
.container {
width: 92vw;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 0 4vw;
position: relative;
padding: 1vw 0;
}
.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;
position: relative;
}
.score-item > image {
position: absolute;
width: 100%;
top: 5%;
}
.score-item > text {
position: relative;
margin-top: 2px;
}
.complete-light {
position: absolute;
}
</style>

View File

@@ -206,7 +206,12 @@ onBeforeUnmount(() => {
<view class="progress-card__header">
<view class="progress-card__profile">
<view class="progress-card__avatar-shell">
<Avatar :src="user.avatar" :size="40" />
<Avatar
:src="avatarSrc"
:size="80"
size-unit="rpx"
image-mode="aspectFill"
/>
</view>
<text class="progress-card__name">{{ displayName }}</text>
</view>
@@ -271,6 +276,9 @@ onBeforeUnmount(() => {
box-sizing: border-box;
border-radius: 50%;
background: linear-gradient(180deg, rgba(255, 209, 153, 1), rgba(162, 119, 55, 1));
display: flex;
align-items: center;
justify-content: center;
}
.progress-card__avatar {