From e6f11d36846f4f6f60ee8b992210deda5ce2ae23 Mon Sep 17 00:00:00 2001
From: zhangyibo95 <690096405@qq.com>
Date: Mon, 27 Jul 2026 16:38:53 +0800
Subject: [PATCH] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E4=B8=AA=E4=BA=BA?=
=?UTF-8?q?=E8=AE=AD=E7=BB=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/Container.vue | 88 +++++++++++++++++++
src/pages/training/components/BowData.vue | 2 +-
src/pages/training/components/ScoreResult.vue | 8 +-
src/pages/training/difficulty.vue | 45 +++++-----
src/pages/training/index.vue | 58 ++++++++----
src/pages/training/practise-one.vue | 68 +++++++++++++-
6 files changed, 221 insertions(+), 48 deletions(-)
diff --git a/src/components/Container.vue b/src/components/Container.vue
index b5ba3f4..3b6e60f 100644
--- a/src/components/Container.vue
+++ b/src/components/Container.vue
@@ -25,6 +25,10 @@ const props = defineProps({
type: Boolean,
default: true,
},
+ usePageScroll: {
+ type: Boolean,
+ default: false,
+ },
isHome: {
type: Boolean,
default: false,
@@ -53,6 +57,14 @@ const props = defineProps({
type: Boolean,
default: true,
},
+ loading: {
+ type: Boolean,
+ default: false,
+ },
+ loadingText: {
+ type: String,
+ default: "",
+ },
});
const isIOS = uni.getDeviceInfo().osName === "ios";
const showHint = ref(false);
@@ -125,9 +137,22 @@ const goCalibration = async () => {
:onBack="onBack"
:whiteBackArrow="whiteBackArrow"
:titleStyle="titleStyle"
+ :style="
+ usePageScroll
+ ? {
+ position: 'sticky',
+ top: capsuleHeight + 'px',
+ zIndex: 10,
+ }
+ : undefined
+ "
/>
+
+
+
{
+
+
+
+
+
+
+ {{ loadingText || "加载中..." }}
+
+
@@ -230,4 +267,55 @@ const goCalibration = async () => {
color: #666;
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;
+}
diff --git a/src/pages/training/components/BowData.vue b/src/pages/training/components/BowData.vue
index cc5fbf6..fadbaf0 100644
--- a/src/pages/training/components/BowData.vue
+++ b/src/pages/training/components/BowData.vue
@@ -115,7 +115,7 @@ const props = defineProps({
}
.header > view:first-child > view:last-child > text:last-child {
font-size: 10px;
- /* background-color: #5f51ff; */
+ background-color: #5f51ff;
padding: 2px 5px;
border-radius: 10px;
margin-top: 5px;
diff --git a/src/pages/training/components/ScoreResult.vue b/src/pages/training/components/ScoreResult.vue
index f7dfe4f..135a62f 100644
--- a/src/pages/training/components/ScoreResult.vue
+++ b/src/pages/training/components/ScoreResult.vue
@@ -35,6 +35,10 @@ const props = defineProps({
type: Number,
default: 0,
},
+ hasNextDifficulty: {
+ type: Boolean,
+ default: false,
+ },
result: {
type: Object,
default: () => ({}),
@@ -297,9 +301,7 @@ const resultRows = computed(() => {
});
});
-const advancesDifficulty = computed(() =>
- ["base", "endurance"].includes(resultTrainingType.value)
-);
+const advancesDifficulty = computed(() => props.hasNextDifficulty);
const primaryText = computed(() =>
advancesDifficulty.value ? "下一难度" : "再来一次"
);
diff --git a/src/pages/training/difficulty.vue b/src/pages/training/difficulty.vue
index 2529501..384d20d 100644
--- a/src/pages/training/difficulty.vue
+++ b/src/pages/training/difficulty.vue
@@ -1,6 +1,6 @@
@@ -768,6 +765,8 @@ onUnload(() => {
:bgType="8"
bgColor="#1c1c23"
:scroll="false"
+ :loading="creatingPractice"
+ loadingText="训练创建中..."
>
diff --git a/src/pages/training/index.vue b/src/pages/training/index.vue
index 20659ce..5791c5f 100644
--- a/src/pages/training/index.vue
+++ b/src/pages/training/index.vue
@@ -17,6 +17,7 @@ const trainingModeRouteMap = {
rhythm: "rhythm",
strength: "power",
};
+const unavailableTrainingIds = new Set(["rhythm", "strength"]);
// 训练项目卡片右侧主图标。
const trainingModeIconMap = {
base_bow:
@@ -79,7 +80,7 @@ const trainingData = ref(createDefaultTrainingData());
const recommendedTrainingId = ref("");
const visibleTrainingItems = computed(() =>
Array.isArray(trainingData.value.training_items)
- ? trainingData.value.training_items.filter((item) => item.id !== "strength")
+ ? trainingData.value.training_items
: []
);
const pageMounted = ref(false);
@@ -91,8 +92,17 @@ const radarFigureWidthRpx = 448;
const radarFigureHeightRpx = Math.round(
(radarFigureWidthRpx * radarImageHeight) / radarImageWidth
);
-const radarCanvasWidth = Math.round(uni.upx2px(radarFigureWidthRpx));
-const radarCanvasHeight = Math.round(uni.upx2px(radarFigureHeightRpx));
+const radarCanvasPixelRatio = Math.max(
+ 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 radarScaleY = radarCanvasHeight / radarImageHeight;
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 radarOuterRadiusY = 110.7089 * radarScaleY;
const radarFigureStyle = {
- width: `${radarFigureWidthRpx}rpx`,
- height: `${radarFigureHeightRpx}rpx`,
+ width: `${radarDisplayWidth}px`,
+ height: `${radarDisplayHeight}px`,
};
+let radarRenderGeneration = 0;
const formatValue = (value, digits = 1) => {
const numberValue = Number(value);
@@ -116,7 +127,7 @@ const formatValue = (value, digits = 1) => {
const getLevelText = (item) => {
if (!item) return "";
const level = Number(item.current_level) || 0;
- return item.is_locked ? `Coming! LV${level}` : `当前进度 LV${level} >`;
+ return `当前进度 LV${level} >`;
};
// 卡路里字段按需求做 K / W 缩写展示。
@@ -156,6 +167,7 @@ const updateRecommendedTraining = () => {
const trainingId = radarDimensionTrainingIdMap[item?.name];
const rawScore = item?.score;
if (
+ unavailableTrainingIds.has(trainingId) ||
!visibleTrainingIds.includes(trainingId) ||
rawScore === undefined ||
rawScore === null ||
@@ -176,9 +188,10 @@ const updateRecommendedTraining = () => {
const lowestCandidates = candidates.filter(
(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 =
- lowestCandidates[selectedIndex]?.trainingId || "";
+ lowestCandidates[0]?.trainingId || "";
};
const getRadarPoint = (centerX, centerY, radiusX, radiusY, angle) => ({
@@ -251,11 +264,14 @@ const drawRadar = () => {
ctx.draw();
};
-// 小程序 canvas 首次渲染时机不稳定,延后一帧再绘制更稳。
+// 小程序 Canvas 首次渲染时机不稳定,延后一帧再绘制更稳。
const refreshRadar = async () => {
+ const generation = ++radarRenderGeneration;
await nextTick();
setTimeout(() => {
- drawRadar();
+ if (generation === radarRenderGeneration) {
+ drawRadar();
+ }
}, 30);
};
@@ -303,17 +319,17 @@ const openTrainingRecord = () => {
};
const openTrainingItem = (item = {}) => {
- const mode = getTrainingMode(item);
- if (!mode) return;
-
- if (item.is_locked) {
+ if (unavailableTrainingIds.has(item.id)) {
uni.showToast({
- title: `${item.name || "训练"} 暂未开放`,
+ title: "功能开发中...",
icon: "none",
});
return;
}
+ const mode = getTrainingMode(item);
+ if (!mode) return;
+
uni.navigateTo({
url: `/pages/training/difficulty?mode=${mode}`,
});
@@ -344,7 +360,12 @@ onShow(async () => {
-
+
{
}
.radar-figure {
- position: absolute;
- left: 50%;
+ position: relative;
top: 54rpx;
- transform: translateX(-50%);
+ margin: 0 auto;
overflow: visible;
}
diff --git a/src/pages/training/practise-one.vue b/src/pages/training/practise-one.vue
index 3068791..efe00cf 100644
--- a/src/pages/training/practise-one.vue
+++ b/src/pages/training/practise-one.vue
@@ -18,6 +18,7 @@ import {
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
+ getTrainingDifficultyListAPI,
} from "@/apis";
import {
connectMatchWebSocket,
@@ -52,6 +53,7 @@ const defaultTargetType = 1;
const total = ref(defaultTotal);
const practiseResult = ref({});
const practiceEndSnapshot = ref({});
+const hasNextDifficulty = ref(false);
const practiseId = ref("");
const showGuide = ref(false);
const tips = ref("");
@@ -64,7 +66,6 @@ const visiblePrecisionTarget = ref({
randomRingArea: 0,
});
const trainingDifficultyStorageKey = "training-selection";
-const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
const useHighlightTest = ref(false);
const highlightTestState = ref({
blocks: 8,
@@ -87,6 +88,8 @@ let stopPracticeTask = null;
let practiceSyncTimer = null;
let waitingPracticeSync = false;
let shotPresentationGeneration = 0;
+let nextDifficultyRequest = null;
+let nextDifficultyRequestGeneration = 0;
const audioWaiters = new Set();
const shotEffectWaiters = new Map();
const PRACTICE_SYNC_TIMEOUT_MS = 5000;
@@ -152,6 +155,63 @@ const currentDifficultyLevel = computed(
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 缺失或非正数都表示整局不限时。
const timeLimit = computed(() => getPositiveInteger(practiceInfo.value.timeLimit));
const hasTimeLimit = computed(() => timeLimit.value > 0);
@@ -1007,11 +1067,13 @@ const onOver = async () => {
try {
const apiResult = (await getPractiseAPI(practiseId.value)) || {};
+ await (nextDifficultyRequest || loadNextDifficultyState(apiResult));
if (!enterPracticeResult(mergePracticeResult(apiResult))) {
pageStage.value = pageStages.DISTANCE;
}
} catch (error) {
if (Object.keys(practiceEndSnapshot.value).length > 0) {
+ await (nextDifficultyRequest || loadNextDifficultyState(practiceEndSnapshot.value));
enterPracticeResult(mergePracticeResult());
return;
}
@@ -1067,6 +1129,7 @@ async function onReceiveMessage(msg) {
}
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
practiceEndSnapshot.value = createPracticeEndSnapshot(msg);
+ void loadNextDifficultyState(practiceEndSnapshot.value);
if (
trainingType.value === "base" &&
Number(msg.status) === 3 &&
@@ -1093,7 +1156,6 @@ function onComplete() {
invalidateShotPresentations();
clearPracticeRuntimeContext();
closePracticeConnection("training-practice-complete");
- uni.$emit(trainingDifficultyRefreshEvent);
uni.navigateBack();
}
@@ -1102,6 +1164,7 @@ async function onRetry() {
setPracticeAppHideResumable(false);
clearHighlightTestTimer();
useHighlightTest.value = false;
+ resetNextDifficultyState();
practiseId.value = "";
serverAddr.value = "";
practiseResult.value = {};
@@ -1332,6 +1395,7 @@ onBeforeUnmount(() => {
:onRetry="onRetry"
:trainingType="trainingType"
:difficultyLevel="currentDifficultyLevel"
+ :hasNextDifficulty="hasNextDifficulty"
:result="practiseResult"
/>