update:对接个人训练改版

This commit is contained in:
2026-07-23 09:44:58 +08:00
parent a78ab1daeb
commit 3c5754b3fd
64 changed files with 1769 additions and 488 deletions
+696 -75
View File
@@ -1,6 +1,6 @@
<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { onHide, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ShootProgress from "./components/ShootProgress.vue";
import BowTarget from "./components/BowTarget.vue";
@@ -13,11 +13,17 @@ import BubbleTip from "./components/BubbleTip.vue";
import audioManager from "@/audioManager";
import {
createPractiseAPI,
createPractiseV2API,
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
} from "@/apis";
import {
connectMatchWebSocket,
closeMatchWebSocket,
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,22 +41,41 @@ const pageStages = Object.freeze({
RESULT: "result",
LOADING: "loading",
});
const pageStage = ref(pageStages.DISTANCE);
const pageStage = ref(pageStages.LOADING);
const scores = ref([]);
// 只在实时 ShootResult 新增一箭时递增,避免同步快照重播飞箭特效。
const shotEffectToken = ref(0);
const defaultTotal = 12;
const defaultShootTime = 120;
const defaultTargetType = 1;
const total = ref(defaultTotal);
const shootTime = ref(defaultShootTime);
const practiseResult = ref({});
const practiceEndSnapshot = ref({});
const practiseId = ref("");
const showGuide = ref(false);
const tips = ref("");
const targetType = ref(defaultTargetType);
const trainingParams = ref({});
const practiceInfo = ref({});
const trainingDifficultyStorageKey = "training-selection";
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
const useHighlightTest = ref(false);
const highlightTestState = ref({
blocks: 8,
randomBlock: 1,
randomRingArea: 0,
});
const highlightTestTimer = ref(null);
const serverAddr = ref("");
const practiceEnded = ref(false);
const stopCompleted = ref(false);
const stopInFlight = ref(false);
const exiting = ref(false);
const hiddenWhileActive = ref(false);
const connectionClosed = ref(true);
let stopPracticeTask = null;
let practiceSyncTimer = null;
let waitingPracticeSync = false;
const PRACTICE_SYNC_TIMEOUT_MS = 5000;
const env = computed(() => {
try {
@@ -66,27 +91,135 @@ const hasPractiseResult = computed(() => !!practiseResult.value?.details);
const showResult = computed(
() => pageStage.value === pageStages.RESULT && hasPractiseResult.value
);
const isSvip = computed(() => practiceInfo.value.sVip === true);
const defaultHighlightAreas = [{ quadrant: 1, rings: [7] }];
const trainingType = computed(
() => practiceInfo.value.trainingType || trainingParams.value.type || ""
);
// 临时高亮测试数据:第 N 项对应第 N 箭,每箭展示一个不同区域。
const highlightTestAreas = [
{ arrowIndex: 1, quadrant: 1, rings: [10] },
{ arrowIndex: 2, quadrant: 2, rings: [9, 10] },
{ arrowIndex: 3, quadrant: 3, rings: [8, 9] },
{ arrowIndex: 4, quadrant: 4, rings: [7, 8] },
{ arrowIndex: 5, quadrant: 1, rings: [6, 7] },
{ arrowIndex: 6, quadrant: 2, rings: [5, 6] },
{ arrowIndex: 7, quadrant: 3, rings: [4, 5] },
{ arrowIndex: 8, quadrant: 4, rings: [3, 4] },
{ arrowIndex: 9, quadrant: 1, rings: "all", scope: "sector" },
{ arrowIndex: 10, quadrant: 2, rings: "all", scope: "sector" },
{ arrowIndex: 11, quadrant: 3, rings: "all", scope: "sector" },
{ arrowIndex: 12, quadrant: 4, rings: "all", scope: "sector" },
];
const getPracticeNumber = (value, fallback = 0) => {
if (value === undefined || value === null || value === "") return fallback;
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : fallback;
};
const targetHighlightAreas = computed(() => {
return useHighlightTest.value ? highlightTestAreas : defaultHighlightAreas;
const getPositiveInteger = (value) => {
const numberValue = Number(value);
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
};
const currentDifficultyLevel = computed(
() =>
getPositiveInteger(practiseResult.value.difficultyLevel) ||
getPositiveInteger(practiseResult.value.difficulty_level) ||
getPositiveInteger(practiceInfo.value.difficultyLevel) ||
getPositiveInteger(trainingParams.value.difficulty)
);
// time_limit 缺失或非正数都表示整局不限时。
const timeLimit = computed(() => getPositiveInteger(practiceInfo.value.timeLimit));
const hasTimeLimit = computed(() => timeLimit.value > 0);
const precisionBlocks = computed(() => {
if (useHighlightTest.value) {
return getPositiveInteger(highlightTestState.value.blocks);
}
if (trainingType.value !== "precision") return 0;
return (
getPositiveInteger(practiceInfo.value.blocks) ||
getPositiveInteger(trainingParams.value.blocks)
);
});
const precisionRandomBlock = computed(() => {
const block = getPositiveInteger(
useHighlightTest.value
? highlightTestState.value.randomBlock
: practiceInfo.value.randomBlock
);
return block <= precisionBlocks.value ? block : 0;
});
const precisionRandomRingArea = computed(() => {
const ring = getPositiveInteger(
useHighlightTest.value
? highlightTestState.value.randomRingArea
: practiceInfo.value.randomRingArea
);
return ring >= 1 && ring <= 10 ? ring : 0;
});
// 只展示后端进度,不在前端重复判断训练是否完成。
const trainingCopy = computed(() => {
if (trainingType.value === "base") {
const hitReq = getPracticeNumber(
practiceInfo.value.hitReq,
trainingParams.value.hitReq
);
const arrowsLeft = getPracticeNumber(
practiceInfo.value.arrowsLeft,
total.value
);
return {
title: `每箭命中${hitReq}环之上`,
details: [
{ text: "剩余" },
{ text: arrowsLeft, highlight: true },
{ text: "箭达到条件" },
],
};
}
if (trainingType.value === "endurance") {
const targetArrows = getPracticeNumber(
practiceInfo.value.targetArrows,
total.value
);
const targetRings = getPracticeNumber(
practiceInfo.value.targetRings,
trainingParams.value.totalReq
);
const currentArrows = getPracticeNumber(practiceInfo.value.currentArrows);
const currentRings = getPracticeNumber(practiceInfo.value.currentRings);
return {
title: `完成${targetArrows}箭并累计${targetRings}`,
details: [
{ text: "已完成" },
{ text: currentArrows, highlight: true },
{ text: "箭,累计" },
{ text: currentRings, highlight: true },
{ text: "环" },
],
};
}
if (trainingType.value === "precision") {
const block = precisionRandomBlock.value;
const ring = precisionRandomRingArea.value;
const arrowsLeft = getPracticeNumber(
practiceInfo.value.arrowsLeft,
total.value
);
const title = block
? ring
? `请命中区域${block}${ring}`
: `请命中区域${block}`
: "等待目标区域";
return {
title,
details: [
{ text: "剩余" },
{ text: arrowsLeft, highlight: true },
{ text: "箭" },
],
};
}
return null;
});
const toRouteNumber = (value, fallback = 0) => {
@@ -99,14 +232,352 @@ const toPositiveRouteNumber = (value, fallback) => {
return numberValue > 0 ? numberValue : fallback;
};
const createPractice = async () => {
const result = await createPractiseAPI(
total.value,
shootTime.value,
targetType.value
);
const practiceInfoFields = [
"id",
"userId",
"status",
"statusText",
"startTime",
"targetType",
"sVip",
"trainingType",
"difficultyLevel",
"hitReq",
"arrowsLeft",
"targetArrows",
"targetRings",
"currentArrows",
"currentRings",
"blocks",
"randomBlock",
"randomRingArea",
"timeLimit",
"completed",
"totalArrows",
"duration",
"averageRing",
"stability",
"maxCombo",
"totalHits",
"deltaTotalHits",
"deltaDuration",
"deltaMaxCombo",
"deltaTotalRings",
"deltaTotalArrows",
"deltaAverageRing",
"deltaStability",
"beforeExp",
"beforeLevel",
"currentExp",
"level",
"upgradeExp",
"calories",
"shootData",
"details",
];
if (result) practiseId.value = result.id;
const practiceResultFields = [
"trainingType",
"difficultyLevel",
"completed",
"totalArrows",
"duration",
"averageRing",
"stability",
"maxCombo",
"totalHits",
"currentRings",
"deltaTotalHits",
"deltaDuration",
"deltaMaxCombo",
"deltaTotalRings",
"deltaTotalArrows",
"deltaAverageRing",
"deltaStability",
"beforeExp",
"beforeLevel",
"currentExp",
"level",
"upgradeExp",
"calories",
"details",
];
const syncPracticeInfo = (message = {}) => {
const nextInfo = practiceInfoFields.reduce((result, field) => {
if (Object.prototype.hasOwnProperty.call(message, field)) {
result[field] = message[field];
}
return result;
}, {});
const isPrecisionSnapshot =
(message.type === MESSAGETYPESV2.BattleStart ||
message.type === MESSAGETYPESV2.ShootResult) &&
(message.trainingType === "precision" || trainingType.value === "precision");
if (isPrecisionSnapshot) {
// proto3 会省略数值 0;新快照未携带时必须清除上一箭的随机目标。
if (!Object.prototype.hasOwnProperty.call(message, "randomBlock")) {
nextInfo.randomBlock = 0;
}
if (!Object.prototype.hasOwnProperty.call(message, "randomRingArea")) {
nextInfo.randomRingArea = 0;
}
}
if (Object.keys(nextInfo).length === 0) return;
practiceInfo.value = {
...practiceInfo.value,
...nextInfo,
};
};
const createPracticeEndSnapshot = (message = {}) => {
const source = {
...practiceInfo.value,
...message,
};
const snapshot = practiceResultFields.reduce((result, field) => {
if (Object.prototype.hasOwnProperty.call(source, field)) {
result[field] = source[field];
}
return result;
}, {});
// proto3 会省略 falsePRACTICE_END 未携带 completed 时按未达标处理。
snapshot.completed = message.completed === true;
snapshot.trainingType = source.trainingType || trainingType.value;
if (Array.isArray(message.details)) {
snapshot.details = message.details;
} else if (scores.value.length > 0) {
snapshot.details = [...scores.value];
} else if (Array.isArray(practiceInfo.value.details)) {
snapshot.details = practiceInfo.value.details;
} else {
delete snapshot.details;
}
return snapshot;
};
const mergePracticeResult = (apiResult = {}) => {
const snapshot = practiceEndSnapshot.value;
const snapshotDetails = Array.isArray(snapshot.details)
? snapshot.details
: null;
const apiDetails = Array.isArray(apiResult.details) ? apiResult.details : null;
const details = snapshotDetails?.length
? snapshotDetails
: apiDetails || snapshotDetails || [...scores.value];
return {
...apiResult,
...snapshot,
details,
};
};
const clearPracticeSyncTimer = () => {
if (!practiceSyncTimer) return;
clearTimeout(practiceSyncTimer);
practiceSyncTimer = null;
};
const cancelPracticeSyncWait = () => {
waitingPracticeSync = false;
clearPracticeSyncTimer();
};
const preparePracticeSyncWait = () => {
cancelPracticeSyncWait();
waitingPracticeSync = true;
};
const startPracticeSyncTimer = () => {
clearPracticeSyncTimer();
waitingPracticeSync = true;
practiceSyncTimer = setTimeout(() => {
practiceSyncTimer = null;
if (!waitingPracticeSync) return;
waitingPracticeSync = false;
if (pageStage.value !== pageStages.LOADING) return;
uni.showToast({
title: "练习信息获取失败,请重试",
icon: "none",
});
setTimeout(() => {
void exitPractice();
}, 500);
}, PRACTICE_SYNC_TIMEOUT_MS);
};
const onMatchSocketState = (event = {}) => {
if (event.state !== "open") return;
if (
event.matchId &&
String(event.matchId) !== String(practiseId.value)
) {
return;
}
// 管理器会在 open 事件后立即发送 5,从这里开始计算响应超时。
startPracticeSyncTimer();
};
const onPracticeInfoSync = (payload = {}) => {
const responseMatchId = String(
payload.matchId || payload.practiceInfo?.id || ""
);
if (!responseMatchId || responseMatchId !== String(practiseId.value)) return;
const snapshot = payload.practiceInfo;
if (!snapshot || typeof snapshot !== "object") return;
const shouldShowDistance =
waitingPracticeSync && pageStage.value === pageStages.LOADING;
cancelPracticeSyncWait();
// 14 是完整快照,先清空旧值,避免 proto3 省略的 0 沿用上一份状态。
practiceInfo.value = {};
practiceEndSnapshot.value = {};
syncPracticeInfo(snapshot);
scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];
if (shouldShowDistance) {
start.value = false;
pageStage.value = pageStages.DISTANCE;
}
};
// 训练在难度页创建,目标页只消费连接上下文,避免进入页面后重复创建。
const getTrainingContext = () => {
const context = uni.getStorageSync(trainingDifficultyStorageKey);
return context && typeof context === "object" ? context : {};
};
const updateTrainingContext = (practice = {}) => {
const context = getTrainingContext();
uni.setStorageSync(trainingDifficultyStorageKey, {
...context,
trainingType: trainingParams.value.type,
difficultyLevel: trainingParams.value.difficulty,
practiceId: practice.id || "",
serverAddr: practice.serverAddr || "",
createdAt: practice.id ? Date.now() : 0,
});
};
const clearPracticeRuntimeContext = () => {
const context = getTrainingContext();
if (
context.practiceId &&
practiseId.value &&
String(context.practiceId) !== String(practiseId.value)
) {
return;
}
const {
practiceId: _practiceId,
serverAddr: _serverAddr,
createdAt: _createdAt,
...selectionContext
} = context;
uni.setStorageSync(trainingDifficultyStorageKey, selectionContext);
};
const closePracticeConnection = (reason) => {
cancelPracticeSyncWait();
if (connectionClosed.value) return;
connectionClosed.value = true;
closeMatchWebSocket({ reason });
};
const connectPracticeServer = () => {
if (!practiseId.value || !String(serverAddr.value || "").trim()) {
return false;
}
cancelPracticeSyncWait();
closeMatchWebSocket({ reason: "training-practice-switch" });
preparePracticeSyncWait();
connectMatchWebSocket({
serverAddr: serverAddr.value,
matchId: practiseId.value,
userId: user.value.id,
requestPracticeInfoOnOpen: true,
});
connectionClosed.value = false;
return true;
};
// 返回、切后台和页面销毁可能连续触发,复用同一个 stop 任务避免重复请求。
const stopCurrentPractice = () => {
if (
!practiseId.value ||
practiceEnded.value ||
stopCompleted.value
) {
return Promise.resolve();
}
if (stopPracticeTask) return stopPracticeTask;
stopInFlight.value = true;
stopPracticeTask = endPractiseAPI(practiseId.value)
.then(() => {
stopCompleted.value = true;
clearPracticeRuntimeContext();
})
.catch((error) => {
console.error("training practice stop failed", error);
})
.finally(() => {
stopInFlight.value = false;
stopPracticeTask = null;
});
return stopPracticeTask;
};
const createPractice = async () => {
const trainingType = trainingParams.value.type;
const difficultyLevel = trainingParams.value.difficulty;
if (!trainingType || difficultyLevel <= 0) {
uni.showToast({
title: "训练参数异常,请重新选择难度",
icon: "none",
});
return null;
}
closePracticeConnection("training-practice-recreate");
const result = await createPractiseV2API(trainingType, difficultyLevel);
if (!result?.id || !String(result?.serverAddr || "").trim()) {
if (result?.id) {
try {
await endPractiseAPI(result.id);
} catch (error) {
console.error("training practice cleanup failed", error);
}
}
uni.showToast({
title: "练习连接信息异常,请重试",
icon: "none",
});
return null;
}
practiseId.value = result.id;
serverAddr.value = result.serverAddr;
practiceEnded.value = false;
stopCompleted.value = false;
stopInFlight.value = false;
stopPracticeTask = null;
updateTrainingContext(result);
connectPracticeServer();
return result;
};
const clearHighlightTestTimer = () => {
@@ -116,23 +587,7 @@ const clearHighlightTestTimer = () => {
}
};
const buildHighlightTestScore = (index) => ({
playerId: user.value?.id,
ring: 9,
ringX: false,
x: ((index % 4) - 1.5) * 2,
y: (Math.floor(index / 4) - 1) * 2,
angle: null,
});
const setHighlightTestArrow = (arrowIndex) => {
const completedCount = Math.max(arrowIndex - 1, 0);
scores.value = Array.from({ length: completedCount }, (_, index) =>
buildHighlightTestScore(index)
);
};
// 临时测试入口:自动切换第 1 到第 12 箭,让 BowTarget 按当前箭展示不同高亮。
// 开发环境测试入口:依次切换 8 个顺时针区域,偶数区域只高亮指定环。
const runHighlightTest = () => {
clearHighlightTestTimer();
useHighlightTest.value = true;
@@ -140,50 +595,83 @@ const runHighlightTest = () => {
pageStage.value = pageStages.SHOOTING;
start.value = true;
let arrowIndex = 1;
setHighlightTestArrow(arrowIndex);
let block = 1;
highlightTestState.value = {
blocks: 8,
randomBlock: block,
randomRingArea: 0,
};
highlightTestTimer.value = setInterval(() => {
if (arrowIndex >= highlightTestAreas.length) {
if (block >= highlightTestState.value.blocks) {
clearHighlightTestTimer();
return;
}
arrowIndex += 1;
setHighlightTestArrow(arrowIndex);
block += 1;
highlightTestState.value = {
blocks: 8,
randomBlock: block,
randomRingArea: block % 2 === 0 ? Math.min(block, 10) : 0,
};
}, 1000);
};
const resetHighlightTest = () => {
clearHighlightTestTimer();
useHighlightTest.value = false;
highlightTestState.value = {
blocks: 8,
randomBlock: 1,
randomRingArea: 0,
};
scores.value = [];
};
onLoad((options = {}) => {
const trainingContext = getTrainingContext();
targetType.value = toPositiveRouteNumber(options.target, defaultTargetType);
total.value = toPositiveRouteNumber(options.arrows, defaultTotal);
shootTime.value = toPositiveRouteNumber(options.time, defaultShootTime);
trainingParams.value = {
type: options.type || "",
type: options.type || trainingContext.trainingType || "",
difficultyId: options.difficultyId || "",
difficulty: toRouteNumber(options.difficulty),
difficulty: toRouteNumber(
options.difficulty,
toRouteNumber(trainingContext.difficultyLevel)
),
recordId: options.recordId || "",
hitReq: toRouteNumber(options.hitReq),
totalReq: toRouteNumber(options.totalReq),
blocks: toRouteNumber(options.blocks),
mode: toRouteNumber(options.mode),
};
practiseId.value = trainingContext.practiceId || "";
serverAddr.value = trainingContext.serverAddr || "";
});
const onReady = async () => {
if (
!practiseId.value ||
practiceEnded.value ||
stopCompleted.value ||
stopInFlight.value
) {
uni.showToast({
title: "训练已结束,请重新进入",
icon: "none",
});
return;
}
pageStage.value = pageStages.LOADING;
clearHighlightTestTimer();
useHighlightTest.value = false;
practiceEndSnapshot.value = {};
try {
await startPractiseAPI();
await startPractiseAPI(practiseId.value);
practiseResult.value = {};
scores.value = [];
shotEffectToken.value = 0;
start.value = true;
pageStage.value = pageStages.SHOOTING;
audioManager.play("练习开始");
@@ -194,6 +682,24 @@ const onReady = async () => {
}
};
const onTimeLimitReached = () => {
if (!hasTimeLimit.value || !isShootingStage.value) return;
// 本地倒计时只负责停止射击展示,最终结算以 PRACTICE_END 为准。
start.value = false;
};
const enterPracticeResult = (result = {}) => {
practiseResult.value = result;
if (!hasPractiseResult.value) return false;
// 正常结算不调用 stop,只清理上下文并断开比赛服连接。
practiceEnded.value = true;
clearPracticeRuntimeContext();
closePracticeConnection("training-practice-result");
pageStage.value = pageStages.RESULT;
return true;
};
const onOver = async () => {
if (!isShootingStage.value) return;
@@ -202,11 +708,15 @@ const onOver = async () => {
start.value = false;
try {
practiseResult.value = (await getPractiseAPI(practiseId.value)) || {};
pageStage.value = hasPractiseResult.value
? pageStages.RESULT
: pageStages.DISTANCE;
const apiResult = (await getPractiseAPI(practiseId.value)) || {};
if (!enterPracticeResult(mergePracticeResult(apiResult))) {
pageStage.value = pageStages.DISTANCE;
}
} catch (error) {
if (Object.keys(practiceEndSnapshot.value).length > 0) {
enterPracticeResult(mergePracticeResult());
return;
}
start.value = true;
pageStage.value = pageStages.SHOOTING;
throw error;
@@ -214,9 +724,30 @@ const onOver = async () => {
};
async function onReceiveMessage(msg) {
syncPracticeInfo(msg);
if (msg.type === MESSAGETYPESV2.ShootResult && isShootingStage.value) {
scores.value = msg.details;
if (Array.isArray(msg.details)) {
const previousScoreLength = scores.value.length;
scores.value = msg.details;
if (msg.details.length === previousScoreLength + 1) {
shotEffectToken.value += 1;
}
}
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
practiceEndSnapshot.value = createPracticeEndSnapshot(msg);
if (
trainingType.value === "base" &&
Number(msg.status) === 3 &&
!Object.prototype.hasOwnProperty.call(msg, "arrowsLeft")
) {
practiceInfo.value = {
...practiceInfo.value,
arrowsLeft: 0,
};
}
practiceEnded.value = true;
clearPracticeRuntimeContext();
// setTimeout(onOver, 1500);
}
}
@@ -224,6 +755,9 @@ async function onReceiveMessage(msg) {
function onComplete() {
pageStage.value = pageStages.LOADING;
start.value = false;
practiceEnded.value = true;
clearPracticeRuntimeContext();
closePracticeConnection("training-practice-complete");
uni.$emit(trainingDifficultyRefreshEvent);
uni.navigateBack();
}
@@ -233,12 +767,18 @@ async function onRetry() {
clearHighlightTestTimer();
useHighlightTest.value = false;
practiseId.value = "";
serverAddr.value = "";
practiseResult.value = {};
practiceEndSnapshot.value = {};
practiceInfo.value = {};
start.value = false;
scores.value = [];
shotEffectToken.value = 0;
try {
await createPractice();
} finally {
const practice = await createPractice();
if (!practice) pageStage.value = pageStages.DISTANCE;
} catch (error) {
console.error("training practice retry failed", error);
pageStage.value = pageStages.DISTANCE;
}
}
@@ -259,28 +799,89 @@ const updateSound = () => {
audioManager.setMuted(!sound.value);
};
const exitPractice = async () => {
if (exiting.value) return;
exiting.value = true;
onMounted(async () => {
try {
await stopCurrentPractice();
} finally {
closePracticeConnection("training-practice-exit");
clearPracticeRuntimeContext();
uni.navigateBack();
}
};
onHide(() => {
// 小程序被切到后台时尽早通知后端,作为杀进程前的尽力兜底。
if (
!exiting.value &&
practiseId.value &&
!practiceEnded.value &&
!stopCompleted.value
) {
hiddenWhileActive.value = true;
clearPracticeRuntimeContext();
void stopCurrentPractice();
}
closePracticeConnection("training-practice-hide");
});
onShow(async () => {
if (!hiddenWhileActive.value || exiting.value) return;
hiddenWhileActive.value = false;
await stopCurrentPractice();
clearPracticeRuntimeContext();
exiting.value = true;
uni.showToast({
title: "训练已结束,请重新进入",
icon: "none",
});
uni.navigateBack();
});
onUnload(() => {
clearPracticeRuntimeContext();
void stopCurrentPractice();
closePracticeConnection("training-practice-unload");
});
onMounted(() => {
// 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);
uni.$on("audioEnded", onAudioEnded);
await createPractice();
if (!connectPracticeServer()) {
uni.showToast({
title: "练习连接信息异常,请重试",
icon: "none",
});
setTimeout(() => {
void exitPractice();
}, 500);
}
});
onBeforeUnmount(() => {
clearPracticeRuntimeContext();
void stopCurrentPractice();
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);
uni.$off("audioEnded", onAudioEnded);
audioManager.stopAll();
clearHighlightTestTimer();
endPractiseAPI();
closePracticeConnection("training-practice-unmount");
});
</script>
@@ -289,14 +890,21 @@ onBeforeUnmount(() => {
:bgType="isDistanceStage ? 9 : 11"
:showBottom="isDistanceStage"
:scroll="!isShootingStage"
:onBack="exitPractice"
>
<view class="practise-content">
<TestDistance v-if="isDistanceStage" />
<TestDistance
v-if="isDistanceStage"
:targetType="practiceInfo.targetType"
/>
<view v-else-if="isShootingStage" class="shooting-layout">
<view class="shooting-fixed">
<ShootProgress
:start="start"
:onStop="onOver"
:total="timeLimit"
:countdownEnabled="hasTimeLimit"
:trainingType="trainingType"
:onStop="onTimeLimitReached"
/>
<view class="user-row">
<!-- <Avatar :src="user.avatar" :size="35" /> -->
@@ -310,8 +918,13 @@ onBeforeUnmount(() => {
:totalRound="start ? total / 4 : 0"
:currentRound="scores.length % 3"
:scores="scores"
:isSvip="isSvip"
:shotEffectToken="shotEffectToken"
:showCrosshair="false"
:highlightAreas="targetHighlightAreas"
:sectorCount="precisionBlocks"
:activeSector="precisionRandomBlock"
:activeRing="precisionRandomRingArea"
:showSectorLabels="precisionBlocks > 0"
/>
<view v-if="env !== 'release'" class="highlight-test-actions">
<button
@@ -319,7 +932,7 @@ onBeforeUnmount(() => {
hover-class="none"
@click="runHighlightTest"
>
高亮测试
扇区测试
</button>
<button
class="highlight-test-btn"
@@ -340,14 +953,20 @@ onBeforeUnmount(() => {
<view class="bat-text-big-box">
<image
class="dao-icon"
src="../../static/training-difficulty-design/dao-icon.png"
src="./static/training-difficulty-design/dao-icon.png"
mode="widthFix"
/>
<view class="bat-text-box">
<view v-if="trainingCopy" class="bat-text-box">
<view class="bat-text-small-box">
<view class="text-round-box">
<view class="text1">每箭命中9环之上</view>
<view class="text2">剩余<text class="text2-yellow">3</text></view>
<view class="text1">{{ trainingCopy.title }}</view>
<view class="text2">
<text
v-for="(part, index) in trainingCopy.details"
:key="index"
:class="{ 'text2-yellow': part.highlight }"
>{{ part.text }}</text>
</view>
</view>
</view>
</view>
@@ -369,6 +988,8 @@ onBeforeUnmount(() => {
:total="total"
:onClose="onComplete"
:onRetry="onRetry"
:trainingType="trainingType"
:difficultyLevel="currentDifficultyLevel"
:result="practiseResult"
/>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
@@ -377,7 +998,7 @@ onBeforeUnmount(() => {
<view class="btn-box">
<image
class="btn-box-bg"
src="../../static/training-difficulty-design/par-star.png"
src="./static/training-difficulty-design/par-star.png"
mode="widthFix"
/>
<button class="btn" @click="onReady">准备好了开始练习</button>