diff --git a/src/App.vue b/src/App.vue
index 4e6d0d8..5ccb912 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -8,9 +8,6 @@
} from "@dcloudio/uni-app";
import websocket from "@/websocket";
import matchWebsocket from "@/matchWebsocket";
- import {
- getDeviceBatteryAPI
- } from "@/apis";
import {
MESSAGETYPES
} from "@/constants";
@@ -27,7 +24,9 @@
} = storeToRefs(store);
const {
updateUser,
- updateOnline,
+ updateDeviceStatus,
+ setDeviceOnline,
+ clearDeviceStatus,
showDeviceChargingDialog,
clearSessionState,
clearDevice
@@ -69,13 +68,19 @@
});
}
- async function emitUpdateOnline() {
- const data = await getDeviceBatteryAPI();
+ function emitUpdateOnline(nextOnline) {
const wasOnline = Boolean(online.value);
- const nextOnline = Boolean(data.online);
- updateOnline(nextOnline);
- if (!device.value.deviceId || wasOnline === nextOnline) return;
- audioManager.play(nextOnline ? "设备已连接" : "设备连接已断开");
+ setDeviceOnline(nextOnline === true);
+ if (!device.value.deviceId || wasOnline === (nextOnline === true)) return;
+ audioManager.play(nextOnline === true ? "设备已连接" : "设备连接已断开");
+ }
+
+ function onDeviceStatusPush(status) {
+ updateDeviceStatus(status);
+ }
+
+ function onShootSocketDisconnected() {
+ clearDeviceStatus();
}
function onDeviceBindInvalid() {
@@ -111,6 +116,10 @@
}
function onShootWsMsg(content) {
+ if (content?.event === "/addons/shoot/battery") {
+ onDeviceStatusPush(content.data);
+ return;
+ }
if(content.type === 'shoot-trigger'){
onDeviceShoot()
}
@@ -122,6 +131,7 @@
void audioManager.warmButton();
uni.$on("update-user", emitUpdateUser);
uni.$on("update-online", emitUpdateOnline);
+ uni.$on("shoot-socket-disconnected", onShootSocketDisconnected);
uni.$on("session-kicked-out", onSessionKickedOut);
uni.$on("device-bind-invalid", onDeviceBindInvalid);
uni.$on("device-charging", onDeviceCharging);
@@ -148,6 +158,7 @@
onHide(() => {
uni.$off("update-user", emitUpdateUser);
uni.$off("update-online", emitUpdateOnline);
+ uni.$off("shoot-socket-disconnected", onShootSocketDisconnected);
uni.$off("session-kicked-out", onSessionKickedOut);
uni.$off("device-bind-invalid", onDeviceBindInvalid);
uni.$off("device-charging", onDeviceCharging);
@@ -157,6 +168,7 @@
matchWebsocket.closeMatchWebSocket({
reason: "app-hide"
});
+ clearDeviceStatus();
websocket.closeWebSocket();
});
diff --git a/src/apis.js b/src/apis.js
index 6bf5715..eb20f8f 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -28,7 +28,8 @@ try {
const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
const API_ROOT_URL = BASE_URL.replace(/\/api\/shoot$/, "");
// 统一处理业务接口请求,包含登录态、业务错误和特定接口空响应兼容。
-function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0]) {
+function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0], options = {}) {
+ const {timeout = 10000, showErrorToast = true} = options;
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
@@ -40,8 +41,16 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0])
method,
header,
data,
- timeout: 10000,
+ timeout,
success: (res) => {
+ if (
+ url === "/user/hardwareBox/connectWifi" &&
+ res.statusCode === 200 &&
+ typeof res.data?.success === "boolean"
+ ) {
+ resolve(res.data);
+ return;
+ }
const acceptsEmptyResponse = [
"/user/hardwareBox/connectWifi",
"/user/device/unbindByQrcodeId",
@@ -111,10 +120,12 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0])
icon: "none",
});
}
- uni.showToast({
- title: message,
- icon: "none",
- });
+ if (showErrorToast) {
+ uni.showToast({
+ title: message,
+ icon: "none",
+ });
+ }
reject(error);
return;
}
@@ -122,7 +133,7 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0])
}
},
fail: (err) => {
- handleRequestError(err, url);
+ if (showErrorToast) handleRequestError(err, url);
reject(err);
},
});
@@ -281,6 +292,14 @@ export const getMyDevicesAPI = () => {
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) => {
return request("POST", "/user/practice/create", {
shootNumber: arrows,
@@ -561,28 +580,32 @@ export const laserCloseAPI = async () => {
return request("POST", "/user/device/closeAim");
};
-export const getDeviceBatteryAPI = async () => {
- return request("GET", "/user/device/battery");
-};
-
// 设备连接指定 WiFi,只下发 WiFi 凭证,不触发 OTA 升级。
export const connectDeviceWifiAPI = async (ssid, password) => {
- return request("POST", "/user/hardwareBox/connectWifi", {ssid, password});
+ return request(
+ "POST",
+ "/user/hardwareBox/connectWifi",
+ {ssid, password},
+ BASE_URL,
+ [0],
+ {timeout: 20000, showErrorToast: false}
+ );
};
-// 获取硬件盒子版本信息,用于判断当前设备是否需要 OTA 升级。
-export const getHardwareBoxVersionAPI = async () => {
- return request("GET", "/user/hardwareBox/version");
+// 查询设备当前联网类型及已连接的 WiFi 名称。
+export const getDeviceWifiStatusAPI = async () => {
+ return request("GET", "/user/device/wifiStatus", {}, BASE_URL, [0], {
+ showErrorToast: false,
+ });
};
// 发送硬件盒子 OTA 更新指令,服务端会返回后续轮询使用的任务 ID。
-export const sendHardwareBoxUpdateAPI = async (data) => {
- return request("POST", "/user/hardwareBox/sendUpdate", data);
-};
-
-// 根据任务 ID 获取硬件盒子 OTA 更新状态。
-export const getHardwareBoxTaskStatusAPI = async (taskId) => {
- return request("GET", `/user/hardwareBox/taskStatus?taskId=${taskId}`);
+export const sendHardwareBoxUpdateAPI = async ({versionNumber, wifiSsid, wifiPassword}) => {
+ return request("POST", "/user/hardwareBox/sendUpdate", {
+ versionNumber,
+ wifiSsid,
+ wifiPassword,
+ });
};
export const addNoteAPI = async (id, remark) => {
diff --git a/src/audioManager.js b/src/audioManager.js
index 595e25f..fec9d59 100644
--- a/src/audioManager.js
+++ b/src/audioManager.js
@@ -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]:
diff --git a/src/canvas.js b/src/canvas.js
index 853fae6..30be04a 100644
--- a/src/canvas.js
+++ b/src/canvas.js
@@ -451,7 +451,7 @@ export const generateShareImage = async (canvasId, data) => {
hasPoint ? 756 : 402
);
renderText(ctx, "扫码打卡", 13, "#FFA118", 142, hasPoint ? 777 : 422);
- const pointImg = await loadCanvasImage(canvas, "../static/point.png");
+ const pointImg = await loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/point.png");
ctx.drawImage(pointImg, 120, hasPoint ? 765 : 410, 18, 14);
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
@@ -635,7 +635,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
);
} else {
ctx.drawImage(
- "/static/score-bg.png",
+ "https://static.shelingxingqiu.com/shootmini/static/score-bg.png",
16 + (i % 9) * 30,
290 + Math.ceil((i + 1) / 9) * 30,
27,
@@ -657,7 +657,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
ctx.drawImage(bgImg, 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, 38);
} else {
ctx.drawImage(
- "/static/score-bg.png",
+ "https://static.shelingxingqiu.com/shootmini/static/score-bg.png",
24 + rowIndex * 42,
i > 5 ? 362 : 320,
38,
@@ -708,10 +708,7 @@ export async function sharePractiseData(canvasId, type, user, data) {
// 头像与段位框属于装饰图片,缺失时使用兜底或跳过,避免阻断分享。
const loadProfileImage = async (src, fallbackSrc = "", label = "") => {
- const normalizedSrc =
- typeof src === "string" && src.startsWith("../static/")
- ? src.slice(2)
- : src;
+ const normalizedSrc = typeof src === "string" ? src : "";
if (normalizedSrc) {
try {
const path = await loadImage(normalizedSrc);
@@ -725,20 +722,20 @@ export async function sharePractiseData(canvasId, type, user, data) {
const avatarImgPromise = loadProfileImage(
user?.avatar,
- "/static/user-icon.png",
+ "https://static.shelingxingqiu.com/shootmini/static/user-icon.png",
"avatar"
);
const lvlImgPromise = loadProfileImage(user?.lvlImage, "", "level");
- let titleImageSrc = "/static/first-try-title.png";
+ let titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/first-try-title.png";
if (type == 2) {
- titleImageSrc = "/static/practise-one-title.png";
+ titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/practise-one-title.png";
} else if (type == 3) {
- titleImageSrc = "/static/practise-two-title.png";
+ titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/practise-two-title.png";
}
const titleImgPromise = loadCanvasImage(canvas, titleImageSrc);
- const scoreBgImgPromise = loadCanvasImage(canvas, "/static/score-bg.png");
- const qrCodeImgPromise = loadCanvasImage(canvas, "/static/qr-code.png");
+ const scoreBgImgPromise = loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/score-bg.png");
+ const qrCodeImgPromise = loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/qr-code.png");
const [avatarImg, lvlImg, titleImg, scoreBgImg, qrCodeImg] =
await Promise.all([
diff --git a/src/components/AppBackground.vue b/src/components/AppBackground.vue
index 7feadea..9495dad 100644
--- a/src/components/AppBackground.vue
+++ b/src/components/AppBackground.vue
@@ -81,6 +81,13 @@ const props = defineProps({
src="https://static.shelingxingqiu.com/shootmini/static/app-bg9.png"
mode="widthFix"
/>
+
+
const tabs = [
- { image: "../static/tab-vip.png" },
+ { image: "https://static.shelingxingqiu.com/shootmini/static/tab-vip.png" },
{ image: "https://static.shelingxingqiu.com/shootmini/static/tab-point-book.png" },
{ image: "https://static.shelingxingqiu.com/shootmini/static/tab-mall.png" },
];
function handleTabClick(index) {
if (index === 0) {
- uni.navigateTo({
- url: "/pages/member/be-vip",
+ uni.showToast({
+ title: "功能尚未开启",
+ icon: "none",
});
+ // uni.navigateTo({
+ // url: "/pages/member/be-vip",
+ // });
}
if (index === 1) {
uni.navigateTo({
@@ -18,7 +22,7 @@ function handleTabClick(index) {
}
if (index === 2) {
uni.navigateTo({
- url: "/pages/device-intro",
+ url: "/pages/device/device-intro",
});
}
}
diff --git a/src/components/Avatar.vue b/src/components/Avatar.vue
index 470db77..7c0fce6 100644
--- a/src/components/Avatar.vue
+++ b/src/components/Avatar.vue
@@ -77,25 +77,25 @@ watch(
/>
{{ rank }}
{
返回房间
-
+
diff --git a/src/components/BattleHeader.vue b/src/components/BattleHeader.vue
index 9cdc95e..9bee7a4 100644
--- a/src/components/BattleHeader.vue
+++ b/src/components/BattleHeader.vue
@@ -72,7 +72,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
@@ -100,7 +100,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
diff --git a/src/components/BowData.vue b/src/components/BowData.vue
index 409f98f..371cadf 100644
--- a/src/components/BowData.vue
+++ b/src/components/BowData.vue
@@ -58,7 +58,7 @@ const props = defineProps({
-
+
diff --git a/src/components/BowPower.vue b/src/components/BowPower.vue
index 528c1e7..d72d6c9 100644
--- a/src/components/BowPower.vue
+++ b/src/components/BowPower.vue
@@ -1,44 +1,15 @@
-
- 电量{{ power || 1 }}%
+
+ {{ power === null ? "电量--" : `电量${power}%` }}
diff --git a/src/components/BowTarget.vue b/src/components/BowTarget.vue
index 77127b9..feb5063 100644
--- a/src/components/BowTarget.vue
+++ b/src/components/BowTarget.vue
@@ -527,7 +527,7 @@ onBeforeUnmount(() => {
-
+
中场休息
@@ -565,7 +565,7 @@ onBeforeUnmount(() => {
@@ -585,7 +585,7 @@ onBeforeUnmount(() => {
diff --git a/src/components/BowTargetEdit.vue b/src/components/BowTargetEdit.vue
index ff7242c..16f880d 100644
--- a/src/components/BowTargetEdit.vue
+++ b/src/components/BowTargetEdit.vue
@@ -266,16 +266,16 @@ onBeforeUnmount(() => {
@touchstart.stop="confirmAdd"
:style="{ ...getNewPos() }"
>
-
+
-
+
-
+
diff --git a/src/components/BubbleTip.vue b/src/components/BubbleTip.vue
index 4fdfdb6..7a61ccd 100644
--- a/src/components/BubbleTip.vue
+++ b/src/components/BubbleTip.vue
@@ -29,14 +29,14 @@ const props = defineProps({
font-size: 24rpx;
}
.normal {
- background-image: url("../static/bubble-tip.png");
+ background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip.png");
width: 157rpx;
height: 105rpx;
padding-top: 10px;
padding-left: 30rpx;
}
.normal2 {
- background-image: url("../static/bubble-tip4.png");
+ background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip4.png");
width: 190rpx;
height: 105rpx;
padding-top: 10px;
@@ -46,14 +46,14 @@ const props = defineProps({
z-index: 1;
}
.long {
- background-image: url("../static/bubble-tip2.png");
+ background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip2.png");
width: 370rpx;
height: 70rpx;
top: -50%;
left: 49%;
}
.short {
- background-image: url("../static/bubble-tip3.png");
+ background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip3.png");
width: 300rpx;
height: 70rpx;
top: -50%;
diff --git a/src/components/Container.vue b/src/components/Container.vue
index cf3c3dc..c06a5f2 100644
--- a/src/components/Container.vue
+++ b/src/components/Container.vue
@@ -124,7 +124,7 @@ const cancelMatching = async () => {
const goCalibration = async () => {
await laserAimAPI();
uni.navigateTo({
- url: "/pages/calibration",
+ url: "/pages/device/calibration",
});
};
diff --git a/src/components/DeviceOnlineEffects.vue b/src/components/DeviceOnlineEffects.vue
new file mode 100644
index 0000000..a039ae5
--- /dev/null
+++ b/src/components/DeviceOnlineEffects.vue
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/EditOption.vue b/src/components/EditOption.vue
index 555d4b2..fe6d52c 100644
--- a/src/components/EditOption.vue
+++ b/src/components/EditOption.vue
@@ -203,7 +203,7 @@ onMounted(async () => {
diff --git a/src/components/HeaderProgress.vue b/src/components/HeaderProgress.vue
index 936d5bd..bd80dad 100644
--- a/src/components/HeaderProgress.vue
+++ b/src/components/HeaderProgress.vue
@@ -146,7 +146,7 @@ onBeforeUnmount(() => {
{{ (tips || "").replace(/你/g, "").replace(/重回/g, "") }}
({{ currentShot }}/{{ totalShot }})
diff --git a/src/components/ModalDialog.vue b/src/components/ModalDialog.vue
index 5ab17a6..f0157e1 100644
--- a/src/components/ModalDialog.vue
+++ b/src/components/ModalDialog.vue
@@ -1,4 +1,6 @@
@@ -89,6 +108,7 @@ const handleConfirm = () => {
{{ confirmText }}
@@ -96,6 +116,13 @@ const handleConfirm = () => {
+
+
+
@@ -108,6 +135,7 @@ const handleConfirm = () => {
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.62);
+ flex-direction: column;
justify-content: center;
align-items: center;
z-index: 999;
@@ -116,6 +144,7 @@ const handleConfirm = () => {
.modal-wrap {
position: relative;
display: flex;
+ flex-direction: column;
width: 549rpx;
min-height: 318rpx;;
padding-top: 168rpx;
@@ -222,6 +251,14 @@ const handleConfirm = () => {
background-color: #ffda3f;
}
+.dialog-button.confirm.disabled {
+ opacity: 0.62;
+}
+
+.dialog-close {
+ margin-top: 28rpx;
+}
+
@keyframes rotateLight {
from {
transform: translateX(-50%) rotate(0deg);
diff --git a/src/components/OtaModal.vue b/src/components/OtaModal.vue
index 303c1a0..30af95b 100644
--- a/src/components/OtaModal.vue
+++ b/src/components/OtaModal.vue
@@ -1,10 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
+ 该设备已经绑定了其他账号,
+ 如需绑定本账号,请先解除绑定。
+
+
+ 重新扫码
+
+
+
+
+
+
+
+
diff --git a/src/pages/device/device-bind-failure.vue b/src/pages/device/device-bind-failure.vue
new file mode 100644
index 0000000..a434278
--- /dev/null
+++ b/src/pages/device/device-bind-failure.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 二维码不正确,
+ 仅支持扫描射灵智能弓箭的设备二维码。
+
+
+ 重新扫码
+
+
+
+
+
+
+
+
diff --git a/src/pages/device/device-bind-success.vue b/src/pages/device/device-bind-success.vue
new file mode 100644
index 0000000..4df34be
--- /dev/null
+++ b/src/pages/device/device-bind-success.vue
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新设备首次绑定礼包:
+ 6个月射灵会员
+ 已自动发放至本账号,有效期{{ bindRewardExpireDate }}。
+
+
+
+ 立即查看新手教程
+
+
+
+
+
+
+
+
diff --git a/src/pages/device-intro.vue b/src/pages/device/device-intro.vue
similarity index 95%
rename from src/pages/device-intro.vue
rename to src/pages/device/device-intro.vue
index f780e14..855b5ec 100644
--- a/src/pages/device-intro.vue
+++ b/src/pages/device/device-intro.vue
@@ -38,7 +38,7 @@ const onScrollView = (e) => {
mode="widthFix"
/>
-
+
+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();
+});
+
+
+
+
+
+
+
+
+ 二维码加载中...
+
+
+ 设备ID:{{ deviceId }}
+
+ 保存至相册
+
+
+ 该二维码为当前绑定弓箭的二维码,你可以截图保存到相册,以便当二维码丢失或不在身边时,可以扫描二维码进行设备绑定。
+
+ 注:解除绑定后将无法查看该二维码。
+
+ 暂无设备二维码
+
+
+
+
+
+
diff --git a/src/pages/device/my-device.vue b/src/pages/device/my-device.vue
new file mode 100644
index 0000000..911d1e9
--- /dev/null
+++ b/src/pages/device/my-device.vue
@@ -0,0 +1,1422 @@
+
+
+
+
+
+
+
+
+ 我的设备
+
+
+ {{ statusText }}
+
+
+
+
+
+
+
+ 请扫描
+ 射灵智能弓箭
+ 设备上的二维码
+
+
+ 新设备首次绑定账号可获赠
+ 6个月射灵会员礼包
+ ,
+ 该礼包仅可使用一次,请确保当前登录账号为您本人账号。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ device.deviceName || "打弓佬" }}
+
+
+
+
+
+ 设备ID:{{ deviceIdText }}
+
+
+
+
+
+
+
+
+ {{ item.label }}
+ {{ item.value }}
+
+
+
+
+
+
+
+
+ 设备介绍
+
+
+
+
+
+ 弓箭调瞄
+
+
+
+
+
+
+ 固件更新
+ New
+
+
+
+
+
+
+
+ WIFI设置
+
+ {{ wifiStatusText }}
+
+
+
+
+
+
+
+ 设备二维码
+
+
+
+
+
+ 解除绑定
+
+
+
+ 如有场地或距离变化,请重新校准以保证智能弓射箭精准度
+ 重新校准
+
+
+
+
+
+ 扫码绑定射灵弓箭
+ 配套令牌样例
+
+ 【注】已被绑定的弓箭无法再次绑定。
+
+ 如有任何疑问,请随时联系:
+
+
+
+
+
+
+
+ 智能弓箭和系统账号需一一对应,你确定要将当前登录用户账号绑定这把弓箭吗?绑定后不可随意更换。
+
+
+ {{ binding ? "绑定中..." : "确认绑定" }}
+
+ 取消
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 仅支持中文、英文、数字、下划线、减号
+ {{ editingNameLength }}/{{ DEVICE_NAME_MAX_LENGTH }}
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/device/ota-wifi.vue b/src/pages/device/ota-wifi.vue
new file mode 100644
index 0000000..213a003
--- /dev/null
+++ b/src/pages/device/ota-wifi.vue
@@ -0,0 +1,1172 @@
+
+
+
+
+
+
+ 请先开启 WiFi 后刷新重试
+
+
+
+
+
+
+
+ 智能弓箭连接无线网络
+ 提醒:本操作将为智能弓箭连接WIFI网络,不会影响手机自身连接的WIFI网络。
+
+
+
+
+ 已连接
+
+ {{ connectedWifiName }}
+
+
+
+
+
+
+ 选择网络
+
+
+
+
+
+
+ {{ wifiList[0].SSID }}
+ 手机当前连接的网络
+
+
+
+
+
+ 手动连接其他网络
+ 由于权限限制,无法获取附近网络列表,如需连接其他网络请手动输入网络名称与密码进行连接。
+
+
+
+
+
+
+
+ {{ wifi.SSID }}
+
+
+
+
+
+
+ 手动连接其他网络
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 请授权允许获取定位信息,并开启定位功能,以查找附近可用无线网络。
+
+ 授权获取定位信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 密码
+
+
+
+
+
+
+ {{ connectStatusText }}
+ {{ connectError }}
+
+
+
+
+
+ {{ connectStatusText }}
+ 该网络为开放网络,点击 ✓ 加入
+ {{ connectError }}
+
+
+
+
+
+
+ 名称
+
+
+
+ 密码
+
+
+
+
+
+
+ {{ ssidWarning }}
+ {{ connectStatusText }}
+ {{ connectError }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/friend-battle-result.vue b/src/pages/friend-battle-result.vue
index b84e46f..db618c5 100644
--- a/src/pages/friend-battle-result.vue
+++ b/src/pages/friend-battle-result.vue
@@ -168,7 +168,7 @@ const meleeRankList = computed(() => {
*/
function getMeleeRankBgSrc(rank) {
const names = ["one", "two", "three"];
- return `../static/friend-battle-result/rank-${names[rank - 1]}.svg`;
+ return `https://static.shelingxingqiu.com/shootmini/static/friend-battle-result/rank-${names[rank - 1]}.svg`;
}
/**
@@ -485,7 +485,7 @@ function goBack() {
diff --git a/src/pages/friend-battle.vue b/src/pages/friend-battle.vue
index 98719b2..e632653 100644
--- a/src/pages/friend-battle.vue
+++ b/src/pages/friend-battle.vue
@@ -190,10 +190,10 @@ onLoad(async (options) => {
-
-
+
-
+
挑战难度
@@ -201,7 +201,7 @@ onLoad(async (options) => {
-
+
enterRoom(roomNumber))">进入房间
@@ -214,7 +214,7 @@ onLoad(async (options) => {
-
+
diff --git a/src/pages/index.vue b/src/pages/index.vue
index b95216b..6e34ff2 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -1,26 +1,26 @@
-
-
-
-
-
-
- 射灵智能弓箭,三模传感系统与独创靶环算法,
- 毫秒级在线实时对战,让你拥有全球约战的乐趣!
-
-
-
- 扫码绑定设灵弓箭
-
- 【注】已被绑定的弓箭无法再次绑定。
-
- 如有任何疑问,请随时联系:
-
-
-
-
-
-
- 智能弓箭和系统账号需一一对应,你确定要将当前登录用户账号绑定这把弓箭吗?
- 绑定后不可随意更换。
-
- 确认绑定
- (confirmBindTip = false)">取消
-
-
-
-
-
-
-
-
- {{ device.deviceName }}
-
-
-
-
- 如有场地/距离变化,需重新校准以保证智能弓射箭精准度
-
-
-
-
-
-
-
- {{ user.nickName }}
- {{
- user.nickName
- }}
-
-
-
-
-
- 恭喜,你的弓箭和账号已成功绑定!
- 已赠送6个月射灵世界会员
-
-
-
-
- 返回首页
-
-
- 进入新手试炼
-
-
-
-
-
-
-
-
- {{ device.deviceName }}
-
-
-
-
- 首次绑定智能弓或场地/距离变化时,应进行校准以确保射箭精度
-
-
-
-
-
-
-
- {{ user.nickName }}
- {{
- user.nickName
- }}
-
-
-
-
- 解绑
-
-
- 设备连接WIFI
-
-
-
-
-
-
diff --git a/src/pages/my-growth.vue b/src/pages/my-growth.vue
index 4353e28..ce29bf2 100644
--- a/src/pages/my-growth.vue
+++ b/src/pages/my-growth.vue
@@ -116,7 +116,7 @@ onLoad((options) => {
{
{
>{{ item.trainingTypeText }}
{{ formatPractiseTime(item.createTime) }}
-
+
diff --git a/src/pages/org-bind.vue b/src/pages/org-bind.vue
index 3f5932d..f3fe10d 100644
--- a/src/pages/org-bind.vue
+++ b/src/pages/org-bind.vue
@@ -130,7 +130,7 @@ onLoad((options) => {
{{
@@ -151,7 +151,7 @@ onLoad((options) => {
/>
diff --git a/src/pages/ota-wifi.vue b/src/pages/ota-wifi.vue
deleted file mode 100644
index 9a8e9b7..0000000
--- a/src/pages/ota-wifi.vue
+++ /dev/null
@@ -1,1261 +0,0 @@
-
-
-
-
-
-
- 请先开启 WiFi 后刷新重试
-
-
-
-
-
-
-
-
-
-
- {{ currentState === 'CONNECTED' ? '连接成功' : '连接无线网络' }}
-
- 网络名称仅支持英文字符及数字
-
-
- 请先在系统 WiFi 设置中连接目标网络,返回后选择当前 WiFi 并填写密码
-
-
-
-
- 连接网络
-
-
-
-
- {{ connectedWifi?.SSID }}
-
-
-
-
-
-
-
-
-
- 网络
-
-
-
-
-
-
-
- 其他...
-
-
-
-
- {{ wifi.SSID }}
-
-
-
-
-
-
- 其他...
-
-
-
-
-
-
-
- 开始更新
-
-
-
-
-
-
-
- 更新中,请稍等片刻...
-
-
-
-
- {{ Math.floor(progress) }}%
-
-
-
-
-
-
- 更新完成
- 请关机并重启智能弓
-
- 完成
-
-
-
-
-
-
- 更新失败
- 请确保:
- 1、智能弓已开启
- 2、网路连接稳定
-
- 重试
-
-
-
-
-
-
-
-
-
-
-
-
-
- 密码
-
-
-
-
-
-
- {{ connectStatusText }}
- {{ connectError }}
-
-
-
-
-
- {{ connectStatusText }}
- 该网络为开放网络,点击 ✓ 加入
- {{ connectError }}
-
-
-
-
-
-
- 名称
-
-
-
- 密码
-
-
-
-
-
-
- {{ ssidWarning }}
- {{ connectStatusText }}
- {{ connectError }}
-
-
-
-
-
-
-
-
-
- 拒绝授权获取wifi列表失败,请重新授权或手动输入
-
-
- 取消授权
-
-
- 重新授权
-
-
-
-
-
-
-
-
diff --git a/src/pages/point-book-detail-share.vue b/src/pages/point-book-detail-share.vue
index 1f51176..9a10b27 100644
--- a/src/pages/point-book-detail-share.vue
+++ b/src/pages/point-book-detail-share.vue
@@ -115,7 +115,7 @@ const targetTypeName = computed(() => {
/>
-
+
距离正式比赛还有
{{ count }}
diff --git a/src/pages/training/practise-one.vue b/src/pages/training/practise-one.vue
index 3dbb376..afb5175 100644
--- a/src/pages/training/practise-one.vue
+++ b/src/pages/training/practise-one.vue
@@ -1918,7 +1918,7 @@ onBeforeUnmount(() => {
diff --git a/src/pages/user.vue b/src/pages/user.vue
index 1e7cdba..1aaef37 100644
--- a/src/pages/user.vue
+++ b/src/pages/user.vue
@@ -130,7 +130,7 @@ const buildVersion = typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : ''
title="我的金币"
:onClick="toCoinPage"
/>
-
+
Lv{{ user.lvl }}
({
deviceName: "",
});
+const getDefaultDeviceStatus = () => ({
+ battery: null,
+ charging: false,
+ version: "",
+ online: false,
+ netType: "",
+ onlineDuration: null,
+ receivedAt: 0,
+});
+
+const getDefaultDeviceUsageDuration = () => ({
+ deviceId: "",
+ seconds: null,
+});
+
const getDefaultGame = () => ({
roomID: "",
inBattle: false,
@@ -100,6 +115,12 @@ export default defineStore("store", {
ringRank: [],
},
online: false,
+ // 设备电量属于运行时状态,null 表示当前没有有效数据。
+ deviceBattery: null,
+ // WebSocket 实时推送的设备状态,不参与持久化。
+ deviceStatus: getDefaultDeviceStatus(),
+ // 按设备记录本次运行期间已知的最大累计使用时长,离线时保持数值不回退。
+ deviceUsageDuration: getDefaultDeviceUsageDuration(),
game: {
roomID: "",
inBattle: false,
@@ -134,7 +155,77 @@ export default defineStore("store", {
this.rankData = { ...(data || {}) };
},
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,
+ charging: status.charging === true,
+ 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.deviceBattery = online ? nextStatus.battery : null;
+ this.updateDeviceUsageDuration(nextStatus.onlineDuration);
+ },
+ updateDeviceUsageDuration(value, deviceId = this.device.deviceId) {
+ if (value === null || value === undefined || String(value).trim() === "") return;
+
+ const seconds = Number(value);
+ const normalizedDeviceId = String(deviceId || "");
+ if (!normalizedDeviceId || !Number.isFinite(seconds) || seconds < 0) return;
+
+ const cachedDeviceId = String(this.deviceUsageDuration.deviceId || "");
+ const cachedSeconds = Number(this.deviceUsageDuration.seconds);
+ this.deviceUsageDuration = {
+ deviceId: normalizedDeviceId,
+ seconds:
+ cachedDeviceId === normalizedDeviceId && Number.isFinite(cachedSeconds)
+ ? Math.max(cachedSeconds, seconds)
+ : seconds,
+ };
+ },
+ clearDeviceUsageDuration() {
+ this.deviceUsageDuration = getDefaultDeviceUsageDuration();
+ },
+ 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 = {}) {
this.user = { ...getDefaultUser(), ...user };
@@ -145,12 +236,17 @@ export default defineStore("store", {
);
},
updateDevice(deviceId, deviceName) {
+ if (String(this.device.deviceId || "") !== String(deviceId || "")) {
+ this.clearDeviceStatus();
+ this.clearDeviceUsageDuration();
+ }
this.device.deviceId = deviceId;
this.device.deviceName = deviceName;
},
clearDevice() {
this.device = getDefaultDevice();
- this.online = false;
+ this.clearDeviceStatus();
+ this.clearDeviceUsageDuration();
},
async updateConfig(config) {
this.config = config;
@@ -199,6 +295,9 @@ export default defineStore("store", {
user: getDefaultUser(),
device: getDefaultDevice(),
online: false,
+ deviceBattery: null,
+ deviceStatus: getDefaultDeviceStatus(),
+ deviceUsageDuration: getDefaultDeviceUsageDuration(),
game: getDefaultGame(),
dailyCount: getDefaultDailyCount(),
deviceChargingDialogVisible: false,
diff --git a/src/websocket.js b/src/websocket.js
index 77bd29a..9ac894b 100644
--- a/src/websocket.js
+++ b/src/websocket.js
@@ -77,19 +77,38 @@ function createWebSocket(token, onMessage) {
socketTask.onMessage((res) => {
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 (!data || typeof data !== "object") return;
- if (data.type) {
+ const passthroughEvents = [
+ "/addons/shoot/battery",
+ "/addons/shoot/otaProgress",
+ "/addons/shoot/otaResult",
+ ];
+ if (passthroughEvents.includes(event)) {
+ 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) {
console.log("收到 WebSocket 消息", getMessageTypeName(data.type));
}
if (onMessage) onMessage({ ...(data.data || {}), type: data.type });
return;
}
- if (!Array.isArray(data.updates)) return;
- if (onMessage) onMessage(data.updates);
- const msg = data.updates[0];
+ const updates = Array.isArray(data?.updates) ? data.updates : [];
+ if (!updates.length) return;
+ if (onMessage) onMessage(updates);
+ const msg = updates[0];
if (msg) {
if (ENABLE_REALTIME_MESSAGE_LOG) {
console.log(
@@ -103,9 +122,9 @@ function createWebSocket(token, onMessage) {
} else if (msg.constructor === MESSAGETYPES.LvlUpdate) {
uni.setStorageSync("latestLvl", msg.lvl);
} else if (msg.constructor === MESSAGETYPES.DeviceOnline) {
- uni.$emit("update-online");
+ uni.$emit("update-online", true);
} else if (msg.constructor === MESSAGETYPES.DeviceOffline) {
- uni.$emit("update-online");
+ uni.$emit("update-online", false);
} else if (msg.constructor === MESSAGETYPES.DeviceCharging) {
uni.$emit("device-charging");
}
@@ -123,6 +142,7 @@ function createWebSocket(token, onMessage) {
stopHeartbeat();
socket = null;
isConnecting = false;
+ uni.$emit("shoot-socket-disconnected");
if (manualClose || kickedOut) return;
await handleUnexpectedClose(onMessage);