update:优化新版个人训练

This commit is contained in:
2026-07-23 14:38:45 +08:00
parent c0d061d7d5
commit ef499e1448
6 changed files with 293 additions and 25 deletions
+2 -1
View File
@@ -400,7 +400,8 @@ function main() {
throw new Error(`未知参数:${unknownArgs.join(", ")}`); throw new Error(`未知参数:${unknownArgs.join(", ")}`);
} }
const source = readFileSync(sourcePath, "utf8"); // 统一换行符,避免 Windows 的 CRLF 让未变更的协议产生不同哈希。
const source = readFileSync(sourcePath, "utf8").replace(/\r\n?/g, "\n");
const { generatedBlock, messageCount, fieldCount } = createGeneratedSource(source); const { generatedBlock, messageCount, fieldCount } = createGeneratedSource(source);
const runtimeSource = readFileSync(runtimePath, "utf8"); const runtimeSource = readFileSync(runtimePath, "utf8");
const startIndex = runtimeSource.indexOf(generatedStartMarker); const startIndex = runtimeSource.indexOf(generatedStartMarker);
+21 -3
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 "./BowTarget.vue"; import BowTarget from "./BowTarget.vue";
@@ -7,6 +8,8 @@ import useStore from "@/store";
import { storeToRefs } from "pinia"; 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 && !isSvip.value);
const props = defineProps({ const props = defineProps({
show: { show: {
@@ -35,8 +38,20 @@ 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
<!-- <text>{{ user.lvlName }}</text> --> :class="[
'header-nickname',
'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>{{ user.lvlName }}</text>
</view> </view>
</view> </view>
<view @click="onClose"> <view @click="onClose">
@@ -95,9 +110,12 @@ const props = defineProps({
margin-left: 10px; margin-left: 10px;
color: #fff; color: #fff;
} }
.header-nickname {
max-width: 240rpx;
}
.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; */
padding: 2px 5px; padding: 2px 5px;
border-radius: 10px; border-radius: 10px;
margin-top: 5px; margin-top: 5px;
+13 -2
View File
@@ -86,6 +86,8 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(["shot-effect-complete"]);
const pMode = ref(true); const pMode = ref(true);
const latestOne = ref(null); const latestOne = ref(null);
const bluelatestOne = ref(null); const bluelatestOne = ref(null);
@@ -248,16 +250,25 @@ function triggerShotEffect(shot, index) {
clearTipTimer(); clearTipTimer();
latestOne.value = null; latestOne.value = null;
hiddenLatestKey.value = key; hiddenLatestKey.value = key;
shotEffect.value = { key, shot }; shotEffect.value = {
key,
shot,
token: props.shotEffectToken,
};
} }
function completeShotEffect(key) { function completeShotEffect(key) {
if (!shotEffect.value || shotEffect.value.key !== key) return; if (!shotEffect.value || shotEffect.value.key !== key) return;
const shot = shotEffect.value.shot; const completedEffect = shotEffect.value;
const shot = completedEffect.shot;
hiddenLatestKey.value = ""; hiddenLatestKey.value = "";
shotEffect.value = null; shotEffect.value = null;
showShotTip(shot); showShotTip(shot);
emit("shot-effect-complete", {
key,
token: completedEffect.token,
});
} }
function shouldHideLatestHit(index) { function shouldHideLatestHit(index) {
@@ -35,6 +35,18 @@ const props = defineProps({
type: String, type: String,
default: "precision", default: "precision",
}, },
isVip: {
type: Boolean,
default: false,
},
isSvip: {
type: Boolean,
default: false,
},
externalShootResultAudio: {
type: Boolean,
default: false,
},
currentRound: { currentRound: {
type: Number, type: Number,
default: 0, default: 0,
@@ -195,6 +207,8 @@ async function onReceiveMessage(msg) {
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
audioManager.play("比赛结束", false); audioManager.play("比赛结束", false);
} else if (msg.type === MESSAGETYPESV2.ShootResult) { } else if (msg.type === MESSAGETYPESV2.ShootResult) {
// 精准训练由页面统一等待语音和飞箭结束,其他训练保持原播放链路。
if (props.externalShootResultAudio) return;
const latestDetail = const latestDetail =
Array.isArray(msg.details) && msg.details.length > 0 Array.isArray(msg.details) && msg.details.length > 0
? msg.details[msg.details.length - 1] ? msg.details[msg.details.length - 1]
@@ -261,7 +275,19 @@ onBeforeUnmount(() => {
image-mode="aspectFill" image-mode="aspectFill"
/> />
</view> </view>
<text class="progress-card__name">{{ displayName }}</text> <view
:class="[
'progress-card__name',
'member-nickname',
isVip && !isSvip ? 'member-nickname--vip' : '',
isSvip ? 'member-nickname--svip' : '',
]"
>
<text class="member-nickname__text">{{ displayName }}</text>
<text v-if="isSvip" class="member-nickname__shine">
{{ displayName }}
</text>
</view>
</view> </view>
<!-- <button class="progress-card__sound" hover-class="none" @click="updateSound"> <!-- <button class="progress-card__sound" hover-class="none" @click="updateSound">
@@ -338,9 +364,10 @@ onBeforeUnmount(() => {
.progress-card__name { .progress-card__name {
width: 86rpx; width: 86rpx;
color: #E7BA80; color: #fff;
font-size: 18rpx; font-size: 18rpx;
line-height: 1; line-height: 1;
justify-content: center;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
+4 -5
View File
@@ -30,6 +30,8 @@ const trainingModeIconMap = {
}; };
// 训练项目卡片标题图,按接口 id 映射 CDN 资源。 // 训练项目卡片标题图,按接口 id 映射 CDN 资源。
const trainingModeTitleImageMap = { const trainingModeTitleImageMap = {
base:
"https://static.shelingxingqiu.com/shootmini/static/training-home/jichuxunlian.png",
endurance: endurance:
"https://static.shelingxingqiu.com/shootmini/static/training-home/nailixunlian.png", "https://static.shelingxingqiu.com/shootmini/static/training-home/nailixunlian.png",
precision: precision:
@@ -515,7 +517,6 @@ onShow(async () => {
/> />
<view class="featured-card-mask"></view> <view class="featured-card-mask"></view>
<view class="featured-card-copy"> <view class="featured-card-copy">
<text class="featured-card-title">常规训练</text>
<text class="featured-card-subtitle">12箭练习</text> <text class="featured-card-subtitle">12箭练习</text>
</view> </view>
</view> </view>
@@ -857,13 +858,12 @@ onShow(async () => {
top: 0; top: 0;
width: 278rpx; width: 278rpx;
height: 150rpx; height: 150rpx;
background: linear-gradient(90deg, #ffdaa0 0%, #f5c580 74%, rgba(245, 197, 128, 0) 100%);
} }
.featured-card-copy { .featured-card-copy {
position: absolute; position: absolute;
left: 30rpx; left: 174rpx;
top: 34rpx; top: 58rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -883,7 +883,6 @@ onShow(async () => {
color: #895409; color: #895409;
font-size: 22rpx; font-size: 22rpx;
line-height: 32rpx; line-height: 32rpx;
opacity: 0.72;
} }
.mode-grid { .mode-grid {
+224 -12
View File
@@ -25,7 +25,7 @@ import {
MATCH_WS_STATE_EVENT, MATCH_WS_STATE_EVENT,
} from "@/matchWebsocket"; } from "@/matchWebsocket";
import { sharePractiseData } from "@/canvas"; import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util"; import { wxShare, debounce, getDirectionText } from "@/util";
import { MESSAGETYPESV2, roundsName } from "@/constants"; import { MESSAGETYPESV2, roundsName } from "@/constants";
import useStore from "@/store"; import useStore from "@/store";
@@ -56,6 +56,11 @@ const tips = ref("");
const targetType = ref(defaultTargetType); const targetType = ref(defaultTargetType);
const trainingParams = ref({}); const trainingParams = ref({});
const practiceInfo = ref({}); const practiceInfo = ref({});
// 服务端状态立即落到 practiceInfo,精准训练的目标区域单独延迟展示。
const visiblePrecisionTarget = ref({
randomBlock: 0,
randomRingArea: 0,
});
const trainingDifficultyStorageKey = "training-selection"; const trainingDifficultyStorageKey = "training-selection";
const trainingDifficultyRefreshEvent = "training-difficulty-refresh"; const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
const useHighlightTest = ref(false); const useHighlightTest = ref(false);
@@ -75,7 +80,14 @@ const connectionClosed = ref(true);
let stopPracticeTask = null; let stopPracticeTask = null;
let practiceSyncTimer = null; let practiceSyncTimer = null;
let waitingPracticeSync = false; let waitingPracticeSync = false;
let shotPresentationGeneration = 0;
const audioWaiters = new Set();
const shotEffectWaiters = new Map();
const PRACTICE_SYNC_TIMEOUT_MS = 5000; const PRACTICE_SYNC_TIMEOUT_MS = 5000;
const SHOT_EFFECT_WAIT_TIMEOUT_MS = 1200;
const AUDIO_TIMEOUT_BASE = 3500;
const AUDIO_TIMEOUT_PER_KEY = 2600;
const AUDIO_TIMEOUT_MAX = 12000;
const env = computed(() => { const env = computed(() => {
try { try {
@@ -92,6 +104,9 @@ const showResult = computed(
() => pageStage.value === pageStages.RESULT && hasPractiseResult.value () => pageStage.value === pageStages.RESULT && hasPractiseResult.value
); );
const isSvip = computed(() => practiceInfo.value.sVip === true); const isSvip = computed(() => practiceInfo.value.sVip === true);
const isVip = computed(
() => practiceInfo.value.vip === true && !isSvip.value
);
const trainingType = computed( const trainingType = computed(
() => practiceInfo.value.trainingType || trainingParams.value.type || "" () => practiceInfo.value.trainingType || trainingParams.value.type || ""
@@ -108,6 +123,15 @@ const getPositiveInteger = (value) => {
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0; return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
}; };
const getPrecisionTargetSnapshot = (source = {}) => ({
randomBlock: getPositiveInteger(source.randomBlock),
randomRingArea: getPositiveInteger(source.randomRingArea),
});
const applyVisiblePrecisionTarget = (source = {}) => {
visiblePrecisionTarget.value = getPrecisionTargetSnapshot(source);
};
const currentDifficultyLevel = computed( const currentDifficultyLevel = computed(
() => () =>
getPositiveInteger(practiseResult.value.difficultyLevel) || getPositiveInteger(practiseResult.value.difficultyLevel) ||
@@ -136,7 +160,7 @@ const precisionRandomBlock = computed(() => {
const block = getPositiveInteger( const block = getPositiveInteger(
useHighlightTest.value useHighlightTest.value
? highlightTestState.value.randomBlock ? highlightTestState.value.randomBlock
: practiceInfo.value.randomBlock : visiblePrecisionTarget.value.randomBlock
); );
return block <= precisionBlocks.value ? block : 0; return block <= precisionBlocks.value ? block : 0;
}); });
@@ -145,7 +169,7 @@ const precisionRandomRingArea = computed(() => {
const ring = getPositiveInteger( const ring = getPositiveInteger(
useHighlightTest.value useHighlightTest.value
? highlightTestState.value.randomRingArea ? highlightTestState.value.randomRingArea
: practiceInfo.value.randomRingArea : visiblePrecisionTarget.value.randomRingArea
); );
return ring >= 1 && ring <= 10 ? ring : 0; return ring >= 1 && ring <= 10 ? ring : 0;
}); });
@@ -239,6 +263,7 @@ const practiceInfoFields = [
"statusText", "statusText",
"startTime", "startTime",
"targetType", "targetType",
"vip",
"sVip", "sVip",
"trainingType", "trainingType",
"difficultyLevel", "difficultyLevel",
@@ -332,6 +357,143 @@ const syncPracticeInfo = (message = {}) => {
}; };
}; };
const buildShootResultAudioKeys = (message = {}) => {
const latestDetail =
Array.isArray(message.details) && message.details.length > 0
? message.details[message.details.length - 1]
: null;
// 与原 ShootProgress 保持一致:优先使用当前箭,details 仅作兼容兜底。
const arrow = message.shootData || latestDetail;
if (!arrow) return [];
if (
arrow.playerId !== undefined &&
arrow.playerId !== null &&
String(arrow.playerId) !== String(user.value?.id)
) {
return [];
}
const keys = [
arrow.ring ? `${arrow.ringX ? "X" : arrow.ring}` : "未上靶",
];
if (arrow.angle !== null && arrow.angle !== undefined) {
keys.push(`${getDirectionText(arrow.angle)}调整`);
}
if (arrow.threeConsecutive10Rings === true) {
keys.push("tententen");
}
return keys;
};
const playAudioKeysAndWait = (keys) => {
const audioKeys = (Array.isArray(keys) ? keys : [keys]).filter(Boolean);
if (audioKeys.length === 0) return Promise.resolve();
const expectedKey = audioKeys[audioKeys.length - 1];
const waitTime = Math.min(
AUDIO_TIMEOUT_MAX,
Math.max(AUDIO_TIMEOUT_BASE, audioKeys.length * AUDIO_TIMEOUT_PER_KEY)
);
return new Promise((resolve) => {
let settled = false;
let timer = null;
const waiter = {
expectedKey,
done: () => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
audioWaiters.delete(waiter);
resolve();
},
};
timer = setTimeout(() => {
if (typeof audioManager.recoverIfStale === "function") {
audioManager.recoverIfStale(expectedKey);
}
waiter.done();
}, waitTime);
audioWaiters.add(waiter);
try {
audioManager.play(audioKeys, false);
} catch (error) {
console.error("training shoot result audio failed", error);
waiter.done();
}
});
};
const shouldWaitForShotEffect = (shot) => {
const x = Number(shot?.x);
const y = Number(shot?.y);
return (
isSvip.value &&
Number(shot?.ring) > 0 &&
Number.isFinite(x) &&
Number.isFinite(y)
);
};
const waitForShotEffect = (token, shouldWait) => {
if (!shouldWait) return Promise.resolve();
return new Promise((resolve) => {
let settled = false;
let timer = null;
const waiter = {
done: () => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
if (shotEffectWaiters.get(token) === waiter) {
shotEffectWaiters.delete(token);
}
resolve();
},
};
timer = setTimeout(waiter.done, SHOT_EFFECT_WAIT_TIMEOUT_MS);
shotEffectWaiters.set(token, waiter);
});
};
const invalidateShotPresentations = ({ resetVisible = false } = {}) => {
shotPresentationGeneration += 1;
Array.from(audioWaiters).forEach((waiter) => waiter.done());
Array.from(shotEffectWaiters.values()).forEach((waiter) => waiter.done());
if (resetVisible) {
applyVisiblePrecisionTarget();
}
return shotPresentationGeneration;
};
const onShotEffectComplete = (payload = {}) => {
const token = Number(payload?.token ?? payload);
if (!Number.isFinite(token)) return;
shotEffectWaiters.get(token)?.done();
};
const commitPrecisionTargetAfterPresentation = async ({
generation,
target,
audioPromise,
effectPromise,
}) => {
await Promise.all([audioPromise, effectPromise]);
if (
generation !== shotPresentationGeneration ||
practiceEnded.value ||
!isShootingStage.value ||
trainingType.value !== "precision"
) {
return;
}
visiblePrecisionTarget.value = target;
};
const createPracticeEndSnapshot = (message = {}) => { const createPracticeEndSnapshot = (message = {}) => {
const source = { const source = {
...practiceInfo.value, ...practiceInfo.value,
@@ -436,11 +598,13 @@ const onPracticeInfoSync = (payload = {}) => {
const shouldShowDistance = const shouldShowDistance =
waitingPracticeSync && pageStage.value === pageStages.LOADING; waitingPracticeSync && pageStage.value === pageStages.LOADING;
cancelPracticeSyncWait(); cancelPracticeSyncWait();
invalidateShotPresentations();
// 14 是完整快照,先清空旧值,避免 proto3 省略的 0 沿用上一份状态。 // 14 是完整快照,先清空旧值,避免 proto3 省略的 0 沿用上一份状态。
practiceInfo.value = {}; practiceInfo.value = {};
practiceEndSnapshot.value = {}; practiceEndSnapshot.value = {};
syncPracticeInfo(snapshot); syncPracticeInfo(snapshot);
applyVisiblePrecisionTarget(practiceInfo.value);
scores.value = Array.isArray(snapshot.details) ? snapshot.details : []; scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];
if (shouldShowDistance) { if (shouldShowDistance) {
@@ -667,6 +831,7 @@ const onReady = async () => {
clearHighlightTestTimer(); clearHighlightTestTimer();
useHighlightTest.value = false; useHighlightTest.value = false;
practiceEndSnapshot.value = {}; practiceEndSnapshot.value = {};
invalidateShotPresentations();
try { try {
await startPractiseAPI(practiseId.value); await startPractiseAPI(practiseId.value);
practiseResult.value = {}; practiseResult.value = {};
@@ -694,6 +859,7 @@ const enterPracticeResult = (result = {}) => {
// 正常结算不调用 stop,只清理上下文并断开比赛服连接。 // 正常结算不调用 stop,只清理上下文并断开比赛服连接。
practiceEnded.value = true; practiceEnded.value = true;
invalidateShotPresentations();
clearPracticeRuntimeContext(); clearPracticeRuntimeContext();
closePracticeConnection("training-practice-result"); closePracticeConnection("training-practice-result");
pageStage.value = pageStages.RESULT; pageStage.value = pageStages.RESULT;
@@ -724,15 +890,47 @@ const onOver = async () => {
}; };
async function onReceiveMessage(msg) { async function onReceiveMessage(msg) {
const previousScoreLength = scores.value.length;
syncPracticeInfo(msg); syncPracticeInfo(msg);
if (msg.type === MESSAGETYPESV2.ShootResult && isShootingStage.value) { if (msg.type === MESSAGETYPESV2.BattleStart) {
invalidateShotPresentations();
applyVisiblePrecisionTarget(practiceInfo.value);
} else if (
msg.type === MESSAGETYPESV2.ShootResult &&
isShootingStage.value
) {
let hasNewShot = false;
let latestShot = null;
if (Array.isArray(msg.details)) { if (Array.isArray(msg.details)) {
const previousScoreLength = scores.value.length;
scores.value = msg.details; scores.value = msg.details;
if (msg.details.length === previousScoreLength + 1) { hasNewShot = msg.details.length === previousScoreLength + 1;
shotEffectToken.value += 1; latestShot = hasNewShot ? msg.details[msg.details.length - 1] : null;
} }
if (trainingType.value === "precision") {
const generation = invalidateShotPresentations();
const target = getPrecisionTargetSnapshot(practiceInfo.value);
const nextEffectToken = hasNewShot ? shotEffectToken.value + 1 : 0;
const effectPromise = waitForShotEffect(
nextEffectToken,
hasNewShot && shouldWaitForShotEffect(latestShot)
);
const audioPromise = playAudioKeysAndWait(
buildShootResultAudioKeys(msg)
);
if (hasNewShot) {
shotEffectToken.value = nextEffectToken;
}
void commitPrecisionTargetAfterPresentation({
generation,
target,
audioPromise,
effectPromise,
});
} else if (hasNewShot) {
shotEffectToken.value += 1;
} }
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
practiceEndSnapshot.value = createPracticeEndSnapshot(msg); practiceEndSnapshot.value = createPracticeEndSnapshot(msg);
@@ -747,6 +945,7 @@ async function onReceiveMessage(msg) {
}; };
} }
practiceEnded.value = true; practiceEnded.value = true;
invalidateShotPresentations();
clearPracticeRuntimeContext(); clearPracticeRuntimeContext();
// setTimeout(onOver, 1500); // setTimeout(onOver, 1500);
} }
@@ -756,6 +955,7 @@ function onComplete() {
pageStage.value = pageStages.LOADING; pageStage.value = pageStages.LOADING;
start.value = false; start.value = false;
practiceEnded.value = true; practiceEnded.value = true;
invalidateShotPresentations();
clearPracticeRuntimeContext(); clearPracticeRuntimeContext();
closePracticeConnection("training-practice-complete"); closePracticeConnection("training-practice-complete");
uni.$emit(trainingDifficultyRefreshEvent); uni.$emit(trainingDifficultyRefreshEvent);
@@ -771,6 +971,7 @@ async function onRetry() {
practiseResult.value = {}; practiseResult.value = {};
practiceEndSnapshot.value = {}; practiceEndSnapshot.value = {};
practiceInfo.value = {}; practiceInfo.value = {};
invalidateShotPresentations({ resetVisible: true });
start.value = false; start.value = false;
scores.value = []; scores.value = [];
shotEffectToken.value = 0; shotEffectToken.value = 0;
@@ -788,9 +989,12 @@ const onClickShare = debounce(async () => {
await wxShare("shareCanvas"); await wxShare("shareCanvas");
}); });
function onAudioEnded(s) { function onAudioEnded(key) {
if (s.indexOf("比赛结束") >= 0) { Array.from(audioWaiters).forEach((waiter) => {
onOver() if (waiter.expectedKey === key) waiter.done();
});
if (String(key || "").includes("比赛结束")) {
void onOver();
} }
} }
@@ -802,6 +1006,7 @@ const updateSound = () => {
const exitPractice = async () => { const exitPractice = async () => {
if (exiting.value) return; if (exiting.value) return;
exiting.value = true; exiting.value = true;
invalidateShotPresentations();
try { try {
await stopCurrentPractice(); await stopCurrentPractice();
@@ -813,6 +1018,7 @@ const exitPractice = async () => {
}; };
onHide(() => { onHide(() => {
invalidateShotPresentations();
// 小程序被切到后台时尽早通知后端,作为杀进程前的尽力兜底。 // 小程序被切到后台时尽早通知后端,作为杀进程前的尽力兜底。
if ( if (
!exiting.value && !exiting.value &&
@@ -842,6 +1048,7 @@ onShow(async () => {
}); });
onUnload(() => { onUnload(() => {
invalidateShotPresentations();
clearPracticeRuntimeContext(); clearPracticeRuntimeContext();
void stopCurrentPractice(); void stopCurrentPractice();
closePracticeConnection("training-practice-unload"); closePracticeConnection("training-practice-unload");
@@ -869,6 +1076,7 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
invalidateShotPresentations();
clearPracticeRuntimeContext(); clearPracticeRuntimeContext();
void stopCurrentPractice(); void stopCurrentPractice();
uni.setKeepScreenOn({ uni.setKeepScreenOn({
@@ -904,6 +1112,9 @@ onBeforeUnmount(() => {
:total="timeLimit" :total="timeLimit"
:countdownEnabled="hasTimeLimit" :countdownEnabled="hasTimeLimit"
:trainingType="trainingType" :trainingType="trainingType"
:isVip="isVip"
:isSvip="isSvip"
:externalShootResultAudio="trainingType === 'precision'"
:onStop="onTimeLimitReached" :onStop="onTimeLimitReached"
/> />
<view class="user-row"> <view class="user-row">
@@ -925,8 +1136,9 @@ onBeforeUnmount(() => {
:activeSector="precisionRandomBlock" :activeSector="precisionRandomBlock"
:activeRing="precisionRandomRingArea" :activeRing="precisionRandomRingArea"
:showSectorLabels="precisionBlocks > 0" :showSectorLabels="precisionBlocks > 0"
@shot-effect-complete="onShotEffectComplete"
/> />
<view v-if="env !== 'release'" class="highlight-test-actions"> <!-- <view v-if="env !== 'release'" class="highlight-test-actions">
<button <button
class="highlight-test-btn" class="highlight-test-btn"
hover-class="none" hover-class="none"
@@ -941,7 +1153,7 @@ onBeforeUnmount(() => {
> >
重置高亮 重置高亮
</button> </button>
</view> </view> -->
<view class="sound-text-box"> <view class="sound-text-box">
<button class="sound-btn" hover-class="none" @click="updateSound"> <button class="sound-btn" hover-class="none" @click="updateSound">
<image <image