update:代码备份

This commit is contained in:
2026-06-01 10:59:26 +08:00
parent b3fc11f1b1
commit f5497c534d
5 changed files with 368 additions and 216 deletions

View File

@@ -0,0 +1,116 @@
<script setup>
import AppBackground from "@/components/AppBackground.vue";
import Avatar from "@/components/Avatar.vue";
import BowTarget from "@./BowTarget.vue";
import ScorePanel from "./ScorePanel.vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const props = defineProps({
show: {
type: Boolean,
default: false,
},
onClose: {
type: Function,
default: () => {},
},
arrows: {
type: Array,
default: () => [],
},
total: {
type: Number,
default: 0,
},
});
</script>
<template>
<view class="container" :style="{ display: show ? 'flex' : 'none' }">
<AppBackground :type="1" />
<view class="header">
<view>
<Avatar :src="user.avatar" :rankLvl="user.rankLvl" :size="45" />
<view>
<text>{{ user.nickName }}</text>
<text>{{ user.lvlName }}</text>
</view>
</view>
<view @click="onClose">
<image src="../static/close-white.png" mode="widthFix" />
</view>
</view>
<view :style="{ width: '100%', marginBottom: '20px' }">
<BowTarget :scores="arrows" />
</view>
<view class="desc">
<text>{{ arrows.length }}</text>
<text>支箭</text>
<text>{{ arrows.reduce((a, b) => a + (b.ring || 0), 0) }}</text>
<text></text>
</view>
<ScorePanel
:completeEffect="false"
:rowCount="total === 12 ? 6 : 9"
:total="total"
:arrows="arrows"
:margin="total === 12 ? 4 : 1"
:fontSize="total === 12 ? 25 : 22"
/>
</view>
</template>
<style scoped>
.container {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: #232323;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
width: calc(100% - 20px);
padding: 10px;
}
.header > view:first-child {
display: flex;
align-items: center;
margin-left: 10px;
}
.header > view:first-child > view:last-child {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 10px;
color: #fff;
}
.header > view:first-child > view:last-child > text:last-child {
font-size: 10px;
background-color: #5f51ff;
padding: 2px 5px;
border-radius: 10px;
margin-top: 5px;
}
.header > view:last-child > image {
width: 40px;
}
.desc {
color: #fff;
margin-bottom: 40px;
}
.desc > text:nth-child(2),
.desc > text:nth-child(4) {
color: #fed847;
}
</style>

View File

@@ -1,8 +1,6 @@
<script setup> <script setup>
import { import {
computed, computed,
getCurrentInstance,
nextTick,
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
ref, ref,
@@ -17,7 +15,6 @@ import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user, device } = storeToRefs(store); const { user, device } = storeToRefs(store);
const instance = getCurrentInstance();
const props = defineProps({ const props = defineProps({
currentRound: { currentRound: {
@@ -278,122 +275,6 @@ const showHighlightCanvas = computed(() => {
return props.totalRound > 0 && currentHighlightAreas.value.length > 0; return props.totalRound > 0 && currentHighlightAreas.value.length > 0;
}); });
const targetImageNaturalSize = ref({
width: 0,
height: 0,
});
const targetLayerRect = ref({
left: 0,
top: 0,
width: 0,
height: 0,
ready: false,
});
const targetHighlightLayerStyle = computed(() => ({
left: `${targetLayerRect.value.left}px`,
top: `${targetLayerRect.value.top}px`,
width: `${targetLayerRect.value.width}px`,
height: `${targetLayerRect.value.height}px`,
}));
const getRectNumber = (value, fallback = 0) => {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : fallback;
};
const getImageContentRect = (imageRect) => {
const naturalWidth = getRectNumber(targetImageNaturalSize.value.width);
const naturalHeight = getRectNumber(targetImageNaturalSize.value.height);
const imageWidth = getRectNumber(imageRect?.width);
const imageHeight = getRectNumber(imageRect?.height);
if (naturalWidth <= 0 || naturalHeight <= 0 || imageWidth <= 0 || imageHeight <= 0) {
return imageRect;
}
const naturalRatio = naturalWidth / naturalHeight;
const boxRatio = imageWidth / imageHeight;
if (naturalRatio > boxRatio) {
const contentHeight = imageWidth / naturalRatio;
return {
left: imageRect.left,
top: imageRect.top + (imageHeight - contentHeight) / 2,
width: imageWidth,
height: contentHeight,
};
}
const contentWidth = imageHeight * naturalRatio;
return {
left: imageRect.left + (imageWidth - contentWidth) / 2,
top: imageRect.top,
width: contentWidth,
height: imageHeight,
};
};
const updateTargetLayerRect = async () => {
await nextTick();
const query = uni.createSelectorQuery().in(instance?.proxy);
query.select(".target").boundingClientRect();
query.select(".target-image").boundingClientRect();
query.exec((rects = []) => {
const stageRect = rects[0];
const imageRect = rects[1];
if (!stageRect || !imageRect || !imageRect.width || !imageRect.height) {
targetLayerRect.value = {
...targetLayerRect.value,
ready: false,
};
return;
}
const contentRect = getImageContentRect(imageRect);
const width = Math.round(getRectNumber(contentRect?.width));
const height = Math.round(getRectNumber(contentRect?.height));
targetLayerRect.value = {
left: getRectNumber(contentRect?.left) - getRectNumber(stageRect.left),
top: getRectNumber(contentRect?.top) - getRectNumber(stageRect.top),
width,
height,
ready: width > 0 && height > 0,
};
});
};
const onTargetImageLoad = (event) => {
const width = getRectNumber(event?.detail?.width);
const height = getRectNumber(event?.detail?.height);
if (width > 0 && height > 0) {
targetImageNaturalSize.value = {
width,
height,
};
}
updateTargetLayerRect();
};
const onWindowResize = () => {
updateTargetLayerRect();
};
watch(
showHighlightCanvas,
(newVal) => {
if (newVal) {
updateTargetLayerRect();
}
}
);
async function onReceiveMessage(message) { async function onReceiveMessage(message) {
if (Array.isArray(message)) return; if (Array.isArray(message)) return;
if (message.type === MESSAGETYPESV2.ShootResult && message.shootData) { if (message.type === MESSAGETYPESV2.ShootResult && message.shootData) {
@@ -418,10 +299,6 @@ async function onReceiveMessage(message) {
onMounted(() => { onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
setTimeout(updateTargetLayerRect, 30);
if (uni.onWindowResize) {
uni.onWindowResize(onWindowResize);
}
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -434,9 +311,6 @@ onBeforeUnmount(() => {
dirTimer.value = null; dirTimer.value = null;
} }
uni.$off("socket-inbox", onReceiveMessage); uni.$off("socket-inbox", onReceiveMessage);
if (uni.offWindowResize) {
uni.offWindowResize(onWindowResize);
}
}); });
</script> </script>
@@ -454,14 +328,10 @@ onBeforeUnmount(() => {
class="target-image" class="target-image"
src="../../../static/bow-target.png" src="../../../static/bow-target.png"
mode="aspectFit" mode="aspectFit"
@load="onTargetImageLoad"
/> />
<TargetCanvas <TargetCanvas
v-if="showHighlightCanvas && targetLayerRect.ready" v-if="showHighlightCanvas"
class="target-highlight-layer" class="target-highlight-layer"
:style="targetHighlightLayerStyle"
:canvasWidth="targetLayerRect.width"
:canvasHeight="targetLayerRect.height"
:coordinateRadius="coordinateRadius" :coordinateRadius="coordinateRadius"
:showCrosshair="false" :showCrosshair="false"
:showQuadrantLabels="false" :showQuadrantLabels="false"
@@ -573,6 +443,10 @@ onBeforeUnmount(() => {
} }
.target-highlight-layer { .target-highlight-layer {
position: absolute; position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;
} }

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import ScreenHint from "@/components/ScreenHint.vue"; import ScreenHint from "./ScreenHint.vue";
import BowData from "@/components/BowData.vue"; import BowData from "./BowData.vue";
import UserUpgrade from "@/components/UserUpgrade.vue"; import UserUpgrade from "@/components/UserUpgrade.vue";
import { directionAdjusts } from "@/constants"; import { directionAdjusts } from "@/constants";
import useStore from "@/store"; import useStore from "@/store";

View File

@@ -0,0 +1,89 @@
<script setup>
import IconButton from "./IconButton.vue";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
onClose: {
type: Function,
default: null,
},
mode: {
type: String,
default: "normal",
},
});
const getContentHeight = () => {
if (props.mode === "tall") return "50vw";
if (props.mode === "square") return "74vw";
return "36vw";
};
</script>
<template>
<view class="container" :style="{ display: show ? 'flex' : 'none' }">
<view class="scale-in" :style="{ height: getContentHeight() }">
<image
v-if="mode === 'normal'"
src="../static/screen-hint-bg.png"
mode="widthFix"
/>
<image
v-if="mode === 'tall'"
src="../static/coach-comment.png"
mode="widthFix"
/>
<image
v-if="mode === 'square'"
src="../static/prompt-bg-square.png"
mode="widthFix"
/>
<image
v-if="mode === 'small'"
src="../static/finish-frame.png"
mode="widthFix"
/>
<slot />
</view>
<IconButton
v-if="!!onClose"
src="../static/close-gold-outline.png"
:width="30"
:onClick="onClose"
/>
</view>
</template>
<style scoped>
.container {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.8);
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 999;
}
.container > view:first-child {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 70vw;
color: #fff;
margin-bottom: 15px;
}
.container > view:first-child > image {
position: absolute;
width: 80vw;
left: -7%;
bottom: -18vw;
z-index: -1;
transform: translateY(-75px);
}
</style>

View File

@@ -29,6 +29,13 @@ const { user } = storeToRefs(store);
const sound = ref(true); const sound = ref(true);
const start = ref(false); 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 scores = ref([]);
const defaultTotal = 12; const defaultTotal = 12;
const defaultShootTime = 120; const defaultShootTime = 120;
@@ -53,6 +60,13 @@ const env = computed(() => {
} }
}); });
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] }]; const defaultHighlightAreas = [{ quadrant: 1, rings: [7] }];
// 临时高亮测试数据:第 N 项对应第 N 箭,每箭展示一个不同区域。 // 临时高亮测试数据:第 N 项对应第 N 箭,每箭展示一个不同区域。
@@ -123,6 +137,7 @@ const runHighlightTest = () => {
clearHighlightTestTimer(); clearHighlightTestTimer();
useHighlightTest.value = true; useHighlightTest.value = true;
practiseResult.value = {}; practiseResult.value = {};
pageStage.value = pageStages.SHOOTING;
start.value = true; start.value = true;
let arrowIndex = 1; let arrowIndex = 1;
@@ -162,21 +177,44 @@ onLoad((options = {}) => {
}); });
const onReady = async () => { const onReady = async () => {
pageStage.value = pageStages.LOADING;
clearHighlightTestTimer(); clearHighlightTestTimer();
useHighlightTest.value = false; useHighlightTest.value = false;
await startPractiseAPI(); try {
scores.value = []; await startPractiseAPI();
start.value = true; practiseResult.value = {};
audioManager.play("练习开始"); 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 () => { const onOver = async () => {
practiseResult.value = await getPractiseAPI(practiseId.value); if (!isShootingStage.value) return;
clearHighlightTestTimer();
pageStage.value = pageStages.LOADING;
start.value = false; 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) { async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) { if (msg.type === MESSAGETYPESV2.ShootResult && isShootingStage.value) {
scores.value = msg.details; scores.value = msg.details;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
// setTimeout(onOver, 1500); // setTimeout(onOver, 1500);
@@ -184,18 +222,25 @@ async function onReceiveMessage(msg) {
} }
function onComplete() { function onComplete() {
pageStage.value = pageStages.LOADING;
start.value = false;
uni.$emit(trainingDifficultyRefreshEvent); uni.$emit(trainingDifficultyRefreshEvent);
uni.navigateBack(); uni.navigateBack();
} }
async function onRetry() { async function onRetry() {
pageStage.value = pageStages.LOADING;
clearHighlightTestTimer(); clearHighlightTestTimer();
useHighlightTest.value = false; useHighlightTest.value = false;
practiseId.value = ""; practiseId.value = "";
practiseResult.value = {}; practiseResult.value = {};
start.value = false; start.value = false;
scores.value = []; scores.value = [];
await createPractice(); try {
await createPractice();
} finally {
pageStage.value = pageStages.DISTANCE;
}
} }
const onClickShare = debounce(async () => { const onClickShare = debounce(async () => {
@@ -241,83 +286,92 @@ onBeforeUnmount(() => {
<template> <template>
<Container <Container
:bgType="!start && !practiseResult.id?9:10" :bgType="isDistanceStage ? 9 : 10"
:showBottom="!start && !scores.length" :showBottom="isDistanceStage"
headerClass="training-practise-header" :scroll="!isShootingStage"
> >
<view> <view class="practise-content">
<TestDistance v-if="!start && !practiseResult.id" /> <TestDistance v-if="isDistanceStage" />
<block v-else> <view v-else-if="isShootingStage" class="shooting-layout">
<ShootProgress <view class="shooting-fixed">
:start="start" <ShootProgress
:onStop="onOver" :start="start"
/> :onStop="onOver"
<view class="user-row"> />
<!-- <Avatar :src="user.avatar" :size="35" /> --> <view class="user-row">
<BubbleTip v-if="showGuide" type="normal2"> <!-- <Avatar :src="user.avatar" :size="35" /> -->
<text>还有两场坚持</text> <BubbleTip v-if="showGuide" type="normal2">
<text>就是胜利💪</text> <text>还有两场坚持</text>
</BubbleTip> <text>就是胜利💪</text>
<!-- <BowPower /> --> </BubbleTip>
</view> <!-- <BowPower /> -->
<BowTarget </view>
:totalRound="start ? total / 4 : 0" <BowTarget
:currentRound="scores.length % 3" :totalRound="start ? total / 4 : 0"
:scores="scores" :currentRound="scores.length % 3"
:showCrosshair="false" :scores="scores"
:highlightAreas="targetHighlightAreas" :showCrosshair="false"
/> :highlightAreas="targetHighlightAreas"
<view v-if="env !== 'release'" class="highlight-test-actions"> />
<button <view v-if="env !== 'release'" class="highlight-test-actions">
class="highlight-test-btn" <button
hover-class="none" class="highlight-test-btn"
@click="runHighlightTest" hover-class="none"
> @click="runHighlightTest"
高亮测试 >
</button> 高亮测试
<button </button>
class="highlight-test-btn" <button
hover-class="none" class="highlight-test-btn"
@click="resetHighlightTest" hover-class="none"
> @click="resetHighlightTest"
重置高亮 >
</button> 重置高亮
</view> </button>
<view class="sound-text-box"> </view>
<button class="sound-btn" hover-class="none" @click="updateSound"> <view class="sound-text-box">
<image <button class="sound-btn" hover-class="none" @click="updateSound">
class="sound-icon" <image
:src="`/static/sound${sound ? '' : '-off'}-yellow.png`" class="sound-icon"
mode="aspectFit" :src="`/static/sound${sound ? '' : '-off'}-yellow.png`"
/> mode="aspectFit"
</button> />
<view class="bat-text-big-box"> </button>
<image <view class="bat-text-big-box">
class="dao-icon" <image
src="../../static/training-difficulty-design/dao-icon.png" class="dao-icon"
mode="widthFix" src="../../static/training-difficulty-design/dao-icon.png"
/> mode="widthFix"
<view class="bat-text-box"> />
<view class="bat-text-small-box"> <view class="bat-text-box">
<view class="text-round-box"> <view class="bat-text-small-box">
<view class="text1">每箭命中9环之上</view> <view class="text-round-box">
<view class="text2">剩余<text class="text2-yellow">3</text></view> <view class="text1">每箭命中9环之上</view>
<view class="text2">剩余<text class="text2-yellow">3</text></view>
</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<ScorePanel2 :arrows="scores" :total="total" /> <scroll-view
<ScoreResult class="score-scroll"
v-if="practiseResult.details" scroll-y
:rowCount="6" :enhanced="true"
:total="total" :show-scrollbar="false"
:onClose="onComplete" >
:onRetry="onRetry" <ScorePanel2 :arrows="scores" :total="total" />
:result="practiseResult" </scroll-view>
/> </view>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas> <ScoreResult
</block> v-else-if="showResult"
:rowCount="6"
:total="total"
:onClose="onComplete"
:onRetry="onRetry"
:result="practiseResult"
/>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
</view> </view>
<template #bottom> <template #bottom>
<view class="btn-box"> <view class="btn-box">
@@ -333,6 +387,30 @@ onBeforeUnmount(() => {
</template> </template>
<style scoped> <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{ .btn-box{
width: 488rpx; width: 488rpx;
height: 234rpx; height: 234rpx;
@@ -360,11 +438,6 @@ onBeforeUnmount(() => {
bottom: -36rpx; bottom: -36rpx;
} }
:deep(.training-practise-header .back-btn) {
position: relative;
z-index: 1000;
}
.highlight-test-actions { .highlight-test-actions {
display: flex; display: flex;
justify-content: center; justify-content: center;