update:新增个人训练重连机制,修复海报

This commit is contained in:
2026-07-24 16:00:46 +08:00
parent f316d52cec
commit c5618fb60b
12 changed files with 811 additions and 69 deletions
+11 -1
View File
@@ -13,7 +13,7 @@ import PointSwitcher from "@/components/PointSwitcher.vue";
import TargetCanvas from "@/components/TargetCanvas.vue";
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
import { simulShootAPI } from "@/apis";
import { simulShootAPI, laserAimAPI, laserCloseAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -372,6 +372,14 @@ const simulShoot2 = async () => {
}
};
const openAim = async () => {
await laserAimAPI();
};
const closeAim = async () => {
await laserCloseAPI();
};
const env = computed(() => {
const accountInfo = uni.getAccountInfoSync();
return accountInfo.miniProgram.envVersion;
@@ -557,6 +565,8 @@ onBeforeUnmount(() => {
<view class="simul" v-if="env !== 'release'">
<button @click="simulShoot">模拟</button>
<button @click="simulShoot2">射箭</button>
<button @click="openAim">开瞄</button>
<button @click="closeAim">关瞄</button>
</view>
</view>
</template>
+156 -26
View File
@@ -14,6 +14,7 @@ import audioManager from "@/audioManager";
import {
createPractiseV2API,
getCurrentPractiseAPI,
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
@@ -21,6 +22,7 @@ import {
import {
connectMatchWebSocket,
closeMatchWebSocket,
setMatchAppHideResumable,
MATCH_WS_PRACTICE_SYNC_EVENT,
MATCH_WS_STATE_EVENT,
} from "@/matchWebsocket";
@@ -76,6 +78,10 @@ const stopCompleted = ref(false);
const stopInFlight = ref(false);
const exiting = ref(false);
const hiddenWhileActive = ref(false);
const resumeInFlight = ref(false);
const foregroundResumeSyncPending = ref(false);
const pageVisible = ref(true);
const appHideResumable = ref(false);
const connectionClosed = ref(true);
let stopPracticeTask = null;
let practiceSyncTimer = null;
@@ -108,6 +114,12 @@ const isVip = computed(
() => practiceInfo.value.vip === true && !isSvip.value
);
const setPracticeAppHideResumable = (enabled) => {
const nextValue = enabled === true;
appHideResumable.value = nextValue;
setMatchAppHideResumable(nextValue);
};
const trainingType = computed(
() => practiceInfo.value.trainingType || trainingParams.value.type || ""
);
@@ -561,6 +573,19 @@ const startPracticeSyncTimer = () => {
practiceSyncTimer = null;
if (!waitingPracticeSync) return;
waitingPracticeSync = false;
if (foregroundResumeSyncPending.value) {
foregroundResumeSyncPending.value = false;
closePracticeConnection("training-practice-resume-timeout", {
sendLeave: false,
});
uni.showToast({
title: "训练重连失败,请重试",
icon: "none",
});
return;
}
if (pageStage.value !== pageStages.LOADING) return;
uni.showToast({
@@ -597,7 +622,9 @@ const onPracticeInfoSync = (payload = {}) => {
const shouldShowDistance =
waitingPracticeSync && pageStage.value === pageStages.LOADING;
const resumedFromForeground = foregroundResumeSyncPending.value;
cancelPracticeSyncWait();
foregroundResumeSyncPending.value = false;
invalidateShotPresentations();
// 14 是完整快照,先清空旧值,避免 proto3 省略的 0 沿用上一份状态。
@@ -607,6 +634,10 @@ const onPracticeInfoSync = (payload = {}) => {
applyVisiblePrecisionTarget(practiceInfo.value);
scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];
if (resumedFromForeground) {
hiddenWhileActive.value = false;
}
if (shouldShowDistance) {
start.value = false;
pageStage.value = pageStages.DISTANCE;
@@ -650,18 +681,21 @@ const clearPracticeRuntimeContext = () => {
uni.setStorageSync(trainingDifficultyStorageKey, selectionContext);
};
const closePracticeConnection = (reason) => {
const closePracticeConnection = (reason, { sendLeave = true } = {}) => {
cancelPracticeSyncWait();
if (connectionClosed.value) return;
connectionClosed.value = true;
closeMatchWebSocket({ reason });
closeMatchWebSocket({ reason, sendLeave });
};
const connectPracticeServer = () => {
const connectPracticeServer = ({
resumableOnAppHide = appHideResumable.value,
} = {}) => {
if (!practiseId.value || !String(serverAddr.value || "").trim()) {
return false;
}
appHideResumable.value = resumableOnAppHide === true;
cancelPracticeSyncWait();
closeMatchWebSocket({ reason: "training-practice-switch" });
preparePracticeSyncWait();
@@ -670,6 +704,7 @@ const connectPracticeServer = () => {
matchId: practiseId.value,
userId: user.value.id,
requestPracticeInfoOnOpen: true,
appHideResumable: appHideResumable.value,
});
connectionClosed.value = false;
return true;
@@ -703,6 +738,97 @@ const stopCurrentPractice = () => {
return stopPracticeTask;
};
// 当前训练不可恢复时,始终使用页面本地的练习 ID 停止。
const stopEndedCurrentPracticeAndExit = async () => {
hiddenWhileActive.value = false;
foregroundResumeSyncPending.value = false;
setPracticeAppHideResumable(false);
exiting.value = true;
try {
await stopCurrentPractice();
} finally {
closePracticeConnection("training-practice-current-missing");
clearPracticeRuntimeContext();
uni.showToast({
title: "训练已结束,请重新进入",
icon: "none",
});
uni.navigateBack();
}
};
// 回到前台后先获取最新比赛服地址,再通过 type 5/type 14 恢复完整快照。
const resumeCurrentPractice = async () => {
if (
resumeInFlight.value ||
!hiddenWhileActive.value ||
!appHideResumable.value ||
exiting.value
) {
return;
}
resumeInFlight.value = true;
try {
let currentPractice;
try {
currentPractice = await getCurrentPractiseAPI();
} catch (error) {
if (!pageVisible.value || exiting.value) return;
console.error("get current practice failed", error);
await stopEndedCurrentPracticeAndExit();
return;
}
console.log(1111111111111111111111, currentPractice)
if (
!pageVisible.value ||
!hiddenWhileActive.value ||
exiting.value
) {
return;
}
const latestPracticeId = currentPractice?.id;
const latestServerAddr = String(
currentPractice?.serverAddr || ""
).trim();
if (
currentPractice === null ||
!latestPracticeId ||
!latestServerAddr
) {
await stopEndedCurrentPracticeAndExit();
return;
}
practiseId.value = latestPracticeId;
serverAddr.value = latestServerAddr;
updateTrainingContext({
id: latestPracticeId,
serverAddr: latestServerAddr,
});
foregroundResumeSyncPending.value = true;
setPracticeAppHideResumable(true);
if (!connectPracticeServer({ resumableOnAppHide: true })) {
foregroundResumeSyncPending.value = false;
await stopEndedCurrentPracticeAndExit();
}
} catch (error) {
foregroundResumeSyncPending.value = false;
if (!pageVisible.value || exiting.value) return;
console.error("training practice resume failed", error);
uni.showToast({
title: "训练重连失败,请重试",
icon: "none",
});
} finally {
resumeInFlight.value = false;
}
};
const createPractice = async () => {
const trainingType = trainingParams.value.type;
const difficultyLevel = trainingParams.value.difficulty;
@@ -740,7 +866,7 @@ const createPractice = async () => {
stopInFlight.value = false;
stopPracticeTask = null;
updateTrainingContext(result);
connectPracticeServer();
connectPracticeServer({ resumableOnAppHide: true });
return result;
};
@@ -839,10 +965,12 @@ const onReady = async () => {
shotEffectToken.value = 0;
start.value = true;
pageStage.value = pageStages.SHOOTING;
setPracticeAppHideResumable(true);
audioManager.play("练习开始");
} catch (error) {
start.value = false;
pageStage.value = pageStages.DISTANCE;
setPracticeAppHideResumable(true);
throw error;
}
};
@@ -851,6 +979,8 @@ const onTimeLimitReached = () => {
if (!hasTimeLimit.value || !isShootingStage.value) return;
// 本地倒计时只负责停止射击展示,最终结算以 PRACTICE_END 为准。
start.value = false;
hiddenWhileActive.value = false;
setPracticeAppHideResumable(false);
};
const enterPracticeResult = (result = {}) => {
@@ -859,6 +989,7 @@ const enterPracticeResult = (result = {}) => {
// 正常结算不调用 stop,只清理上下文并断开比赛服连接。
practiceEnded.value = true;
setPracticeAppHideResumable(false);
invalidateShotPresentations();
clearPracticeRuntimeContext();
closePracticeConnection("training-practice-result");
@@ -872,6 +1003,7 @@ const onOver = async () => {
clearHighlightTestTimer();
pageStage.value = pageStages.LOADING;
start.value = false;
setPracticeAppHideResumable(false);
try {
const apiResult = (await getPractiseAPI(practiseId.value)) || {};
@@ -885,6 +1017,7 @@ const onOver = async () => {
}
start.value = true;
pageStage.value = pageStages.SHOOTING;
setPracticeAppHideResumable(true);
throw error;
}
};
@@ -945,6 +1078,7 @@ async function onReceiveMessage(msg) {
};
}
practiceEnded.value = true;
setPracticeAppHideResumable(false);
invalidateShotPresentations();
clearPracticeRuntimeContext();
// setTimeout(onOver, 1500);
@@ -955,6 +1089,7 @@ function onComplete() {
pageStage.value = pageStages.LOADING;
start.value = false;
practiceEnded.value = true;
setPracticeAppHideResumable(false);
invalidateShotPresentations();
clearPracticeRuntimeContext();
closePracticeConnection("training-practice-complete");
@@ -964,6 +1099,7 @@ function onComplete() {
async function onRetry() {
pageStage.value = pageStages.LOADING;
setPracticeAppHideResumable(false);
clearHighlightTestTimer();
useHighlightTest.value = false;
practiseId.value = "";
@@ -1006,6 +1142,7 @@ const updateSound = () => {
const exitPractice = async () => {
if (exiting.value) return;
exiting.value = true;
setPracticeAppHideResumable(false);
invalidateShotPresentations();
try {
@@ -1018,36 +1155,28 @@ const exitPractice = async () => {
};
onHide(() => {
invalidateShotPresentations();
// 小程序被切到后台时尽早通知后端,作为杀进程前的尽力兜底。
pageVisible.value = false;
if (
!exiting.value &&
practiseId.value &&
!practiceEnded.value &&
!stopCompleted.value
!appHideResumable.value ||
exiting.value ||
!practiseId.value ||
practiceEnded.value ||
stopCompleted.value
) {
hiddenWhileActive.value = true;
clearPracticeRuntimeContext();
void stopCurrentPractice();
return;
}
closePracticeConnection("training-practice-hide");
hiddenWhileActive.value = true;
invalidateShotPresentations();
});
onShow(async () => {
if (!hiddenWhileActive.value || exiting.value) return;
hiddenWhileActive.value = false;
await stopCurrentPractice();
clearPracticeRuntimeContext();
exiting.value = true;
uni.showToast({
title: "训练已结束,请重新进入",
icon: "none",
});
uni.navigateBack();
pageVisible.value = true;
await resumeCurrentPractice();
});
onUnload(() => {
setPracticeAppHideResumable(false);
invalidateShotPresentations();
clearPracticeRuntimeContext();
void stopCurrentPractice();
@@ -1064,7 +1193,7 @@ onMounted(() => {
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
uni.$on("share-image", onClickShare);
uni.$on("audioEnded", onAudioEnded);
if (!connectPracticeServer()) {
if (!connectPracticeServer({ resumableOnAppHide: true })) {
uni.showToast({
title: "练习连接信息异常,请重试",
icon: "none",
@@ -1076,6 +1205,7 @@ onMounted(() => {
});
onBeforeUnmount(() => {
setPracticeAppHideResumable(false);
invalidateShotPresentations();
clearPracticeRuntimeContext();
void stopCurrentPractice();