1037 lines
23 KiB
Plaintext
1037 lines
23 KiB
Plaintext
<template>
|
||
<scroll-view class="device-management-page">
|
||
<!-- Header -->
|
||
<view class="header">
|
||
<view class="header-left">
|
||
<button @click="goBack" class="back-btn">
|
||
<simple-icon type="arrow-left" :size="16" color="#FFFFFF" />
|
||
<text>返回</text>
|
||
</button>
|
||
<text class="title">设备管理</text>
|
||
</view>
|
||
<view class="header-actions">
|
||
<button @click="scanDevices" class="scan-btn">
|
||
<simple-icon type="search" :size="16" color="#FFFFFF" />
|
||
<text>扫描</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Loading State -->
|
||
<view v-if="loading" class="loading-container">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- Content -->
|
||
<scroll-view v-else class="content" scroll-y="true" :style="{ height: contentHeight + 'px' }">
|
||
<!-- Device Statistics -->
|
||
<view class="stats-section">
|
||
<view class="stat-card">
|
||
<text class="stat-number">{{ connectedDevices }}</text>
|
||
<text class="stat-label">已连接</text>
|
||
</view>
|
||
<view class="stat-card">
|
||
<text class="stat-number">{{ totalDevices }}</text>
|
||
<text class="stat-label">设备总数</text>
|
||
</view>
|
||
<view class="stat-card">
|
||
<text class="stat-number">{{ onlineDevices }}</text>
|
||
<text class="stat-label">在线设备</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Add Device Section -->
|
||
<view class="add-device-section">
|
||
<text class="section-title">添加新设备</text>
|
||
<view class="device-types">
|
||
<view class="device-type-item" v-for="type in deviceTypes" :key="type.type"
|
||
@click="addDevice(type)">
|
||
<text class="device-type-icon">{{ type.icon }}</text>
|
||
<text class="device-type-name">{{ type.name }}</text>
|
||
<text class="device-type-desc">{{ type.description }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- My Devices Section -->
|
||
<view class="devices-section">
|
||
<text class="section-title">我的设备</text>
|
||
|
||
<view v-if="userDevices.length === 0" class="empty-state">
|
||
<text class="empty-icon"></text>
|
||
<text class="empty-title">还没有绑定设备</text>
|
||
<text class="empty-subtitle">添加您的智能设备,开始记录运动数据</text>
|
||
<button @click="scanDevices" class="scan-devices-btn">扫描设备</button>
|
||
</view>
|
||
|
||
<view v-else class="devices-list">
|
||
<view class="device-item" v-for="device in userDevices" :key="device.id"
|
||
@click="deviceDetail(device)">
|
||
<view class="device-icon">
|
||
<text class="device-emoji">{{ getDeviceIcon(device.device_type as string) }}</text>
|
||
<view class="device-status" :class="getDeviceStatusClass(device.status as string)">
|
||
<text class="status-dot"></text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="device-info">
|
||
<text class="device-name">{{ getDeviceName(device) }}</text>
|
||
<text class="device-type">{{ getDeviceTypeName(device.device_type as string) }}</text>
|
||
<text class="device-status-text">{{ getDeviceStatusText(device) }}</text>
|
||
</view>
|
||
|
||
<view class="device-actions">
|
||
<view class="device-battery" v-if="getDeviceBattery(device) > 0">
|
||
<text class="battery-icon"></text>
|
||
<text class="battery-level">{{ getDeviceBattery(device) }}%</text>
|
||
</view>
|
||
<button class="action-btn" @click.stop="toggleDevice(device)">
|
||
<text class="action-text">{{ device.status === 'active' ? '断开' : '连接' }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Scanning Animation -->
|
||
<view v-if="isScanning" class="scanning-overlay">
|
||
<view class="scanning-content">
|
||
<view class="scanning-animation">
|
||
<text class="scanning-icon"></text>
|
||
<view class="scanning-rings">
|
||
<view class="ring ring-1"></view>
|
||
<view class="ring ring-2"></view>
|
||
<view class="ring ring-3"></view>
|
||
</view>
|
||
</view>
|
||
<text class="scanning-text">正在扫描设备...</text>
|
||
<text class="scanning-hint">请确保设备已开启并处于配对模式</text>
|
||
<button @click="stopScanning" class="stop-scan-btn">停止扫描</button>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- Add Device Modal -->
|
||
<view v-if="showAddModal" class="modal-overlay" @click="closeAddModal">
|
||
<view class="modal-content">
|
||
<view class="modal-header">
|
||
<text class="modal-title">添加{{ selectedDeviceType?.name }}</text>
|
||
<text class="modal-close" @click="closeAddModal">×</text>
|
||
</view>
|
||
|
||
<view class="modal-body">
|
||
<view class="form-group">
|
||
<text class="form-label">设备名称</text>
|
||
<input class="form-input" v-model="deviceForm.device_name" placeholder="为您的设备起个名字" />
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">设备MAC地址</text>
|
||
<input class="form-input" v-model="deviceForm.device_mac" placeholder="00:00:00:00:00:00" />
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">设备描述</text>
|
||
<textarea class="form-textarea" v-model="deviceForm.description" placeholder="描述设备特征或用途..." />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal-footer">
|
||
<button class="modal-button cancel" @click="closeAddModal">取消</button>
|
||
<button class="modal-button confirm" @click="saveDevice">添加设备</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted, computed, onUnmounted } from 'vue'
|
||
import { onLoad, onResize } from '@dcloudio/uni-app'
|
||
import supaClient from '@/components/supadb/aksupainstance.uts'
|
||
import { getCurrentUserId } from '@/utils/store.uts'
|
||
|
||
const userId = ref('')
|
||
|
||
// Responsive state - using onResize for dynamic updates
|
||
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
|
||
|
||
// Computed properties for responsive design
|
||
const isLargeScreen = computed(() : boolean => {
|
||
return screenWidth.value >= 768
|
||
})
|
||
|
||
const userDevices = ref<UTSJSONObject[]>([])
|
||
const loading = ref<boolean>(true)
|
||
const isScanning = ref<boolean>(false)
|
||
const showAddModal = ref<boolean>(false)
|
||
const selectedDeviceType = ref<UTSJSONObject | null>(null)
|
||
|
||
const connectedDevices = ref<number>(0)
|
||
const totalDevices = ref<number>(0)
|
||
const onlineDevices = ref<number>(0)
|
||
|
||
const deviceForm = ref<UTSJSONObject>({
|
||
device_name: '',
|
||
device_mac: '',
|
||
device_type: '',
|
||
description: ''
|
||
})
|
||
|
||
const deviceTypes = ref<UTSJSONObject[]>([
|
||
{
|
||
type: 'smart_band',
|
||
name: '智能手环',
|
||
icon: '⌚',
|
||
description: '监测心率、步数、睡眠等数据'
|
||
},
|
||
{
|
||
type: 'smart_watch',
|
||
name: '智能手表',
|
||
icon: '⌚',
|
||
description: '功能更全面的智能穿戴设备'
|
||
},
|
||
{
|
||
type: 'smart_ring',
|
||
name: '智能指环',
|
||
icon: '',
|
||
description: '轻巧便携的健康监测设备'
|
||
},
|
||
{
|
||
type: 'heart_rate_monitor',
|
||
name: '心率监测器',
|
||
icon: '❤️',
|
||
description: '专业的心率监测设备'
|
||
},
|
||
{
|
||
type: 'fitness_tracker',
|
||
name: '健身追踪器',
|
||
icon: '',
|
||
description: '专注运动数据的追踪设备'
|
||
},
|
||
{
|
||
type: 'smart_scale',
|
||
name: '智能体重秤',
|
||
icon: '⚖️',
|
||
description: '测量体重、体脂等身体数据'
|
||
}
|
||
])
|
||
|
||
const contentHeight = computed(() => {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
return systemInfo.windowHeight - 120 // 减去header高度
|
||
})
|
||
|
||
|
||
|
||
const getMockDevices = () : UTSJSONObject[] => {
|
||
return [
|
||
{
|
||
id: '1',
|
||
device_type: 'smart_band',
|
||
device_name: '我的小米手环',
|
||
device_mac: 'AA:BB:CC:DD:EE:FF',
|
||
status: 'active',
|
||
bind_time: new Date().toISOString(),
|
||
extra: {
|
||
battery: 85,
|
||
firmware_version: '1.2.3',
|
||
last_sync: new Date().toISOString()
|
||
}
|
||
},
|
||
{
|
||
id: '2',
|
||
device_type: 'smart_watch',
|
||
device_name: 'Apple Watch',
|
||
device_mac: '11:22:33:44:55:66',
|
||
status: 'offline',
|
||
bind_time: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||
extra: {
|
||
battery: 45,
|
||
firmware_version: '2.1.0',
|
||
last_sync: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString()
|
||
}
|
||
}
|
||
]
|
||
}
|
||
const calculateStats = () => {
|
||
totalDevices.value = userDevices.value.length
|
||
connectedDevices.value = userDevices.value.filter(device =>
|
||
device instanceof UTSJSONObject && device.getString('status') === 'active'
|
||
).length
|
||
onlineDevices.value = userDevices.value.filter(device => {
|
||
if (!(device instanceof UTSJSONObject)) return false
|
||
const extra = device.get('extra')
|
||
let lastSync = ''
|
||
if (extra instanceof UTSJSONObject) {
|
||
lastSync = extra.getString('last_sync') ?? ''
|
||
}
|
||
if (lastSync === '') return false
|
||
const lastSyncTime = new Date(lastSync).getTime()
|
||
const now = new Date().getTime()
|
||
return now - lastSyncTime < 30 * 60 * 1000 // 30分钟内同步过算在线
|
||
}).length
|
||
}
|
||
const loadUserDevices = async () => {
|
||
try {
|
||
loading.value = true
|
||
|
||
const result = await supaClient
|
||
.from('ak_devices')
|
||
.select('*', { count: 'exact' })
|
||
.eq('user_id', userId.value)
|
||
.order('bind_time', { ascending: false })
|
||
.execute()
|
||
|
||
if (result.status == 200) {
|
||
userDevices.value = result.data as UTSJSONObject[]
|
||
calculateStats()
|
||
} else {
|
||
console.error('加载设备列表失败:', result.error)
|
||
// 使用模拟数据
|
||
userDevices.value = getMockDevices()
|
||
calculateStats()
|
||
}
|
||
} catch (error) {
|
||
console.error('加载设备列表异常:', error)
|
||
userDevices.value = getMockDevices()
|
||
calculateStats()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const getDeviceTypeName = (deviceType : string) : string => {
|
||
const typeMap = new UTSJSONObject()
|
||
typeMap.set('smart_band', '智能手环')
|
||
typeMap.set('smart_watch', '智能手表')
|
||
typeMap.set('smart_ring', '智能指环')
|
||
typeMap.set('heart_rate_monitor', '心率监测器')
|
||
typeMap.set('fitness_tracker', '健身追踪器')
|
||
typeMap.set('smart_scale', '智能体重秤')
|
||
return typeMap.getString(deviceType) ?? '未知设备'
|
||
}
|
||
|
||
// 设备相关获取函数
|
||
const getDeviceName = (device : UTSJSONObject) : string => {
|
||
if (!(device instanceof UTSJSONObject)) return ''
|
||
const name = device.getString('device_name') ?? ''
|
||
return name !== '' ? name : `未命名${getDeviceTypeName(device.getString('device_type') ?? '')}`
|
||
}
|
||
|
||
|
||
|
||
const getDeviceIcon = (deviceType : string) : string => {
|
||
const iconMap = new UTSJSONObject()
|
||
iconMap.set('smart_band', '⌚')
|
||
iconMap.set('smart_watch', '⌚')
|
||
iconMap.set('smart_ring', '')
|
||
iconMap.set('heart_rate_monitor', '❤️')
|
||
iconMap.set('fitness_tracker', '')
|
||
iconMap.set('smart_scale', '⚖️')
|
||
return iconMap.getString(deviceType) ?? ''
|
||
}
|
||
|
||
const getDeviceStatusClass = (status : string) : string => {
|
||
return `status-${status}`
|
||
}
|
||
|
||
const getDeviceStatusText = (device : UTSJSONObject) : string => {
|
||
if (!(device instanceof UTSJSONObject)) return ''
|
||
const status = device.getString('status') ?? 'offline'
|
||
let lastSync = ''
|
||
const extra = device.get('extra')
|
||
if (extra instanceof UTSJSONObject) {
|
||
lastSync = extra.getString('last_sync') ?? ''
|
||
}
|
||
if (status === 'active') {
|
||
if (typeof lastSync === 'string' && lastSync.length > 0) {
|
||
const lastSyncTime = new Date(lastSync).getTime()
|
||
const now = new Date().getTime()
|
||
const diffMinutes = Math.floor((now - lastSyncTime) / (60 * 1000))
|
||
if (diffMinutes < 5) {
|
||
return '在线'
|
||
} else if (diffMinutes < 30) {
|
||
return `${diffMinutes}分钟前同步`
|
||
} else {
|
||
return '已连接但离线'
|
||
}
|
||
}
|
||
return '已连接'
|
||
}
|
||
return '离线'
|
||
}
|
||
// 关闭添加设备弹窗
|
||
const closeAddModal = () => {
|
||
showAddModal.value = false
|
||
selectedDeviceType.value = null
|
||
deviceForm.value = new UTSJSONObject()
|
||
deviceForm.value.set('device_name', '')
|
||
deviceForm.value.set('device_mac', '')
|
||
deviceForm.value.set('device_type', '')
|
||
deviceForm.value.set('description', '')
|
||
}
|
||
|
||
const getDeviceBattery = (device : UTSJSONObject) : number => {
|
||
if (!(device instanceof UTSJSONObject)) return 0
|
||
const extra = device.get('extra')
|
||
if (extra instanceof UTSJSONObject) {
|
||
return extra.getNumber('battery') ?? 0
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// 扫描设备
|
||
const scanDevices = () => {
|
||
isScanning.value = true
|
||
|
||
// 模拟扫描过程
|
||
setTimeout(() => {
|
||
isScanning.value = false
|
||
uni.showToast({
|
||
title: '扫描完成',
|
||
icon: 'success'
|
||
})
|
||
}, 3000)
|
||
}
|
||
|
||
const stopScanning = () => {
|
||
isScanning.value = false
|
||
}
|
||
|
||
// 添加设备
|
||
const addDevice = (deviceType : UTSJSONObject) => {
|
||
selectedDeviceType.value = deviceType
|
||
deviceForm.value = new UTSJSONObject()
|
||
deviceForm.value.set('device_name', '')
|
||
deviceForm.value.set('device_mac', '')
|
||
deviceForm.value.set('device_type', deviceType.getString('type') ?? '')
|
||
deviceForm.value.set('description', '')
|
||
showAddModal.value = true
|
||
}
|
||
|
||
// 保存设备
|
||
const saveDevice = async () => {
|
||
try {
|
||
if (!(deviceForm.value instanceof UTSJSONObject)) return
|
||
if (deviceForm.value.getString('device_name') === '' || deviceForm.value.getString('device_type') === '') {
|
||
uni.showToast({
|
||
title: '请填写设备信息',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
const deviceData = new UTSJSONObject()
|
||
deviceData.set('user_id', userId.value)
|
||
deviceData.set('device_type', deviceForm.value.getString('device_type') ?? '')
|
||
deviceData.set('device_name', deviceForm.value.getString('device_name') ?? '')
|
||
deviceData.set('device_mac', deviceForm.value.getString('device_mac') ?? '')
|
||
deviceData.set('status', 'active')
|
||
const extra = new UTSJSONObject()
|
||
extra.set('description', deviceForm.value.getString('description') ?? '')
|
||
extra.set('battery', 100)
|
||
extra.set('firmware_version', '1.0.0')
|
||
extra.set('last_sync', new Date().toISOString())
|
||
deviceData.set('extra', extra)
|
||
const result = await supaClient
|
||
.from('ak_devices')
|
||
.insert(deviceData)
|
||
.execute()
|
||
if (result.status === 201 || result.status === 200) {
|
||
uni.showToast({
|
||
title: '设备添加成功',
|
||
icon: 'success'
|
||
})
|
||
closeAddModal()
|
||
loadUserDevices()
|
||
} else {
|
||
uni.showToast({
|
||
title: '添加失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('添加设备失败:', error)
|
||
uni.showToast({
|
||
title: '添加失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 切换设备状态
|
||
const toggleDevice = async (device : UTSJSONObject) => {
|
||
if (!(device instanceof UTSJSONObject)) return
|
||
const deviceId = device.getString('id') ?? ''
|
||
const currentStatus = device.getString('status') ?? ''
|
||
const newStatus = currentStatus === 'active' ? 'offline' : 'active'
|
||
try {
|
||
const result = await supaClient
|
||
.from('ak_devices')
|
||
.update({ status: newStatus })
|
||
.eq('id', deviceId)
|
||
.execute()
|
||
if (result.status === 201 || result.status === 200) {
|
||
uni.showToast({
|
||
title: newStatus === 'active' ? '设备已连接' : '设备已断开',
|
||
icon: 'success'
|
||
})
|
||
loadUserDevices()
|
||
} else {
|
||
uni.showToast({
|
||
title: '操作失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('切换设备状态失败:', error)
|
||
uni.showToast({
|
||
title: '操作失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 设备详情
|
||
const deviceDetail = (device : UTSJSONObject) => {
|
||
if (!(device instanceof UTSJSONObject)) return
|
||
const deviceId = device.getString('id') ?? ''
|
||
uni.navigateTo({
|
||
url: `/pages/sport/student/device-detail?deviceId=${deviceId}`
|
||
})
|
||
}
|
||
|
||
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
uni.navigateBack()
|
||
}
|
||
onLoad((options : OnLoadOptions) => {
|
||
userId.value = options['id'] ?? getCurrentUserId()
|
||
loadUserDevices()
|
||
})
|
||
|
||
onMounted(() => {
|
||
// Initialize screen width
|
||
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
||
})
|
||
|
||
// Handle resize events for responsive design
|
||
onResize((size) => {
|
||
screenWidth.value = size.size.windowWidth
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.device-management-page {
|
||
display: flex;
|
||
flex:1;
|
||
min-height: 100vh;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.header {
|
||
flex-direction: row;
|
||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||
padding: 20rpx 30rpx;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
color: white;
|
||
}
|
||
.header-left {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
.header-left > * {
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.back-btn,
|
||
.scan-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
padding: 15rpx 20rpx;
|
||
border-radius: 20rpx;
|
||
border: none;
|
||
color: white;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.back-btn simple-icon,
|
||
.scan-btn simple-icon {
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: white;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 400rpx;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 32rpx;
|
||
color: #8f9bb3;
|
||
}
|
||
|
||
.content {
|
||
padding: 30rpx;
|
||
}
|
||
/* Statistics */
|
||
.stats-section {
|
||
display: flex;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.stats-section .stat-card {
|
||
margin-right: 20rpx;
|
||
}
|
||
.stat-card {
|
||
flex: 1;
|
||
background: white;
|
||
border-radius: 20rpx;
|
||
padding: 30rpx 20rpx;
|
||
box-shadow: 0 8rpx 25rpx rgba(0, 0, 0, 0.1);
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.stat-number { font-size: 44rpx;
|
||
font-weight: bold;
|
||
color: #667eea;
|
||
line-height: 1;
|
||
margin-bottom: 10rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 24rpx;
|
||
color: #8f9bb3;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Add Device Section */
|
||
.add-device-section {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #2d3748;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.device-types {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
}
|
||
.device-types .device-type-item {
|
||
width: 47%;
|
||
margin-right: 15rpx;
|
||
margin-bottom: 15rpx;
|
||
}
|
||
.device-type-item {
|
||
background: white;
|
||
border-radius: 16rpx;
|
||
padding: 25rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||
transition: transform 0.2s;
|
||
margin-right: 20rpx;
|
||
}
|
||
.device-type-icon {
|
||
font-size: 40rpx;
|
||
margin-bottom: 10rpx;
|
||
text-align: center;
|
||
}
|
||
.device-type-name {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #2d3748;
|
||
margin-bottom: 5rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.device-type-desc {
|
||
font-size: 22rpx;
|
||
color: #718096;
|
||
line-height: 1.3;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Devices Section */
|
||
.devices-section {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.empty-state {
|
||
background: white;
|
||
border-radius: 20rpx;
|
||
padding: 60rpx 40rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||
}
|
||
.empty-icon {
|
||
font-size: 80rpx;
|
||
margin-bottom: 20rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #2d3748;
|
||
margin-bottom: 10rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-subtitle {
|
||
font-size: 28rpx;
|
||
color: #8f9bb3;
|
||
line-height: 1.4;
|
||
margin-bottom: 30rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.scan-devices-btn {
|
||
background: #667eea;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 25rpx;
|
||
padding: 25rpx 40rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
} .devices-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.devices-list .device-item {
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
.device-item {
|
||
background: white;
|
||
border-radius: 20rpx;
|
||
padding: 25rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||
transition: transform 0.2s;
|
||
}
|
||
.device-item .device-icon {
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.device-icon {
|
||
position: relative;
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #f7fafc;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.device-emoji {
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.device-status {
|
||
position: absolute;
|
||
bottom: 0;
|
||
right: 0;
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
border-radius: 50%;
|
||
border: 3rpx solid white;
|
||
}
|
||
|
||
.status-active {
|
||
background: #48bb78;
|
||
}
|
||
|
||
.status-offline {
|
||
background: #a0aec0;
|
||
}
|
||
|
||
.device-info {
|
||
flex: 1;
|
||
}
|
||
.device-name {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #2d3748;
|
||
margin-bottom: 5rpx;
|
||
}
|
||
|
||
.device-type {
|
||
font-size: 26rpx;
|
||
color: #718096;
|
||
margin-bottom: 3rpx;
|
||
}
|
||
|
||
.device-status-text {
|
||
font-size: 24rpx;
|
||
color: #8f9bb3;
|
||
}
|
||
.device-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.device-actions > * {
|
||
margin-bottom: 10rpx;
|
||
}
|
||
.device-battery {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.device-battery .battery-icon {
|
||
margin-right: 5rpx;
|
||
}
|
||
|
||
.battery-icon {
|
||
font-size: 20rpx;
|
||
}
|
||
|
||
.battery-level {
|
||
font-size: 22rpx;
|
||
color: #4a5568;
|
||
}
|
||
|
||
.action-btn {
|
||
background: #667eea;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 15rpx;
|
||
padding: 10rpx 20rpx;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
/* Scanning Animation */
|
||
.scanning-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.scanning-content {
|
||
background: white;
|
||
border-radius: 20rpx;
|
||
padding: 60rpx 40rpx;
|
||
margin: 40rpx;
|
||
}
|
||
|
||
.scanning-animation {
|
||
position: relative;
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
margin: 0 auto 30rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.scanning-icon {
|
||
font-size: 48rpx;
|
||
z-index: 1;
|
||
}
|
||
|
||
.scanning-rings {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
}
|
||
|
||
.ring {
|
||
position: absolute;
|
||
border: 2rpx solid #667eea;
|
||
border-radius: 50%;
|
||
opacity: 0;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.ring-1 {
|
||
top: 10rpx;
|
||
left: 10rpx;
|
||
right: 10rpx;
|
||
bottom: 10rpx;
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
.ring-2 {
|
||
top: 5rpx;
|
||
left: 5rpx;
|
||
right: 5rpx;
|
||
bottom: 5rpx;
|
||
animation-delay: 0.5s;
|
||
}
|
||
|
||
.ring-3 {
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
animation-delay: 1s;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
opacity: 1;
|
||
transform: scale(0.8);
|
||
}
|
||
|
||
100% {
|
||
opacity: 0;
|
||
transform: scale(1.2);
|
||
}
|
||
}
|
||
.scanning-text {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #2d3748;
|
||
margin-bottom: 10rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.scanning-hint {
|
||
font-size: 26rpx;
|
||
color: #718096;
|
||
line-height: 1.4;
|
||
margin-bottom: 30rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.stop-scan-btn {
|
||
background: #e53e3e;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 25rpx;
|
||
padding: 25rpx 40rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
}
|
||
|
||
/* Modal */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 24rpx;
|
||
width: 100%;
|
||
max-width: 600rpx;
|
||
max-height: 80vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 40rpx 40rpx 20rpx;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #2d3748;
|
||
}
|
||
|
||
.modal-close {
|
||
font-size: 48rpx;
|
||
color: #a0aec0;
|
||
line-height: 1;
|
||
padding: 10rpx;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 40rpx;
|
||
flex: 1;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 30rpx;
|
||
}
|
||
.form-label {
|
||
font-size: 28rpx;
|
||
color: #4a5568;
|
||
font-weight: 400;
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
.form-input,
|
||
.form-textarea {
|
||
width: 100%;
|
||
padding: 25rpx 20rpx;
|
||
border: 2rpx solid #e2e8f0;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
color: #2d3748;
|
||
background: #f7fafc;
|
||
}
|
||
.form-textarea {
|
||
height: 120rpx;
|
||
resize: none;
|
||
}
|
||
.modal-footer {
|
||
padding: 20rpx 40rpx 40rpx;
|
||
display: flex;
|
||
border-top: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.modal-footer .modal-button {
|
||
margin-right: 20rpx;
|
||
}
|
||
.modal-button {
|
||
flex: 1;
|
||
padding: 25rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
border: none;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.modal-button.cancel {
|
||
background: #f7fafc;
|
||
color: #4a5568;
|
||
}
|
||
.modal-button.confirm {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
</style> |