Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46cbd37102 | ||
|
|
d4d690cb8e | ||
|
|
b51813dd33 | ||
|
|
a11e7f7532 | ||
|
|
03fa2f4d48 |
@@ -28,6 +28,7 @@
|
||||
const {
|
||||
updateUser,
|
||||
updateOnline,
|
||||
updateDeviceBattery,
|
||||
showDeviceChargingDialog,
|
||||
clearSessionState,
|
||||
clearDevice
|
||||
@@ -74,6 +75,7 @@
|
||||
const wasOnline = Boolean(online.value);
|
||||
const nextOnline = Boolean(data.online);
|
||||
updateOnline(nextOnline);
|
||||
updateDeviceBattery(nextOnline ? data?.battery ?? data?.power : null);
|
||||
if (!device.value.deviceId || wasOnline === nextOnline) return;
|
||||
audioManager.play(nextOnline ? "设备已连接" : "设备连接已断开");
|
||||
}
|
||||
|
||||
@@ -281,6 +281,10 @@ export const getMyDevicesAPI = () => {
|
||||
return request("GET", "/user/device/getBindings");
|
||||
};
|
||||
|
||||
export const getDeviceDetailAPI = (deviceId) => {
|
||||
return request("GET", `/user/device/getDetail?deviceId=${encodeURIComponent(deviceId)}`);
|
||||
};
|
||||
|
||||
export const createPractiseAPI = (arrows, time, target) => {
|
||||
return request("POST", "/user/practice/create", {
|
||||
shootNumber: arrows,
|
||||
|
||||
@@ -2,7 +2,7 @@ export const AUDIO_INTERRUPTION_BEGIN_EVENT = "audio-interruption-begin";
|
||||
export const AUDIO_INTERRUPTION_END_EVENT = "audio-interruption-end";
|
||||
export const RHYTHM_SHOOT_WINDOW_AUDIO_KEY = "请射箭";
|
||||
export const STABILITY_START_AUDIO_KEY =
|
||||
"请在计时结束前能量条达到百分百";
|
||||
"请在计时结束时能量条达到标记区域";
|
||||
export const STABILITY_ENERGY_50_AUDIO_KEY = "加油!到达50%啦";
|
||||
export const STABILITY_ENERGY_70_AUDIO_KEY =
|
||||
"70%啦!还差一点,胜利就在眼前";
|
||||
@@ -12,7 +12,7 @@ const TRAINING_START_AUDIO_KEY_MAP = Object.freeze({
|
||||
endurance: "请于计时前完成累计环数和箭量",
|
||||
precision: "请射箭命中高亮区域",
|
||||
rhythm: "请在读条推进到标记区间内射箭",
|
||||
stability: "请在计时结束前能量条达到百分百",
|
||||
stability: STABILITY_START_AUDIO_KEY,
|
||||
});
|
||||
|
||||
export const getTrainingStartAudioKey = (trainingType, fallback = "") =>
|
||||
@@ -176,7 +176,7 @@ export const audioFils = {
|
||||
练习开始:
|
||||
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3",
|
||||
[STABILITY_START_AUDIO_KEY]:
|
||||
"https://static.shelingxingqiu.com/shootaudio/%E8%AF%B7%E5%9C%A8%E8%AE%A1%E6%97%B6%E7%BB%93%E6%9D%9F%E5%89%8D%E8%83%BD%E9%87%8F%E6%9D%A1%E8%BE%BE%E5%88%B0%E7%99%BE%E5%88%86%E7%99%BE.MP3",
|
||||
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E8%AF%B7%E5%9C%A8%E8%AE%A1%E6%97%B6%E7%BB%93%E6%9D%9F%E6%97%B6%E8%83%BD%E9%87%8F%E6%9D%A1%E8%BE%BE%E5%88%B0%E6%A0%87%E8%AE%B0%E5%8C%BA%E5%9F%9F.MP3",
|
||||
[STABILITY_ENERGY_50_AUDIO_KEY]:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E5%8A%A0%E6%B2%B9%EF%BC%81%E5%88%B0%E8%BE%BE50%25%E5%95%A6.MP3",
|
||||
[STABILITY_ENERGY_70_AUDIO_KEY]:
|
||||
|
||||
@@ -81,6 +81,13 @@ const props = defineProps({
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/app-bg9.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<!-- 我的设备未绑定/绑定成功页面背景 -->
|
||||
<image
|
||||
class="bg-image"
|
||||
v-if="type === 12"
|
||||
src="../static/device-assets/my-device-unbound-background.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
class="bg-image"
|
||||
v-if="type === 10"
|
||||
|
||||
@@ -18,7 +18,7 @@ function handleTabClick(index) {
|
||||
}
|
||||
if (index === 2) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/device-intro",
|
||||
url: "/pages/device/device-intro",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ const cancelMatching = async () => {
|
||||
const goCalibration = async () => {
|
||||
await laserAimAPI();
|
||||
uni.navigateTo({
|
||||
url: "/pages/calibration",
|
||||
url: "/pages/device/calibration",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
{
|
||||
"path": "pages/audio-test"
|
||||
},
|
||||
{
|
||||
"path": "pages/calibration"
|
||||
},
|
||||
{
|
||||
"path": "pages/about-us"
|
||||
},
|
||||
@@ -60,12 +57,6 @@
|
||||
{
|
||||
"path": "pages/match-page"
|
||||
},
|
||||
{
|
||||
"path": "pages/my-device"
|
||||
},
|
||||
{
|
||||
"path": "pages/device-intro"
|
||||
},
|
||||
{
|
||||
"path": "pages/user"
|
||||
},
|
||||
@@ -105,16 +96,10 @@
|
||||
{
|
||||
"path": "pages/melee-bow-data"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine-bow-data"
|
||||
},
|
||||
{
|
||||
"path": "pages/ota-wifi",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
"path": "pages/mine-bow-data"
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"backgroundColor": "@bgColor",
|
||||
"backgroundColorBottom": "@bgColorBottom",
|
||||
@@ -159,6 +144,27 @@
|
||||
{
|
||||
"root": "pages/device",
|
||||
"pages": [
|
||||
{
|
||||
"path": "my-device"
|
||||
},
|
||||
{
|
||||
"path": "device-bind-success"
|
||||
},
|
||||
{
|
||||
"path": "device-bind-failure"
|
||||
},
|
||||
{
|
||||
"path": "device-intro"
|
||||
},
|
||||
{
|
||||
"path": "ota-wifi",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "calibration"
|
||||
},
|
||||
{
|
||||
"path": "unbind-device"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { bindDeviceAPIV2 } from "@/apis";
|
||||
|
||||
export function useDeviceBinding({
|
||||
token,
|
||||
confirmBindTip,
|
||||
binding,
|
||||
updateDevice,
|
||||
deviceDetails,
|
||||
refreshDeviceStatus,
|
||||
}) {
|
||||
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 || {};
|
||||
void refreshDeviceStatus();
|
||||
};
|
||||
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,129 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { getDeviceBatteryAPI, getMyDevicesAPI, unbindDeviceAPI } from "@/apis";
|
||||
|
||||
export const DEVICE_NAME_STORAGE_KEY = "device_name_overrides";
|
||||
|
||||
export function useDeviceStatus({
|
||||
user,
|
||||
device,
|
||||
online,
|
||||
updateDevice,
|
||||
updateOnline,
|
||||
clearDevice,
|
||||
unbindDialogVisible,
|
||||
}) {
|
||||
const deviceStatus = ref({});
|
||||
const deviceDetails = ref({});
|
||||
|
||||
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 value = Number(
|
||||
deviceStatus.value.battery ?? deviceStatus.value.power ?? 0
|
||||
);
|
||||
return Number.isFinite(value) && value > 0 ? Math.min(100, value) : 0;
|
||||
});
|
||||
const batteryText = computed(() =>
|
||||
battery.value ? `${battery.value}%` : "暂无数据"
|
||||
);
|
||||
const networkText = computed(() => {
|
||||
const netType = String(deviceStatus.value.netType || "").toLowerCase();
|
||||
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 refreshDeviceStatus = async () => {
|
||||
if (!device.value.deviceId) return;
|
||||
try {
|
||||
const result = await getDeviceBatteryAPI();
|
||||
deviceStatus.value = result || {};
|
||||
updateOnline(result?.online === true);
|
||||
} catch (error) {
|
||||
deviceStatus.value = {};
|
||||
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();
|
||||
deviceDetails.value = currentDevice;
|
||||
updateDevice(
|
||||
currentDevice.deviceId,
|
||||
nameOverrides[currentDevice.deviceId] ||
|
||||
currentDevice.deviceName ||
|
||||
currentDevice.name ||
|
||||
"我的智能弓"
|
||||
);
|
||||
await refreshDeviceStatus();
|
||||
return;
|
||||
}
|
||||
clearDevice();
|
||||
deviceStatus.value = {};
|
||||
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();
|
||||
deviceStatus.value = {};
|
||||
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,
|
||||
refreshDeviceStatus,
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
<text
|
||||
:style="{ opacity: addBg ? 1 : 0, color: '#fff', fontWeight: 'bold' }"
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -650,16 +650,16 @@ onUnmounted(() => {
|
||||
</view>
|
||||
<view v-if="currentState === 'CONNECTED'" class="wifi-list-card connected-wifi-card">
|
||||
<view class="wifi-item connected-wifi-item">
|
||||
<image class="check-icon" src="../static/sicon/check.png" mode="aspectFit" />
|
||||
<image class="check-icon" src="../../static/sicon/check.png" mode="aspectFit" />
|
||||
<text class="wifi-ssid connected-wifi-ssid">{{ connectedWifi?.SSID }}</text>
|
||||
<view class="wifi-icons connected-wifi-icons">
|
||||
<image
|
||||
v-if="connectedWifi?.secure"
|
||||
class="security-icon"
|
||||
src="../static/sicon/pwd.png"
|
||||
src="../../static/sicon/pwd.png"
|
||||
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>
|
||||
@@ -668,7 +668,7 @@ onUnmounted(() => {
|
||||
<view class="section-label-row">
|
||||
<text class="section-label">网络</text>
|
||||
<image
|
||||
src="../static/sicon/refresh.png"
|
||||
src="../../static/sicon/refresh.png"
|
||||
mode="aspectFit"
|
||||
:style="{ width: '34rpx', height: '34rpx', marginLeft: '8rpx', opacity: isRefreshing ? 0.3 : 0.7 }"
|
||||
@click="startScanning"
|
||||
@@ -699,10 +699,10 @@ onUnmounted(() => {
|
||||
<image
|
||||
v-if="wifi.secure"
|
||||
class="security-icon"
|
||||
src="../static/sicon/pwd.png"
|
||||
src="../../static/sicon/pwd.png"
|
||||
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 class="wifi-item" @click="selectOther">
|
||||
@@ -763,7 +763,7 @@ onUnmounted(() => {
|
||||
<block v-if="connectMode === 'secure'">
|
||||
<view class="sheet-header">
|
||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||
<image src="../../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||
</view>
|
||||
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
||||
<view
|
||||
@@ -771,7 +771,7 @@ onUnmounted(() => {
|
||||
:class="{ 'nav-disabled': joinDisabled }"
|
||||
@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 class="input-row-card">
|
||||
@@ -789,7 +789,7 @@ onUnmounted(() => {
|
||||
<!-- 密码显示/隐藏切换按钮 -->
|
||||
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
||||
<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"
|
||||
style="width: 40rpx; height: 40rpx;"
|
||||
/>
|
||||
@@ -803,11 +803,11 @@ onUnmounted(() => {
|
||||
<block v-else-if="connectMode === 'open'">
|
||||
<view class="sheet-header">
|
||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||
<image src="../../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||
</view>
|
||||
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
||||
<view class="sheet-nav-btn" @click="joinNetwork">
|
||||
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||||
<image src="../../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
|
||||
@@ -819,7 +819,7 @@ onUnmounted(() => {
|
||||
<block v-else-if="connectMode === 'manual'">
|
||||
<view class="sheet-header">
|
||||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||||
<image src="../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||
<image src="../../static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||||
</view>
|
||||
<text class="sheet-title">加入无线网络</text>
|
||||
<view
|
||||
@@ -827,7 +827,7 @@ onUnmounted(() => {
|
||||
:class="{ 'nav-disabled': joinDisabled }"
|
||||
@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 class="input-row-card">
|
||||
@@ -854,7 +854,7 @@ onUnmounted(() => {
|
||||
<!-- 密码显示/隐藏切换按钮 -->
|
||||
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
||||
<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"
|
||||
style="width: 40rpx; height: 40rpx;"
|
||||
/>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
||||
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import {onHide, onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import AppFooter from "@/components/AppFooter.vue";
|
||||
import UserHeader from "@/components/UserHeader.vue";
|
||||
@@ -34,12 +34,123 @@ const {
|
||||
getLvlName,
|
||||
getLvlNameByScore,
|
||||
updateOnline,
|
||||
updateDeviceBattery,
|
||||
} = store;
|
||||
const {user, device, online, game} = storeToRefs(store);
|
||||
const {user, device, online, deviceBattery, game} = storeToRefs(store);
|
||||
|
||||
const showModal = ref(false);
|
||||
const showGuide = ref(false);
|
||||
const scoreRankList = ref([]);
|
||||
const HOME_DEVICE_STATUS_POLL_INTERVAL = 10000;
|
||||
let isHomePageVisible = false;
|
||||
let homeDeviceStatusTimer = null;
|
||||
let isHomeDeviceStatusRequesting = false;
|
||||
|
||||
// 设备状态接口可能返回布尔值、数字或字符串,统一成 true / false / null。
|
||||
// null 表示接口没有给出可判断的状态,调用方应保留已有状态。
|
||||
const normalizeDeviceOnline = (value) => {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value === 1;
|
||||
|
||||
const normalized = String(value ?? "").trim().toLowerCase();
|
||||
if (["true", "1", "online", "connected"].includes(normalized)) return true;
|
||||
if (["false", "0", "offline", "disconnected"].includes(normalized)) return false;
|
||||
return null;
|
||||
};
|
||||
|
||||
// 将设备状态接口响应统一同步到首页 Store,离线时清空无效电量。
|
||||
const applyHomeDeviceStatus = (data) => {
|
||||
const normalizedOnline = normalizeDeviceOnline(data?.online);
|
||||
if (normalizedOnline !== null) {
|
||||
updateOnline(normalizedOnline);
|
||||
}
|
||||
if (normalizedOnline === false) {
|
||||
updateDeviceBattery(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const battery = data?.battery ?? data?.power;
|
||||
if (battery !== undefined && battery !== null) {
|
||||
updateDeviceBattery(battery);
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新首页设备在线状态和电量;上一轮未结束时跳过,避免请求堆积。
|
||||
const refreshHomeDeviceStatus = async () => {
|
||||
if (
|
||||
!isHomePageVisible ||
|
||||
!user.value?.id ||
|
||||
!device.value?.deviceId ||
|
||||
isHomeDeviceStatusRequesting
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
isHomeDeviceStatusRequesting = true;
|
||||
try {
|
||||
const data = await getDeviceBatteryAPI();
|
||||
if (isHomePageVisible) {
|
||||
applyHomeDeviceStatus(data);
|
||||
}
|
||||
} catch (error) {
|
||||
// 请求失败时保留上一次有效状态,等待下一轮自动恢复。
|
||||
console.log("刷新首页设备状态失败", error);
|
||||
} finally {
|
||||
isHomeDeviceStatusRequesting = false;
|
||||
}
|
||||
};
|
||||
|
||||
const stopHomeDeviceStatusPolling = () => {
|
||||
clearInterval(homeDeviceStatusTimer);
|
||||
homeDeviceStatusTimer = null;
|
||||
};
|
||||
|
||||
// 首页可见且用户已绑定设备时,每 10 秒刷新一次设备状态。
|
||||
const startHomeDeviceStatusPolling = () => {
|
||||
stopHomeDeviceStatusPolling();
|
||||
if (!isHomePageVisible || !user.value?.id || !device.value?.deviceId) return;
|
||||
|
||||
homeDeviceStatusTimer = setInterval(() => {
|
||||
void refreshHomeDeviceStatus();
|
||||
}, HOME_DEVICE_STATUS_POLL_INTERVAL);
|
||||
};
|
||||
|
||||
// 首页停留期间登录或绑定状态变化时,及时启停设备状态轮询。
|
||||
watch(
|
||||
[() => user.value?.id, () => device.value?.deviceId],
|
||||
() => {
|
||||
if (isHomePageVisible) {
|
||||
startHomeDeviceStatusPolling();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 首页设备卡片按“未绑定 / 已绑定未连接 / 已绑定已连接”三态展示。
|
||||
const deviceCardState = computed(() => {
|
||||
// 未登录时始终展示绑定入口,避免本地残留设备状态误显示为已绑定。
|
||||
if (!user.value?.id || !device.value?.deviceId) return "unbound";
|
||||
return normalizeDeviceOnline(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 相关
|
||||
const otaVisible = ref(false);
|
||||
@@ -50,6 +161,7 @@ const otaInfo = ref({
|
||||
versionInfo: "",
|
||||
resourceUrl: "",
|
||||
forceUpdate: false,
|
||||
needUpdate: false,
|
||||
});
|
||||
const isStartingOta = ref(false);
|
||||
let isCheckingOta = false;
|
||||
@@ -107,6 +219,8 @@ const applyOtaVersionInfo = (versionInfo) => {
|
||||
versionInfo: versionInfo?.versionInfo || "",
|
||||
resourceUrl: versionInfo?.resourceUrl || "",
|
||||
forceUpdate: Number(versionInfo?.forceUpdate) === 1,
|
||||
needUpdate:
|
||||
versionInfo?.needUpdate === true || Number(versionInfo?.needUpdate) === 1,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -122,7 +236,7 @@ const checkOtaUpdate = async () => {
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
if (deviceStatus?.online !== true || otaVisible.value) return;
|
||||
if (normalizeDeviceOnline(deviceStatus?.online) !== true || otaVisible.value) return;
|
||||
|
||||
let versionInfo;
|
||||
try {
|
||||
@@ -130,8 +244,9 @@ const checkOtaUpdate = async () => {
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
if (!versionInfo?.needUpdate || otaVisible.value) return;
|
||||
if (otaVisible.value) return;
|
||||
applyOtaVersionInfo(versionInfo);
|
||||
if (!otaInfo.value.needUpdate) return;
|
||||
|
||||
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
||||
const now = Date.now();
|
||||
@@ -165,7 +280,7 @@ const getOtaWifiUrl = () => {
|
||||
`versionNumber=${encodeURIComponent(versionNumber)}`,
|
||||
`resourceUrl=${encodeURIComponent(resourceUrl)}`,
|
||||
].join("&");
|
||||
return `/pages/ota-wifi?${query}`;
|
||||
return `/pages/device/ota-wifi?${query}`;
|
||||
};
|
||||
|
||||
// 处理 OTA 弹窗暂不更新,强制更新时不允许关闭。
|
||||
@@ -189,6 +304,7 @@ const completeHomeOtaUpdate = (runId) => {
|
||||
if (!isOtaUpdateRunActive(runId)) return;
|
||||
invalidateOtaUpdateRun();
|
||||
isStartingOta.value = false;
|
||||
otaInfo.value = {...otaInfo.value, needUpdate: false};
|
||||
otaProgress.value = 100;
|
||||
const completedRunId = otaUpdateRunId;
|
||||
otaResultTimer = setTimeout(() => {
|
||||
@@ -284,7 +400,7 @@ const handleOtaUpdate = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (deviceStatus?.online !== true) {
|
||||
if (normalizeDeviceOnline(deviceStatus?.online) !== true) {
|
||||
isStartingOta.value = false;
|
||||
uni.showToast({
|
||||
title: "请先开启智能弓",
|
||||
@@ -336,13 +452,72 @@ 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 previousDeviceId = String(device.value?.deviceId || "");
|
||||
const deviceId = String(currentDevice.deviceId || "");
|
||||
updateDevice(
|
||||
deviceId,
|
||||
currentDevice.deviceName || currentDevice.name || ""
|
||||
);
|
||||
|
||||
// 切换到新设备时先以离线态初始化,避免沿用上一台设备的在线状态。
|
||||
if (previousDeviceId !== deviceId) {
|
||||
updateOnline(false);
|
||||
updateDeviceBattery(null);
|
||||
}
|
||||
|
||||
await refreshHomeDeviceStatus();
|
||||
};
|
||||
|
||||
onShow(async (options) => {
|
||||
isHomePageVisible = true;
|
||||
startHomeDeviceStatusPolling();
|
||||
|
||||
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
||||
const token = uni.getStorageSync(`${env}_token`);
|
||||
|
||||
// 检查是否从 OTA 更新页面返回
|
||||
if (options && options.updateResult) {
|
||||
otaState.value = options.updateResult;
|
||||
if (options.updateResult === "update_success") {
|
||||
otaInfo.value = {...otaInfo.value, needUpdate: false};
|
||||
}
|
||||
otaVisible.value = true;
|
||||
} else if (token || user.value.id) {
|
||||
await checkOtaUpdate();
|
||||
@@ -400,19 +575,17 @@ onShow(async (options) => {
|
||||
showGuide.value = false;
|
||||
}, 3000);
|
||||
}
|
||||
const devices = await getMyDevicesAPI();
|
||||
if (devices.bindings && devices.bindings.length) {
|
||||
updateDevice(
|
||||
devices.bindings[0].deviceId,
|
||||
devices.bindings[0].deviceName
|
||||
);
|
||||
const data = await getDeviceBatteryAPI();
|
||||
updateOnline(data.online);
|
||||
} else {
|
||||
clearDevice();
|
||||
}
|
||||
await syncHomeDevice();
|
||||
}
|
||||
}
|
||||
|
||||
// 登录态或绑定设备可能在本次 onShow 中发生变化,按最新状态重建轮询。
|
||||
startHomeDeviceStatusPolling();
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
isHomePageVisible = false;
|
||||
stopHomeDeviceStatusPolling();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -422,6 +595,8 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
isHomePageVisible = false;
|
||||
stopHomeDeviceStatusPolling();
|
||||
invalidateOtaUpdateRun();
|
||||
});
|
||||
|
||||
@@ -468,30 +643,90 @@ onShareTimeline(() => {
|
||||
/> -->
|
||||
</view>
|
||||
<UserHeader showRank :onSignin="() => (showModal = true)"/>
|
||||
<view :style="{ padding: '12px 10px' }">
|
||||
<view class="feature-grid">
|
||||
<view :style="{ padding: '24rpx 20rpx' }">
|
||||
<view class="feature-grid">
|
||||
<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"
|
||||
: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
|
||||
v-if="online"
|
||||
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
|
||||
class="device-tutorial"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/first-try.png"
|
||||
mode="widthFix"
|
||||
@click="() => toPage('/pages/first-try')"
|
||||
mode="scaleToFill"
|
||||
@click="$clickSound(() => toPage('/pages/first-try'))"
|
||||
/>
|
||||
<BubbleTip v-if="showGuide" :location="{ top: '60%', left: '47%' }">
|
||||
<text>新人必刷!</text>
|
||||
@@ -599,7 +834,7 @@ onShareTimeline(() => {
|
||||
.feature-grid {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.feature-grid > view {
|
||||
@@ -614,22 +849,168 @@ onShareTimeline(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.feature-grid > view > image {
|
||||
.device-visual {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 390rpx;
|
||||
overflow: hidden;
|
||||
background: #16161e;
|
||||
}
|
||||
|
||||
.bow-card > text {
|
||||
.device-visual-background,
|
||||
.device-visual-bow {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 66%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
color: #b3b3b3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bow-card > image:nth-child(3) {
|
||||
transform: translateY(-1px);
|
||||
.device-visual-background {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.device-visual-bow {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 388rpx;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -642,8 +1023,8 @@ onShareTimeline(() => {
|
||||
}
|
||||
|
||||
.ranking-section {
|
||||
border-radius: 15px;
|
||||
padding: 15px;
|
||||
border-radius: 30rpx;
|
||||
padding: 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -657,23 +1038,23 @@ onShareTimeline(() => {
|
||||
|
||||
.into-btn {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: calc(50% - 100px);
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
top: 80rpx;
|
||||
left: calc(50% - 200rpx);
|
||||
width: 400rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.ranking-players {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 40rpx;
|
||||
margin-top: 42%;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.ranking-players > image:first-child {
|
||||
width: 28%;
|
||||
transform: translateX(-10px) translateY(-8px);
|
||||
transform: translateX(-20rpx) translateY(-16rpx);
|
||||
}
|
||||
|
||||
.player-avatars {
|
||||
@@ -682,10 +1063,10 @@ onShareTimeline(() => {
|
||||
}
|
||||
|
||||
.divide-line {
|
||||
width: 1px;
|
||||
height: 35px;
|
||||
width: 2rpx;
|
||||
height: 70rpx;
|
||||
background-color: #80808033;
|
||||
margin-right: 8px;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.player-avatar,
|
||||
@@ -712,10 +1093,10 @@ onShareTimeline(() => {
|
||||
border-radius: 50%;
|
||||
background: #777777;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
line-height: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 20rpx;
|
||||
line-height: 36rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -727,20 +1108,20 @@ onShareTimeline(() => {
|
||||
|
||||
.more-players {
|
||||
background: #3c445a;
|
||||
font-size: 9px;
|
||||
font-size: 18rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.more-players > text {
|
||||
margin-left: 2px;
|
||||
margin-left: 4rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.my-data {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
margin-top: 40rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -750,7 +1131,7 @@ onShareTimeline(() => {
|
||||
|
||||
.my-data > view:first-child > image {
|
||||
width: 100%;
|
||||
transform: translateX(-8px);
|
||||
transform: translateX(-16rpx);
|
||||
}
|
||||
|
||||
.my-data > view:nth-child(2) {
|
||||
@@ -767,7 +1148,7 @@ onShareTimeline(() => {
|
||||
|
||||
.my-data > view:nth-child(2) > view {
|
||||
width: 28%;
|
||||
border-radius: 10px;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(180deg, #303b4c 30%, #2c384a 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -777,7 +1158,7 @@ onShareTimeline(() => {
|
||||
|
||||
.my-data > view:nth-child(2) > view > text:last-child {
|
||||
color: #fff;
|
||||
line-height: 25px;
|
||||
line-height: 50rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -787,7 +1168,7 @@ onShareTimeline(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
height: 120rpx;
|
||||
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>
|
||||
@@ -111,7 +111,7 @@ const cancelMatching = async () => {
|
||||
const goCalibration = async () => {
|
||||
await laserAimAPI();
|
||||
uni.navigateTo({
|
||||
url: "/pages/calibration",
|
||||
url: "/pages/device/calibration",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -89,7 +89,7 @@ const previewLines = computed(() => {
|
||||
}
|
||||
|
||||
.difficulty-preview__copy {
|
||||
width: 80%;
|
||||
width: 84%;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
color: #ffffff;
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 519 B |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 630 KiB |
|
After Width: | Height: | Size: 519 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 7.1 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: 24 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: 1.4 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 875 B |
|
After Width: | Height: | Size: 687 B |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 681 B |
|
After Width: | Height: | Size: 895 B |
|
After Width: | Height: | Size: 972 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -100,6 +100,8 @@ export default defineStore("store", {
|
||||
ringRank: [],
|
||||
},
|
||||
online: false,
|
||||
// 设备电量属于运行时状态,null 表示当前没有有效数据。
|
||||
deviceBattery: null,
|
||||
game: {
|
||||
roomID: "",
|
||||
inBattle: false,
|
||||
@@ -136,6 +138,17 @@ export default defineStore("store", {
|
||||
updateOnline(online) {
|
||||
this.online = 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;
|
||||
},
|
||||
async updateUser(user = {}) {
|
||||
this.user = { ...getDefaultUser(), ...user };
|
||||
this.user.lvlName = getLvlNameByScore(this.user.scores, this.config.randInfos)
|
||||
@@ -151,6 +164,7 @@ export default defineStore("store", {
|
||||
clearDevice() {
|
||||
this.device = getDefaultDevice();
|
||||
this.online = false;
|
||||
this.deviceBattery = null;
|
||||
},
|
||||
async updateConfig(config) {
|
||||
this.config = config;
|
||||
@@ -199,6 +213,7 @@ export default defineStore("store", {
|
||||
user: getDefaultUser(),
|
||||
device: getDefaultDevice(),
|
||||
online: false,
|
||||
deviceBattery: null,
|
||||
game: getDefaultGame(),
|
||||
dailyCount: getDefaultDailyCount(),
|
||||
deviceChargingDialogVisible: false,
|
||||
|
||||
@@ -7,7 +7,7 @@ const { Reader, Writer } = protobuf;
|
||||
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
|
||||
// <match-schema-generated>
|
||||
// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。
|
||||
// 来源:src/utils/match.min.js(sha256: bece92f31bde3072)
|
||||
// 来源:src/utils/match.min.js(sha256: ec0371baeba9a9bf)
|
||||
// 协议命名空间:rpc;消息数:12;字段数:163
|
||||
|
||||
export const ServerMessageType = {
|
||||
@@ -188,7 +188,7 @@ const SCHEMAS = {
|
||||
43: { name: "calories", kind: "double" },
|
||||
44: { name: "score_slot", 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" },
|
||||
48: { name: "energy_req_percent", kind: "int32" },
|
||||
49: { name: "delta_current_energy", kind: "int32" },
|
||||
|
||||