946 lines
24 KiB
Vue
946 lines
24 KiB
Vue
<template>
|
||
<view class="page">
|
||
<u-navbar :title="voiceName || '语音配置'" :border-bottom="false" :background="bgc" back-icon-color="#262B37" title-color='#262B37'
|
||
title-size='36' height='36' id="navbar">
|
||
</u-navbar>
|
||
|
||
<scroll-view class="content-container" scroll-y>
|
||
<!-- 选择车辆区域 -->
|
||
<view class="section">
|
||
<view class="section-title">选择车辆</view>
|
||
<view class="select-device-btn" @click.stop="openDeviceModal">
|
||
<u-icon name="plus" color="#3996FD" size="24"></u-icon>
|
||
<text>选择车辆</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 已选车辆列表(始终展示该区域,空时显示提示) -->
|
||
<view class="section section-selected">
|
||
<view class="section-title">已选车辆 ({{ selectedDevices.length }})</view>
|
||
<view class="device-list" v-if="selectedDevices.length > 0">
|
||
<view class="device-item" v-for="(device, index) in selectedDevices" :key="getDeviceKeyForCompare(device) || index">
|
||
<view class="device-info">
|
||
<view class="device-name">SN: {{ device.sn || device.deviceSn || '--' }}</view>
|
||
<view class="device-meta">车牌: {{ device.vehicleNum || '--' }}</view>
|
||
<!-- 失败提示:放在车牌下方 -->
|
||
<view class="device-error" v-if="device.syncStatus === 'failed' && device.errorMsg">
|
||
{{ device.errorMsg }}
|
||
</view>
|
||
</view>
|
||
<view class="device-status">
|
||
<view class="status-info" v-if="device.syncStatus">
|
||
<u-icon
|
||
:name="device.syncStatus === 'success' ? 'checkmark-circle' : device.syncStatus === 'loading' ? 'loading' : 'close-circle'"
|
||
:color="device.syncStatus === 'success' ? '#64B6A7' : device.syncStatus === 'loading' ? '#3996FD' : '#ff4444'"
|
||
size="32"
|
||
></u-icon>
|
||
<text class="status-text" :class="device.syncStatus">
|
||
{{ device.syncStatus === 'success' ? '成功' : device.syncStatus === 'loading' ? '同步中...' : '失败' }}
|
||
</text>
|
||
</view>
|
||
<!-- 重试按钮:右上角(成功/失败都展示;同步中置灰不可点) -->
|
||
<view
|
||
class="retry-btn"
|
||
:class="{ disabled: device.syncStatus === 'loading' }"
|
||
@click.stop="device.syncStatus === 'loading' ? null : retrySync(device, index)"
|
||
v-if="device.syncStatus"
|
||
>
|
||
重试
|
||
</view>
|
||
<view class="delete-btn" @click.stop="removeDevice(index)" v-if="!device.syncStatus || device.syncStatus !== 'loading'">
|
||
<u-icon name="close" color="#999" size="20"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="selected-empty" v-else>
|
||
<text>暂无已选车辆,请点击上方「选择车辆」添加</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部同步按钮 -->
|
||
<view class="sync-btn" :class="{ disabled: selectedDevices.length === 0 || isSyncing }" @click="syncVoice" v-if="!showDeviceModal">
|
||
<u-icon name="reload" color="#fff" size="24" :class="{ rotating: isSyncing }"></u-icon>
|
||
<text>{{ isSyncing ? '同步中...' : '同步语音' }}</text>
|
||
</view>
|
||
|
||
<!-- 车辆选择弹窗遮罩 -->
|
||
<view class="modal-mask" v-if="showDeviceModal" @tap.stop="closeDeviceModal"></view>
|
||
|
||
<!-- 车辆选择弹窗 -->
|
||
<view class="device-modal" v-if="showDeviceModal" @tap.stop="preventClose">
|
||
<view class="modal-header">
|
||
<text class="modal-title">选择车辆</text>
|
||
<view class="close-btn" @tap="closeDeviceModal">
|
||
<u-icon name="close" color="#999" size="32"></u-icon>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal-content">
|
||
<!-- 搜索框 -->
|
||
<view class="search-box">
|
||
<input
|
||
class="search-input"
|
||
v-model="searchKeyword"
|
||
placeholder="搜索SN或车牌号"
|
||
@input="searchDevices"
|
||
/>
|
||
<u-icon name="search" color="#999" size="20" class="search-icon"></u-icon>
|
||
</view>
|
||
|
||
<!-- 车辆列表:全部展示,点击选中/取消,选中项高亮 -->
|
||
<scroll-view class="modal-device-list" scroll-y :show-scrollbar="true">
|
||
<view
|
||
class="modal-device-item"
|
||
:class="{ 'selected': isDeviceSelected(device) }"
|
||
v-for="(device, index) in filteredDeviceList"
|
||
:key="getDeviceKey(device, index)"
|
||
@click.stop="onSelectDeviceByIndex(index)"
|
||
>
|
||
<view class="device-checkbox">
|
||
<u-icon
|
||
:name="isDeviceSelected(device) ? 'checkbox-mark' : 'checkbox'"
|
||
:color="isDeviceSelected(device) ? '#3996FD' : '#ddd'"
|
||
size="40"
|
||
></u-icon>
|
||
</view>
|
||
<view class="modal-device-info">
|
||
<view class="modal-device-name">SN: {{ device.sn || device.deviceSn || '--' }}</view>
|
||
<view class="modal-device-meta">车牌: {{ device.vehicleNum || '--' }}</view>
|
||
</view>
|
||
</view>
|
||
<view class="empty-tip" v-if="filteredDeviceList.length === 0 && deviceList.length === 0">
|
||
暂无车辆数据
|
||
</view>
|
||
<view class="empty-tip" v-if="filteredDeviceList.length === 0 && deviceList.length > 0">
|
||
未搜索到匹配的车辆
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 已选车辆数量 + 全选复选框(方框+勾,一眼看出是复选框) -->
|
||
<view class="selected-count">
|
||
<text>已选择 {{ tempSelectedDevices.length }} 辆</text>
|
||
<view class="select-all-wrap" @tap.stop="toggleSelectAll" v-if="filteredDeviceList.length > 0">
|
||
<view class="checkbox-box" :class="{ checked: isAllSelected }">
|
||
<text class="checkbox-tick" v-if="isAllSelected">✓</text>
|
||
</view>
|
||
<text class="select-all-label">全选</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal-footer">
|
||
<view class="modal-btn cancel-btn" @tap="closeDeviceModal">取消</view>
|
||
<view class="modal-btn confirm-btn" @tap="confirmSelectDevices">确定</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
bgc: {
|
||
backgroundColor: "#fff",
|
||
},
|
||
voiceId: '',
|
||
voiceName: '',
|
||
areaId: '',
|
||
selectedDevices: [],
|
||
deviceList: [],
|
||
filteredDeviceList: [],
|
||
showDeviceModal: false,
|
||
searchKeyword: '',
|
||
tempSelectedDevices: [],
|
||
tempSelectedKeys: [], // 选中项的 key 列表,用于高亮(用字符串保证响应式)
|
||
isSyncing: false,
|
||
currentSyncIndex: -1
|
||
}
|
||
},
|
||
computed: {
|
||
// 当前列表是否已全选(复选框为 true)
|
||
isAllSelected() {
|
||
const list = this.filteredDeviceList || []
|
||
if (list.length === 0) return false
|
||
return list.every(device => this.isDeviceSelected(device))
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
if (options.voiceId) {
|
||
this.voiceId = options.voiceId
|
||
}
|
||
if (options.voiceName) {
|
||
this.voiceName = decodeURIComponent(options.voiceName)
|
||
}
|
||
// 获取当前用户的运营区ID
|
||
this.getUserArea()
|
||
},
|
||
methods: {
|
||
preventClose() {},
|
||
closeDeviceModal() {
|
||
this.showDeviceModal = false
|
||
},
|
||
// 打开设备选择弹窗
|
||
openDeviceModal() {
|
||
if (!this.areaId) {
|
||
uni.showToast({
|
||
title: '未获取到运营区信息',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
return
|
||
}
|
||
if (!this.deviceList || this.deviceList.length === 0) {
|
||
this.getDeviceList().then(() => {
|
||
this.showDeviceModal = true
|
||
this.initTempSelected()
|
||
}).catch(() => {})
|
||
} else {
|
||
this.showDeviceModal = true
|
||
this.initTempSelected()
|
||
}
|
||
},
|
||
// 打开弹窗时初始化临时选中(与当前已选车辆一致)
|
||
initTempSelected() {
|
||
const list = this.selectedDevices || []
|
||
this.tempSelectedDevices = list.map(d => ({ ...d }))
|
||
this.tempSelectedKeys = list.map(d => this.getDeviceKeyForCompare(d))
|
||
},
|
||
// 获取用户运营区
|
||
getUserArea() {
|
||
// 优先从存储中获取
|
||
if (uni.getStorageSync('adminAreaid')) {
|
||
this.areaId = uni.getStorageSync('adminAreaid')
|
||
this.getDeviceList()
|
||
return
|
||
}
|
||
// 从用户信息中获取
|
||
this.$u.get("/getInfo").then(res => {
|
||
if (res.code == 200 && res.user && res.user.areaId) {
|
||
this.areaId = res.user.areaId
|
||
this.getDeviceList()
|
||
} else {
|
||
uni.showToast({
|
||
title: '未获取到运营区信息',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
}).catch(err => {
|
||
uni.showToast({
|
||
title: '获取运营区信息失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
})
|
||
},
|
||
// 获取设备列表
|
||
getDeviceList() {
|
||
return new Promise((resolve, reject) => {
|
||
if (!this.areaId) {
|
||
uni.showToast({
|
||
title: '未获取到运营区信息',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
reject('未获取到运营区信息')
|
||
return
|
||
}
|
||
|
||
uni.showLoading({
|
||
title: '加载车辆中...',
|
||
mask: true
|
||
})
|
||
|
||
this.$u.get(`/bst/device/all?areaId=${this.areaId}&supportLocation=true`).then((res) => {
|
||
console.log('设备列表接口返回:', res)
|
||
if (res.code == 200) {
|
||
this.deviceList = res.data || []
|
||
this.filteredDeviceList = this.deviceList
|
||
console.log('设备列表加载成功,数量:', this.deviceList.length)
|
||
console.log('设备列表数据:', this.deviceList)
|
||
resolve(this.deviceList)
|
||
} else {
|
||
console.log('获取设备列表失败:', res.msg)
|
||
uni.showToast({
|
||
title: res.msg || '获取车辆列表失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
reject(res.msg || '获取车辆列表失败')
|
||
}
|
||
}).catch(err => {
|
||
console.log('获取车辆列表异常:', err)
|
||
uni.showToast({
|
||
title: '获取车辆列表失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
reject(err)
|
||
}).finally(() => {
|
||
uni.hideLoading()
|
||
})
|
||
})
|
||
},
|
||
// 搜索车辆(按 SN 或 车牌)
|
||
searchDevices() {
|
||
if (!this.searchKeyword) {
|
||
this.filteredDeviceList = this.deviceList
|
||
return
|
||
}
|
||
const keyword = this.searchKeyword.trim().toLowerCase()
|
||
this.filteredDeviceList = this.deviceList.filter(device => {
|
||
const sn = (device.sn || device.deviceSn || '').toLowerCase()
|
||
const plate = (device.vehicleNum || '').toLowerCase()
|
||
const name = (device.deviceName || '').toLowerCase()
|
||
return sn.includes(keyword) || plate.includes(keyword) || name.includes(keyword)
|
||
})
|
||
},
|
||
// 列表项 :key 用(带 index 保证唯一)
|
||
getDeviceKey(device, index) {
|
||
if (!device) return 'dev-' + index
|
||
const k = this.getDeviceKeyForCompare(device)
|
||
return k || 'idx-' + index
|
||
},
|
||
// 设备唯一标识(不含 index),用于选中状态和高亮
|
||
getDeviceKeyForCompare(device) {
|
||
if (!device) return ''
|
||
if (device.id != null && device.id !== '') return 'id-' + device.id
|
||
if (device.deviceId != null && device.deviceId !== '') return 'did-' + device.deviceId
|
||
if (device.sn) return 'sn-' + String(device.sn).trim()
|
||
if (device.deviceSn) return 'dsn-' + String(device.deviceSn).trim()
|
||
return ''
|
||
},
|
||
// 判断是否为同一设备(兼容 id / deviceId / sn / deviceSn)
|
||
isSameDevice(a, b) {
|
||
if (!a || !b) return false
|
||
if (a.id != null && b.id != null && String(a.id) === String(b.id)) return true
|
||
if (a.deviceId != null && b.deviceId != null && String(a.deviceId) === String(b.deviceId)) return true
|
||
if (a.sn && b.sn && String(a.sn).trim() === String(b.sn).trim()) return true
|
||
if (a.deviceSn && b.deviceSn && String(a.deviceSn).trim() === String(b.deviceSn).trim()) return true
|
||
return false
|
||
},
|
||
// 是否已选中(用 key 列表判断,保证高亮一定更新)
|
||
isDeviceSelected(device) {
|
||
const key = this.getDeviceKeyForCompare(device)
|
||
return key ? this.tempSelectedKeys.indexOf(key) !== -1 : false
|
||
},
|
||
// 弹窗内点击:用索引从列表取设备,避免 scroll-view 下 device 闭包为 undefined
|
||
onSelectDeviceByIndex(index) {
|
||
const list = this.filteredDeviceList || []
|
||
const device = list[index]
|
||
if (!device) return
|
||
const key = this.getDeviceKeyForCompare(device)
|
||
if (!key) return
|
||
const keyIdx = this.tempSelectedKeys.indexOf(key)
|
||
if (keyIdx >= 0) {
|
||
this.tempSelectedKeys = this.tempSelectedKeys.filter((_, i) => i !== keyIdx)
|
||
this.tempSelectedDevices = this.tempSelectedDevices.filter((_, i) => i !== keyIdx)
|
||
} else {
|
||
this.tempSelectedKeys = [...this.tempSelectedKeys, key]
|
||
this.tempSelectedDevices = [...this.tempSelectedDevices, { ...device }]
|
||
}
|
||
this.$forceUpdate()
|
||
},
|
||
// 全选/全不选切换:复选框 true 时点击则全不选,false 时点击则全选
|
||
toggleSelectAll() {
|
||
if (this.isAllSelected) {
|
||
this.clearAllDevices()
|
||
} else {
|
||
this.selectAllDevices()
|
||
}
|
||
},
|
||
// 全选当前列表(filteredDeviceList 中未选中的全部加入)
|
||
selectAllDevices() {
|
||
const list = this.filteredDeviceList || []
|
||
const keys = [...this.tempSelectedKeys]
|
||
const devices = [...this.tempSelectedDevices]
|
||
list.forEach(device => {
|
||
const key = this.getDeviceKeyForCompare(device)
|
||
if (!key || keys.indexOf(key) !== -1) return
|
||
keys.push(key)
|
||
devices.push({ ...device })
|
||
})
|
||
this.tempSelectedKeys = keys
|
||
this.tempSelectedDevices = devices
|
||
this.$forceUpdate()
|
||
},
|
||
// 全不选:清空已选
|
||
clearAllDevices() {
|
||
this.tempSelectedKeys = []
|
||
this.tempSelectedDevices = []
|
||
this.$forceUpdate()
|
||
},
|
||
// 确认选择:写入已选列表并关闭弹窗
|
||
confirmSelectDevices() {
|
||
this.selectedDevices = this.tempSelectedDevices.map(d => ({ ...d }))
|
||
this.showDeviceModal = false
|
||
this.searchKeyword = ''
|
||
this.filteredDeviceList = this.deviceList
|
||
this.$forceUpdate()
|
||
},
|
||
// 移除设备
|
||
removeDevice(index) {
|
||
this.selectedDevices.splice(index, 1)
|
||
},
|
||
// 同步语音
|
||
async syncVoice() {
|
||
if (this.selectedDevices.length === 0 || this.isSyncing) {
|
||
return
|
||
}
|
||
|
||
this.isSyncing = true
|
||
this.currentSyncIndex = 0
|
||
|
||
// 重置所有设备的同步状态
|
||
this.selectedDevices.forEach(device => {
|
||
device.syncStatus = null
|
||
device.errorMsg = ''
|
||
})
|
||
|
||
// 逐个同步设备
|
||
for (let i = 0; i < this.selectedDevices.length; i++) {
|
||
this.currentSyncIndex = i
|
||
const device = this.selectedDevices[i]
|
||
|
||
// 设置加载状态
|
||
device.syncStatus = 'loading'
|
||
|
||
try {
|
||
// 构建请求参数
|
||
const params = {
|
||
voiceId: this.voiceId
|
||
}
|
||
|
||
// 只传 sn 或 id(按后端要求)
|
||
if (device.sn) {
|
||
params.sn = device.sn
|
||
} else if (device.deviceSn) {
|
||
params.sn = device.deviceSn
|
||
} else if (device.id != null && device.id !== '') {
|
||
params.id = device.id
|
||
} else if (device.deviceId != null && device.deviceId !== '') {
|
||
params.id = device.deviceId
|
||
} else {
|
||
device.syncStatus = 'failed'
|
||
device.errorMsg = '设备信息不完整'
|
||
continue
|
||
}
|
||
|
||
// 调用同步接口
|
||
const res = await this.$u.put('/bst/device/iot/syncVoice', params)
|
||
|
||
if (res.code == 200) {
|
||
device.syncStatus = 'success'
|
||
device.errorMsg = ''
|
||
} else {
|
||
device.syncStatus = 'failed'
|
||
device.errorMsg = res.msg || '同步失败'
|
||
}
|
||
} catch (error) {
|
||
device.syncStatus = 'failed'
|
||
device.errorMsg = error.message || '请求失败'
|
||
}
|
||
|
||
// 等待一小段时间再同步下一个设备,避免请求过快
|
||
if (i < this.selectedDevices.length - 1) {
|
||
await new Promise(resolve => setTimeout(resolve, 300))
|
||
}
|
||
}
|
||
|
||
this.isSyncing = false
|
||
this.currentSyncIndex = -1
|
||
|
||
// 统计同步结果
|
||
const successCount = this.selectedDevices.filter(d => d.syncStatus === 'success').length
|
||
const failCount = this.selectedDevices.filter(d => d.syncStatus === 'failed').length
|
||
|
||
uni.showToast({
|
||
title: `同步完成:成功${successCount}个,失败${failCount}个`,
|
||
icon: 'none',
|
||
duration: 3000
|
||
})
|
||
},
|
||
// 重试同步
|
||
async retrySync(device, index) {
|
||
if (this.isSyncing) {
|
||
return
|
||
}
|
||
|
||
device.syncStatus = 'loading'
|
||
device.errorMsg = ''
|
||
|
||
try {
|
||
const params = {
|
||
voiceId: this.voiceId
|
||
}
|
||
|
||
// 只传 sn 或 id(按后端要求)
|
||
if (device.sn) {
|
||
params.sn = device.sn
|
||
} else if (device.deviceSn) {
|
||
params.sn = device.deviceSn
|
||
} else if (device.id != null && device.id !== '') {
|
||
params.id = device.id
|
||
} else if (device.deviceId != null && device.deviceId !== '') {
|
||
params.id = device.deviceId
|
||
} else {
|
||
device.syncStatus = 'failed'
|
||
device.errorMsg = '设备信息不完整'
|
||
return
|
||
}
|
||
|
||
const res = await this.$u.put('/bst/device/iot/syncVoice', params)
|
||
|
||
if (res.code == 200) {
|
||
device.syncStatus = 'success'
|
||
device.errorMsg = ''
|
||
uni.showToast({
|
||
title: '同步成功',
|
||
icon: 'success',
|
||
duration: 2000
|
||
})
|
||
} else {
|
||
device.syncStatus = 'failed'
|
||
device.errorMsg = res.msg || '同步失败'
|
||
uni.showToast({
|
||
title: res.msg || '同步失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
} catch (error) {
|
||
device.syncStatus = 'failed'
|
||
device.errorMsg = error.message || '请求失败'
|
||
uni.showToast({
|
||
title: '请求失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
page {
|
||
background: #F7F7F7;
|
||
}
|
||
|
||
.page {
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 20rpx;
|
||
overflow: hidden;
|
||
padding-bottom: 100rpx;
|
||
}
|
||
|
||
.content-container {
|
||
flex: 1;
|
||
}
|
||
|
||
.section {
|
||
background: #fff;
|
||
border-radius: 20rpx;
|
||
padding: 32rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 28rpx;
|
||
bottom: 28rpx;
|
||
width: 6rpx;
|
||
border-radius: 3rpx;
|
||
}
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #262B37;
|
||
margin-bottom: 20rpx;
|
||
padding-left: 8rpx;
|
||
}
|
||
|
||
.select-device-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12rpx;
|
||
height: 88rpx;
|
||
border: 2rpx dashed #3996FD;
|
||
border-radius: 16rpx;
|
||
color: #3996FD;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
background: #F8FBFF;
|
||
|
||
&:active {
|
||
background: #EFF6FF;
|
||
}
|
||
}
|
||
|
||
.device-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.selected-empty {
|
||
padding: 40rpx 24rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.device-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
padding: 20rpx;
|
||
background: #fff;
|
||
border: 2rpx solid #E5E7EB;
|
||
border-radius: 12rpx;
|
||
margin-bottom: 12rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||
position: relative;
|
||
}
|
||
|
||
.device-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.device-name {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #262B37;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.device-meta {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.device-error {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: #ff4444;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.device-status {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 12rpx;
|
||
min-width: 120rpx;
|
||
/* 预留右上角“重试”按钮空间,避免与状态文案重叠 */
|
||
padding-top: 56rpx;
|
||
}
|
||
|
||
.status-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 24rpx;
|
||
|
||
&.success {
|
||
color: #64B6A7;
|
||
}
|
||
|
||
&.loading {
|
||
color: #3996FD;
|
||
}
|
||
|
||
&.failed {
|
||
color: #ff4444;
|
||
}
|
||
}
|
||
|
||
.error-msg {
|
||
font-size: 22rpx;
|
||
color: #ff4444;
|
||
max-width: 200rpx;
|
||
text-align: right;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.retry-btn {
|
||
position: absolute;
|
||
top: 18rpx;
|
||
right: 18rpx;
|
||
padding: 8rpx 18rpx;
|
||
background: #3996FD;
|
||
color: #fff;
|
||
border-radius: 6rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1;
|
||
}
|
||
.retry-btn.disabled {
|
||
background: #C7CDD3;
|
||
}
|
||
|
||
.delete-btn {
|
||
padding: 8rpx;
|
||
}
|
||
|
||
.sync-btn {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 100rpx;
|
||
background: linear-gradient(135deg, #3996FD 0%, #64B6A7 100%);
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12rpx;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
z-index: 100;
|
||
box-shadow: 0 -4rpx 20rpx rgba(57, 150, 253, 0.25);
|
||
|
||
&.disabled {
|
||
background: #ccc;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.rotating {
|
||
animation: rotate 1s linear infinite;
|
||
}
|
||
}
|
||
|
||
@keyframes rotate {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.modal-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
z-index: 9998;
|
||
}
|
||
|
||
.device-modal {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: #fff;
|
||
border-radius: 24rpx 24rpx 0 0;
|
||
max-height: 80vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
z-index: 9999;
|
||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 30rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: #262B37;
|
||
}
|
||
|
||
.close-btn {
|
||
padding: 8rpx;
|
||
}
|
||
|
||
.modal-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
|
||
.search-box {
|
||
position: relative;
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.search-input {
|
||
width: 630rpx;
|
||
height: 70rpx;
|
||
padding: 0 60rpx 0 20rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.search-icon {
|
||
position: absolute;
|
||
right: 40rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.modal-device-list {
|
||
// flex: 1;
|
||
// min-height: 0;
|
||
// overflow-y: auto;
|
||
max-height: 54vh;
|
||
overflow: scroll;
|
||
}
|
||
|
||
.modal-device-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 24rpx;
|
||
margin: 0 20rpx;
|
||
margin-bottom: 12rpx;
|
||
border-radius: 12rpx;
|
||
border: 2rpx solid #E5E7EB;
|
||
background: #fff;
|
||
transition: all 0.2s;
|
||
|
||
&:active {
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
&.selected {
|
||
background: #E3F2FD;
|
||
border-color: #3996FD;
|
||
border-width: 3rpx;
|
||
box-shadow: 0 0 0 2rpx rgba(57, 150, 253, 0.2);
|
||
|
||
.modal-device-name {
|
||
color: #1976D2;
|
||
font-weight: 700;
|
||
}
|
||
.modal-device-meta {
|
||
color: #3996FD;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.device-checkbox {
|
||
margin-right: 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.modal-device-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.modal-device-name {
|
||
font-size: 30rpx;
|
||
color: #262B37;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.modal-device-meta {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.empty-tip {
|
||
text-align: center;
|
||
padding: 40rpx;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.selected-count {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx 24rpx;
|
||
font-size: 28rpx;
|
||
color: #3996FD;
|
||
font-weight: 600;
|
||
background: #EFF6FF;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
}
|
||
.select-all-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
.checkbox-box {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border: 2rpx solid #ddd;
|
||
border-radius: 6rpx;
|
||
background: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.checkbox-box.checked {
|
||
background: #3996FD;
|
||
border-color: #3996FD;
|
||
}
|
||
.checkbox-tick {
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
line-height: 1;
|
||
}
|
||
.select-all-label {
|
||
font-size: 28rpx;
|
||
color: #3996FD;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
padding: 30rpx;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.modal-btn {
|
||
flex: 1;
|
||
height: 80rpx;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.cancel-btn {
|
||
background: #f5f5f5;
|
||
color: #666;
|
||
}
|
||
|
||
.confirm-btn {
|
||
background: linear-gradient(135deg, #3996FD 0%, #64B6A7 100%);
|
||
color: #fff;
|
||
box-shadow: 0 4rpx 12rpx rgba(57, 150, 253, 0.25);
|
||
}
|
||
</style> |