update:个人训练优化
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}秒射击`,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user