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
+254 -8
View File
@@ -1,6 +1,6 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { ref, nextTick, onMounted, onBeforeUnmount } from "vue";
import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import BowTarget from "@/components/BowTarget.vue";
@@ -16,10 +16,17 @@ import audioManager from "@/audioManager";
import {
createPractiseAPI,
endPractiseAPI,
getCurrentPractiseAPI,
getPractiseAPI,
startPractiseAPI,
} from "@/apis";
import { connectMatchWebSocket, closeMatchWebSocket } from "@/matchWebsocket";
import {
connectMatchWebSocket,
closeMatchWebSocket,
setMatchAppHideResumable,
MATCH_WS_PRACTICE_SYNC_EVENT,
MATCH_WS_STATE_EVENT,
} from "@/matchWebsocket";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
import { MESSAGETYPESV2, roundsName } from "@/constants";
@@ -35,11 +42,21 @@ const isSvip = ref(false);
const total = 12;
const practiseResult = ref({});
const practiseId = ref("");
const serverAddr = ref("");
const showGuide = ref(false);
const tips = ref("");
const targetType = ref(1);
const sharing = 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 restoringSnapshot = ref(false);
let practiceSyncTimer = null;
let waitingPracticeSync = false;
const PRACTICE_SYNC_TIMEOUT_MS = 5000;
const RESULT_TIP_CDN = "https://static.shelingxingqiu.com/shootmini/static";
onLoad((options) => {
@@ -48,6 +65,63 @@ onLoad((options) => {
}
});
const setPracticeAppHideResumable = (enabled) => {
appHideResumable.value = enabled === true;
setMatchAppHideResumable(appHideResumable.value);
};
const clearPracticeSyncTimer = () => {
if (!practiceSyncTimer) return;
clearTimeout(practiceSyncTimer);
practiceSyncTimer = null;
};
const cancelPracticeSyncWait = () => {
waitingPracticeSync = false;
clearPracticeSyncTimer();
};
const startPracticeSyncTimer = () => {
clearPracticeSyncTimer();
waitingPracticeSync = true;
practiceSyncTimer = setTimeout(() => {
practiceSyncTimer = null;
if (!waitingPracticeSync) return;
waitingPracticeSync = false;
foregroundResumeSyncPending.value = false;
hiddenWhileActive.value = false;
closeMatchWebSocket({
reason: "legacy-practice-resume-timeout",
sendLeave: false,
});
uni.showToast({
title: "训练重连失败,请重试",
icon: "none",
});
}, PRACTICE_SYNC_TIMEOUT_MS);
};
const connectPracticeServer = () => {
const latestServerAddr = String(serverAddr.value || "").trim();
if (!practiseId.value || !latestServerAddr) return false;
cancelPracticeSyncWait();
closeMatchWebSocket({
reason: "legacy-practice-switch",
sendLeave: false,
});
connectMatchWebSocket({
serverAddr: latestServerAddr,
matchId: practiseId.value,
userId: user.value.id,
requestPracticeInfoOnOpen: true,
appHideResumable: true,
});
setPracticeAppHideResumable(true);
return true;
};
const createPractise = async () => {
closeMatchWebSocket({ reason: "practice-recreate" });
const result = await createPractiseAPI(
@@ -64,11 +138,8 @@ const createPractise = async () => {
});
return null;
}
connectMatchWebSocket({
serverAddr: result.serverAddr,
matchId: result.id,
userId: user.value.id,
});
serverAddr.value = result.serverAddr;
connectPracticeServer();
return result;
};
@@ -94,6 +165,10 @@ const onReady = async () => {
};
const onOver = async (message) => {
setPracticeAppHideResumable(false);
hiddenWhileActive.value = false;
foregroundResumeSyncPending.value = false;
cancelPracticeSyncWait();
practiseResult.value = Array.isArray(message?.details)
? message
: await getPractiseAPI(practiseId.value);
@@ -105,11 +180,16 @@ async function onReceiveMessage(msg) {
isSvip.value = msg.sVip === true;
scores.value = Array.isArray(msg.details) ? msg.details : scores.value;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setPracticeAppHideResumable(false);
hiddenWhileActive.value = false;
foregroundResumeSyncPending.value = false;
cancelPracticeSyncWait();
setTimeout(() => onOver(msg), 1500);
}
}
async function onComplete() {
setPracticeAppHideResumable(false);
const validArrows = (practiseResult.value.details || []).filter(
(a) => a.x !== -30 && a.y !== -30
);
@@ -117,6 +197,7 @@ async function onComplete() {
uni.navigateBack();
} else {
practiseId.value = "";
serverAddr.value = "";
practiseResult.value = {};
start.value = false;
scores.value = [];
@@ -128,6 +209,10 @@ async function onComplete() {
async function exitPractise() {
if (exiting.value) return;
exiting.value = true;
setPracticeAppHideResumable(false);
hiddenWhileActive.value = false;
foregroundResumeSyncPending.value = false;
cancelPracticeSyncWait();
try {
if (practiseId.value && !practiseResult.value?.details) {
@@ -140,6 +225,141 @@ async function exitPractise() {
}
}
const stopMissingCurrentPracticeAndExit = async () => {
if (exiting.value) return;
const localPractiseId = practiseId.value;
exiting.value = true;
setPracticeAppHideResumable(false);
hiddenWhileActive.value = false;
foregroundResumeSyncPending.value = false;
cancelPracticeSyncWait();
try {
if (localPractiseId) {
await endPractiseAPI(localPractiseId);
}
} catch (error) {
console.error("Failed to stop missing current practice", error);
} finally {
closeMatchWebSocket({ reason: "legacy-practice-current-missing" });
practiseId.value = "";
serverAddr.value = "";
practiseResult.value = {};
start.value = false;
scores.value = [];
isSvip.value = false;
uni.showToast({
title: "训练已结束,请重新进入",
icon: "none",
});
uni.navigateBack();
}
};
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("Failed to get current practice", error);
await stopMissingCurrentPracticeAndExit();
return;
}
if (!pageVisible.value || !hiddenWhileActive.value || exiting.value) return;
const latestPractiseId = currentPractice?.id;
const latestServerAddr = String(currentPractice?.serverAddr || "").trim();
if (
currentPractice === null ||
!latestPractiseId ||
!latestServerAddr
) {
await stopMissingCurrentPracticeAndExit();
return;
}
practiseId.value = latestPractiseId;
serverAddr.value = latestServerAddr;
foregroundResumeSyncPending.value = true;
if (!connectPracticeServer()) {
foregroundResumeSyncPending.value = false;
await stopMissingCurrentPracticeAndExit();
}
} finally {
resumeInFlight.value = false;
}
};
const onMatchSocketState = (event = {}) => {
if (event.state !== "open") return;
if (
event.matchId &&
String(event.matchId) !== String(practiseId.value)
) {
return;
}
startPracticeSyncTimer();
};
const onPracticeInfoSync = async (payload = {}) => {
const responsePractiseId = String(
payload.matchId || payload.practiceInfo?.id || ""
);
if (
!responsePractiseId ||
responsePractiseId !== String(practiseId.value)
) {
return;
}
const snapshot = payload.practiceInfo;
if (!snapshot || typeof snapshot !== "object") return;
cancelPracticeSyncWait();
foregroundResumeSyncPending.value = false;
hiddenWhileActive.value = false;
restoringSnapshot.value = true;
await nextTick();
try {
scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];
isSvip.value = snapshot.sVip === true;
const status = Number(snapshot.status);
if (status === 1) {
start.value = false;
} else if (status === 2) {
start.value = true;
}
} finally {
restoringSnapshot.value = false;
await nextTick();
}
const timeLimit = Number(snapshot.timeLimit);
const duration = Number(snapshot.duration);
if (Number(snapshot.status) === 2 && Number.isFinite(timeLimit) && timeLimit > 0) {
uni.$emit(
"update-remain",
Math.max(0, timeLimit - (Number.isFinite(duration) ? duration : 0))
);
}
};
const getResultTipSrc = (result = {}) => {
const validCount = (result.details || []).filter(
(arrow) => arrow.x !== -30 && arrow.y !== -30
@@ -163,6 +383,25 @@ const onClickShare = debounce(async () => {
}
});
onHide(() => {
pageVisible.value = false;
if (
!appHideResumable.value ||
exiting.value ||
!practiseId.value ||
practiseResult.value?.details
) {
return;
}
hiddenWhileActive.value = true;
});
onShow(async () => {
pageVisible.value = true;
await resumeCurrentPractice();
});
onMounted(async () => {
void audioManager.warmAll();
// audioManager.play("第一轮");
@@ -170,15 +409,21 @@ onMounted(async () => {
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on(MATCH_WS_PRACTICE_SYNC_EVENT, onPracticeInfoSync);
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
uni.$on("share-image", onClickShare);
await createPractise();
});
onBeforeUnmount(() => {
setPracticeAppHideResumable(false);
cancelPracticeSyncWait();
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off(MATCH_WS_PRACTICE_SYNC_EVENT, onPracticeInfoSync);
uni.$off(MATCH_WS_STATE_EVENT, onMatchSocketState);
uni.$off("share-image", onClickShare);
audioManager.stopAll();
closeMatchWebSocket({ reason: "practice-leave" });
@@ -215,6 +460,7 @@ onBeforeUnmount(() => {
<BowPower />
</view>
<BowTarget
v-if="!restoringSnapshot"
:totalRound="start ? total / 4 : 0"
:currentRound="scores.length % 3"
:scores="scores"