Files
shoot-miniprograms/src/pages/practise-one.vue
T

489 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
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";
import ScorePanel2 from "@/components/ScorePanel2.vue";
import ScoreResult from "@/components/ScoreResult.vue";
import SButton from "@/components/SButton.vue";
import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue";
import TestDistance from "@/components/TestDistance.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import audioManager from "@/audioManager";
import {
createPractiseAPI,
endPractiseAPI,
getCurrentPractiseAPI,
getPractiseAPI,
startPractiseAPI,
} from "@/apis";
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";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, device } = storeToRefs(store);
const start = ref(false);
const scores = ref([]);
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) => {
if (options.target) {
targetType.value = Number(options.target);
}
});
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(
total,
120,
targetType.value,
device.value.deviceId
);
if (result?.id) practiseId.value = result.id;
if (!result?.serverAddr) {
uni.showToast({
title: "练习连接信息异常,请重试",
icon: "none",
});
return null;
}
serverAddr.value = result.serverAddr;
connectPracticeServer();
return result;
};
const beginPractise = async () => {
if (!practiseId.value) {
uni.showToast({
title: "练习未创建,请重试",
icon: "none",
});
return false;
}
await startPractiseAPI(practiseId.value);
return true;
};
const onReady = async () => {
const result = await beginPractise();
if (!result) return;
scores.value = [];
isSvip.value = false;
start.value = true;
audioManager.play("练习开始");
};
const onOver = async (message) => {
setPracticeAppHideResumable(false);
hiddenWhileActive.value = false;
foregroundResumeSyncPending.value = false;
cancelPracticeSyncWait();
practiseResult.value = Array.isArray(message?.details)
? message
: await getPractiseAPI(practiseId.value);
start.value = false;
};
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) {
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
);
if (validArrows.length === total) {
uni.navigateBack();
} else {
practiseId.value = "";
serverAddr.value = "";
practiseResult.value = {};
start.value = false;
scores.value = [];
isSvip.value = false;
await createPractise();
}
}
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) {
await endPractiseAPI(practiseId.value);
}
} catch (error) {
console.error("Failed to stop practice", error);
} finally {
uni.navigateBack();
}
}
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
).length;
return `${RESULT_TIP_CDN}/${validCount < total ? "un" : ""}finish-tip.png`;
};
const onClickShare = debounce(async () => {
if (sharing.value) return;
sharing.value = true;
try {
await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
await wxShare("shareCanvas");
} catch (e) {
uni.showToast({
title: "海报生成失败,请稍后重试",
icon: "none",
});
} finally {
sharing.value = false;
}
});
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("第一轮");
uni.setKeepScreenOn({
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" });
});
</script>
<template>
<Container
:bgType="1"
title="个人单组练习"
:showBottom="!start && !scores.length"
:onBack="exitPractise"
>
<view>
<TestDistance v-if="!start && !practiseResult.id" />
<block v-else>
<ShootProgress
:tips="`${
!start || scores.length === 12
? ''
: `请开始射箭第${
roundsName[Math.ceil((scores.length + 1) / 3)]
}轮`
}`"
:start="start"
:onStop="onOver"
end-audio-key="练习结束"
/>
<view class="user-row">
<Avatar :src="user.avatar" :size="35" />
<BubbleTip v-if="showGuide" type="normal2">
<text>还有两场坚持</text>
<text>就是胜利!💪</text>
</BubbleTip>
<BowPower />
</view>
<BowTarget
v-if="!restoringSnapshot"
:totalRound="start ? total / 4 : 0"
:currentRound="scores.length % 3"
:scores="scores"
:isSvip="isSvip"
/>
<ScorePanel2 :arrows="scores" />
<ScoreResult
v-if="practiseResult.details"
:rowCount="6"
:total="total"
:onClose="onComplete"
:result="practiseResult"
:tipSrc="getResultTipSrc(practiseResult)"
/>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
</block>
</view>
<template #bottom>
<SButton :onClick="onReady">准备好了直接开始</SButton>
</template>
</Container>
</template>
<style scoped></style>