Files
xiaozhi-esp32-server/main/manager-mobile/src/pages/device/index.vue
T

335 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import type { Device, FirmwareType } from '@/api/device'
import { computed, onMounted, ref } from 'vue'
import { useMessage } from 'wot-design-uni'
import { bindDevice, getBindDevices, getFirmwareTypes, unbindDevice, updateDeviceAutoUpdate } from '@/api/device'
import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({
name: 'DeviceManage',
})
// 接收props
interface Props {
agentId?: string
}
const props = withDefaults(defineProps<Props>(), {
agentId: 'default'
})
// 获取屏幕边界到安全区域距离
let safeAreaInsets: any
let systemInfo: any
// #ifdef MP-WEIXIN
systemInfo = uni.getWindowInfo()
safeAreaInsets = systemInfo.safeArea
? {
top: systemInfo.safeArea.top,
right: systemInfo.windowWidth - systemInfo.safeArea.right,
bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
left: systemInfo.safeArea.left,
}
: null
// #endif
// #ifndef MP-WEIXIN
systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets
// #endif
// 设备数据
const deviceList = ref<Device[]>([])
const firmwareTypes = ref<FirmwareType[]>([])
const loading = ref(false)
// 消息组件
const message = useMessage()
// 使用传入的智能体ID
const currentAgentId = computed(() => {
return props.agentId
})
// 获取设备列表
async function loadDeviceList() {
try {
console.log('获取设备列表')
// 检查是否有当前选中的智能体
if (!currentAgentId.value) {
console.warn('没有选中的智能体')
deviceList.value = []
return
}
loading.value = true
const response = await getBindDevices(currentAgentId.value)
deviceList.value = response || []
}
catch (error) {
console.error('获取设备列表失败:', error)
deviceList.value = []
}
finally {
loading.value = false
}
}
// 暴露给父组件的刷新方法
async function refresh() {
await loadDeviceList()
}
// 获取设备类型名称
function getDeviceTypeName(boardKey: string): string {
const firmwareType = firmwareTypes.value.find(type => type.key === boardKey)
return firmwareType?.name || boardKey
}
// 格式化时间
function formatTime(timeStr: string) {
if (!timeStr)
return t('device.neverConnected')
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000)
return t('device.justNow')
if (diff < 3600000)
return t('device.minutesAgo', { minutes: Math.floor(diff / 60000) })
if (diff < 86400000)
return t('device.hoursAgo', { hours: Math.floor(diff / 3600000) })
if (diff < 604800000)
return t('device.daysAgo', { days: Math.floor(diff / 86400000) })
return date.toLocaleDateString()
}
// 切换OTA自动更新
async function toggleAutoUpdate(device: Device) {
try {
const newStatus = device.autoUpdate === 1 ? 0 : 1
await updateDeviceAutoUpdate(device.id, newStatus)
device.autoUpdate = newStatus
toast.success(newStatus === 1 ? t('device.otaAutoUpdateEnabled') : t('device.otaAutoUpdateDisabled'))
}
catch (error: any) {
console.error('更新设备OTA状态失败:', error)
toast.error(t('device.operationFailed'))
}
}
// 解绑设备
async function handleUnbindDevice(device: Device) {
try {
await unbindDevice(device.id)
await loadDeviceList()
toast.success(t('device.deviceUnbound'))
}
catch (error: any) {
console.error('解绑设备失败:', error)
toast.error(t('device.unbindFailed'))
}
}
// 确认解绑设备
function confirmUnbindDevice(device: Device) {
message.confirm({
title: t('device.unbindDevice'),
msg: t('device.confirmUnbindDevice', { macAddress: device.macAddress }),
confirmButtonText: t('device.confirmUnbind'),
cancelButtonText: t('device.cancel'),
}).then(() => {
handleUnbindDevice(device)
}).catch(() => {
// 用户取消
})
}
// 绑定新设备
async function handleBindDevice(code: string) {
try {
if (!currentAgentId.value) {
toast.error(t('device.pleaseSelectAgent'))
return
}
await bindDevice(currentAgentId.value, code.trim())
await loadDeviceList()
toast.success(t('device.deviceBindSuccess'))
}
catch (error: any) {
console.error('绑定设备失败:', error)
const errorMessage = error?.message || '绑定失败,请检查验证码是否正确'
toast.error(errorMessage || t('device.bindFailed'))
}
}
// 打开绑定设备对话框
function openBindDialog() {
message
.prompt({
title: t('device.bindDevice'),
inputPlaceholder: t('device.enterDeviceCode'),
inputValue: '',
inputPattern: /^\d{6}$/,
confirmButtonText: t('device.bindNow'),
cancelButtonText: t('device.cancel'),
})
.then(async (result: any) => {
if (result.value && String(result.value).trim()) {
await handleBindDevice(String(result.value).trim())
}
})
.catch(() => {
// 用户取消操作
})
}
// 获取设备类型列表
async function loadFirmwareTypes() {
try {
const response = await getFirmwareTypes()
firmwareTypes.value = response
}
catch (error) {
console.error('获取设备类型失败:', error)
}
}
onMounted(async () => {
// 智能体已简化为默认
loadFirmwareTypes()
loadDeviceList()
})
// 暴露方法给父组件
defineExpose({
refresh,
})
</script>
<template>
<view class="device-container" style="background: #f5f7fb; min-height: 100%;">
<!-- 加载状态 -->
<view v-if="loading && deviceList.length === 0" class="loading-container">
<wd-loading color="#336cff" />
<text class="loading-text">
{{ t('device.loading') }}
</text>
</view>
<!-- 设备列表 -->
<view v-else-if="deviceList.length > 0" class="device-list">
<!-- 设备卡片列表 -->
<view class="box-border flex flex-col gap-[24rpx] p-[20rpx]">
<view v-for="device in deviceList" :key="device.id">
<wd-swipe-action>
<view class="cursor-pointer bg-[#fbfbfb] p-[32rpx] transition-all duration-200 active:bg-[#f8f9fa]">
<view class="flex items-start justify-between">
<view class="flex-1">
<view class="mb-[16rpx] flex items-center justify-between">
<text class="max-w-[60%] break-all text-[32rpx] text-[#232338] font-semibold">
{{ getDeviceTypeName(device.board) }}
</text>
</view>
<view class="mb-[20rpx]">
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.macAddress') }}{{ device.macAddress }}
</text>
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.firmwareVersion') }}{{ device.appVersion }}
</text>
<text class="block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.lastConnection') }}{{ formatTime(device.lastConnectedAt) }}
</text>
</view>
<view class="flex items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx]">
<text class="text-[28rpx] text-[#232338] font-medium">
{{ t('device.otaUpdate') }}
</text>
<wd-switch
:model-value="device.autoUpdate === 1"
size="24"
@change="toggleAutoUpdate(device)"
/>
</view>
</view>
</view>
</view>
<template #right>
<view class="h-full flex">
<view
class="h-full min-w-[120rpx] flex items-center justify-center bg-[#ff4d4f] p-x-[32rpx] text-[28rpx] text-white font-medium"
@click.stop="confirmUnbindDevice(device)"
>
<wd-icon name="delete" />
<text>{{ t('device.unbind') }}</text>
</view>
</view>
</template>
</wd-swipe-action>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else-if="!loading" class="empty-container">
<view class="flex flex-col items-center justify-center p-[100rpx_40rpx] text-center">
<wd-icon name="phone" custom-class="text-[120rpx] text-[#d9d9d9] mb-[32rpx]" />
<text class="mb-[16rpx] text-[32rpx] text-[#666666] font-medium">
{{ t('device.noDevice') }}
</text>
<text class="text-[26rpx] text-[#999999] leading-[1.5]">
{{ t('device.clickToBindFirstDevice') }}
</text>
</view>
</view>
<!-- FAB 绑定设备按钮 -->
<wd-fab type="primary" size="small" icon="add" :draggable="true" :expandable="false" @click="openBindDialog" />
<!-- MessageBox 组件 -->
<wd-message-box />
</view>
</template>
<style scoped>
.device-container {
position: relative;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #666666;
}
:deep(.wd-swipe-action) {
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid #eeeeee;
}
:deep(.wd-icon) {
font-size: 32rpx;
}
</style>