Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d5866b82a | |||
| 2a5394155a | |||
| ef5b97530a | |||
| d932ce3dea | |||
| b8d1654476 | |||
| c5a8100c38 | |||
| 1e568db861 | |||
| fe8b38bc6f | |||
| ef2a71f793 |
1
.gitignore
vendored
@@ -10,7 +10,6 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
.history
|
.history
|
||||||
.github
|
.github
|
||||||
.claude
|
|
||||||
openspec
|
openspec
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
docs
|
docs
|
||||||
|
|||||||
@@ -34,6 +34,18 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
targetRadius: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
hitRadiusPx: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
zoomHitRadiusPx: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const pMode = ref(true);
|
const pMode = ref(true);
|
||||||
@@ -45,6 +57,8 @@ 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;
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.scores,
|
() => props.scores,
|
||||||
@@ -80,13 +94,92 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function calcRealX(num, offset = 3.4) {
|
const safeTargetRadius = computed(() => {
|
||||||
const len = 20.4 + num;
|
const radius = Number(props.targetRadius);
|
||||||
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
|
return Number.isFinite(radius) && radius > 0 ? radius : 20;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 calcRealY(num, offset = 3.4) {
|
|
||||||
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
|
function getPointDirection(point) {
|
||||||
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
|
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 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const simulShoot = async () => {
|
const simulShoot = async () => {
|
||||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||||
@@ -169,20 +262,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 +280,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 +298,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,8 +311,7 @@ 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',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@@ -292,6 +371,31 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
@keyframes target-tip-fade-in-out {
|
||||||
|
0% {
|
||||||
|
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;
|
||||||
|
}
|
||||||
.target > image:last-child {
|
.target > image:last-child {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -302,21 +406,11 @@ onBeforeUnmount(() => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
box-sizing: border-box;
|
||||||
.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: 1;
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -332,6 +426,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;
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ onBeforeUnmount(() => {
|
|||||||
</view>
|
</view>
|
||||||
<block
|
<block
|
||||||
v-if="
|
v-if="
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) === -1 ||
|
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === -1 ||
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) === 10
|
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === 11
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<text>{{ title }}</text>
|
<text>{{ title }}</text>
|
||||||
@@ -151,12 +151,12 @@ onBeforeUnmount(() => {
|
|||||||
<block
|
<block
|
||||||
v-if="
|
v-if="
|
||||||
title &&
|
title &&
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) !== -1 &&
|
'-箭前准备-感知距离-小试牛刀'.indexOf(title) !== -1 &&
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) !== 10
|
'-箭前准备-感知距离-小试牛刀'.indexOf(title) !== 11
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<view class="first-try-steps">
|
<view class="first-try-steps">
|
||||||
<text :class="title === '-凹造型' ? 'current-step' : ''">凹造型</text>
|
<text :class="title === '-箭前准备' ? 'current-step' : ''">箭前准备</text>
|
||||||
<text>-</text>
|
<text>-</text>
|
||||||
<text :class="title === '-感知距离' ? 'current-step' : ''"
|
<text :class="title === '-感知距离' ? 'current-step' : ''"
|
||||||
>感知距离</text
|
>感知距离</text
|
||||||
|
|||||||
@@ -1,396 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from "vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
type: String,
|
|
||||||
default: "new_version", // new_version | update_progress | update_success | update_failure
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
progress: {
|
|
||||||
type: Number,
|
|
||||||
default: 40,
|
|
||||||
},
|
|
||||||
// 副标题:如“新版本将优化智能弓体验”
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
// 详细说明:如“升级前请确保:...”
|
|
||||||
changelog: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
forceUpdate: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["update", "skip", "close", "done", "retry"]);
|
|
||||||
|
|
||||||
const isNewVersion = computed(() => props.state === "new_version");
|
|
||||||
const isProgress = computed(() => props.state === "update_progress");
|
|
||||||
const isSuccess = computed(() => props.state === "update_success");
|
|
||||||
const isFailure = computed(() => props.state === "update_failure");
|
|
||||||
// Clamp progress to keep the progress bar width within its container.
|
|
||||||
const progressValue = computed(() => Math.min(100, Math.max(0, Number(props.progress) || 0)));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<view v-if="visible" class="ota-mask">
|
|
||||||
<!-- 图标 + 弹窗卡片 容器 -->
|
|
||||||
<view
|
|
||||||
class="ota-outer"
|
|
||||||
:class="isNewVersion ? 'outer-new' : 'outer-result'"
|
|
||||||
>
|
|
||||||
<!-- 悬浮图标(溢出卡片顶部) -->
|
|
||||||
<image
|
|
||||||
v-if="isNewVersion"
|
|
||||||
src="../static/ota/ota-mascot.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
class="float-icon float-mascot"
|
|
||||||
/>
|
|
||||||
<image
|
|
||||||
v-else-if="isSuccess"
|
|
||||||
src="../static/ota/check-char.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
class="float-icon float-check"
|
|
||||||
/>
|
|
||||||
<image
|
|
||||||
v-else-if="isFailure"
|
|
||||||
src="../static/ota/close-char.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
class="float-icon float-close"
|
|
||||||
/>
|
|
||||||
<image
|
|
||||||
v-else-if="isProgress"
|
|
||||||
src="../static/ota/target-char.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
class="float-icon float-target"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 弹窗卡片:overflow:visible 允许按钮溢出底部,背景图通过 ota-bg-clip 独立裁剪保持圆角 -->
|
|
||||||
<view class="ota-dialog">
|
|
||||||
<view class="ota-bg-clip">
|
|
||||||
<image src="../static/ota/ota-bg.png" mode="aspectFill" class="ota-bg" />
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="ota-content"
|
|
||||||
:class="{ 'content-new': isNewVersion, 'content-result': isProgress || isSuccess || isFailure }"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- 发现新版本:new-ver.png 已包含标题图,不再重复文字;版本号使用 ota-ver.png 胶囊背景 -->
|
|
||||||
<block v-if="isNewVersion">
|
|
||||||
<image src="../static/ota/new-ver.png" mode="aspectFit" class="new-ver-img" />
|
|
||||||
<view v-if="version" class="version-tag-wrap">
|
|
||||||
<image src="../static/ota/ota-ver.png" mode="aspectFill" class="version-tag-bg-img" />
|
|
||||||
<text class="version-tag">{{ version }}</text>
|
|
||||||
</view>
|
|
||||||
<!-- 副标题:如“新版本将优化智能弓体验”,离下方详情 12rpx -->
|
|
||||||
<text v-if="description" class="desc-text">{{ description }}</text>
|
|
||||||
<!-- 详细说明:如“升级前请确保:...” -->
|
|
||||||
<text v-if="changelog" class="changelog-text">{{ changelog }}</text>
|
|
||||||
<view class="btn-group">
|
|
||||||
<view class="primary-btn" @click="emit('update')">
|
|
||||||
<text class="primary-btn-text">立即更新</text>
|
|
||||||
</view>
|
|
||||||
<text v-if="!forceUpdate" class="skip-text" @click="emit('skip')">暂不更新</text>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
|
|
||||||
<!-- 更新成功:图片左边距 34rpx,文案左边距 44rpx,按钮浮动底部居中 -->
|
|
||||||
<block v-else-if="isSuccess">
|
|
||||||
<image src="../static/ota/update-ok.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
|
|
||||||
<text class="dialog-desc">请关机并重启智能弓</text>
|
|
||||||
<view class="btn-group-result">
|
|
||||||
<view class="primary-btn" @click="emit('done')">
|
|
||||||
<text class="primary-btn-text">完成</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
|
|
||||||
<!-- 更新中:复用成功标题图,正文区域展示进度条,无底部按钮 -->
|
|
||||||
<block v-else-if="isProgress">
|
|
||||||
<image src="../static/ota/update_progress.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
|
|
||||||
<view class="progress-wrap">
|
|
||||||
<view class="progress-track">
|
|
||||||
<view class="progress-fill" :style="{ width: `${progressValue}%` }"></view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
|
|
||||||
<!-- 更新失败:图片左边距 34rpx,文案左对齐 44rpx,按钮浮动底部居中 -->
|
|
||||||
<block v-else-if="isFailure">
|
|
||||||
<image src="../static/ota/update-fail.png" mode="aspectFit" class="result-title-img" style="width: 222rpx; height: 62rpx;" />
|
|
||||||
<text class="dialog-desc">请确保:</text>
|
|
||||||
<text class="dialog-desc">1、智能弓已开启</text>
|
|
||||||
<text class="dialog-desc">2、网路连接稳定</text>
|
|
||||||
<view class="btn-group-result">
|
|
||||||
<view class="primary-btn" @click="emit('retry')">
|
|
||||||
<text class="primary-btn-text">重试</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 关闭按钮(仅新版本状态,非强制更新时,位于弹窗下方) -->
|
|
||||||
<view
|
|
||||||
v-if="isNewVersion && !forceUpdate"
|
|
||||||
class="ota-close-below"
|
|
||||||
@click="emit('close')"
|
|
||||||
>
|
|
||||||
<image src="../static/sicon/close.png" mode="aspectFit" style="width: 56rpx; height: 56rpx;" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.ota-mask {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 外层容器:相对定位,为浮动图标创造溢出空间 */
|
|
||||||
.ota-outer {
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
/* 设计图:吉祥物向上突出弹窗顶部 40px(375px基准)× 2 = 80rpx */
|
|
||||||
.outer-new {
|
|
||||||
padding-top: 80rpx;
|
|
||||||
}
|
|
||||||
.outer-result {
|
|
||||||
padding-top: 80rpx;
|
|
||||||
padding-bottom: 66rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 浮动图标(绝对定位,位于卡片顶部上方) */
|
|
||||||
.float-icon {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
/* 吉祥物尺寸:设计图 149×109px(375px基准)× 2 = 298×218rpx */
|
|
||||||
.float-mascot {
|
|
||||||
width: 298rpx;
|
|
||||||
height: 218rpx;
|
|
||||||
top: -5px;
|
|
||||||
right: -74rpx;
|
|
||||||
}
|
|
||||||
.float-check {
|
|
||||||
width: 194rpx;
|
|
||||||
height: 166rpx;
|
|
||||||
top: 20px;
|
|
||||||
right: 30rpx;
|
|
||||||
}
|
|
||||||
.float-close {
|
|
||||||
width: 194rpx;
|
|
||||||
height: 164rpx;
|
|
||||||
top: 20px;
|
|
||||||
right: 30rpx;
|
|
||||||
}
|
|
||||||
.float-target {
|
|
||||||
width: 194rpx;
|
|
||||||
height: 166rpx;
|
|
||||||
top: 20px;
|
|
||||||
right: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 弹窗卡片:overflow:visible 允许按钮溢出底部,背景通过 ota-bg-clip 独立裁剪 */
|
|
||||||
.ota-dialog {
|
|
||||||
position: relative;
|
|
||||||
width: 482rpx;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
border: 2rpx solid #F9D5A1;
|
|
||||||
overflow: visible;
|
|
||||||
background-color: #392F1D;
|
|
||||||
}
|
|
||||||
/* 背景图裁剪层:独立 overflow:hidden + border-radius 保持圆角效果 */
|
|
||||||
.ota-bg-clip {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
.ota-bg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.ota-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
/* 按钮以外内容均左对齐 */
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.content-new {
|
|
||||||
padding: 30rpx 0 40rpx 0;
|
|
||||||
}
|
|
||||||
.content-result {
|
|
||||||
padding: 30rpx 0 66rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 发现新版本内容 */
|
|
||||||
.new-ver-img {
|
|
||||||
width: 274rpx;
|
|
||||||
height: 62rpx;
|
|
||||||
/* 左边距 34rpx,去掉 margin-bottom */
|
|
||||||
margin-left: 34rpx;
|
|
||||||
}
|
|
||||||
/* 版本号胶囊容器:相对定位,使 ota-ver.png 作为背景衬底 */
|
|
||||||
.version-tag-wrap {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
/* 离标题图 -10rpx,左边距 50rpx,离下方副标题 22rpx */
|
|
||||||
margin-top: -10rpx;
|
|
||||||
margin-left: 50rpx;
|
|
||||||
margin-bottom: 22rpx;
|
|
||||||
}
|
|
||||||
/* ota-ver.png 胶囊背景图 */
|
|
||||||
.version-tag-bg-img {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
/* 版本号文字:浮于背景图之上 */
|
|
||||||
.version-tag {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
color: rgba(254, 222, 100, 1);
|
|
||||||
font-size: 24rpx;
|
|
||||||
padding: 8rpx 22rpx 4rpx 24rpx;
|
|
||||||
}
|
|
||||||
/* 副标题(如"新版本将优化智能弓体验"):左边距 44rpx,离下方文案 12rpx */
|
|
||||||
.desc-text {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #FFFFFF;
|
|
||||||
line-height: 36rpx;
|
|
||||||
text-align: left;
|
|
||||||
margin-left: 44rpx;
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
}
|
|
||||||
/* 详细说明文案(如“升级前请确保:...”):左边距 44rpx */
|
|
||||||
.changelog-text {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #FFFFFF;
|
|
||||||
line-height: 40rpx;
|
|
||||||
text-align: left;
|
|
||||||
margin-left: 44rpx;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮组(新版本状态):离上方文案 30rpx,内部按钮间距 24rpx */
|
|
||||||
.btn-group {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 30rpx;
|
|
||||||
gap: 24rpx;
|
|
||||||
}
|
|
||||||
/* 按钮组(结果状态):绝对定位,溢出卡片底边 -35rpx 悬浮在底边中间 */
|
|
||||||
.btn-group-result {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -35rpx;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
/* 主按钮:按照设计规范 width: 232rpx, height: 70rpx */
|
|
||||||
.primary-btn {
|
|
||||||
width: 232rpx;
|
|
||||||
height: 70rpx;
|
|
||||||
background-color: #FED847;
|
|
||||||
border-radius: 44rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.primary-btn-text {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #000000;
|
|
||||||
line-height: 36rpx;
|
|
||||||
}
|
|
||||||
/* 暂不更新:设计规范颜色 #5FADFF 蓝色 */
|
|
||||||
.skip-text {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #5FADFF;
|
|
||||||
line-height: 36rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 更新结果内容:图片左边距 34rpx,下边距 16rpx */
|
|
||||||
.result-title-img {
|
|
||||||
margin-left: 34rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
/* 结果页文案:左对齐,左边距 44rpx,与 new_version 保持一致 */
|
|
||||||
.dialog-desc {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #FFFFFF;
|
|
||||||
line-height: 40rpx;
|
|
||||||
text-align: left;
|
|
||||||
margin-left: 44rpx;
|
|
||||||
}
|
|
||||||
.progress-wrap {
|
|
||||||
width: 394rpx;
|
|
||||||
margin-top: 40rpx;
|
|
||||||
margin-left: 44rpx;
|
|
||||||
}
|
|
||||||
.progress-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 18rpx;
|
|
||||||
background-color: rgba(255, 255, 255, 0.28);
|
|
||||||
border-radius: 999rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #FED847;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 关闭按钮(位于弹窗下方) */
|
|
||||||
.ota-close-below {
|
|
||||||
margin-top: 40rpx;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
interval: {
|
interval: {
|
||||||
@@ -14,13 +14,24 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
current: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
onChange: {
|
onChange: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: (index) => {},
|
default: (index) => {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentIndex = ref(0);
|
const currentIndex = ref(props.current);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.current,
|
||||||
|
(index) => {
|
||||||
|
currentIndex.value = index;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
currentIndex.value = e.detail.current;
|
currentIndex.value = e.detail.current;
|
||||||
@@ -75,7 +86,7 @@ const handleChange = (e) => {
|
|||||||
|
|
||||||
.dots {
|
.dots {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 5%;
|
bottom: 2%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -64,11 +64,7 @@
|
|||||||
"usingComponents" : true,
|
"usingComponents" : true,
|
||||||
"darkmode" : true,
|
"darkmode" : true,
|
||||||
"themeLocation" : "theme.json",
|
"themeLocation" : "theme.json",
|
||||||
"permission" : {
|
"permission" : {},
|
||||||
"scope.userLocation": {
|
|
||||||
"desc": "用于扫描附近 WiFi,完成设备 OTA 升级网络连接"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,12 +119,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/mine-bow-data"
|
"path": "pages/mine-bow-data"
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/ota-wifi",
|
|
||||||
"style": {
|
|
||||||
"navigationStyle": "custom"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import Guide from "@/components/Guide.vue";
|
import Guide from "@/components/Guide.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
import Swiper from "@/components/Swiper.vue";
|
import Swiper from "@/components/Swiper.vue";
|
||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
startPractiseAPI,
|
startPractiseAPI,
|
||||||
endPractiseAPI,
|
endPractiseAPI,
|
||||||
getPractiseAPI,
|
getPractiseAPI,
|
||||||
|
laserAimAPI,
|
||||||
|
laserCloseAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
import { sharePractiseData } from "@/canvas";
|
import { sharePractiseData } from "@/canvas";
|
||||||
import { wxShare, debounce } from "@/util";
|
import { wxShare, debounce } from "@/util";
|
||||||
@@ -32,6 +34,7 @@ const total = 12;
|
|||||||
const stepButtonTexts = [
|
const stepButtonTexts = [
|
||||||
"开始",
|
"开始",
|
||||||
"进入下一个任务",
|
"进入下一个任务",
|
||||||
|
"我已校准",
|
||||||
"进入下一个任务",
|
"进入下一个任务",
|
||||||
"我准备好了,开始",
|
"我准备好了,开始",
|
||||||
"",
|
"",
|
||||||
@@ -43,8 +46,11 @@ const practiseResult = ref({});
|
|||||||
const btnDisabled = ref(false);
|
const btnDisabled = ref(false);
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
|
const laserActive = ref(false);
|
||||||
|
const guideSwiperIndex = ref(0);
|
||||||
|
|
||||||
const guideImages = [
|
const guideImages = [
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/target.png",
|
||||||
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wwdpgwt9e6du.png",
|
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wwdpgwt9e6du.png",
|
||||||
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wvv9sw4zioqk.png",
|
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wvv9sw4zioqk.png",
|
||||||
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0ww3khaycallu.png",
|
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0ww3khaycallu.png",
|
||||||
@@ -54,10 +60,45 @@ const guideImages = [
|
|||||||
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wwr6hfjhyfn5.png",
|
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wwr6hfjhyfn5.png",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const calibrationGuides = [
|
||||||
|
{
|
||||||
|
title: "箭头面向靶子",
|
||||||
|
src: "https://static.shelingxingqiu.com/attachment/2025-10-30/ddv9p5fk5wscg7hrfo.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "摆出拉弓姿势",
|
||||||
|
src: "https://static.shelingxingqiu.com/attachment/2025-10-30/ddv9p5fk5b7ljrhx3o.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "调整瞄准器",
|
||||||
|
src: "https://static.shelingxingqiu.com/attachment/2025-10-29/dduexjgrcxf9wjaiv4.png",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const onSwiperIndexChange = (index) => {
|
const onSwiperIndexChange = (index) => {
|
||||||
if (index + 1 === guideImages.length) {
|
guideSwiperIndex.value = index;
|
||||||
showGuide.value = true;
|
showGuide.value = index + 1 === guideImages.length;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const isGuideLastImage = computed(
|
||||||
|
() => guideSwiperIndex.value + 1 === guideImages.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentStepButtonText = computed(() => {
|
||||||
|
if (step.value === 1 && isGuideLastImage.value) return "去校准智能弓";
|
||||||
|
return stepButtonTexts[step.value];
|
||||||
|
});
|
||||||
|
|
||||||
|
const openCalibrationLaser = async () => {
|
||||||
|
if (laserActive.value) return;
|
||||||
|
await laserAimAPI();
|
||||||
|
laserActive.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCalibrationLaser = async () => {
|
||||||
|
if (!laserActive.value) return;
|
||||||
|
await laserCloseAPI();
|
||||||
|
laserActive.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPractise = async (arrows) => {
|
const createPractise = async (arrows) => {
|
||||||
@@ -75,7 +116,7 @@ async function onReceiveMessage(msg) {
|
|||||||
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);
|
||||||
} else if (msg.type === MESSAGETYPESV2.TestDistance) {
|
} else if (msg.type === MESSAGETYPESV2.TestDistance && step.value === 3) {
|
||||||
if (msg.shootData.distance / 100 >= 5) {
|
if (msg.shootData.distance / 100 >= 5) {
|
||||||
audioManager.play("距离合格");
|
audioManager.play("距离合格");
|
||||||
btnDisabled.value = false;
|
btnDisabled.value = false;
|
||||||
@@ -110,12 +151,13 @@ onMounted(() => {
|
|||||||
uni.$on("share-image", onClickShare);
|
uni.$on("share-image", onClickShare);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(async () => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
uni.$off("share-image", onClickShare);
|
uni.$off("share-image", onClickShare);
|
||||||
|
await closeCalibrationLaser();
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
endPractiseAPI();
|
endPractiseAPI();
|
||||||
});
|
});
|
||||||
@@ -123,28 +165,39 @@ onBeforeUnmount(() => {
|
|||||||
const nextStep = async () => {
|
const nextStep = async () => {
|
||||||
if (step.value === 0) {
|
if (step.value === 0) {
|
||||||
step.value = 1;
|
step.value = 1;
|
||||||
title.value = "-凹造型";
|
title.value = "-箭前准备";
|
||||||
} else if (step.value === 1) {
|
} else if (step.value === 1) {
|
||||||
|
if (!isGuideLastImage.value) {
|
||||||
|
guideSwiperIndex.value += 1;
|
||||||
|
showGuide.value = guideSwiperIndex.value + 1 === guideImages.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showGuide.value = false;
|
||||||
|
step.value = 2;
|
||||||
|
// title.value = "-校准智能弓";
|
||||||
|
await openCalibrationLaser();
|
||||||
|
} else if (step.value === 2) {
|
||||||
|
await closeCalibrationLaser();
|
||||||
showGuide.value = false;
|
showGuide.value = false;
|
||||||
btnDisabled.value = true;
|
btnDisabled.value = true;
|
||||||
step.value = 2;
|
step.value = 3;
|
||||||
title.value = "-感知距离";
|
title.value = "-感知距离";
|
||||||
const result = await createPractiseAPI(total, 120);
|
const result = await createPractiseAPI(total, 120);
|
||||||
if (result) practiseId.value = result.id;
|
if (result) practiseId.value = result.id;
|
||||||
} else if (step.value === 2) {
|
|
||||||
showGuide.value = false;
|
|
||||||
step.value = 3;
|
|
||||||
title.value = "-小试牛刀";
|
|
||||||
} else if (step.value === 3) {
|
} else if (step.value === 3) {
|
||||||
|
showGuide.value = false;
|
||||||
|
step.value = 4;
|
||||||
|
title.value = "-小试牛刀";
|
||||||
|
} else if (step.value === 4) {
|
||||||
title.value = "小试牛刀";
|
title.value = "小试牛刀";
|
||||||
await startPractiseAPI();
|
await startPractiseAPI();
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
step.value = 4;
|
step.value = 5;
|
||||||
start.value = true;
|
start.value = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.$emit("play-sound", "请开始射击");
|
uni.$emit("play-sound", "请开始射击");
|
||||||
}, 300);
|
}, 300);
|
||||||
} else if (step.value === 5) {
|
} else if (step.value === 6) {
|
||||||
uni.navigateBack({
|
uni.navigateBack({
|
||||||
delta: 1,
|
delta: 1,
|
||||||
});
|
});
|
||||||
@@ -159,13 +212,13 @@ const onClose = async () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
showGuide.value = false;
|
showGuide.value = false;
|
||||||
step.value = 5;
|
step.value = 6;
|
||||||
}, 500);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
step.value = 3;
|
step.value = 4;
|
||||||
const result = await createPractiseAPI(total, 120);
|
const result = await createPractiseAPI(total, 120);
|
||||||
if (result) practiseId.value = result.id;
|
if (result) practiseId.value = result.id;
|
||||||
}
|
}
|
||||||
@@ -173,14 +226,14 @@ const onClose = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container :bgType="1" :title="title" :showBottom="step !== 4">
|
<Container :bgType="1" :title="title" :showBottom="step !== 5">
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<Guide
|
<Guide
|
||||||
v-if="step !== 4"
|
v-if="step !== 5"
|
||||||
:type="
|
:type="
|
||||||
step === 2
|
step === 3
|
||||||
? 2
|
? 2
|
||||||
: step === 5 || (step === 0 && user.nickName.length > 6)
|
: step === 6 || (step === 0 && user.nickName.length > 6)
|
||||||
? 1
|
? 1
|
||||||
: 0
|
: 0
|
||||||
"
|
"
|
||||||
@@ -196,25 +249,28 @@ const onClose = async () => {
|
|||||||
,这是新人必刷小任务,0基础小白也能快速掌握弓箭技巧和游戏规则哦~:)
|
,这是新人必刷小任务,0基础小白也能快速掌握弓箭技巧和游戏规则哦~:)
|
||||||
</text>
|
</text>
|
||||||
<text v-if="step === 1" :style="{ fontSize: '28rpx' }"
|
<text v-if="step === 1" :style="{ fontSize: '28rpx' }"
|
||||||
>这是我们人帅技高的高教练。首先,请按教练示范,尝试自己去做这些动作和手势吧。</text
|
>这位就是人帅技高的高教练!接下来请跟随教练指引,做好射箭前期准备。</text
|
||||||
|
>
|
||||||
|
<text v-if="step === 2" :style="{ fontSize: '28rpx' }"
|
||||||
|
>请按下方步骤完成智能弓校准,让瞄准器和靶子保持对齐。</text
|
||||||
>
|
>
|
||||||
<view
|
<view
|
||||||
class="guide-tips"
|
class="guide-tips"
|
||||||
:style="{ marginTop: '8rpx' }"
|
:style="{ marginTop: '8rpx' }"
|
||||||
v-if="step === 2"
|
v-if="step === 3"
|
||||||
>
|
>
|
||||||
<text>你知道5米射程有多远吗?</text>
|
<text>你知道5米射程有多远吗?</text>
|
||||||
<text>
|
<text>
|
||||||
在我们的排位赛中,射程小于5米的成绩无效、哦!建议平时练习距离至少5米。现在,来边射箭边调整你的站位点吧!
|
在我们的排位赛中,射程小于5米的成绩无效、哦!建议平时练习距离至少5米。现在,来边射箭边调整你的站位点吧!
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="guide-tips" v-if="step === 3">
|
<view class="guide-tips" v-if="step === 4">
|
||||||
<text>一切准备就绪</text>
|
<text>一切准备就绪</text>
|
||||||
<text :style="{ fontSize: '28rpx' }"
|
<text :style="{ fontSize: '28rpx' }"
|
||||||
>试着完成一个真正的弓箭手任务吧!</text
|
>试着完成一个真正的弓箭手任务吧!</text
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
<view class="guide-tips" v-if="step === 5">
|
<view class="guide-tips" v-if="step === 6">
|
||||||
<text>新手试炼场通关啦,优秀!</text>
|
<text>新手试炼场通关啦,优秀!</text>
|
||||||
<text :style="{ fontSize: '28rpx' }"
|
<text :style="{ fontSize: '28rpx' }"
|
||||||
>反曲弓运动基本知识和射灵世界系统规则你已Get,是不是挺容易呀:)</text
|
>反曲弓运动基本知识和射灵世界系统规则你已Get,是不是挺容易呀:)</text
|
||||||
@@ -231,35 +287,53 @@ const onClose = async () => {
|
|||||||
src="https://static.shelingxingqiu.com/attachment/2025-11-17/deas80ef1sf9td0leq.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-11-17/deas80ef1sf9td0leq.png"
|
||||||
class="try-tip"
|
class="try-tip"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
v-if="step === 3"
|
v-if="step === 4"
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-07-01/db0ehpz9lav58g5drl.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-07-01/db0ehpz9lav58g5drl.png"
|
||||||
class="try-tip"
|
class="try-tip"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
v-if="step === 5"
|
v-if="step === 6"
|
||||||
/>
|
/>
|
||||||
<view style="height: 570px" v-if="step === 1">
|
<view style="height: 570px" v-if="step === 1">
|
||||||
<Swiper :onChange="onSwiperIndexChange" :data="guideImages" />
|
<Swiper
|
||||||
|
:current="guideSwiperIndex"
|
||||||
|
:onChange="onSwiperIndexChange"
|
||||||
|
:data="guideImages"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
<ShootProgress v-if="step === 4" tips="请开始连续射箭" :start="start" />
|
<view class="calibration-container" v-if="step === 2">
|
||||||
<TestDistance v-if="step === 2" :guide="false" />
|
<view
|
||||||
|
v-for="(guide, index) in calibrationGuides"
|
||||||
|
:key="guide.title"
|
||||||
|
class="calibration-guide"
|
||||||
|
>
|
||||||
|
<view>
|
||||||
|
<text>{{ index + 1 }}</text>
|
||||||
|
<text>{{ guide.title }}</text>
|
||||||
|
</view>
|
||||||
|
<image :src="guide.src" mode="widthFix" />
|
||||||
|
</view>
|
||||||
|
<text>请完成以上步骤校准智能弓</text>
|
||||||
|
</view>
|
||||||
|
<ShootProgress v-if="step === 5" tips="请开始连续射箭" :start="start" />
|
||||||
|
<TestDistance v-if="step === 3" :guide="false" />
|
||||||
<view
|
<view
|
||||||
class="user-row"
|
class="user-row"
|
||||||
v-if="step === 4"
|
v-if="step === 5"
|
||||||
:style="{ marginBottom: step === 2 ? '40px' : '0' }"
|
:style="{ marginBottom: '0' }"
|
||||||
>
|
>
|
||||||
<Avatar :src="user.avatar" :size="35" />
|
<Avatar :src="user.avatar" :size="35" />
|
||||||
<BowPower />
|
<BowPower />
|
||||||
</view>
|
</view>
|
||||||
<BowTarget
|
<BowTarget
|
||||||
v-if="step === 4"
|
v-if="step === 5"
|
||||||
:currentRound="step === 4 ? scores.length : 0"
|
:currentRound="step === 5 ? scores.length : 0"
|
||||||
:totalRound="step === 4 ? total : 0"
|
:totalRound="step === 5 ? total : 0"
|
||||||
:scores="scores"
|
:scores="scores"
|
||||||
/>
|
/>
|
||||||
<ScorePanel
|
<ScorePanel
|
||||||
v-if="step === 4"
|
v-if="step === 5"
|
||||||
:total="total"
|
:total="total"
|
||||||
:rowCount="6"
|
:rowCount="6"
|
||||||
:arrows="scores"
|
:arrows="scores"
|
||||||
@@ -287,7 +361,7 @@ const onClose = async () => {
|
|||||||
step === 1 ? "学会了,我摆得比教练还帅" : "我找到合适的点位了"
|
step === 1 ? "学会了,我摆得比教练还帅" : "我找到合适的点位了"
|
||||||
}}</text>
|
}}</text>
|
||||||
</BubbleTip>
|
</BubbleTip>
|
||||||
{{ stepButtonTexts[step] }}
|
{{ currentStepButtonText }}
|
||||||
</SButton>
|
</SButton>
|
||||||
</template>
|
</template>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -301,4 +375,43 @@ const onClose = async () => {
|
|||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
|
.calibration-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.calibration-guide {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
.calibration-guide > view {
|
||||||
|
width: 100%;
|
||||||
|
margin: 25rpx 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.calibration-guide > view > text:first-child {
|
||||||
|
font-size: 24rpx;
|
||||||
|
background: #e89024;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 15rpx;
|
||||||
|
}
|
||||||
|
.calibration-guide > image {
|
||||||
|
width: 630rpx;
|
||||||
|
height: 250rpx;
|
||||||
|
}
|
||||||
|
.calibration-container > text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #fff9;
|
||||||
|
margin: 30rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import AppFooter from "@/components/AppFooter.vue";
|
|||||||
import UserHeader from "@/components/UserHeader.vue";
|
import UserHeader from "@/components/UserHeader.vue";
|
||||||
import Signin from "@/components/Signin.vue";
|
import Signin from "@/components/Signin.vue";
|
||||||
import BubbleTip from "@/components/BubbleTip.vue";
|
import BubbleTip from "@/components/BubbleTip.vue";
|
||||||
import OtaModal from "@/components/OtaModal.vue";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
checkUserBindAPI,
|
checkUserBindAPI,
|
||||||
@@ -37,44 +36,6 @@ const showModal = ref(false);
|
|||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const scoreRankList = ref([]);
|
const scoreRankList = ref([]);
|
||||||
|
|
||||||
// OTA 相关
|
|
||||||
const otaVisible = ref(false);
|
|
||||||
const otaState = ref("new_version");
|
|
||||||
const OTA_MOCK = {
|
|
||||||
hasUpdate: true,
|
|
||||||
version: "V8.7.0",
|
|
||||||
description: "新版本将优化智能弓体验",
|
|
||||||
details: "升级前请确保:\n1、智能弓已开启,且电量充足\n2、所处稳定的 Wi-Fi 环境中。",
|
|
||||||
forceUpdate: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkOtaUpdate = () => {
|
|
||||||
if (!OTA_MOCK.hasUpdate) return;
|
|
||||||
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
|
||||||
const now = Date.now();
|
|
||||||
if (dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
|
|
||||||
otaState.value = "new_version";
|
|
||||||
otaVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOtaDismiss = () => {
|
|
||||||
uni.setStorageSync("ota_dismissed_at", Date.now());
|
|
||||||
otaVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOtaUpdate = () => {
|
|
||||||
otaVisible.value = false;
|
|
||||||
uni.navigateTo({ url: "/pages/ota-wifi" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOtaDone = () => {
|
|
||||||
otaVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOtaRetry = () => {
|
|
||||||
otaVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
|
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
|
||||||
const getScoreRankData = (result) => {
|
const getScoreRankData = (result) => {
|
||||||
if (Array.isArray(result)) return result;
|
if (Array.isArray(result)) return result;
|
||||||
@@ -102,15 +63,7 @@ const toRankListPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onShow(async (options) => {
|
onShow(async () => {
|
||||||
// 检查是否从 OTA 更新页面返回
|
|
||||||
if (options && options.updateResult) {
|
|
||||||
otaState.value = options.updateResult;
|
|
||||||
otaVisible.value = true;
|
|
||||||
} else {
|
|
||||||
checkOtaUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
||||||
const token = uni.getStorageSync(`${env}_token`);
|
const token = uni.getStorageSync(`${env}_token`);
|
||||||
|
|
||||||
@@ -205,20 +158,6 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container :isHome="true" :showBackToGame="true">
|
<Container :isHome="true" :showBackToGame="true">
|
||||||
<!-- OTA 升级弹窗:使用 visible 控制显隐,description 为副标题,changelog 为详细说明 -->
|
|
||||||
<OtaModal
|
|
||||||
:visible="otaVisible"
|
|
||||||
:state="otaState"
|
|
||||||
:version="OTA_MOCK.version"
|
|
||||||
:description="OTA_MOCK.description"
|
|
||||||
:changelog="OTA_MOCK.details"
|
|
||||||
:forceUpdate="OTA_MOCK.forceUpdate"
|
|
||||||
@update="handleOtaUpdate"
|
|
||||||
@skip="handleOtaDismiss"
|
|
||||||
@close="handleOtaDismiss"
|
|
||||||
@done="handleOtaDone"
|
|
||||||
@retry="handleOtaRetry"
|
|
||||||
/>
|
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="top-theme">
|
<view class="top-theme">
|
||||||
<!-- <image
|
<!-- <image
|
||||||
|
|||||||
@@ -93,11 +93,6 @@ const unbindDevice = async () => {
|
|||||||
device.value = {};
|
device.value = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 连接wifi跳转到wifi列表页面 */
|
|
||||||
const joinWifi = () => {
|
|
||||||
uni.navigateTo({ url: "/pages/ota-wifi" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const toDeviceIntroPage = () => {
|
const toDeviceIntroPage = () => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/device-intro",
|
url: "/pages/device-intro",
|
||||||
@@ -217,33 +212,35 @@ onShow(() => {
|
|||||||
<text>{{ user.nickName }}</text>
|
<text>{{ user.nickName }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<block v-if="calibration">
|
<!-- <block v-if="calibration"> -->
|
||||||
<SButton :onClick="toFristTryPage" width="60vw" :rounded="40"
|
|
||||||
>进入新手试炼</SButton
|
|
||||||
>
|
|
||||||
<view :style="{ marginTop: '15px' }">
|
|
||||||
<SButton
|
|
||||||
:onClick="backToHome"
|
|
||||||
backgroundColor="#fff3"
|
|
||||||
color="#fff"
|
|
||||||
width="60vw"
|
|
||||||
:rounded="40"
|
|
||||||
>返回首页</SButton
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
<block v-else>
|
|
||||||
<view>
|
<view>
|
||||||
<text>恭喜,你的弓箭和账号已成功绑定!</text>
|
<text>恭喜,你的弓箭和账号已成功绑定!</text>
|
||||||
<text :style="{ color: '#fed847' }">已赠送6个月射灵世界会员</text>
|
<text :style="{ color: '#fed847' }">已赠送6个月射灵世界会员</text>
|
||||||
</view>
|
</view>
|
||||||
<SButton :onClick="goCalibration" width="60vw" :rounded="40">
|
<!-- <SButton :onClick="goCalibration" width="60vw" :rounded="40">
|
||||||
开启智能弓进行校准
|
开启智能弓进行校准
|
||||||
</SButton>
|
</SButton>
|
||||||
<text :style="{ marginTop: '20rpx', fontSize: '24rpx', color: '#fff9' }"
|
<text :style="{ marginTop: '20rpx', fontSize: '24rpx', color: '#fff9' }"
|
||||||
>校准时弓箭激光将开启,请勿直视激光</text
|
>校准时弓箭激光将开启,请勿直视激光</text
|
||||||
>
|
> -->
|
||||||
</block>
|
|
||||||
|
<view>
|
||||||
|
<SButton
|
||||||
|
:onClick="backToHome"
|
||||||
|
backgroundColor="#fff3"
|
||||||
|
color="#fff"
|
||||||
|
width="60vw"
|
||||||
|
:rounded="40"
|
||||||
|
>返回首页</SButton
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<view :style="{ marginTop: '15px' }">
|
||||||
|
<SButton :onClick="toFristTryPage" width="60vw" :rounded="40">进入新手试炼</SButton>
|
||||||
|
</view>
|
||||||
|
<!-- </block> -->
|
||||||
|
<!-- <block v-else>
|
||||||
|
|
||||||
|
</block> -->
|
||||||
</view>
|
</view>
|
||||||
<view v-if="device.deviceId && !justBind" class="has-device">
|
<view v-if="device.deviceId && !justBind" class="has-device">
|
||||||
<view class="device-binded">
|
<view class="device-binded">
|
||||||
@@ -278,11 +275,6 @@ onShow(() => {
|
|||||||
>解绑</SButton
|
>解绑</SButton
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
<view :style="{ marginTop: '20rpx' }">
|
|
||||||
<SButton :onClick="() => $clickSound(joinWifi)" width="80vw" :rounded="40"
|
|
||||||
>设备连接WIFI</SButton
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
@@ -404,7 +396,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;
|
||||||
|
|||||||
@@ -1,979 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
|
||||||
import Container from "@/components/Container.vue";
|
|
||||||
import ScreenHint from "@/components/ScreenHint.vue";
|
|
||||||
|
|
||||||
const STATES = {
|
|
||||||
SCANNING: "SCANNING",
|
|
||||||
LIST: "LIST",
|
|
||||||
CONNECTING: "CONNECTING",
|
|
||||||
CONNECTED: "CONNECTED",
|
|
||||||
UPDATING: "UPDATING",
|
|
||||||
DONE: "DONE",
|
|
||||||
FAILED: "FAILED",
|
|
||||||
};
|
|
||||||
|
|
||||||
const isIOS = uni.getDeviceInfo().osName === "ios";
|
|
||||||
const currentState = ref(STATES.SCANNING);
|
|
||||||
const connectedWifi = ref(null);
|
|
||||||
const wifiList = ref([]);
|
|
||||||
const showWifiBanner = ref(false);
|
|
||||||
|
|
||||||
const connectingWifi = ref(null);
|
|
||||||
const connectInput = ref({ ssid: "", password: "" });
|
|
||||||
const connectMode = ref("secure"); // secure | open | manual
|
|
||||||
const connectError = ref("");
|
|
||||||
const keyboardHeight = ref(0);
|
|
||||||
// 控制密码输入框是否显示明文
|
|
||||||
const showPassword = ref(false);
|
|
||||||
// 刷新防抖标志:扫描进行中为 true,禁止重复点击;扫描结束(成功/失败)后重置为 false。
|
|
||||||
const isRefreshing = ref(false);
|
|
||||||
|
|
||||||
const progress = ref(0);
|
|
||||||
let progressTimer = null;
|
|
||||||
let timeoutTimer = null;
|
|
||||||
// 控制授权拒绝弹窗显示/隐藏
|
|
||||||
const wifiAuthDeniedVisible = ref(false);
|
|
||||||
|
|
||||||
// 判断 WiFi 列表失败是否由用户拒绝授权引起(兼容 errno:103 及各平台 errMsg 变体)。
|
|
||||||
const isWifiPermissionDenied = (err) => {
|
|
||||||
if (err?.errno === 103) return true;
|
|
||||||
const errMsg = err?.errMsg || "";
|
|
||||||
return /auth den|authorize|permission denied|user denied|scope\.userLocation/i.test(errMsg);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 显示授权拒绝弹窗,通过 ScreenHint 组件呈现。
|
|
||||||
const showWifiPermissionDeniedModal = () => {
|
|
||||||
wifiAuthDeniedVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 用户点击「重新授权」时,打开小程序权限设置页,引导用户开启位置权限后重新扫描。
|
|
||||||
const handleReauthorize = () => {
|
|
||||||
wifiAuthDeniedVisible.value = false;
|
|
||||||
uni.openSetting({
|
|
||||||
success(res) {
|
|
||||||
// 若用户在设置页开启了位置权限,则重新启动 WiFi 扫描
|
|
||||||
if (res.authSetting["scope.userLocation"]) {
|
|
||||||
startScanning();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将 iOS 当前手机连接的 WiFi 写入候选列表,供用户继续填写设备连接密码;扫描结束后重置刷新防抖标志。
|
|
||||||
const applyIOSConnectedWifi = (wifi) => {
|
|
||||||
const ssid = wifi?.SSID;
|
|
||||||
if (!ssid) {
|
|
||||||
wifiList.value = [];
|
|
||||||
currentState.value = STATES.LIST;
|
|
||||||
isRefreshing.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wifiList.value = [
|
|
||||||
{
|
|
||||||
...wifi,
|
|
||||||
SSID: ssid,
|
|
||||||
secure: wifi.secure !== false,
|
|
||||||
fromCurrentWifi: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
currentState.value = STATES.LIST;
|
|
||||||
isRefreshing.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取 iOS 当前手机连接的 WiFi,并把它作为可选网络展示。
|
|
||||||
const syncIOSConnectedWifi = () => {
|
|
||||||
wx.getConnectedWifi({
|
|
||||||
success(res) {
|
|
||||||
console.log("[OTA WiFi] wx.getConnectedWifi success:", res);
|
|
||||||
applyIOSConnectedWifi(res.wifi || res);
|
|
||||||
},
|
|
||||||
fail(err) {
|
|
||||||
console.warn("[OTA WiFi] wx.getConnectedWifi fail:", {
|
|
||||||
errCode: err?.errCode,
|
|
||||||
errMsg: err?.errMsg,
|
|
||||||
err,
|
|
||||||
});
|
|
||||||
wifiList.value = [];
|
|
||||||
currentState.value = STATES.LIST;
|
|
||||||
isRefreshing.value = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 启动 iOS WiFi 能力;iOS 只用当前手机连接的 WiFi 作为候选网络。
|
|
||||||
const startIOSScanning = () => {
|
|
||||||
wx.startWifi({
|
|
||||||
success(res) {
|
|
||||||
console.log("[OTA WiFi] wx.startWifi success:", res);
|
|
||||||
syncIOSConnectedWifi();
|
|
||||||
},
|
|
||||||
fail(err) {
|
|
||||||
console.error("[OTA WiFi] wx.startWifi fail:", {
|
|
||||||
errCode: err?.errCode,
|
|
||||||
errMsg: err?.errMsg,
|
|
||||||
err,
|
|
||||||
});
|
|
||||||
if (err.errCode === 12005) showWifiBanner.value = true;
|
|
||||||
currentState.value = STATES.LIST;
|
|
||||||
isRefreshing.value = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 启动小程序 WiFi 能力并按平台获取可用网络;也作为刷新按钮的点击处理器,重新获取最新 WiFi 列表。
|
|
||||||
// 扫描进行中时防抖:若上次扫描尚未完成则直接 return,避免重复触发导致弹窗异常。
|
|
||||||
const startScanning = () => {
|
|
||||||
if (isRefreshing.value) return;
|
|
||||||
isRefreshing.value = true;
|
|
||||||
currentState.value = STATES.SCANNING;
|
|
||||||
wifiList.value = [];
|
|
||||||
showWifiBanner.value = false;
|
|
||||||
connectError.value = "";
|
|
||||||
|
|
||||||
if (isIOS) {
|
|
||||||
startIOSScanning();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wx.startWifi({
|
|
||||||
success(res) {
|
|
||||||
console.log("[OTA WiFi] wx.startWifi success:", res);
|
|
||||||
wx.getWifiList({
|
|
||||||
success(res) {
|
|
||||||
console.log("[OTA WiFi] wx.getWifiList request success:", res);
|
|
||||||
},
|
|
||||||
fail(err) {
|
|
||||||
console.error("[OTA WiFi] wx.getWifiList fail:", {
|
|
||||||
errCode: err?.errCode,
|
|
||||||
errMsg: err?.errMsg,
|
|
||||||
err,
|
|
||||||
});
|
|
||||||
if (isWifiPermissionDenied(err)) showWifiPermissionDeniedModal();
|
|
||||||
if (err.errCode === 12005) showWifiBanner.value = true;
|
|
||||||
isRefreshing.value = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fail(err) {
|
|
||||||
console.error("[OTA WiFi] wx.startWifi fail:", {
|
|
||||||
errCode: err?.errCode,
|
|
||||||
errMsg: err?.errMsg,
|
|
||||||
err,
|
|
||||||
});
|
|
||||||
if (isWifiPermissionDenied(err)) showWifiPermissionDeniedModal();
|
|
||||||
if (err.errCode === 12005) showWifiBanner.value = true;
|
|
||||||
isRefreshing.value = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
wx.onGetWifiList((res) => {
|
|
||||||
const realWifiList = res.wifiList || [];
|
|
||||||
if (realWifiList.length) {
|
|
||||||
console.log("[OTA WiFi] wx.onGetWifiList received wifiList:", realWifiList);
|
|
||||||
} else {
|
|
||||||
console.warn("[OTA WiFi] wx.onGetWifiList received empty wifiList:", res);
|
|
||||||
}
|
|
||||||
wifiList.value = realWifiList;
|
|
||||||
currentState.value = STATES.LIST;
|
|
||||||
isRefreshing.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectWifi = (wifi) => {
|
|
||||||
connectingWifi.value = wifi;
|
|
||||||
connectInput.value = { ssid: wifi.SSID, password: "" };
|
|
||||||
connectMode.value = wifi.secure ? "secure" : "open";
|
|
||||||
connectError.value = "";
|
|
||||||
currentState.value = STATES.CONNECTING;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectOther = () => {
|
|
||||||
connectingWifi.value = null;
|
|
||||||
connectInput.value = { ssid: "", password: "" };
|
|
||||||
connectMode.value = "manual";
|
|
||||||
connectError.value = "";
|
|
||||||
currentState.value = STATES.CONNECTING;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeConnectSheet = () => {
|
|
||||||
connectError.value = "";
|
|
||||||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasChinese = (str) => /[\u4e00-\u9fa5]/.test(str);
|
|
||||||
|
|
||||||
const ssidWarning = computed(() => {
|
|
||||||
if (connectMode.value === "manual" && hasChinese(connectInput.value.ssid)) {
|
|
||||||
return "网络名称仅支持英文字符及数字,请连接英文名网络或把网络改为英文";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
|
|
||||||
const joinDisabled = computed(() => {
|
|
||||||
if (connectMode.value === "secure") return !connectInput.value.password;
|
|
||||||
if (connectMode.value === "manual") return !connectInput.value.ssid;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 按当前页面状态限制 WiFi 列表高度,避免列表撑开整页。
|
|
||||||
const wifiListScrollHeight = computed(() => {
|
|
||||||
if (currentState.value === STATES.SCANNING) return "auto";
|
|
||||||
const itemCount = wifiList.value.length + 1;
|
|
||||||
const maxHeight = currentState.value === STATES.CONNECTED ? 276 : 420;
|
|
||||||
return `${Math.min(itemCount * 92, maxHeight)}rpx`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 提交 WiFi 配置给游戏设备;后端接口未接入前只提示占位信息并停留在弹窗。
|
|
||||||
const submitDeviceWifiConfig = ({ ssid, password }) => {
|
|
||||||
console.log("[OTA WiFi] submit device wifi config pending:", {
|
|
||||||
ssid,
|
|
||||||
hasPassword: !!password,
|
|
||||||
});
|
|
||||||
connectError.value = "设备连接接口待接入";
|
|
||||||
uni.showToast({
|
|
||||||
title: "设备连接接口待接入",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 校验用户输入并提交 WiFi 配置,不再把手机连接结果当作设备连接成功。
|
|
||||||
const joinNetwork = () => {
|
|
||||||
if (joinDisabled.value) return;
|
|
||||||
connectError.value = "";
|
|
||||||
const ssid = connectInput.value.ssid;
|
|
||||||
const password = connectInput.value.password;
|
|
||||||
|
|
||||||
submitDeviceWifiConfig({ ssid, password });
|
|
||||||
};
|
|
||||||
|
|
||||||
const startUpdate = () => {
|
|
||||||
currentState.value = STATES.UPDATING;
|
|
||||||
progress.value = 0;
|
|
||||||
clearInterval(progressTimer);
|
|
||||||
progressTimer = setInterval(() => {
|
|
||||||
if (progress.value >= 90) {
|
|
||||||
clearInterval(progressTimer);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const increment = Math.max(0.5, 2 - progress.value / 60);
|
|
||||||
progress.value = Math.min(90, progress.value + increment);
|
|
||||||
}, 500);
|
|
||||||
clearTimeout(timeoutTimer);
|
|
||||||
timeoutTimer = setTimeout(() => {
|
|
||||||
if (currentState.value === STATES.UPDATING) {
|
|
||||||
clearInterval(progressTimer);
|
|
||||||
currentState.value = STATES.FAILED;
|
|
||||||
}
|
|
||||||
}, 5 * 60 * 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWsDone = () => {
|
|
||||||
clearInterval(progressTimer);
|
|
||||||
clearTimeout(timeoutTimer);
|
|
||||||
progress.value = 100;
|
|
||||||
setTimeout(() => {
|
|
||||||
currentState.value = STATES.DONE;
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWsFail = () => {
|
|
||||||
clearInterval(progressTimer);
|
|
||||||
clearTimeout(timeoutTimer);
|
|
||||||
currentState.value = STATES.FAILED;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDone = () => {
|
|
||||||
uni.navigateBack({
|
|
||||||
delta: 1,
|
|
||||||
success() {
|
|
||||||
const pages = getCurrentPages();
|
|
||||||
const prevPage = pages[pages.length - 2];
|
|
||||||
if (prevPage) {
|
|
||||||
prevPage.$vm.otaState = "update_success";
|
|
||||||
prevPage.$vm.otaVisible = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRetry = () => {
|
|
||||||
if (connectedWifi.value) {
|
|
||||||
currentState.value = STATES.CONNECTED;
|
|
||||||
} else {
|
|
||||||
startScanning();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听系统输入法高度,用于让底部弹窗避开键盘遮挡。
|
|
||||||
const handleKeyboardHeightChange = (res) => {
|
|
||||||
keyboardHeight.value = res?.height || 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 切换密码显示状态前先收起键盘,避免安全键盘和普通输入法切换时复用旧高度产生黑条。
|
|
||||||
const togglePasswordVisibility = () => {
|
|
||||||
uni.hideKeyboard({
|
|
||||||
complete() {
|
|
||||||
keyboardHeight.value = 0;
|
|
||||||
showPassword.value = !showPassword.value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面挂载后启动 WiFi 扫描流程。
|
|
||||||
onMounted(() => {
|
|
||||||
if (typeof uni.onKeyboardHeightChange === "function") {
|
|
||||||
uni.onKeyboardHeightChange(handleKeyboardHeightChange);
|
|
||||||
}
|
|
||||||
startScanning();
|
|
||||||
});
|
|
||||||
|
|
||||||
// iOS 从系统 WiFi 设置返回小程序后,同步当前手机连接的 WiFi。
|
|
||||||
onShow(() => {
|
|
||||||
if (isIOS && currentState.value === STATES.LIST) {
|
|
||||||
syncIOSConnectedWifi();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (typeof uni.offKeyboardHeightChange === "function") {
|
|
||||||
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
|
|
||||||
}
|
|
||||||
clearInterval(progressTimer);
|
|
||||||
clearTimeout(timeoutTimer);
|
|
||||||
wx.offGetWifiList && wx.offGetWifiList();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Container title="连接无线网络">
|
|
||||||
<!-- WiFi 不可用 Banner -->
|
|
||||||
<view v-if="showWifiBanner" class="wifi-banner">
|
|
||||||
<text class="wifi-banner-text">请先开启 WiFi 后刷新重试</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- SCANNING / LIST / CONNECTED -->
|
|
||||||
<view
|
|
||||||
v-if="currentState === 'SCANNING' || currentState === 'LIST' || currentState === 'CONNECTED'"
|
|
||||||
class="wifi-page"
|
|
||||||
>
|
|
||||||
<!-- Hero: wifi1.png 单图(已内含吉祥物) -->
|
|
||||||
<view class="hero-area">
|
|
||||||
<image src="../static/ota/wifi1.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<text class="page-title" :class="{ 'connected-page-title': currentState === 'CONNECTED' }">
|
|
||||||
{{ currentState === 'CONNECTED' ? '连接成功' : '连接无线网络' }}
|
|
||||||
</text>
|
|
||||||
<text v-if="currentState !== 'CONNECTED'" class="page-subtitle">网络名称仅支持英文字符及数字</text>
|
|
||||||
|
|
||||||
<view v-if="isIOS && currentState !== 'CONNECTED'" class="ios-guide">
|
|
||||||
<text class="ios-guide-text">请先在系统 WiFi 设置中连接目标网络,返回后选择当前 WiFi 并填写密码</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 已连接网络卡片 -->
|
|
||||||
<view v-if="currentState === 'CONNECTED'" class="section-label-row connected-section-label-row">
|
|
||||||
<text class="section-label">连接网络</text>
|
|
||||||
</view>
|
|
||||||
<view v-if="currentState === 'CONNECTED'" class="wifi-list-card connected-wifi-card">
|
|
||||||
<view class="wifi-item connected-wifi-item">
|
|
||||||
<image class="check-icon" src="../static/sicon/check.png" mode="aspectFit" />
|
|
||||||
<text class="wifi-ssid connected-wifi-ssid">{{ connectedWifi?.SSID }}</text>
|
|
||||||
<view class="wifi-icons connected-wifi-icons">
|
|
||||||
<image
|
|
||||||
v-if="connectedWifi?.secure"
|
|
||||||
class="security-icon"
|
|
||||||
src="../static/sicon/pwd.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
/>
|
|
||||||
<image class="signal-icon" src="../static/sicon/wifi.png" mode="aspectFit" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 网络列表标题 -->
|
|
||||||
<view class="section-label-row">
|
|
||||||
<text class="section-label">网络</text>
|
|
||||||
<image
|
|
||||||
src="../static/sicon/refresh.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
:style="{ width: '34rpx', height: '34rpx', marginLeft: '8rpx', opacity: isRefreshing ? 0.3 : 0.7 }"
|
|
||||||
@click="startScanning"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 网络列表卡片 -->
|
|
||||||
<scroll-view
|
|
||||||
scroll-y
|
|
||||||
class="wifi-list-card"
|
|
||||||
:class="{ 'wifi-list-scroll': currentState !== 'SCANNING' }"
|
|
||||||
:style="{ height: wifiListScrollHeight }"
|
|
||||||
>
|
|
||||||
<block v-if="currentState === 'SCANNING'">
|
|
||||||
<view class="wifi-item" @click="selectOther">
|
|
||||||
<text class="wifi-ssid other-text">其他...</text>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
<block v-else>
|
|
||||||
<view
|
|
||||||
v-for="(wifi, idx) in wifiList"
|
|
||||||
:key="wifi.SSID"
|
|
||||||
class="wifi-item"
|
|
||||||
@click="selectWifi(wifi)"
|
|
||||||
>
|
|
||||||
<text class="wifi-ssid">{{ wifi.SSID }}</text>
|
|
||||||
<view class="wifi-icons">
|
|
||||||
<image
|
|
||||||
v-if="wifi.secure"
|
|
||||||
class="security-icon"
|
|
||||||
src="../static/sicon/pwd.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
/>
|
|
||||||
<image class="signal-icon" src="../static/sicon/wifi.png" mode="aspectFit" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="wifi-item" @click="selectOther">
|
|
||||||
<text class="wifi-ssid other-text">其他...</text>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<!-- CONNECTED:开始更新按钮 -->
|
|
||||||
<view v-if="currentState === 'CONNECTED'" class="bottom-btn-area connected-bottom-btn-area">
|
|
||||||
<view class="primary-btn update-btn" @click="startUpdate">
|
|
||||||
<text class="primary-btn-text">开始更新</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- UPDATING -->
|
|
||||||
<view v-else-if="currentState === 'UPDATING'" class="center-page">
|
|
||||||
<image src="../static/ota/target-char.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
|
|
||||||
<text class="page-title" style="margin-top: 24rpx;">更新中,请稍等片刻...</text>
|
|
||||||
<view class="progress-wrap">
|
|
||||||
<view class="progress-track">
|
|
||||||
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
|
|
||||||
</view>
|
|
||||||
<text class="progress-pct">{{ Math.floor(progress) }}%</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- DONE -->
|
|
||||||
<view v-else-if="currentState === 'DONE'" class="center-page">
|
|
||||||
<image src="../static/ota/check-char.png" mode="aspectFit" style="width: 194rpx; height: 166rpx;" />
|
|
||||||
<text class="page-title" style="margin-top: 24rpx;">更新完成</text>
|
|
||||||
<text class="page-desc-white">请关机并重启智能弓</text>
|
|
||||||
<view class="primary-btn done-btn" style="margin-top:20px" @click="handleDone">
|
|
||||||
<text class="primary-btn-text">完成</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- FAILED -->
|
|
||||||
<view v-else-if="currentState === 'FAILED'" class="center-page">
|
|
||||||
<image src="../static/ota/close-char.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
|
|
||||||
<text class="page-title fail-title" style="margin-top: 24rpx;">更新失败</text>
|
|
||||||
<text class="page-desc-white">请确保:</text>
|
|
||||||
<text class="page-desc-white">1、智能弓已开启</text>
|
|
||||||
<text class="page-desc-white">2、网路连接稳定</text>
|
|
||||||
<view class="primary-btn done-btn" style="margin-top: 40rpx;" @click="handleRetry">
|
|
||||||
<text class="primary-btn-text">重试</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- CONNECTING 底部弹窗 -->
|
|
||||||
<view v-if="currentState === 'CONNECTING'" class="sheet-mask">
|
|
||||||
<view class="sheet" :style="{ marginBottom: keyboardHeight + 'px' }" @click.stop="">
|
|
||||||
<image src="../static/ota/ota-bg.png" mode="aspectFill" class="sheet-bg" />
|
|
||||||
<view class="sheet-inner">
|
|
||||||
|
|
||||||
<!-- 有密码网络 -->
|
|
||||||
<block v-if="connectMode === 'secure'">
|
|
||||||
<view class="sheet-header">
|
|
||||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
|
||||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
|
||||||
</view>
|
|
||||||
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
|
||||||
<view
|
|
||||||
class="sheet-nav-btn"
|
|
||||||
:class="{ 'nav-disabled': joinDisabled }"
|
|
||||||
@click="joinNetwork"
|
|
||||||
>
|
|
||||||
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="input-row-card">
|
|
||||||
<text class="input-label">密码</text>
|
|
||||||
<input
|
|
||||||
class="input-field"
|
|
||||||
:password="!showPassword"
|
|
||||||
:adjust-position="false"
|
|
||||||
v-model="connectInput.password"
|
|
||||||
placeholder="输入网络密码"
|
|
||||||
placeholder-class="input-placeholder"
|
|
||||||
confirm-type="done"
|
|
||||||
@confirm="joinNetwork"
|
|
||||||
/>
|
|
||||||
<!-- 密码显示/隐藏切换按钮 -->
|
|
||||||
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
|
||||||
<image
|
|
||||||
:src="showPassword ? '../static/sicon/eye-on.png' : '../static/sicon/eye-off.png'"
|
|
||||||
mode="aspectFit"
|
|
||||||
style="width: 40rpx; height: 40rpx;"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
|
|
||||||
</block>
|
|
||||||
|
|
||||||
<!-- 无密码网络 -->
|
|
||||||
<block v-else-if="connectMode === 'open'">
|
|
||||||
<view class="sheet-header">
|
|
||||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
|
||||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
|
||||||
</view>
|
|
||||||
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
|
||||||
<view class="sheet-nav-btn" @click="joinNetwork">
|
|
||||||
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="sheet-hint">该网络为开放网络,点击 ✓ 加入</text>
|
|
||||||
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
|
|
||||||
</block>
|
|
||||||
|
|
||||||
<!-- 手动输入 -->
|
|
||||||
<block v-else-if="connectMode === 'manual'">
|
|
||||||
<view class="sheet-header">
|
|
||||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
|
||||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
|
||||||
</view>
|
|
||||||
<text class="sheet-title">加入无线网络</text>
|
|
||||||
<view
|
|
||||||
class="sheet-nav-btn"
|
|
||||||
:class="{ 'nav-disabled': joinDisabled }"
|
|
||||||
@click="joinNetwork"
|
|
||||||
>
|
|
||||||
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="input-row-card">
|
|
||||||
<text class="input-label">名称</text>
|
|
||||||
<input
|
|
||||||
class="input-field"
|
|
||||||
v-model="connectInput.ssid"
|
|
||||||
placeholder="输入网络名称"
|
|
||||||
placeholder-class="input-placeholder"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
<view class="input-row-card">
|
|
||||||
<text class="input-label">密码</text>
|
|
||||||
<input
|
|
||||||
class="input-field"
|
|
||||||
:password="!showPassword"
|
|
||||||
:adjust-position="false"
|
|
||||||
v-model="connectInput.password"
|
|
||||||
placeholder="输入网络密码"
|
|
||||||
placeholder-class="input-placeholder"
|
|
||||||
confirm-type="done"
|
|
||||||
@confirm="joinNetwork"
|
|
||||||
/>
|
|
||||||
<!-- 密码显示/隐藏切换按钮 -->
|
|
||||||
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
|
||||||
<image
|
|
||||||
:src="showPassword ? '../static/sicon/eye-on.png' : '../static/sicon/eye-off.png'"
|
|
||||||
mode="aspectFit"
|
|
||||||
style="width: 40rpx; height: 40rpx;"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text v-if="ssidWarning" class="connect-error">{{ ssidWarning }}</text>
|
|
||||||
<text v-else-if="connectError" class="connect-error">{{ connectError }}</text>
|
|
||||||
</block>
|
|
||||||
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 授权拒绝弹窗 -->
|
|
||||||
<ScreenHint :show="wifiAuthDeniedVisible" :onClose="() => (wifiAuthDeniedVisible = false)">
|
|
||||||
<view class="wifi-auth-denied">
|
|
||||||
<text class="wifi-auth-denied-text">拒绝授权获取wifi列表失败,请重新授权或手动输入</text>
|
|
||||||
<view class="wifi-auth-denied-btns">
|
|
||||||
<view class="wifi-auth-denied-btn cancel-btn" @click="wifiAuthDeniedVisible = false">
|
|
||||||
<text>取消授权</text>
|
|
||||||
</view>
|
|
||||||
<view class="wifi-auth-denied-btn confirm-btn" @click="handleReauthorize">
|
|
||||||
<text>重新授权</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</ScreenHint>
|
|
||||||
</Container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.wifi-auth-denied {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 32rpx 40rpx 24rpx;
|
|
||||||
gap: 32rpx;
|
|
||||||
}
|
|
||||||
.wifi-auth-denied-text {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 28rpx;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.wifi-auth-denied-btns {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 24rpx;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.wifi-auth-denied-btn {
|
|
||||||
flex: 1;
|
|
||||||
height: 72rpx;
|
|
||||||
border-radius: 36rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
.wifi-auth-denied-btn.cancel-btn {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.wifi-auth-denied-btn.confirm-btn {
|
|
||||||
background: rgba(254, 216, 71, 1);
|
|
||||||
color: #1a1a2e;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wifi-banner {
|
|
||||||
background-color: rgba(255, 200, 0, 0.12);
|
|
||||||
border: 1rpx solid rgba(254, 216, 71, 0.5);
|
|
||||||
border-radius: 8rpx;
|
|
||||||
padding: 16rpx 24rpx;
|
|
||||||
margin: 16rpx 24rpx 0;
|
|
||||||
}
|
|
||||||
.wifi-banner-text {
|
|
||||||
color: rgba(254, 216, 71, 1);
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wifi-page {
|
|
||||||
padding: 0 67rpx 40rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-area {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40rpx 0 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
color: rgba(91, 196, 255, 1);
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
.connected-page-title {
|
|
||||||
color: #4DD9C8;
|
|
||||||
}
|
|
||||||
.page-subtitle {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
font-size: 24rpx;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 32rpx;
|
|
||||||
}
|
|
||||||
.fail-title {
|
|
||||||
color: rgba(255, 100, 100, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ios-guide {
|
|
||||||
background-color: rgba(95, 173, 255, 0.12);
|
|
||||||
border-radius: 8rpx;
|
|
||||||
padding: 16rpx 24rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
.ios-guide-text {
|
|
||||||
color: rgba(95, 173, 255, 1);
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
padding: 0 22rpx;
|
|
||||||
}
|
|
||||||
.connected-section-label-row {
|
|
||||||
margin-top: 32rpx;
|
|
||||||
}
|
|
||||||
.section-label {
|
|
||||||
color: rgba(255, 255, 255, 0.56);
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wifi-list-card {
|
|
||||||
background-color: rgba(30, 35, 50, 0.96);
|
|
||||||
border-radius: 24rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
.wifi-list-scroll {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.connected-wifi-card {
|
|
||||||
background-color: rgba(35, 40, 56, 0.96);
|
|
||||||
margin-bottom: 44rpx;
|
|
||||||
}
|
|
||||||
.wifi-item {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 92rpx;
|
|
||||||
padding: 0 26rpx 0 66rpx;
|
|
||||||
}
|
|
||||||
.wifi-item:not(:last-child)::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 66rpx;
|
|
||||||
right: 26rpx;
|
|
||||||
bottom: 0;
|
|
||||||
height: 1rpx;
|
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
.connected-wifi-item {
|
|
||||||
min-height: 88rpx;
|
|
||||||
padding-left: 28rpx;
|
|
||||||
}
|
|
||||||
.wifi-ssid {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
font-size: 28rpx;
|
|
||||||
line-height: 36rpx;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.connected-wifi-ssid {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.other-text {
|
|
||||||
color: rgba(255, 255, 255, 0.86);
|
|
||||||
}
|
|
||||||
.wifi-icons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.connected-wifi-icons {
|
|
||||||
gap: 14rpx;
|
|
||||||
}
|
|
||||||
.check-icon {
|
|
||||||
width: 28rpx;
|
|
||||||
height: 24rpx;
|
|
||||||
margin-right: 16rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.security-icon {
|
|
||||||
width: 24rpx;
|
|
||||||
height: 30rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.signal-icon {
|
|
||||||
width: 36rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-btn-area {
|
|
||||||
margin-top: 24rpx;
|
|
||||||
}
|
|
||||||
.connected-bottom-btn-area {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-btn {
|
|
||||||
background-color: rgba(254, 216, 71, 1);
|
|
||||||
border-radius: 50rpx;
|
|
||||||
height: 88rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.update-btn {
|
|
||||||
width: 412rpx;
|
|
||||||
}
|
|
||||||
.primary-btn-text {
|
|
||||||
color: rgba(0, 0, 0, 1);
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 80rpx 32rpx 40rpx;
|
|
||||||
}
|
|
||||||
.page-desc-white {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
font-size: 28rpx;
|
|
||||||
line-height: 52rpx;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.done-btn {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-wrap {
|
|
||||||
width: 80%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16rpx;
|
|
||||||
margin-top: 40rpx;
|
|
||||||
}
|
|
||||||
.progress-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 10rpx;
|
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 5rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(91, 196, 255, 1);
|
|
||||||
border-radius: 5rpx;
|
|
||||||
transition: width 0.4s ease;
|
|
||||||
}
|
|
||||||
.progress-pct {
|
|
||||||
color: rgba(91, 196, 255, 1);
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sheet-mask {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 999;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
.sheet {
|
|
||||||
position: relative;
|
|
||||||
background-color: rgba(57, 47, 29, 1);
|
|
||||||
border-top-left-radius: 28rpx;
|
|
||||||
border-top-right-radius: 28rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
min-height: 320rpx;
|
|
||||||
border: 1rpx solid rgba(249, 213, 161, 0.4);
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.sheet-bg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
.sheet-inner {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 32rpx 32rpx 112rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sheet-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 32rpx;
|
|
||||||
}
|
|
||||||
.sheet-nav-btn {
|
|
||||||
width: 64rpx;
|
|
||||||
height: 64rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.nav-disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
.sheet-title {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.sheet-hint {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
font-size: 26rpx;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
|
||||||
.connect-error {
|
|
||||||
color: rgba(254, 216, 71, 1);
|
|
||||||
font-size: 26rpx;
|
|
||||||
line-height: 40rpx;
|
|
||||||
margin-top: 16rpx;
|
|
||||||
display: block;
|
|
||||||
/* 错误提示文案居中展示 */
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-row-card {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 16rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 24rpx 24rpx;
|
|
||||||
margin-left: 48rpx;
|
|
||||||
margin-right: 48rpx;
|
|
||||||
}
|
|
||||||
.input-row-card + .input-row-card {
|
|
||||||
margin-top: 16rpx;
|
|
||||||
}
|
|
||||||
.input-label {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 28rpx;
|
|
||||||
width: 80rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.input-field {
|
|
||||||
flex: 1;
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
font-size: 28rpx;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 0 8rpx;
|
|
||||||
}
|
|
||||||
.input-placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 密码查看切换按钮 */
|
|
||||||
.pwd-eye-btn {
|
|
||||||
width: 56rpx;
|
|
||||||
height: 56rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -38,6 +38,18 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
targetRadius: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
hitRadiusPx: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
zoomHitRadiusPx: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const pMode = ref(true);
|
const pMode = ref(true);
|
||||||
@@ -47,6 +59,8 @@ 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;
|
||||||
|
|
||||||
function showShotFlash(flash) {
|
function showShotFlash(flash) {
|
||||||
const shootData = flash?.shootData;
|
const shootData = flash?.shootData;
|
||||||
@@ -75,13 +89,92 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
function calcRealX(num, offset = 3.4) {
|
const safeTargetRadius = computed(() => {
|
||||||
const len = 20.4 + num;
|
const radius = Number(props.targetRadius);
|
||||||
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
|
return Number.isFinite(radius) && radius > 0 ? radius : 20;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 calcRealY(num, offset = 3.4) {
|
|
||||||
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
|
function getPointDirection(point) {
|
||||||
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
|
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 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const simulShoot = async () => {
|
const simulShoot = async () => {
|
||||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||||
@@ -164,20 +257,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>
|
||||||
@@ -188,20 +275,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
|
||||||
>
|
>
|
||||||
@@ -212,8 +293,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
|
||||||
@@ -226,8 +306,7 @@ 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',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@@ -287,6 +366,31 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
@keyframes target-tip-fade-in-out {
|
||||||
|
0% {
|
||||||
|
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;
|
||||||
|
}
|
||||||
.target > image:last-child {
|
.target > image:last-child {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -297,21 +401,11 @@ onBeforeUnmount(() => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
box-sizing: border-box;
|
||||||
.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: 1;
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -327,6 +421,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;
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ onBeforeUnmount(() => {
|
|||||||
</view>
|
</view>
|
||||||
<block
|
<block
|
||||||
v-if="
|
v-if="
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) === -1 ||
|
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === -1 ||
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) === 10
|
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === 11
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<text>{{ title }}</text>
|
<text>{{ title }}</text>
|
||||||
@@ -151,12 +151,12 @@ onBeforeUnmount(() => {
|
|||||||
<block
|
<block
|
||||||
v-if="
|
v-if="
|
||||||
title &&
|
title &&
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) !== -1 &&
|
'-箭前准备-感知距离-小试牛刀'.indexOf(title) !== -1 &&
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) !== 10
|
'-箭前准备-感知距离-小试牛刀'.indexOf(title) !== 11
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<view class="first-try-steps">
|
<view class="first-try-steps">
|
||||||
<text :class="title === '-凹造型' ? 'current-step' : ''">凹造型</text>
|
<text :class="title === '-箭前准备' ? 'current-step' : ''">箭前准备</text>
|
||||||
<text>-</text>
|
<text>-</text>
|
||||||
<text :class="title === '-感知距离' ? 'current-step' : ''"
|
<text :class="title === '-感知距离' ? 'current-step' : ''"
|
||||||
>感知距离</text
|
>感知距离</text
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 237 B |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 277 B |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 390 B |
|
Before Width: | Height: | Size: 603 B |
|
Before Width: | Height: | Size: 395 B |
|
Before Width: | Height: | Size: 457 B |
|
Before Width: | Height: | Size: 407 B |
|
Before Width: | Height: | Size: 494 B |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 548 B |