diff --git a/src/pages/device/composables/useDeviceStatus.js b/src/pages/device/composables/useDeviceStatus.js index d7b8dd6..d99eefb 100644 --- a/src/pages/device/composables/useDeviceStatus.js +++ b/src/pages/device/composables/useDeviceStatus.js @@ -11,8 +11,10 @@ export function useDeviceStatus({ user, device, deviceStatus, + deviceUsageDuration, online, updateDevice, + updateDeviceUsageDuration, clearDevice, unbindDialogVisible, }) { @@ -41,14 +43,28 @@ export function useDeviceStatus({ const batteryText = computed(() => battery.value === null ? "暂无数据" : `${battery.value}%` ); + const normalizeDuration = (value) => { + if (value == null || String(value).trim() === "") return null; + const seconds = Number(value); + return Number.isFinite(seconds) && seconds >= 0 ? seconds : null; + }; + const onlineDurationText = computed(() => { - const rawDuration = deviceStatus.value.onlineDuration; - if (rawDuration == null || String(rawDuration).trim() === "") return "--"; + const currentDeviceId = String(device.value.deviceId || ""); + const cachedDuration = + String(deviceUsageDuration.value.deviceId || "") === currentDeviceId + ? normalizeDuration(deviceUsageDuration.value.seconds) + : null; + const durations = [ + normalizeDuration(deviceStatus.value.onlineDuration), + normalizeDuration(deviceDetails.value.onlineDuration), + cachedDuration, + ].filter((value) => value !== null); - const seconds = Number(rawDuration); - if (!Number.isFinite(seconds) || seconds < 0) return "--"; + if (!durations.length) return "--"; + const seconds = Math.max(...durations); - // WS 推送秒数,页面按完整分钟展示累计使用时间。 + // 累计时长应单调递增,页面按完整分钟展示当前已知最大值。 const totalMinutes = Math.floor(seconds / 60); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; @@ -109,6 +125,7 @@ export function useDeviceStatus({ detail.qrCodeUrl ?? deviceDetails.value.qrCodeUrl ?? "" ).trim(), }; + updateDeviceUsageDuration(detail.onlineDuration, deviceId); } catch (error) { // 实时刷新失败时保留当前页面数据,等待下次通知或页面重新显示。 console.log("刷新设备详情失败", error); @@ -158,6 +175,7 @@ export function useDeviceStatus({ latestDevice.name || "我的智能弓" ); + updateDeviceUsageDuration(latestDevice.onlineDuration, latestDevice.deviceId); return; } clearDevice(); diff --git a/src/pages/device/my-device.vue b/src/pages/device/my-device.vue index 596acd0..01ccfe9 100644 --- a/src/pages/device/my-device.vue +++ b/src/pages/device/my-device.vue @@ -22,8 +22,8 @@ import { import { useOtaUpdate } from "@/composables/useOtaUpdate"; const store = useStore(); -const { updateDevice, clearDevice } = store; -const { user, device, deviceStatus, online } = storeToRefs(store); +const { updateDevice, updateDeviceUsageDuration, clearDevice } = store; +const { user, device, deviceStatus, deviceUsageDuration, online } = storeToRefs(store); const DEVICE_NAME_MAX_LENGTH = 10; const formatBindingDate = (value) => { @@ -97,8 +97,10 @@ const { user, device, deviceStatus, + deviceUsageDuration, online, updateDevice, + updateDeviceUsageDuration, clearDevice, unbindDialogVisible, }); @@ -112,6 +114,28 @@ const bindingDateText = computed(() => formatBindingDate(deviceDetails.value.bindTime) ); +// 固件版本统一以大写 V 开头,避免接口返回格式不一致影响弹窗展示。 +const formatFirmwareVersion = (value) => { + const version = String(value ?? "").trim(); + if (!version) return ""; + return /^v/i.test(version) ? `V${version.slice(1)}` : `V${version}`; +}; + +const firmwareConfirmContent = computed(() => { + const currentVersion = formatFirmwareVersion(deviceStatus.value.version); + const latestVersion = formatFirmwareVersion( + pendingOtaInfo.value?.versionNumber + ); + + if (currentVersion && latestVersion) { + return `当前固件版本为 ${currentVersion},发现新固件版本 ${latestVersion},是否立即更新固件?`; + } + if (latestVersion) { + return `发现新固件版本 ${latestVersion},是否立即更新固件?`; + } + return "发现新固件版本,是否立即更新固件?"; +}); + const deviceIdText = computed(() => showDeviceId.value && device.value.deviceName ? device.value.deviceName @@ -519,7 +543,7 @@ onShow(async () => { class="device-bow" :class="{ 'device-bow--online': isDeviceOnline }" :src="isDeviceOnline - ? 'https://static.shelingxingqiu.com/shootmini/static/home-device/device-bow--online.png' + ? 'https://static.shelingxingqiu.com/shootmini/static/home-device/device-bow.png' : 'https://static.shelingxingqiu.com/shootmini/static/home-device/device-bow.png'" :mode="isDeviceOnline ? 'aspectFit' : 'widthFix'" /> @@ -697,9 +721,9 @@ onShow(async () => { @@ -975,7 +999,7 @@ onShow(async () => { .device-bow { position: absolute; - top: 0; + top: -2rpx; left: 50%; z-index: 4; width: 562rpx; diff --git a/src/static/home-device/device-bow.png b/src/static/home-device/device-bow.png index c5b5db3..d94ea85 100644 Binary files a/src/static/home-device/device-bow.png and b/src/static/home-device/device-bow.png differ diff --git a/src/store.js b/src/store.js index d75639a..3774d66 100644 --- a/src/store.js +++ b/src/store.js @@ -27,6 +27,11 @@ const getDefaultDeviceStatus = () => ({ receivedAt: 0, }); +const getDefaultDeviceUsageDuration = () => ({ + deviceId: "", + seconds: null, +}); + const getDefaultGame = () => ({ roomID: "", inBattle: false, @@ -114,6 +119,8 @@ export default defineStore("store", { deviceBattery: null, // WebSocket 实时推送的设备状态,不参与持久化。 deviceStatus: getDefaultDeviceStatus(), + // 按设备记录本次运行期间已知的最大累计使用时长,离线时保持数值不回退。 + deviceUsageDuration: getDefaultDeviceUsageDuration(), game: { roomID: "", inBattle: false, @@ -183,6 +190,27 @@ export default defineStore("store", { 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; @@ -210,6 +238,7 @@ 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; @@ -217,6 +246,7 @@ export default defineStore("store", { clearDevice() { this.device = getDefaultDevice(); this.clearDeviceStatus(); + this.clearDeviceUsageDuration(); }, async updateConfig(config) { this.config = config; @@ -267,6 +297,7 @@ export default defineStore("store", { online: false, deviceBattery: null, deviceStatus: getDefaultDeviceStatus(), + deviceUsageDuration: getDefaultDeviceUsageDuration(), game: getDefaultGame(), dailyCount: getDefaultDailyCount(), deviceChargingDialogVisible: false,