From 5357f131de3c74a4bb259aa247ab64dbfc456713 Mon Sep 17 00:00:00 2001
From: zhangyibo95 <690096405@qq.com>
Date: Tue, 22 Sep 2026 15:15:16 +0800
Subject: [PATCH 1/3] =?UTF-8?q?update=EF=BC=9A=E4=BC=98=E5=8C=96=E7=B2=BE?=
=?UTF-8?q?=E5=87=86=E7=A8=B3=E5=AE=9A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
scripts/generate-match-schema.mjs | 5 -
src/components/TargetCanvas.vue | 78 ++-
src/pages/training/components/BowTarget.vue | 10 +-
.../training/components/ShootProgress.vue | 76 +--
.../components/StabilityEnergyTrack.vue | 626 ++++++++++++++++++
src/pages/training/practise-one.vue | 69 +-
.../stability-energy/progress-empty.png | Bin 0 -> 17625 bytes
.../stability-energy/progress-full.png | Bin 0 -> 22043 bytes
.../stability-energy/progress-glow.png | Bin 0 -> 4937 bytes
src/utils/match.min.js | 2 +-
src/utils/matchProtocol.js | 43 +-
11 files changed, 797 insertions(+), 112 deletions(-)
create mode 100644 src/pages/training/components/StabilityEnergyTrack.vue
create mode 100644 src/static/training-difficulty-design/stability-energy/progress-empty.png
create mode 100644 src/static/training-difficulty-design/stability-energy/progress-full.png
create mode 100644 src/static/training-difficulty-design/stability-energy/progress-glow.png
diff --git a/scripts/generate-match-schema.mjs b/scripts/generate-match-schema.mjs
index 47d7a76..f012020 100644
--- a/scripts/generate-match-schema.mjs
+++ b/scripts/generate-match-schema.mjs
@@ -264,11 +264,6 @@ function buildSchema({ messageName, definition, messages, enumNames }) {
}
const repeated = rule === "repeated";
- if (repeated && !isMessage && !["string", "bytes"].includes(kind)) {
- throw new Error(
- `${messageName}.${fieldKey} 是 packed scalar repeated,当前通用解码器尚不支持`
- );
- }
const field = isMessage
? { name: fieldName, kind: "message", type: typeName }
diff --git a/src/components/TargetCanvas.vue b/src/components/TargetCanvas.vue
index fd3478a..7026814 100644
--- a/src/components/TargetCanvas.vue
+++ b/src/components/TargetCanvas.vue
@@ -46,10 +46,10 @@ const props = defineProps({
type: Number,
default: 0,
},
- // 指定环数,1 到 10;无效值表示高亮整个区域。
- activeRing: {
- type: Number,
- default: 0,
+ // 指定一个或多个环数,空数组表示高亮整个区域。
+ activeRings: {
+ type: Array,
+ default: () => [],
},
// 每次变化时以固定低帧数重新展开当前高亮扇区;默认关闭。
highlightRefreshToken: {
@@ -192,6 +192,25 @@ const getPositiveInteger = (value) => {
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
};
+// 过滤、去重并合并连续环,避免 3、4 环同时高亮时绘制中间描边。
+const getActiveRingRanges = (ringCount) => {
+ const rings = [...new Set(
+ (Array.isArray(props.activeRings) ? props.activeRings : [])
+ .map(getPositiveInteger)
+ .filter((ring) => ring >= 1 && ring <= ringCount)
+ )].sort((first, second) => first - second);
+
+ return rings.reduce((ranges, ring) => {
+ const lastRange = ranges[ranges.length - 1];
+ if (lastRange && ring === lastRange.end + 1) {
+ lastRange.end = ring;
+ } else {
+ ranges.push({ start: ring, end: ring });
+ }
+ return ranges;
+ }, []);
+};
+
// 正上方作为第一区起始边界,Canvas 角度递增方向即为顺时针。
const getSectorAngles = (sector, sectorCount) => {
const count = getPositiveInteger(sectorCount);
@@ -256,7 +275,7 @@ const drawTargetRings = (ctx, centerX, centerY, targetRadius, config) => {
}
};
-// 高亮后端指定区域;activeRing 有效时只高亮该区域内的单个环。
+// 高亮后端指定区域;activeRings 非空时高亮该区域内的一个或多个环。
const drawSectorHighlight = (
ctx,
centerX,
@@ -274,32 +293,35 @@ const drawSectorHighlight = (
);
if (safeRevealProgress <= 0) return;
- const ring = getPositiveInteger(props.activeRing);
- const hasActiveRing = ring >= 1 && ring <= config.ringCount;
- const innerRadius = hasActiveRing
- ? targetRadius * ((config.ringCount - ring) / config.ringCount)
- : 0;
- const outerRadius = hasActiveRing
- ? targetRadius * ((config.ringCount + 1 - ring) / config.ringCount)
- : targetRadius;
+ const ringRanges = getActiveRingRanges(config.ringCount);
+ const highlightRanges = ringRanges.length > 0
+ ? ringRanges
+ : [{ start: 1, end: config.ringCount }];
const style = {
...defaultHighlightStyle,
...props.highlightStyle,
};
- drawAnnularSector(
- ctx,
- centerX,
- centerY,
- innerRadius,
- outerRadius,
- angles.startAngle,
- angles.startAngle +
- (angles.endAngle - angles.startAngle) * safeRevealProgress,
- style.color,
- style.strokeColor,
- Math.max(1, targetRadius * style.lineWidthRatio)
- );
+ highlightRanges.forEach((range) => {
+ const innerRadius =
+ targetRadius * ((config.ringCount - range.end) / config.ringCount);
+ const outerRadius =
+ targetRadius * ((config.ringCount + 1 - range.start) / config.ringCount);
+
+ drawAnnularSector(
+ ctx,
+ centerX,
+ centerY,
+ innerRadius,
+ outerRadius,
+ angles.startAngle,
+ angles.startAngle +
+ (angles.endAngle - angles.startAngle) * safeRevealProgress,
+ style.color,
+ style.strokeColor,
+ Math.max(1, targetRadius * style.lineWidthRatio)
+ );
+ });
};
// 从正上方开始顺时针绘制所有区域边界。
@@ -420,7 +442,7 @@ const getDrawKey = (width, height) => {
showRingLabels: props.showRingLabels,
sectorCount: props.sectorCount,
activeSector: props.activeSector,
- activeRing: props.activeRing,
+ activeRings: props.activeRings,
showSectorLabels: props.showSectorLabels,
targetStyleConfig: props.targetStyleConfig,
crosshairStyle: props.crosshairStyle,
@@ -591,7 +613,7 @@ watch(
props.showRingLabels,
props.sectorCount,
props.activeSector,
- props.activeRing,
+ props.activeRings,
props.highlightRefreshToken,
props.showSectorLabels,
props.highlightOnly,
diff --git a/src/pages/training/components/BowTarget.vue b/src/pages/training/components/BowTarget.vue
index 27311fa..e2771c9 100644
--- a/src/pages/training/components/BowTarget.vue
+++ b/src/pages/training/components/BowTarget.vue
@@ -84,9 +84,9 @@ const props = defineProps({
type: Number,
default: 0,
},
- activeRing: {
- type: Number,
- default: 0,
+ activeRings: {
+ type: Array,
+ default: () => [],
},
showSectorLabels: {
type: Boolean,
@@ -548,7 +548,7 @@ onBeforeUnmount(() => {
:highlightOnly="true"
:sectorCount="sectorCount"
:activeSector="activeSector"
- :activeRing="activeRing"
+ :activeRings="activeRings"
:highlightRefreshToken="highlightRefreshToken"
:showSectorLabels="showSectorLabels"
/>
@@ -877,7 +877,7 @@ onBeforeUnmount(() => {
width: calc(100% - 20px);
padding: 0 10px;
display: flex;
- margin-top: -40px;
+ margin-top: 15px;
justify-content: flex-end;
}
.footer > image {
diff --git a/src/pages/training/components/ShootProgress.vue b/src/pages/training/components/ShootProgress.vue
index e2ce352..47c6d18 100644
--- a/src/pages/training/components/ShootProgress.vue
+++ b/src/pages/training/components/ShootProgress.vue
@@ -73,11 +73,7 @@ const props = defineProps({
type: Number,
default: 0,
},
- energyPercent: {
- type: Number,
- default: 0,
- },
- energyReqPercent: {
+ energyCostPerSec: {
type: Number,
default: 0,
},
@@ -187,17 +183,10 @@ const progressPercent = computed(() => {
return Math.max(0, Math.min(100, (remain.value / props.total) * 100));
});
-const stabilityEnergyPercent = computed(() =>
- Math.max(0, Math.min(100, Number(props.energyPercent) || 0))
-);
-const stabilityEnergyReqPercent = computed(() =>
- Math.max(0, Math.min(100, Number(props.energyReqPercent) || 0))
-);
-const stabilityEnergyReached = computed(
- () =>
- stabilityEnergyReqPercent.value > 0 &&
- stabilityEnergyPercent.value >= stabilityEnergyReqPercent.value
-);
+const stabilityEnergyCostText = computed(() => {
+ const value = Math.max(0, Number(props.energyCostPerSec) || 0);
+ return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(2)));
+});
const rhythmMarkerPercent = computed(() => {
if (!validRhythmRoundTime.value) return 0;
@@ -568,22 +557,18 @@ onBeforeUnmount(() => {
"
>
- {{ remain }}秒
-
+
+ 每秒失去{{ stabilityEnergyCostText }}点能量
+
+
-
-
- {{ Math.round(stabilityEnergyPercent) }}%
-
+ 剩余{{ remain }}秒
@@ -811,17 +796,17 @@ onBeforeUnmount(() => {
margin: 32rpx 84rpx 0;
}
-.stability-progress__time {
+.stability-progress__cost {
display: block;
margin-bottom: 20rpx;
color: #ffffff;
- font-size: 34rpx;
+ font-size: 32rpx;
font-weight: 500;
- line-height: 48rpx;
+ line-height: 44rpx;
text-align: center;
}
-.stability-progress__track {
+.stability-progress__countdown {
position: relative;
width: 100%;
height: 24rpx;
@@ -830,38 +815,23 @@ onBeforeUnmount(() => {
background: #444444;
}
-.stability-progress__fill {
+.stability-progress__countdown-fill {
position: absolute;
top: 0;
bottom: 0;
left: 0;
border-radius: 18rpx;
- background: linear-gradient(90deg, #87f1df 0%, #5ba8e8 100%);
- transition: width 240ms linear, background 240ms ease;
+ background: linear-gradient(133deg, #ffd19a 0%, #a17636 100%);
}
-.stability-progress__fill--reached {
- background: linear-gradient(90deg, #a5df62 0%, #61c787 100%);
-}
-
-.stability-progress__marker {
- position: absolute;
- top: 0;
- bottom: 0;
- z-index: 2;
- width: 4rpx;
- transform: translateX(-2rpx);
- background: rgba(26, 24, 22, 0.92);
-}
-
-.stability-progress__value {
+.stability-progress__countdown-text {
position: absolute;
inset: 0;
- z-index: 3;
+ z-index: 2;
display: flex;
align-items: center;
justify-content: center;
- color: #eefaff;
+ color: #fff7de;
font-size: 18rpx;
line-height: 24rpx;
text-align: center;
diff --git a/src/pages/training/components/StabilityEnergyTrack.vue b/src/pages/training/components/StabilityEnergyTrack.vue
new file mode 100644
index 0000000..2828a4c
--- /dev/null
+++ b/src/pages/training/components/StabilityEnergyTrack.vue
@@ -0,0 +1,626 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/training/practise-one.vue b/src/pages/training/practise-one.vue
index 65f01f4..f319576 100644
--- a/src/pages/training/practise-one.vue
+++ b/src/pages/training/practise-one.vue
@@ -3,6 +3,7 @@ import { computed, ref, onMounted, onBeforeUnmount } from "vue";
import { onHide, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ShootProgress from "./components/ShootProgress.vue";
+import StabilityEnergyTrack from "./components/StabilityEnergyTrack.vue";
import BowTarget from "./components/BowTarget.vue";
import ScorePanel2 from "@/components/TrainingScorePanel.vue";
import ScoreResult from "./components/ScoreResult.vue";
@@ -105,14 +106,14 @@ const rhythmTargetArrows = ref(0);
// 服务端状态立即落到 practiceInfo,精准训练的目标区域单独延迟展示。
const visiblePrecisionTarget = ref({
randomBlock: 0,
- randomRingArea: 0,
+ randomRingAreas: [],
});
const trainingDifficultyStorageKey = "training-selection";
const useHighlightTest = ref(false);
const highlightTestState = ref({
blocks: 8,
randomBlock: 1,
- randomRingArea: 0,
+ randomRingAreas: [],
});
const highlightTestTimer = ref(null);
const serverAddr = ref("");
@@ -195,9 +196,23 @@ const getPositiveInteger = (value) => {
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
};
+const normalizePrecisionRingAreas = (source = {}) => {
+ const multipleRings = Array.isArray(source.randomRingAreas)
+ ? [...new Set(source.randomRingAreas.map(getPositiveInteger))].filter(
+ (ring) => ring >= 1 && ring <= 10
+ )
+ : [];
+
+ if (multipleRings.length > 0) return multipleRings;
+
+ const singleRing = getPositiveInteger(source.randomRingArea);
+ return singleRing >= 1 && singleRing <= 10 ? [singleRing] : [];
+};
+
const getPrecisionTargetSnapshot = (source = {}) => ({
randomBlock: getPositiveInteger(source.randomBlock),
- randomRingArea: getPositiveInteger(source.randomRingArea),
+ // 多环字段优先;后端仅在单环时继续下发旧单值字段。
+ randomRingAreas: normalizePrecisionRingAreas(source),
});
const applyVisiblePrecisionTarget = (source = {}) => {
@@ -345,6 +360,15 @@ const stabilityEnergyPercent = computed(() => {
(stabilityCurrentEnergy.value / stabilityScoreSlot.value) * 100
);
});
+const stabilityEnergyCostPerSec = computed(() =>
+ Math.max(
+ 0,
+ getPracticeNumber(
+ practiceInfo.value.energyCostPerSec,
+ trainingParams.value.energyCostPerSec
+ )
+ )
+);
const initializeRhythmFirstRoundCountdown = () => {
rhythmHasActiveServerAnchor.value = false;
@@ -384,14 +408,13 @@ const precisionRandomBlock = computed(() => {
return block <= precisionBlocks.value ? block : 0;
});
-const precisionRandomRingArea = computed(() => {
- const ring = getPositiveInteger(
+const precisionRandomRingAreas = computed(() =>
+ normalizePrecisionRingAreas(
useHighlightTest.value
- ? highlightTestState.value.randomRingArea
- : visiblePrecisionTarget.value.randomRingArea
- );
- return ring >= 1 && ring <= 10 ? ring : 0;
-});
+ ? highlightTestState.value
+ : visiblePrecisionTarget.value
+ )
+);
// 只展示后端进度,不在前端重复判断训练是否完成。
const trainingCopy = computed(() => {
@@ -555,6 +578,7 @@ const practiceInfoFields = [
"blocks",
"randomBlock",
"randomRingArea",
+ "randomRingAreas",
"roundTime",
"shootTime",
"shootWindowStart",
@@ -746,6 +770,9 @@ const syncPracticeInfo = (message = {}) => {
if (!Object.prototype.hasOwnProperty.call(message, "randomRingArea")) {
nextInfo.randomRingArea = 0;
}
+ if (!Object.prototype.hasOwnProperty.call(message, "randomRingAreas")) {
+ nextInfo.randomRingAreas = [];
+ }
}
if (Object.keys(nextInfo).length === 0) return;
@@ -1290,7 +1317,7 @@ const clearHighlightTestTimer = () => {
}
};
-// 开发环境测试入口:依次切换 8 个顺时针区域,偶数区域只高亮指定环。
+// 开发环境测试入口:依次切换 8 个顺时针区域,偶数区域同时高亮两个环。
const runHighlightTest = () => {
clearHighlightTestTimer();
useHighlightTest.value = true;
@@ -1302,7 +1329,7 @@ const runHighlightTest = () => {
highlightTestState.value = {
blocks: 8,
randomBlock: block,
- randomRingArea: 0,
+ randomRingAreas: [],
};
highlightTestTimer.value = setInterval(() => {
@@ -1315,7 +1342,10 @@ const runHighlightTest = () => {
highlightTestState.value = {
blocks: 8,
randomBlock: block,
- randomRingArea: block % 2 === 0 ? Math.min(block, 10) : 0,
+ randomRingAreas:
+ block % 2 === 0
+ ? [Math.min(block, 9), Math.min(block + 1, 10)]
+ : [],
};
}, 1000);
};
@@ -1326,7 +1356,7 @@ const resetHighlightTest = () => {
highlightTestState.value = {
blocks: 8,
randomBlock: 1,
- randomRingArea: 0,
+ randomRingAreas: [],
};
scores.value = [];
};
@@ -1790,8 +1820,7 @@ onBeforeUnmount(() => {
:inShootWindow="rhythmInShootWindow"
:serverTimestamp="rhythmCountdownTimestamp"
:hitReq="rhythmHitReq"
- :energyPercent="stabilityEnergyPercent"
- :energyReqPercent="stabilityEnergyReqPercent"
+ :energyCostPerSec="stabilityEnergyCostPerSec"
:isVip="isVip"
:isSvip="isSvip"
:externalShootResultAudio="
@@ -1819,11 +1848,15 @@ onBeforeUnmount(() => {
:showCrosshair="false"
:sectorCount="precisionBlocks"
:activeSector="precisionRandomBlock"
- :activeRing="precisionRandomRingArea"
+ :activeRings="precisionRandomRingAreas"
:highlightRefreshToken="precisionTargetRefreshToken"
stable-shot-effect
@shot-effect-complete="onShotEffectComplete"
/>
+