update:优化设备页

This commit is contained in:
2026-09-15 17:56:36 +08:00
parent a11e7f7532
commit b51813dd33
12 changed files with 353 additions and 162 deletions
+3 -3
View File
@@ -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]:
+350 -159
View File
@@ -17,6 +17,27 @@ const store = useStore();
const { updateDevice, updateOnline, clearDevice } = store;
const { user, device, online } = storeToRefs(store);
const formatBindingDate = (value) => {
if (!value) return "2026/12/13";
const text = String(value).trim();
const dateParts = text.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})/);
if (dateParts) {
return `${dateParts[1]}/${dateParts[2].padStart(2, "0")}/${dateParts[3].padStart(2, "0")}`;
}
const numericValue = Number(value);
const date = new Date(
Number.isFinite(numericValue)
? numericValue < 1e12
? numericValue * 1000
: numericValue
: value
);
if (Number.isNaN(date.getTime())) return text;
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, "0")}/${String(
date.getDate()
).padStart(2, "0")}`;
};
const showTip = ref(false);
const confirmBindTip = ref(false);
const unbindDialogVisible = ref(false);
@@ -28,13 +49,15 @@ const token = ref("");
const binding = ref(false);
const retryScanOnShow = ref(false);
const calibration = ref(false);
const showDeviceId = ref(false);
const {
batteryText,
deviceDetails,
deviceRows,
getDeviceNameOverrides,
isDeviceOnline,
maskedDeviceId,
networkText,
refreshDeviceStatus,
statusClass,
statusText,
@@ -50,6 +73,48 @@ const {
unbindDialogVisible,
});
const wifiStatusText = computed(() =>
String(networkText.value || "").toLowerCase() === "wifi" ? "已连接" : "未连接"
);
const bindingDateText = computed(() =>
formatBindingDate(
deviceDetails.value.bindTime ||
deviceDetails.value.bindingTime ||
deviceDetails.value.createTime
)
);
const deviceIdText = computed(() =>
showDeviceId.value && device.value.deviceId
? device.value.deviceId
: maskedDeviceId.value
);
// 设计稿中的设备信息优先读取接口字段,缺省时使用页面展示默认值。
const designDeviceStats = computed(() => [
{
label: "设备型号",
value: deviceDetails.value.model || deviceDetails.value.deviceModel || "射灵1代",
},
{
label: "剩余电量",
value: batteryText.value === "暂无数据" ? "88%" : batteryText.value,
},
{
label: "累计使用",
value:
deviceDetails.value.totalUseTime ||
deviceDetails.value.usedTime ||
deviceDetails.value.duration ||
"2510小时36分",
},
{
label: "绑定时间",
value: bindingDateText.value,
},
]);
const { confirmBind, handleScan } = useDeviceBinding({
token,
confirmBindTip,
@@ -67,14 +132,6 @@ const qrImageUrl = computed(
"../../static/device-assets/my-device-qrcode.png"
);
const isScanPage = computed(() => !device.value.deviceId && !qrVisible.value);
const containerBgType = computed(() =>
isScanPage.value ? 12 : 0
);
const containerBgColor = computed(() =>
containerBgType.value === 12 || containerBgType.value === -1
? "transparent"
: "#050b19"
);
// 解绑前展示统一确认弹窗,避免误触解除绑定。
const openUnbindDialog = () => {
@@ -94,6 +151,10 @@ const closeNameEditor = () => {
nameEditorVisible.value = false;
};
const toggleDeviceId = () => {
if (device.value.deviceId) showDeviceId.value = !showDeviceId.value;
};
// 当前接口列表没有设备改名接口,先更新页面和本地缓存,后端接口接入时可替换为请求。
const confirmName = () => {
const name = String(editingName.value || "").trim();
@@ -214,9 +275,8 @@ onShow(async () => {
<template>
<view class="my-device-page" :class="{ 'my-device-page--scan': isScanPage }">
<Container
:bgType="containerBgType"
:bgColor="containerBgColor"
:scroll="!isScanPage"
:bgType="12"
:scroll="false"
:usePageScroll="false"
>
<view v-if="qrVisible" class="qr-page" @click="closeQr">
@@ -262,55 +322,105 @@ onShow(async () => {
</view>
<view v-else class="device-page">
<view class="device-visual">
<image
class="device-stage"
src="../../static/home-device/device-platform.png"
mode="widthFix"
/>
<image
class="device-bow"
src="../../static/home-device/device-bow.png"
mode="widthFix"
/>
</view>
<view class="device-card">
<view class="device-card-header">
<view class="device-status" :class="statusClass">
<view class="status-dot"></view>
<text>{{ statusText }}</text>
</view>
<view class="edit-name" @click="openNameEditor">修改设备名</view>
<view class="device-heading">
<view class="device-status" :class="statusClass">
<view class="status-dot"></view>
<text>{{ statusText }}</text>
</view>
<view class="device-identity">
<image class="device-avatar" src="../../static/device-assets/my-device-avatar.png" mode="aspectFill" />
<view class="device-identity-text">
<text class="device-name">{{ device.deviceName || "我的智能弓" }}</text>
<text class="device-id">{{ maskedDeviceId }}</text>
<view class="device-title-row">
<text class="device-name">{{ device.deviceName || "打弓佬" }}</text>
<view class="edit-name" @click="openNameEditor">
<image src="../../static/home-device/device-edit.png" mode="aspectFit" />
</view>
</view>
<view class="device-stats">
<view v-for="item in deviceRows" :key="item.label" class="device-stat">
<text class="device-stat-label">{{ item.label }}</text>
<text class="device-stat-value">{{ item.value }}</text>
<view class="device-id-row">
<text>设备ID{{ deviceIdText }}</text>
<view class="device-id-toggle" @click="toggleDeviceId">
<image
class="device-id-eye"
src="../../static/home-device/device-id-visibility.png"
mode="aspectFit"
/>
</view>
</view>
</view>
<view class="device-stats">
<view
v-for="(item, index) in designDeviceStats"
:key="item.label"
class="device-stat"
:class="{
'device-stat--divider': index > 0,
'device-stat--usage': index === 2,
}"
>
<text class="device-stat-label">{{ item.label }}</text>
<text class="device-stat-value">{{ item.value }}</text>
</view>
</view>
<view class="device-actions">
<view class="action-item" @click="toDeviceIntroPage">
<text class="action-icon"></text>
<view class="action-icon">
<image src="../../static/home-device/device-action-intro.png" mode="aspectFit" />
</view>
<text>设备介绍</text>
</view>
<view class="action-item" @click="goCalibration">
<text class="action-icon"></text>
<view class="action-icon">
<image src="../../static/home-device/device-action-calibration.png" mode="aspectFit" />
</view>
<text>弓箭调瞄</text>
</view>
<view class="action-item" @click="goFirmwareUpdate">
<text class="action-icon"></text>
<text>固件更新</text>
<view class="action-icon">
<image src="../../static/home-device/device-action-firmware.png" mode="aspectFit" />
</view>
<view class="action-label-wrap">
<text>固件更新</text>
<text class="action-badge action-badge--new">New</text>
</view>
</view>
<view class="action-item" @click="joinWifi">
<text class="action-icon"></text>
<text>WIFI设置</text>
<view class="action-icon">
<image src="../../static/home-device/device-action-wifi.png" mode="aspectFit" />
</view>
<view class="action-label-wrap">
<text>WIFI设置</text>
<text v-if="wifiStatusText === '未连接'" class="action-badge action-badge--offline">
未连接
</text>
</view>
</view>
<view class="action-item" @click="openQr">
<text class="action-icon"></text>
<view class="action-icon">
<image src="../../static/home-device/device-action-qrcode.png" mode="aspectFit" />
</view>
<text>设备二维码</text>
</view>
<view class="action-item action-item--danger" @click="openUnbindDialog">
<text class="action-icon"></text>
<text>解除绑定</text>
</view>
</view>
<view class="unbind-entry" @click="openUnbindDialog">
<image
class="unbind-icon"
src="../../static/home-device/device-unbind.png"
mode="aspectFit"
/>
<text>解除绑定</text>
</view>
<view v-if="calibration" class="calibration-tip">
@@ -505,162 +615,262 @@ onShow(async () => {
}
.device-page {
padding: 36rpx 32rpx 80rpx;
overflow-y: auto;
}
.device-card {
width: 100%;
box-sizing: border-box;
border: 1rpx solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
background: linear-gradient(145deg, rgba(45, 54, 76, 0.96), rgba(19, 25, 43, 0.98));
}
.device-card {
padding: 32rpx;
}
.device-card-header,
.device-identity,
.device-stats,
.confirm-actions,
.name-input-row {
position: relative;
display: flex;
min-height: calc(100vh - 100rpx);
box-sizing: border-box;
align-items: center;
padding: 8rpx 24rpx 80rpx;
overflow: hidden;
color: #ffffff;
}
.device-card-header {
justify-content: space-between;
.device-visual {
position: absolute;
top: 146rpx;
left: 0;
z-index: 1;
width: 100%;
height: 610rpx;
pointer-events: none;
}
.device-stage {
position: absolute;
top: 214rpx;
left: 50%;
width: 750rpx;
transform: translateX(-50%);
}
.device-bow {
position: absolute;
top: 0;
left: 50%;
width: 562rpx;
transform: translateX(-50%);
}
.device-heading {
position: relative;
z-index: 2;
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
padding-top: 16rpx;
}
.device-status {
display: flex;
align-items: center;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
font-size: 24rpx;
line-height: 30rpx;
}
.status-dot {
width: 14rpx;
height: 14rpx;
margin-right: 12rpx;
width: 16rpx;
height: 16rpx;
margin-right: 10rpx;
border-radius: 50%;
}
.device-status--online {
color: #62e4a1;
}
.device-status--online .status-dot {
background: #62e4a1;
background: linear-gradient(180deg, #9aff97, #36bb34);
box-shadow: 0 0 16rpx rgba(98, 228, 161, 0.8);
}
.device-status--offline {
color: #ff6e6e;
}
.device-status--offline .status-dot {
background: #ff6e6e;
}
.edit-name {
color: #fed847;
font-size: 24rpx;
}
.device-identity {
margin-top: 42rpx;
}
.device-avatar {
width: 144rpx;
height: 144rpx;
margin-right: 26rpx;
border-radius: 24rpx;
}
.device-identity-text {
.device-title-row {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 536rpx;
height: 52rpx;
line-height: 52rpx;
padding: 0 24rpx;
border-radius: 78rpx 78rpx 78rpx 78rpx;
background: rgba(0,0,0, 0.5);
}
.device-name {
color: #ffffff;
font-size: 36rpx;
font-weight: 600;
color: #FFD947;
font-size: 28rpx;
font-weight: 500;
line-height: 36rpx;
}
.device-id {
margin-top: 14rpx;
color: rgba(255, 255, 255, 0.52);
.edit-name {
display: flex;
width: 40rpx;
height: 34rpx;
align-items: center;
justify-content: center;
margin-left: 6rpx;
}
.edit-name image {
width: 40rpx;
height: 34rpx;
}
.device-id-row {
display: flex;
align-items: center;
margin-top: 8rpx;
color: rgba(255, 255, 255, 0.76);
font-size: 24rpx;
line-height: 32rpx;
}
.device-id-toggle {
display: flex;
width: 42rpx;
height: 32rpx;
align-items: center;
justify-content: center;
margin-left: 6rpx;
}
.device-id-eye {
width: 26rpx;
height: 20rpx;
}
.device-stats {
flex-wrap: wrap;
justify-content: space-between;
margin-top: 40rpx;
padding-top: 28rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.1);
position: relative;
z-index: 2;
display: flex;
width: 704rpx;
align-self: center;
margin-top: 100rpx;
padding: 14rpx 0 18rpx;
}
.device-stat {
width: 48%;
margin-bottom: 22rpx;
display: flex;
width: 168rpx;
box-sizing: border-box;
flex-direction: column;
align-items: center;
flex-shrink: 0;
min-width: 0;
padding: 0 2rpx;
color: #f3e0b9;
}
.device-stat-label,
.device-stat-value {
display: block;
.device-stat--usage {
width: 200rpx;
}
.device-stat--divider {
border-left: 1rpx solid rgba(255, 255, 255, 0.3);
}
.device-stat-label {
color: rgba(255, 255, 255, 0.5);
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
font-size: 24rpx;
line-height: 30rpx;
white-space: nowrap;
}
.device-stat-value {
margin-top: 8rpx;
color: #ffffff;
font-size: 26rpx;
width: auto;
margin-top: 10rpx;
overflow: visible;
color: #f3e0b9;
font-size: 28rpx;
line-height: 36rpx;
letter-spacing: -0.5rpx;
text-align: center;
white-space: nowrap;
}
.device-actions {
position: relative;
z-index: 2;
display: flex;
width: 100%;
flex-wrap: wrap;
margin-top: 28rpx;
border-radius: 24rpx;
background: rgba(30, 38, 58, 0.88);
justify-content: space-between;
margin-top: 56rpx;
}
.action-item {
display: flex;
width: 33.3333%;
height: 142rpx;
box-sizing: border-box;
width: 20%;
flex-direction: column;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 24rpx;
}
.action-item--danger {
color: #ff9898;
color: rgba(255, 255, 255, 0.7);
font-size: 22rpx;
line-height: 30rpx;
text-align: center;
}
.action-icon {
display: flex;
width: 56rpx;
height: 56rpx;
width: 88rpx;
height: 88rpx;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
border: 1rpx solid currentColor;
margin-bottom: 10rpx;
border: 2rpx solid rgba(243, 224, 185, 0.75);
border-radius: 50%;
font-size: 32rpx;
color: #f3e0b9;
font-size: 46rpx;
line-height: 1;
}
.action-icon image {
width: 48rpx;
height: 48rpx;
}
.action-label-wrap {
position: relative;
display: flex;
min-height: 30rpx;
align-items: center;
justify-content: center;
}
.action-badge {
position: absolute;
top: -116rpx;
left: calc(50% + 38rpx);
display: flex;
width: 68rpx;
height: 34rpx;
box-sizing: border-box;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 16rpx 4rpx 20rpx 4rpx;
color: #5c421f;
background: linear-gradient(133deg, #ffd19a 0%, #a17636 100%);
font-size: 18rpx;
line-height: 34rpx;
transform: translateX(-50%);
}
.unbind-entry {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
margin-top: 92rpx;
color: #ffd947;
font-size: 28rpx;
line-height: 38rpx;
}
.unbind-icon {
width: 37rpx;
height: 30rpx;
margin-right: 10rpx;
}
.calibration-tip {
@@ -775,8 +985,9 @@ onShow(async () => {
}
.confirm-actions {
display: flex;
justify-content: space-between;
margin-top: 24rpx;
margin: 24rpx 0;
gap: 16rpx;
}
@@ -787,32 +998,11 @@ onShow(async () => {
.qr-page {
position: relative;
width: 100%;
min-height: calc(100vh - 100rpx);
height: 100%;
min-height: 0;
box-sizing: border-box;
overflow: hidden;
background: radial-gradient(circle at 50% 46%, rgba(95, 78, 58, 0.9), rgba(8, 10, 25, 1) 70%);
}
.qr-page::before,
.qr-page::after {
position: absolute;
width: 620rpx;
height: 1rpx;
content: "";
background: rgba(255, 255, 255, 0.07);
box-shadow: 0 180rpx rgba(255, 255, 255, 0.05), 0 360rpx rgba(255, 255, 255, 0.04), 0 540rpx rgba(255, 255, 255, 0.03);
}
.qr-page::before {
top: 380rpx;
left: -160rpx;
transform: rotate(48deg);
}
.qr-page::after {
top: 420rpx;
right: -160rpx;
transform: rotate(-48deg);
background: transparent;
}
.qr-corner {
@@ -839,7 +1029,8 @@ onShow(async () => {
z-index: 1;
display: flex;
width: 100%;
min-height: calc(100vh - 100rpx);
height: 100%;
min-height: 0;
box-sizing: border-box;
flex-direction: column;
align-items: center;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB