Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b060d8f987 | ||
|
|
46cbd37102 | ||
|
|
d4d690cb8e | ||
|
|
b51813dd33 | ||
|
|
a11e7f7532 | ||
|
|
88febb92e5 | ||
|
|
03fa2f4d48 | ||
|
|
c8533de5cf |
@@ -8,9 +8,6 @@
|
|||||||
} from "@dcloudio/uni-app";
|
} from "@dcloudio/uni-app";
|
||||||
import websocket from "@/websocket";
|
import websocket from "@/websocket";
|
||||||
import matchWebsocket from "@/matchWebsocket";
|
import matchWebsocket from "@/matchWebsocket";
|
||||||
import {
|
|
||||||
getDeviceBatteryAPI
|
|
||||||
} from "@/apis";
|
|
||||||
import {
|
import {
|
||||||
MESSAGETYPES
|
MESSAGETYPES
|
||||||
} from "@/constants";
|
} from "@/constants";
|
||||||
@@ -27,7 +24,9 @@
|
|||||||
} = storeToRefs(store);
|
} = storeToRefs(store);
|
||||||
const {
|
const {
|
||||||
updateUser,
|
updateUser,
|
||||||
updateOnline,
|
updateDeviceStatus,
|
||||||
|
setDeviceOnline,
|
||||||
|
clearDeviceStatus,
|
||||||
showDeviceChargingDialog,
|
showDeviceChargingDialog,
|
||||||
clearSessionState,
|
clearSessionState,
|
||||||
clearDevice
|
clearDevice
|
||||||
@@ -69,13 +68,19 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emitUpdateOnline() {
|
function emitUpdateOnline(nextOnline) {
|
||||||
const data = await getDeviceBatteryAPI();
|
|
||||||
const wasOnline = Boolean(online.value);
|
const wasOnline = Boolean(online.value);
|
||||||
const nextOnline = Boolean(data.online);
|
setDeviceOnline(nextOnline === true);
|
||||||
updateOnline(nextOnline);
|
if (!device.value.deviceId || wasOnline === (nextOnline === true)) return;
|
||||||
if (!device.value.deviceId || wasOnline === nextOnline) return;
|
audioManager.play(nextOnline === true ? "设备已连接" : "设备连接已断开");
|
||||||
audioManager.play(nextOnline ? "设备已连接" : "设备连接已断开");
|
}
|
||||||
|
|
||||||
|
function onDeviceStatusPush(status) {
|
||||||
|
updateDeviceStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShootSocketDisconnected() {
|
||||||
|
clearDeviceStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDeviceBindInvalid() {
|
function onDeviceBindInvalid() {
|
||||||
@@ -111,6 +116,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onShootWsMsg(content) {
|
function onShootWsMsg(content) {
|
||||||
|
if (content?.event === "/addons/shoot/battery") {
|
||||||
|
onDeviceStatusPush(content.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(content.type === 'shoot-trigger'){
|
if(content.type === 'shoot-trigger'){
|
||||||
onDeviceShoot()
|
onDeviceShoot()
|
||||||
}
|
}
|
||||||
@@ -122,6 +131,7 @@
|
|||||||
void audioManager.warmButton();
|
void audioManager.warmButton();
|
||||||
uni.$on("update-user", emitUpdateUser);
|
uni.$on("update-user", emitUpdateUser);
|
||||||
uni.$on("update-online", emitUpdateOnline);
|
uni.$on("update-online", emitUpdateOnline);
|
||||||
|
uni.$on("shoot-socket-disconnected", onShootSocketDisconnected);
|
||||||
uni.$on("session-kicked-out", onSessionKickedOut);
|
uni.$on("session-kicked-out", onSessionKickedOut);
|
||||||
uni.$on("device-bind-invalid", onDeviceBindInvalid);
|
uni.$on("device-bind-invalid", onDeviceBindInvalid);
|
||||||
uni.$on("device-charging", onDeviceCharging);
|
uni.$on("device-charging", onDeviceCharging);
|
||||||
@@ -148,6 +158,7 @@
|
|||||||
onHide(() => {
|
onHide(() => {
|
||||||
uni.$off("update-user", emitUpdateUser);
|
uni.$off("update-user", emitUpdateUser);
|
||||||
uni.$off("update-online", emitUpdateOnline);
|
uni.$off("update-online", emitUpdateOnline);
|
||||||
|
uni.$off("shoot-socket-disconnected", onShootSocketDisconnected);
|
||||||
uni.$off("session-kicked-out", onSessionKickedOut);
|
uni.$off("session-kicked-out", onSessionKickedOut);
|
||||||
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
||||||
uni.$off("device-charging", onDeviceCharging);
|
uni.$off("device-charging", onDeviceCharging);
|
||||||
@@ -157,6 +168,7 @@
|
|||||||
matchWebsocket.closeMatchWebSocket({
|
matchWebsocket.closeMatchWebSocket({
|
||||||
reason: "app-hide"
|
reason: "app-hide"
|
||||||
});
|
});
|
||||||
|
clearDeviceStatus();
|
||||||
websocket.closeWebSocket();
|
websocket.closeWebSocket();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -281,6 +281,14 @@ export const getMyDevicesAPI = () => {
|
|||||||
return request("GET", "/user/device/getBindings");
|
return request("GET", "/user/device/getBindings");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDeviceDetailAPI = (deviceId) => {
|
||||||
|
return request("GET", `/user/device/getDetail?deviceId=${encodeURIComponent(deviceId)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDeviceAliasAPI = (deviceId, alias) => {
|
||||||
|
return request("POST", "/user/device/updateAlias", {deviceId, alias});
|
||||||
|
};
|
||||||
|
|
||||||
export const createPractiseAPI = (arrows, time, target) => {
|
export const createPractiseAPI = (arrows, time, target) => {
|
||||||
return request("POST", "/user/practice/create", {
|
return request("POST", "/user/practice/create", {
|
||||||
shootNumber: arrows,
|
shootNumber: arrows,
|
||||||
@@ -561,10 +569,6 @@ export const laserCloseAPI = async () => {
|
|||||||
return request("POST", "/user/device/closeAim");
|
return request("POST", "/user/device/closeAim");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDeviceBatteryAPI = async () => {
|
|
||||||
return request("GET", "/user/device/battery");
|
|
||||||
};
|
|
||||||
|
|
||||||
// 设备连接指定 WiFi,只下发 WiFi 凭证,不触发 OTA 升级。
|
// 设备连接指定 WiFi,只下发 WiFi 凭证,不触发 OTA 升级。
|
||||||
export const connectDeviceWifiAPI = async (ssid, password) => {
|
export const connectDeviceWifiAPI = async (ssid, password) => {
|
||||||
return request("POST", "/user/hardwareBox/connectWifi", {ssid, password});
|
return request("POST", "/user/hardwareBox/connectWifi", {ssid, password});
|
||||||
|
|||||||
@@ -81,6 +81,13 @@ const props = defineProps({
|
|||||||
src="https://static.shelingxingqiu.com/shootmini/static/app-bg9.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/app-bg9.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
|
<!-- 我的设备未绑定/绑定成功页面背景 -->
|
||||||
|
<image
|
||||||
|
class="bg-image"
|
||||||
|
v-if="type === 12"
|
||||||
|
src="../static/device-assets/my-device-unbound-background.png"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
<image
|
<image
|
||||||
class="bg-image"
|
class="bg-image"
|
||||||
v-if="type === 10"
|
v-if="type === 10"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function handleTabClick(index) {
|
|||||||
}
|
}
|
||||||
if (index === 2) {
|
if (index === 2) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/device-intro",
|
url: "/pages/device/device-intro",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import useStore from "@/store";
|
||||||
import { getDeviceBatteryAPI } from "@/apis";
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
const power = ref(0);
|
const store = useStore();
|
||||||
const timer = ref(null);
|
const { deviceBattery: power } = storeToRefs(store);
|
||||||
let disposed = false;
|
|
||||||
let requestInFlight = false;
|
|
||||||
|
|
||||||
const refreshPower = async () => {
|
|
||||||
if (disposed || requestInFlight) return;
|
|
||||||
requestInFlight = true;
|
|
||||||
try {
|
|
||||||
const data = await getDeviceBatteryAPI();
|
|
||||||
if (!disposed) power.value = data.battery;
|
|
||||||
} catch (_) {
|
|
||||||
// 电量轮询失败时等待下一轮,避免产生未处理的 Promise 拒绝。
|
|
||||||
} finally {
|
|
||||||
requestInFlight = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await refreshPower();
|
|
||||||
if (disposed) return;
|
|
||||||
timer.value = setInterval(() => {
|
|
||||||
void refreshPower();
|
|
||||||
}, 1000 * 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
disposed = true;
|
|
||||||
clearInterval(timer.value);
|
|
||||||
timer.value = null;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<image src="../static/b-power.png" mode="widthFix" />
|
<image src="../static/b-power.png" mode="widthFix" />
|
||||||
<view>电量{{ power || 1 }}%</view>
|
<view>{{ power === null ? "电量--" : `电量${power}%` }}</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const cancelMatching = async () => {
|
|||||||
const goCalibration = async () => {
|
const goCalibration = async () => {
|
||||||
await laserAimAPI();
|
await laserAimAPI();
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/calibration",
|
url: "/pages/device/calibration",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { getDeviceBatteryAPI } from "@/apis";
|
import useStore from "@/store";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
const OTA_MIN_BATTERY = 50;
|
|
||||||
const OTA_LOW_BATTERY_TEXT = "电量不足 50%,暂不支持 OTA 升级";
|
|
||||||
const OTA_OFFLINE_TEXT = "请先开启智能弓";
|
const OTA_OFFLINE_TEXT = "请先开启智能弓";
|
||||||
|
const store = useStore();
|
||||||
|
const { deviceStatus } = storeToRefs(store);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -48,27 +49,13 @@ const isFailure = computed(() => props.state === "update_failure");
|
|||||||
// Clamp progress to keep the progress bar width within its container.
|
// Clamp progress to keep the progress bar width within its container.
|
||||||
const progressValue = computed(() => Math.min(100, Math.max(0, Number(props.progress) || 0)));
|
const progressValue = computed(() => Math.min(100, Math.max(0, Number(props.progress) || 0)));
|
||||||
|
|
||||||
// 点击立即更新前先校验设备在线状态,再校验设备电量。
|
// 点击立即更新前先校验设备在线状态。
|
||||||
const handleUpdateClick = async () => {
|
const handleUpdateClick = () => {
|
||||||
try {
|
if (deviceStatus.value?.online !== true) {
|
||||||
const deviceStatus = await getDeviceBatteryAPI();
|
uni.showToast({
|
||||||
if (deviceStatus?.online !== true) {
|
title: OTA_OFFLINE_TEXT,
|
||||||
uni.showToast({
|
icon: "none",
|
||||||
title: OTA_OFFLINE_TEXT,
|
});
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number(deviceStatus?.battery) <= OTA_MIN_BATTERY) {
|
|
||||||
uni.showToast({
|
|
||||||
title: OTA_LOW_BATTERY_TEXT,
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
emit("update");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit("update");
|
emit("update");
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import {
|
|||||||
getHomeData,
|
getHomeData,
|
||||||
getPhoneNumberAPI,
|
getPhoneNumberAPI,
|
||||||
getPhoneNumberAPIv2,
|
getPhoneNumberAPIv2,
|
||||||
getDeviceBatteryAPI,
|
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { updateUser, updateDevice, updateOnline, clearDevice } = store;
|
const { updateUser, updateDevice, clearDevice } = store;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
@@ -125,8 +124,6 @@ async function doLogin() {
|
|||||||
devices.bindings[0].deviceId,
|
devices.bindings[0].deviceId,
|
||||||
devices.bindings[0].deviceName
|
devices.bindings[0].deviceName
|
||||||
);
|
);
|
||||||
const data = await getDeviceBatteryAPI();
|
|
||||||
updateOnline(data.online);
|
|
||||||
} else {
|
} else {
|
||||||
clearDevice();
|
clearDevice();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ onBeforeUnmount(() => {
|
|||||||
</button>
|
</button>
|
||||||
<view class="warnning-text">
|
<view class="warnning-text">
|
||||||
<block v-if="statusText">
|
<block v-if="statusText">
|
||||||
<text v-if="distance > 0">当前距离{{ distance }}米</text>
|
|
||||||
<text>{{ statusText }}</text>
|
<text>{{ statusText }}</text>
|
||||||
</block>
|
</block>
|
||||||
<block v-else>
|
<block v-else>
|
||||||
|
|||||||
@@ -21,9 +21,6 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/audio-test"
|
"path": "pages/audio-test"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/calibration"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/about-us"
|
"path": "pages/about-us"
|
||||||
},
|
},
|
||||||
@@ -60,12 +57,6 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/match-page"
|
"path": "pages/match-page"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/my-device"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/device-intro"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/user"
|
"path": "pages/user"
|
||||||
},
|
},
|
||||||
@@ -107,12 +98,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/mine-bow-data"
|
"path": "pages/mine-bow-data"
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/ota-wifi",
|
|
||||||
"style": {
|
|
||||||
"navigationStyle": "custom"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
@@ -159,6 +144,30 @@
|
|||||||
{
|
{
|
||||||
"root": "pages/device",
|
"root": "pages/device",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "my-device"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "device-qrcode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "device-bind-success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "device-bind-failure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "device-intro"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ota-wifi",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "calibration"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "unbind-device"
|
"path": "unbind-device"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { bindDeviceAPIV2 } from "@/apis";
|
||||||
|
|
||||||
|
export function useDeviceBinding({
|
||||||
|
token,
|
||||||
|
confirmBindTip,
|
||||||
|
binding,
|
||||||
|
updateDevice,
|
||||||
|
deviceDetails,
|
||||||
|
}) {
|
||||||
|
const showBindFailurePage = () => {
|
||||||
|
uni.hideToast();
|
||||||
|
uni.navigateTo({
|
||||||
|
url: "/pages/device/device-bind-failure",
|
||||||
|
fail: (error) => {
|
||||||
|
console.error("打开绑定失败页失败", error);
|
||||||
|
uni.showToast({ title: "二维码不正确,请重新扫码", icon: "none" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScan = () => {
|
||||||
|
uni.scanCode({
|
||||||
|
onlyFromCamera: true,
|
||||||
|
scanType: ["qrCode"],
|
||||||
|
success: (result) => {
|
||||||
|
if (!result?.result) {
|
||||||
|
showBindFailurePage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
token.value = result.result;
|
||||||
|
confirmBindTip.value = true;
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
const message = String(error?.errMsg || error?.message || "");
|
||||||
|
if (/cancel|取消/i.test(message)) return;
|
||||||
|
showBindFailurePage();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmBind = async () => {
|
||||||
|
if (!token.value || binding.value) return;
|
||||||
|
binding.value = true;
|
||||||
|
try {
|
||||||
|
const result = await bindDeviceAPIV2(token.value);
|
||||||
|
confirmBindTip.value = false;
|
||||||
|
if (result?.binded) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "设备已绑定其他账号,请解绑后再绑定",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const deviceId = String(result?.deviceId || "").trim();
|
||||||
|
const deviceName = String(result?.name || result?.deviceName || "").trim();
|
||||||
|
if (!deviceId || !deviceName) {
|
||||||
|
confirmBindTip.value = false;
|
||||||
|
token.value = "";
|
||||||
|
showBindFailurePage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const applyBoundDevice = () => {
|
||||||
|
updateDevice(deviceId, deviceName);
|
||||||
|
deviceDetails.value = result || {};
|
||||||
|
};
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/device/device-bind-success?deviceId=${encodeURIComponent(deviceId)}`,
|
||||||
|
success: applyBoundDevice,
|
||||||
|
fail: (navigationError) => {
|
||||||
|
applyBoundDevice();
|
||||||
|
console.error("打开绑定成功页失败", navigationError);
|
||||||
|
uni.showToast({ title: "绑定成功,请返回查看设备", icon: "none" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("绑定设备失败", error);
|
||||||
|
confirmBindTip.value = false;
|
||||||
|
token.value = "";
|
||||||
|
showBindFailurePage();
|
||||||
|
} finally {
|
||||||
|
binding.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { confirmBind, handleScan, showBindFailurePage };
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { computed, ref } from "vue";
|
||||||
|
import {
|
||||||
|
getDeviceDetailAPI,
|
||||||
|
getMyDevicesAPI,
|
||||||
|
unbindDeviceAPI,
|
||||||
|
} from "@/apis";
|
||||||
|
|
||||||
|
export const DEVICE_NAME_STORAGE_KEY = "device_name_overrides";
|
||||||
|
|
||||||
|
export function useDeviceStatus({
|
||||||
|
user,
|
||||||
|
device,
|
||||||
|
deviceStatus,
|
||||||
|
online,
|
||||||
|
updateDevice,
|
||||||
|
clearDevice,
|
||||||
|
unbindDialogVisible,
|
||||||
|
}) {
|
||||||
|
const deviceDetails = ref({});
|
||||||
|
let deviceDetailRequestVersion = 0;
|
||||||
|
|
||||||
|
const isDeviceOnline = computed(
|
||||||
|
() => deviceStatus.value.online === true || online.value === true
|
||||||
|
);
|
||||||
|
const statusText = computed(() => (isDeviceOnline.value ? "已连接" : "未连接"));
|
||||||
|
const statusClass = computed(() =>
|
||||||
|
isDeviceOnline.value ? "device-status--online" : "device-status--offline"
|
||||||
|
);
|
||||||
|
const battery = computed(() => {
|
||||||
|
const rawValue = deviceStatus.value.battery ?? deviceStatus.value.power;
|
||||||
|
if (rawValue === null || rawValue === undefined || rawValue === "") return null;
|
||||||
|
const value = Number(rawValue);
|
||||||
|
return Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : null;
|
||||||
|
});
|
||||||
|
const batteryText = computed(() =>
|
||||||
|
battery.value === null ? "暂无数据" : `${battery.value}%`
|
||||||
|
);
|
||||||
|
const onlineDurationText = computed(() => {
|
||||||
|
const rawDuration = deviceStatus.value.onlineDuration;
|
||||||
|
if (rawDuration == null || String(rawDuration).trim() === "") return "--";
|
||||||
|
|
||||||
|
const seconds = Number(rawDuration);
|
||||||
|
if (!Number.isFinite(seconds) || seconds < 0) return "--";
|
||||||
|
|
||||||
|
// WS 推送秒数,页面按完整分钟展示累计使用时间。
|
||||||
|
const totalMinutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(totalMinutes / 60);
|
||||||
|
const minutes = totalMinutes % 60;
|
||||||
|
return hours > 0 ? `${hours}小时${minutes}分钟` : `${minutes}分钟`;
|
||||||
|
});
|
||||||
|
const networkType = computed(() =>
|
||||||
|
String(deviceStatus.value.netType ?? "").trim().toLowerCase()
|
||||||
|
);
|
||||||
|
const networkText = computed(() => {
|
||||||
|
const netType = networkType.value;
|
||||||
|
if (netType === "wifi") return "WiFi";
|
||||||
|
if (netType === "4g") return "4G";
|
||||||
|
return isDeviceOnline.value ? "在线" : "未连接";
|
||||||
|
});
|
||||||
|
const maskedDeviceId = computed(() => {
|
||||||
|
const id = String(device.value.deviceId || "");
|
||||||
|
if (!id) return "暂无设备编号";
|
||||||
|
if (id.length <= 3) return id;
|
||||||
|
return `${"*".repeat(Math.min(5, id.length - 3))}${id.slice(-3)}`;
|
||||||
|
});
|
||||||
|
const deviceRows = computed(() => [
|
||||||
|
{ label: "设备型号", value: deviceDetails.value.model || "射灵智能弓" },
|
||||||
|
{ label: "当前电量", value: batteryText.value },
|
||||||
|
{ label: "连接方式", value: networkText.value },
|
||||||
|
{ label: "设备编号", value: maskedDeviceId.value },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getDeviceNameOverrides = () => {
|
||||||
|
const value = uni.getStorageSync(DEVICE_NAME_STORAGE_KEY);
|
||||||
|
return value && typeof value === "object" ? value : {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDeviceDetails = async () => {
|
||||||
|
const deviceId = device.value.deviceId;
|
||||||
|
if (!deviceId) return;
|
||||||
|
|
||||||
|
const requestVersion = ++deviceDetailRequestVersion;
|
||||||
|
try {
|
||||||
|
const detailResponse = await getDeviceDetailAPI(deviceId);
|
||||||
|
const detail = detailResponse?.detail || detailResponse?.data?.detail;
|
||||||
|
if (
|
||||||
|
!detail ||
|
||||||
|
typeof detail !== "object" ||
|
||||||
|
requestVersion !== deviceDetailRequestVersion ||
|
||||||
|
device.value.deviceId !== deviceId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceDetails.value = {
|
||||||
|
...deviceDetails.value,
|
||||||
|
...detail,
|
||||||
|
deviceModelName: detail.deviceModelName ?? "",
|
||||||
|
bindTime: String(
|
||||||
|
detail.bindTime ?? deviceDetails.value.bindTime ?? ""
|
||||||
|
).trim(),
|
||||||
|
qrCodeUrl: String(
|
||||||
|
detail.qrCodeUrl ?? deviceDetails.value.qrCodeUrl ?? ""
|
||||||
|
).trim(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// 实时刷新失败时保留当前页面数据,等待下次通知或页面重新显示。
|
||||||
|
console.log("刷新设备详情失败", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncDeviceBinding = async () => {
|
||||||
|
if (!user.value.id) return;
|
||||||
|
try {
|
||||||
|
const devices = await getMyDevicesAPI();
|
||||||
|
if (Array.isArray(devices?.bindings) && devices.bindings.length > 0) {
|
||||||
|
const currentDevice = devices.bindings[0];
|
||||||
|
const nameOverrides = getDeviceNameOverrides();
|
||||||
|
// 二维码和绑定时间仅取详情接口,绑定列表不能作为这两项的回退数据。
|
||||||
|
let latestDevice = {
|
||||||
|
...currentDevice,
|
||||||
|
deviceModelName: "",
|
||||||
|
bindTime: "",
|
||||||
|
qrCodeUrl: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detailResponse = await getDeviceDetailAPI(currentDevice.deviceId);
|
||||||
|
const detail = detailResponse?.detail || detailResponse?.data?.detail;
|
||||||
|
if (detail && typeof detail === "object") {
|
||||||
|
latestDevice = {
|
||||||
|
...currentDevice,
|
||||||
|
...detail,
|
||||||
|
deviceModelName: detail.deviceModelName ?? "",
|
||||||
|
bindTime: String(detail.bindTime ?? "").trim(),
|
||||||
|
qrCodeUrl: String(detail.qrCodeUrl ?? "").trim(),
|
||||||
|
deviceName:
|
||||||
|
detail.deviceAlias || detail.deviceName || currentDevice.deviceName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 详情接口失败时保留绑定关系,二维码和绑定时间仍保持为空。
|
||||||
|
console.log("获取设备详情失败", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceDetails.value = latestDevice;
|
||||||
|
updateDevice(
|
||||||
|
latestDevice.deviceId,
|
||||||
|
nameOverrides[latestDevice.deviceId] ||
|
||||||
|
latestDevice.deviceAlias ||
|
||||||
|
latestDevice.deviceName ||
|
||||||
|
latestDevice.name ||
|
||||||
|
"我的智能弓"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearDevice();
|
||||||
|
deviceDetailRequestVersion += 1;
|
||||||
|
deviceDetails.value = {};
|
||||||
|
} catch (error) {
|
||||||
|
console.log("同步设备绑定失败", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unbindDevice = async () => {
|
||||||
|
if (!device.value.deviceId) return;
|
||||||
|
try {
|
||||||
|
await unbindDeviceAPI(device.value.deviceId);
|
||||||
|
uni.setStorageSync("calibration", false);
|
||||||
|
clearDevice();
|
||||||
|
deviceDetailRequestVersion += 1;
|
||||||
|
deviceDetails.value = {};
|
||||||
|
unbindDialogVisible.value = false;
|
||||||
|
uni.showToast({ title: "解绑成功", icon: "success" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解绑设备失败", error);
|
||||||
|
if (error?.type === "DEVICE_BIND_INVALID") {
|
||||||
|
clearDevice();
|
||||||
|
unbindDialogVisible.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
batteryText,
|
||||||
|
deviceDetails,
|
||||||
|
deviceRows,
|
||||||
|
getDeviceNameOverrides,
|
||||||
|
isDeviceOnline,
|
||||||
|
maskedDeviceId,
|
||||||
|
networkText,
|
||||||
|
networkType,
|
||||||
|
onlineDurationText,
|
||||||
|
refreshDeviceDetails,
|
||||||
|
statusClass,
|
||||||
|
statusText,
|
||||||
|
syncDeviceBinding,
|
||||||
|
unbindDevice,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<script setup>
|
||||||
|
import Container from "@/components/Container.vue";
|
||||||
|
|
||||||
|
// 失败页由设备页进入,返回时优先回到上一页;页面栈异常时兜底回到设备页。
|
||||||
|
const goBackToDevicePage = () => {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
|
uni.navigateBack({ delta: 1 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.redirectTo({ url: "/pages/device/my-device" });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回设备页后沿用原有扫码绑定流程,不在结果页重复处理二维码数据。
|
||||||
|
const retryScan = () => {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1,
|
||||||
|
success: () => uni.$emit("device-bind-retry-scan"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.redirectTo({ url: "/pages/device/my-device?retryScan=1" });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="device-bind-failure-page">
|
||||||
|
<Container
|
||||||
|
:bgType="12"
|
||||||
|
bgColor="transparent"
|
||||||
|
:onBack="goBackToDevicePage"
|
||||||
|
headerClass="bind-failure-header"
|
||||||
|
:scroll="false"
|
||||||
|
:usePageScroll="true"
|
||||||
|
>
|
||||||
|
<view class="bind-failure-page">
|
||||||
|
<view class="bind-failure-scene">
|
||||||
|
<view class="bind-failure-hero-wrap">
|
||||||
|
<view class="bind-failure-hero-shadow"></view>
|
||||||
|
<image
|
||||||
|
class="bind-failure-hero"
|
||||||
|
src="../../static/device-assets/device-bind-failure-hero.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="bind-failure-message">
|
||||||
|
<text>二维码不正确,</text>
|
||||||
|
<text>仅支持扫描射灵智能弓箭的设备二维码。</text>
|
||||||
|
</view>
|
||||||
|
<view class="bind-failure-retry" @click="$clickSound(retryScan)">
|
||||||
|
<text>重新扫码</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</Container>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.device-bind-failure-page {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-failure-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 与绑定成功页共用固定画布,页面内的视觉尺寸全部按 375 宽设计稿换算为 rpx。 */
|
||||||
|
.bind-failure-page {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-failure-scene {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-failure-hero-wrap,
|
||||||
|
.bind-failure-message,
|
||||||
|
.bind-failure-retry {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-failure-hero-wrap {
|
||||||
|
top: 568rpx;
|
||||||
|
left: 244rpx;
|
||||||
|
width: 260rpx;
|
||||||
|
height: 222rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-failure-hero-shadow {
|
||||||
|
position: absolute;
|
||||||
|
left: 22rpx;
|
||||||
|
bottom: 0;
|
||||||
|
width: 238rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-failure-hero {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 260rpx;
|
||||||
|
height: 222rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-failure-message {
|
||||||
|
top: 824rpx;
|
||||||
|
left: 122rpx;
|
||||||
|
display: flex;
|
||||||
|
width: 504rpx;
|
||||||
|
min-height: 80rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: PingFang SC-Regular;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-failure-retry {
|
||||||
|
top: 960rpx;
|
||||||
|
left: 195rpx;
|
||||||
|
display: flex;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2rpx solid #ffd947;
|
||||||
|
border-radius: 78rpx;
|
||||||
|
color: #ffd947;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 26rpx;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
|
import Container from "@/components/Container.vue";
|
||||||
|
import { getDeviceDetailAPI, getHomeData } from "@/apis";
|
||||||
|
|
||||||
|
const bindResult = ref({
|
||||||
|
isFirstBind: false,
|
||||||
|
expireDate: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const bindSuccessHero = computed(() =>
|
||||||
|
bindResult.value.isFirstBind
|
||||||
|
? "../../static/device-assets/device-bind-success-hero-first.png"
|
||||||
|
: "../../static/device-assets/device-bind-success-hero-nonfirst.png"
|
||||||
|
);
|
||||||
|
const bindRewardExpireDate = computed(() => bindResult.value.expireDate);
|
||||||
|
|
||||||
|
// 与会员页使用相同的到期时间格式,设备详情接口本身不提供会员有效期。
|
||||||
|
const formatVipDate = (value) => {
|
||||||
|
if (!value) return "";
|
||||||
|
const numericValue = Number(value);
|
||||||
|
const timestamp = Number.isNaN(numericValue)
|
||||||
|
? new Date(value).getTime()
|
||||||
|
: numericValue < 1000000000000
|
||||||
|
? numericValue * 1000
|
||||||
|
: numericValue;
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
if (Number.isNaN(date.getTime())) return "";
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadBindResult = async (deviceId) => {
|
||||||
|
if (!deviceId) return;
|
||||||
|
try {
|
||||||
|
const data = await getDeviceDetailAPI(deviceId);
|
||||||
|
if (data?.detail?.isFirstBind !== true) return;
|
||||||
|
bindResult.value.isFirstBind = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const homeData = await getHomeData();
|
||||||
|
bindResult.value.expireDate = formatVipDate(
|
||||||
|
homeData?.user?.normalVipExpiredAt
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取赠送会员有效期失败", error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取设备详情失败", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toFirstTryPage = () => {
|
||||||
|
uni.navigateTo({ url: "/pages/first-try" });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 成功页由设备页 navigateTo 进入,优先返回上一页;页面栈异常时兜底回到设备页。
|
||||||
|
const goBackToDevicePage = () => {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
|
uni.navigateBack({ delta: 1 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.redirectTo({ url: "/pages/device/my-device" });
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((options = {}) => {
|
||||||
|
bindResult.value = {
|
||||||
|
isFirstBind: false,
|
||||||
|
expireDate: "",
|
||||||
|
};
|
||||||
|
void loadBindResult(options.deviceId);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="device-bind-success-page">
|
||||||
|
<Container
|
||||||
|
:bgType="12"
|
||||||
|
bgColor="transparent"
|
||||||
|
:onBack="goBackToDevicePage"
|
||||||
|
headerClass="bind-success-header"
|
||||||
|
:scroll="false"
|
||||||
|
:usePageScroll="true"
|
||||||
|
>
|
||||||
|
<view class="bind-success-page">
|
||||||
|
<view
|
||||||
|
class="bind-success-scene"
|
||||||
|
:class="{ 'bind-success-scene--first': bindResult.isFirstBind }"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="bind-success-confetti"
|
||||||
|
src="../../static/device-assets/device-bind-success-confetti.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<image class="bind-success-hero" :src="bindSuccessHero" mode="aspectFit" />
|
||||||
|
<image
|
||||||
|
class="bind-success-title-image"
|
||||||
|
src="../../static/device-assets/device-bind-success-title.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<view v-if="bindResult.isFirstBind" class="bind-reward-card">
|
||||||
|
<image
|
||||||
|
class="bind-reward-gift"
|
||||||
|
src="../../static/device-assets/device-bind-success-reward-gift.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="bind-reward-text">
|
||||||
|
新设备首次绑定礼包:
|
||||||
|
<text class="bind-reward-highlight">6个月射灵会员</text>
|
||||||
|
已自动发放至本账号<text v-if="bindRewardExpireDate">,有效期{{ bindRewardExpireDate }}</text>。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="bind-success-tutorial" @click="$clickSound(toFirstTryPage)">
|
||||||
|
<text>立即查看新手教程</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</Container>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.device-bind-success-page {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 绑定结果页使用固定画布,背景和前景素材按蓝湖 375 x 812 画布定位。 */
|
||||||
|
.bind-success-page {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-scene {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-confetti,
|
||||||
|
.bind-success-hero,
|
||||||
|
.bind-success-title-image,
|
||||||
|
.bind-reward-card,
|
||||||
|
.bind-success-tutorial {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-confetti {
|
||||||
|
top: 488rpx;
|
||||||
|
left: 78rpx;
|
||||||
|
width: 588rpx;
|
||||||
|
height: 508rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-hero {
|
||||||
|
top: 568rpx;
|
||||||
|
left: 244rpx;
|
||||||
|
width: 260rpx;
|
||||||
|
height: 222rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-scene--first .bind-success-confetti {
|
||||||
|
top: 314rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-scene--first .bind-success-hero {
|
||||||
|
top: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-title-image {
|
||||||
|
top: 828rpx;
|
||||||
|
left: 216rpx;
|
||||||
|
width: 316rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-scene--first .bind-success-title-image {
|
||||||
|
top: 660rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-reward-card {
|
||||||
|
top: 768rpx;
|
||||||
|
left: 170rpx;
|
||||||
|
width: 508rpx;
|
||||||
|
height: 152rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 16rpx 16rpx 16rpx 90rpx;
|
||||||
|
border-radius: 16rpx 64rpx 16rpx 64rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-reward-gift {
|
||||||
|
position: absolute;
|
||||||
|
top: -18rpx;
|
||||||
|
left: -100rpx;
|
||||||
|
width: 178rpx;
|
||||||
|
height: 176rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-reward-text {
|
||||||
|
display: block;
|
||||||
|
width: 380rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: PingFang SC-Regular;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-reward-highlight {
|
||||||
|
color: #ffd947;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-success-tutorial {
|
||||||
|
top: 960rpx;
|
||||||
|
left: 195rpx;
|
||||||
|
display: flex;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2rpx solid #ffd947;
|
||||||
|
border-radius: 78rpx;
|
||||||
|
color: #ffd947;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 26rpx;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -38,7 +38,7 @@ const onScrollView = (e) => {
|
|||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<navigator open-type="navigateBack">
|
<navigator open-type="navigateBack">
|
||||||
<image class="header-back" src="../static/back.png" mode="widthFix" />
|
<image class="header-back" src="../../static/back.png" mode="widthFix" />
|
||||||
</navigator>
|
</navigator>
|
||||||
<text
|
<text
|
||||||
:style="{ opacity: addBg ? 1 : 0, color: '#fff', fontWeight: 'bold' }"
|
:style="{ opacity: addBg ? 1 : 0, color: '#fff', fontWeight: 'bold' }"
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
|
import Container from "@/components/Container.vue";
|
||||||
|
import { getDeviceDetailAPI } from "@/apis";
|
||||||
|
import useStore from "@/store";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { device } = storeToRefs(store);
|
||||||
|
const deviceId = ref("");
|
||||||
|
const qrImageUrl = ref("");
|
||||||
|
const qrSaved = ref(false);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
const loadQrCode = async () => {
|
||||||
|
if (!deviceId.value) {
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getDeviceDetailAPI(deviceId.value);
|
||||||
|
const detail = response?.detail || response?.data?.detail;
|
||||||
|
qrImageUrl.value = String(detail?.qrCodeUrl ?? "").trim();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取设备二维码失败", error);
|
||||||
|
qrImageUrl.value = "";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveQrCode = async () => {
|
||||||
|
if (!qrImageUrl.value) return;
|
||||||
|
|
||||||
|
let filePath = qrImageUrl.value;
|
||||||
|
try {
|
||||||
|
if (/^https?:\/\//.test(filePath)) {
|
||||||
|
filePath = await new Promise((resolve, reject) => {
|
||||||
|
uni.downloadFile({
|
||||||
|
url: filePath,
|
||||||
|
success: (result) =>
|
||||||
|
result.statusCode === 200
|
||||||
|
? resolve(result.tempFilePath)
|
||||||
|
: reject(new Error("二维码下载失败")),
|
||||||
|
fail: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
uni.saveImageToPhotosAlbum({ success: resolve, fail: reject, filePath });
|
||||||
|
});
|
||||||
|
qrSaved.value = true;
|
||||||
|
uni.showToast({ title: "已保存至相册", icon: "success" });
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: "请长按二维码保存", icon: "none" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((options = {}) => {
|
||||||
|
try {
|
||||||
|
deviceId.value = decodeURIComponent(options.deviceId || "") || device.value.deviceId || "";
|
||||||
|
} catch (error) {
|
||||||
|
deviceId.value = device.value.deviceId || "";
|
||||||
|
}
|
||||||
|
void loadQrCode();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Container :bgType="12" :scroll="false">
|
||||||
|
<view class="qr-page">
|
||||||
|
<view class="qr-corner qr-corner--top"></view>
|
||||||
|
<view class="qr-corner qr-corner--bottom"></view>
|
||||||
|
<view class="qr-canvas">
|
||||||
|
<text v-if="loading" class="qr-empty">二维码加载中...</text>
|
||||||
|
<template v-else-if="qrImageUrl">
|
||||||
|
<image class="qr-image" :src="qrImageUrl" mode="aspectFit" show-menu-by-longpress />
|
||||||
|
<text v-if="qrSaved" class="qr-device-id">设备ID:{{ deviceId }}</text>
|
||||||
|
<view v-else class="qr-save-button" @click="saveQrCode">
|
||||||
|
<text>保存至相册</text>
|
||||||
|
</view>
|
||||||
|
<text class="qr-description">
|
||||||
|
该二维码为当前绑定弓箭的二维码,你可以截图保存到相册,以便当二维码丢失或不在身边时,可以扫描二维码进行设备绑定。
|
||||||
|
</text>
|
||||||
|
<text class="qr-note">注:解除绑定后将无法查看该二维码。</text>
|
||||||
|
</template>
|
||||||
|
<text v-else class="qr-empty">暂无设备二维码</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.qr-page {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 300rpx;
|
||||||
|
height: 300rpx;
|
||||||
|
background: #ffeb00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-corner--top {
|
||||||
|
top: -220rpx;
|
||||||
|
right: -170rpx;
|
||||||
|
transform: rotate(42deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-corner--bottom {
|
||||||
|
bottom: -220rpx;
|
||||||
|
left: -170rpx;
|
||||||
|
transform: rotate(42deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-canvas {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 260rpx 62rpx 70rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-image {
|
||||||
|
width: 432rpx;
|
||||||
|
height: 432rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-save-button {
|
||||||
|
display: flex;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 34rpx;
|
||||||
|
border: 1rpx solid #e8c840;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
color: #ffe846;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-device-id,
|
||||||
|
.qr-empty {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-device-id {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-empty {
|
||||||
|
margin-top: 140rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-description,
|
||||||
|
.qr-note {
|
||||||
|
width: 100%;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 36rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-description {
|
||||||
|
margin-top: 42rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-note {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
color: rgba(255, 232, 70, 0.7);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import ScreenHint from "@/components/ScreenHint.vue";
|
import ScreenHint from "@/components/ScreenHint.vue";
|
||||||
import {
|
import {
|
||||||
connectDeviceWifiAPI,
|
connectDeviceWifiAPI,
|
||||||
getDeviceBatteryAPI,
|
|
||||||
getHardwareBoxTaskStatusAPI,
|
getHardwareBoxTaskStatusAPI,
|
||||||
getHardwareBoxVersionAPI,
|
getHardwareBoxVersionAPI,
|
||||||
sendHardwareBoxUpdateAPI,
|
sendHardwareBoxUpdateAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
|
import useStore from "@/store";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { deviceStatus } = storeToRefs(store);
|
||||||
|
|
||||||
const STATES = {
|
const STATES = {
|
||||||
SCANNING: "SCANNING",
|
SCANNING: "SCANNING",
|
||||||
@@ -48,14 +52,13 @@ const progress = ref(0);
|
|||||||
let progressTimer = null;
|
let progressTimer = null;
|
||||||
let timeoutTimer = null;
|
let timeoutTimer = null;
|
||||||
let statusTimer = null;
|
let statusTimer = null;
|
||||||
let wifiConnectTimer = null;
|
let wifiConnectTimeoutTimer = null;
|
||||||
|
let stopWifiStatusWatcher = null;
|
||||||
|
let settleWifiConnectWaiting = null;
|
||||||
let wifiConnectRequestId = 0;
|
let wifiConnectRequestId = 0;
|
||||||
let wifiConnectPollCount = 0;
|
const WIFI_CONNECT_TIMEOUT = 60000;
|
||||||
const WIFI_CONNECT_POLL_INTERVAL = 2000;
|
const DEVICE_STATUS_STALE_TIME = 6000;
|
||||||
const WIFI_CONNECT_MAX_POLL_COUNT = 30;
|
|
||||||
const WIFI_CONNECT_FAILED_TEXT = "连接失败,请检查WiFi密码或WiFi状态";
|
const WIFI_CONNECT_FAILED_TEXT = "连接失败,请检查WiFi密码或WiFi状态";
|
||||||
const OTA_MIN_BATTERY = 50;
|
|
||||||
const OTA_LOW_BATTERY_TEXT = "电量不足 50%,暂不支持 OTA 升级";
|
|
||||||
// 控制授权拒绝弹窗显示/隐藏
|
// 控制授权拒绝弹窗显示/隐藏
|
||||||
const wifiAuthDeniedVisible = ref(false);
|
const wifiAuthDeniedVisible = ref(false);
|
||||||
|
|
||||||
@@ -207,7 +210,7 @@ const startScanning = () => {
|
|||||||
|
|
||||||
// 选择列表中的 WiFi,并打开密码输入弹窗。
|
// 选择列表中的 WiFi,并打开密码输入弹窗。
|
||||||
const selectWifi = (wifi) => {
|
const selectWifi = (wifi) => {
|
||||||
cancelWifiConnectPolling();
|
cancelWifiConnectWaiting();
|
||||||
connectingWifi.value = wifi;
|
connectingWifi.value = wifi;
|
||||||
connectInput.value = { ssid: wifi.SSID, password: "" };
|
connectInput.value = { ssid: wifi.SSID, password: "" };
|
||||||
connectMode.value = wifi.secure ? "secure" : "open";
|
connectMode.value = wifi.secure ? "secure" : "open";
|
||||||
@@ -217,7 +220,7 @@ const selectWifi = (wifi) => {
|
|||||||
|
|
||||||
// 选择手动输入 WiFi,并打开手动输入弹窗。
|
// 选择手动输入 WiFi,并打开手动输入弹窗。
|
||||||
const selectOther = () => {
|
const selectOther = () => {
|
||||||
cancelWifiConnectPolling();
|
cancelWifiConnectWaiting();
|
||||||
connectingWifi.value = null;
|
connectingWifi.value = null;
|
||||||
connectInput.value = { ssid: "", password: "" };
|
connectInput.value = { ssid: "", password: "" };
|
||||||
connectMode.value = "manual";
|
connectMode.value = "manual";
|
||||||
@@ -225,9 +228,9 @@ const selectOther = () => {
|
|||||||
currentState.value = STATES.CONNECTING;
|
currentState.value = STATES.CONNECTING;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 关闭连接弹窗,并停止当前 WiFi 连接轮询。
|
// 关闭连接弹窗,并停止等待当前 WiFi 连接结果。
|
||||||
const closeConnectSheet = () => {
|
const closeConnectSheet = () => {
|
||||||
cancelWifiConnectPolling();
|
cancelWifiConnectWaiting();
|
||||||
connectError.value = "";
|
connectError.value = "";
|
||||||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||||||
};
|
};
|
||||||
@@ -257,89 +260,84 @@ const wifiListScrollHeight = computed(() => {
|
|||||||
return `${Math.min(itemCount * 92, maxHeight)}rpx`;
|
return `${Math.min(itemCount * 92, maxHeight)}rpx`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清理 WiFi 连接轮询定时器。
|
// 清理本次 WiFi 连接状态监听和超时计时器。
|
||||||
const clearWifiConnectTimer = () => {
|
const clearWifiConnectWatcher = () => {
|
||||||
clearTimeout(wifiConnectTimer);
|
clearTimeout(wifiConnectTimeoutTimer);
|
||||||
wifiConnectTimer = null;
|
wifiConnectTimeoutTimer = null;
|
||||||
wifiConnectPollCount = 0;
|
if (stopWifiStatusWatcher) {
|
||||||
|
stopWifiStatusWatcher();
|
||||||
|
stopWifiStatusWatcher = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 取消当前 WiFi 连接轮询,并恢复弹窗提交状态。
|
// 取消当前 WiFi 连接确认,并恢复弹窗提交状态。
|
||||||
const cancelWifiConnectPolling = () => {
|
const cancelWifiConnectWaiting = () => {
|
||||||
wifiConnectRequestId += 1;
|
wifiConnectRequestId += 1;
|
||||||
clearWifiConnectTimer();
|
if (settleWifiConnectWaiting) {
|
||||||
|
settleWifiConnectWaiting(false);
|
||||||
|
}
|
||||||
|
clearWifiConnectWatcher();
|
||||||
connectStatusText.value = "";
|
connectStatusText.value = "";
|
||||||
isSubmittingWifi.value = false;
|
isSubmittingWifi.value = false;
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 判断设备电量接口返回的 online/netType 字段,确定设备是否已通过 WiFi 在线。
|
// 根据实时推送的 online/netType 判断设备是否已通过 WiFi 在线。
|
||||||
// 返回值含义:true → WiFi 在线成功;"net_fail" → 设备走 4g 失败;false → 未就绪,需继续轮询。
|
// 返回值含义:true → WiFi 在线成功;"net_fail" → 设备走 4g 失败;false → 未就绪。
|
||||||
const isDeviceConnectedByWifi = (deviceStatus) => {
|
const isDeviceConnectedByWifi = (deviceStatus) => {
|
||||||
// online 不为 true → 设备不在线,需继续轮询
|
// online 不为 true → 设备尚未在线,继续等待推送
|
||||||
if (deviceStatus?.online !== true) return false;
|
if (deviceStatus?.online !== true) return false;
|
||||||
const netType = String(deviceStatus?.netType || "").toLowerCase();
|
const netType = String(deviceStatus?.netType || "").toLowerCase();
|
||||||
// online:true + netType:4g → 设备已切 4g,WiFi 连接失败
|
// online:true + netType:4g → 设备已切 4g,WiFi 连接失败
|
||||||
if (netType === "4g") return "net_fail";
|
if (netType === "4g") return "net_fail";
|
||||||
// online:true + netType:wifi → WiFi 连接成功
|
// online:true + netType:wifi → WiFi 连接成功
|
||||||
// online:true + netType:"" → 设备在线但 netType 暂未上报,继续轮询等待
|
// online:true + netType:"" → 设备在线但 netType 暂未上报,继续等待推送
|
||||||
return netType === "wifi";
|
return netType === "wifi";
|
||||||
};
|
};
|
||||||
|
|
||||||
// 轮询设备电量接口,等待设备切到 WiFi 在线;netType:4g 快速失败,超时 30 次后放弃。
|
// 等待提交配置后的新 WS 状态;忽略提交前的旧状态,超时后按连接失败处理。
|
||||||
const waitForDeviceWifiConnected = (requestId) => {
|
const waitForDeviceWifiConnected = (requestId, receivedAfter) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const poll = async () => {
|
let settled = false;
|
||||||
if (requestId !== wifiConnectRequestId) {
|
const finish = (result) => {
|
||||||
resolve(false);
|
if (settled) return;
|
||||||
return;
|
settled = true;
|
||||||
}
|
clearWifiConnectWatcher();
|
||||||
|
settleWifiConnectWaiting = null;
|
||||||
wifiConnectPollCount += 1;
|
resolve(result);
|
||||||
try {
|
|
||||||
const deviceStatus = await getDeviceBatteryAPI();
|
|
||||||
if (requestId !== wifiConnectRequestId) {
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const connResult = isDeviceConnectedByWifi(deviceStatus);
|
|
||||||
// online:true + netType:wifi → 成功
|
|
||||||
if (connResult === true) {
|
|
||||||
resolve(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// online:true + netType:4g → 立即失败(设备已切 4g,WiFi 连不上)
|
|
||||||
if (connResult === "net_fail") {
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// online:false + netType:"" → 继续轮询
|
|
||||||
// online:true + netType:"" → 忽略,继续轮询(netType 暂未上报)
|
|
||||||
} catch (err) {
|
|
||||||
if (requestId !== wifiConnectRequestId) {
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wifiConnectPollCount >= WIFI_CONNECT_MAX_POLL_COUNT) {
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wifiConnectTimer = setTimeout(poll, WIFI_CONNECT_POLL_INTERVAL);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
poll();
|
settleWifiConnectWaiting = finish;
|
||||||
|
stopWifiStatusWatcher = watch(
|
||||||
|
deviceStatus,
|
||||||
|
(status) => {
|
||||||
|
if (requestId !== wifiConnectRequestId) {
|
||||||
|
finish(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Number(status?.receivedAt) <= receivedAfter) return;
|
||||||
|
|
||||||
|
const connResult = isDeviceConnectedByWifi(status);
|
||||||
|
if (connResult === true) {
|
||||||
|
finish(true);
|
||||||
|
} else if (connResult === "net_fail") {
|
||||||
|
finish(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
wifiConnectTimeoutTimer = setTimeout(
|
||||||
|
() => finish(false),
|
||||||
|
WIFI_CONNECT_TIMEOUT
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交 WiFi 配置给游戏设备,并轮询确认设备真实连上 WiFi 后再展示成功。
|
// 提交 WiFi 配置给游戏设备,并通过 WS 确认设备真实连上 WiFi 后再展示成功。
|
||||||
const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
||||||
if (isSubmittingWifi.value) return;
|
if (isSubmittingWifi.value) return;
|
||||||
wifiConnectRequestId += 1;
|
wifiConnectRequestId += 1;
|
||||||
const requestId = wifiConnectRequestId;
|
const requestId = wifiConnectRequestId;
|
||||||
clearWifiConnectTimer();
|
clearWifiConnectWatcher();
|
||||||
isSubmittingWifi.value = true;
|
isSubmittingWifi.value = true;
|
||||||
connectStatusText.value = "WiFi连接中...";
|
connectStatusText.value = "WiFi连接中...";
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
@@ -348,7 +346,8 @@ const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await connectDeviceWifiAPI(ssid, password);
|
await connectDeviceWifiAPI(ssid, password);
|
||||||
const isConnected = await waitForDeviceWifiConnected(requestId);
|
if (requestId !== wifiConnectRequestId) return;
|
||||||
|
const isConnected = await waitForDeviceWifiConnected(requestId, Date.now());
|
||||||
if (requestId !== wifiConnectRequestId) return;
|
if (requestId !== wifiConnectRequestId) return;
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
connectError.value = WIFI_CONNECT_FAILED_TEXT;
|
connectError.value = WIFI_CONNECT_FAILED_TEXT;
|
||||||
@@ -369,7 +368,7 @@ const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (requestId === wifiConnectRequestId) {
|
if (requestId === wifiConnectRequestId) {
|
||||||
clearWifiConnectTimer();
|
clearWifiConnectWatcher();
|
||||||
connectStatusText.value = "";
|
connectStatusText.value = "";
|
||||||
isSubmittingWifi.value = false;
|
isSubmittingWifi.value = false;
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
@@ -456,7 +455,9 @@ const pollUpdateTaskStatus = (taskId) => {
|
|||||||
// 判断设备是否满足 OTA 更新条件,不满足时返回精确提示文案。
|
// 判断设备是否满足 OTA 更新条件,不满足时返回精确提示文案。
|
||||||
const getUpdateDisabledReason = (deviceStatus) => {
|
const getUpdateDisabledReason = (deviceStatus) => {
|
||||||
if (deviceStatus?.online !== true) return "请先开启智能弓";
|
if (deviceStatus?.online !== true) return "请先开启智能弓";
|
||||||
if (Number(deviceStatus?.battery) <= OTA_MIN_BATTERY) return OTA_LOW_BATTERY_TEXT;
|
if (Date.now() - Number(deviceStatus?.receivedAt || 0) > DEVICE_STATUS_STALE_TIME) {
|
||||||
|
return "设备状态同步中,请稍后重试";
|
||||||
|
}
|
||||||
if (String(deviceStatus?.netType || "").toLowerCase() !== "wifi") return "设备当前未连接 WiFi,请先连接 WiFi 后再更新";
|
if (String(deviceStatus?.netType || "").toLowerCase() !== "wifi") return "设备当前未连接 WiFi,请先连接 WiFi 后再更新";
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
@@ -480,8 +481,7 @@ const startUpdate = async () => {
|
|||||||
isStartingUpdate.value = true;
|
isStartingUpdate.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deviceStatus = await getDeviceBatteryAPI();
|
const disabledReason = getUpdateDisabledReason(deviceStatus.value);
|
||||||
const disabledReason = getUpdateDisabledReason(deviceStatus);
|
|
||||||
if (disabledReason) {
|
if (disabledReason) {
|
||||||
isStartingUpdate.value = false;
|
isStartingUpdate.value = false;
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -612,7 +612,7 @@ onUnmounted(() => {
|
|||||||
if (typeof uni.offKeyboardHeightChange === "function") {
|
if (typeof uni.offKeyboardHeightChange === "function") {
|
||||||
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
|
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
|
||||||
}
|
}
|
||||||
cancelWifiConnectPolling();
|
cancelWifiConnectWaiting();
|
||||||
clearUpdateTimers();
|
clearUpdateTimers();
|
||||||
wx.offGetWifiList && wx.offGetWifiList();
|
wx.offGetWifiList && wx.offGetWifiList();
|
||||||
});
|
});
|
||||||
@@ -650,16 +650,16 @@ onUnmounted(() => {
|
|||||||
</view>
|
</view>
|
||||||
<view v-if="currentState === 'CONNECTED'" class="wifi-list-card connected-wifi-card">
|
<view v-if="currentState === 'CONNECTED'" class="wifi-list-card connected-wifi-card">
|
||||||
<view class="wifi-item connected-wifi-item">
|
<view class="wifi-item connected-wifi-item">
|
||||||
<image class="check-icon" src="../static/sicon/check.png" mode="aspectFit" />
|
<image class="check-icon" src="../../static/sicon/check.png" mode="aspectFit" />
|
||||||
<text class="wifi-ssid connected-wifi-ssid">{{ connectedWifi?.SSID }}</text>
|
<text class="wifi-ssid connected-wifi-ssid">{{ connectedWifi?.SSID }}</text>
|
||||||
<view class="wifi-icons connected-wifi-icons">
|
<view class="wifi-icons connected-wifi-icons">
|
||||||
<image
|
<image
|
||||||
v-if="connectedWifi?.secure"
|
v-if="connectedWifi?.secure"
|
||||||
class="security-icon"
|
class="security-icon"
|
||||||
src="../static/sicon/pwd.png"
|
src="../../static/sicon/pwd.png"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
/>
|
/>
|
||||||
<image class="signal-icon" src="../static/sicon/wifi.png" mode="aspectFit" />
|
<image class="signal-icon" src="../../static/sicon/wifi.png" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -668,7 +668,7 @@ onUnmounted(() => {
|
|||||||
<view class="section-label-row">
|
<view class="section-label-row">
|
||||||
<text class="section-label">网络</text>
|
<text class="section-label">网络</text>
|
||||||
<image
|
<image
|
||||||
src="../static/sicon/refresh.png"
|
src="../../static/sicon/refresh.png"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
:style="{ width: '34rpx', height: '34rpx', marginLeft: '8rpx', opacity: isRefreshing ? 0.3 : 0.7 }"
|
:style="{ width: '34rpx', height: '34rpx', marginLeft: '8rpx', opacity: isRefreshing ? 0.3 : 0.7 }"
|
||||||
@click="startScanning"
|
@click="startScanning"
|
||||||
@@ -699,10 +699,10 @@ onUnmounted(() => {
|
|||||||
<image
|
<image
|
||||||
v-if="wifi.secure"
|
v-if="wifi.secure"
|
||||||
class="security-icon"
|
class="security-icon"
|
||||||
src="../static/sicon/pwd.png"
|
src="../../static/sicon/pwd.png"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
/>
|
/>
|
||||||
<image class="signal-icon" src="../static/sicon/wifi.png" mode="aspectFit" />
|
<image class="signal-icon" src="../../static/sicon/wifi.png" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="wifi-item" @click="selectOther">
|
<view class="wifi-item" @click="selectOther">
|
||||||
@@ -763,7 +763,7 @@ onUnmounted(() => {
|
|||||||
<block v-if="connectMode === 'secure'">
|
<block v-if="connectMode === 'secure'">
|
||||||
<view class="sheet-header">
|
<view class="sheet-header">
|
||||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
<image src="../../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||||
</view>
|
</view>
|
||||||
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
||||||
<view
|
<view
|
||||||
@@ -771,7 +771,7 @@ onUnmounted(() => {
|
|||||||
:class="{ 'nav-disabled': joinDisabled }"
|
:class="{ 'nav-disabled': joinDisabled }"
|
||||||
@click="joinNetwork"
|
@click="joinNetwork"
|
||||||
>
|
>
|
||||||
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
<image src="../../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="input-row-card">
|
<view class="input-row-card">
|
||||||
@@ -789,7 +789,7 @@ onUnmounted(() => {
|
|||||||
<!-- 密码显示/隐藏切换按钮 -->
|
<!-- 密码显示/隐藏切换按钮 -->
|
||||||
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
||||||
<image
|
<image
|
||||||
:src="showPassword ? '../static/sicon/eye-on.png' : '../static/sicon/eye-off.png'"
|
:src="showPassword ? '../../static/sicon/eye-on.png' : '../../static/sicon/eye-off.png'"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
style="width: 40rpx; height: 40rpx;"
|
style="width: 40rpx; height: 40rpx;"
|
||||||
/>
|
/>
|
||||||
@@ -803,11 +803,11 @@ onUnmounted(() => {
|
|||||||
<block v-else-if="connectMode === 'open'">
|
<block v-else-if="connectMode === 'open'">
|
||||||
<view class="sheet-header">
|
<view class="sheet-header">
|
||||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
<image src="../../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||||
</view>
|
</view>
|
||||||
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
||||||
<view class="sheet-nav-btn" @click="joinNetwork">
|
<view class="sheet-nav-btn" @click="joinNetwork">
|
||||||
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
<image src="../../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text v-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
|
<text v-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
|
||||||
@@ -819,7 +819,7 @@ onUnmounted(() => {
|
|||||||
<block v-else-if="connectMode === 'manual'">
|
<block v-else-if="connectMode === 'manual'">
|
||||||
<view class="sheet-header">
|
<view class="sheet-header">
|
||||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
<image src="../../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||||
</view>
|
</view>
|
||||||
<text class="sheet-title">加入无线网络</text>
|
<text class="sheet-title">加入无线网络</text>
|
||||||
<view
|
<view
|
||||||
@@ -827,7 +827,7 @@ onUnmounted(() => {
|
|||||||
:class="{ 'nav-disabled': joinDisabled }"
|
:class="{ 'nav-disabled': joinDisabled }"
|
||||||
@click="joinNetwork"
|
@click="joinNetwork"
|
||||||
>
|
>
|
||||||
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
<image src="../../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="input-row-card">
|
<view class="input-row-card">
|
||||||
@@ -854,7 +854,7 @@ onUnmounted(() => {
|
|||||||
<!-- 密码显示/隐藏切换按钮 -->
|
<!-- 密码显示/隐藏切换按钮 -->
|
||||||
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
||||||
<image
|
<image
|
||||||
:src="showPassword ? '../static/sicon/eye-on.png' : '../static/sicon/eye-off.png'"
|
:src="showPassword ? '../../static/sicon/eye-on.png' : '../../static/sicon/eye-off.png'"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
style="width: 40rpx; height: 40rpx;"
|
style="width: 40rpx; height: 40rpx;"
|
||||||
/>
|
/>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, onUnmounted, ref, watch} from "vue";
|
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
|
||||||
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import AppFooter from "@/components/AppFooter.vue";
|
import AppFooter from "@/components/AppFooter.vue";
|
||||||
@@ -11,7 +11,6 @@ import OtaModal from "@/components/OtaModal.vue";
|
|||||||
import {
|
import {
|
||||||
checkUserBindAPI,
|
checkUserBindAPI,
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getDeviceBatteryAPI,
|
|
||||||
getHardwareBoxTaskStatusAPI,
|
getHardwareBoxTaskStatusAPI,
|
||||||
getHardwareBoxVersionAPI,
|
getHardwareBoxVersionAPI,
|
||||||
getHomeData,
|
getHomeData,
|
||||||
@@ -33,13 +32,40 @@ const {
|
|||||||
clearDevice,
|
clearDevice,
|
||||||
getLvlName,
|
getLvlName,
|
||||||
getLvlNameByScore,
|
getLvlNameByScore,
|
||||||
updateOnline,
|
|
||||||
} = store;
|
} = store;
|
||||||
const {user, device, online, game} = storeToRefs(store);
|
const {user, device, deviceStatus, online, deviceBattery, game} = storeToRefs(store);
|
||||||
|
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const scoreRankList = ref([]);
|
const scoreRankList = ref([]);
|
||||||
|
const DEVICE_STATUS_STALE_TIME = 6000;
|
||||||
|
|
||||||
|
// 首页设备卡片按“未绑定 / 已绑定未连接 / 已绑定已连接”三态展示。
|
||||||
|
const deviceCardState = computed(() => {
|
||||||
|
// 未登录时始终展示绑定入口,避免本地残留设备状态误显示为已绑定。
|
||||||
|
if (!user.value?.id || !device.value?.deviceId) return "unbound";
|
||||||
|
return online.value === true ? "online" : "offline";
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceCardAssets = {
|
||||||
|
unbound: {
|
||||||
|
bow: "../static/home-device/device-bow--unbound.png",
|
||||||
|
background: "../static/home-device/device-card-bg--unbound.png",
|
||||||
|
actionBackground: "../static/home-device/device-action-bg--bind.png",
|
||||||
|
},
|
||||||
|
offline: {
|
||||||
|
bow: "../static/home-device/device-bow--offline.png",
|
||||||
|
background: "../static/home-device/device-card-bg--bound.png",
|
||||||
|
actionBackground: "../static/home-device/device-action-bg--device.png",
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
bow: "../static/home-device/device-bow--online.png",
|
||||||
|
background: "../static/home-device/device-card-bg--bound.png",
|
||||||
|
actionBackground: "../static/home-device/device-action-bg--device.png",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deviceCardAsset = computed(() => deviceCardAssets[deviceCardState.value]);
|
||||||
|
|
||||||
// OTA 相关
|
// OTA 相关
|
||||||
const otaVisible = ref(false);
|
const otaVisible = ref(false);
|
||||||
@@ -50,6 +76,7 @@ const otaInfo = ref({
|
|||||||
versionInfo: "",
|
versionInfo: "",
|
||||||
resourceUrl: "",
|
resourceUrl: "",
|
||||||
forceUpdate: false,
|
forceUpdate: false,
|
||||||
|
needUpdate: false,
|
||||||
});
|
});
|
||||||
const isStartingOta = ref(false);
|
const isStartingOta = ref(false);
|
||||||
let isCheckingOta = false;
|
let isCheckingOta = false;
|
||||||
@@ -107,6 +134,8 @@ const applyOtaVersionInfo = (versionInfo) => {
|
|||||||
versionInfo: versionInfo?.versionInfo || "",
|
versionInfo: versionInfo?.versionInfo || "",
|
||||||
resourceUrl: versionInfo?.resourceUrl || "",
|
resourceUrl: versionInfo?.resourceUrl || "",
|
||||||
forceUpdate: Number(versionInfo?.forceUpdate) === 1,
|
forceUpdate: Number(versionInfo?.forceUpdate) === 1,
|
||||||
|
needUpdate:
|
||||||
|
versionInfo?.needUpdate === true || Number(versionInfo?.needUpdate) === 1,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,13 +145,7 @@ const checkOtaUpdate = async () => {
|
|||||||
isCheckingOta = true;
|
isCheckingOta = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let deviceStatus;
|
if (online.value !== true || otaVisible.value) return;
|
||||||
try {
|
|
||||||
deviceStatus = await getDeviceBatteryAPI();
|
|
||||||
} catch (err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (deviceStatus?.online !== true || otaVisible.value) return;
|
|
||||||
|
|
||||||
let versionInfo;
|
let versionInfo;
|
||||||
try {
|
try {
|
||||||
@@ -130,8 +153,9 @@ const checkOtaUpdate = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!versionInfo?.needUpdate || otaVisible.value) return;
|
if (otaVisible.value) return;
|
||||||
applyOtaVersionInfo(versionInfo);
|
applyOtaVersionInfo(versionInfo);
|
||||||
|
if (!otaInfo.value.needUpdate) return;
|
||||||
|
|
||||||
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -165,7 +189,7 @@ const getOtaWifiUrl = () => {
|
|||||||
`versionNumber=${encodeURIComponent(versionNumber)}`,
|
`versionNumber=${encodeURIComponent(versionNumber)}`,
|
||||||
`resourceUrl=${encodeURIComponent(resourceUrl)}`,
|
`resourceUrl=${encodeURIComponent(resourceUrl)}`,
|
||||||
].join("&");
|
].join("&");
|
||||||
return `/pages/ota-wifi?${query}`;
|
return `/pages/device/ota-wifi?${query}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理 OTA 弹窗暂不更新,强制更新时不允许关闭。
|
// 处理 OTA 弹窗暂不更新,强制更新时不允许关闭。
|
||||||
@@ -189,6 +213,7 @@ const completeHomeOtaUpdate = (runId) => {
|
|||||||
if (!isOtaUpdateRunActive(runId)) return;
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
invalidateOtaUpdateRun();
|
invalidateOtaUpdateRun();
|
||||||
isStartingOta.value = false;
|
isStartingOta.value = false;
|
||||||
|
otaInfo.value = {...otaInfo.value, needUpdate: false};
|
||||||
otaProgress.value = 100;
|
otaProgress.value = 100;
|
||||||
const completedRunId = otaUpdateRunId;
|
const completedRunId = otaUpdateRunId;
|
||||||
otaResultTimer = setTimeout(() => {
|
otaResultTimer = setTimeout(() => {
|
||||||
@@ -272,19 +297,9 @@ const startHomeOtaUpdate = async () => {
|
|||||||
const handleOtaUpdate = async () => {
|
const handleOtaUpdate = async () => {
|
||||||
if (isStartingOta.value) return;
|
if (isStartingOta.value) return;
|
||||||
isStartingOta.value = true;
|
isStartingOta.value = true;
|
||||||
let deviceStatus;
|
const currentDeviceStatus = deviceStatus.value;
|
||||||
try {
|
|
||||||
deviceStatus = await getDeviceBatteryAPI();
|
|
||||||
} catch (err) {
|
|
||||||
isStartingOta.value = false;
|
|
||||||
uni.showToast({
|
|
||||||
title: "获取设备状态失败,请重试",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceStatus?.online !== true) {
|
if (currentDeviceStatus?.online !== true) {
|
||||||
isStartingOta.value = false;
|
isStartingOta.value = false;
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "请先开启智能弓",
|
title: "请先开启智能弓",
|
||||||
@@ -293,7 +308,19 @@ const handleOtaUpdate = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (String(deviceStatus?.netType || "").toLowerCase() === "wifi") {
|
if (
|
||||||
|
Date.now() - Number(currentDeviceStatus?.receivedAt || 0) >
|
||||||
|
DEVICE_STATUS_STALE_TIME
|
||||||
|
) {
|
||||||
|
isStartingOta.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: "设备状态同步中,请稍后重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(currentDeviceStatus?.netType || "").toLowerCase() === "wifi") {
|
||||||
startHomeOtaUpdate();
|
startHomeOtaUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -336,6 +363,51 @@ const toRankListPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 清理首页设备相关状态;clearDevice 会同步清空 deviceId 和在线状态。
|
||||||
|
const clearHomeDeviceState = () => {
|
||||||
|
clearDevice();
|
||||||
|
uni.setStorageSync("calibration", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从后端同步首页设备绑定关系和连接状态,绑定关系与在线状态共同决定卡片三态。
|
||||||
|
const syncHomeDevice = async () => {
|
||||||
|
let devices;
|
||||||
|
try {
|
||||||
|
devices = await getMyDevicesAPI();
|
||||||
|
} catch (error) {
|
||||||
|
// 绑定接口失败时保留当前设备,避免网络抖动被误判为未绑定。
|
||||||
|
console.log("同步首页设备绑定失败", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindings = devices?.bindings;
|
||||||
|
|
||||||
|
// 服务端用 null 或空数组表示当前账号没有绑定设备。
|
||||||
|
if (bindings === null || (Array.isArray(bindings) && bindings.length === 0)) {
|
||||||
|
clearHomeDeviceState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bindings 缺失或类型错误才属于异常响应,保留已有设备状态。
|
||||||
|
if (!Array.isArray(bindings)) {
|
||||||
|
console.log("首页设备绑定响应格式异常", devices);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDevice = bindings.find((item) => item?.deviceId);
|
||||||
|
if (!currentDevice) {
|
||||||
|
clearHomeDeviceState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceId = String(currentDevice.deviceId || "");
|
||||||
|
updateDevice(
|
||||||
|
deviceId,
|
||||||
|
currentDevice.deviceName || currentDevice.name || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
onShow(async (options) => {
|
onShow(async (options) => {
|
||||||
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
||||||
const token = uni.getStorageSync(`${env}_token`);
|
const token = uni.getStorageSync(`${env}_token`);
|
||||||
@@ -343,6 +415,9 @@ onShow(async (options) => {
|
|||||||
// 检查是否从 OTA 更新页面返回
|
// 检查是否从 OTA 更新页面返回
|
||||||
if (options && options.updateResult) {
|
if (options && options.updateResult) {
|
||||||
otaState.value = options.updateResult;
|
otaState.value = options.updateResult;
|
||||||
|
if (options.updateResult === "update_success") {
|
||||||
|
otaInfo.value = {...otaInfo.value, needUpdate: false};
|
||||||
|
}
|
||||||
otaVisible.value = true;
|
otaVisible.value = true;
|
||||||
} else if (token || user.value.id) {
|
} else if (token || user.value.id) {
|
||||||
await checkOtaUpdate();
|
await checkOtaUpdate();
|
||||||
@@ -363,8 +438,6 @@ onShow(async (options) => {
|
|||||||
// devices.bindings[0].deviceId,
|
// devices.bindings[0].deviceId,
|
||||||
// devices.bindings[0].deviceName
|
// devices.bindings[0].deviceName
|
||||||
// );
|
// );
|
||||||
// const data = await getDeviceBatteryAPI();
|
|
||||||
// updateOnline(data.online);
|
|
||||||
// }
|
// }
|
||||||
// } else {
|
// } else {
|
||||||
// showModal.value = true;
|
// showModal.value = true;
|
||||||
@@ -400,19 +473,10 @@ onShow(async (options) => {
|
|||||||
showGuide.value = false;
|
showGuide.value = false;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
const devices = await getMyDevicesAPI();
|
await syncHomeDevice();
|
||||||
if (devices.bindings && devices.bindings.length) {
|
|
||||||
updateDevice(
|
|
||||||
devices.bindings[0].deviceId,
|
|
||||||
devices.bindings[0].deviceName
|
|
||||||
);
|
|
||||||
const data = await getDeviceBatteryAPI();
|
|
||||||
updateOnline(data.online);
|
|
||||||
} else {
|
|
||||||
clearDevice();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -468,30 +532,91 @@ onShareTimeline(() => {
|
|||||||
/> -->
|
/> -->
|
||||||
</view>
|
</view>
|
||||||
<UserHeader showRank :onSignin="() => (showModal = true)"/>
|
<UserHeader showRank :onSignin="() => (showModal = true)"/>
|
||||||
<view :style="{ padding: '12px 10px' }">
|
<view :style="{ padding: '24rpx 20rpx' }">
|
||||||
<view class="feature-grid">
|
<view class="feature-grid">
|
||||||
<view class="bow-card">
|
<view class="bow-card">
|
||||||
|
<view
|
||||||
|
class="device-visual"
|
||||||
|
@click="$clickSound(() => toPage('/pages/device/my-device'))"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="device-visual-background"
|
||||||
|
:src="deviceCardAsset.background"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
class="device-visual-bow"
|
||||||
|
:class="{ 'device-visual-bow--floating': deviceCardState === 'online' }"
|
||||||
|
:src="deviceCardAsset.bow"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-if="user.id && deviceCardState === 'offline'"
|
||||||
|
class="device-status-badge device-status-badge--offline"
|
||||||
|
@click.stop="$clickSound(() => toPage('/pages/device/my-device'))"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="device-status-icon"
|
||||||
|
src="../static/home-device/device-state-dot--offline.png"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
<text>未连接</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="user.id && deviceCardState === 'online'"
|
||||||
|
class="device-status-badge device-status-badge--online"
|
||||||
|
@click.stop="$clickSound(() => toPage('/pages/device/my-device'))"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
v-if="otaInfo.needUpdate"
|
||||||
|
class="device-status-refresh"
|
||||||
|
src="../static/home-device/device-state-refresh.png"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
<view class="device-battery-value">
|
||||||
|
<image
|
||||||
|
class="device-battery-frame"
|
||||||
|
src="../static/home-device/device-state-battery.png"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
<text class="device-battery-text">{{ deviceBattery === null ? "--" : deviceBattery }}</text>
|
||||||
|
</view>
|
||||||
|
<image
|
||||||
|
class="device-status-icon device-status-icon--online"
|
||||||
|
src="../static/home-device/device-state-dot--online.png"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="device-action-badge"
|
||||||
|
:class="`device-action-badge--${deviceCardState}`"
|
||||||
|
@click.stop="$clickSound(() => toPage('/pages/device/my-device'))"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="device-action-background"
|
||||||
|
:src="deviceCardAsset.actionBackground"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-if="deviceCardState === 'unbound'"
|
||||||
|
class="device-action-bind-icon"
|
||||||
|
src="../static/home-device/device-action-bind-icon.png"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
:class="{
|
||||||
|
'device-action-text--bound': deviceCardState !== 'unbound'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ deviceCardState === "unbound" ? "绑定智能弓箭" : "我的设备" }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<image
|
<image
|
||||||
v-if="online"
|
class="device-tutorial"
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-08-07/dbvt1o6dvhr2rop3kn.webp"
|
|
||||||
mode="widthFix"
|
|
||||||
@click="() => toPage('/pages/my-device')"
|
|
||||||
/>
|
|
||||||
<image
|
|
||||||
v-else
|
|
||||||
src="https://static.shelingxingqiu.com/attachment/2026-01-04/dffohwtk1gwh0xfa6h.png"
|
|
||||||
mode="widthFix"
|
|
||||||
@click="$clickSound(() => toPage('/pages/my-device'))"
|
|
||||||
/>
|
|
||||||
<block v-if="user.id">
|
|
||||||
<text v-if="!device.deviceId">绑定我的智能弓</text>
|
|
||||||
<text v-else-if="!online">设备离线</text>
|
|
||||||
<text v-else-if="online">设备在线</text>
|
|
||||||
</block>
|
|
||||||
<image
|
|
||||||
src="https://static.shelingxingqiu.com/shootmini/static/first-try.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/first-try.png"
|
||||||
mode="widthFix"
|
mode="scaleToFill"
|
||||||
@click="() => toPage('/pages/first-try')"
|
@click="$clickSound(() => toPage('/pages/first-try'))"
|
||||||
/>
|
/>
|
||||||
<BubbleTip v-if="showGuide" :location="{ top: '60%', left: '47%' }">
|
<BubbleTip v-if="showGuide" :location="{ top: '60%', left: '47%' }">
|
||||||
<text>新人必刷!</text>
|
<text>新人必刷!</text>
|
||||||
@@ -599,7 +724,7 @@ onShareTimeline(() => {
|
|||||||
.feature-grid {
|
.feature-grid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-grid > view {
|
.feature-grid > view {
|
||||||
@@ -614,22 +739,182 @@ onShareTimeline(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-grid > view > image {
|
.device-visual {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 390rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #16161e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bow-card > text {
|
.device-visual-background,
|
||||||
|
.device-visual-bow {
|
||||||
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 66%;
|
pointer-events: none;
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #b3b3b3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bow-card > image:nth-child(3) {
|
.device-visual-background {
|
||||||
transform: translateY(-1px);
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-visual-bow {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 388rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-visual-bow--floating {
|
||||||
|
animation: device-bow-float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes device-bow-float {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0) rotate(-0.5deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10rpx) rotate(0.5deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-badge,
|
||||||
|
.device-action-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 10rpx;
|
||||||
|
right: 14rpx;
|
||||||
|
z-index: 2;
|
||||||
|
height: 34rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: Inter-Regular, sans-serif;
|
||||||
|
font-size: 18rpx;
|
||||||
|
line-height: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-badge--online {
|
||||||
|
top: 9rpx;
|
||||||
|
right: 10rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-badge--offline .device-status-icon {
|
||||||
|
margin-right: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-badge--online .device-status-icon--online {
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-icon {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-icon--online {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-refresh {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 三个状态项使用等大的方形槽位,电池框在槽位内垂直居中。 */
|
||||||
|
.device-battery-value {
|
||||||
|
position: relative;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-battery-frame {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 38rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文字容器只覆盖电池主体(60rpx),避开右侧凸起,使数字在框内水平垂直居中。 */
|
||||||
|
.device-battery-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 9rpx;
|
||||||
|
left: 0x;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
font-size: 14rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-action-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 28rpx;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
width: 218rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
justify-content: center;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-action-badge--offline,
|
||||||
|
.device-action-badge--online {
|
||||||
|
width: 166rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-action-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-action-badge > text,
|
||||||
|
.device-action-bind-icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-action-text--bound {
|
||||||
|
background: linear-gradient(90deg, #fcfeef 0%, #fdeaa1 100%);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-action-bind-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-tutorial {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 114rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-card {
|
.play-card {
|
||||||
@@ -642,8 +927,8 @@ onShareTimeline(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ranking-section {
|
.ranking-section {
|
||||||
border-radius: 15px;
|
border-radius: 30rpx;
|
||||||
padding: 15px;
|
padding: 30rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,23 +942,23 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
.into-btn {
|
.into-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 40px;
|
top: 80rpx;
|
||||||
left: calc(50% - 100px);
|
left: calc(50% - 200rpx);
|
||||||
width: 200px;
|
width: 400rpx;
|
||||||
height: 100px;
|
height: 200rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ranking-players {
|
.ranking-players {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 40rpx;
|
||||||
margin-top: 42%;
|
margin-top: 42%;
|
||||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
|
border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ranking-players > image:first-child {
|
.ranking-players > image:first-child {
|
||||||
width: 28%;
|
width: 28%;
|
||||||
transform: translateX(-10px) translateY(-8px);
|
transform: translateX(-20rpx) translateY(-16rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-avatars {
|
.player-avatars {
|
||||||
@@ -682,10 +967,10 @@ onShareTimeline(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.divide-line {
|
.divide-line {
|
||||||
width: 1px;
|
width: 2rpx;
|
||||||
height: 35px;
|
height: 70rpx;
|
||||||
background-color: #80808033;
|
background-color: #80808033;
|
||||||
margin-right: 8px;
|
margin-right: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-avatar,
|
.player-avatar,
|
||||||
@@ -712,10 +997,10 @@ onShareTimeline(() => {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #777777;
|
background: #777777;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 20rpx;
|
||||||
line-height: 18px;
|
line-height: 36rpx;
|
||||||
width: 18px;
|
width: 36rpx;
|
||||||
height: 18px;
|
height: 36rpx;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,20 +1012,20 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
.more-players {
|
.more-players {
|
||||||
background: #3c445a;
|
background: #3c445a;
|
||||||
font-size: 9px;
|
font-size: 18rpx;
|
||||||
line-height: 80rpx;
|
line-height: 80rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-players > text {
|
.more-players > text {
|
||||||
margin-left: 2px;
|
margin-left: 4rpx;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-data {
|
.my-data {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 20px;
|
margin-top: 40rpx;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,7 +1035,7 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
.my-data > view:first-child > image {
|
.my-data > view:first-child > image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transform: translateX(-8px);
|
transform: translateX(-16rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-data > view:nth-child(2) {
|
.my-data > view:nth-child(2) {
|
||||||
@@ -767,7 +1052,7 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
.my-data > view:nth-child(2) > view {
|
.my-data > view:nth-child(2) > view {
|
||||||
width: 28%;
|
width: 28%;
|
||||||
border-radius: 10px;
|
border-radius: 20rpx;
|
||||||
background: linear-gradient(180deg, #303b4c 30%, #2c384a 100%);
|
background: linear-gradient(180deg, #303b4c 30%, #2c384a 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -777,7 +1062,7 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
.my-data > view:nth-child(2) > view > text:last-child {
|
.my-data > view:nth-child(2) > view > text:last-child {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
line-height: 25px;
|
line-height: 50rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,7 +1072,7 @@ onShareTimeline(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
height: 120rpx;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,526 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
|
||||||
import Container from "@/components/Container.vue";
|
|
||||||
import ScreenHint from "@/components/ScreenHint.vue";
|
|
||||||
import SButton from "@/components/SButton.vue";
|
|
||||||
import {
|
|
||||||
bindDeviceAPI,
|
|
||||||
getMyDevicesAPI,
|
|
||||||
unbindDeviceAPI,
|
|
||||||
laserAimAPI, bindDeviceAPIV2,
|
|
||||||
} from "@/apis";
|
|
||||||
import useStore from "@/store";
|
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
const showTip = ref(false);
|
|
||||||
const confirmBindTip = ref(false);
|
|
||||||
const addDevice = ref();
|
|
||||||
const store = useStore();
|
|
||||||
const { updateDevice, clearDevice } = store;
|
|
||||||
const { user, device } = storeToRefs(store);
|
|
||||||
const justBind = ref(false);
|
|
||||||
const calibration = ref(false);
|
|
||||||
const token = ref(null);
|
|
||||||
const isSVip = computed(() => user.value.sVip === true);
|
|
||||||
const isVip = computed(() => user.value.vip === true && !isSVip.value);
|
|
||||||
|
|
||||||
// 扫描二维码方法
|
|
||||||
const handleScan = () => {
|
|
||||||
// 调用扫码API
|
|
||||||
uni.scanCode({
|
|
||||||
// 只支持扫码二维码
|
|
||||||
onlyFromCamera: true,
|
|
||||||
scanType: ["qrCode"],
|
|
||||||
success: async (res) => {
|
|
||||||
try {
|
|
||||||
// const base64Decode = (str) => {
|
|
||||||
// // 将 base64 转换为 utf8 字符串
|
|
||||||
// const bytes = wx.base64ToArrayBuffer(str);
|
|
||||||
// return String.fromCharCode.apply(null, new Uint8Array(bytes));
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// addDevice.value = JSON.parse(base64Decode(res.result));
|
|
||||||
token.value = res.result;
|
|
||||||
confirmBindTip.value = true;
|
|
||||||
} catch (err) {
|
|
||||||
uni.showToast({
|
|
||||||
title: "无效二维码",
|
|
||||||
icon: "none",
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error("扫码失败:", err);
|
|
||||||
uni.showToast({
|
|
||||||
title: "扫码失败",
|
|
||||||
icon: "error",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmBind = async () => {
|
|
||||||
if (!justBind.value && token.value) {
|
|
||||||
const result = await bindDeviceAPIV2(token.value);
|
|
||||||
confirmBindTip.value = false;
|
|
||||||
if (result.binded) {
|
|
||||||
return uni.showToast({
|
|
||||||
title: "设备已绑定其他账号,请解绑后再绑定",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
updateDevice(result.deviceId, result.name);
|
|
||||||
justBind.value = true;
|
|
||||||
uni.showToast({
|
|
||||||
title: "绑定成功",
|
|
||||||
icon: "success",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toFristTryPage = () => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: "/pages/first-try",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const unbindDevice = async () => {
|
|
||||||
try {
|
|
||||||
await unbindDeviceAPI(device.value.deviceId);
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.type === "DEVICE_BIND_INVALID") {
|
|
||||||
uni.setStorageSync("calibration", false);
|
|
||||||
clearDevice();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uni.setStorageSync("calibration", false);
|
|
||||||
uni.showToast({
|
|
||||||
title: "解绑成功",
|
|
||||||
icon: "success",
|
|
||||||
});
|
|
||||||
clearDevice();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 连接wifi跳转到wifi列表页面 */
|
|
||||||
const joinWifi = () => {
|
|
||||||
uni.navigateTo({ url: "/pages/ota-wifi" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const toDeviceIntroPage = () => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: "/pages/device-intro",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const backToHome = () => {
|
|
||||||
uni.navigateBack();
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyEmail = () => {
|
|
||||||
uni.setClipboardData({
|
|
||||||
data: "shelingxingqiu@163.com",
|
|
||||||
success: () => {
|
|
||||||
uni.showToast({
|
|
||||||
title: "邮箱已复制",
|
|
||||||
icon: "success",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const goCalibration = async () => {
|
|
||||||
await laserAimAPI();
|
|
||||||
uni.navigateTo({
|
|
||||||
url: "/pages/calibration",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncDeviceBinding = async () => {
|
|
||||||
if (!user.value.id) return;
|
|
||||||
try {
|
|
||||||
const devices = await getMyDevicesAPI();
|
|
||||||
if (devices.bindings && devices.bindings.length) {
|
|
||||||
updateDevice(devices.bindings[0].deviceId, devices.bindings[0].deviceName);
|
|
||||||
} else {
|
|
||||||
clearDevice();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("sync device binding error", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onShow(async () => {
|
|
||||||
calibration.value = uni.getStorageSync("calibration");
|
|
||||||
await syncDeviceBinding();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Container title="弓箭绑定">
|
|
||||||
<view v-if="!device.deviceId" class="scan-code">
|
|
||||||
<button hover-class="none" @click="$clickSound(handleScan)">
|
|
||||||
<image src="https://static.shelingxingqiu.com/shootmini/static/scan.png" mode="widthFix" />
|
|
||||||
</button>
|
|
||||||
<button hover-class="none" @click="showTip = true">
|
|
||||||
<text>扫</text>
|
|
||||||
<text :style="{ color: '#fed847' }">射灵弓箭</text>
|
|
||||||
<text>上的二维码</text>
|
|
||||||
<image src="../static/s-question-mark-white.png" mode="widthFix" />
|
|
||||||
</button>
|
|
||||||
<text>射灵智能弓箭,三模传感系统与独创靶环算法,</text>
|
|
||||||
<text>毫秒级在线实时对战,让你拥有全球约战的乐趣!</text>
|
|
||||||
<button hover-class="none" @click="toDeviceIntroPage">
|
|
||||||
<image src="https://static.shelingxingqiu.com/shootmini/static/have-no-device.png" mode="widthFix" />
|
|
||||||
</button>
|
|
||||||
<ScreenHint
|
|
||||||
mode="square"
|
|
||||||
:show="showTip"
|
|
||||||
:onClose="() => (showTip = false)"
|
|
||||||
>
|
|
||||||
<view class="scan-tips">
|
|
||||||
<text>扫码绑定设灵弓箭</text>
|
|
||||||
<image
|
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-08-05/dbuacrelri7jr3axiy.png"
|
|
||||||
mode="widthFix"
|
|
||||||
/>
|
|
||||||
<text>【注】已被绑定的弓箭无法再次绑定。</text>
|
|
||||||
<view>
|
|
||||||
<text>如有任何疑问,请随时联系:</text>
|
|
||||||
<button hover-class="none" @click="copyEmail">
|
|
||||||
shelingxingqiu@163.com
|
|
||||||
</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</ScreenHint>
|
|
||||||
<ScreenHint
|
|
||||||
:show="confirmBindTip"
|
|
||||||
:onClose="() => (confirmBindTip = false)"
|
|
||||||
>
|
|
||||||
<view class="confirm-bind">
|
|
||||||
<text
|
|
||||||
>智能弓箭和系统账号需一一对应,你确定要将<text
|
|
||||||
:style="{ color: '#fed847' }"
|
|
||||||
>当前登录用户账号</text
|
|
||||||
>绑定<text :style="{ color: '#fed847' }">这把弓箭</text>吗?
|
|
||||||
绑定后不可随意更换。</text
|
|
||||||
>
|
|
||||||
<view>
|
|
||||||
<view @click="confirmBind">确认绑定</view>
|
|
||||||
<view @click="() => (confirmBindTip = false)">取消</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</ScreenHint>
|
|
||||||
</view>
|
|
||||||
<view v-if="justBind" class="just-bind">
|
|
||||||
<view
|
|
||||||
class="device-binded"
|
|
||||||
:style="{ marginBottom: calibration ? '250rpx' : '100rpx' }"
|
|
||||||
>
|
|
||||||
<view>
|
|
||||||
<image src="https://static.shelingxingqiu.com/shootmini/static/device-icon.png" mode="widthFix" />
|
|
||||||
<text>{{ device.deviceName }}</text>
|
|
||||||
<view class="calibration" v-if="calibration">
|
|
||||||
<button hover-class="none" @click="goCalibration">
|
|
||||||
<text>重新校准</text>
|
|
||||||
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
|
|
||||||
</button>
|
|
||||||
<view>
|
|
||||||
<image src="../static/calibration-tip.png" mode="widthFix" />
|
|
||||||
<text>如有场地/距离变化,需重新校准以保证智能弓射箭精准度</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<image src="../static/bind-success.png" mode="widthFix" />
|
|
||||||
<view>
|
|
||||||
<image
|
|
||||||
:src="user.avatar || '../static/user-icon.png'"
|
|
||||||
mode="widthFix"
|
|
||||||
:style="{ borderRadius: '50%' }"
|
|
||||||
/>
|
|
||||||
<view
|
|
||||||
:class="[
|
|
||||||
'member-nickname',
|
|
||||||
isVip ? 'member-nickname--vip' : '',
|
|
||||||
isSVip ? 'member-nickname--svip' : '',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<text class="member-nickname__text">{{ user.nickName }}</text>
|
|
||||||
<text v-if="isSVip" class="member-nickname__shine">{{
|
|
||||||
user.nickName
|
|
||||||
}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<!-- <block v-if="calibration"> -->
|
|
||||||
<view>
|
|
||||||
<text>恭喜,你的弓箭和账号已成功绑定!</text>
|
|
||||||
<text :style="{ color: '#fed847' }">已赠送6个月射灵世界会员</text>
|
|
||||||
</view>
|
|
||||||
<!-- <SButton :onClick="goCalibration" width="60vw" :rounded="40">
|
|
||||||
开启智能弓进行校准
|
|
||||||
</SButton>
|
|
||||||
<text :style="{ marginTop: '20rpx', fontSize: '24rpx', color: '#fff9' }"
|
|
||||||
>校准时弓箭激光将开启,请勿直视激光</text
|
|
||||||
> -->
|
|
||||||
|
|
||||||
<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 v-if="device.deviceId && !justBind" class="has-device">
|
|
||||||
<view class="device-binded">
|
|
||||||
<view>
|
|
||||||
<image src="https://static.shelingxingqiu.com/shootmini/static/device-icon.png" mode="widthFix" />
|
|
||||||
<text>{{ device.deviceName }}</text>
|
|
||||||
<view class="calibration">
|
|
||||||
<button hover-class="none" @click="goCalibration">
|
|
||||||
<text>去校准</text>
|
|
||||||
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
|
|
||||||
</button>
|
|
||||||
<view>
|
|
||||||
<image src="../static/calibration-tip.png" mode="widthFix" />
|
|
||||||
<text
|
|
||||||
>首次绑定智能弓或场地/距离变化时,应进行校准以确保射箭精度</text
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<image src="../static/bind.png" mode="widthFix" />
|
|
||||||
<view>
|
|
||||||
<image
|
|
||||||
:src="user.avatar || '../static/user-icon.png'"
|
|
||||||
mode="widthFix"
|
|
||||||
:style="{ borderRadius: '50%' }"
|
|
||||||
/>
|
|
||||||
<view
|
|
||||||
:class="[
|
|
||||||
'member-nickname',
|
|
||||||
isVip ? 'member-nickname--vip' : '',
|
|
||||||
isSVip ? 'member-nickname--svip' : '',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<text class="member-nickname__text">{{ user.nickName }}</text>
|
|
||||||
<text v-if="isSVip" class="member-nickname__shine">{{
|
|
||||||
user.nickName
|
|
||||||
}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view :style="{ marginTop: '240rpx' }">
|
|
||||||
<SButton :onClick="() => $clickSound(unbindDevice)" width="80vw" :rounded="40"
|
|
||||||
>解绑</SButton
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
<view :style="{ marginTop: '20rpx' }">
|
|
||||||
<SButton :onClick="() => $clickSound(joinWifi)" width="80vw" :rounded="40"
|
|
||||||
>设备连接WIFI</SButton
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</Container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.scan-code,
|
|
||||||
.just-bind,
|
|
||||||
.has-device {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.scan-code {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
.scan-code > button:first-child {
|
|
||||||
margin-top: 22%;
|
|
||||||
}
|
|
||||||
.scan-code > button:first-child > image {
|
|
||||||
width: 300rpx;
|
|
||||||
}
|
|
||||||
.scan-code > button:nth-child(2) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #ffffff;
|
|
||||||
margin: 50rpx;
|
|
||||||
}
|
|
||||||
.scan-code > button:nth-child(2) > image {
|
|
||||||
width: 28rpx;
|
|
||||||
margin-left: 10rpx;
|
|
||||||
}
|
|
||||||
.scan-code > text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #fff9;
|
|
||||||
}
|
|
||||||
.scan-code > button:nth-child(5) {
|
|
||||||
margin-top: 25%;
|
|
||||||
}
|
|
||||||
.scan-code > button:nth-child(5) > image {
|
|
||||||
width: 380rpx;
|
|
||||||
}
|
|
||||||
.scan-tips {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: 14px;
|
|
||||||
width: 90%;
|
|
||||||
margin-top: 20%;
|
|
||||||
}
|
|
||||||
.scan-tips > text {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 24rpx;
|
|
||||||
}
|
|
||||||
.scan-tips > text:first-child {
|
|
||||||
color: #fed847;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 32rpx;
|
|
||||||
}
|
|
||||||
.scan-tips > view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.scan-tips > view:last-child {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
|
||||||
.scan-tips > view:last-child > button {
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #39a8ff;
|
|
||||||
}
|
|
||||||
.scan-tips > image {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.confirm-bind {
|
|
||||||
color: #fff9;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.confirm-bind > view:last-child {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.confirm-bind > view:last-child > view {
|
|
||||||
width: 48%;
|
|
||||||
border-radius: 20px;
|
|
||||||
background-color: #fed847;
|
|
||||||
color: #000;
|
|
||||||
line-height: 40px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.confirm-bind > view:last-child > view:nth-child(2) {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #fff3;
|
|
||||||
}
|
|
||||||
.device-binded {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-top: 200rpx;
|
|
||||||
}
|
|
||||||
.device-binded > view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
|
||||||
.device-binded > view > image {
|
|
||||||
width: 140rpx;
|
|
||||||
height: 140rpx;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.device-binded > view > text {
|
|
||||||
width: 120px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.device-binded .member-nickname {
|
|
||||||
justify-content: center;
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
.device-binded .member-nickname__text,
|
|
||||||
.device-binded .member-nickname__shine {
|
|
||||||
font-size: 26rpx;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.device-binded > image {
|
|
||||||
width: 100rpx;
|
|
||||||
margin: 0 20px;
|
|
||||||
}
|
|
||||||
.has-device,
|
|
||||||
.just-bind {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
.has-device > view:nth-child(2),
|
|
||||||
.just-bind > view:nth-child(2) {
|
|
||||||
color: #fff9;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 28rpx;
|
|
||||||
margin-bottom: 100rpx;
|
|
||||||
}
|
|
||||||
.has-device > view:nth-child(2) > text,
|
|
||||||
.just-bind > view:nth-child(2) > text {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
.calibration {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -145rpx;
|
|
||||||
left: 20rpx;
|
|
||||||
}
|
|
||||||
.calibration > button {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #287fff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-bottom: 15rpx;
|
|
||||||
padding-left: 50rpx;
|
|
||||||
}
|
|
||||||
.calibration > button > image {
|
|
||||||
width: 28rpx;
|
|
||||||
height: 28rpx;
|
|
||||||
}
|
|
||||||
.calibration > view {
|
|
||||||
position: relative;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #fff9;
|
|
||||||
padding-top: 34rpx;
|
|
||||||
padding-left: 35rpx;
|
|
||||||
width: 322rpx;
|
|
||||||
}
|
|
||||||
.calibration > view > image {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 370rpx;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,44 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import useStore from "@/store";
|
||||||
import { getDeviceBatteryAPI } from "@/apis";
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
const power = ref(0);
|
const store = useStore();
|
||||||
const timer = ref(null);
|
const { deviceBattery: power } = storeToRefs(store);
|
||||||
let disposed = false;
|
|
||||||
let requestInFlight = false;
|
|
||||||
|
|
||||||
const refreshPower = async () => {
|
|
||||||
if (disposed || requestInFlight) return;
|
|
||||||
requestInFlight = true;
|
|
||||||
try {
|
|
||||||
const data = await getDeviceBatteryAPI();
|
|
||||||
if (!disposed) power.value = data.battery;
|
|
||||||
} catch (_) {
|
|
||||||
// 电量轮询失败时等待下一轮,避免产生未处理的 Promise 拒绝。
|
|
||||||
} finally {
|
|
||||||
requestInFlight = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await refreshPower();
|
|
||||||
if (disposed) return;
|
|
||||||
timer.value = setInterval(() => {
|
|
||||||
void refreshPower();
|
|
||||||
}, 1000 * 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
disposed = true;
|
|
||||||
clearInterval(timer.value);
|
|
||||||
timer.value = null;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<image src="../../../static/b-power.png" mode="widthFix" />
|
<image src="../../../static/b-power.png" mode="widthFix" />
|
||||||
<view>电量{{ power || 1 }}%</view>
|
<view>{{ power === null ? "电量--" : `电量${power}%` }}</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ const cancelMatching = async () => {
|
|||||||
const goCalibration = async () => {
|
const goCalibration = async () => {
|
||||||
await laserAimAPI();
|
await laserAimAPI();
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/calibration",
|
url: "/pages/device/calibration",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ onBeforeUnmount(() => {
|
|||||||
</button>
|
</button>
|
||||||
<view class="warnning-text">
|
<view class="warnning-text">
|
||||||
<block v-if="statusText">
|
<block v-if="statusText">
|
||||||
<text v-if="distance > 0">当前距离{{ distance }}米</text>
|
|
||||||
<text>{{ statusText }}</text>
|
<text>{{ statusText }}</text>
|
||||||
</block>
|
</block>
|
||||||
<block v-else>
|
<block v-else>
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ onBeforeUnmount(() => {
|
|||||||
<view class="warnning-text">
|
<view class="warnning-text">
|
||||||
<view class="target-tip">当前靶纸为<text class="text-yellow">{{ targetType }}cm</text>全环靶</view>
|
<view class="target-tip">当前靶纸为<text class="text-yellow">{{ targetType }}cm</text>全环靶</view>
|
||||||
<block v-if="statusText">
|
<block v-if="statusText">
|
||||||
<text v-if="distance > 0">当前距离<text class="text-yellow">{{ distance }}</text>米</text>
|
|
||||||
<text>{{ statusText }}</text>
|
<text>{{ statusText }}</text>
|
||||||
</block>
|
</block>
|
||||||
<block v-else>
|
<block v-else>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const previewLines = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.difficulty-preview__copy {
|
.difficulty-preview__copy {
|
||||||
width: 80%;
|
width: 84%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: block;
|
display: block;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 433 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 433 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 813 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 245 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 944 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 850 B |
|
After Width: | Height: | Size: 479 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 371 B |
|
After Width: | Height: | Size: 294 B |
|
After Width: | Height: | Size: 687 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 413 B |
|
After Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 841 B |
|
After Width: | Height: | Size: 587 B |
@@ -17,6 +17,15 @@ const getDefaultDevice = () => ({
|
|||||||
deviceName: "",
|
deviceName: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getDefaultDeviceStatus = () => ({
|
||||||
|
battery: null,
|
||||||
|
version: "",
|
||||||
|
online: false,
|
||||||
|
netType: "",
|
||||||
|
onlineDuration: null,
|
||||||
|
receivedAt: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const getDefaultGame = () => ({
|
const getDefaultGame = () => ({
|
||||||
roomID: "",
|
roomID: "",
|
||||||
inBattle: false,
|
inBattle: false,
|
||||||
@@ -100,6 +109,10 @@ export default defineStore("store", {
|
|||||||
ringRank: [],
|
ringRank: [],
|
||||||
},
|
},
|
||||||
online: false,
|
online: false,
|
||||||
|
// 设备电量属于运行时状态,null 表示当前没有有效数据。
|
||||||
|
deviceBattery: null,
|
||||||
|
// WebSocket 实时推送的设备状态,不参与持久化。
|
||||||
|
deviceStatus: getDefaultDeviceStatus(),
|
||||||
game: {
|
game: {
|
||||||
roomID: "",
|
roomID: "",
|
||||||
inBattle: false,
|
inBattle: false,
|
||||||
@@ -134,7 +147,55 @@ export default defineStore("store", {
|
|||||||
this.rankData = { ...(data || {}) };
|
this.rankData = { ...(data || {}) };
|
||||||
},
|
},
|
||||||
updateOnline(online) {
|
updateOnline(online) {
|
||||||
|
this.setDeviceOnline(online);
|
||||||
|
},
|
||||||
|
updateDeviceBattery(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
this.deviceBattery = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const battery = Number(value);
|
||||||
|
this.deviceBattery = Number.isFinite(battery)
|
||||||
|
? Math.min(100, Math.max(0, battery))
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
updateDeviceStatus(status = {}) {
|
||||||
|
const battery = Number(status.battery ?? status.power);
|
||||||
|
const onlineDuration = Number(status.onlineDuration);
|
||||||
|
const online = status.online === true;
|
||||||
|
const nextStatus = {
|
||||||
|
battery: Number.isFinite(battery)
|
||||||
|
? Math.min(100, Math.max(0, battery))
|
||||||
|
: null,
|
||||||
|
version: String(status.version ?? "").trim(),
|
||||||
|
online,
|
||||||
|
netType: String(status.netType ?? "").trim().toLowerCase(),
|
||||||
|
onlineDuration:
|
||||||
|
Number.isFinite(onlineDuration) && onlineDuration >= 0
|
||||||
|
? onlineDuration
|
||||||
|
: null,
|
||||||
|
receivedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.deviceStatus = nextStatus;
|
||||||
this.online = online;
|
this.online = online;
|
||||||
|
this.deviceBattery = online ? nextStatus.battery : null;
|
||||||
|
},
|
||||||
|
setDeviceOnline(online) {
|
||||||
|
const nextOnline = online === true;
|
||||||
|
this.online = nextOnline;
|
||||||
|
if (nextOnline) {
|
||||||
|
this.deviceStatus = { ...this.deviceStatus, online: true };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deviceBattery = null;
|
||||||
|
this.deviceStatus = getDefaultDeviceStatus();
|
||||||
|
},
|
||||||
|
clearDeviceStatus() {
|
||||||
|
this.online = false;
|
||||||
|
this.deviceBattery = null;
|
||||||
|
this.deviceStatus = getDefaultDeviceStatus();
|
||||||
},
|
},
|
||||||
async updateUser(user = {}) {
|
async updateUser(user = {}) {
|
||||||
this.user = { ...getDefaultUser(), ...user };
|
this.user = { ...getDefaultUser(), ...user };
|
||||||
@@ -145,12 +206,15 @@ export default defineStore("store", {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
updateDevice(deviceId, deviceName) {
|
updateDevice(deviceId, deviceName) {
|
||||||
|
if (String(this.device.deviceId || "") !== String(deviceId || "")) {
|
||||||
|
this.clearDeviceStatus();
|
||||||
|
}
|
||||||
this.device.deviceId = deviceId;
|
this.device.deviceId = deviceId;
|
||||||
this.device.deviceName = deviceName;
|
this.device.deviceName = deviceName;
|
||||||
},
|
},
|
||||||
clearDevice() {
|
clearDevice() {
|
||||||
this.device = getDefaultDevice();
|
this.device = getDefaultDevice();
|
||||||
this.online = false;
|
this.clearDeviceStatus();
|
||||||
},
|
},
|
||||||
async updateConfig(config) {
|
async updateConfig(config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
@@ -199,6 +263,8 @@ export default defineStore("store", {
|
|||||||
user: getDefaultUser(),
|
user: getDefaultUser(),
|
||||||
device: getDefaultDevice(),
|
device: getDefaultDevice(),
|
||||||
online: false,
|
online: false,
|
||||||
|
deviceBattery: null,
|
||||||
|
deviceStatus: getDefaultDeviceStatus(),
|
||||||
game: getDefaultGame(),
|
game: getDefaultGame(),
|
||||||
dailyCount: getDefaultDailyCount(),
|
dailyCount: getDefaultDailyCount(),
|
||||||
deviceChargingDialogVisible: false,
|
deviceChargingDialogVisible: false,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const { Reader, Writer } = protobuf;
|
|||||||
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
|
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
|
||||||
// <match-schema-generated>
|
// <match-schema-generated>
|
||||||
// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。
|
// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。
|
||||||
// 来源:src/utils/match.min.js(sha256: bece92f31bde3072)
|
// 来源:src/utils/match.min.js(sha256: ec0371baeba9a9bf)
|
||||||
// 协议命名空间:rpc;消息数:12;字段数:163
|
// 协议命名空间:rpc;消息数:12;字段数:163
|
||||||
|
|
||||||
export const ServerMessageType = {
|
export const ServerMessageType = {
|
||||||
@@ -188,7 +188,7 @@ const SCHEMAS = {
|
|||||||
43: { name: "calories", kind: "double" },
|
43: { name: "calories", kind: "double" },
|
||||||
44: { name: "score_slot", kind: "int32" },
|
44: { name: "score_slot", kind: "int32" },
|
||||||
45: { name: "current_energy", kind: "int32" },
|
45: { name: "current_energy", kind: "int32" },
|
||||||
46: { name: "energy_cost_per_sec", kind: "int32" },
|
46: { name: "energy_cost_per_sec", kind: "float" },
|
||||||
47: { name: "energy_per_hit", kind: "int32" },
|
47: { name: "energy_per_hit", kind: "int32" },
|
||||||
48: { name: "energy_req_percent", kind: "int32" },
|
48: { name: "energy_req_percent", kind: "int32" },
|
||||||
49: { name: "delta_current_energy", kind: "int32" },
|
49: { name: "delta_current_energy", kind: "int32" },
|
||||||
|
|||||||
@@ -77,17 +77,33 @@ function createWebSocket(token, onMessage) {
|
|||||||
socketTask.onMessage((res) => {
|
socketTask.onMessage((res) => {
|
||||||
if (socket !== socketTask) return;
|
if (socket !== socketTask) return;
|
||||||
|
|
||||||
const { data, event } = JSON.parse(res.data);
|
let response;
|
||||||
|
try {
|
||||||
|
response = JSON.parse(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("WebSocket 消息解析失败", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, event, code, timestamp } = response || {};
|
||||||
if (event === "pong") return;
|
if (event === "pong") return;
|
||||||
if (data.type) {
|
if (event === "/addons/shoot/battery") {
|
||||||
|
if ((code == null || Number(code) === 0) && onMessage && data && typeof data === "object") {
|
||||||
|
onMessage({ event, data, code, timestamp });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data?.type) {
|
||||||
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
||||||
console.log("收到 WebSocket 消息", getMessageTypeName(data.type));
|
console.log("收到 WebSocket 消息", getMessageTypeName(data.type));
|
||||||
}
|
}
|
||||||
if (onMessage) onMessage({ ...(data.data || {}), type: data.type });
|
if (onMessage) onMessage({ ...(data.data || {}), type: data.type });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (onMessage && data.updates) onMessage(data.updates);
|
const updates = Array.isArray(data?.updates) ? data.updates : [];
|
||||||
const msg = data.updates[0];
|
if (!updates.length) return;
|
||||||
|
if (onMessage) onMessage(updates);
|
||||||
|
const msg = updates[0];
|
||||||
if (msg) {
|
if (msg) {
|
||||||
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -101,9 +117,9 @@ function createWebSocket(token, onMessage) {
|
|||||||
} else if (msg.constructor === MESSAGETYPES.LvlUpdate) {
|
} else if (msg.constructor === MESSAGETYPES.LvlUpdate) {
|
||||||
uni.setStorageSync("latestLvl", msg.lvl);
|
uni.setStorageSync("latestLvl", msg.lvl);
|
||||||
} else if (msg.constructor === MESSAGETYPES.DeviceOnline) {
|
} else if (msg.constructor === MESSAGETYPES.DeviceOnline) {
|
||||||
uni.$emit("update-online");
|
uni.$emit("update-online", true);
|
||||||
} else if (msg.constructor === MESSAGETYPES.DeviceOffline) {
|
} else if (msg.constructor === MESSAGETYPES.DeviceOffline) {
|
||||||
uni.$emit("update-online");
|
uni.$emit("update-online", false);
|
||||||
} else if (msg.constructor === MESSAGETYPES.DeviceCharging) {
|
} else if (msg.constructor === MESSAGETYPES.DeviceCharging) {
|
||||||
uni.$emit("device-charging");
|
uni.$emit("device-charging");
|
||||||
}
|
}
|
||||||
@@ -121,6 +137,7 @@ function createWebSocket(token, onMessage) {
|
|||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
socket = null;
|
socket = null;
|
||||||
isConnecting = false;
|
isConnecting = false;
|
||||||
|
uni.$emit("shoot-socket-disconnected");
|
||||||
|
|
||||||
if (manualClose || kickedOut) return;
|
if (manualClose || kickedOut) return;
|
||||||
await handleUnexpectedClose(onMessage);
|
await handleUnexpectedClose(onMessage);
|
||||||
|
|||||||