update:对接个人训练改版
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
<script setup>
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import BowShotEffect from "@/components/BowShotEffect.vue";
|
||||
import PointSwitcher from "@/components/PointSwitcher.vue";
|
||||
import TargetCanvas from "@/components/TargetCanvas.vue";
|
||||
|
||||
@@ -33,6 +36,14 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isSvip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shotEffectToken: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: "solo", // solo 单排,team 双排
|
||||
@@ -57,23 +68,22 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showQuadrantLabels: {
|
||||
sectorCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
activeSector: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
activeRing: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showSectorLabels: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
quadrantLabels: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
1: "1",
|
||||
2: "2",
|
||||
3: "3",
|
||||
4: "4",
|
||||
}),
|
||||
},
|
||||
highlightAreas: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const pMode = ref(true);
|
||||
@@ -85,6 +95,12 @@ const timer = ref(null);
|
||||
const dirTimer = ref(null);
|
||||
const angle = ref(null);
|
||||
const circleColor = ref("");
|
||||
const shotEffect = ref(null);
|
||||
const hiddenLatestKey = ref("");
|
||||
const targetShaking = ref(false);
|
||||
const targetSize = ref({ width: 0, height: 0 });
|
||||
const shakeTimer = ref(null);
|
||||
const instance = getCurrentInstance();
|
||||
const ROUND_TIP_OFFSET_Y = -32;
|
||||
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
||||
|
||||
@@ -162,6 +178,13 @@ function getHitStyle(shot) {
|
||||
};
|
||||
}
|
||||
|
||||
function getSvipHitBgStyle(shot) {
|
||||
const radius = currentHitRadiusPx.value;
|
||||
const point = getShotPoint(shot);
|
||||
|
||||
return getTargetPositionStyle(point, radius);
|
||||
}
|
||||
|
||||
function getRoundTipStyle(shot) {
|
||||
const point = getShotPoint(shot, true);
|
||||
return getTargetPositionStyle(
|
||||
@@ -180,15 +203,116 @@ function getExperienceTipStyle(shot) {
|
||||
);
|
||||
}
|
||||
|
||||
function clearTipTimer() {
|
||||
if (!timer.value) return;
|
||||
clearTimeout(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
|
||||
function showShotTip(shot) {
|
||||
clearTipTimer();
|
||||
latestOne.value = shot;
|
||||
timer.value = setTimeout(() => {
|
||||
latestOne.value = null;
|
||||
timer.value = null;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function hasShotPoint(shot) {
|
||||
return !!getShotPoint(shot);
|
||||
}
|
||||
|
||||
function shouldPlayShotEffect(shot) {
|
||||
return (
|
||||
props.isSvip &&
|
||||
!!shot &&
|
||||
Number(shot.ring) > 0 &&
|
||||
hasShotPoint(shot)
|
||||
);
|
||||
}
|
||||
|
||||
function buildShotEffectKey(shot, index) {
|
||||
return [
|
||||
props.shotEffectToken,
|
||||
index,
|
||||
shot?.playerId ?? "",
|
||||
shot?.x ?? "",
|
||||
shot?.y ?? "",
|
||||
shot?.ring ?? "",
|
||||
shot?.ringX ? 1 : 0,
|
||||
].join("-");
|
||||
}
|
||||
|
||||
function triggerShotEffect(shot, index) {
|
||||
const key = buildShotEffectKey(shot, index);
|
||||
clearTipTimer();
|
||||
latestOne.value = null;
|
||||
hiddenLatestKey.value = key;
|
||||
shotEffect.value = { key, shot };
|
||||
}
|
||||
|
||||
function completeShotEffect(key) {
|
||||
if (!shotEffect.value || shotEffect.value.key !== key) return;
|
||||
|
||||
const shot = shotEffect.value.shot;
|
||||
hiddenLatestKey.value = "";
|
||||
shotEffect.value = null;
|
||||
showShotTip(shot);
|
||||
}
|
||||
|
||||
function shouldHideLatestHit(index) {
|
||||
return !!hiddenLatestKey.value && index === props.scores.length - 1;
|
||||
}
|
||||
|
||||
function shakeTarget() {
|
||||
targetShaking.value = false;
|
||||
if (shakeTimer.value) {
|
||||
clearTimeout(shakeTimer.value);
|
||||
shakeTimer.value = null;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
targetShaking.value = true;
|
||||
shakeTimer.value = setTimeout(() => {
|
||||
targetShaking.value = false;
|
||||
shakeTimer.value = null;
|
||||
}, 260);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTargetSize() {
|
||||
nextTick(() => {
|
||||
const query = instance?.proxy
|
||||
? uni.createSelectorQuery().in(instance.proxy)
|
||||
: uni.createSelectorQuery();
|
||||
|
||||
query
|
||||
.select(".target")
|
||||
.boundingClientRect((rect) => {
|
||||
const width = Number(rect?.width);
|
||||
const height = Number(rect?.height);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) return;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
targetSize.value = { width, height };
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
}
|
||||
|
||||
function handleWindowResize() {
|
||||
updateTargetSize();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.scores,
|
||||
(newVal) => {
|
||||
if (newVal.length - prevScores.value.length === 1) {
|
||||
latestOne.value = newVal[newVal.length - 1];
|
||||
if (timer.value) clearTimeout(timer.value);
|
||||
timer.value = setTimeout(() => {
|
||||
latestOne.value = null;
|
||||
}, 1000);
|
||||
showShotTip(newVal[newVal.length - 1]);
|
||||
} else if (newVal.length < prevScores.value.length) {
|
||||
clearTipTimer();
|
||||
latestOne.value = null;
|
||||
hiddenLatestKey.value = "";
|
||||
shotEffect.value = null;
|
||||
}
|
||||
prevScores.value = [...newVal];
|
||||
},
|
||||
@@ -197,6 +321,19 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.shotEffectToken,
|
||||
(token) => {
|
||||
// token 只由实时 ShootResult 推进,同步快照不会重播飞箭。
|
||||
if (!token || props.scores.length === 0) return;
|
||||
const latestIndex = props.scores.length - 1;
|
||||
const latestShot = props.scores[latestIndex];
|
||||
if (shouldPlayShotEffect(latestShot)) {
|
||||
triggerShotEffect(latestShot, latestIndex);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.blueScores,
|
||||
(newVal) => {
|
||||
@@ -237,42 +374,9 @@ const arrowStyle = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const currentArrowIndex = computed(() => {
|
||||
return props.scores.length + props.blueScores.length + 1;
|
||||
});
|
||||
|
||||
const getHighlightArrowIndex = (area = {}) => {
|
||||
const arrowIndex = Number(area.arrowIndex ?? area.arrowNo ?? area.arrow);
|
||||
return Number.isInteger(arrowIndex) && arrowIndex > 0 ? arrowIndex : null;
|
||||
};
|
||||
|
||||
const currentHighlightAreas = computed(() => {
|
||||
if (!Array.isArray(props.highlightAreas) || props.highlightAreas.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hasExplicitArrowIndex = props.highlightAreas.some((area = {}) => {
|
||||
return getHighlightArrowIndex(area) !== null;
|
||||
});
|
||||
|
||||
const matchedAreas = props.highlightAreas.filter((area = {}) => {
|
||||
return getHighlightArrowIndex(area) === currentArrowIndex.value;
|
||||
});
|
||||
|
||||
if (hasExplicitArrowIndex) {
|
||||
return matchedAreas;
|
||||
}
|
||||
|
||||
if (props.highlightAreas.length === 1) {
|
||||
return props.highlightAreas.slice(0, 1);
|
||||
}
|
||||
|
||||
const currentArea = props.highlightAreas[currentArrowIndex.value - 1];
|
||||
return currentArea ? [currentArea] : [];
|
||||
});
|
||||
|
||||
const showHighlightCanvas = computed(() => {
|
||||
return props.totalRound > 0 && currentHighlightAreas.value.length > 0;
|
||||
const showSectorCanvas = computed(() => {
|
||||
const count = Number(props.sectorCount);
|
||||
return props.totalRound > 0 && Number.isInteger(count) && count > 0;
|
||||
});
|
||||
|
||||
async function onReceiveMessage(message) {
|
||||
@@ -299,23 +403,27 @@ async function onReceiveMessage(message) {
|
||||
|
||||
onMounted(() => {
|
||||
uni.$on("socket-inbox", onReceiveMessage);
|
||||
updateTargetSize();
|
||||
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
clearTipTimer();
|
||||
if (dirTimer.value) {
|
||||
clearTimeout(dirTimer.value);
|
||||
dirTimer.value = null;
|
||||
}
|
||||
if (shakeTimer.value) {
|
||||
clearTimeout(shakeTimer.value);
|
||||
shakeTimer.value = null;
|
||||
}
|
||||
uni.$off("socket-inbox", onReceiveMessage);
|
||||
if (uni.offWindowResize) uni.offWindowResize(handleWindowResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="container">
|
||||
<view :class="['container', { 'container--effecting': shotEffect }]">
|
||||
<!-- <view class="header" v-if="totalRound > 0">
|
||||
<text v-if="totalRound > 0" class="round-count">{{
|
||||
(currentRound > totalRound ? totalRound : currentRound) +
|
||||
@@ -323,37 +431,44 @@ onBeforeUnmount(() => {
|
||||
totalRound
|
||||
}}</text>
|
||||
</view> -->
|
||||
<view class="target">
|
||||
<view :class="['target', { 'target--shake': targetShaking }]">
|
||||
<image
|
||||
class="target-image"
|
||||
src="../../../static/bow-target.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<TargetCanvas
|
||||
v-if="showHighlightCanvas"
|
||||
v-if="showSectorCanvas"
|
||||
class="target-highlight-layer"
|
||||
:coordinateRadius="coordinateRadius"
|
||||
:showCrosshair="false"
|
||||
:showQuadrantLabels="false"
|
||||
:showRingLabels="false"
|
||||
:highlightOnly="true"
|
||||
:highlightAreas="currentHighlightAreas"
|
||||
:sectorCount="sectorCount"
|
||||
:activeSector="activeSector"
|
||||
:activeRing="activeRing"
|
||||
:showSectorLabels="showSectorLabels"
|
||||
/>
|
||||
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
|
||||
<view :style="{ background: circleColor }">
|
||||
<image src="../../../static/dot-circle.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/dot-circle.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="stop" class="stop-sign">中场休息</view>
|
||||
<view
|
||||
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
|
||||
v-if="
|
||||
!shotEffect &&
|
||||
latestOne &&
|
||||
latestOne.ring &&
|
||||
user.id === latestOne.playerId
|
||||
"
|
||||
class="e-value fade-in-out"
|
||||
:style="getExperienceTipStyle(latestOne)"
|
||||
>
|
||||
经验 +1
|
||||
</view>
|
||||
<view
|
||||
v-if="latestOne"
|
||||
v-if="!shotEffect && latestOne"
|
||||
class="round-tip fade-in-out"
|
||||
:style="getRoundTipStyle(latestOne)"
|
||||
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
|
||||
@@ -378,8 +493,15 @@ onBeforeUnmount(() => {
|
||||
}}<text v-if="bluelatestOne.ring">环</text></view
|
||||
>
|
||||
<block v-for="(bow, index) in scores" :key="index">
|
||||
<image
|
||||
v-if="pMode && isSvip && bow.ring > 0 && !shouldHideLatestHit(index)"
|
||||
class="svip-hit-bg"
|
||||
src="../../../static/vip/svip-xuan.png"
|
||||
:style="getSvipHitBgStyle(bow)"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view
|
||||
v-if="bow.ring > 0"
|
||||
v-if="bow.ring > 0 && !shouldHideLatestHit(index)"
|
||||
:class="`hit ${pMode ? 'b' : 's'}-point ${
|
||||
index === scores.length - 1 && latestOne ? 'pump-in' : ''
|
||||
}`"
|
||||
@@ -404,6 +526,16 @@ onBeforeUnmount(() => {
|
||||
<text v-if="pMode">{{ index + 1 }}</text>
|
||||
</view>
|
||||
</block>
|
||||
<BowShotEffect
|
||||
:shot="shotEffect && shotEffect.shot"
|
||||
:playKey="shotEffect ? shotEffect.key : ''"
|
||||
:targetRadius="safeTargetRadius"
|
||||
:targetWidth="targetSize.width"
|
||||
:targetHeight="targetSize.height"
|
||||
:hitOffsetPx="currentHitRadiusPx"
|
||||
@impact="shakeTarget"
|
||||
@complete="completeShotEffect"
|
||||
/>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<PointSwitcher
|
||||
@@ -424,13 +556,22 @@ onBeforeUnmount(() => {
|
||||
height: calc(100vw - 30px);
|
||||
padding: 0px 15px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.container--effecting {
|
||||
z-index: 10000;
|
||||
}
|
||||
.target {
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 20px);
|
||||
z-index: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
transform-origin: center center;
|
||||
}
|
||||
.target--shake {
|
||||
animation: target-shake 0.26s ease-out;
|
||||
}
|
||||
.target-image {
|
||||
position: absolute;
|
||||
@@ -499,6 +640,15 @@ onBeforeUnmount(() => {
|
||||
.e-value.fade-in-out {
|
||||
animation: target-tip-fade-in-out 1.2s ease forwards;
|
||||
}
|
||||
.svip-hit-bg {
|
||||
position: absolute;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
transform-origin: center center;
|
||||
animation: svip-hit-xuan 1.2s linear infinite;
|
||||
}
|
||||
.hit {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
@@ -527,6 +677,20 @@ onBeforeUnmount(() => {
|
||||
transform: translate(-50%, -50%);*/
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
@keyframes svip-hit-xuan {
|
||||
0% {
|
||||
opacity: 0.9;
|
||||
transform: translate(-50%, -50%) rotate(0deg) scale(0.92);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) rotate(180deg) scale(1.08);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
transform: translate(-50%, -50%) rotate(360deg) scale(0.92);
|
||||
}
|
||||
}
|
||||
@keyframes target-pump-in {
|
||||
from {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
@@ -536,6 +700,29 @@ onBeforeUnmount(() => {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes target-shake {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
14% {
|
||||
transform: translate(-20rpx, 8rpx);
|
||||
}
|
||||
28% {
|
||||
transform: translate(16rpx, -8rpx);
|
||||
}
|
||||
44% {
|
||||
transform: translate(-12rpx, 6rpx);
|
||||
}
|
||||
64% {
|
||||
transform: translate(8rpx, -4rpx);
|
||||
}
|
||||
82% {
|
||||
transform: translate(-4rpx, 2rpx);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
.hit.pump-in {
|
||||
animation: target-pump-in 0.3s ease-out forwards;
|
||||
transform-origin: center center;
|
||||
|
||||
@@ -85,8 +85,8 @@ onBeforeUnmount(() => {
|
||||
class="score-item-bg"
|
||||
:src="
|
||||
isLowScore(arrows[index])
|
||||
? '/static/training-difficulty-design/block-gray.png'
|
||||
: '/static/training-difficulty-design/block-gold.png'
|
||||
? '../static/training-difficulty-design/block-gray.png'
|
||||
: '../static/training-difficulty-design/block-gold.png'
|
||||
"
|
||||
/>
|
||||
<text
|
||||
|
||||
@@ -25,9 +25,8 @@ const isLowScore = (arrow = {}) => {
|
||||
|
||||
const displayArrows = computed(() => {
|
||||
const list = [...props.arrows];
|
||||
if (props.total > 0 && list.length < props.total) {
|
||||
list.push(null);
|
||||
}
|
||||
// total 是达标箭数,不是实际射箭上限;训练中始终预留下一箭空框。
|
||||
list.push(null);
|
||||
return list;
|
||||
});
|
||||
</script>
|
||||
@@ -40,7 +39,7 @@ const displayArrows = computed(() => {
|
||||
:key="index"
|
||||
class="score-card"
|
||||
>
|
||||
<image class="score-card-bg" :src="isLowScore(arrow)?'/static/training-difficulty-design/block-gray.png':'/static/training-difficulty-design/block-gold.png'"></image>
|
||||
<image class="score-card-bg" :src="isLowScore(arrow)?'../static/training-difficulty-design/block-gray.png':'../static/training-difficulty-design/block-gold.png'"></image>
|
||||
<text
|
||||
class="score-value"
|
||||
:class="{ 'score-value--low': isLowScore(arrow) }"
|
||||
|
||||
@@ -27,6 +27,14 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
trainingType: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
difficultyLevel: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
result: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
@@ -60,12 +68,6 @@ function onClickShare() {
|
||||
uni.$emit("share-image");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.result.lvl > user.value.lvl) {
|
||||
showUpgrade.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const details = computed(() => props.result.details || []);
|
||||
|
||||
const arrows = computed(() => {
|
||||
@@ -81,25 +83,89 @@ const totalRing = computed(() =>
|
||||
details.value.reduce((last, next) => last + (Number(next.ring) || 0), 0)
|
||||
);
|
||||
|
||||
const gainedExp = computed(
|
||||
() => props.result.exp || props.result.experience || validArrows.value
|
||||
const hasResultValue = (...keys) =>
|
||||
keys.some((key) => {
|
||||
const value = props.result[key];
|
||||
return value !== undefined && value !== null && value !== "";
|
||||
});
|
||||
|
||||
const readResultNumber = (keys, fallback = 0) => {
|
||||
for (const key of keys) {
|
||||
const value = props.result[key];
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
const numberValue = Number(value);
|
||||
if (Number.isFinite(numberValue)) return numberValue;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const beforeExp = computed(() =>
|
||||
readResultNumber(["beforeExp", "before_exp"])
|
||||
);
|
||||
|
||||
const currentLevel = computed(
|
||||
() => props.result.lvl || user.value.lvl || user.value.rankLvl || 1
|
||||
);
|
||||
const currentExp = computed(() => {
|
||||
const userScores = Number(user.value.scores);
|
||||
return readResultNumber(
|
||||
["currentExp", "current_exp", "score"],
|
||||
Number.isFinite(userScores) ? userScores : 0
|
||||
);
|
||||
});
|
||||
|
||||
const currentExp = computed(
|
||||
() => props.result.currentExp || props.result.score || user.value.scores || 0
|
||||
);
|
||||
// 新版练习结算返回练习前后累计经验,本局经验由两者相减得到。
|
||||
const gainedExp = computed(() => {
|
||||
if (
|
||||
hasResultValue("beforeExp", "before_exp") &&
|
||||
hasResultValue("currentExp", "current_exp")
|
||||
) {
|
||||
return Math.max(0, currentExp.value - beforeExp.value);
|
||||
}
|
||||
return Math.max(0, readResultNumber(["exp", "experience"]));
|
||||
});
|
||||
|
||||
const nextExp = computed(
|
||||
() => props.result.nextExp || props.result.upgradeScore || 100
|
||||
const beforeLevel = computed(() => {
|
||||
const currentUserLevel = Number(user.value.lvl);
|
||||
return readResultNumber(
|
||||
["beforeLevel", "before_level"],
|
||||
Number.isFinite(currentUserLevel) ? currentUserLevel : 0
|
||||
);
|
||||
});
|
||||
|
||||
const userLevel = computed(() => {
|
||||
const fallbackLevel = Number(user.value.lvl ?? user.value.rankLvl ?? 1);
|
||||
const level = readResultNumber(
|
||||
["level", "lvl"],
|
||||
Number.isFinite(fallbackLevel) ? fallbackLevel : 1
|
||||
);
|
||||
return Math.max(1, Math.trunc(level));
|
||||
});
|
||||
|
||||
const resultDifficultyLevel = computed(() => {
|
||||
const level = Number(props.difficultyLevel);
|
||||
return Number.isInteger(level) && level > 0 ? level : "--";
|
||||
});
|
||||
|
||||
const upgradeExp = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
readResultNumber(
|
||||
["upgradeExp", "upgrade_exp", "nextExp", "upgradeScore"],
|
||||
100
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const expPercent = computed(() => {
|
||||
if (!nextExp.value) return 0;
|
||||
return Math.min(100, Math.max(0, (currentExp.value / nextExp.value) * 100));
|
||||
if (!upgradeExp.value) return 0;
|
||||
return Math.min(
|
||||
100,
|
||||
Math.max(0, (currentExp.value / upgradeExp.value) * 100)
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (userLevel.value > beforeLevel.value) {
|
||||
showUpgrade.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const findValue = (...keys) => {
|
||||
@@ -108,83 +174,184 @@ const findValue = (...keys) => {
|
||||
};
|
||||
|
||||
const formatDuration = (value) => {
|
||||
const seconds = Number(value || 0);
|
||||
if (!seconds) return "--";
|
||||
const valueNumber = Number(value);
|
||||
const seconds = Number.isFinite(valueNumber)
|
||||
? Math.max(0, Math.round(valueNumber))
|
||||
: 0;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const rest = seconds % 60;
|
||||
return minutes ? `${minutes}分${rest}秒` : `${rest}秒`;
|
||||
};
|
||||
|
||||
const usedTime = computed(() =>
|
||||
findValue("duration", "usedTime", "shootTime", "time")
|
||||
const formatMetricNumber = (value) => {
|
||||
const valueNumber = Number(value);
|
||||
if (!Number.isFinite(valueNumber)) return "0";
|
||||
return String(Number(valueNumber.toFixed(2)));
|
||||
};
|
||||
|
||||
const readMetricNumber = (keys, fallback = 0) => {
|
||||
const value = findValue(...keys);
|
||||
const valueNumber = Number(value);
|
||||
return Number.isFinite(valueNumber) ? valueNumber : fallback;
|
||||
};
|
||||
|
||||
const resultTrainingType = computed(
|
||||
() => props.result.trainingType || props.trainingType || "precision"
|
||||
);
|
||||
|
||||
const hitCompare = computed(
|
||||
() => Number(findValue("hitCompare", "hitDiff", "hitDelta") || 0)
|
||||
const metricConfigs = {
|
||||
base: [
|
||||
{
|
||||
label: "平均环数",
|
||||
valueKeys: ["averageRing", "average_ring"],
|
||||
unit: "环",
|
||||
deltaKeys: ["deltaAverageRing", "delta_average_ring"],
|
||||
deltaUnit: "环",
|
||||
},
|
||||
{
|
||||
label: "稳定性",
|
||||
valueKeys: ["stability"],
|
||||
unit: "",
|
||||
deltaKeys: ["deltaStability", "delta_stability"],
|
||||
deltaUnit: "",
|
||||
},
|
||||
],
|
||||
rhythm: [
|
||||
{
|
||||
label: "最高连击次数",
|
||||
valueKeys: ["maxCombo", "max_combo"],
|
||||
unit: "连",
|
||||
deltaKeys: ["deltaMaxCombo", "delta_max_combo"],
|
||||
deltaUnit: "连",
|
||||
},
|
||||
{
|
||||
label: "共命中环数",
|
||||
valueKeys: ["currentRings", "current_rings"],
|
||||
unit: "环",
|
||||
deltaKeys: ["deltaTotalRings", "delta_total_rings"],
|
||||
deltaUnit: "环",
|
||||
},
|
||||
],
|
||||
endurance: [
|
||||
{
|
||||
label: "完成箭数",
|
||||
valueKeys: ["totalArrows", "total_arrows"],
|
||||
unit: "支",
|
||||
deltaKeys: ["deltaTotalArrows", "delta_total_arrows"],
|
||||
deltaUnit: "支",
|
||||
},
|
||||
{
|
||||
label: "命中环数",
|
||||
valueKeys: ["currentRings", "current_rings"],
|
||||
unit: "环",
|
||||
deltaKeys: ["deltaTotalRings", "delta_total_rings"],
|
||||
deltaUnit: "环",
|
||||
},
|
||||
],
|
||||
precision: [
|
||||
{
|
||||
label: "共命中目标",
|
||||
valueKeys: ["totalHits", "total_hits"],
|
||||
fallback: () => 0,
|
||||
unit: "次",
|
||||
deltaKeys: [
|
||||
"deltaTotalHits",
|
||||
"delta_total_hits",
|
||||
"hitCompare",
|
||||
"hitDiff",
|
||||
"hitDelta",
|
||||
],
|
||||
deltaUnit: "次",
|
||||
},
|
||||
{
|
||||
label: "用时",
|
||||
valueKeys: ["duration", "usedTime", "shootTime", "time"],
|
||||
unit: "",
|
||||
deltaKeys: [
|
||||
"deltaDuration",
|
||||
"delta_duration",
|
||||
"timeCompare",
|
||||
"timeDiff",
|
||||
"durationDiff",
|
||||
],
|
||||
deltaUnit: "",
|
||||
duration: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resultRows = computed(() => {
|
||||
const configs = metricConfigs[resultTrainingType.value] || metricConfigs.precision;
|
||||
return configs.map((config) => {
|
||||
const fallback = config.fallback ? config.fallback() : 0;
|
||||
const value = readMetricNumber(config.valueKeys, fallback);
|
||||
const delta = readMetricNumber(config.deltaKeys);
|
||||
const formatter = config.duration ? formatDuration : formatMetricNumber;
|
||||
|
||||
return {
|
||||
...config,
|
||||
valueText: formatter(value),
|
||||
delta,
|
||||
deltaText: formatter(Math.abs(delta)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const advancesDifficulty = computed(() =>
|
||||
["base", "endurance"].includes(resultTrainingType.value)
|
||||
);
|
||||
const primaryText = computed(() =>
|
||||
advancesDifficulty.value ? "下一难度" : "再来一次"
|
||||
);
|
||||
|
||||
const timeCompare = computed(
|
||||
() => Number(findValue("timeCompare", "timeDiff", "durationDiff") || 0)
|
||||
);
|
||||
const handlePrimary = () => {
|
||||
if (advancesDifficulty.value) {
|
||||
closePanel();
|
||||
return;
|
||||
}
|
||||
retryPractice();
|
||||
};
|
||||
|
||||
const calories = computed(
|
||||
() => Number(findValue("calories", "calorie", "kcal") || 0)
|
||||
() => formatMetricNumber(readMetricNumber(["calories", "calorie", "kcal"]))
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="['result-mask', showPanel ? 'result-mask--show' : 'result-mask--hide']">
|
||||
<image class="hero-glow" src="/static/training-difficulty-design/result-bg.png" mode="widthFix" />
|
||||
<image class="hero-glow" src="../static/training-difficulty-design/result-bg.png" mode="widthFix" />
|
||||
<view class="result-title">
|
||||
<image class="result-title-bg" src="/static/training-difficulty-design/result-t-bg.png" mode="widthFix" />
|
||||
<view class="result-title-text">Lv{{ currentLevel }}</view>
|
||||
<image class="result-title-bg" src="../static/training-difficulty-design/result-t-bg.png" mode="widthFix" />
|
||||
<view class="result-title-text">Lv{{ resultDifficultyLevel }}</view>
|
||||
</view>
|
||||
|
||||
<view class="result-panel">
|
||||
<view class="line-top"></view>
|
||||
<view class="line-bottom"></view>
|
||||
<view class="stats">
|
||||
<view class="stat-row">
|
||||
<image class="stat-bg" src="/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||
<view v-for="row in resultRows" :key="row.label" class="stat-row">
|
||||
<image class="stat-bg" src="../static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||
<view class="stat-cell">
|
||||
<text class="stat-label">共命中目标</text>
|
||||
<text class="stat-label">{{ row.label }}</text>
|
||||
<view class="stat-value">
|
||||
<text>{{ validArrows }}</text>
|
||||
<text class="stat-unit">次</text>
|
||||
<text>{{ row.valueText }}</text>
|
||||
<text v-if="row.unit" class="stat-unit">{{ row.unit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-cell stat-cell--compare">
|
||||
<text class="stat-label">对比上次</text>
|
||||
<view class="stat-value">
|
||||
<text>{{ Math.abs(hitCompare) }}</text>
|
||||
<text class="stat-unit">次</text>
|
||||
<image class="trend-icon" :class="{ 'trend-icon--down': hitCompare < 0 }"
|
||||
src="/static/training-difficulty-design/result-up.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stat-row">
|
||||
<image class="stat-bg" src="/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||
<view class="stat-cell">
|
||||
<text class="stat-label">用时</text>
|
||||
<view class="stat-value">
|
||||
<text>{{ formatDuration(usedTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-cell stat-cell--compare">
|
||||
<text class="stat-label">对比上次</text>
|
||||
<view class="stat-value">
|
||||
<text>{{ formatDuration(Math.abs(timeCompare)) }}</text>
|
||||
<image class="trend-icon" :class="{ 'trend-icon--down': timeCompare <= 0 }"
|
||||
src="/static/training-difficulty-design/result-up.png" mode="widthFix" />
|
||||
<view v-if="row.delta !== 0" class="stat-value">
|
||||
<text>{{ row.delta > 0 ? "+" : "-" }}{{ row.deltaText }}</text>
|
||||
<text v-if="row.deltaUnit" class="stat-unit">{{ row.deltaUnit }}</text>
|
||||
<image class="trend-icon" :class="{ 'trend-icon--down': row.delta < 0 }"
|
||||
src="../static/training-difficulty-design/result-up.png" mode="widthFix" />
|
||||
</view>
|
||||
<view v-else class="stat-value">--</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-row">
|
||||
<image class="stat-bg" src="/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||
<image class="stat-bg" src="../static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||
<view class="stat-cell">
|
||||
<text class="stat-label">消耗卡路里</text>
|
||||
<view class="stat-value">
|
||||
@@ -196,7 +363,7 @@ const calories = computed(
|
||||
<view class="stat-cell stat-cell--compare">
|
||||
<view class="stat-value">
|
||||
<image v-for="index in 3" :key="index" class="rice-icon"
|
||||
src="/static/training-difficulty-design/result-rice.png" mode="widthFix" />
|
||||
src="../static/training-difficulty-design/result-rice.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -204,15 +371,15 @@ const calories = computed(
|
||||
|
||||
<view class="actions">
|
||||
<view class="action-item" @click="() => (showBowData = true)">
|
||||
<image class="action-icon" src="/static/training-difficulty-design/result-icon-1.png" mode="widthFix" />
|
||||
<image class="action-icon" src="../static/training-difficulty-design/result-icon-1.png" mode="widthFix" />
|
||||
<text>查看靶纸</text>
|
||||
</view>
|
||||
<view v-if="validArrows === total" class="action-item" @click="() => (showComment = true)">
|
||||
<image class="action-icon" src="/static/training-difficulty-design/result-icon-2.png" mode="widthFix" />
|
||||
<view class="action-item" @click="() => (showComment = true)">
|
||||
<image class="action-icon" src="../static/training-difficulty-design/result-icon-2.png" mode="widthFix" />
|
||||
<text>教练点评</text>
|
||||
</view>
|
||||
<view v-if="validArrows === total" class="action-item" @click="onClickShare">
|
||||
<image class="action-icon" src="/static/training-difficulty-design/result-icon-3.png" mode="widthFix" />
|
||||
<view class="action-item" @click="onClickShare">
|
||||
<image class="action-icon" src="../static/training-difficulty-design/result-icon-3.png" mode="widthFix" />
|
||||
<text>分享成绩</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -222,20 +389,20 @@ const calories = computed(
|
||||
<view class="exp-area">
|
||||
<text class="exp-gain">+{{ gainedExp }}经验</text>
|
||||
<view class="level-progress">
|
||||
<text class="level-text">LV.{{ currentLevel }}</text>
|
||||
<text class="level-text">LV.{{ userLevel }}</text>
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: `${expPercent}%` }"></view>
|
||||
</view>
|
||||
<text class="progress-text">{{ currentExp }} / {{ nextExp }}</text>
|
||||
<text class="progress-text">{{ currentExp }} / {{ upgradeExp }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-actions">
|
||||
<view class="result-btn result-btn--muted" @click="closePanel">
|
||||
<text>{{ validArrows === total ? "完成" : "返回" }}</text>
|
||||
<text>完成</text>
|
||||
</view>
|
||||
<view class="result-btn result-btn--primary" @click="retryPractice">
|
||||
<text>再来一次</text>
|
||||
<view class="result-btn result-btn--primary" @click="handlePrimary">
|
||||
<text>{{ primaryText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -268,7 +435,7 @@ const calories = computed(
|
||||
</ScreenHint>
|
||||
<BowData :total="arrows.length" :arrows="result.details" :show="showBowData"
|
||||
:onClose="() => (showBowData = false)" />
|
||||
<UserUpgrade :show="showUpgrade" :onClose="() => (showUpgrade = false)" :lvl="result.lvl" />
|
||||
<UserUpgrade :show="showUpgrade" :onClose="() => (showUpgrade = false)" :lvl="userLevel" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -27,29 +27,29 @@ const getContentHeight = () => {
|
||||
<view class="scale-in" :style="{ height: getContentHeight() }">
|
||||
<image
|
||||
v-if="mode === 'normal'"
|
||||
src="/static/screen-hint-bg.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/screen-hint-bg.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
v-if="mode === 'tall'"
|
||||
src="/static/coach-comment.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/coach-comment.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
v-if="mode === 'square'"
|
||||
src="/static/prompt-bg-square.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/prompt-bg-square.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
v-if="mode === 'small'"
|
||||
src="/static/finish-frame.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/finish-frame.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<slot />
|
||||
</view>
|
||||
<IconButton
|
||||
v-if="!!onClose"
|
||||
src="/static/close-gold-outline.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/close-gold-outline.png"
|
||||
:width="30"
|
||||
:onClick="onClose"
|
||||
/>
|
||||
|
||||
@@ -27,6 +27,14 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 120,
|
||||
},
|
||||
countdownEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
trainingType: {
|
||||
type: String,
|
||||
default: "precision",
|
||||
},
|
||||
currentRound: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
@@ -45,8 +53,19 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const trainingTitleIconMap = Object.freeze({
|
||||
base: "../static/training-difficulty-design/text-icon-jcxl.png",
|
||||
precision: "../static/training-difficulty-design/text-icon-jingzxl.png",
|
||||
rhythm: "../static/training-difficulty-design/text-icon-jzxl.png",
|
||||
endurance: "../static/training-difficulty-design/text-icon-nlxl.png",
|
||||
});
|
||||
const trainingTitleIcon = computed(
|
||||
() =>
|
||||
trainingTitleIconMap[props.trainingType] || trainingTitleIconMap.precision
|
||||
);
|
||||
|
||||
const barColor = ref("#fed847");
|
||||
const remain = ref(props.total);
|
||||
const remain = ref(props.countdownEnabled ? props.total : 0);
|
||||
const timer = ref(null);
|
||||
const sound = ref(true);
|
||||
const currentRound = ref(props.currentRound);
|
||||
@@ -56,7 +75,7 @@ const wait = ref(0);
|
||||
const transitionStyle = ref("all 1s linear");
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!props.total) return 0;
|
||||
if (!props.countdownEnabled || !props.total) return 0;
|
||||
return Math.max(0, Math.min(100, (remain.value / props.total) * 100));
|
||||
});
|
||||
|
||||
@@ -98,9 +117,23 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const clearTimer = () => {
|
||||
if (!timer.value) return;
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
};
|
||||
|
||||
const resetTimer = (count) => {
|
||||
if (timer.value) clearInterval(timer.value);
|
||||
const newVal = Math.round(count);
|
||||
clearTimer();
|
||||
if (!props.countdownEnabled) {
|
||||
remain.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const countValue = Number(count);
|
||||
const newVal = Number.isFinite(countValue)
|
||||
? Math.max(0, Math.round(countValue))
|
||||
: 0;
|
||||
|
||||
if (newVal >= remain.value) {
|
||||
transitionStyle.value = "none";
|
||||
@@ -115,7 +148,7 @@ const resetTimer = (count) => {
|
||||
if (remain.value > 0) {
|
||||
timer.value = setInterval(() => {
|
||||
if (remain.value === 0) {
|
||||
clearInterval(timer.value);
|
||||
clearTimer();
|
||||
props.onStop();
|
||||
}
|
||||
if (remain.value > 0) remain.value--;
|
||||
@@ -124,13 +157,13 @@ const resetTimer = (count) => {
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.start,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
() => [props.start, props.countdownEnabled],
|
||||
([started, countdownEnabled]) => {
|
||||
if (started && countdownEnabled) {
|
||||
resetTimer(props.total);
|
||||
} else {
|
||||
clearTimer();
|
||||
remain.value = 0;
|
||||
clearInterval(timer.value);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -158,18 +191,29 @@ async function onReceiveMessage(msg) {
|
||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||
audioManager.play("比赛结束", false);
|
||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||
let arrow = {};
|
||||
if (msg.details && Array.isArray(msg.details)) {
|
||||
arrow = msg.details[msg.details.length - 1];
|
||||
} else {
|
||||
if (msg.shootData.playerId !== user.value.id) return;
|
||||
if (msg.shootData) arrow = msg.shootData;
|
||||
const latestDetail =
|
||||
Array.isArray(msg.details) && msg.details.length > 0
|
||||
? msg.details[msg.details.length - 1]
|
||||
: null;
|
||||
// 语音和 ACK 优先使用同一份当前箭数据,details 仅作为兼容兜底。
|
||||
const arrow = msg.shootData || latestDetail;
|
||||
if (!arrow) return;
|
||||
if (
|
||||
arrow.playerId !== undefined &&
|
||||
arrow.playerId !== null &&
|
||||
String(arrow.playerId) !== String(user.value?.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let key = [];
|
||||
|
||||
const key = [];
|
||||
key.push(arrow.ring ? `${arrow.ringX ? "X" : arrow.ring}环` : "未上靶");
|
||||
if (arrow.angle !== null) {
|
||||
if (arrow.angle !== null && arrow.angle !== undefined) {
|
||||
key.push(`向${getDirectionText(arrow.angle)}调整`);
|
||||
}
|
||||
if (arrow.threeConsecutive10Rings === true) {
|
||||
key.push("tententen");
|
||||
}
|
||||
audioManager.play(key, false);
|
||||
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
|
||||
halfTime.value = true;
|
||||
@@ -197,7 +241,7 @@ onBeforeUnmount(() => {
|
||||
uni.$off("update-remain", resetTimer);
|
||||
uni.$off("socket-inbox", onReceiveMessage);
|
||||
uni.$off("play-sound", playSound);
|
||||
if (timer.value) clearInterval(timer.value);
|
||||
clearTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -228,10 +272,10 @@ onBeforeUnmount(() => {
|
||||
<view class="progress-card__track-wrap">
|
||||
<image
|
||||
class="progress-card__titile"
|
||||
src="../../../static/training-difficulty-design/text-icon-cgxl.png"
|
||||
:src="trainingTitleIcon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="progress-card__track">
|
||||
<view v-if="countdownEnabled" class="progress-card__track">
|
||||
<view
|
||||
class="progress-card__fill"
|
||||
:style="{
|
||||
|
||||
@@ -23,6 +23,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
targetType: {
|
||||
type: [Number, String],
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
const arrow = ref({});
|
||||
const distance = ref(0);
|
||||
@@ -78,7 +82,7 @@ onBeforeUnmount(() => {
|
||||
<view class="test-area">
|
||||
<image
|
||||
class="text-bg"
|
||||
src="../../../static/training-difficulty-design/par-bg.png"
|
||||
src="../static/training-difficulty-design/par-bg.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<button
|
||||
@@ -90,7 +94,7 @@ onBeforeUnmount(() => {
|
||||
模拟射箭
|
||||
</button>
|
||||
<view class="warnning-text">
|
||||
<view class="target-tip">当前靶子为<text class="text-yellow">20cm</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>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { computed } from "vue";
|
||||
|
||||
const lockedBadgeBackground =
|
||||
"/static/training-difficulty-design/unlock.svg";
|
||||
"../static/training-difficulty-design/unlock.svg";
|
||||
const unlockedBadgeBackground =
|
||||
"/static/training-difficulty-design/lock.svg";
|
||||
"../static/training-difficulty-design/lock.svg";
|
||||
|
||||
const props = defineProps({
|
||||
node: {
|
||||
|
||||
@@ -21,7 +21,7 @@ const previewLines = computed(() => {
|
||||
<view class="difficulty-preview">
|
||||
<image
|
||||
class="difficulty-preview__bg"
|
||||
src="/static/training-difficulty-design/text.png"
|
||||
src="../static/training-difficulty-design/text.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view class="difficulty-preview__content">
|
||||
@@ -52,10 +52,15 @@ const previewLines = computed(() => {
|
||||
|
||||
.difficulty-preview__content {
|
||||
position: absolute;
|
||||
top: 28rpx;
|
||||
top: 0;
|
||||
left: 30rpx;
|
||||
box-sizing: border-box;
|
||||
width: 486rpx;
|
||||
height: 93%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.difficulty-preview__title {
|
||||
|
||||
@@ -21,7 +21,7 @@ const handleClick = () => {
|
||||
>
|
||||
<image
|
||||
class="difficulty-start__button"
|
||||
src="/static/training-difficulty-design/btn.png"
|
||||
src="../static/training-difficulty-design/btn.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user