update:优化个人训练
This commit is contained in:
@@ -25,6 +25,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
usePageScroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
isHome: {
|
isHome: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -53,6 +57,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const isIOS = uni.getDeviceInfo().osName === "ios";
|
const isIOS = uni.getDeviceInfo().osName === "ios";
|
||||||
const showHint = ref(false);
|
const showHint = ref(false);
|
||||||
@@ -125,9 +137,22 @@ const goCalibration = async () => {
|
|||||||
:onBack="onBack"
|
:onBack="onBack"
|
||||||
:whiteBackArrow="whiteBackArrow"
|
:whiteBackArrow="whiteBackArrow"
|
||||||
:titleStyle="titleStyle"
|
:titleStyle="titleStyle"
|
||||||
|
:style="
|
||||||
|
usePageScroll
|
||||||
|
? {
|
||||||
|
position: 'sticky',
|
||||||
|
top: capsuleHeight + 'px',
|
||||||
|
zIndex: 10,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<BackToGame v-if="showBackToGame" />
|
<BackToGame v-if="showBackToGame" />
|
||||||
|
<view v-if="usePageScroll">
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
<scroll-view
|
<scroll-view
|
||||||
|
v-else
|
||||||
:scroll-y="scroll"
|
:scroll-y="scroll"
|
||||||
:enhanced="true"
|
:enhanced="true"
|
||||||
:bounces="false"
|
:bounces="false"
|
||||||
@@ -189,6 +214,18 @@ const goCalibration = async () => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ScreenHint>
|
</ScreenHint>
|
||||||
|
<view v-if="loading" class="audio-progress">
|
||||||
|
<image
|
||||||
|
src="https://static.shelingxingqiu.com/attachment/2025-11-26/deihtj15xjwcz3c1tx.png"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
|
<view>
|
||||||
|
<view :style="{ width: '100%' }"></view>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<text>{{ loadingText || "加载中..." }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -230,4 +267,55 @@ const goCalibration = async () => {
|
|||||||
color: #666;
|
color: #666;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-progress {
|
||||||
|
z-index: 999;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: rgb(0 0 0 / 0.8);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-progress > image:nth-child(1) {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 150rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-progress > view:nth-child(2) {
|
||||||
|
width: 380rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: #595959;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-progress > view:nth-child(2) > view {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 6rpx;
|
||||||
|
background: #ffe431;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-progress > view:nth-child(3) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-progress > view:nth-child(3) > text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #a2a2a2;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
.header > view:first-child > view:last-child > text:last-child {
|
.header > view:first-child > view:last-child > text:last-child {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
/* background-color: #5f51ff; */
|
background-color: #5f51ff;
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
hasNextDifficulty: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
result: {
|
result: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
@@ -297,9 +301,7 @@ const resultRows = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const advancesDifficulty = computed(() =>
|
const advancesDifficulty = computed(() => props.hasNextDifficulty);
|
||||||
["base", "endurance"].includes(resultTrainingType.value)
|
|
||||||
);
|
|
||||||
const primaryText = computed(() =>
|
const primaryText = computed(() =>
|
||||||
advancesDifficulty.value ? "下一难度" : "再来一次"
|
advancesDifficulty.value ? "下一难度" : "再来一次"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, ref } from "vue";
|
import { computed, nextTick, ref } from "vue";
|
||||||
import { onLoad, onShow, onUnload } 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 TrainingDifficultyBadge from "./components/TrainingDifficultyBadge.vue";
|
import TrainingDifficultyBadge from "./components/TrainingDifficultyBadge.vue";
|
||||||
import TrainingDifficultyPreviewCard from "./components/TrainingDifficultyPreviewCard.vue";
|
import TrainingDifficultyPreviewCard from "./components/TrainingDifficultyPreviewCard.vue";
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
// 1. 接口:GET /training/difficulty/list?type=base/endurance/precision/rhythm
|
// 1. 接口:GET /training/difficulty/list?type=base/endurance/precision/rhythm
|
||||||
// 2. 当前进度:接口 user_levels / list.completed,路由参数可覆盖选中难度
|
// 2. 当前进度:接口 user_levels / list.completed,路由参数可覆盖选中难度
|
||||||
const trainingDifficultyStorageKey = "training-selection";
|
const trainingDifficultyStorageKey = "training-selection";
|
||||||
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
|
|
||||||
const defaultTrainingType = "precision";
|
const defaultTrainingType = "precision";
|
||||||
const defaultUnlockedDifficultyId = "lv1";
|
const defaultUnlockedDifficultyId = "lv1";
|
||||||
const trainingTypeMetaMap = {
|
const trainingTypeMetaMap = {
|
||||||
@@ -239,8 +238,9 @@ const selectedDifficultyId = ref(defaultUnlockedDifficultyId);
|
|||||||
const nodesScrollTop = ref(0);
|
const nodesScrollTop = ref(0);
|
||||||
const nodesScrollWithAnimation = ref(false);
|
const nodesScrollWithAnimation = ref(false);
|
||||||
const routeOptions = ref({});
|
const routeOptions = ref({});
|
||||||
const needRefreshProgress = ref(false);
|
const shouldRefreshOnShow = ref(false);
|
||||||
const creatingPractice = ref(false);
|
const creatingPractice = ref(false);
|
||||||
|
let pageStateRequestGeneration = 0;
|
||||||
|
|
||||||
const difficultyProgressMap = computed(() => {
|
const difficultyProgressMap = computed(() => {
|
||||||
return pageConfig.value?.progressMap || {};
|
return pageConfig.value?.progressMap || {};
|
||||||
@@ -533,6 +533,7 @@ const applyPageState = (options = {}, config) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initPageState = async (options = {}, refreshOptions = {}) => {
|
const initPageState = async (options = {}, refreshOptions = {}) => {
|
||||||
|
const requestGeneration = ++pageStateRequestGeneration;
|
||||||
const { keepCurrent = false } = refreshOptions;
|
const { keepCurrent = false } = refreshOptions;
|
||||||
const trainingType = resolveTrainingType(options.mode);
|
const trainingType = resolveTrainingType(options.mode);
|
||||||
const fallbackConfig = createEmptyModeConfig(trainingType);
|
const fallbackConfig = createEmptyModeConfig(trainingType);
|
||||||
@@ -543,11 +544,19 @@ const initPageState = async (options = {}, refreshOptions = {}) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await getTrainingDifficultyListAPI(trainingType);
|
const result = await getTrainingDifficultyListAPI(trainingType);
|
||||||
|
if (requestGeneration !== pageStateRequestGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
applyPageState(
|
applyPageState(
|
||||||
options,
|
options,
|
||||||
normalizeTrainingDifficultyConfig(result, trainingType)
|
normalizeTrainingDifficultyConfig(result, trainingType)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (requestGeneration !== pageStateRequestGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("training difficulty load failed", error);
|
console.log("training difficulty load failed", error);
|
||||||
if (!keepCurrent) {
|
if (!keepCurrent) {
|
||||||
applyPageState(options, fallbackConfig);
|
applyPageState(options, fallbackConfig);
|
||||||
@@ -687,10 +696,6 @@ const handleStart = async () => {
|
|||||||
let createdPracticeId = "";
|
let createdPracticeId = "";
|
||||||
|
|
||||||
creatingPractice.value = true;
|
creatingPractice.value = true;
|
||||||
uni.showLoading({
|
|
||||||
title: "训练创建中",
|
|
||||||
mask: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先由业务接口创建训练和比赛服,再把连接上下文交给目标页。
|
// 先由业务接口创建训练和比赛服,再把连接上下文交给目标页。
|
||||||
@@ -731,35 +736,27 @@ const handleStart = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
uni.hideLoading();
|
|
||||||
creatingPractice.value = false;
|
creatingPractice.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const markProgressRefresh = () => {
|
|
||||||
needRefreshProgress.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
onLoad((options = {}) => {
|
onLoad((options = {}) => {
|
||||||
routeOptions.value = { ...options };
|
routeOptions.value = { ...options };
|
||||||
uni.$on(trainingDifficultyRefreshEvent, markProgressRefresh);
|
void initPageState(options);
|
||||||
initPageState(options);
|
});
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
shouldRefreshOnShow.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (!needRefreshProgress.value) {
|
if (!shouldRefreshOnShow.value) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
needRefreshProgress.value = false;
|
shouldRefreshOnShow.value = false;
|
||||||
initPageState(routeOptions.value, {
|
void initPageState(routeOptions.value, {
|
||||||
keepCurrent: true,
|
keepCurrent: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnload(() => {
|
|
||||||
uni.$off(trainingDifficultyRefreshEvent, markProgressRefresh);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -768,6 +765,8 @@ onUnload(() => {
|
|||||||
:bgType="8"
|
:bgType="8"
|
||||||
bgColor="#1c1c23"
|
bgColor="#1c1c23"
|
||||||
:scroll="false"
|
:scroll="false"
|
||||||
|
:loading="creatingPractice"
|
||||||
|
loadingText="训练创建中..."
|
||||||
>
|
>
|
||||||
<view class="difficulty-page">
|
<view class="difficulty-page">
|
||||||
<view class="difficulty-page__nodes">
|
<view class="difficulty-page__nodes">
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const trainingModeRouteMap = {
|
|||||||
rhythm: "rhythm",
|
rhythm: "rhythm",
|
||||||
strength: "power",
|
strength: "power",
|
||||||
};
|
};
|
||||||
|
const unavailableTrainingIds = new Set(["rhythm", "strength"]);
|
||||||
// 训练项目卡片右侧主图标。
|
// 训练项目卡片右侧主图标。
|
||||||
const trainingModeIconMap = {
|
const trainingModeIconMap = {
|
||||||
base_bow:
|
base_bow:
|
||||||
@@ -79,7 +80,7 @@ const trainingData = ref(createDefaultTrainingData());
|
|||||||
const recommendedTrainingId = ref("");
|
const recommendedTrainingId = ref("");
|
||||||
const visibleTrainingItems = computed(() =>
|
const visibleTrainingItems = computed(() =>
|
||||||
Array.isArray(trainingData.value.training_items)
|
Array.isArray(trainingData.value.training_items)
|
||||||
? trainingData.value.training_items.filter((item) => item.id !== "strength")
|
? trainingData.value.training_items
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
const pageMounted = ref(false);
|
const pageMounted = ref(false);
|
||||||
@@ -91,8 +92,17 @@ const radarFigureWidthRpx = 448;
|
|||||||
const radarFigureHeightRpx = Math.round(
|
const radarFigureHeightRpx = Math.round(
|
||||||
(radarFigureWidthRpx * radarImageHeight) / radarImageWidth
|
(radarFigureWidthRpx * radarImageHeight) / radarImageWidth
|
||||||
);
|
);
|
||||||
const radarCanvasWidth = Math.round(uni.upx2px(radarFigureWidthRpx));
|
const radarCanvasPixelRatio = Math.max(
|
||||||
const radarCanvasHeight = Math.round(uni.upx2px(radarFigureHeightRpx));
|
1,
|
||||||
|
Number(uni.getDeviceInfo().pixelRatio) || 1
|
||||||
|
);
|
||||||
|
const radarTargetWidth = uni.upx2px(radarFigureWidthRpx);
|
||||||
|
const radarTargetHeight = uni.upx2px(radarFigureHeightRpx);
|
||||||
|
// 画布按设备像素比直接绘制,并让显示尺寸与物理像素严格对应,避免图片二次缩放。
|
||||||
|
const radarCanvasWidth = Math.round(radarTargetWidth * radarCanvasPixelRatio);
|
||||||
|
const radarCanvasHeight = Math.round(radarTargetHeight * radarCanvasPixelRatio);
|
||||||
|
const radarDisplayWidth = radarCanvasWidth / radarCanvasPixelRatio;
|
||||||
|
const radarDisplayHeight = radarCanvasHeight / radarCanvasPixelRatio;
|
||||||
const radarScaleX = radarCanvasWidth / radarImageWidth;
|
const radarScaleX = radarCanvasWidth / radarImageWidth;
|
||||||
const radarScaleY = radarCanvasHeight / radarImageHeight;
|
const radarScaleY = radarCanvasHeight / radarImageHeight;
|
||||||
const radarScale = Math.min(radarScaleX, radarScaleY);
|
const radarScale = Math.min(radarScaleX, radarScaleY);
|
||||||
@@ -103,9 +113,10 @@ const radarPointRadius = Math.max(2.5, 3.5 * radarScale);
|
|||||||
const radarOuterRadiusX = 110.7089 * radarScaleX;
|
const radarOuterRadiusX = 110.7089 * radarScaleX;
|
||||||
const radarOuterRadiusY = 110.7089 * radarScaleY;
|
const radarOuterRadiusY = 110.7089 * radarScaleY;
|
||||||
const radarFigureStyle = {
|
const radarFigureStyle = {
|
||||||
width: `${radarFigureWidthRpx}rpx`,
|
width: `${radarDisplayWidth}px`,
|
||||||
height: `${radarFigureHeightRpx}rpx`,
|
height: `${radarDisplayHeight}px`,
|
||||||
};
|
};
|
||||||
|
let radarRenderGeneration = 0;
|
||||||
|
|
||||||
const formatValue = (value, digits = 1) => {
|
const formatValue = (value, digits = 1) => {
|
||||||
const numberValue = Number(value);
|
const numberValue = Number(value);
|
||||||
@@ -116,7 +127,7 @@ const formatValue = (value, digits = 1) => {
|
|||||||
const getLevelText = (item) => {
|
const getLevelText = (item) => {
|
||||||
if (!item) return "";
|
if (!item) return "";
|
||||||
const level = Number(item.current_level) || 0;
|
const level = Number(item.current_level) || 0;
|
||||||
return item.is_locked ? `Coming! LV${level}` : `当前进度 LV${level} >`;
|
return `当前进度 LV${level} >`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 卡路里字段按需求做 K / W 缩写展示。
|
// 卡路里字段按需求做 K / W 缩写展示。
|
||||||
@@ -156,6 +167,7 @@ const updateRecommendedTraining = () => {
|
|||||||
const trainingId = radarDimensionTrainingIdMap[item?.name];
|
const trainingId = radarDimensionTrainingIdMap[item?.name];
|
||||||
const rawScore = item?.score;
|
const rawScore = item?.score;
|
||||||
if (
|
if (
|
||||||
|
unavailableTrainingIds.has(trainingId) ||
|
||||||
!visibleTrainingIds.includes(trainingId) ||
|
!visibleTrainingIds.includes(trainingId) ||
|
||||||
rawScore === undefined ||
|
rawScore === undefined ||
|
||||||
rawScore === null ||
|
rawScore === null ||
|
||||||
@@ -176,9 +188,10 @@ const updateRecommendedTraining = () => {
|
|||||||
const lowestCandidates = candidates.filter(
|
const lowestCandidates = candidates.filter(
|
||||||
(item) => item.score === lowestScore
|
(item) => item.score === lowestScore
|
||||||
);
|
);
|
||||||
const selectedIndex = Math.floor(Math.random() * lowestCandidates.length);
|
const selectedIndex = Math.floor(Math.random() * lowestCandidates.length); //暂时先不随机,默认使用第一个
|
||||||
|
console.log(candidates, selectedIndex, lowestCandidates)
|
||||||
recommendedTrainingId.value =
|
recommendedTrainingId.value =
|
||||||
lowestCandidates[selectedIndex]?.trainingId || "";
|
lowestCandidates[0]?.trainingId || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRadarPoint = (centerX, centerY, radiusX, radiusY, angle) => ({
|
const getRadarPoint = (centerX, centerY, radiusX, radiusY, angle) => ({
|
||||||
@@ -251,11 +264,14 @@ const drawRadar = () => {
|
|||||||
ctx.draw();
|
ctx.draw();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 小程序 canvas 首次渲染时机不稳定,延后一帧再绘制更稳。
|
// 小程序 Canvas 首次渲染时机不稳定,延后一帧再绘制更稳。
|
||||||
const refreshRadar = async () => {
|
const refreshRadar = async () => {
|
||||||
|
const generation = ++radarRenderGeneration;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (generation === radarRenderGeneration) {
|
||||||
drawRadar();
|
drawRadar();
|
||||||
|
}
|
||||||
}, 30);
|
}, 30);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -303,17 +319,17 @@ const openTrainingRecord = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openTrainingItem = (item = {}) => {
|
const openTrainingItem = (item = {}) => {
|
||||||
const mode = getTrainingMode(item);
|
if (unavailableTrainingIds.has(item.id)) {
|
||||||
if (!mode) return;
|
|
||||||
|
|
||||||
if (item.is_locked) {
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: `${item.name || "训练"} 暂未开放`,
|
title: "功能开发中...",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mode = getTrainingMode(item);
|
||||||
|
if (!mode) return;
|
||||||
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/training/difficulty?mode=${mode}`,
|
url: `/pages/training/difficulty?mode=${mode}`,
|
||||||
});
|
});
|
||||||
@@ -344,7 +360,12 @@ onShow(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container :showBackToGame="true" :bgType="7" bgColor="#050b19">
|
<Container
|
||||||
|
:showBackToGame="true"
|
||||||
|
:bgType="7"
|
||||||
|
bgColor="#050b19"
|
||||||
|
:usePageScroll="true"
|
||||||
|
>
|
||||||
<view class="training-home">
|
<view class="training-home">
|
||||||
<view class="week-grid">
|
<view class="week-grid">
|
||||||
<view
|
<view
|
||||||
@@ -818,10 +839,9 @@ onShow(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.radar-figure {
|
.radar-figure {
|
||||||
position: absolute;
|
position: relative;
|
||||||
left: 50%;
|
|
||||||
top: 54rpx;
|
top: 54rpx;
|
||||||
transform: translateX(-50%);
|
margin: 0 auto;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
startPractiseAPI,
|
startPractiseAPI,
|
||||||
endPractiseAPI,
|
endPractiseAPI,
|
||||||
getPractiseAPI,
|
getPractiseAPI,
|
||||||
|
getTrainingDifficultyListAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
import {
|
import {
|
||||||
connectMatchWebSocket,
|
connectMatchWebSocket,
|
||||||
@@ -52,6 +53,7 @@ const defaultTargetType = 1;
|
|||||||
const total = ref(defaultTotal);
|
const total = ref(defaultTotal);
|
||||||
const practiseResult = ref({});
|
const practiseResult = ref({});
|
||||||
const practiceEndSnapshot = ref({});
|
const practiceEndSnapshot = ref({});
|
||||||
|
const hasNextDifficulty = ref(false);
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const tips = ref("");
|
const tips = ref("");
|
||||||
@@ -64,7 +66,6 @@ const visiblePrecisionTarget = ref({
|
|||||||
randomRingArea: 0,
|
randomRingArea: 0,
|
||||||
});
|
});
|
||||||
const trainingDifficultyStorageKey = "training-selection";
|
const trainingDifficultyStorageKey = "training-selection";
|
||||||
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
|
|
||||||
const useHighlightTest = ref(false);
|
const useHighlightTest = ref(false);
|
||||||
const highlightTestState = ref({
|
const highlightTestState = ref({
|
||||||
blocks: 8,
|
blocks: 8,
|
||||||
@@ -87,6 +88,8 @@ let stopPracticeTask = null;
|
|||||||
let practiceSyncTimer = null;
|
let practiceSyncTimer = null;
|
||||||
let waitingPracticeSync = false;
|
let waitingPracticeSync = false;
|
||||||
let shotPresentationGeneration = 0;
|
let shotPresentationGeneration = 0;
|
||||||
|
let nextDifficultyRequest = null;
|
||||||
|
let nextDifficultyRequestGeneration = 0;
|
||||||
const audioWaiters = new Set();
|
const audioWaiters = new Set();
|
||||||
const shotEffectWaiters = new Map();
|
const shotEffectWaiters = new Map();
|
||||||
const PRACTICE_SYNC_TIMEOUT_MS = 5000;
|
const PRACTICE_SYNC_TIMEOUT_MS = 5000;
|
||||||
@@ -152,6 +155,63 @@ const currentDifficultyLevel = computed(
|
|||||||
getPositiveInteger(trainingParams.value.difficulty)
|
getPositiveInteger(trainingParams.value.difficulty)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resetNextDifficultyState = () => {
|
||||||
|
nextDifficultyRequestGeneration += 1;
|
||||||
|
nextDifficultyRequest = null;
|
||||||
|
hasNextDifficulty.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadNextDifficultyState = (result = {}) => {
|
||||||
|
const requestGeneration = ++nextDifficultyRequestGeneration;
|
||||||
|
const currentLevel =
|
||||||
|
getPositiveInteger(result.difficultyLevel) ||
|
||||||
|
getPositiveInteger(result.difficulty_level) ||
|
||||||
|
currentDifficultyLevel.value;
|
||||||
|
const currentTrainingType = result.trainingType || trainingType.value;
|
||||||
|
|
||||||
|
hasNextDifficulty.value = false;
|
||||||
|
if (!currentTrainingType || !currentLevel) {
|
||||||
|
nextDifficultyRequest = Promise.resolve(false);
|
||||||
|
return nextDifficultyRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDifficultyRequest = getTrainingDifficultyListAPI(currentTrainingType)
|
||||||
|
.then((difficultyResult) => {
|
||||||
|
const levels = Array.isArray(difficultyResult?.list)
|
||||||
|
? difficultyResult.list
|
||||||
|
.filter(
|
||||||
|
(item) => !item?.type || item.type === currentTrainingType
|
||||||
|
)
|
||||||
|
.map((item) => getPositiveInteger(item?.difficulty))
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const maxLevel = Math.max(0, ...levels);
|
||||||
|
const highestCompletedLevel = getPositiveInteger(
|
||||||
|
difficultyResult?.user_levels?.[currentTrainingType]
|
||||||
|
);
|
||||||
|
const latestUnlockedLevel = Math.min(
|
||||||
|
highestCompletedLevel + 1,
|
||||||
|
maxLevel
|
||||||
|
);
|
||||||
|
const canAdvance =
|
||||||
|
currentLevel < maxLevel && latestUnlockedLevel > currentLevel;
|
||||||
|
|
||||||
|
if (requestGeneration === nextDifficultyRequestGeneration) {
|
||||||
|
hasNextDifficulty.value = canAdvance;
|
||||||
|
}
|
||||||
|
return canAdvance;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("training next difficulty load failed", error);
|
||||||
|
if (requestGeneration === nextDifficultyRequestGeneration) {
|
||||||
|
hasNextDifficulty.value = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return nextDifficultyRequest;
|
||||||
|
};
|
||||||
|
|
||||||
// time_limit 缺失或非正数都表示整局不限时。
|
// time_limit 缺失或非正数都表示整局不限时。
|
||||||
const timeLimit = computed(() => getPositiveInteger(practiceInfo.value.timeLimit));
|
const timeLimit = computed(() => getPositiveInteger(practiceInfo.value.timeLimit));
|
||||||
const hasTimeLimit = computed(() => timeLimit.value > 0);
|
const hasTimeLimit = computed(() => timeLimit.value > 0);
|
||||||
@@ -1007,11 +1067,13 @@ const onOver = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const apiResult = (await getPractiseAPI(practiseId.value)) || {};
|
const apiResult = (await getPractiseAPI(practiseId.value)) || {};
|
||||||
|
await (nextDifficultyRequest || loadNextDifficultyState(apiResult));
|
||||||
if (!enterPracticeResult(mergePracticeResult(apiResult))) {
|
if (!enterPracticeResult(mergePracticeResult(apiResult))) {
|
||||||
pageStage.value = pageStages.DISTANCE;
|
pageStage.value = pageStages.DISTANCE;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (Object.keys(practiceEndSnapshot.value).length > 0) {
|
if (Object.keys(practiceEndSnapshot.value).length > 0) {
|
||||||
|
await (nextDifficultyRequest || loadNextDifficultyState(practiceEndSnapshot.value));
|
||||||
enterPracticeResult(mergePracticeResult());
|
enterPracticeResult(mergePracticeResult());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1067,6 +1129,7 @@ async function onReceiveMessage(msg) {
|
|||||||
}
|
}
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
practiceEndSnapshot.value = createPracticeEndSnapshot(msg);
|
practiceEndSnapshot.value = createPracticeEndSnapshot(msg);
|
||||||
|
void loadNextDifficultyState(practiceEndSnapshot.value);
|
||||||
if (
|
if (
|
||||||
trainingType.value === "base" &&
|
trainingType.value === "base" &&
|
||||||
Number(msg.status) === 3 &&
|
Number(msg.status) === 3 &&
|
||||||
@@ -1093,7 +1156,6 @@ function onComplete() {
|
|||||||
invalidateShotPresentations();
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
closePracticeConnection("training-practice-complete");
|
closePracticeConnection("training-practice-complete");
|
||||||
uni.$emit(trainingDifficultyRefreshEvent);
|
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1102,6 +1164,7 @@ async function onRetry() {
|
|||||||
setPracticeAppHideResumable(false);
|
setPracticeAppHideResumable(false);
|
||||||
clearHighlightTestTimer();
|
clearHighlightTestTimer();
|
||||||
useHighlightTest.value = false;
|
useHighlightTest.value = false;
|
||||||
|
resetNextDifficultyState();
|
||||||
practiseId.value = "";
|
practiseId.value = "";
|
||||||
serverAddr.value = "";
|
serverAddr.value = "";
|
||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
@@ -1332,6 +1395,7 @@ onBeforeUnmount(() => {
|
|||||||
:onRetry="onRetry"
|
:onRetry="onRetry"
|
||||||
:trainingType="trainingType"
|
:trainingType="trainingType"
|
||||||
:difficultyLevel="currentDifficultyLevel"
|
:difficultyLevel="currentDifficultyLevel"
|
||||||
|
:hasNextDifficulty="hasNextDifficulty"
|
||||||
:result="practiseResult"
|
:result="practiseResult"
|
||||||
/>
|
/>
|
||||||
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
|
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
|
||||||
|
|||||||
Reference in New Issue
Block a user