Files
shoot-miniprograms/src/pages/training/practise-one.vue
2026-06-01 10:59:26 +08:00

538 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad } 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 ScoreResult from "./components/ScoreResult.vue";
import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue";
import TestDistance from "./components/TestDistance.vue";
import BubbleTip from "./components/BubbleTip.vue";
import audioManager from "@/audioManager";
import {
createPractiseAPI,
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
} from "@/apis";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
import { MESSAGETYPESV2, roundsName } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const sound = ref(true);
const start = ref(false);
const pageStages = Object.freeze({
DISTANCE: "distance",
SHOOTING: "shooting",
RESULT: "result",
LOADING: "loading",
});
const pageStage = ref(pageStages.DISTANCE);
const scores = ref([]);
const defaultTotal = 12;
const defaultShootTime = 120;
const defaultTargetType = 1;
const total = ref(defaultTotal);
const shootTime = ref(defaultShootTime);
const practiseResult = ref({});
const practiseId = ref("");
const showGuide = ref(false);
const tips = ref("");
const targetType = ref(defaultTargetType);
const trainingParams = ref({});
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
const useHighlightTest = ref(false);
const highlightTestTimer = ref(null);
const env = computed(() => {
try {
return uni.getAccountInfoSync().miniProgram.envVersion;
} catch (error) {
return "release";
}
});
const isDistanceStage = computed(() => pageStage.value === pageStages.DISTANCE);
const isShootingStage = computed(() => pageStage.value === pageStages.SHOOTING);
const hasPractiseResult = computed(() => !!practiseResult.value?.details);
const showResult = computed(
() => pageStage.value === pageStages.RESULT && hasPractiseResult.value
);
const defaultHighlightAreas = [{ quadrant: 1, rings: [7] }];
// 临时高亮测试数据:第 N 项对应第 N 箭,每箭展示一个不同区域。
const highlightTestAreas = [
{ arrowIndex: 1, quadrant: 1, rings: [10] },
{ arrowIndex: 2, quadrant: 2, rings: [9, 10] },
{ arrowIndex: 3, quadrant: 3, rings: [8, 9] },
{ arrowIndex: 4, quadrant: 4, rings: [7, 8] },
{ arrowIndex: 5, quadrant: 1, rings: [6, 7] },
{ arrowIndex: 6, quadrant: 2, rings: [5, 6] },
{ arrowIndex: 7, quadrant: 3, rings: [4, 5] },
{ arrowIndex: 8, quadrant: 4, rings: [3, 4] },
{ arrowIndex: 9, quadrant: 1, rings: "all", scope: "sector" },
{ arrowIndex: 10, quadrant: 2, rings: "all", scope: "sector" },
{ arrowIndex: 11, quadrant: 3, rings: "all", scope: "sector" },
{ arrowIndex: 12, quadrant: 4, rings: "all", scope: "sector" },
];
const targetHighlightAreas = computed(() => {
return useHighlightTest.value ? highlightTestAreas : defaultHighlightAreas;
});
const toRouteNumber = (value, fallback = 0) => {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : fallback;
};
const toPositiveRouteNumber = (value, fallback) => {
const numberValue = toRouteNumber(value, fallback);
return numberValue > 0 ? numberValue : fallback;
};
const createPractice = async () => {
const result = await createPractiseAPI(
total.value,
shootTime.value,
targetType.value
);
if (result) practiseId.value = result.id;
};
const clearHighlightTestTimer = () => {
if (highlightTestTimer.value) {
clearInterval(highlightTestTimer.value);
highlightTestTimer.value = null;
}
};
const buildHighlightTestScore = (index) => ({
playerId: user.value?.id,
ring: 9,
ringX: false,
x: ((index % 4) - 1.5) * 2,
y: (Math.floor(index / 4) - 1) * 2,
angle: null,
});
const setHighlightTestArrow = (arrowIndex) => {
const completedCount = Math.max(arrowIndex - 1, 0);
scores.value = Array.from({ length: completedCount }, (_, index) =>
buildHighlightTestScore(index)
);
};
// 临时测试入口:自动切换第 1 到第 12 箭,让 BowTarget 按当前箭展示不同高亮。
const runHighlightTest = () => {
clearHighlightTestTimer();
useHighlightTest.value = true;
practiseResult.value = {};
pageStage.value = pageStages.SHOOTING;
start.value = true;
let arrowIndex = 1;
setHighlightTestArrow(arrowIndex);
highlightTestTimer.value = setInterval(() => {
if (arrowIndex >= highlightTestAreas.length) {
clearHighlightTestTimer();
return;
}
arrowIndex += 1;
setHighlightTestArrow(arrowIndex);
}, 1000);
};
const resetHighlightTest = () => {
clearHighlightTestTimer();
useHighlightTest.value = false;
scores.value = [];
};
onLoad((options = {}) => {
targetType.value = toPositiveRouteNumber(options.target, defaultTargetType);
total.value = toPositiveRouteNumber(options.arrows, defaultTotal);
shootTime.value = toPositiveRouteNumber(options.time, defaultShootTime);
trainingParams.value = {
type: options.type || "",
difficultyId: options.difficultyId || "",
difficulty: toRouteNumber(options.difficulty),
recordId: options.recordId || "",
hitReq: toRouteNumber(options.hitReq),
totalReq: toRouteNumber(options.totalReq),
blocks: toRouteNumber(options.blocks),
mode: toRouteNumber(options.mode),
};
});
const onReady = async () => {
pageStage.value = pageStages.LOADING;
clearHighlightTestTimer();
useHighlightTest.value = false;
try {
await startPractiseAPI();
practiseResult.value = {};
scores.value = [];
start.value = true;
pageStage.value = pageStages.SHOOTING;
audioManager.play("练习开始");
} catch (error) {
start.value = false;
pageStage.value = pageStages.DISTANCE;
throw error;
}
};
const onOver = async () => {
if (!isShootingStage.value) return;
clearHighlightTestTimer();
pageStage.value = pageStages.LOADING;
start.value = false;
try {
practiseResult.value = (await getPractiseAPI(practiseId.value)) || {};
pageStage.value = hasPractiseResult.value
? pageStages.RESULT
: pageStages.DISTANCE;
} catch (error) {
start.value = true;
pageStage.value = pageStages.SHOOTING;
throw error;
}
};
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult && isShootingStage.value) {
scores.value = msg.details;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
// setTimeout(onOver, 1500);
}
}
function onComplete() {
pageStage.value = pageStages.LOADING;
start.value = false;
uni.$emit(trainingDifficultyRefreshEvent);
uni.navigateBack();
}
async function onRetry() {
pageStage.value = pageStages.LOADING;
clearHighlightTestTimer();
useHighlightTest.value = false;
practiseId.value = "";
practiseResult.value = {};
start.value = false;
scores.value = [];
try {
await createPractice();
} finally {
pageStage.value = pageStages.DISTANCE;
}
}
const onClickShare = debounce(async () => {
await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
await wxShare("shareCanvas");
});
function onAudioEnded(s) {
if (s.indexOf("比赛结束") >= 0) {
onOver()
}
}
const updateSound = () => {
sound.value = !sound.value;
audioManager.setMuted(!sound.value);
};
onMounted(async () => {
// audioManager.play("第一轮");
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("share-image", onClickShare);
uni.$on("audioEnded", onAudioEnded);
await createPractice();
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("share-image", onClickShare);
uni.$off("audioEnded", onAudioEnded);
audioManager.stopAll();
clearHighlightTestTimer();
endPractiseAPI();
});
</script>
<template>
<Container
:bgType="isDistanceStage ? 9 : 10"
:showBottom="isDistanceStage"
:scroll="!isShootingStage"
>
<view class="practise-content">
<TestDistance v-if="isDistanceStage" />
<view v-else-if="isShootingStage" class="shooting-layout">
<view class="shooting-fixed">
<ShootProgress
:start="start"
:onStop="onOver"
/>
<view class="user-row">
<!-- <Avatar :src="user.avatar" :size="35" /> -->
<BubbleTip v-if="showGuide" type="normal2">
<text>还有两场坚持</text>
<text>就是胜利💪</text>
</BubbleTip>
<!-- <BowPower /> -->
</view>
<BowTarget
:totalRound="start ? total / 4 : 0"
:currentRound="scores.length % 3"
:scores="scores"
:showCrosshair="false"
:highlightAreas="targetHighlightAreas"
/>
<view v-if="env !== 'release'" class="highlight-test-actions">
<button
class="highlight-test-btn"
hover-class="none"
@click="runHighlightTest"
>
高亮测试
</button>
<button
class="highlight-test-btn"
hover-class="none"
@click="resetHighlightTest"
>
重置高亮
</button>
</view>
<view class="sound-text-box">
<button class="sound-btn" hover-class="none" @click="updateSound">
<image
class="sound-icon"
:src="`/static/sound${sound ? '' : '-off'}-yellow.png`"
mode="aspectFit"
/>
</button>
<view class="bat-text-big-box">
<image
class="dao-icon"
src="../../static/training-difficulty-design/dao-icon.png"
mode="widthFix"
/>
<view class="bat-text-box">
<view class="bat-text-small-box">
<view class="text-round-box">
<view class="text1">每箭命中9环之上</view>
<view class="text2">剩余<text class="text2-yellow">3</text></view>
</view>
</view>
</view>
</view>
</view>
</view>
<scroll-view
class="score-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="false"
>
<ScorePanel2 :arrows="scores" :total="total" />
</scroll-view>
</view>
<ScoreResult
v-else-if="showResult"
:rowCount="6"
:total="total"
:onClose="onComplete"
:onRetry="onRetry"
:result="practiseResult"
/>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
</view>
<template #bottom>
<view class="btn-box">
<image
class="btn-box-bg"
src="../../static/training-difficulty-design/par-star.png"
mode="widthFix"
/>
<button class="btn" @click="onReady">准备好了开始练习</button>
</view>
</template>
</Container>
</template>
<style scoped>
.practise-content {
height: 100%;
min-height: 0;
}
.shooting-layout {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.shooting-fixed {
flex-shrink: 0;
}
.score-scroll {
flex: 1;
height: 0;
min-height: 0;
overflow: hidden;
}
.btn-box{
width: 488rpx;
height: 234rpx;
position: fixed;
bottom: 130rpx;
left: 50%;
transform: translateX(-50%);
}
.btn-box-bg{
width: 100%;
}
.btn{
width: 330rpx;
height: 70rpx;
line-height: 70rpx;
background: #FED847;
border-radius: 44rpx;
text-align: center;
color: #000000;
font-size: 28rpx;
font-weight: 500;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -36rpx;
}
.highlight-test-actions {
display: flex;
justify-content: center;
margin-top: -24rpx;
position: relative;
z-index: 10;
}
.highlight-test-btn {
width: 150rpx;
height: 48rpx;
line-height: 48rpx;
padding: 0;
border-radius: 24rpx;
background: rgba(0, 0, 0, 0.48);
color: #fed847;
font-size: 22rpx;
}
.highlight-test-btn::after {
border: none;
}
.highlight-test-btn + .highlight-test-btn {
margin-left: 16rpx;
}
.sound-text-box{
height: 125rpx;
padding: 0 56rpx;
display: flex;
align-items: flex-end;
}
.sound-btn {
width: 76rpx;
height: 70rpx;
border: none;
}
.sound-btn::after {
border: none;
}
.sound-icon {
width: 76rpx;
height: 70rpx;
}
.bat-text-big-box{
flex: 1;
position: relative;
}
.dao-icon{
width: 160rpx;
height: 125rpx;
position: absolute;
left: 0;
bottom: 0;
}
.text-round-box{
width: 100%;
}
.bat-text-box{
display: flex;
}
.bat-text-small-box{
background: rgba(0, 0, 0, 0.5);
width: auto;
min-width: 100rpx;
border-radius: 16rpx 60rpx 60rpx 16rpx;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 176rpx;
height: 112rpx;
padding-right: 30rpx;
font-size: 30rpx;
color: #E7BA80;
}
.text1{
font-size: 30rpx;
font-weight: 400;
color: #E7BA80;
line-height: 42rpx;
}
.text2{
color: #FFFFFF;
font-size: 26rpx;
font-weight: 400;
line-height: 36rpx;
}
.text2-yellow{
font-size: 30rpx;
color: #FFD947;
font-weight: 500;
margin: 0 4rpx;
}
</style>