update:个人训练优化

This commit is contained in:
2026-08-14 17:13:23 +08:00
parent d662db9465
commit e7f073ed7c
25 changed files with 481 additions and 344 deletions
@@ -44,6 +44,10 @@ const props = defineProps({
type: Number,
default: 0,
},
highlightRefreshToken: {
type: Number,
default: 0,
},
mode: {
type: String,
default: "solo", // solo 单排,team 双排
@@ -530,6 +534,7 @@ onBeforeUnmount(() => {
:sectorCount="sectorCount"
:activeSector="activeSector"
:activeRing="activeRing"
:highlightRefreshToken="highlightRefreshToken"
:showSectorLabels="showSectorLabels"
/>
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
@@ -1,108 +0,0 @@
<script setup>
import { computed } from "vue";
const props = defineProps({
arrows: {
type: Array,
default: () => [],
},
total: {
type: Number,
default: 0,
},
});
const getDisplayText = (arrow = {}) => {
if (!arrow) return "";
if (!arrow.ring) return "0";
return arrow.ringX ? "X" : String(arrow.ring);
};
const isLowScore = (arrow = {}) => {
if (!arrow || arrow.ringX) return false;
return Number(arrow.ring) < 6;
};
const displayArrows = computed(() => {
const list = [...props.arrows];
// total 是达标箭数,不是实际射箭上限;训练中始终预留下一箭空框。
list.push(null);
return list;
});
</script>
<template>
<view v-if="displayArrows.length" class="score-panel">
<view class="score-grid">
<view
v-for="(arrow, index) in displayArrows"
:key="index"
class="score-card"
>
<image class="score-card-bg" :src="isLowScore(arrow)?'https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/block-gray.png':'https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/block-gold.png'"></image>
<text
class="score-value"
:class="{ 'score-value--low': isLowScore(arrow) }"
>
{{ getDisplayText(arrow) }}
</text>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.score-panel {
width: 100%;
padding: 30rpx 40rpx 0 40rpx;
box-sizing: border-box;
}
.score-grid {
display: flex;
flex-wrap: wrap;
}
.score-card {
position: relative;
width: 100rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-right: 14rpx;
margin-bottom: 14rpx;
}
.score-card:nth-child(6n) {
margin-right: 0;
}
.score-card-bg {
position: absolute;
top: 0;
left: 0;
width: 100rpx;
height: 56rpx;
}
.score-value {
position: relative;
z-index: 1;
min-width: 28rpx;
text-align: center;
font-size: 34rpx;
line-height: 1;
font-weight: 700;
font-style: italic;
color: #f6e3b2;
text-shadow: 0 2rpx 0 rgba(36, 36, 48, 0.5);
margin-left: -10rpx;
}
.score-value--low {
color: #cfcfcf;
text-shadow: 0 2rpx 0 rgba(0, 0, 0, 0.5);
}
</style>
@@ -2,7 +2,7 @@
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import audioManager from "@/audioManager";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText } from "@/util";
import { getDirectionText, getInvalidShotAudioKey } from "@/util";
import Avatar from "@/components/Avatar.vue";
import useStore from "@/store";
@@ -241,7 +241,7 @@ async function onReceiveMessage(msg) {
title: "距离不足,无效",
icon: "none",
});
audioManager.play("射击无效");
audioManager.play(getInvalidShotAudioKey(msg.shootData));
}
}
@@ -55,8 +55,13 @@ onBeforeUnmount(() => {
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.TestDistance) {
distance.value = Number((msg.shootData.distance / 100).toFixed(2));
if (distance.value >= 5) audioManager.play("距离合格");
const rawDistance = Number(msg.shootData?.distance);
distance.value = Number.isFinite(rawDistance)
? Number((rawDistance / 100).toFixed(2))
: 0;
if (rawDistance === 0) {
audioManager.play("未发现靶纸,请瞄准靶纸射箭");
} else if (distance.value >= 5) audioManager.play("距离合格");
else audioManager.play("距离不足");
}
}
@@ -94,7 +99,7 @@ onBeforeUnmount(() => {
模拟射箭
</button>
<view class="warnning-text">
<view class="target-tip">当前靶<text class="text-yellow">{{ targetType }}cm</text>全环靶,请更换靶子</view>
<view class="target-tip">当前靶<text class="text-yellow">{{ targetType }}cm</text>全环靶</view>
<block v-if="distance > 0">
<text>当前距离<text class="text-yellow">{{ distance }}</text></text>
<text v-if="distance >= 5">已达到距离要求</text>
+12 -15
View File
@@ -107,30 +107,27 @@ const createDifficultySummary = (item = {}) => {
const timeLimit = toNumber(item.time_limit);
const hitReq = toNumber(item.hit_req);
const totalReq = toNumber(item.total_req);
const blocks = toNumber(item.blocks);
const promoteCnt = toNumber(item.promote_cnt);
const timeText = timeLimit > 0 ? `${timeLimit}秒内完成` : "不限时完成";
const shootingTimeText =
timeLimit > 0 ? `${timeLimit}秒内进行射箭` : "不限时进行射箭";
const enduranceTimeText =
timeLimit > 0
? `${timeLimit}秒内完成${arrows}`
: `不限时完成${arrows}`;
const promoteText = promoteCnt > 0 ? `完成${promoteCnt}次晋级` : "";
const summaryMap = {
base: [
desc || (hitReq > 0 ? `每箭命中${hitReq}环以上` : "上靶即可"),
[`${arrows}`, promoteText].filter(Boolean).join(" · "),
shootingTimeText,
`需要有${arrows}命中${hitReq}环内`,
],
endurance: [
desc || `${timeText}${arrows}`,
[`累计${totalReq}`, promoteText].filter(Boolean).join(" · "),
enduranceTimeText,
`累计环数大于${totalReq}`,
],
precision: [
desc || `命中${blocks}个指定区域`,
[
`${arrows}`,
timeText,
getDifficultyModeText(item.mode),
promoteText,
]
.filter(Boolean)
.join(" · "),
shootingTimeText,
`需要有${arrows}箭命中高亮区域`,
],
rhythm: [
desc || `间隔${timeLimit}秒射击`,
+9 -9
View File
@@ -64,8 +64,8 @@ const createDefaultTrainingData = () => ({
stats: {
total_training_days: 0,
total_arrows: 0,
hit_rate: 0,
endurance_shoot_speed: 0,
target_rate: 0,
ten_ring_count: 0,
total_calories: 0,
},
beat_percent: 0,
@@ -286,8 +286,8 @@ const loadPersonalTrainingData = async () => {
stats: {
total_training_days: result?.stats?.total_training_days ?? 0,
total_arrows: result?.stats?.total_arrows ?? 0,
hit_rate: result?.stats?.hit_rate ?? 0,
endurance_shoot_speed: result?.stats?.endurance_shoot_speed ?? 0,
target_rate: result?.stats?.target_rate ?? 0,
ten_ring_count: result?.stats?.ten_ring_count ?? 0,
total_calories: result?.stats?.total_calories ?? 0,
},
beat_percent: result?.beat_percent ?? 0,
@@ -431,26 +431,26 @@ onShow(async () => {
<view class="stats-value-row">
<view class="stats-value-group">
<text class="stats-value">
{{ formatValue(trainingData.stats.hit_rate) }}
{{ formatValue(trainingData.stats.target_rate) }}
</text>
<text class="stats-unit">%</text>
<view class="stats-value-decoration"></view>
</view>
</view>
<text class="stats-label">命中</text>
<text class="stats-label">上靶</text>
</view>
<view class="stats-item">
<view class="stats-value-row">
<view class="stats-value-group">
<text class="stats-value">
{{ formatValue(trainingData.stats.endurance_shoot_speed, 0) }}
{{ formatValue(trainingData.stats.ten_ring_count, 0) }}
</text>
<text class="stats-unit">/分钟</text>
<text class="stats-unit"></text>
<view class="stats-value-decoration"></view>
</view>
</view>
<text class="stats-label">耐力射击</text>
<text class="stats-label">10环数</text>
</view>
<view class="stats-item">
+78 -33
View File
@@ -4,7 +4,7 @@ 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";
import ScorePanel2 from "./components/ScorePanel2.vue";
import ScorePanel2 from "@/components/TrainingScorePanel.vue";
import ScoreResult from "./components/ScoreResult.vue";
import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue";
@@ -48,6 +48,8 @@ const pageStage = ref(pageStages.LOADING);
const scores = ref([]);
// 只在实时 ShootResult 新增一箭时递增,避免同步快照重播飞箭特效。
const shotEffectToken = ref(0);
// 可见区域每次正式提交都递增,同一区域连续刷新也能触发动效。
const precisionTargetRefreshToken = ref(0);
const defaultTotal = 12;
const defaultTargetType = 1;
const total = ref(defaultTotal);
@@ -253,17 +255,23 @@ const trainingCopy = computed(() => {
practiceInfo.value.hitReq,
trainingParams.value.hitReq
);
const arrowsLeft = getPracticeNumber(
practiceInfo.value.arrowsLeft,
total.value
const targetArrows = getPositiveInteger(total.value);
const arrowsLeft = Math.min(
targetArrows,
Math.max(
0,
getPracticeNumber(practiceInfo.value.arrowsLeft, targetArrows)
)
);
const completedArrows = targetArrows - arrowsLeft;
return {
title: `每箭命中${hitReq}环之上`,
inline: true,
details: [
{ text: "剩余" },
{ text: arrowsLeft, highlight: true },
{ text: "箭达到条件" },
{ text: "计时结束前需要有" },
{ text: `(${completedArrows}/${targetArrows})`, highlight: true },
{ text: "箭命中" },
{ text: `${hitReq}`, highlight: true },
],
};
}
@@ -281,35 +289,33 @@ const trainingCopy = computed(() => {
const currentRings = getPracticeNumber(practiceInfo.value.currentRings);
return {
title: `完成${targetArrows}箭并累计${targetRings}`,
inline: true,
details: [
{ text: "完成" },
{ text: currentArrows, highlight: true },
{ text: "箭累计" },
{ text: currentRings, highlight: true },
{ text: "计时结束前完成" },
{ text: `(${currentArrows}/${targetArrows})`, highlight: true },
{ text: "箭累计命中" },
{ text: `(${currentRings}/${targetRings})`, highlight: true },
{ text: "环" },
],
};
}
if (trainingType.value === "precision") {
const block = precisionRandomBlock.value;
const ring = precisionRandomRingArea.value;
const arrowsLeft = getPracticeNumber(
practiceInfo.value.arrowsLeft,
total.value
const targetArrows = getPositiveInteger(total.value);
const arrowsLeft = Math.min(
targetArrows,
Math.max(
0,
getPracticeNumber(practiceInfo.value.arrowsLeft, targetArrows)
)
);
const title = block
? ring
? `请命中区域${block}${ring}`
: `请命中区域${block}`
: "等待目标区域";
const completedArrows = targetArrows - arrowsLeft;
return {
title,
inline: true,
details: [
{ text: "剩余" },
{ text: arrowsLeft, highlight: true },
{ text: "射箭命中高亮区域需完成" },
{ text: `(${completedArrows}/${targetArrows})`, highlight: true },
{ text: "箭" },
],
};
@@ -563,7 +569,10 @@ const commitPrecisionTargetAfterPresentation = async ({
) {
return;
}
visiblePrecisionTarget.value = target;
applyVisiblePrecisionTarget(target);
if (precisionRandomBlock.value > 0) {
precisionTargetRefreshToken.value += 1;
}
};
const createPracticeEndSnapshot = (message = {}) => {
@@ -1329,6 +1338,7 @@ onBeforeUnmount(() => {
:sectorCount="precisionBlocks"
:activeSector="precisionRandomBlock"
:activeRing="precisionRandomRingArea"
:highlightRefreshToken="precisionTargetRefreshToken"
:showSectorLabels="precisionBlocks > 0"
stable-shot-effect
@shot-effect-complete="onShotEffectComplete"
@@ -1349,7 +1359,7 @@ onBeforeUnmount(() => {
重置高亮
</button>
</view> -->
<view class="sound-text-box">
<view class="sound-row">
<button class="sound-btn" hover-class="none" @click="updateSound">
<image
class="sound-icon"
@@ -1357,6 +1367,8 @@ onBeforeUnmount(() => {
mode="aspectFit"
/>
</button>
</view>
<view class="sound-text-box">
<view class="bat-text-big-box">
<image
class="dao-icon"
@@ -1366,14 +1378,23 @@ onBeforeUnmount(() => {
<view v-if="trainingCopy" class="bat-text-box">
<view class="bat-text-small-box">
<view class="text-round-box">
<view class="text1">{{ trainingCopy.title }}</view>
<view class="text2">
<view v-if="trainingCopy.inline" class="training-copy-inline">
<text
v-for="(part, index) in trainingCopy.details"
:key="index"
:class="{ 'text2-yellow': part.highlight }"
>{{ part.text }}</text>
</view>
<view v-else>
<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>
</view>
@@ -1386,7 +1407,11 @@ onBeforeUnmount(() => {
:enhanced="true"
:show-scrollbar="false"
>
<ScorePanel2 :arrows="scores" :total="total" />
<ScorePanel2
:arrows="scores"
:total="total"
:trainingType="trainingType"
/>
</scroll-view>
</view>
<ScoreResult
@@ -1494,6 +1519,12 @@ onBeforeUnmount(() => {
margin-left: 16rpx;
}
.sound-row {
height: 70rpx;
padding: 0 56rpx;
display: flex;
align-items: center;
}
.sound-text-box{
height: 125rpx;
padding: 0 56rpx;
@@ -1503,6 +1534,9 @@ onBeforeUnmount(() => {
.sound-btn {
width: 76rpx;
height: 70rpx;
padding: 0;
margin: 0;
background: transparent;
border: none;
}
@@ -1515,7 +1549,7 @@ onBeforeUnmount(() => {
height: 70rpx;
}
.bat-text-big-box{
flex: 1;
width: 100%;
position: relative;
}
.dao-icon{
@@ -1530,11 +1564,13 @@ onBeforeUnmount(() => {
}
.bat-text-box{
display: flex;
width: 100%;
}
.bat-text-small-box{
background: rgba(0, 0, 0, 0.5);
width: auto;
width: 100%;
min-width: 100rpx;
box-sizing: border-box;
border-radius: 16rpx 60rpx 60rpx 16rpx;
display: flex;
flex-direction: column;
@@ -1545,6 +1581,15 @@ onBeforeUnmount(() => {
font-size: 30rpx;
color: #E7BA80;
}
.training-copy-inline {
width: 100%;
color: #FFFFFF;
font-size: 26rpx;
font-weight: 400;
line-height: 40rpx;
white-space: normal;
word-break: break-all;
}
.text1{
font-size: 30rpx;
font-weight: 400;