Compare commits
17 Commits
0e82416800
...
feat-prac
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bf4773400 | |||
| 69ed5bda1b | |||
| 5bab30d2e4 | |||
| 8b8366f30f | |||
| 8b2ea24f38 | |||
| c30aa45e5b | |||
| 5cf243d187 | |||
| bfdd40ec93 | |||
| 996754be7f | |||
| 50a2829519 | |||
| 022683aff1 | |||
| 0d5866b82a | |||
| 72518fa17e | |||
| f5497c534d | |||
| b3fc11f1b1 | |||
| 8b25a10d4c | |||
| 2a5394155a |
10
src/App.vue
10
src/App.vue
@@ -22,7 +22,8 @@
|
|||||||
const {
|
const {
|
||||||
updateUser,
|
updateUser,
|
||||||
updateOnline,
|
updateOnline,
|
||||||
clearSessionState
|
clearSessionState,
|
||||||
|
clearDevice
|
||||||
} = store;
|
} = store;
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -63,6 +64,11 @@
|
|||||||
updateOnline(data.online);
|
updateOnline(data.online);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDeviceBindInvalid() {
|
||||||
|
clearDevice();
|
||||||
|
uni.setStorageSync("calibration", false);
|
||||||
|
}
|
||||||
|
|
||||||
function onDeviceShoot() {
|
function onDeviceShoot() {
|
||||||
// audioManager.play("射箭声音")
|
// audioManager.play("射箭声音")
|
||||||
}
|
}
|
||||||
@@ -78,6 +84,7 @@
|
|||||||
uni.$on("update-user", emitUpdateUser);
|
uni.$on("update-user", emitUpdateUser);
|
||||||
uni.$on("update-online", emitUpdateOnline);
|
uni.$on("update-online", emitUpdateOnline);
|
||||||
uni.$on("session-kicked-out", onSessionKickedOut);
|
uni.$on("session-kicked-out", onSessionKickedOut);
|
||||||
|
uni.$on("device-bind-invalid", onDeviceBindInvalid);
|
||||||
const token = uni.getStorageSync(
|
const token = uni.getStorageSync(
|
||||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
);
|
);
|
||||||
@@ -91,6 +98,7 @@
|
|||||||
uni.$off("update-user", emitUpdateUser);
|
uni.$off("update-user", emitUpdateUser);
|
||||||
uni.$off("update-online", emitUpdateOnline);
|
uni.$off("update-online", emitUpdateOnline);
|
||||||
uni.$off("session-kicked-out", onSessionKickedOut);
|
uni.$off("session-kicked-out", onSessionKickedOut);
|
||||||
|
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
||||||
websocket.closeWebSocket();
|
websocket.closeWebSocket();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ function request(method, url, data = {}) {
|
|||||||
resolve({binded: true});
|
resolve({binded: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (message === "BIND_FAILD") {
|
||||||
|
uni.$emit("device-bind-invalid");
|
||||||
|
uni.showToast({
|
||||||
|
title: "设备绑定状态已失效,请重新绑定",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
reject({type: "DEVICE_BIND_INVALID", message});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (message === "ERROR_ORDER_UNPAY") {
|
if (message === "ERROR_ORDER_UNPAY") {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "当前有未支付订单",
|
title: "当前有未支付订单",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -26,12 +26,33 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 45,
|
default: 45,
|
||||||
},
|
},
|
||||||
|
sizeUnit: {
|
||||||
|
type: String,
|
||||||
|
default: "px",
|
||||||
|
},
|
||||||
|
imageMode: {
|
||||||
|
type: String,
|
||||||
|
default: "widthFix",
|
||||||
|
},
|
||||||
borderColor: {
|
borderColor: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const avatarFrame = ref("");
|
const avatarFrame = ref("");
|
||||||
|
const sizeValue = computed(() => `${Number(props.size)}${props.sizeUnit}`);
|
||||||
|
const frameSizeValue = computed(() => `${Number(props.size) + 10}${props.sizeUnit}`);
|
||||||
|
const avatarImageStyle = computed(() => ({
|
||||||
|
width: sizeValue.value,
|
||||||
|
height: sizeValue.value,
|
||||||
|
minHeight: sizeValue.value,
|
||||||
|
borderColor: props.borderColor || "#fff",
|
||||||
|
}));
|
||||||
|
const avatarFrameStyle = computed(() => ({
|
||||||
|
width: frameSizeValue.value,
|
||||||
|
height: frameSizeValue.value,
|
||||||
|
}));
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [config.value, props.rankLvl],
|
() => [config.value, props.rankLvl],
|
||||||
() => {
|
() => {
|
||||||
@@ -51,10 +72,7 @@ watch(
|
|||||||
v-if="avatarFrame"
|
v-if="avatarFrame"
|
||||||
:src="avatarFrame"
|
:src="avatarFrame"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
:style="{
|
:style="avatarFrameStyle"
|
||||||
width: Number(size) + 10 + 'px',
|
|
||||||
height: Number(size) + 10 + 'px',
|
|
||||||
}"
|
|
||||||
class="avatar-frame"
|
class="avatar-frame"
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
@@ -78,13 +96,8 @@ watch(
|
|||||||
<view v-if="rank > 3" class="rank-view">{{ rank }}</view>
|
<view v-if="rank > 3" class="rank-view">{{ rank }}</view>
|
||||||
<image
|
<image
|
||||||
:src="src || '../static/user-icon.png'"
|
:src="src || '../static/user-icon.png'"
|
||||||
mode="widthFix"
|
:mode="imageMode"
|
||||||
:style="{
|
:style="avatarImageStyle"
|
||||||
width: size + 'px',
|
|
||||||
height: size + 'px',
|
|
||||||
minHeight: size + 'px',
|
|
||||||
borderColor: borderColor || '#fff',
|
|
||||||
}"
|
|
||||||
class="avatar-image"
|
class="avatar-image"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
headerClass: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
showBottom: {
|
showBottom: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@@ -152,6 +156,7 @@ const goCalibration = async () => {
|
|||||||
<AppBackground :type="bgType" :bgColor="bgColor" />
|
<AppBackground :type="bgType" :bgColor="bgColor" />
|
||||||
<Header
|
<Header
|
||||||
v-if="!isHome"
|
v-if="!isHome"
|
||||||
|
:class="headerClass"
|
||||||
:title="title"
|
:title="title"
|
||||||
:onBack="onBack"
|
:onBack="onBack"
|
||||||
:whiteBackArrow="whiteBackArrow"
|
:whiteBackArrow="whiteBackArrow"
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const rowCount = new Array(6).fill(0);
|
const rowCount = new Array(6).fill(0);
|
||||||
|
|
||||||
|
const getRingText = (arrow) => {
|
||||||
|
if (!arrow) return "-";
|
||||||
|
if (arrow.ringX && arrow.ring) return "X环";
|
||||||
|
return arrow.ring ? `${arrow.ring}环` : "-";
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -35,23 +41,19 @@ const rowCount = new Array(6).fill(0);
|
|||||||
<view>
|
<view>
|
||||||
<view>
|
<view>
|
||||||
<view v-for="(_, index) in rowCount" :key="index">
|
<view v-for="(_, index) in rowCount" :key="index">
|
||||||
<text>{{
|
<text>{{ getRingText(scores[0]?.[index]) }}</text>
|
||||||
scores[0] && scores[0][index] ? `${scores[0][index].ring}环` : "-"
|
|
||||||
}}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<view v-for="(_, index) in rowCount" :key="index">
|
<view v-for="(_, index) in rowCount" :key="index">
|
||||||
<text>{{
|
<text>{{ getRingText(scores[1]?.[index]) }}</text>
|
||||||
scores[1] && scores[1][index] ? `${scores[1][index].ring}环` : "-"
|
|
||||||
}}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text
|
<text
|
||||||
>{{
|
>{{
|
||||||
scores
|
scores
|
||||||
.map((s) => s.reduce((last, next) => last + next.ring, 0))
|
.map((s) => (s || []).reduce((last, next) => last + next.ring, 0))
|
||||||
.reduce((last, next) => last + next, 0)
|
.reduce((last, next) => last + next, 0)
|
||||||
}}环</text
|
}}环</text
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { updateUser, updateDevice, updateOnline } = store;
|
const { updateUser, updateDevice, updateOnline, clearDevice } = store;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
@@ -107,6 +107,8 @@ async function doLogin() {
|
|||||||
);
|
);
|
||||||
const data = await getDeviceBatteryAPI();
|
const data = await getDeviceBatteryAPI();
|
||||||
updateOnline(data.online);
|
updateOnline(data.online);
|
||||||
|
} else {
|
||||||
|
clearDevice();
|
||||||
}
|
}
|
||||||
props.onClose();
|
props.onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
535
src/components/TargetCanvas.vue
Normal file
535
src/components/TargetCanvas.vue
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
|
const defaultCanvasSize = 300;
|
||||||
|
const defaultRingCount = 10;
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// canvas 唯一标识;不传时组件内部自动生成,避免多个靶面 canvas-id 冲突。
|
||||||
|
canvasId: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
// 业务坐标半径,例如 20 表示命中点坐标范围为 -20 到 20。
|
||||||
|
// 当前组件主要用它参与重绘判断,外层命中点定位也应使用同一半径。
|
||||||
|
coordinateRadius: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
// 是否显示靶心十字辅助线。
|
||||||
|
showCrosshair: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 是否显示象限文字。
|
||||||
|
showQuadrantLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 是否显示环数文字。
|
||||||
|
showRingLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 象限文字配置,key 为 1/2/3/4。
|
||||||
|
quadrantLabels: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
1: "1",
|
||||||
|
2: "2",
|
||||||
|
3: "3",
|
||||||
|
4: "4",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// 高亮区域数组。
|
||||||
|
// quadrant: 1/2/3/4,表示第几个象限。
|
||||||
|
// rings: "all" 或环数数组,例如 [7, 8, 9, 10]。
|
||||||
|
// scope: "box" 表示整象限矩形,"sector" 表示环形扇区。
|
||||||
|
// style: 可覆盖高亮填充色、描边色、线宽比例。
|
||||||
|
highlightAreas: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// 只绘制透明高亮层,不绘制完整靶纸;用于叠加在靶纸图片上。
|
||||||
|
highlightOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 外部指定 canvas 绘制尺寸;用于让高亮层跟随靶图真实显示区域。
|
||||||
|
canvasWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
canvasHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
// 靶纸样式覆盖配置,例如环数、环色、环线颜色、环数字体等。
|
||||||
|
targetStyleConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
// 十字辅助线样式覆盖配置。
|
||||||
|
crosshairStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
// 象限文字样式覆盖配置。
|
||||||
|
quadrantLabelStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const localCanvasId = `target-canvas-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
const currentCanvasId = computed(() => props.canvasId || localCanvasId);
|
||||||
|
const lastDrawKey = ref("");
|
||||||
|
const canvasSize = ref({
|
||||||
|
width: defaultCanvasSize,
|
||||||
|
height: defaultCanvasSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 完整靶纸默认样式,调用方可以通过 targetStyleConfig 局部覆盖。
|
||||||
|
const defaultTargetStyleConfig = {
|
||||||
|
ringCount: defaultRingCount,
|
||||||
|
ringColors: {
|
||||||
|
1: "#f8f8f3",
|
||||||
|
2: "#f8f8f3",
|
||||||
|
3: "#595959",
|
||||||
|
4: "#595959",
|
||||||
|
5: "#24aee0",
|
||||||
|
6: "#24aee0",
|
||||||
|
7: "#ff1f35",
|
||||||
|
8: "#ff1f35",
|
||||||
|
9: "#f7d34a",
|
||||||
|
10: "#f7d34a",
|
||||||
|
},
|
||||||
|
ringLineColor: "rgba(150, 150, 150, 0.55)",
|
||||||
|
ringLineWidthRatio: 0.0022,
|
||||||
|
centerDotColor: "#ffffff",
|
||||||
|
centerDotRadiusRatio: 0.0048,
|
||||||
|
ringLabelFontRatio: 0.032,
|
||||||
|
ringLabelDarkColor: "#111111",
|
||||||
|
ringLabelLightColor: "#ffffff",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 十字辅助线默认样式。
|
||||||
|
const defaultCrosshairStyle = {
|
||||||
|
color: "rgba(20, 20, 20, 0.38)",
|
||||||
|
lineWidthRatio: 0.0025,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 象限文字默认样式。
|
||||||
|
const defaultQuadrantLabelStyle = {
|
||||||
|
color: "#ffffff",
|
||||||
|
fontSizeRatio: 0.045,
|
||||||
|
offsetRatio: 0.78,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 高亮区域默认样式。
|
||||||
|
const defaultHighlightStyle = {
|
||||||
|
color: "rgba(254, 216, 71, 0.34)",
|
||||||
|
strokeColor: "rgba(254, 216, 71, 0.82)",
|
||||||
|
lineWidthRatio: 0.003,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 合并默认靶纸样式和外部传入样式,ringColors 单独深合并。
|
||||||
|
const mergeTargetStyleConfig = () => ({
|
||||||
|
...defaultTargetStyleConfig,
|
||||||
|
...props.targetStyleConfig,
|
||||||
|
ringColors: {
|
||||||
|
...defaultTargetStyleConfig.ringColors,
|
||||||
|
...(props.targetStyleConfig?.ringColors || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 统一把外部传入值转成有效数字,非法值使用 fallback。
|
||||||
|
const getNumber = (value, fallback = 0) => {
|
||||||
|
const numberValue = Number(value);
|
||||||
|
return Number.isFinite(numberValue) ? numberValue : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取指定环数的填充色,兼容数字 key 和字符串 key。
|
||||||
|
const getRingColor = (ring, config) => {
|
||||||
|
return config.ringColors?.[ring] || config.ringColors?.[String(ring)] || "#ffffff";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 规范化高亮环数配置;"all" 表示全部环,数组或单值会过滤非法环数。
|
||||||
|
const normalizeRings = (rings, ringCount) => {
|
||||||
|
if (rings === "all") {
|
||||||
|
return "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawRings = Array.isArray(rings) ? rings : [rings];
|
||||||
|
return rawRings
|
||||||
|
.map((ring) => Number(ring))
|
||||||
|
.filter((ring) => Number.isInteger(ring) && ring >= 1 && ring <= ringCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取象限对应的扇形弧度范围。
|
||||||
|
const getQuadrantAngles = (quadrant) => {
|
||||||
|
const angleMap = {
|
||||||
|
1: [Math.PI, Math.PI * 1.5],
|
||||||
|
2: [Math.PI * 1.5, Math.PI * 2],
|
||||||
|
3: [Math.PI * 0.5, Math.PI],
|
||||||
|
4: [0, Math.PI * 0.5],
|
||||||
|
};
|
||||||
|
|
||||||
|
return angleMap[Number(quadrant)] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取象限对应的矩形区域,用于整象限高亮。
|
||||||
|
const getQuadrantBox = (quadrant, centerX, centerY, radius) => {
|
||||||
|
const boxMap = {
|
||||||
|
1: [centerX - radius, centerY - radius, radius, radius],
|
||||||
|
2: [centerX, centerY - radius, radius, radius],
|
||||||
|
3: [centerX - radius, centerY, radius, radius],
|
||||||
|
4: [centerX, centerY, radius, radius],
|
||||||
|
};
|
||||||
|
|
||||||
|
return boxMap[Number(quadrant)] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制实心圆,靶纸环区和中心点都会用到。
|
||||||
|
const drawCircle = (ctx, centerX, centerY, radius, fillColor) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||||
|
ctx.setFillStyle(fillColor);
|
||||||
|
ctx.fill();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制环形扇区,用于按象限高亮指定环数。
|
||||||
|
const drawAnnularSector = (
|
||||||
|
ctx,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
innerRadius,
|
||||||
|
outerRadius,
|
||||||
|
startAngle,
|
||||||
|
endAngle,
|
||||||
|
fillColor,
|
||||||
|
strokeColor = "",
|
||||||
|
lineWidth = 0
|
||||||
|
) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, outerRadius, startAngle, endAngle);
|
||||||
|
|
||||||
|
if (innerRadius > 0) {
|
||||||
|
ctx.arc(centerX, centerY, innerRadius, endAngle, startAngle, true);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(centerX, centerY);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.setFillStyle(fillColor);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
if (strokeColor && lineWidth > 0) {
|
||||||
|
ctx.setStrokeStyle(strokeColor);
|
||||||
|
ctx.setLineWidth(lineWidth);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从外到内绘制完整靶纸色环。
|
||||||
|
const drawTargetRings = (ctx, centerX, centerY, targetRadius, config) => {
|
||||||
|
for (let ring = 1; ring <= config.ringCount; ring += 1) {
|
||||||
|
const radius = targetRadius * ((config.ringCount + 1 - ring) / config.ringCount);
|
||||||
|
drawCircle(ctx, centerX, centerY, radius, getRingColor(ring, config));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制所有高亮区域,支持整象限矩形高亮和指定环数扇区高亮。
|
||||||
|
const drawHighlights = (ctx, centerX, centerY, targetRadius, config) => {
|
||||||
|
props.highlightAreas.forEach((area = {}) => {
|
||||||
|
const angles = getQuadrantAngles(area.quadrant);
|
||||||
|
|
||||||
|
if (!angles) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightStyle = {
|
||||||
|
...defaultHighlightStyle,
|
||||||
|
...(area.style || {}),
|
||||||
|
};
|
||||||
|
const highlightLineWidth = Math.max(1, targetRadius * highlightStyle.lineWidthRatio);
|
||||||
|
const rings = normalizeRings(area.rings || "all", config.ringCount);
|
||||||
|
const scope = area.scope || (rings === "all" ? "box" : "sector");
|
||||||
|
|
||||||
|
// 整象限默认画成矩形高亮,便于对应 1/2/3/4 号框训练提示。
|
||||||
|
if (rings === "all" && scope === "box") {
|
||||||
|
const box = getQuadrantBox(area.quadrant, centerX, centerY, targetRadius);
|
||||||
|
if (!box) return;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(...box);
|
||||||
|
ctx.setFillStyle(highlightStyle.color);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.setStrokeStyle(highlightStyle.strokeColor);
|
||||||
|
ctx.setLineWidth(highlightLineWidth);
|
||||||
|
ctx.stroke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRings = rings === "all"
|
||||||
|
? Array.from({ length: config.ringCount }, (_, index) => index + 1)
|
||||||
|
: rings;
|
||||||
|
|
||||||
|
targetRings.forEach((ring) => {
|
||||||
|
const innerRadius = targetRadius * ((config.ringCount - ring) / config.ringCount);
|
||||||
|
const outerRadius = targetRadius * ((config.ringCount + 1 - ring) / config.ringCount);
|
||||||
|
drawAnnularSector(
|
||||||
|
ctx,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
innerRadius,
|
||||||
|
outerRadius,
|
||||||
|
angles[0],
|
||||||
|
angles[1],
|
||||||
|
highlightStyle.color,
|
||||||
|
highlightStyle.strokeColor,
|
||||||
|
highlightLineWidth
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制各环之间的分割线。
|
||||||
|
const drawRingLines = (ctx, centerX, centerY, targetRadius, config) => {
|
||||||
|
const lineWidth = Math.max(1, targetRadius * config.ringLineWidthRatio);
|
||||||
|
|
||||||
|
ctx.setStrokeStyle(config.ringLineColor);
|
||||||
|
ctx.setLineWidth(lineWidth);
|
||||||
|
|
||||||
|
for (let index = 1; index <= config.ringCount; index += 1) {
|
||||||
|
const radius = targetRadius * (index / config.ringCount);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制靶心十字辅助线。
|
||||||
|
const drawCrosshair = (ctx, centerX, centerY, targetRadius) => {
|
||||||
|
if (!props.showCrosshair) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
...defaultCrosshairStyle,
|
||||||
|
...props.crosshairStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(centerX - targetRadius, centerY);
|
||||||
|
ctx.lineTo(centerX + targetRadius, centerY);
|
||||||
|
ctx.moveTo(centerX, centerY - targetRadius);
|
||||||
|
ctx.lineTo(centerX, centerY + targetRadius);
|
||||||
|
ctx.setStrokeStyle(style.color);
|
||||||
|
ctx.setLineWidth(Math.max(1, targetRadius * style.lineWidthRatio));
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制环数文字。
|
||||||
|
const drawRingLabels = (ctx, centerX, centerY, targetRadius, config) => {
|
||||||
|
if (!props.showRingLabels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ringWidth = targetRadius / config.ringCount;
|
||||||
|
const fontSize = Math.max(10, targetRadius * config.ringLabelFontRatio);
|
||||||
|
|
||||||
|
ctx.setFontSize(fontSize);
|
||||||
|
ctx.setTextAlign("center");
|
||||||
|
ctx.setTextBaseline("middle");
|
||||||
|
|
||||||
|
for (let ring = config.ringCount; ring >= 1; ring -= 1) {
|
||||||
|
const y = centerY + (config.ringCount - ring + 0.45) * ringWidth;
|
||||||
|
const color = ring <= 2 ? config.ringLabelDarkColor : config.ringLabelLightColor;
|
||||||
|
ctx.setFillStyle(color);
|
||||||
|
ctx.fillText(String(ring), centerX, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制象限文字。
|
||||||
|
const drawQuadrantLabels = (ctx, centerX, centerY, targetRadius) => {
|
||||||
|
if (!props.showQuadrantLabels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
...defaultQuadrantLabelStyle,
|
||||||
|
...props.quadrantLabelStyle,
|
||||||
|
};
|
||||||
|
const offset = targetRadius * style.offsetRatio;
|
||||||
|
const positions = {
|
||||||
|
1: [centerX - offset, centerY - offset],
|
||||||
|
2: [centerX + offset, centerY - offset],
|
||||||
|
3: [centerX - offset, centerY + offset],
|
||||||
|
4: [centerX + offset, centerY + offset],
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.setFontSize(Math.max(12, targetRadius * style.fontSizeRatio));
|
||||||
|
ctx.setTextAlign("center");
|
||||||
|
ctx.setTextBaseline("middle");
|
||||||
|
ctx.setFillStyle(style.color);
|
||||||
|
|
||||||
|
Object.entries(positions).forEach(([key, position]) => {
|
||||||
|
const label = props.quadrantLabels?.[key] || props.quadrantLabels?.[Number(key)];
|
||||||
|
if (label === undefined || label === null || label === "") return;
|
||||||
|
ctx.fillText(String(label), position[0], position[1]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成高亮区域的绘制 key,只保留真正影响画面的字段。
|
||||||
|
const getHighlightDrawKeyAreas = () => {
|
||||||
|
return props.highlightAreas.map((area = {}) => ({
|
||||||
|
quadrant: area.quadrant,
|
||||||
|
rings: area.rings,
|
||||||
|
scope: area.scope,
|
||||||
|
style: area.style,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成本次绘制状态的唯一 key,用于避免相同内容重复 draw。
|
||||||
|
const getDrawKey = (width, height) => {
|
||||||
|
return JSON.stringify({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
coordinateRadius: props.coordinateRadius,
|
||||||
|
showCrosshair: props.showCrosshair,
|
||||||
|
showQuadrantLabels: props.showQuadrantLabels,
|
||||||
|
showRingLabels: props.showRingLabels,
|
||||||
|
highlightAreas: getHighlightDrawKeyAreas(),
|
||||||
|
targetStyleConfig: props.targetStyleConfig,
|
||||||
|
crosshairStyle: props.crosshairStyle,
|
||||||
|
quadrantLabelStyle: props.quadrantLabelStyle,
|
||||||
|
highlightOnly: props.highlightOnly,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主绘制入口:根据 highlightOnly 决定画完整靶纸,还是只画透明高亮层。
|
||||||
|
const drawTarget = () => {
|
||||||
|
const width = Math.max(getNumber(canvasSize.value.width, defaultCanvasSize), 1);
|
||||||
|
const height = Math.max(getNumber(canvasSize.value.height, defaultCanvasSize), 1);
|
||||||
|
const drawKey = getDrawKey(width, height);
|
||||||
|
|
||||||
|
if (drawKey === lastDrawKey.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = Math.min(width, height);
|
||||||
|
const centerX = width / 2;
|
||||||
|
const centerY = height / 2;
|
||||||
|
const targetRadius = size / 2;
|
||||||
|
const config = mergeTargetStyleConfig();
|
||||||
|
const ctx = uni.createCanvasContext(currentCanvasId.value, instance?.proxy);
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
if (!props.highlightOnly) {
|
||||||
|
drawTargetRings(ctx, centerX, centerY, targetRadius, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHighlights(ctx, centerX, centerY, targetRadius, config);
|
||||||
|
|
||||||
|
if (!props.highlightOnly) {
|
||||||
|
drawRingLines(ctx, centerX, centerY, targetRadius, config);
|
||||||
|
drawCircle(
|
||||||
|
ctx,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
Math.max(1, targetRadius * config.centerDotRadiusRatio),
|
||||||
|
config.centerDotColor
|
||||||
|
);
|
||||||
|
drawCrosshair(ctx, centerX, centerY, targetRadius);
|
||||||
|
drawRingLabels(ctx, centerX, centerY, targetRadius, config);
|
||||||
|
drawQuadrantLabels(ctx, centerX, centerY, targetRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.draw();
|
||||||
|
lastDrawKey.value = drawKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCanvasSizeAndDraw = async (width, height) => {
|
||||||
|
canvasSize.value = {
|
||||||
|
width: width > 0 ? width : defaultCanvasSize,
|
||||||
|
height: height > 0 ? height : width || defaultCanvasSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
drawTarget();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 读取 canvas 实际渲染尺寸后再绘制,保证小程序真机尺寸和坐标一致。
|
||||||
|
const measureAndDraw = () => {
|
||||||
|
const propWidth = Math.round(getNumber(props.canvasWidth, 0));
|
||||||
|
const propHeight = Math.round(getNumber(props.canvasHeight, 0));
|
||||||
|
|
||||||
|
if (propWidth > 0 && propHeight > 0) {
|
||||||
|
setCanvasSizeAndDraw(propWidth, propHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = uni.createSelectorQuery().in(instance?.proxy);
|
||||||
|
|
||||||
|
query
|
||||||
|
.select(`#${currentCanvasId.value}`)
|
||||||
|
.boundingClientRect(async (rect) => {
|
||||||
|
const width = Math.round(getNumber(rect?.width, defaultCanvasSize));
|
||||||
|
const height = Math.round(getNumber(rect?.height, width || defaultCanvasSize));
|
||||||
|
|
||||||
|
await setCanvasSizeAndDraw(width, height);
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 等待 Vue 完成 DOM 更新后重新测量和绘制。
|
||||||
|
const scheduleDraw = async () => {
|
||||||
|
await nextTick();
|
||||||
|
measureAndDraw();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
props.coordinateRadius,
|
||||||
|
props.showCrosshair,
|
||||||
|
props.showQuadrantLabels,
|
||||||
|
props.showRingLabels,
|
||||||
|
props.highlightAreas,
|
||||||
|
props.highlightOnly,
|
||||||
|
props.canvasWidth,
|
||||||
|
props.canvasHeight,
|
||||||
|
props.quadrantLabels,
|
||||||
|
props.targetStyleConfig,
|
||||||
|
props.crosshairStyle,
|
||||||
|
props.quadrantLabelStyle,
|
||||||
|
],
|
||||||
|
scheduleDraw,
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(measureAndDraw, 30);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas
|
||||||
|
:id="currentCanvasId"
|
||||||
|
class="target-canvas"
|
||||||
|
:canvas-id="currentCanvasId"
|
||||||
|
:width="canvasSize.width"
|
||||||
|
:height="canvasSize.height"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.target-canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -305,7 +305,7 @@ function goBack() {
|
|||||||
<Container
|
<Container
|
||||||
:bgType="data.mode > 3 ? -1 : 0"
|
:bgType="data.mode > 3 ? -1 : 0"
|
||||||
bgColor="#000000"
|
bgColor="#000000"
|
||||||
:onBack="goBack"
|
:onBack="exit"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- ----- Banner 区:game 胜负展示图(仅 NvN 对抗模式)----- -->
|
<!-- ----- Banner 区:game 胜负展示图(仅 NvN 对抗模式)----- -->
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const {
|
|||||||
updateConfig,
|
updateConfig,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateDevice,
|
updateDevice,
|
||||||
|
clearDevice,
|
||||||
getLvlName,
|
getLvlName,
|
||||||
getLvlNameByScore,
|
getLvlNameByScore,
|
||||||
updateOnline,
|
updateOnline,
|
||||||
@@ -127,6 +128,8 @@ onShow(async () => {
|
|||||||
);
|
);
|
||||||
const data = await getDeviceBatteryAPI();
|
const data = await getDeviceBatteryAPI();
|
||||||
updateOnline(data.online);
|
updateOnline(data.online);
|
||||||
|
} else {
|
||||||
|
clearDevice();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,65 @@ const playersSorted = ref([]);
|
|||||||
const playersScores = ref([]);
|
const playersScores = ref([]);
|
||||||
const halfTimeTip = ref(false);
|
const halfTimeTip = ref(false);
|
||||||
const halfRest = ref(false);
|
const halfRest = ref(false);
|
||||||
|
const HALF_REST_SECONDS = 20;
|
||||||
|
const halfRestRemain = ref(HALF_REST_SECONDS);
|
||||||
|
let halfRestTimer = null;
|
||||||
/** 控制设备离线提示弹窗的显示状态 */
|
/** 控制设备离线提示弹窗的显示状态 */
|
||||||
const showOfflineModal = ref(false);
|
const showOfflineModal = ref(false);
|
||||||
/** 记录每位玩家当前半场连续 X 环数,key 为 playerId,用于触发 tententen 音效 */
|
/** 记录每位玩家当前半场连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
|
||||||
const xRingStreaks = ref({});
|
const xRingStreaks = ref({});
|
||||||
|
|
||||||
|
function clearHalfRestCountdown() {
|
||||||
|
if (halfRestTimer) {
|
||||||
|
clearInterval(halfRestTimer);
|
||||||
|
halfRestTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHalfRestSeconds(battleInfo) {
|
||||||
|
const remainCandidates = [
|
||||||
|
battleInfo?.halfRestRemain,
|
||||||
|
battleInfo?.halfRestRemainSeconds,
|
||||||
|
battleInfo?.restRemain,
|
||||||
|
battleInfo?.restRemainSeconds,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const item of remainCandidates) {
|
||||||
|
const remain = Number(item);
|
||||||
|
if (Number.isFinite(remain) && remain > 0 && remain <= HALF_REST_SECONDS) {
|
||||||
|
return Math.ceil(remain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = Number(battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime);
|
||||||
|
if (!Number.isFinite(endTime) || endTime <= 0) return HALF_REST_SECONDS;
|
||||||
|
|
||||||
|
const timestamp = endTime < 1e12 ? endTime * 1000 : endTime;
|
||||||
|
const diffSeconds = (timestamp - Date.now()) / 1000;
|
||||||
|
if (diffSeconds > 0 && diffSeconds <= HALF_REST_SECONDS) {
|
||||||
|
return Math.ceil(diffSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HALF_REST_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startHalfRestCountdown(seconds = HALF_REST_SECONDS) {
|
||||||
|
clearHalfRestCountdown();
|
||||||
|
halfRestRemain.value = Math.max(0, Math.ceil(Number(seconds) || HALF_REST_SECONDS));
|
||||||
|
|
||||||
|
if (halfRestRemain.value <= 0) return;
|
||||||
|
|
||||||
|
halfRestTimer = setInterval(() => {
|
||||||
|
if (halfRestRemain.value <= 1) {
|
||||||
|
halfRestRemain.value = 0;
|
||||||
|
clearHalfRestCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
halfRestRemain.value -= 1;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户
|
* 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户
|
||||||
*/
|
*/
|
||||||
@@ -91,8 +145,7 @@ function recoverData(battleInfo, { force = false } = {}) {
|
|||||||
halfTimeTip.value = true;
|
halfTimeTip.value = true;
|
||||||
halfRest.value = true;
|
halfRest.value = true;
|
||||||
tips.value = "准备下半场";
|
tips.value = "准备下半场";
|
||||||
// 剩余休息时间
|
startHalfRestCountdown(getHalfRestSeconds(battleInfo));
|
||||||
// const remain = (Date.now() - battleInfo.timeoutTime) / 1000;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.$emit("update-remain", 0);
|
uni.$emit("update-remain", 0);
|
||||||
}, 200);
|
}, 200);
|
||||||
@@ -123,23 +176,27 @@ onLoad(async (options) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测指定玩家连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
* 检测指定玩家连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
||||||
* @param {number|string} playerId - 本次射手的 ID(大乱斗中 ShootResult 保留 playerId)
|
* @param {number|string} playerId - 本次射手的 ID(大乱斗中 ShootResult 保留 playerId)
|
||||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||||
*/
|
*/
|
||||||
function checkAndPlayTententen(playerId, isXRing) {
|
function isTenPlusRing(shot) {
|
||||||
|
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndPlayTententen(playerId, isTenPlusRingShot) {
|
||||||
if (!playerId) return;
|
if (!playerId) return;
|
||||||
const id = parseInt(playerId);
|
const id = parseInt(playerId);
|
||||||
if (isXRing) {
|
if (isTenPlusRingShot) {
|
||||||
xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
|
xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
|
||||||
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放
|
// 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
|
||||||
if (xRingStreaks.value[id] >= 3) {
|
if (xRingStreaks.value[id] >= 3) {
|
||||||
xRingStreaks.value[id] = 0;
|
xRingStreaks.value[id] = 0;
|
||||||
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
||||||
nextTick(() => audioManager.play("tententen", false));
|
nextTick(() => audioManager.play("tententen", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 X 环则重置该玩家的连续计数
|
// 低于 10 环或未上靶则重置该玩家的连续计数
|
||||||
xRingStreaks.value[id] = 0;
|
xRingStreaks.value[id] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +204,7 @@ function checkAndPlayTententen(playerId, isXRing) {
|
|||||||
async function onReceiveMessage(msg) {
|
async function onReceiveMessage(msg) {
|
||||||
if (Array.isArray(msg)) return;
|
if (Array.isArray(msg)) return;
|
||||||
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||||
|
clearHalfRestCountdown();
|
||||||
halfTimeTip.value = false;
|
halfTimeTip.value = false;
|
||||||
halfRest.value = false;
|
halfRest.value = false;
|
||||||
recoverData(msg);
|
recoverData(msg);
|
||||||
@@ -161,22 +219,23 @@ async function onReceiveMessage(msg) {
|
|||||||
// 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据
|
// 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据
|
||||||
const newRound = playersScores.value[playersScores.value.length - 1] || {};
|
const newRound = playersScores.value[playersScores.value.length - 1] || {};
|
||||||
let shooterId = null;
|
let shooterId = null;
|
||||||
let isXRing = false;
|
let isTenPlusRingShot = false;
|
||||||
for (const pid of Object.keys(newRound)) {
|
for (const pid of Object.keys(newRound)) {
|
||||||
const newLen = (newRound[pid] || []).length;
|
const newLen = (newRound[pid] || []).length;
|
||||||
if (newLen > (prevCounts[pid] || 0)) {
|
if (newLen > (prevCounts[pid] || 0)) {
|
||||||
shooterId = parseInt(pid);
|
shooterId = parseInt(pid);
|
||||||
const shot = newRound[pid][newLen - 1];
|
const shot = newRound[pid][newLen - 1];
|
||||||
isXRing = !!(shot?.ringX && shot?.ring);
|
isTenPlusRingShot = isTenPlusRing(shot);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 检测同一玩家三箭全 X 环,触发 tententen 音效
|
// 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
|
||||||
checkAndPlayTententen(shooterId, isXRing);
|
checkAndPlayTententen(shooterId, isTenPlusRingShot);
|
||||||
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
|
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
|
||||||
halfTimeTip.value = true;
|
halfTimeTip.value = true;
|
||||||
halfRest.value = true;
|
halfRest.value = true;
|
||||||
tips.value = "准备下半场";
|
tips.value = "准备下半场";
|
||||||
|
startHalfRestCountdown();
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 全部跳转到新结算页
|
// 全部跳转到新结算页
|
||||||
@@ -197,6 +256,7 @@ onBeforeUnmount(() => {
|
|||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
|
clearHalfRestCountdown();
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
});
|
});
|
||||||
@@ -261,7 +321,7 @@ onShow(async () => {
|
|||||||
>
|
>
|
||||||
<view class="half-time-tip">
|
<view class="half-time-tip">
|
||||||
<text>上半场结束,休息一下吧:)</text>
|
<text>上半场结束,休息一下吧:)</text>
|
||||||
<text>20秒后开始下半场</text>
|
<text>{{ halfRestRemain }}秒后开始下半场</text>
|
||||||
</view>
|
</view>
|
||||||
</ScreenHint>
|
</ScreenHint>
|
||||||
<!-- 设备离线提示弹窗 -->
|
<!-- 设备离线提示弹窗 -->
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const showTip = ref(false);
|
|||||||
const confirmBindTip = ref(false);
|
const confirmBindTip = ref(false);
|
||||||
const addDevice = ref();
|
const addDevice = ref();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { updateDevice } = store;
|
const { updateDevice, clearDevice } = store;
|
||||||
const { user, device } = storeToRefs(store);
|
const { user, device } = storeToRefs(store);
|
||||||
const justBind = ref(false);
|
const justBind = ref(false);
|
||||||
const calibration = ref(false);
|
const calibration = ref(false);
|
||||||
@@ -84,13 +84,21 @@ const toFristTryPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const unbindDevice = async () => {
|
const unbindDevice = async () => {
|
||||||
await unbindDeviceAPI(device.value.deviceId);
|
try {
|
||||||
|
await unbindDeviceAPI(device.value.deviceId);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.type === "DEVICE_BIND_INVALID") {
|
||||||
|
uni.setStorageSync("calibration", false);
|
||||||
|
clearDevice();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
uni.setStorageSync("calibration", false);
|
uni.setStorageSync("calibration", false);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "解绑成功",
|
title: "解绑成功",
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
device.value = {};
|
clearDevice();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toDeviceIntroPage = () => {
|
const toDeviceIntroPage = () => {
|
||||||
@@ -122,8 +130,23 @@ const goCalibration = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onShow(() => {
|
const syncDeviceBinding = async () => {
|
||||||
|
if (!user.value.id) return;
|
||||||
|
try {
|
||||||
|
const devices = await getMyDevicesAPI();
|
||||||
|
if (devices.bindings && devices.bindings.length) {
|
||||||
|
updateDevice(devices.bindings[0].deviceId, devices.bindings[0].deviceName);
|
||||||
|
} else {
|
||||||
|
clearDevice();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("sync device binding error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
calibration.value = uni.getStorageSync("calibration");
|
calibration.value = uni.getStorageSync("calibration");
|
||||||
|
await syncDeviceBinding();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -396,7 +419,7 @@ onShow(() => {
|
|||||||
width: 140rpx;
|
width: 140rpx;
|
||||||
height: 140rpx;
|
height: 140rpx;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
.device-binded > view > text {
|
.device-binded > view > text {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const { user } = storeToRefs(store);
|
|||||||
const start = ref(false);
|
const start = ref(false);
|
||||||
const scores = ref([]);
|
const scores = ref([]);
|
||||||
const total = 12;
|
const total = 12;
|
||||||
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
|
/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
|
||||||
const xRingStreak = ref(0);
|
const xRingStreak = ref(0);
|
||||||
const practiseResult = ref({});
|
const practiseResult = ref({});
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
@@ -48,7 +48,7 @@ onLoad((options) => {
|
|||||||
const onReady = async () => {
|
const onReady = async () => {
|
||||||
await startPractiseAPI();
|
await startPractiseAPI();
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
xRingStreak.value = 0; // 新一局开始,重置 X 环连续计数
|
xRingStreak.value = 0; // 新一局开始,重置 10 环及以上连续计数
|
||||||
start.value = true;
|
start.value = true;
|
||||||
audioManager.play("练习开始");
|
audioManager.play("练习开始");
|
||||||
};
|
};
|
||||||
@@ -59,19 +59,23 @@ const onOver = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效
|
* 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
|
||||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||||
*/
|
*/
|
||||||
function checkAndPlayTententen(isXRing) {
|
function isTenPlusRing(shot) {
|
||||||
if (isXRing) {
|
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndPlayTententen(isTenPlusRingShot) {
|
||||||
|
if (isTenPlusRingShot) {
|
||||||
xRingStreak.value += 1;
|
xRingStreak.value += 1;
|
||||||
// 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
// 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
||||||
if (xRingStreak.value >= 3) {
|
if (xRingStreak.value >= 3) {
|
||||||
xRingStreak.value = 0;
|
xRingStreak.value = 0;
|
||||||
nextTick(() => audioManager.play("tententen", false));
|
nextTick(() => audioManager.play("tententen", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 X 环则重置连续计数
|
// 低于 10 环或未上靶则重置连续计数
|
||||||
xRingStreak.value = 0;
|
xRingStreak.value = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,10 +84,10 @@ async function onReceiveMessage(msg) {
|
|||||||
if (msg.type === MESSAGETYPESV2.ShootResult) {
|
if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
const prevLen = scores.value.length;
|
const prevLen = scores.value.length;
|
||||||
scores.value = msg.details;
|
scores.value = msg.details;
|
||||||
// 有新箭时取最后一箭判断是否 X 环并检测连续计数
|
// 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
|
||||||
if (scores.value.length > prevLen) {
|
if (scores.value.length > prevLen) {
|
||||||
const latestArrow = scores.value[scores.value.length - 1];
|
const latestArrow = scores.value[scores.value.length - 1];
|
||||||
checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring));
|
checkAndPlayTententen(isTenPlusRing(latestArrow));
|
||||||
}
|
}
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
// setTimeout(onOver, 1500);
|
// setTimeout(onOver, 1500);
|
||||||
@@ -101,7 +105,7 @@ async function onComplete() {
|
|||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
xRingStreak.value = 0; // 重新开始练习,重置 X 环连续计数
|
xRingStreak.value = 0; // 重新开始练习,重置 10 环及以上连续计数
|
||||||
const result = await createPractiseAPI(total, 120);
|
const result = await createPractiseAPI(total, 120);
|
||||||
if (result) practiseId.value = result.id;
|
if (result) practiseId.value = result.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const { user } = storeToRefs(store);
|
|||||||
const start = ref(false);
|
const start = ref(false);
|
||||||
const scores = ref([]);
|
const scores = ref([]);
|
||||||
const total = 36;
|
const total = 36;
|
||||||
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
|
/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
|
||||||
const xRingStreak = ref(0);
|
const xRingStreak = ref(0);
|
||||||
const practiseResult = ref({});
|
const practiseResult = ref({});
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
@@ -47,7 +47,7 @@ onLoad((options) => {
|
|||||||
const onReady = async () => {
|
const onReady = async () => {
|
||||||
await startPractiseAPI();
|
await startPractiseAPI();
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
xRingStreak.value = 0; // 新一局开始,重置 X 环连续计数
|
xRingStreak.value = 0; // 新一局开始,重置 10 环及以上连续计数
|
||||||
start.value = true;
|
start.value = true;
|
||||||
audioManager.play("练习开始");
|
audioManager.play("练习开始");
|
||||||
};
|
};
|
||||||
@@ -58,19 +58,23 @@ const onOver = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效
|
* 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
|
||||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||||
*/
|
*/
|
||||||
function checkAndPlayTententen(isXRing) {
|
function isTenPlusRing(shot) {
|
||||||
if (isXRing) {
|
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndPlayTententen(isTenPlusRingShot) {
|
||||||
|
if (isTenPlusRingShot) {
|
||||||
xRingStreak.value += 1;
|
xRingStreak.value += 1;
|
||||||
// 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
// 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
||||||
if (xRingStreak.value >= 3) {
|
if (xRingStreak.value >= 3) {
|
||||||
xRingStreak.value = 0;
|
xRingStreak.value = 0;
|
||||||
nextTick(() => audioManager.play("tententen", false));
|
nextTick(() => audioManager.play("tententen", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 X 环则重置连续计数
|
// 低于 10 环或未上靶则重置连续计数
|
||||||
xRingStreak.value = 0;
|
xRingStreak.value = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,10 +83,10 @@ async function onReceiveMessage(msg) {
|
|||||||
if (msg.type === MESSAGETYPESV2.ShootResult) {
|
if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
const prevLen = scores.value.length;
|
const prevLen = scores.value.length;
|
||||||
scores.value = msg.details;
|
scores.value = msg.details;
|
||||||
// 有新箭时取最后一箭判断是否 X 环并检测连续计数
|
// 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
|
||||||
if (scores.value.length > prevLen) {
|
if (scores.value.length > prevLen) {
|
||||||
const latestArrow = scores.value[scores.value.length - 1];
|
const latestArrow = scores.value[scores.value.length - 1];
|
||||||
checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring));
|
checkAndPlayTententen(isTenPlusRing(latestArrow));
|
||||||
}
|
}
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
setTimeout(onOver, 1500);
|
setTimeout(onOver, 1500);
|
||||||
@@ -116,7 +120,7 @@ async function onComplete() {
|
|||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
xRingStreak.value = 0; // 重新开始练习,重置 X 环连续计数
|
xRingStreak.value = 0; // 重新开始练习,重置 10 环及以上连续计数
|
||||||
const result = await createPractiseAPI(total, 3600);
|
const result = await createPractiseAPI(total, 3600);
|
||||||
if (result) practiseId.value = result.id;
|
if (result) practiseId.value = result.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const battleWay = ref(0);
|
|||||||
const lastToSomeoneShootKey = ref("");
|
const lastToSomeoneShootKey = ref("");
|
||||||
/** 控制设备离线提示弹窗的显示状态 */
|
/** 控制设备离线提示弹窗的显示状态 */
|
||||||
const showOfflineModal = ref(false);
|
const showOfflineModal = ref(false);
|
||||||
/** 记录每位玩家当前轮连续 X 环数,key 为 playerId,用于触发 tententen 音效 */
|
/** 记录每位玩家当前轮连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
|
||||||
const xRingStreaks = ref({});
|
const xRingStreaks = ref({});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,22 +234,26 @@ function onNewRound(msg, prevRound) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测指定射手连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
* 检测指定射手连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
||||||
* @param {number} shooterId - 本次射手的 ID(取自 currentShooterId.value)
|
* @param {number} shooterId - 本次射手的 ID(取自 currentShooterId.value)
|
||||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||||
*/
|
*/
|
||||||
function checkAndPlayTententen(shooterId, isXRing) {
|
function isTenPlusRing(shot) {
|
||||||
|
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndPlayTententen(shooterId, isTenPlusRingShot) {
|
||||||
if (!shooterId) return;
|
if (!shooterId) return;
|
||||||
if (isXRing) {
|
if (isTenPlusRingShot) {
|
||||||
xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1;
|
xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1;
|
||||||
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放
|
// 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
|
||||||
if (xRingStreaks.value[shooterId] >= 3) {
|
if (xRingStreaks.value[shooterId] >= 3) {
|
||||||
xRingStreaks.value[shooterId] = 0;
|
xRingStreaks.value[shooterId] = 0;
|
||||||
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
||||||
nextTick(() => audioManager.play("tententen", false));
|
nextTick(() => audioManager.play("tententen", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 X 环则重置该玩家的连续计数
|
// 低于 10 环或未上靶则重置该玩家的连续计数
|
||||||
xRingStreaks.value[shooterId] = 0;
|
xRingStreaks.value[shooterId] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,9 +272,9 @@ async function onReceiveMessage(msg) {
|
|||||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
showRoundTip.value = false;
|
showRoundTip.value = false;
|
||||||
recoverData(msg, {arrowOnly: true});
|
recoverData(msg, {arrowOnly: true});
|
||||||
// 检测同一玩家三箭全 X 环,触发 tententen 音效
|
// 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
|
||||||
// currentShooterId 在 ToSomeoneShoot 时写入,ShootResult 不会覆盖,可靠识别本次射手
|
// currentShooterId 在 ToSomeoneShoot 时写入,ShootResult 不会覆盖,可靠识别本次射手
|
||||||
checkAndPlayTententen(currentShooterId.value, !!(msg.shootData?.ringX && msg.shootData?.ring));
|
checkAndPlayTententen(currentShooterId.value, isTenPlusRing(msg.shootData));
|
||||||
} else if (msg.type === MESSAGETYPESV2.NewRound) {
|
} else if (msg.type === MESSAGETYPESV2.NewRound) {
|
||||||
// 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮
|
// 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮
|
||||||
const prevRound = currentRound.value;
|
const prevRound = currentRound.value;
|
||||||
|
|||||||
@@ -318,6 +318,22 @@ function enqueueBattleMessage(message) {
|
|||||||
if (battleEnded && message.type !== MESSAGETYPESV2.BattleEnd) return;
|
if (battleEnded && message.type !== MESSAGETYPESV2.BattleEnd) return;
|
||||||
if (message.type === MESSAGETYPESV2.BattleEnd) battleEnded = true;
|
if (message.type === MESSAGETYPESV2.BattleEnd) battleEnded = true;
|
||||||
|
|
||||||
|
if (message.type === MESSAGETYPESV2.InvalidShot) {
|
||||||
|
const receivedAt = Date.now();
|
||||||
|
const order = ++queueOrder;
|
||||||
|
battleQueue.value.push({
|
||||||
|
message,
|
||||||
|
type: message.type,
|
||||||
|
key: `${message.type}:invalid:${receivedAt}:${order}`,
|
||||||
|
serverTime: 0,
|
||||||
|
receivedAt,
|
||||||
|
order,
|
||||||
|
});
|
||||||
|
sortBattleQueue();
|
||||||
|
runBattleQueue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 入队阶段只做排序、去重和时间边界判断,不直接改 UI。
|
// 入队阶段只做排序、去重和时间边界判断,不直接改 UI。
|
||||||
const serverTime = getServerTime(message);
|
const serverTime = getServerTime(message);
|
||||||
const key = getMessageKey(message);
|
const key = getMessageKey(message);
|
||||||
@@ -463,7 +479,8 @@ function updateGoldenRound(battleInfo) {
|
|||||||
}
|
}
|
||||||
const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
|
const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
|
||||||
const finishedGoldCount = rounds.filter((round) => !!round?.ifGold).length;
|
const finishedGoldCount = rounds.filter((round) => !!round?.ifGold).length;
|
||||||
goldenRound.value = Math.max(1, finishedGoldCount + (battleInfo.current?.playerId ? 1 : 0));
|
// goldenRound.value = Math.max(1, finishedGoldCount + (battleInfo.current?.playerId ? 1 : 0));
|
||||||
|
goldenRound.value = Math.max(1, finishedGoldCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore an info snapshot whose eventType points at the NewRound phase.
|
// Restore an info snapshot whose eventType points at the NewRound phase.
|
||||||
@@ -841,10 +858,14 @@ async function runToSomeoneShootTask(task, runId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateXRingStreak(shooterId, isXRing) {
|
function isTenPlusRing(shot) {
|
||||||
|
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateXRingStreak(shooterId, isTenPlusRingShot) {
|
||||||
if (!shooterId) return false;
|
if (!shooterId) return false;
|
||||||
const id = String(shooterId);
|
const id = String(shooterId);
|
||||||
if (!isXRing) {
|
if (!isTenPlusRingShot) {
|
||||||
xRingStreaks.value[id] = 0;
|
xRingStreaks.value[id] = 0;
|
||||||
saveXRingStreaks();
|
saveXRingStreaks();
|
||||||
return false;
|
return false;
|
||||||
@@ -889,7 +910,7 @@ async function runShootResultTask(task) {
|
|||||||
|
|
||||||
const isTententen = updateXRingStreak(
|
const isTententen = updateXRingStreak(
|
||||||
currentShooterId.value,
|
currentShooterId.value,
|
||||||
!!(battleInfo.shootData?.ringX && battleInfo.shootData?.ring)
|
isTenPlusRing(battleInfo.shootData)
|
||||||
);
|
);
|
||||||
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
|
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
|
||||||
if (isTententen) audioKeys.push("tententen");
|
if (isTententen) audioKeys.push("tententen");
|
||||||
@@ -1233,7 +1254,7 @@ onShow(() => {
|
|||||||
<view class="offline-modal">
|
<view class="offline-modal">
|
||||||
<text class="offline-title">设备已离线</text>
|
<text class="offline-title">设备已离线</text>
|
||||||
<text class="offline-desc">检测到设备已断开连接,请检查设备后继续比赛</text>
|
<text class="offline-desc">检测到设备已断开连接,请检查设备后继续比赛</text>
|
||||||
<SButton @click="showOfflineModal = false">我知道了</SButton>
|
<SButton :onClick="() => (showOfflineModal = false)">我知道了</SButton>
|
||||||
</view>
|
</view>
|
||||||
</SModal>
|
</SModal>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
116
src/pages/training/components/BowData.vue
Normal file
116
src/pages/training/components/BowData.vue
Normal 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="10" />
|
||||||
|
<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>
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
|
import {
|
||||||
|
computed,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from "vue";
|
||||||
import PointSwitcher from "@/components/PointSwitcher.vue";
|
import PointSwitcher from "@/components/PointSwitcher.vue";
|
||||||
|
import TargetCanvas from "@/components/TargetCanvas.vue";
|
||||||
|
|
||||||
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
||||||
import { simulShootAPI } from "@/apis";
|
import { simulShootAPI } from "@/apis";
|
||||||
@@ -34,6 +41,39 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
coordinateRadius: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
hitRadiusPx: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
zoomHitRadiusPx: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
showCrosshair: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showQuadrantLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
quadrantLabels: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
1: "1",
|
||||||
|
2: "2",
|
||||||
|
3: "3",
|
||||||
|
4: "4",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
highlightAreas: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const pMode = ref(true);
|
const pMode = ref(true);
|
||||||
@@ -45,6 +85,100 @@ const timer = ref(null);
|
|||||||
const dirTimer = ref(null);
|
const dirTimer = ref(null);
|
||||||
const angle = ref(null);
|
const angle = ref(null);
|
||||||
const circleColor = ref("");
|
const circleColor = ref("");
|
||||||
|
const ROUND_TIP_OFFSET_Y = -32;
|
||||||
|
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
||||||
|
|
||||||
|
const getNumber = (value, fallback = 0) => {
|
||||||
|
const numberValue = Number(value);
|
||||||
|
return Number.isFinite(numberValue) ? numberValue : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeTargetRadius = computed(() => {
|
||||||
|
return Math.max(getNumber(props.coordinateRadius, 20), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentHitRadiusPx = computed(() => {
|
||||||
|
const radius = Number(
|
||||||
|
pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
|
||||||
|
);
|
||||||
|
return Number.isFinite(radius) && radius >= 0 ? radius : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getShotPoint(shot, fallbackCenter = false) {
|
||||||
|
const x = Number(shot?.x);
|
||||||
|
const y = Number(shot?.y);
|
||||||
|
if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
|
||||||
|
return fallbackCenter ? { x: 0, y: 0 } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPointDirection(point) {
|
||||||
|
if (!point) return null;
|
||||||
|
const distance = Math.sqrt(point.x * point.x + point.y * point.y);
|
||||||
|
if (distance === 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: point.x / distance,
|
||||||
|
y: point.y / distance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPxOffset(value) {
|
||||||
|
if (!value) return "";
|
||||||
|
const operator = value > 0 ? "+" : "-";
|
||||||
|
return ` ${operator} ${Math.abs(value)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTargetPosition(percent, offset) {
|
||||||
|
const pxOffset = formatPxOffset(offset);
|
||||||
|
return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetPositionStyle(point, offsetPx = 0, extraOffset = {}) {
|
||||||
|
if (!point) return { display: "none" };
|
||||||
|
|
||||||
|
const radius = safeTargetRadius.value;
|
||||||
|
const diameter = radius * 2;
|
||||||
|
const direction = getPointDirection(point);
|
||||||
|
const xOffset = (direction ? direction.x * offsetPx : 0) + (extraOffset.x || 0);
|
||||||
|
const yOffset = (direction ? -direction.y * offsetPx : 0) + (extraOffset.y || 0);
|
||||||
|
const leftPercent = ((point.x + radius) / diameter) * 100;
|
||||||
|
const topPercent = ((radius - point.y) / diameter) * 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: formatTargetPosition(leftPercent, xOffset),
|
||||||
|
top: formatTargetPosition(topPercent, yOffset),
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHitStyle(shot) {
|
||||||
|
const radius = currentHitRadiusPx.value;
|
||||||
|
const point = getShotPoint(shot);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...getTargetPositionStyle(point, radius),
|
||||||
|
width: `${radius * 2}px`,
|
||||||
|
height: `${radius * 2}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoundTipStyle(shot) {
|
||||||
|
const point = getShotPoint(shot, true);
|
||||||
|
return getTargetPositionStyle(
|
||||||
|
point,
|
||||||
|
shot?.ring ? currentHitRadiusPx.value : 0,
|
||||||
|
{ y: ROUND_TIP_OFFSET_Y }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExperienceTipStyle(shot) {
|
||||||
|
const point = getShotPoint(shot, true);
|
||||||
|
return getTargetPositionStyle(
|
||||||
|
point,
|
||||||
|
shot?.ring ? currentHitRadiusPx.value : 0,
|
||||||
|
{ y: EXPERIENCE_TIP_OFFSET_Y }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.scores,
|
() => props.scores,
|
||||||
@@ -80,14 +214,6 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function calcRealX(num, offset = 3.4) {
|
|
||||||
const len = 20.4 + num;
|
|
||||||
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
|
|
||||||
}
|
|
||||||
function calcRealY(num, offset = 3.4) {
|
|
||||||
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
|
|
||||||
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
|
|
||||||
}
|
|
||||||
const simulShoot = async () => {
|
const simulShoot = async () => {
|
||||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||||
};
|
};
|
||||||
@@ -111,6 +237,44 @@ 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;
|
||||||
|
});
|
||||||
|
|
||||||
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) {
|
||||||
@@ -160,6 +324,21 @@ onBeforeUnmount(() => {
|
|||||||
}}</text>
|
}}</text>
|
||||||
</view> -->
|
</view> -->
|
||||||
<view class="target">
|
<view class="target">
|
||||||
|
<image
|
||||||
|
class="target-image"
|
||||||
|
src="../../../static/bow-target.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<TargetCanvas
|
||||||
|
v-if="showHighlightCanvas"
|
||||||
|
class="target-highlight-layer"
|
||||||
|
:coordinateRadius="coordinateRadius"
|
||||||
|
:showCrosshair="false"
|
||||||
|
:showQuadrantLabels="false"
|
||||||
|
:showRingLabels="false"
|
||||||
|
:highlightOnly="true"
|
||||||
|
:highlightAreas="currentHighlightAreas"
|
||||||
|
/>
|
||||||
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
|
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
|
||||||
<view :style="{ background: circleColor }">
|
<view :style="{ background: circleColor }">
|
||||||
<image src="../../../static/dot-circle.png" mode="widthFix" />
|
<image src="../../../static/dot-circle.png" mode="widthFix" />
|
||||||
@@ -169,20 +348,14 @@ onBeforeUnmount(() => {
|
|||||||
<view
|
<view
|
||||||
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
|
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
|
||||||
class="e-value fade-in-out"
|
class="e-value fade-in-out"
|
||||||
:style="{
|
:style="getExperienceTipStyle(latestOne)"
|
||||||
left: calcRealX(latestOne.ring ? latestOne.x : 0, 20),
|
|
||||||
top: calcRealY(latestOne.ring ? latestOne.y : 0, 40),
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
经验 +1
|
经验 +1
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="latestOne"
|
v-if="latestOne"
|
||||||
class="round-tip fade-in-out"
|
class="round-tip fade-in-out"
|
||||||
:style="{
|
:style="getRoundTipStyle(latestOne)"
|
||||||
left: calcRealX(latestOne.ring ? latestOne.x : 0, 28),
|
|
||||||
top: calcRealY(latestOne.ring ? latestOne.y : 0, 28),
|
|
||||||
}"
|
|
||||||
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
|
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
|
||||||
}}<text v-if="latestOne.ring">环</text>
|
}}<text v-if="latestOne.ring">环</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -193,20 +366,14 @@ onBeforeUnmount(() => {
|
|||||||
user.id === bluelatestOne.playerId
|
user.id === bluelatestOne.playerId
|
||||||
"
|
"
|
||||||
class="e-value fade-in-out"
|
class="e-value fade-in-out"
|
||||||
:style="{
|
:style="getExperienceTipStyle(bluelatestOne)"
|
||||||
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 20),
|
|
||||||
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 40),
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
经验 +1
|
经验 +1
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="bluelatestOne"
|
v-if="bluelatestOne"
|
||||||
class="round-tip fade-in-out"
|
class="round-tip fade-in-out"
|
||||||
:style="{
|
:style="getRoundTipStyle(bluelatestOne)"
|
||||||
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 28),
|
|
||||||
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 28),
|
|
||||||
}"
|
|
||||||
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
|
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
|
||||||
}}<text v-if="bluelatestOne.ring">环</text></view
|
}}<text v-if="bluelatestOne.ring">环</text></view
|
||||||
>
|
>
|
||||||
@@ -217,8 +384,7 @@ onBeforeUnmount(() => {
|
|||||||
index === scores.length - 1 && latestOne ? 'pump-in' : ''
|
index === scores.length - 1 && latestOne ? 'pump-in' : ''
|
||||||
}`"
|
}`"
|
||||||
:style="{
|
:style="{
|
||||||
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
|
...getHitStyle(bow),
|
||||||
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
|
|
||||||
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
|
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
|
||||||
}"
|
}"
|
||||||
><text v-if="pMode">{{ index + 1 }}</text></view
|
><text v-if="pMode">{{ index + 1 }}</text></view
|
||||||
@@ -231,15 +397,13 @@ onBeforeUnmount(() => {
|
|||||||
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
|
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
|
||||||
}`"
|
}`"
|
||||||
:style="{
|
:style="{
|
||||||
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
|
...getHitStyle(bow),
|
||||||
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
|
|
||||||
backgroundColor: '#1840FF',
|
backgroundColor: '#1840FF',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<text v-if="pMode">{{ index + 1 }}</text>
|
<text v-if="pMode">{{ index + 1 }}</text>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
<image src="../../../static/bow-target.png" mode="widthFix" />
|
|
||||||
</view>
|
</view>
|
||||||
<view class="footer">
|
<view class="footer">
|
||||||
<PointSwitcher
|
<PointSwitcher
|
||||||
@@ -266,7 +430,25 @@ onBeforeUnmount(() => {
|
|||||||
margin: 10px;
|
margin: 10px;
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
height: calc(100% - 20px);
|
height: calc(100% - 20px);
|
||||||
z-index: -1;
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.target-image {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.target-highlight-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.e-value {
|
.e-value {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -275,7 +457,7 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 4px 7px;
|
padding: 4px 7px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
z-index: 2;
|
z-index: 4;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -284,7 +466,7 @@ onBeforeUnmount(() => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
z-index: 2;
|
z-index: 4;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -292,31 +474,44 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
.target > image:last-child {
|
@keyframes target-tip-fade-in-out {
|
||||||
width: 100%;
|
0% {
|
||||||
height: 100%;
|
transform: translate(-50%, -50%) translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.round-tip.fade-in-out,
|
||||||
|
.e-value.fade-in-out {
|
||||||
|
animation: target-tip-fade-in-out 1.2s ease forwards;
|
||||||
}
|
}
|
||||||
.hit {
|
.hit {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
z-index: 1;
|
z-index: 3;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.s-point {
|
.s-point {
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
min-width: 4px;
|
|
||||||
min-height: 4px;
|
|
||||||
}
|
}
|
||||||
.b-point {
|
.b-point {
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
min-width: 10px;
|
|
||||||
min-height: 10px;
|
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
z-index: 1;
|
z-index: 3;
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -332,6 +527,19 @@ onBeforeUnmount(() => {
|
|||||||
transform: translate(-50%, -50%);*/
|
transform: translate(-50%, -50%);*/
|
||||||
margin-top: 2rpx;
|
margin-top: 2rpx;
|
||||||
}
|
}
|
||||||
|
@keyframes target-pump-in {
|
||||||
|
from {
|
||||||
|
transform: translate(-50%, -50%) scale(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hit.pump-in {
|
||||||
|
animation: target-pump-in 0.3s ease-out forwards;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
.header {
|
.header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -382,7 +590,7 @@ onBeforeUnmount(() => {
|
|||||||
height: 60px;
|
height: 60px;
|
||||||
left: calc(50% - 100px);
|
left: calc(50% - 100px);
|
||||||
top: calc(50% - 30px);
|
top: calc(50% - 30px);
|
||||||
z-index: 99;
|
z-index: 5;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.arrow-dir {
|
.arrow-dir {
|
||||||
@@ -391,6 +599,7 @@ onBeforeUnmount(() => {
|
|||||||
height: 52%;
|
height: 52%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 50%;
|
bottom: 50%;
|
||||||
|
z-index: 4;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
157
src/pages/training/components/ScorePanel.vue
Normal file
157
src/pages/training/components/ScorePanel.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
rowCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
arrows: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 25,
|
||||||
|
},
|
||||||
|
completeEffect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const items = ref(new Array(props.total).fill(9));
|
||||||
|
const bgImages = [
|
||||||
|
"../static/complete-light1.png",
|
||||||
|
"../static/complete-light2.png",
|
||||||
|
];
|
||||||
|
const bgIndex = ref(0);
|
||||||
|
|
||||||
|
const getDisplayText = (arrow) => {
|
||||||
|
if (!arrow) return "-";
|
||||||
|
if (arrow.ringX) return "X";
|
||||||
|
return arrow.ring ?? "-";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLowScore = (arrow) => {
|
||||||
|
if (!arrow || arrow.ringX) return false;
|
||||||
|
const ring = Number(arrow.ring);
|
||||||
|
return Number.isFinite(ring) && ring < 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.total,
|
||||||
|
(newValue) => {
|
||||||
|
items.value = new Array(newValue).fill(9);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const timer = ref(null);
|
||||||
|
onMounted(() => {
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
bgIndex.value = bgIndex.value === 0 ? 1 : 0;
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<image
|
||||||
|
v-if="total > 0 && arrows.length === total && completeEffect"
|
||||||
|
:src="bgImages[bgIndex]"
|
||||||
|
class="complete-light"
|
||||||
|
:style="{
|
||||||
|
width: `calc(${(100 / (rowCount + 2)) * rowCount}vw + ${
|
||||||
|
(100 / (total * 2)) * (rowCount * 2 + (total === 12 ? 8 : 24))
|
||||||
|
}px)`,
|
||||||
|
height: `calc(${(100 / (rowCount + 2)) * (total / rowCount)}vw + ${
|
||||||
|
(100 / (total * 2)) *
|
||||||
|
((total / rowCount) * 2 + (total === 12 ? 7 : 24))
|
||||||
|
}px)`,
|
||||||
|
top: `${total === 12 ? -2 : -3}vw`,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-for="(_, index) in items"
|
||||||
|
:key="index"
|
||||||
|
class="score-item"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="score-item-bg"
|
||||||
|
:src="
|
||||||
|
isLowScore(arrows[index])
|
||||||
|
? '/static/training-difficulty-design/block-gray.png'
|
||||||
|
: '/static/training-difficulty-design/block-gold.png'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
class="score-value"
|
||||||
|
:class="{ 'score-value--low': isLowScore(arrows[index]) }"
|
||||||
|
>
|
||||||
|
{{ getDisplayText(arrows[index]) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
padding: 30rpx 40rpx 0 40rpx;
|
||||||
|
}
|
||||||
|
.score-item {
|
||||||
|
position: relative;
|
||||||
|
width: 100rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 14rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-item:nth-child(6n) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-item-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.complete-light {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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";
|
||||||
|
|||||||
89
src/pages/training/components/ScreenHint.vue
Normal file
89
src/pages/training/components/ScreenHint.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script setup>
|
||||||
|
import IconButton from "@/components/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>
|
||||||
@@ -206,7 +206,12 @@ onBeforeUnmount(() => {
|
|||||||
<view class="progress-card__header">
|
<view class="progress-card__header">
|
||||||
<view class="progress-card__profile">
|
<view class="progress-card__profile">
|
||||||
<view class="progress-card__avatar-shell">
|
<view class="progress-card__avatar-shell">
|
||||||
<Avatar :src="user.avatar" :size="40" />
|
<Avatar
|
||||||
|
:src="avatarSrc"
|
||||||
|
:size="80"
|
||||||
|
size-unit="rpx"
|
||||||
|
image-mode="aspectFill"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
<text class="progress-card__name">{{ displayName }}</text>
|
<text class="progress-card__name">{{ displayName }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -271,6 +276,9 @@ onBeforeUnmount(() => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(180deg, rgba(255, 209, 153, 1), rgba(162, 119, 55, 1));
|
background: linear-gradient(180deg, rgba(255, 209, 153, 1), rgba(162, 119, 55, 1));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-card__avatar {
|
.progress-card__avatar {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { computed, nextTick, ref } from "vue";
|
import { computed, nextTick, ref } from "vue";
|
||||||
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import TargetPicker from "@/components/TargetPicker.vue";
|
|
||||||
import TrainingDifficultyBadge from "./components/TrainingDifficultyBadge.vue";
|
import TrainingDifficultyBadge from "./components/TrainingDifficultyBadge.vue";
|
||||||
import TrainingDifficultyPreviewCard from "./components/TrainingDifficultyPreviewCard.vue";
|
import TrainingDifficultyPreviewCard from "./components/TrainingDifficultyPreviewCard.vue";
|
||||||
import TrainingDifficultyStartButton from "./components/TrainingDifficultyStartButton.vue";
|
import TrainingDifficultyStartButton from "./components/TrainingDifficultyStartButton.vue";
|
||||||
@@ -40,6 +39,7 @@ const routeModeTypeMap = {
|
|||||||
precision: "precision",
|
precision: "precision",
|
||||||
rhythm: "rhythm",
|
rhythm: "rhythm",
|
||||||
};
|
};
|
||||||
|
const defaultTargetType = 1;
|
||||||
|
|
||||||
const resolveTrainingType = (mode) => {
|
const resolveTrainingType = (mode) => {
|
||||||
const normalizedMode = String(mode || "").toLowerCase();
|
const normalizedMode = String(mode || "").toLowerCase();
|
||||||
@@ -232,7 +232,6 @@ const emptyDifficulty = {
|
|||||||
const pageConfig = ref(createEmptyModeConfig(defaultTrainingType));
|
const pageConfig = ref(createEmptyModeConfig(defaultTrainingType));
|
||||||
const unlockedDifficultyId = ref(defaultUnlockedDifficultyId);
|
const unlockedDifficultyId = ref(defaultUnlockedDifficultyId);
|
||||||
const selectedDifficultyId = ref(defaultUnlockedDifficultyId);
|
const selectedDifficultyId = ref(defaultUnlockedDifficultyId);
|
||||||
const showTargetPicker = ref(false);
|
|
||||||
const nodesScrollTop = ref(0);
|
const nodesScrollTop = ref(0);
|
||||||
const nodesScrollWithAnimation = ref(false);
|
const nodesScrollWithAnimation = ref(false);
|
||||||
const routeOptions = ref({});
|
const routeOptions = ref({});
|
||||||
@@ -555,11 +554,63 @@ const initPageState = async (options = {}, refreshOptions = {}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveTargetPaperType = (target) => {
|
const cleanQueryValue = (value) => {
|
||||||
return Number(target) === 1 ? "20厘米全环靶" : "40厘米全环靶";
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number" && !Number.isFinite(value)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveTrainingContext = (target) => {
|
const createPracticeQuery = (difficulty) => {
|
||||||
|
const trainingType = pageConfig.value.key || defaultTrainingType;
|
||||||
|
const commonQuery = {
|
||||||
|
type: trainingType,
|
||||||
|
difficultyId: difficulty.id,
|
||||||
|
difficulty: difficulty.level,
|
||||||
|
recordId: difficulty.recordId,
|
||||||
|
arrows: toNumber(difficulty.arrows, 12),
|
||||||
|
time: toNumber(difficulty.time_limit, 120) || 120,
|
||||||
|
target: defaultTargetType,
|
||||||
|
};
|
||||||
|
const typedQueryMap = {
|
||||||
|
base: {
|
||||||
|
hitReq: toNumber(difficulty.hit_req),
|
||||||
|
},
|
||||||
|
endurance: {
|
||||||
|
totalReq: toNumber(difficulty.total_req),
|
||||||
|
},
|
||||||
|
precision: {
|
||||||
|
blocks: toNumber(difficulty.blocks),
|
||||||
|
mode: toNumber(difficulty.mode),
|
||||||
|
},
|
||||||
|
rhythm: {
|
||||||
|
hitReq: toNumber(difficulty.hit_req),
|
||||||
|
mode: toNumber(difficulty.mode),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...commonQuery,
|
||||||
|
...(typedQueryMap[trainingType] || {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPracticeUrl = (difficulty) => {
|
||||||
|
const query = Object.entries(createPracticeQuery(difficulty))
|
||||||
|
.map(([key, value]) => [key, cleanQueryValue(value)])
|
||||||
|
.filter(([, value]) => value !== "")
|
||||||
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
return `/pages/training/practise-one${query ? `?${query}` : ""}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveTrainingContext = () => {
|
||||||
const difficulty = selectedDifficulty.value;
|
const difficulty = selectedDifficulty.value;
|
||||||
|
|
||||||
if (!difficulty.id) {
|
if (!difficulty.id) {
|
||||||
@@ -571,9 +622,8 @@ const saveTrainingContext = (target) => {
|
|||||||
trainingTitle: pageConfig.value.title,
|
trainingTitle: pageConfig.value.title,
|
||||||
difficultyId: difficulty.id,
|
difficultyId: difficulty.id,
|
||||||
difficultyLabel: difficulty.label,
|
difficultyLabel: difficulty.label,
|
||||||
targetPaperType: target
|
targetType: defaultTargetType,
|
||||||
? resolveTargetPaperType(target)
|
targetPaperType: difficulty.targetPaperType,
|
||||||
: difficulty.targetPaperType,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -606,30 +656,8 @@ const handleStart = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveTrainingContext();
|
saveTrainingContext();
|
||||||
uni.showToast({
|
|
||||||
title: `${selectedDifficulty.value.title} 即将开始`,
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const openTargetPicker = () => {
|
|
||||||
if (!selectedDifficulty.value.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showTargetPicker.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTargetConfirm = (target) => {
|
|
||||||
showTargetPicker.value = false;
|
|
||||||
saveTrainingContext(target);
|
|
||||||
// uni.showToast({
|
|
||||||
// title: `${selectedDifficulty.value.title} 即将开始`,
|
|
||||||
// icon: "none",
|
|
||||||
// });
|
|
||||||
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/training/practise-one?target=${target}`,
|
url: createPracticeUrl(selectedDifficulty.value),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -708,15 +736,10 @@ onUnload(() => {
|
|||||||
<view class="difficulty-page__start">
|
<view class="difficulty-page__start">
|
||||||
<TrainingDifficultyStartButton
|
<TrainingDifficultyStartButton
|
||||||
:text="selectedDifficulty.startText"
|
:text="selectedDifficulty.startText"
|
||||||
@click="openTargetPicker"
|
@click="handleStart"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<TargetPicker
|
|
||||||
:show="showTargetPicker"
|
|
||||||
:onClose="() => (showTargetPicker = false)"
|
|
||||||
:onConfirm="handleTargetConfirm"
|
|
||||||
/>
|
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import ShootProgress from "./components/ShootProgress.vue";
|
import ShootProgress from "./components/ShootProgress.vue";
|
||||||
@@ -29,35 +29,192 @@ 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 total = 12;
|
const defaultTotal = 12;
|
||||||
|
const defaultShootTime = 120;
|
||||||
|
const defaultTargetType = 1;
|
||||||
|
const total = ref(defaultTotal);
|
||||||
|
const shootTime = ref(defaultShootTime);
|
||||||
const practiseResult = ref({});
|
const practiseResult = ref({});
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const tips = ref("");
|
const tips = ref("");
|
||||||
const targetType = ref(1);
|
const targetType = ref(defaultTargetType);
|
||||||
|
const trainingParams = ref({});
|
||||||
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
|
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
|
||||||
|
const useHighlightTest = ref(false);
|
||||||
|
const highlightTestTimer = ref(null);
|
||||||
|
|
||||||
onLoad((options) => {
|
const env = computed(() => {
|
||||||
if (options.target) {
|
try {
|
||||||
targetType.value = Number(options.target);
|
return uni.getAccountInfoSync().miniProgram.envVersion;
|
||||||
|
} catch (error) {
|
||||||
|
return "release";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onReady = async () => {
|
const isDistanceStage = computed(() => pageStage.value === pageStages.DISTANCE);
|
||||||
await startPractiseAPI();
|
const isShootingStage = computed(() => pageStage.value === pageStages.SHOOTING);
|
||||||
scores.value = [];
|
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;
|
start.value = true;
|
||||||
audioManager.play("练习开始");
|
|
||||||
|
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 () => {
|
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);
|
||||||
@@ -65,17 +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();
|
||||||
|
useHighlightTest.value = false;
|
||||||
practiseId.value = "";
|
practiseId.value = "";
|
||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
const result = await createPractiseAPI(total, 120, targetType.value);
|
try {
|
||||||
if (result) practiseId.value = result.id;
|
await createPractice();
|
||||||
|
} finally {
|
||||||
|
pageStage.value = pageStages.DISTANCE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickShare = debounce(async () => {
|
const onClickShare = debounce(async () => {
|
||||||
@@ -103,8 +268,7 @@ onMounted(async () => {
|
|||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
uni.$on("share-image", onClickShare);
|
uni.$on("share-image", onClickShare);
|
||||||
uni.$on("audioEnded", onAudioEnded);
|
uni.$on("audioEnded", onAudioEnded);
|
||||||
const result = await createPractiseAPI(total, 120, targetType.value);
|
await createPractice();
|
||||||
if (result) practiseId.value = result.id;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -115,70 +279,99 @@ onBeforeUnmount(() => {
|
|||||||
uni.$off("share-image", onClickShare);
|
uni.$off("share-image", onClickShare);
|
||||||
uni.$off("audioEnded", onAudioEnded);
|
uni.$off("audioEnded", onAudioEnded);
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
|
clearHighlightTestTimer();
|
||||||
endPractiseAPI();
|
endPractiseAPI();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container
|
<Container
|
||||||
:bgType="!start && !practiseResult.id?9:10"
|
:bgType="isDistanceStage ? 9 : 10"
|
||||||
:showBottom="!start && !scores.length"
|
:showBottom="isDistanceStage"
|
||||||
|
: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"
|
||||||
/>
|
:scores="scores"
|
||||||
<view class="sound-text-box">
|
:showCrosshair="false"
|
||||||
<button class="sound-btn" hover-class="none" @click="updateSound">
|
:highlightAreas="targetHighlightAreas"
|
||||||
<image
|
/>
|
||||||
class="sound-icon"
|
<view v-if="env !== 'release'" class="highlight-test-actions">
|
||||||
:src="`/static/sound${sound ? '' : '-off'}-yellow.png`"
|
<button
|
||||||
mode="aspectFit"
|
class="highlight-test-btn"
|
||||||
/>
|
hover-class="none"
|
||||||
</button>
|
@click="runHighlightTest"
|
||||||
<view class="bat-text-big-box">
|
>
|
||||||
<image
|
高亮测试
|
||||||
class="dao-icon"
|
</button>
|
||||||
src="../../static/training-difficulty-design/dao-icon.png"
|
<button
|
||||||
mode="widthFix"
|
class="highlight-test-btn"
|
||||||
/>
|
hover-class="none"
|
||||||
<view class="bat-text-box">
|
@click="resetHighlightTest"
|
||||||
<view class="bat-text-small-box">
|
>
|
||||||
<view class="text-round-box">
|
重置高亮
|
||||||
<view class="text1">每箭命中9环之上</view>
|
</button>
|
||||||
<view class="text2">剩余<text class="text2-yellow">3</text>箭</view>
|
</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>
|
</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">
|
||||||
@@ -194,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;
|
||||||
@@ -221,6 +438,33 @@ onBeforeUnmount(() => {
|
|||||||
bottom: -36rpx;
|
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{
|
.sound-text-box{
|
||||||
height: 125rpx;
|
height: 125rpx;
|
||||||
padding: 0 56rpx;
|
padding: 0 56rpx;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 12 KiB |
@@ -137,6 +137,10 @@ export default defineStore("store", {
|
|||||||
this.device.deviceId = deviceId;
|
this.device.deviceId = deviceId;
|
||||||
this.device.deviceName = deviceName;
|
this.device.deviceName = deviceName;
|
||||||
},
|
},
|
||||||
|
clearDevice() {
|
||||||
|
this.device = getDefaultDevice();
|
||||||
|
this.online = false;
|
||||||
|
},
|
||||||
async updateConfig(config) {
|
async updateConfig(config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
if (this.user.scores !== undefined) {
|
if (this.user.scores !== undefined) {
|
||||||
|
|||||||
Reference in New Issue
Block a user