update:新增个人训练重连机制,修复海报
This commit is contained in:
+5
-1
@@ -8,7 +8,7 @@ try {
|
|||||||
|
|
||||||
switch (envVersion) {
|
switch (envVersion) {
|
||||||
case "develop": // 开发版
|
case "develop": // 开发版
|
||||||
// BASE_URL = "http://192.168.1.5:8000/api/shoot";
|
// BASE_URL = "http://192.168.1.2:8000/api/shoot";
|
||||||
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
|
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
|
||||||
break;
|
break;
|
||||||
case "trial": // 体验版
|
case "trial": // 体验版
|
||||||
@@ -269,6 +269,10 @@ export const createPractiseV2API = (trainingType, difficultyLevel) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCurrentPractiseAPI = () => {
|
||||||
|
return request("GET", "/user/practice/current");
|
||||||
|
};
|
||||||
|
|
||||||
export const startPractiseAPI = (id) => {
|
export const startPractiseAPI = (id) => {
|
||||||
return request("POST", "/user/practice/begin", { id });
|
return request("POST", "/user/practice/begin", { id });
|
||||||
};
|
};
|
||||||
|
|||||||
+32
-15
@@ -635,7 +635,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
"../static/score-bg.png",
|
"/static/score-bg.png",
|
||||||
16 + (i % 9) * 30,
|
16 + (i % 9) * 30,
|
||||||
290 + Math.ceil((i + 1) / 9) * 30,
|
290 + Math.ceil((i + 1) / 9) * 30,
|
||||||
27,
|
27,
|
||||||
@@ -657,7 +657,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
|
|||||||
ctx.drawImage(bgImg, 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, 38);
|
ctx.drawImage(bgImg, 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, 38);
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
"../static/score-bg.png",
|
"/static/score-bg.png",
|
||||||
24 + rowIndex * 42,
|
24 + rowIndex * 42,
|
||||||
i > 5 ? 362 : 320,
|
i > 5 ? 362 : 320,
|
||||||
38,
|
38,
|
||||||
@@ -706,22 +706,39 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
|||||||
);
|
);
|
||||||
const bgImg = await loadCanvasImage(canvas, bgImgSrc);
|
const bgImg = await loadCanvasImage(canvas, bgImgSrc);
|
||||||
|
|
||||||
const avatarImgPromise = loadImage(user.avatar).then((path) =>
|
// 头像与段位框属于装饰图片,缺失时使用兜底或跳过,避免阻断分享。
|
||||||
loadCanvasImage(canvas, path)
|
const loadProfileImage = async (src, fallbackSrc = "", label = "") => {
|
||||||
);
|
const normalizedSrc =
|
||||||
const lvlImgPromise = loadImage(user.lvlImage).then((path) =>
|
typeof src === "string" && src.startsWith("../static/")
|
||||||
loadCanvasImage(canvas, path)
|
? src.slice(2)
|
||||||
);
|
: src;
|
||||||
|
if (normalizedSrc) {
|
||||||
|
try {
|
||||||
|
const path = await loadImage(normalizedSrc);
|
||||||
|
return await loadCanvasImage(canvas, path);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`share ${label} image load failed`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallbackSrc ? loadCanvasImage(canvas, fallbackSrc) : null;
|
||||||
|
};
|
||||||
|
|
||||||
let titleImageSrc = "../static/first-try-title.png";
|
const avatarImgPromise = loadProfileImage(
|
||||||
|
user?.avatar,
|
||||||
|
"/static/user-icon.png",
|
||||||
|
"avatar"
|
||||||
|
);
|
||||||
|
const lvlImgPromise = loadProfileImage(user?.lvlImage, "", "level");
|
||||||
|
|
||||||
|
let titleImageSrc = "/static/first-try-title.png";
|
||||||
if (type == 2) {
|
if (type == 2) {
|
||||||
titleImageSrc = "../static/practise-one-title.png";
|
titleImageSrc = "/static/practise-one-title.png";
|
||||||
} else if (type == 3) {
|
} else if (type == 3) {
|
||||||
titleImageSrc = "../static/practise-two-title.png";
|
titleImageSrc = "/static/practise-two-title.png";
|
||||||
}
|
}
|
||||||
const titleImgPromise = loadCanvasImage(canvas, titleImageSrc);
|
const titleImgPromise = loadCanvasImage(canvas, titleImageSrc);
|
||||||
const scoreBgImgPromise = loadCanvasImage(canvas, "../static/score-bg.png");
|
const scoreBgImgPromise = loadCanvasImage(canvas, "/static/score-bg.png");
|
||||||
const qrCodeImgPromise = loadCanvasImage(canvas, "../static/qr-code.png");
|
const qrCodeImgPromise = loadCanvasImage(canvas, "/static/qr-code.png");
|
||||||
|
|
||||||
const [avatarImg, lvlImg, titleImg, scoreBgImg, qrCodeImg] =
|
const [avatarImg, lvlImg, titleImg, scoreBgImg, qrCodeImg] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -738,8 +755,8 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
|||||||
|
|
||||||
ctx.drawImage(bgImg, 0, 0, width, height);
|
ctx.drawImage(bgImg, 0, 0, width, height);
|
||||||
|
|
||||||
drawRoundImage(ctx, avatarImg, 17, 20, 32, 32, 20);
|
if (avatarImg) drawRoundImage(ctx, avatarImg, 17, 20, 32, 32, 20);
|
||||||
ctx.drawImage(lvlImg, 12, 15, 42, 42);
|
if (lvlImg) ctx.drawImage(lvlImg, 12, 15, 42, 42);
|
||||||
|
|
||||||
renderText(ctx, user.nickName, 13, "#fff", 58, 34);
|
renderText(ctx, user.nickName, 13, "#fff", 58, 34);
|
||||||
renderRankTitle(ctx, user.lvlName);
|
renderRankTitle(ctx, user.lvlName);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="container" :style="{ paddingTop: showHeader ? '5px' : '0' }">
|
<view class="container">
|
||||||
<image
|
<image
|
||||||
v-if="showHeader"
|
v-if="showHeader"
|
||||||
:src="`https://static.shelingxingqiu.com/shootmini/static/battle-header${players.length ? '-melee' : ''}.png`"
|
:src="`https://static.shelingxingqiu.com/shootmini/static/battle-header${players.length ? '-melee' : ''}.png`"
|
||||||
@@ -47,7 +47,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
|||||||
<view
|
<view
|
||||||
v-if="!players.length && blueTeam.length && redTeam.length"
|
v-if="!players.length && blueTeam.length && redTeam.length"
|
||||||
class="players"
|
class="players"
|
||||||
:style="{ paddingTop: showHeader ? '15px' : '0' }"
|
:style="{ paddingTop: showHeader ? '48rpx' : '0' }"
|
||||||
>
|
>
|
||||||
<view>
|
<view>
|
||||||
<view
|
<view
|
||||||
@@ -158,7 +158,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
|||||||
.container > image:first-child {
|
.container > image:first-child {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: -5px;
|
/* top: -5px; */
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import PointSwitcher from "@/components/PointSwitcher.vue";
|
|||||||
import BowShotEffect from "@/components/BowShotEffect.vue";
|
import BowShotEffect from "@/components/BowShotEffect.vue";
|
||||||
|
|
||||||
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
||||||
import { simulShootAPI } from "@/apis";
|
import { simulShootAPI, laserAimAPI, laserCloseAPI } from "@/apis";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -357,6 +357,14 @@ const simulShoot2 = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openAim = async () => {
|
||||||
|
await laserAimAPI();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAim = async () => {
|
||||||
|
await laserCloseAPI();
|
||||||
|
};
|
||||||
|
|
||||||
const env = computed(() => {
|
const env = computed(() => {
|
||||||
const accountInfo = uni.getAccountInfoSync();
|
const accountInfo = uni.getAccountInfoSync();
|
||||||
return accountInfo.miniProgram.envVersion;
|
return accountInfo.miniProgram.envVersion;
|
||||||
@@ -524,6 +532,8 @@ onBeforeUnmount(() => {
|
|||||||
<view class="simul" v-if="env !== 'release'">
|
<view class="simul" v-if="env !== 'release'">
|
||||||
<button @click="simulShoot">模拟</button>
|
<button @click="simulShoot">模拟</button>
|
||||||
<button @click="simulShoot2">射箭</button>
|
<button @click="simulShoot2">射箭</button>
|
||||||
|
<button @click="openAim">开瞄</button>
|
||||||
|
<button @click="closeAim">关瞄</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+17
-1
@@ -849,6 +849,7 @@ export function connectMatchWebSocket(options = {}) {
|
|||||||
token,
|
token,
|
||||||
mode,
|
mode,
|
||||||
requestPracticeInfoOnOpen = false,
|
requestPracticeInfoOnOpen = false,
|
||||||
|
appHideResumable = false,
|
||||||
force = false,
|
force = false,
|
||||||
reconnecting = false,
|
reconnecting = false,
|
||||||
reconnectReason = "",
|
reconnectReason = "",
|
||||||
@@ -920,6 +921,7 @@ export function connectMatchWebSocket(options = {}) {
|
|||||||
isMelee:
|
isMelee:
|
||||||
Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined,
|
Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined,
|
||||||
requestPracticeInfoOnOpen: requestPracticeInfoOnOpen === true,
|
requestPracticeInfoOnOpen: requestPracticeInfoOnOpen === true,
|
||||||
|
appHideResumable: appHideResumable === true,
|
||||||
meleeHalfRest: isSameContext
|
meleeHalfRest: isSameContext
|
||||||
? currentContext?.meleeHalfRest === true
|
? currentContext?.meleeHalfRest === true
|
||||||
: false,
|
: false,
|
||||||
@@ -1016,9 +1018,22 @@ export function connectMatchWebSocketFromNotice(notice, fallbackUserId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setMatchAppHideResumable(enabled) {
|
||||||
|
if (!currentContext) return;
|
||||||
|
currentContext.appHideResumable = enabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
export function closeMatchWebSocket(options = {}) {
|
export function closeMatchWebSocket(options = {}) {
|
||||||
// 默认关闭时会发送 LEAVE;内部切换连接可通过 sendLeave:false 跳过。
|
// 默认关闭时会发送 LEAVE;内部切换连接可通过 sendLeave:false 跳过。
|
||||||
const { sendLeave: shouldSendLeave = true, reason = "manual" } = options;
|
const { reason = "manual" } = options;
|
||||||
|
// 可恢复训练切后台只断开传输层,不能把它上报成主动离场。
|
||||||
|
const shouldSendLeave =
|
||||||
|
options.sendLeave === undefined
|
||||||
|
? !(
|
||||||
|
reason === "app-hide" &&
|
||||||
|
currentContext?.appHideResumable === true
|
||||||
|
)
|
||||||
|
: options.sendLeave === true;
|
||||||
|
|
||||||
manualClose = true;
|
manualClose = true;
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
@@ -1051,6 +1066,7 @@ export default {
|
|||||||
ServerMessageType,
|
ServerMessageType,
|
||||||
connectMatchWebSocket,
|
connectMatchWebSocket,
|
||||||
connectMatchWebSocketFromNotice,
|
connectMatchWebSocketFromNotice,
|
||||||
|
setMatchAppHideResumable,
|
||||||
closeMatchWebSocket,
|
closeMatchWebSocket,
|
||||||
forceReconnectMatchWebSocket,
|
forceReconnectMatchWebSocket,
|
||||||
handleMatchNetworkStatusChange,
|
handleMatchNetworkStatusChange,
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ const data = ref({
|
|||||||
rounds: [],
|
rounds: [],
|
||||||
});
|
});
|
||||||
const players = ref([]);
|
const players = ref([]);
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const loadError = ref("");
|
||||||
|
|
||||||
onLoad(async (options) => {
|
const loadBattle = async () => {
|
||||||
if (!options.battleId) return;
|
|
||||||
battleId.value = options.battleId || "60510101693403136";
|
|
||||||
const result = await getBattleAPI(battleId.value);
|
const result = await getBattleAPI(battleId.value);
|
||||||
data.value = result;
|
data.value = result;
|
||||||
if (result.mode > 3) {
|
if (result.mode > 3) {
|
||||||
@@ -62,6 +62,32 @@ onLoad(async (options) => {
|
|||||||
|
|
||||||
players.value = [...rankedPlayers, ...unrankedPlayers];
|
players.value = [...rankedPlayers, ...unrankedPlayers];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据和派生列表均已就绪后再关闭占位,避免空壳闪烁。
|
||||||
|
isLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestBattle = () => {
|
||||||
|
if (!battleId.value) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
loadError.value = "";
|
||||||
|
loadBattle().catch((error) => {
|
||||||
|
console.error("加载比赛详情失败:", error);
|
||||||
|
loadError.value = "赛况加载失败";
|
||||||
|
isLoading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
if (!options.battleId) {
|
||||||
|
loadError.value = "缺少比赛信息";
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
battleId.value = options.battleId;
|
||||||
|
requestBattle();
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkBowData = (selected) => {
|
const checkBowData = (selected) => {
|
||||||
@@ -80,6 +106,17 @@ const checkBowData = (selected) => {
|
|||||||
<template>
|
<template>
|
||||||
<Container title="详情">
|
<Container title="详情">
|
||||||
<view class="container">
|
<view class="container">
|
||||||
|
<view v-if="isLoading" class="page-state">
|
||||||
|
<text>赛况加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-else-if="loadError"
|
||||||
|
class="page-state page-state--error"
|
||||||
|
@click="requestBattle"
|
||||||
|
>
|
||||||
|
<text>{{ loadError }}</text>
|
||||||
|
<text v-if="battleId" class="retry-text">点击重新加载</text>
|
||||||
|
</view>
|
||||||
<BattleHeader
|
<BattleHeader
|
||||||
v-if="data.mode <= 3"
|
v-if="data.mode <= 3"
|
||||||
:winner="data.winTeam"
|
:winner="data.winTeam"
|
||||||
@@ -165,6 +202,22 @@ const checkBowData = (selected) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.page-state {
|
||||||
|
min-height: 60vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
.page-state--error {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
.retry-text {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
color: #fed847;
|
||||||
|
}
|
||||||
.score-header,
|
.score-header,
|
||||||
.score-row {
|
.score-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+254
-8
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import { ref, nextTick, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import ShootProgress from "@/components/ShootProgress.vue";
|
import ShootProgress from "@/components/ShootProgress.vue";
|
||||||
import BowTarget from "@/components/BowTarget.vue";
|
import BowTarget from "@/components/BowTarget.vue";
|
||||||
@@ -16,10 +16,17 @@ import audioManager from "@/audioManager";
|
|||||||
import {
|
import {
|
||||||
createPractiseAPI,
|
createPractiseAPI,
|
||||||
endPractiseAPI,
|
endPractiseAPI,
|
||||||
|
getCurrentPractiseAPI,
|
||||||
getPractiseAPI,
|
getPractiseAPI,
|
||||||
startPractiseAPI,
|
startPractiseAPI,
|
||||||
} from "@/apis";
|
} 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 { sharePractiseData } from "@/canvas";
|
||||||
import { wxShare, debounce } from "@/util";
|
import { wxShare, debounce } from "@/util";
|
||||||
import { MESSAGETYPESV2, roundsName } from "@/constants";
|
import { MESSAGETYPESV2, roundsName } from "@/constants";
|
||||||
@@ -35,11 +42,21 @@ const isSvip = ref(false);
|
|||||||
const total = 12;
|
const total = 12;
|
||||||
const practiseResult = ref({});
|
const practiseResult = ref({});
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
|
const serverAddr = ref("");
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const tips = ref("");
|
const tips = ref("");
|
||||||
const targetType = ref(1);
|
const targetType = ref(1);
|
||||||
const sharing = ref(false);
|
const sharing = ref(false);
|
||||||
const exiting = 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";
|
const RESULT_TIP_CDN = "https://static.shelingxingqiu.com/shootmini/static";
|
||||||
|
|
||||||
onLoad((options) => {
|
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 () => {
|
const createPractise = async () => {
|
||||||
closeMatchWebSocket({ reason: "practice-recreate" });
|
closeMatchWebSocket({ reason: "practice-recreate" });
|
||||||
const result = await createPractiseAPI(
|
const result = await createPractiseAPI(
|
||||||
@@ -64,11 +138,8 @@ const createPractise = async () => {
|
|||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
connectMatchWebSocket({
|
serverAddr.value = result.serverAddr;
|
||||||
serverAddr: result.serverAddr,
|
connectPracticeServer();
|
||||||
matchId: result.id,
|
|
||||||
userId: user.value.id,
|
|
||||||
});
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,6 +165,10 @@ const onReady = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onOver = async (message) => {
|
const onOver = async (message) => {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
hiddenWhileActive.value = false;
|
||||||
|
foregroundResumeSyncPending.value = false;
|
||||||
|
cancelPracticeSyncWait();
|
||||||
practiseResult.value = Array.isArray(message?.details)
|
practiseResult.value = Array.isArray(message?.details)
|
||||||
? message
|
? message
|
||||||
: await getPractiseAPI(practiseId.value);
|
: await getPractiseAPI(practiseId.value);
|
||||||
@@ -105,11 +180,16 @@ async function onReceiveMessage(msg) {
|
|||||||
isSvip.value = msg.sVip === true;
|
isSvip.value = msg.sVip === true;
|
||||||
scores.value = Array.isArray(msg.details) ? msg.details : scores.value;
|
scores.value = Array.isArray(msg.details) ? msg.details : scores.value;
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
hiddenWhileActive.value = false;
|
||||||
|
foregroundResumeSyncPending.value = false;
|
||||||
|
cancelPracticeSyncWait();
|
||||||
setTimeout(() => onOver(msg), 1500);
|
setTimeout(() => onOver(msg), 1500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onComplete() {
|
async function onComplete() {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
const validArrows = (practiseResult.value.details || []).filter(
|
const validArrows = (practiseResult.value.details || []).filter(
|
||||||
(a) => a.x !== -30 && a.y !== -30
|
(a) => a.x !== -30 && a.y !== -30
|
||||||
);
|
);
|
||||||
@@ -117,6 +197,7 @@ async function onComplete() {
|
|||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
} else {
|
} else {
|
||||||
practiseId.value = "";
|
practiseId.value = "";
|
||||||
|
serverAddr.value = "";
|
||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
@@ -128,6 +209,10 @@ async function onComplete() {
|
|||||||
async function exitPractise() {
|
async function exitPractise() {
|
||||||
if (exiting.value) return;
|
if (exiting.value) return;
|
||||||
exiting.value = true;
|
exiting.value = true;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
hiddenWhileActive.value = false;
|
||||||
|
foregroundResumeSyncPending.value = false;
|
||||||
|
cancelPracticeSyncWait();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (practiseId.value && !practiseResult.value?.details) {
|
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 getResultTipSrc = (result = {}) => {
|
||||||
const validCount = (result.details || []).filter(
|
const validCount = (result.details || []).filter(
|
||||||
(arrow) => arrow.x !== -30 && arrow.y !== -30
|
(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 () => {
|
onMounted(async () => {
|
||||||
void audioManager.warmAll();
|
void audioManager.warmAll();
|
||||||
// audioManager.play("第一轮");
|
// audioManager.play("第一轮");
|
||||||
@@ -170,15 +409,21 @@ onMounted(async () => {
|
|||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
uni.$on("socket-inbox", onReceiveMessage);
|
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);
|
uni.$on("share-image", onClickShare);
|
||||||
await createPractise();
|
await createPractise();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
cancelPracticeSyncWait();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
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);
|
uni.$off("share-image", onClickShare);
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
closeMatchWebSocket({ reason: "practice-leave" });
|
closeMatchWebSocket({ reason: "practice-leave" });
|
||||||
@@ -215,6 +460,7 @@ onBeforeUnmount(() => {
|
|||||||
<BowPower />
|
<BowPower />
|
||||||
</view>
|
</view>
|
||||||
<BowTarget
|
<BowTarget
|
||||||
|
v-if="!restoringSnapshot"
|
||||||
:totalRound="start ? total / 4 : 0"
|
:totalRound="start ? total / 4 : 0"
|
||||||
:currentRound="scores.length % 3"
|
:currentRound="scores.length % 3"
|
||||||
:scores="scores"
|
:scores="scores"
|
||||||
|
|||||||
+254
-8
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import { ref, nextTick, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import ShootProgress from "@/components/ShootProgress.vue";
|
import ShootProgress from "@/components/ShootProgress.vue";
|
||||||
import BowTarget from "@/components/BowTarget.vue";
|
import BowTarget from "@/components/BowTarget.vue";
|
||||||
@@ -15,17 +16,23 @@ import audioManager from "@/audioManager";
|
|||||||
import {
|
import {
|
||||||
createPractiseAPI,
|
createPractiseAPI,
|
||||||
endPractiseAPI,
|
endPractiseAPI,
|
||||||
|
getCurrentPractiseAPI,
|
||||||
getPractiseAPI,
|
getPractiseAPI,
|
||||||
startPractiseAPI,
|
startPractiseAPI,
|
||||||
} from "@/apis";
|
} 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 { sharePractiseData } from "@/canvas";
|
||||||
import { wxShare, debounce } from "@/util";
|
import { wxShare, debounce } from "@/util";
|
||||||
import { MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPESV2 } from "@/constants";
|
||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import {onLoad} from "@dcloudio/uni-app";
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { user, device } = storeToRefs(store);
|
const { user, device } = storeToRefs(store);
|
||||||
|
|
||||||
@@ -35,10 +42,20 @@ const isSvip = ref(false);
|
|||||||
const total = 36;
|
const total = 36;
|
||||||
const practiseResult = ref({});
|
const practiseResult = ref({});
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
|
const serverAddr = ref("");
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const targetType = ref(1);
|
const targetType = ref(1);
|
||||||
const sharing = ref(false);
|
const sharing = ref(false);
|
||||||
const exiting = 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";
|
const RESULT_TIP_CDN = "https://static.shelingxingqiu.com/shootmini/static";
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
@@ -47,6 +64,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 () => {
|
const createPractise = async () => {
|
||||||
closeMatchWebSocket({ reason: "practice-recreate" });
|
closeMatchWebSocket({ reason: "practice-recreate" });
|
||||||
const result = await createPractiseAPI(
|
const result = await createPractiseAPI(
|
||||||
@@ -63,11 +137,8 @@ const createPractise = async () => {
|
|||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
connectMatchWebSocket({
|
serverAddr.value = result.serverAddr;
|
||||||
serverAddr: result.serverAddr,
|
connectPracticeServer();
|
||||||
matchId: result.id,
|
|
||||||
userId: user.value.id,
|
|
||||||
});
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,6 +164,10 @@ const onReady = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onOver = async (message) => {
|
const onOver = async (message) => {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
hiddenWhileActive.value = false;
|
||||||
|
foregroundResumeSyncPending.value = false;
|
||||||
|
cancelPracticeSyncWait();
|
||||||
practiseResult.value = Array.isArray(message?.details)
|
practiseResult.value = Array.isArray(message?.details)
|
||||||
? message
|
? message
|
||||||
: await getPractiseAPI(practiseId.value);
|
: await getPractiseAPI(practiseId.value);
|
||||||
@@ -104,6 +179,10 @@ async function onReceiveMessage(msg) {
|
|||||||
isSvip.value = msg.sVip === true;
|
isSvip.value = msg.sVip === true;
|
||||||
scores.value = Array.isArray(msg.details) ? msg.details : scores.value;
|
scores.value = Array.isArray(msg.details) ? msg.details : scores.value;
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
hiddenWhileActive.value = false;
|
||||||
|
foregroundResumeSyncPending.value = false;
|
||||||
|
cancelPracticeSyncWait();
|
||||||
setTimeout(() => onOver(msg), 1500);
|
setTimeout(() => onOver(msg), 1500);
|
||||||
}
|
}
|
||||||
// messages.forEach((msg) => {
|
// messages.forEach((msg) => {
|
||||||
@@ -125,6 +204,7 @@ async function onReceiveMessage(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onComplete() {
|
async function onComplete() {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
const validArrows = (practiseResult.value.details || []).filter(
|
const validArrows = (practiseResult.value.details || []).filter(
|
||||||
(a) => a.x !== -30 && a.y !== -30
|
(a) => a.x !== -30 && a.y !== -30
|
||||||
);
|
);
|
||||||
@@ -132,6 +212,7 @@ async function onComplete() {
|
|||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
} else {
|
} else {
|
||||||
practiseId.value = "";
|
practiseId.value = "";
|
||||||
|
serverAddr.value = "";
|
||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
@@ -143,6 +224,10 @@ async function onComplete() {
|
|||||||
async function exitPractise() {
|
async function exitPractise() {
|
||||||
if (exiting.value) return;
|
if (exiting.value) return;
|
||||||
exiting.value = true;
|
exiting.value = true;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
hiddenWhileActive.value = false;
|
||||||
|
foregroundResumeSyncPending.value = false;
|
||||||
|
cancelPracticeSyncWait();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (practiseId.value && !practiseResult.value?.details) {
|
if (practiseId.value && !practiseResult.value?.details) {
|
||||||
@@ -155,6 +240,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 getResultTipSrc = (result = {}) => {
|
||||||
const validCount = (result.details || []).filter(
|
const validCount = (result.details || []).filter(
|
||||||
(arrow) => arrow.x !== -30 && arrow.y !== -30
|
(arrow) => arrow.x !== -30 && arrow.y !== -30
|
||||||
@@ -178,21 +398,46 @@ 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 () => {
|
onMounted(async () => {
|
||||||
void audioManager.warmAll();
|
void audioManager.warmAll();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
uni.$on("socket-inbox", onReceiveMessage);
|
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);
|
uni.$on("share-image", onClickShare);
|
||||||
await createPractise();
|
await createPractise();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
cancelPracticeSyncWait();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
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);
|
uni.$off("share-image", onClickShare);
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
closeMatchWebSocket({ reason: "practice-leave" });
|
closeMatchWebSocket({ reason: "practice-leave" });
|
||||||
@@ -224,6 +469,7 @@ onBeforeUnmount(() => {
|
|||||||
<BowPower />
|
<BowPower />
|
||||||
</view>
|
</view>
|
||||||
<BowTarget
|
<BowTarget
|
||||||
|
v-if="!restoringSnapshot"
|
||||||
:currentRound="scores.length"
|
:currentRound="scores.length"
|
||||||
:totalRound="start ? total : 0"
|
:totalRound="start ? total : 0"
|
||||||
:scores="scores"
|
:scores="scores"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import PointSwitcher from "./PointSwitcher.vue";
|
|||||||
import BowShotEffect from "@/components/BowShotEffect.vue";
|
import BowShotEffect from "@/components/BowShotEffect.vue";
|
||||||
|
|
||||||
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
||||||
import { simulShootAPI } from "@/apis";
|
import { simulShootAPI, laserAimAPI, laserCloseAPI } from "@/apis";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -363,6 +363,14 @@ const simulShoot2 = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openAim = async () => {
|
||||||
|
await laserAimAPI();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAim = async () => {
|
||||||
|
await laserCloseAPI();
|
||||||
|
};
|
||||||
|
|
||||||
const env = computed(() => {
|
const env = computed(() => {
|
||||||
const accountInfo = uni.getAccountInfoSync();
|
const accountInfo = uni.getAccountInfoSync();
|
||||||
return accountInfo.miniProgram.envVersion;
|
return accountInfo.miniProgram.envVersion;
|
||||||
@@ -542,6 +550,8 @@ onBeforeUnmount(() => {
|
|||||||
<view class="simul" v-if="env !== 'release'">
|
<view class="simul" v-if="env !== 'release'">
|
||||||
<button @click="simulShoot">模拟</button>
|
<button @click="simulShoot">模拟</button>
|
||||||
<button @click="simulShoot2">射箭</button>
|
<button @click="simulShoot2">射箭</button>
|
||||||
|
<button @click="openAim">开瞄</button>
|
||||||
|
<button @click="closeAim">关瞄</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import PointSwitcher from "@/components/PointSwitcher.vue";
|
|||||||
import TargetCanvas from "@/components/TargetCanvas.vue";
|
import TargetCanvas from "@/components/TargetCanvas.vue";
|
||||||
|
|
||||||
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
||||||
import { simulShootAPI } from "@/apis";
|
import { simulShootAPI, laserAimAPI, laserCloseAPI } from "@/apis";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -372,6 +372,14 @@ const simulShoot2 = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openAim = async () => {
|
||||||
|
await laserAimAPI();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAim = async () => {
|
||||||
|
await laserCloseAPI();
|
||||||
|
};
|
||||||
|
|
||||||
const env = computed(() => {
|
const env = computed(() => {
|
||||||
const accountInfo = uni.getAccountInfoSync();
|
const accountInfo = uni.getAccountInfoSync();
|
||||||
return accountInfo.miniProgram.envVersion;
|
return accountInfo.miniProgram.envVersion;
|
||||||
@@ -557,6 +565,8 @@ onBeforeUnmount(() => {
|
|||||||
<view class="simul" v-if="env !== 'release'">
|
<view class="simul" v-if="env !== 'release'">
|
||||||
<button @click="simulShoot">模拟</button>
|
<button @click="simulShoot">模拟</button>
|
||||||
<button @click="simulShoot2">射箭</button>
|
<button @click="simulShoot2">射箭</button>
|
||||||
|
<button @click="openAim">开瞄</button>
|
||||||
|
<button @click="closeAim">关瞄</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import audioManager from "@/audioManager";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
createPractiseV2API,
|
createPractiseV2API,
|
||||||
|
getCurrentPractiseAPI,
|
||||||
startPractiseAPI,
|
startPractiseAPI,
|
||||||
endPractiseAPI,
|
endPractiseAPI,
|
||||||
getPractiseAPI,
|
getPractiseAPI,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
connectMatchWebSocket,
|
connectMatchWebSocket,
|
||||||
closeMatchWebSocket,
|
closeMatchWebSocket,
|
||||||
|
setMatchAppHideResumable,
|
||||||
MATCH_WS_PRACTICE_SYNC_EVENT,
|
MATCH_WS_PRACTICE_SYNC_EVENT,
|
||||||
MATCH_WS_STATE_EVENT,
|
MATCH_WS_STATE_EVENT,
|
||||||
} from "@/matchWebsocket";
|
} from "@/matchWebsocket";
|
||||||
@@ -76,6 +78,10 @@ const stopCompleted = ref(false);
|
|||||||
const stopInFlight = ref(false);
|
const stopInFlight = ref(false);
|
||||||
const exiting = ref(false);
|
const exiting = ref(false);
|
||||||
const hiddenWhileActive = 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);
|
const connectionClosed = ref(true);
|
||||||
let stopPracticeTask = null;
|
let stopPracticeTask = null;
|
||||||
let practiceSyncTimer = null;
|
let practiceSyncTimer = null;
|
||||||
@@ -108,6 +114,12 @@ const isVip = computed(
|
|||||||
() => practiceInfo.value.vip === true && !isSvip.value
|
() => practiceInfo.value.vip === true && !isSvip.value
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setPracticeAppHideResumable = (enabled) => {
|
||||||
|
const nextValue = enabled === true;
|
||||||
|
appHideResumable.value = nextValue;
|
||||||
|
setMatchAppHideResumable(nextValue);
|
||||||
|
};
|
||||||
|
|
||||||
const trainingType = computed(
|
const trainingType = computed(
|
||||||
() => practiceInfo.value.trainingType || trainingParams.value.type || ""
|
() => practiceInfo.value.trainingType || trainingParams.value.type || ""
|
||||||
);
|
);
|
||||||
@@ -561,6 +573,19 @@ const startPracticeSyncTimer = () => {
|
|||||||
practiceSyncTimer = null;
|
practiceSyncTimer = null;
|
||||||
if (!waitingPracticeSync) return;
|
if (!waitingPracticeSync) return;
|
||||||
waitingPracticeSync = false;
|
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;
|
if (pageStage.value !== pageStages.LOADING) return;
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -597,7 +622,9 @@ const onPracticeInfoSync = (payload = {}) => {
|
|||||||
|
|
||||||
const shouldShowDistance =
|
const shouldShowDistance =
|
||||||
waitingPracticeSync && pageStage.value === pageStages.LOADING;
|
waitingPracticeSync && pageStage.value === pageStages.LOADING;
|
||||||
|
const resumedFromForeground = foregroundResumeSyncPending.value;
|
||||||
cancelPracticeSyncWait();
|
cancelPracticeSyncWait();
|
||||||
|
foregroundResumeSyncPending.value = false;
|
||||||
invalidateShotPresentations();
|
invalidateShotPresentations();
|
||||||
|
|
||||||
// 14 是完整快照,先清空旧值,避免 proto3 省略的 0 沿用上一份状态。
|
// 14 是完整快照,先清空旧值,避免 proto3 省略的 0 沿用上一份状态。
|
||||||
@@ -607,6 +634,10 @@ const onPracticeInfoSync = (payload = {}) => {
|
|||||||
applyVisiblePrecisionTarget(practiceInfo.value);
|
applyVisiblePrecisionTarget(practiceInfo.value);
|
||||||
scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];
|
scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];
|
||||||
|
|
||||||
|
if (resumedFromForeground) {
|
||||||
|
hiddenWhileActive.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldShowDistance) {
|
if (shouldShowDistance) {
|
||||||
start.value = false;
|
start.value = false;
|
||||||
pageStage.value = pageStages.DISTANCE;
|
pageStage.value = pageStages.DISTANCE;
|
||||||
@@ -650,18 +681,21 @@ const clearPracticeRuntimeContext = () => {
|
|||||||
uni.setStorageSync(trainingDifficultyStorageKey, selectionContext);
|
uni.setStorageSync(trainingDifficultyStorageKey, selectionContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closePracticeConnection = (reason) => {
|
const closePracticeConnection = (reason, { sendLeave = true } = {}) => {
|
||||||
cancelPracticeSyncWait();
|
cancelPracticeSyncWait();
|
||||||
if (connectionClosed.value) return;
|
if (connectionClosed.value) return;
|
||||||
connectionClosed.value = true;
|
connectionClosed.value = true;
|
||||||
closeMatchWebSocket({ reason });
|
closeMatchWebSocket({ reason, sendLeave });
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectPracticeServer = () => {
|
const connectPracticeServer = ({
|
||||||
|
resumableOnAppHide = appHideResumable.value,
|
||||||
|
} = {}) => {
|
||||||
if (!practiseId.value || !String(serverAddr.value || "").trim()) {
|
if (!practiseId.value || !String(serverAddr.value || "").trim()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appHideResumable.value = resumableOnAppHide === true;
|
||||||
cancelPracticeSyncWait();
|
cancelPracticeSyncWait();
|
||||||
closeMatchWebSocket({ reason: "training-practice-switch" });
|
closeMatchWebSocket({ reason: "training-practice-switch" });
|
||||||
preparePracticeSyncWait();
|
preparePracticeSyncWait();
|
||||||
@@ -670,6 +704,7 @@ const connectPracticeServer = () => {
|
|||||||
matchId: practiseId.value,
|
matchId: practiseId.value,
|
||||||
userId: user.value.id,
|
userId: user.value.id,
|
||||||
requestPracticeInfoOnOpen: true,
|
requestPracticeInfoOnOpen: true,
|
||||||
|
appHideResumable: appHideResumable.value,
|
||||||
});
|
});
|
||||||
connectionClosed.value = false;
|
connectionClosed.value = false;
|
||||||
return true;
|
return true;
|
||||||
@@ -703,6 +738,97 @@ const stopCurrentPractice = () => {
|
|||||||
return stopPracticeTask;
|
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 createPractice = async () => {
|
||||||
const trainingType = trainingParams.value.type;
|
const trainingType = trainingParams.value.type;
|
||||||
const difficultyLevel = trainingParams.value.difficulty;
|
const difficultyLevel = trainingParams.value.difficulty;
|
||||||
@@ -740,7 +866,7 @@ const createPractice = async () => {
|
|||||||
stopInFlight.value = false;
|
stopInFlight.value = false;
|
||||||
stopPracticeTask = null;
|
stopPracticeTask = null;
|
||||||
updateTrainingContext(result);
|
updateTrainingContext(result);
|
||||||
connectPracticeServer();
|
connectPracticeServer({ resumableOnAppHide: true });
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -839,10 +965,12 @@ const onReady = async () => {
|
|||||||
shotEffectToken.value = 0;
|
shotEffectToken.value = 0;
|
||||||
start.value = true;
|
start.value = true;
|
||||||
pageStage.value = pageStages.SHOOTING;
|
pageStage.value = pageStages.SHOOTING;
|
||||||
|
setPracticeAppHideResumable(true);
|
||||||
audioManager.play("练习开始");
|
audioManager.play("练习开始");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
start.value = false;
|
start.value = false;
|
||||||
pageStage.value = pageStages.DISTANCE;
|
pageStage.value = pageStages.DISTANCE;
|
||||||
|
setPracticeAppHideResumable(true);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -851,6 +979,8 @@ const onTimeLimitReached = () => {
|
|||||||
if (!hasTimeLimit.value || !isShootingStage.value) return;
|
if (!hasTimeLimit.value || !isShootingStage.value) return;
|
||||||
// 本地倒计时只负责停止射击展示,最终结算以 PRACTICE_END 为准。
|
// 本地倒计时只负责停止射击展示,最终结算以 PRACTICE_END 为准。
|
||||||
start.value = false;
|
start.value = false;
|
||||||
|
hiddenWhileActive.value = false;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const enterPracticeResult = (result = {}) => {
|
const enterPracticeResult = (result = {}) => {
|
||||||
@@ -859,6 +989,7 @@ const enterPracticeResult = (result = {}) => {
|
|||||||
|
|
||||||
// 正常结算不调用 stop,只清理上下文并断开比赛服连接。
|
// 正常结算不调用 stop,只清理上下文并断开比赛服连接。
|
||||||
practiceEnded.value = true;
|
practiceEnded.value = true;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
invalidateShotPresentations();
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
closePracticeConnection("training-practice-result");
|
closePracticeConnection("training-practice-result");
|
||||||
@@ -872,6 +1003,7 @@ const onOver = async () => {
|
|||||||
clearHighlightTestTimer();
|
clearHighlightTestTimer();
|
||||||
pageStage.value = pageStages.LOADING;
|
pageStage.value = pageStages.LOADING;
|
||||||
start.value = false;
|
start.value = false;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiResult = (await getPractiseAPI(practiseId.value)) || {};
|
const apiResult = (await getPractiseAPI(practiseId.value)) || {};
|
||||||
@@ -885,6 +1017,7 @@ const onOver = async () => {
|
|||||||
}
|
}
|
||||||
start.value = true;
|
start.value = true;
|
||||||
pageStage.value = pageStages.SHOOTING;
|
pageStage.value = pageStages.SHOOTING;
|
||||||
|
setPracticeAppHideResumable(true);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -945,6 +1078,7 @@ async function onReceiveMessage(msg) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
practiceEnded.value = true;
|
practiceEnded.value = true;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
invalidateShotPresentations();
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
// setTimeout(onOver, 1500);
|
// setTimeout(onOver, 1500);
|
||||||
@@ -955,6 +1089,7 @@ function onComplete() {
|
|||||||
pageStage.value = pageStages.LOADING;
|
pageStage.value = pageStages.LOADING;
|
||||||
start.value = false;
|
start.value = false;
|
||||||
practiceEnded.value = true;
|
practiceEnded.value = true;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
invalidateShotPresentations();
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
closePracticeConnection("training-practice-complete");
|
closePracticeConnection("training-practice-complete");
|
||||||
@@ -964,6 +1099,7 @@ function onComplete() {
|
|||||||
|
|
||||||
async function onRetry() {
|
async function onRetry() {
|
||||||
pageStage.value = pageStages.LOADING;
|
pageStage.value = pageStages.LOADING;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
clearHighlightTestTimer();
|
clearHighlightTestTimer();
|
||||||
useHighlightTest.value = false;
|
useHighlightTest.value = false;
|
||||||
practiseId.value = "";
|
practiseId.value = "";
|
||||||
@@ -1006,6 +1142,7 @@ const updateSound = () => {
|
|||||||
const exitPractice = async () => {
|
const exitPractice = async () => {
|
||||||
if (exiting.value) return;
|
if (exiting.value) return;
|
||||||
exiting.value = true;
|
exiting.value = true;
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
invalidateShotPresentations();
|
invalidateShotPresentations();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1018,36 +1155,28 @@ const exitPractice = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onHide(() => {
|
onHide(() => {
|
||||||
invalidateShotPresentations();
|
pageVisible.value = false;
|
||||||
// 小程序被切到后台时尽早通知后端,作为杀进程前的尽力兜底。
|
|
||||||
if (
|
if (
|
||||||
!exiting.value &&
|
!appHideResumable.value ||
|
||||||
practiseId.value &&
|
exiting.value ||
|
||||||
!practiceEnded.value &&
|
!practiseId.value ||
|
||||||
!stopCompleted.value
|
practiceEnded.value ||
|
||||||
|
stopCompleted.value
|
||||||
) {
|
) {
|
||||||
hiddenWhileActive.value = true;
|
return;
|
||||||
clearPracticeRuntimeContext();
|
|
||||||
void stopCurrentPractice();
|
|
||||||
}
|
}
|
||||||
closePracticeConnection("training-practice-hide");
|
|
||||||
|
hiddenWhileActive.value = true;
|
||||||
|
invalidateShotPresentations();
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
if (!hiddenWhileActive.value || exiting.value) return;
|
pageVisible.value = true;
|
||||||
|
await resumeCurrentPractice();
|
||||||
hiddenWhileActive.value = false;
|
|
||||||
await stopCurrentPractice();
|
|
||||||
clearPracticeRuntimeContext();
|
|
||||||
exiting.value = true;
|
|
||||||
uni.showToast({
|
|
||||||
title: "训练已结束,请重新进入",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
uni.navigateBack();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
invalidateShotPresentations();
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
void stopCurrentPractice();
|
void stopCurrentPractice();
|
||||||
@@ -1064,7 +1193,7 @@ onMounted(() => {
|
|||||||
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
|
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
|
||||||
uni.$on("share-image", onClickShare);
|
uni.$on("share-image", onClickShare);
|
||||||
uni.$on("audioEnded", onAudioEnded);
|
uni.$on("audioEnded", onAudioEnded);
|
||||||
if (!connectPracticeServer()) {
|
if (!connectPracticeServer({ resumableOnAppHide: true })) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "练习连接信息异常,请重试",
|
title: "练习连接信息异常,请重试",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
@@ -1076,6 +1205,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
setPracticeAppHideResumable(false);
|
||||||
invalidateShotPresentations();
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
void stopCurrentPractice();
|
void stopCurrentPractice();
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ function createWebSocket(token, onMessage) {
|
|||||||
|
|
||||||
switch (envVersion) {
|
switch (envVersion) {
|
||||||
case "develop": // 开发版
|
case "develop": // 开发版
|
||||||
// url = "ws://192.168.1.5:8000/socket";
|
// url = "ws://192.168.1.2:8000/socket";
|
||||||
url = "wss://apitest.shelingxingqiu.com/socket";
|
url = "wss://apitest.shelingxingqiu.com/socket";
|
||||||
break;
|
break;
|
||||||
case "trial": // 体验版
|
case "trial": // 体验版
|
||||||
|
|||||||
Reference in New Issue
Block a user