Merge branch 'feat-yuyin' into test

This commit is contained in:
2026-08-20 17:57:33 +08:00
6 changed files with 132 additions and 20 deletions
+1 -2
View File
@@ -23,10 +23,9 @@ export const getPrecisionShotAudioKeys = (shootData, directionText = "") => {
return ["未上靶", directionAudioKey].filter(Boolean); return ["未上靶", directionAudioKey].filter(Boolean);
} }
const ringAudioKey = shootData.ringX === true ? "X环" : `${ring}`;
// protobuf 的 false 可能不编码;只要不是明确命中,本箭就反馈未命中。 // protobuf 的 false 可能不编码;只要不是明确命中,本箭就反馈未命中。
const hitAudioKey = shootData.ok === true ? "Bingo命中目标" : "未命中"; const hitAudioKey = shootData.ok === true ? "Bingo命中目标" : "未命中";
return [ringAudioKey, directionAudioKey, hitAudioKey].filter(Boolean); return [directionAudioKey, hitAudioKey].filter(Boolean);
}; };
export const audioFils = { export const audioFils = {
+44 -2
View File
@@ -6,11 +6,16 @@ import Avatar from "@/components/Avatar.vue";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis"; import { simulShootAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants"; import { MESSAGETYPESV2 } from "@/constants";
import { getDistanceCheckAudioKey, getDistanceCheckText } from "@/util"; import {
getDistanceCheckAudioKey,
getDistanceCheckText,
getShootValidation,
} from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user, device } = storeToRefs(store); const { user, device } = storeToRefs(store);
const emit = defineEmits(["passed"]);
const props = defineProps({ const props = defineProps({
guide: { guide: {
type: Boolean, type: Boolean,
@@ -24,6 +29,10 @@ const props = defineProps({
type: Number, type: Number,
default: 15, default: 15,
}, },
autoStart: {
type: Boolean,
default: false,
},
}); });
const arrow = ref({}); const arrow = ref({});
const distance = ref(0); const distance = ref(0);
@@ -31,6 +40,29 @@ const statusText = ref("");
const showsimul = ref(false); const showsimul = ref(false);
const count = ref(props.count); const count = ref(props.count);
const timer = ref(null); const timer = ref(null);
const autoStartPending = ref(false);
const autoStartTriggered = ref(false);
let autoStartTimer = null;
const DISTANCE_PASSED_AUDIO_KEY = "站距合格,靶纸正确";
const AUTO_START_TIMEOUT_MS = 6000;
const clearAutoStartTimer = () => {
if (!autoStartTimer) return;
clearTimeout(autoStartTimer);
autoStartTimer = null;
};
const triggerAutoStart = () => {
if (!autoStartPending.value || autoStartTriggered.value) return;
clearAutoStartTimer();
autoStartPending.value = false;
autoStartTriggered.value = true;
emit("passed");
};
const onAudioEnded = (key) => {
if (key === DISTANCE_PASSED_AUDIO_KEY) triggerAutoStart();
};
const updateTimer = (value) => { const updateTimer = (value) => {
count.value = Math.round(value); count.value = Math.round(value);
@@ -44,21 +76,31 @@ onMounted(() => {
}, 1000); }, 1000);
} }
uni.$on("update-timer", updateTimer); uni.$on("update-timer", updateTimer);
uni.$on("audioEnded", onAudioEnded);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timer.value) clearInterval(timer.value); if (timer.value) clearInterval(timer.value);
clearAutoStartTimer();
uni.$off("update-timer", updateTimer); uni.$off("update-timer", updateTimer);
uni.$off("audioEnded", onAudioEnded);
}); });
async function onReceiveMessage(msg) { async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return; if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.TestDistance) { if (msg.type === MESSAGETYPESV2.TestDistance) {
if (autoStartPending.value || autoStartTriggered.value) return;
const rawDistance = Number(msg.shootData?.distance ?? msg.shootData?.dst); const rawDistance = Number(msg.shootData?.distance ?? msg.shootData?.dst);
distance.value = Number.isFinite(rawDistance) distance.value = Number.isFinite(rawDistance)
? Number((rawDistance / 100).toFixed(2)) ? Number((rawDistance / 100).toFixed(2))
: 0; : 0;
statusText.value = getDistanceCheckText(msg.shootData); statusText.value = getDistanceCheckText(msg.shootData);
audioManager.play(getDistanceCheckAudioKey(msg.shootData)); const audioKey = getDistanceCheckAudioKey(msg.shootData);
const validation = getShootValidation(msg.shootData);
if (props.autoStart && validation.distanceOk && validation.targetOk) {
autoStartPending.value = true;
autoStartTimer = setTimeout(triggerAutoStart, AUTO_START_TIMEOUT_MS);
}
audioManager.play(audioKey);
} }
} }
+12 -1
View File
@@ -37,6 +37,7 @@ const store = useStore();
const { user, device } = storeToRefs(store); const { user, device } = storeToRefs(store);
const start = ref(false); const start = ref(false);
const practiceStarting = ref(false);
const scores = ref([]); const scores = ref([]);
const isSvip = ref(false); const isSvip = ref(false);
const total = 12; const total = 12;
@@ -156,12 +157,18 @@ const beginPractise = async () => {
}; };
const onReady = async () => { const onReady = async () => {
if (practiceStarting.value || start.value) return;
practiceStarting.value = true;
try {
const result = await beginPractise(); const result = await beginPractise();
if (!result) return; if (!result) return;
scores.value = []; scores.value = [];
isSvip.value = false; isSvip.value = false;
start.value = true; start.value = true;
audioManager.play("练习开始"); audioManager.play("练习开始");
} finally {
practiceStarting.value = false;
}
}; };
const onOver = async (message) => { const onOver = async (message) => {
@@ -438,7 +445,11 @@ onBeforeUnmount(() => {
:onBack="exitPractise" :onBack="exitPractise"
> >
<view> <view>
<TestDistance v-if="!start && !practiseResult.id" /> <TestDistance
v-if="!start && !practiseResult.id"
:autoStart="true"
@passed="onReady"
/>
<block v-else> <block v-else>
<ShootProgress <ShootProgress
:tips="`${ :tips="`${
+12 -1
View File
@@ -37,6 +37,7 @@ const store = useStore();
const { user, device } = storeToRefs(store); const { user, device } = storeToRefs(store);
const start = ref(false); const start = ref(false);
const practiceStarting = ref(false);
const scores = ref([]); const scores = ref([]);
const isSvip = ref(false); const isSvip = ref(false);
const total = 36; const total = 36;
@@ -155,12 +156,18 @@ const beginPractise = async () => {
}; };
const onReady = async () => { const onReady = async () => {
if (practiceStarting.value || start.value) return;
practiceStarting.value = true;
try {
const result = await beginPractise(); const result = await beginPractise();
if (!result) return; if (!result) return;
scores.value = []; scores.value = [];
isSvip.value = false; isSvip.value = false;
start.value = true; start.value = true;
audioManager.play("练习开始"); audioManager.play("练习开始");
} finally {
practiceStarting.value = false;
}
}; };
const onOver = async (message) => { const onOver = async (message) => {
@@ -452,7 +459,11 @@ onBeforeUnmount(() => {
:onBack="exitPractise" :onBack="exitPractise"
> >
<view> <view>
<TestDistance v-if="!start && !practiseResult.id" /> <TestDistance
v-if="!start && !practiseResult.id"
:autoStart="true"
@passed="onReady"
/>
<block v-else> <block v-else>
<ShootProgress <ShootProgress
:tips="`请连续射${total}支箭`" :tips="`请连续射${total}支箭`"
+44 -2
View File
@@ -6,11 +6,16 @@ import Avatar from "@/components/Avatar.vue";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis"; import { simulShootAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants"; import { MESSAGETYPESV2 } from "@/constants";
import { getDistanceCheckAudioKey, getDistanceCheckText } from "@/util"; import {
getDistanceCheckAudioKey,
getDistanceCheckText,
getShootValidation,
} from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user, device } = storeToRefs(store); const { user, device } = storeToRefs(store);
const emit = defineEmits(["passed"]);
const props = defineProps({ const props = defineProps({
guide: { guide: {
type: Boolean, type: Boolean,
@@ -28,6 +33,10 @@ const props = defineProps({
type: [Number, String], type: [Number, String],
default: "", default: "",
}, },
autoStart: {
type: Boolean,
default: false,
},
}); });
const arrow = ref({}); const arrow = ref({});
const distance = ref(0); const distance = ref(0);
@@ -35,6 +44,29 @@ const statusText = ref("");
const showsimul = ref(false); const showsimul = ref(false);
const count = ref(props.count); const count = ref(props.count);
const timer = ref(null); const timer = ref(null);
const autoStartPending = ref(false);
const autoStartTriggered = ref(false);
let autoStartTimer = null;
const DISTANCE_PASSED_AUDIO_KEY = "站距合格,靶纸正确";
const AUTO_START_TIMEOUT_MS = 6000;
const clearAutoStartTimer = () => {
if (!autoStartTimer) return;
clearTimeout(autoStartTimer);
autoStartTimer = null;
};
const triggerAutoStart = () => {
if (!autoStartPending.value || autoStartTriggered.value) return;
clearAutoStartTimer();
autoStartPending.value = false;
autoStartTriggered.value = true;
emit("passed");
};
const onAudioEnded = (key) => {
if (key === DISTANCE_PASSED_AUDIO_KEY) triggerAutoStart();
};
const updateTimer = (value) => { const updateTimer = (value) => {
count.value = Math.round(value); count.value = Math.round(value);
@@ -48,21 +80,31 @@ onMounted(() => {
}, 1000); }, 1000);
} }
uni.$on("update-timer", updateTimer); uni.$on("update-timer", updateTimer);
uni.$on("audioEnded", onAudioEnded);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timer.value) clearInterval(timer.value); if (timer.value) clearInterval(timer.value);
clearAutoStartTimer();
uni.$off("update-timer", updateTimer); uni.$off("update-timer", updateTimer);
uni.$off("audioEnded", onAudioEnded);
}); });
async function onReceiveMessage(msg) { async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return; if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.TestDistance) { if (msg.type === MESSAGETYPESV2.TestDistance) {
if (autoStartPending.value || autoStartTriggered.value) return;
const rawDistance = Number(msg.shootData?.distance ?? msg.shootData?.dst); const rawDistance = Number(msg.shootData?.distance ?? msg.shootData?.dst);
distance.value = Number.isFinite(rawDistance) distance.value = Number.isFinite(rawDistance)
? Number((rawDistance / 100).toFixed(2)) ? Number((rawDistance / 100).toFixed(2))
: 0; : 0;
statusText.value = getDistanceCheckText(msg.shootData); statusText.value = getDistanceCheckText(msg.shootData);
audioManager.play(getDistanceCheckAudioKey(msg.shootData)); const audioKey = getDistanceCheckAudioKey(msg.shootData);
const validation = getShootValidation(msg.shootData);
if (props.autoStart && validation.distanceOk && validation.targetOk) {
autoStartPending.value = true;
autoStartTimer = setTimeout(triggerAutoStart, AUTO_START_TIMEOUT_MS);
}
audioManager.play(audioKey);
} }
} }
+7
View File
@@ -41,6 +41,7 @@ const { user } = storeToRefs(store);
const sound = ref(true); const sound = ref(true);
const start = ref(false); const start = ref(false);
const practiceStarting = ref(false);
const pageStages = Object.freeze({ const pageStages = Object.freeze({
DISTANCE: "distance", DISTANCE: "distance",
SHOOTING: "shooting", SHOOTING: "shooting",
@@ -1031,6 +1032,7 @@ onLoad((options = {}) => {
}); });
const onReady = async () => { const onReady = async () => {
if (practiceStarting.value) return;
if ( if (
!practiseId.value || !practiseId.value ||
practiceEnded.value || practiceEnded.value ||
@@ -1044,6 +1046,7 @@ const onReady = async () => {
return; return;
} }
practiceStarting.value = true;
pageStage.value = pageStages.LOADING; pageStage.value = pageStages.LOADING;
clearHighlightTestTimer(); clearHighlightTestTimer();
useHighlightTest.value = false; useHighlightTest.value = false;
@@ -1066,6 +1069,8 @@ const onReady = async () => {
pageStage.value = pageStages.DISTANCE; pageStage.value = pageStages.DISTANCE;
setPracticeAppHideResumable(true); setPracticeAppHideResumable(true);
throw error; throw error;
} finally {
practiceStarting.value = false;
} }
}; };
@@ -1332,6 +1337,8 @@ onBeforeUnmount(() => {
<TestDistance <TestDistance
v-if="isDistanceStage" v-if="isDistanceStage"
:targetType="practiceInfo.targetType" :targetType="practiceInfo.targetType"
:autoStart="true"
@passed="onReady"
/> />
<view v-else-if="isShootingStage" class="shooting-layout"> <view v-else-if="isShootingStage" class="shooting-layout">
<view class="shooting-fixed"> <view class="shooting-fixed">