smart-home/smart-home-app/pages/device/add.vue
2026-02-26 09:16:34 +08:00

1205 lines
36 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.

<template>
<view class="container">
<!-- 步骤指示器 -->
<view class="steps">
<view :class="['step', step >= 1 ? 'active' : '']">
<view class="step-num">1</view>
<text class="step-text">{{$t('stepSelectType')}}</text>
</view>
<view class="step-line"></view>
<view :class="['step', step >= 2 ? 'active' : '']">
<view class="step-num">2</view>
<text class="step-text">{{addType === 'host' ? $t('stepConfigHost') : $t('stepPairDevice')}}</text>
</view>
<view class="step-line"></view>
<view :class="['step', step >= 3 ? 'active' : '']">
<view class="step-num">3</view>
<text class="step-text">{{$t('stepDone')}}</text>
</view>
</view>
<!-- 步骤1选择设备类型 -->
<view class="step-content" v-if="step === 1">
<text class="section-title">{{$t('selectDeviceType')}}</text>
<view class="type-list">
<view class="type-item" @click="selectType('host')">
<view class="type-icon host">📡</view>
<view class="type-info">
<text class="type-title">{{$t('typeEsp32Host')}}</text>
<text class="type-desc">{{$t('typeEsp32HostDesc')}}</text>
</view>
<text class="type-arrow">></text>
</view>
<view class="type-item" @click="selectType('ac')">
<view class="type-icon ac">❄️</view>
<view class="type-info">
<text class="type-title">{{$t('typeAirConditioner')}}</text>
<text class="type-desc">{{$t('typeAirConditionerDesc')}}</text>
</view>
<text class="type-arrow">></text>
</view>
<view class="type-item" @click="selectType('ir-device')">
<view class="type-icon ir">💡</view>
<view class="type-info">
<text class="type-title">{{$t('typeIrDevice')}}</text>
<text class="type-desc">{{$t('typeIrDeviceDesc')}}</text>
</view>
<text class="type-arrow">></text>
</view>
<view class="type-item" @click="selectType('rf433')">
<view class="type-icon rf">📻</view>
<view class="type-info">
<text class="type-title">{{$t('typeRf433Device')}}</text>
<text class="type-desc">{{$t('typeRf433DeviceDesc')}}</text>
</view>
<text class="type-arrow">></text>
</view>
<view class="type-item" @click="selectType('gas-valve')">
<view class="type-icon gas">🔥</view>
<view class="type-info">
<text class="type-title">{{$t('typeGasValve')}}</text>
<text class="type-desc">{{$t('typeGasValveDesc')}}</text>
</view>
<text class="type-arrow">></text>
</view>
</view>
</view>
<!-- 步骤2配置 -->
<view class="step-content" v-if="step === 2">
<!-- 主机配置 -->
<view class="host-config" v-if="addType === 'host'">
<text class="section-title">{{$t('addEsp32Host')}}</text>
<view class="method-list">
<view :class="['method-item', method === 'scan' ? 'active' : '']" @click="method = 'scan'; startScan()">
<view class="method-icon">📡</view>
<view class="method-info">
<text class="method-title">{{$t('lanScan')}}</text>
<text class="method-desc">{{$t('lanScanDesc')}}</text>
</view>
</view>
<view :class="['method-item', method === 'manual' ? 'active' : '']" @click="method = 'manual'">
<view class="method-icon">✏️</view>
<view class="method-info">
<text class="method-title">{{$t('manualInput')}}</text>
<text class="method-desc">{{$t('manualInputDesc')}}</text>
</view>
</view>
</view>
<view class="scan-result" v-if="method === 'scan'">
<view class="scanning" v-if="scanning">
<view class="scan-animation">📡</view>
<text class="scan-text">{{$t('scanningDevices')}}</text>
</view>
<view class="found-devices" v-else-if="foundDevices.length > 0">
<text class="sub-title">{{$t('foundDevices')}}</text>
<view class="device-item" v-for="d in foundDevices" :key="d.id" @click="selectDevice(d)">
<view class="device-icon">📡</view>
<view class="device-info">
<text class="device-name">{{d.name}}</text>
<text class="device-id">IP: {{d.ip}}</text>
</view>
<view :class="['select-circle', selectedDevice && selectedDevice.id === d.id ? 'selected' : '']"></view>
</view>
</view>
</view>
<view class="manual-input" v-if="method === 'manual'">
<view class="input-group">
<text class="input-label">{{$t('hostIpAddress')}}</text>
<input class="input" v-model="hostIP" :placeholder="$t('ipExample')" />
</view>
</view>
</view>
<!-- 红外设备 - 选择设备子类型 -->
<view class="ir-device-select" v-if="addType === 'ir-device' && !irSubType">
<text class="section-title">{{$t('irSelectTypeTitle')}}</text>
<view class="ir-type-grid">
<view class="ir-type-item" @click="selectIrSubType('light')">
<view class="ir-type-icon">💡</view>
<text class="ir-type-name">{{$t('irLight')}}</text>
</view>
<view class="ir-type-item" @click="selectIrSubType('power-strip')">
<view class="ir-type-icon">🔌</view>
<text class="ir-type-name">{{$t('irPowerStrip')}}</text>
</view>
<view class="ir-type-item" @click="selectIrSubType('fan')">
<view class="ir-type-icon">🌀</view>
<text class="ir-type-name">{{$t('irFan')}}</text>
</view>
<view class="ir-type-item" @click="selectIrSubType('other')">
<view class="ir-type-icon">📱</view>
<text class="ir-type-name">{{$t('irOther')}}</text>
</view>
</view>
<view class="btn-group">
<view class="btn-secondary" @click="step = 1; irSubType = ''">
<text>{{$t('previous')}}</text>
</view>
</view>
</view>
<!-- 红外设备 - 选择品牌 -->
<view class="ir-brand-select" v-else-if="addType === 'ir-device' && irSubType && !selectedBrand">
<text class="section-title">{{$t('irSelectBrandPrefix')}}{{getIrSubTypeName(irSubType)}}{{$t('irSelectBrandSuffix')}}</text>
<view class="brand-list">
<view
class="brand-item"
v-for="brand in getIrBrands(irSubType)"
:key="brand.id"
@click="selectBrand(brand)"
>
<text class="brand-name">{{brand.name}}</text>
<text class="brand-codes">{{brand.codeCount}}{{$t('codeLibraries')}}</text>
</view>
</view>
<view class="btn-group">
<view class="btn-secondary" @click="irSubType = ''">
<text>{{$t('previous')}}</text>
</view>
</view>
</view>
<!-- 红外设备 - 配对和配置 -->
<view class="ir-pair-config" v-else-if="addType === 'ir-device' && irSubType && selectedBrand && !irPaired">
<text class="section-title">{{$t('pairPrefix')}}{{selectedBrand.name}}{{getIrSubTypeName(irSubType)}}</text>
<!-- 选择主机 -->
<view class="input-group">
<text class="input-label">{{$t('selectControlHost')}}</text>
<view class="host-list">
<view
:class="['host-item', selectedHost && selectedHost.id === h.id ? 'selected' : '']"
v-for="h in availableHosts"
:key="h.id"
@click="selectedHost = h"
>
<text class="host-icon">📡</text>
<text class="host-name">{{h.name}}</text>
</view>
</view>
<text class="input-hint" v-if="availableHosts.length === 0">{{$t('pleaseAddEsp32HostFirst')}}</text>
</view>
<!-- 手动逐个码库配对 -->
<view class="pair-section">
<text class="pair-title">{{$t('irCodePairing')}}</text>
<!-- 配对进度 -->
<view class="pair-progress-info">
<text class="progress-text">{{$t('currentCode')}}: {{currentCodeIndex}} / {{selectedBrand.codeCount}}</text>
<view class="progress-bar">
<view class="progress-fill" :style="{width: pairProgress + '%'}"></view>
</view>
</view>
<!-- 配对状态 -->
<view class="pair-status-area" v-if="irPairState === 'idle' || irPairState === 'testing'">
<text class="pair-hint">{{$t('pairHint')}}</text>
<view class="code-info" v-if="currentCodeIndex > 0">
<text class="code-label">{{$t('testing')}}: {{selectedBrand.name}}_{{String(currentCodeIndex).padStart(3, '0')}}</text>
</view>
<view class="pair-action-btns">
<view class="btn-primary send-btn" @click="sendCurrentCode">
<text>📡 {{$t('sendCurrentCode')}}</text>
</view>
</view>
<view class="pair-result-btns">
<view class="btn-success" @click="onCodeMatched">
<text>✅ {{$t('deviceResponded')}}</text>
</view>
<view class="btn-secondary" @click="tryNextCode">
<text>❌ {{$t('noResponseNext')}}</text>
</view>
</view>
</view>
<!-- 配对成功 -->
<view class="pair-status success" v-else-if="irPairState === 'success'">
<text class="pair-icon">✅</text>
<text class="pair-result">{{$t('pairingSuccess')}}</text>
<text class="pair-code">{{$t('matchedCode')}}: {{selectedBrand.name}}_{{String(matchedCodeIndex).padStart(3, '0')}}</text>
</view>
</view>
<view class="btn-group" v-if="irPairState === 'success'">
<view class="btn-secondary" @click="resetIrPair">
<text>{{$t('rePair')}}</text>
</view>
<view class="btn-primary" @click="irPaired = true">
<text>{{$t('next')}}</text>
</view>
</view>
<view class="btn-group" v-else>
<view class="btn-secondary" @click="selectedBrand = null; resetIrPair()">
<text>{{$t('previous')}}</text>
</view>
</view>
</view>
<!-- 红外设备 - 最终配置 -->
<view class="ir-final-config" v-else-if="addType === 'ir-device' && irPaired">
<text class="section-title">{{$t('completeDeviceInfo')}}</text>
<view class="input-group">
<text class="input-label">{{$t('deviceName')}}</text>
<input class="input" v-model="deviceName" :placeholder="$t('irDeviceNameExamplePrefix') + getIrSubTypeName(irSubType)" />
</view>
<view class="input-group">
<text class="input-label">{{$t('room')}}</text>
<picker :range="rooms" @change="roomChange">
<view class="picker-value">{{selectedRoom || $t('chooseRoom')}}</view>
</picker>
</view>
<view class="device-summary">
<view class="summary-row">
<text class="summary-label">{{$t('deviceType')}}</text>
<text class="summary-value">{{getIrSubTypeName(irSubType)}}</text>
</view>
<view class="summary-row">
<text class="summary-label">{{$t('brand')}}</text>
<text class="summary-value">{{selectedBrand.name}}</text>
</view>
<view class="summary-row">
<text class="summary-label">{{$t('controlHost')}}</text>
<text class="summary-value">{{selectedHost ? selectedHost.name : '-'}}</text>
</view>
<view class="summary-row">
<text class="summary-label">{{$t('pairStatus')}}</text>
<text class="summary-value paired">{{$t('paired')}}</text>
</view>
</view>
<view class="btn-group">
<view class="btn-secondary" @click="irPaired = false">
<text>{{$t('previous')}}</text>
</view>
<view class="btn-primary" @click="goToStep3">
<text>{{$t('addDeviceTitle')}}</text>
</view>
</view>
</view>
<!-- 其他子设备配置 - 需要选择主机 -->
<view class="sub-device-config" v-else-if="addType !== 'host' && addType !== 'ir-device'">
<text class="section-title">{{$t('addPrefix')}}{{getTypeName(addType)}}</text>
<!-- 选择主机 -->
<view class="input-group">
<text class="input-label">{{$t('selectControlHost')}}</text>
<view class="host-list">
<view
:class="['host-item', selectedHost && selectedHost.id === h.id ? 'selected' : '']"
v-for="h in availableHosts"
:key="h.id"
@click="selectedHost = h"
>
<text class="host-icon">📡</text>
<text class="host-name">{{h.name}}</text>
</view>
</view>
<text class="input-hint" v-if="availableHosts.length === 0">{{$t('pleaseAddEsp32HostFirst')}}</text>
</view>
<!-- 设备名称 -->
<view class="input-group">
<text class="input-label">{{$t('deviceName')}}</text>
<input class="input" v-model="deviceName" :placeholder="$t('deviceNameExampleAc')" />
</view>
<!-- 所在房间 -->
<view class="input-group">
<text class="input-label">{{$t('room')}}</text>
<picker :range="rooms" @change="roomChange">
<view class="picker-value">{{selectedRoom || $t('chooseRoom')}}</view>
</picker>
</view>
<!-- 空调需要配对 -->
<view class="pair-notice" v-if="addType === 'ac'">
<text class="notice-icon">💡</text>
<text class="notice-text">{{$t('acNeedPairNotice')}}</text>
</view>
<view class="btn-group">
<view class="btn-secondary" @click="step = 1">
<text>{{$t('previous')}}</text>
</view>
<view class="btn-primary" @click="goToStep3">
<text>{{addType === 'ac' ? $t('next') : $t('addDeviceTitle')}}</text>
</view>
</view>
</view>
<!-- 主机配置的按钮 -->
<view class="btn-group" v-if="addType === 'host'">
<view class="btn-secondary" @click="step = 1">
<text>{{$t('previous')}}</text>
</view>
<view class="btn-primary" @click="goToStep3">
<text>{{$t('connectHost')}}</text>
</view>
</view>
</view>
<!-- 步骤3完成 -->
<view class="step-content" v-if="step === 3">
<view class="success-icon">✅</view>
<text class="success-title">{{addType === 'host' ? $t('hostAddedSuccess') : $t('deviceAddedSuccess')}}</text>
<view class="result-info">
<view class="info-row">
<text class="info-label">{{$t('deviceType')}}</text>
<text class="info-value">{{getTypeName(addType)}}</text>
</view>
<view class="info-row">
<text class="info-label">{{$t('deviceName')}}</text>
<text class="info-value">{{deviceName}}</text>
</view>
<view class="info-row" v-if="addType !== 'host'">
<text class="info-label">{{$t('room')}}</text>
<text class="info-value">{{selectedRoom}}</text>
</view>
<view class="info-row" v-if="addType !== 'host'">
<text class="info-label">{{$t('controlHost')}}</text>
<text class="info-value">{{selectedHost ? selectedHost.name : '-'}}</text>
</view>
<view class="info-row" v-if="addType === 'ac'">
<text class="info-label">{{$t('pairStatus')}}</text>
<text class="info-value unpaired">{{$t('unpaired')}}</text>
</view>
</view>
<!-- 空调需要配对 -->
<view class="pair-action" v-if="addType === 'ac'">
<text class="pair-hint">{{$t('acNeedPairHint')}}</text>
<view class="btn-success" @click="goToPair">
<text>{{$t('pairNow')}}</text>
</view>
</view>
<view class="btn-group">
<view class="btn-secondary" @click="addAnother">
<text>{{$t('continueAdd')}}</text>
</view>
<view class="btn-primary" @click="finishAdd">
<text>{{$t('finish')}}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { getHostDevices, addDevice, updateDevice, getAllRooms, getTypeName, getTypeIcon } from '@/utils/deviceStore.js'
import { $t } from '@/utils/simpleI18n.js'
import { fetchEsp32MacInfoByIp, isValidPhysicalUid } from '@/utils/esp32MacApi.js'
import { getESP32IP } from '@/utils/deviceConfig.js'
export default {
data() {
return {
step: 1,
addType: '', // host, ac, ir-device, rf433, gas-valve
method: '',
scanning: false,
foundDevices: [],
selectedDevice: null,
hostIP: '',
deviceName: '',
rooms: [],
selectedRoom: '',
selectedHost: null,
newDeviceId: '',
// 可用主机列表
availableHosts: [],
// 红外设备相关
irSubType: '', // light, power-strip, fan, other
selectedBrand: null,
irPaired: false,
irPairState: 'idle', // idle, pairing, success
currentCodeIndex: 0,
matchedCodeIndex: 0,
pairTimer: null,
// 红外设备品牌库
irBrands: {
'light': [
{ id: 1, name: '通用红外灯', codeCount: 20 },
{ id: 2, name: '欧普照明', codeCount: 35 },
{ id: 3, name: '雷士照明', codeCount: 28 },
{ id: 4, name: '飞利浦', codeCount: 42 },
{ id: 5, name: '松下', codeCount: 38 },
{ id: 6, name: '其他品牌', codeCount: 50 }
],
'power-strip': [
{ id: 1, name: '国际电工', codeCount: 15 },
{ id: 2, name: '公牛', codeCount: 12 },
{ id: 3, name: '小米', codeCount: 18 },
{ id: 4, name: '通用遥控插排', codeCount: 25 }
],
'fan': [
{ id: 1, name: '美的', codeCount: 45 },
{ id: 2, name: '格力', codeCount: 38 },
{ id: 3, name: '艾美特', codeCount: 32 },
{ id: 4, name: '通用风扇', codeCount: 50 }
],
'other': [
{ id: 1, name: '通用红外设备', codeCount: 30 }
]
}
}
},
computed: {
pairProgress() {
if (!this.selectedBrand || this.selectedBrand.codeCount === 0) return 0
return Math.round((this.currentCodeIndex / this.selectedBrand.codeCount) * 100)
}
},
onLoad() {
// 加载所有可用的主机设备和房间
this.loadAvailableHosts()
this.loadRooms()
},
onUnload() {
// 页面卸载时停止配对
this.stopIrPair()
},
methods: {
$t(key) {
return $t(key)
},
getESP32IP() {
return getESP32IP() || '192.168.1.98' // 提供备用IP
},
selectType(type) {
this.addType = type
// 如果是空调类型,直接跳转到配对页面
if (type === 'ac') {
const deviceName = '客厅空调'
const esp32IP = this.getESP32IP()
uni.navigateTo({
url: `/pages/control/ac-pair?deviceId=&deviceName=${encodeURIComponent(deviceName)}&esp32IP=${encodeURIComponent(esp32IP)}`
})
return
}
// 其他类型继续到步骤2
this.step = 2
this.loadAvailableHosts()
},
loadAvailableHosts() {
this.selectedRoom = ''
this.selectedHost = null
try {
const hosts = getHostDevices()
this.availableHosts = Array.isArray(hosts) ? hosts : []
} catch (e) {
this.availableHosts = []
}
// 重置红外设备相关状态
this.irSubType = ''
this.selectedBrand = null
this.irPaired = false
this.irPairState = 'idle'
this.currentCodeIndex = 0
this.matchedCodeIndex = 0
},
loadRooms() {
try {
const rs = getAllRooms()
this.rooms = Array.isArray(rs) ? rs : []
} catch (e) {
this.rooms = []
}
if (!this.selectedRoom && this.rooms && this.rooms.length) {
this.selectedRoom = this.rooms[0]
}
},
// 红外设备子类型选择
selectIrSubType(subType) {
this.irSubType = subType
},
getIrSubTypeName(subType) {
const keys = {
'light': 'irLight',
'power-strip': 'irPowerStrip',
'fan': 'irFan',
'other': 'irOther'
}
return this.$t(keys[subType] || subType)
},
getIrSubTypeIcon(subType) {
const icons = {
'light': '💡',
'power-strip': '🔌',
'fan': '🌀',
'other': '📱'
}
return icons[subType] || '📱'
},
getIrBrands(subType) {
return this.irBrands[subType] || []
},
selectBrand(brand) {
this.selectedBrand = brand
},
// 红外配对相关方法
startIrPair() {
if (!this.selectedHost) {
uni.showToast({ title: this.$t('pleaseSelectControlHost'), icon: 'none' })
return
}
this.irPairState = 'pairing'
this.currentCodeIndex = 0
this.continuousSendIr()
},
continuousSendIr() {
if (this.irPairState !== 'pairing') return
if (this.currentCodeIndex >= this.selectedBrand.codeCount) {
// 所有码库发送完毕
this.irPairState = 'idle'
uni.showToast({ title: this.$t('noMatchedCodeRetry'), icon: 'none' })
return
}
this.currentCodeIndex++
// 实际应调用ESP32接口发送红外信号
// uni.request({ url: `http://${host_ip}/ir/send`, ... })
this.pairTimer = setTimeout(() => {
this.continuousSendIr()
}, 800)
},
onDeviceResponded() {
this.stopIrPair()
this.matchedCodeIndex = this.currentCodeIndex
this.irPairState = 'success'
},
stopIrPair() {
if (this.pairTimer) {
clearTimeout(this.pairTimer)
this.pairTimer = null
}
if (this.irPairState === 'pairing') {
this.irPairState = 'idle'
}
},
resetIrPair() {
this.irPairState = 'idle'
this.currentCodeIndex = 0
this.matchedCodeIndex = 0
},
// 手动发送当前码库
sendCurrentCode() {
if (!this.selectedHost) {
uni.showToast({ title: this.$t('pleaseSelectControlHost'), icon: 'none' })
return
}
// 如果是第一次点击从1开始
if (this.currentCodeIndex === 0) {
this.currentCodeIndex = 1
}
this.irPairState = 'testing'
const codeId = `${this.selectedBrand.name}_${String(this.currentCodeIndex).padStart(3, '0')}`
console.log(`发送码库: ${codeId}`)
uni.showToast({ title: this.$t('sendingCode') + ' ' + this.currentCodeIndex, icon: 'none', duration: 1000 })
// 实际应调用ESP32接口发送红外信号
// uni.request({
// url: `http://${this.selectedHost.ip}/ir/send`,
// method: 'POST',
// data: { brand: this.selectedBrand.name, codeIndex: this.currentCodeIndex, command: 'power' }
// })
},
// 尝试下一个码库
tryNextCode() {
if (this.currentCodeIndex >= this.selectedBrand.codeCount) {
uni.showModal({
title: this.$t('tip'),
content: this.$t('testedAllNoMatchRestart'),
success: (res) => {
if (res.confirm) {
this.currentCodeIndex = 0
this.irPairState = 'idle'
}
}
})
return
}
this.currentCodeIndex++
this.sendCurrentCode()
},
// 码库匹配成功
onCodeMatched() {
if (this.currentCodeIndex === 0) {
uni.showToast({ title: this.$t('pleaseSendCodeFirst'), icon: 'none' })
return
}
this.matchedCodeIndex = this.currentCodeIndex
this.irPairState = 'success'
uni.showToast({ title: this.$t('pairingSuccess'), icon: 'success' })
},
getTypeName(type) {
return getTypeName(type)
},
startScan() {
this.scanning = true
setTimeout(() => {
this.scanning = false
this.foundDevices = [
{ id: 'ESP32_003', name: '智能主机_003', ip: '192.168.1.102' },
{ id: 'ESP32_004', name: '智能主机_004', ip: '192.168.1.103' }
]
}, 2000)
},
selectDevice(device) {
this.selectedDevice = device
this.deviceName = device.name
},
goToStep3() {
// 验证
if (this.addType === 'host') {
if (this.method === 'scan' && !this.selectedDevice) {
uni.showToast({ title: this.$t('pleaseSelectDevice'), icon: 'none' })
return
}
if (this.method === 'manual' && !this.hostIP) {
uni.showToast({ title: this.$t('pleaseEnterIp'), icon: 'none' })
return
}
if (!this.method) {
uni.showToast({ title: this.$t('pleaseSelectAddMethod'), icon: 'none' })
return
}
if (!this.deviceName) {
this.deviceName = this.method === 'scan' ? this.selectedDevice.name : this.$t('newHost')
}
} else {
if (!this.selectedHost) {
uni.showToast({ title: this.$t('pleaseSelectControlHost'), icon: 'none' })
return
}
if (!this.deviceName) {
uni.showToast({ title: this.$t('pleaseEnterDeviceName'), icon: 'none' })
return
}
if (!this.selectedRoom) {
uni.showToast({ title: this.$t('pleaseSelectRoom'), icon: 'none' })
return
}
}
// 真正添加设备到存储
uni.showLoading({ title: this.$t('adding') })
setTimeout(() => {
try {
let newDevice = null
if (this.addType === 'host') {
// 添加主机设备
newDevice = addDevice({
name: this.deviceName,
room: '客厅',
type: 'host',
typeName: 'ESP32主机',
icon: getTypeIcon('host'),
status: 'online',
ip: this.method === 'scan' ? this.selectedDevice.ip : this.hostIP,
temp: 25,
hasHuman: false,
signal: 80
})
// 后台拉取 ESP32 MAC 并写入 physicalUid不阻塞添加流程
try {
const ip = newDevice && newDevice.ip ? String(newDevice.ip) : ''
if (ip) {
fetchEsp32MacInfoByIp(ip, 5000).then(info => {
if (info && info.physicalUid && isValidPhysicalUid(info.physicalUid)) {
updateDevice(newDevice.id, {
physicalUid: info.physicalUid,
mac: info.macAddress,
deviceId: info.deviceId
})
}
}).catch(e => {
console.error('获取ESP32 MAC失败:', e)
})
}
} catch (e) {
// ignore
}
} else if (this.addType === 'ir-device') {
// 添加红外设备
newDevice = addDevice({
name: this.deviceName,
room: this.selectedRoom,
type: 'ir-switch',
typeName: this.getIrSubTypeName(this.irSubType),
subType: this.irSubType,
icon: this.getIrSubTypeIcon(this.irSubType),
status: 'online',
hostId: this.selectedHost.id,
temp: 0,
hasHuman: false,
signal: 0,
paired: true,
brand: this.selectedBrand.name,
codeIndex: this.matchedCodeIndex
})
} else {
// 添加其他子设备
newDevice = addDevice({
name: this.deviceName,
room: this.selectedRoom,
type: this.addType,
typeName: getTypeName(this.addType),
icon: getTypeIcon(this.addType),
status: 'online',
hostId: this.selectedHost.id,
temp: this.addType === 'ac' ? 26 : 0,
hasHuman: false,
signal: 0,
paired: this.addType !== 'ac',
brand: ''
})
}
this.newDeviceId = newDevice.id
uni.hideLoading()
this.step = 3
} catch (e) {
uni.hideLoading()
uni.showToast({ title: this.$t('addFailed'), icon: 'none' })
}
}, 1000)
},
roomChange(e) {
this.selectedRoom = this.rooms[e.detail.value]
},
finishAdd() {
uni.showToast({ title: this.$t('addSuccess'), icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
},
addAnother() {
this.step = 1
this.addType = ''
this.method = ''
this.deviceName = ''
this.selectedRoom = ''
this.selectedHost = null
this.selectedDevice = null
},
goToPair() {
// 跳转到配对页面
uni.navigateTo({
url: '/pages/control/ac-pair?deviceId=' + this.newDeviceId + '&deviceName=' + encodeURIComponent(this.deviceName)
})
}
}
}
</script>
<style>
.container {
min-height: 100vh;
background: #f5f5f5;
}
.steps {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 30rpx;
background: #fff;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
}
.step-num {
width: 48rpx;
height: 48rpx;
line-height: 48rpx;
text-align: center;
border-radius: 50%;
background: #ddd;
color: #fff;
font-size: 24rpx;
}
.step.active .step-num {
background: #3498DB;
}
.step-text {
font-size: 24rpx;
color: #999;
margin-top: 12rpx;
}
.step.active .step-text {
color: #3498DB;
}
.step-line {
width: 80rpx;
height: 4rpx;
background: #ddd;
margin: 0 20rpx;
margin-bottom: 36rpx;
}
.step-content {
padding: 30rpx;
}
.method-list {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.method-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.method-item:last-child {
border-bottom: none;
}
.method-icon {
font-size: 48rpx;
margin-right: 24rpx;
}
.method-info {
flex: 1;
}
.method-title {
font-size: 32rpx;
color: #333;
display: block;
}
.method-desc {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
.method-arrow {
font-size: 32rpx;
color: #ccc;
}
.scanning {
text-align: center;
padding: 80rpx 0;
}
.scan-animation {
font-size: 80rpx;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
.scan-text {
font-size: 28rpx;
color: #666;
margin-top: 30rpx;
display: block;
}
.section-title {
font-size: 28rpx;
color: #999;
margin-bottom: 20rpx;
display: block;
}
.device-item {
display: flex;
align-items: center;
padding: 24rpx;
background: #fff;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
.device-icon {
font-size: 40rpx;
margin-right: 20rpx;
}
.device-info {
flex: 1;
}
.device-name {
font-size: 30rpx;
color: #333;
display: block;
}
.device-id {
font-size: 24rpx;
color: #999;
display: block;
}
.select-circle {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #ddd;
border-radius: 50%;
}
.select-circle.selected {
border-color: #3498DB;
background: #3498DB;
}
.input-group {
margin-bottom: 30rpx;
}
.input-label {
font-size: 28rpx;
color: #666;
margin-bottom: 16rpx;
display: block;
}
.input {
width: 100%;
height: 88rpx;
background: #fff;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 30rpx;
box-sizing: border-box;
}
.scan-qr {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
background: #fff;
border-radius: 12rpx;
margin-bottom: 30rpx;
}
.qr-icon {
font-size: 40rpx;
margin-right: 16rpx;
}
.qr-text {
font-size: 30rpx;
color: #3498DB;
}
.wifi-config {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-top: 30rpx;
}
.btn-group {
display: flex;
gap: 20rpx;
margin-top: 40rpx;
}
.btn-primary, .btn-secondary {
flex: 1;
height: 88rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary {
background: linear-gradient(135deg, #3498DB, #2980B9);
}
.btn-primary text {
color: #fff;
font-size: 30rpx;
}
.btn-secondary {
background: #f5f5f5;
}
.btn-secondary text {
color: #666;
font-size: 30rpx;
}
.btn-primary.full {
margin-top: 40rpx;
}
.success-icon {
font-size: 120rpx;
text-align: center;
display: block;
margin: 40rpx 0;
}
.success-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
display: block;
margin-bottom: 40rpx;
}
.device-config {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
}
.picker-value {
height: 88rpx;
line-height: 88rpx;
background: #f5f5f5;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 30rpx;
color: #333;
}
.scene-select {
display: flex;
gap: 20rpx;
}
.scene-btn {
flex: 1;
padding: 24rpx;
background: #f5f5f5;
border-radius: 12rpx;
text-align: center;
border: 4rpx solid transparent;
}
.scene-btn.active {
border-color: #3498DB;
background: rgba(52, 152, 219, 0.1);
}
.scene-btn text {
display: block;
font-size: 30rpx;
color: #333;
}
.scene-desc {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
/* 新增样式 */
.type-list { background: #fff; border-radius: 16rpx; overflow: hidden; }
.type-item { display: flex; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
.type-item:last-child { border-bottom: none; }
.type-icon { width: 80rpx; height: 80rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 40rpx; margin-right: 24rpx; }
.type-icon.host { background: rgba(52,152,219,0.1); }
.type-icon.ac { background: rgba(52,152,219,0.1); }
.type-icon.ir { background: rgba(241,196,15,0.1); }
.type-icon.rf { background: rgba(155,89,182,0.1); }
.type-icon.gas { background: rgba(231,76,60,0.1); }
.type-info { flex: 1; }
.type-title { font-size: 32rpx; color: #333; display: block; }
.type-desc { font-size: 24rpx; color: #999; margin-top: 8rpx; display: block; }
.type-arrow { font-size: 32rpx; color: #ccc; }
.method-item.active { background: rgba(52,152,219,0.05); border-left: 6rpx solid #3498DB; }
.sub-title { font-size: 26rpx; color: #666; margin: 20rpx 0; display: block; }
.host-list { display: flex; flex-wrap: wrap; gap: 16rpx; }
.host-item { display: flex; align-items: center; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; border: 2rpx solid transparent; }
.host-item.selected { border-color: #3498DB; background: rgba(52,152,219,0.1); }
.host-icon { font-size: 32rpx; margin-right: 12rpx; }
.host-name { font-size: 28rpx; color: #333; }
.input-hint { font-size: 24rpx; color: #E74C3C; margin-top: 12rpx; display: block; }
.pair-notice { display: flex; align-items: center; background: rgba(241,196,15,0.1); padding: 20rpx; border-radius: 12rpx; margin-top: 20rpx; }
.notice-icon { font-size: 32rpx; margin-right: 12rpx; }
.notice-text { font-size: 26rpx; color: #F39C12; }
.result-info { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; }
.info-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #f0f0f0; }
.info-row:last-child { border-bottom: none; }
.info-label { font-size: 28rpx; color: #666; }
.info-value { font-size: 28rpx; color: #333; }
.info-value.unpaired { color: #E74C3C; }
.pair-action { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; text-align: center; }
.pair-hint { font-size: 26rpx; color: #666; margin-bottom: 20rpx; display: block; }
.btn-success { background: #27AE60; height: 88rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.btn-success text { color: #fff; font-size: 30rpx; }
/* 红外设备类型选择 - 2列网格 */
.ir-type-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin-bottom: 30rpx;
}
.ir-type-item {
background: #fff;
border-radius: 16rpx;
padding: 40rpx 20rpx;
text-align: center;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.05);
}
.ir-type-item:active { background: rgba(52,152,219,0.1); }
.ir-type-icon { font-size: 56rpx; display: block; margin-bottom: 12rpx; }
.ir-type-name { font-size: 28rpx; color: #333; font-weight: 500; }
/* 品牌选择 */
.brand-list { background: #fff; border-radius: 16rpx; overflow: hidden; }
.brand-item { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
.brand-item:last-child { border-bottom: none; }
.brand-item:active { background: #f5f5f5; }
.brand-name { font-size: 32rpx; color: #333; }
.brand-codes { font-size: 24rpx; color: #999; }
/* 配对区域 */
.pair-section { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-top: 20rpx; }
.pair-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; display: block; }
.pair-status { text-align: center; padding: 30rpx 0; }
.pair-status.success { background: rgba(39,174,96,0.1); border-radius: 12rpx; padding: 30rpx; }
.pair-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
.pair-result { font-size: 32rpx; font-weight: bold; color: #27AE60; display: block; }
.pair-code { font-size: 24rpx; color: #666; margin-top: 12rpx; display: block; }
/* 配对进度 */
.pair-progress-info { margin-bottom: 20rpx; }
.progress-text { font-size: 26rpx; color: #666; display: block; margin-bottom: 12rpx; }
/* 配对状态区域 */
.pair-status-area { text-align: center; }
.pair-hint { font-size: 26rpx; color: #666; display: block; margin-bottom: 16rpx; }
.code-info { background: #f0f7ff; border-radius: 8rpx; padding: 16rpx; margin-bottom: 20rpx; }
.code-label { font-size: 28rpx; color: #3498DB; font-weight: 500; }
/* 配对操作按钮 */
.pair-action-btns { margin-bottom: 24rpx; }
.send-btn { background: #3498DB !important; }
.pair-result-btns { display: flex; gap: 16rpx; }
.pair-result-btns .btn-success, .pair-result-btns .btn-secondary { flex: 1; height: 80rpx; }
/* 进度条 */
.progress-bar { width: 100%; height: 12rpx; background: #f0f0f0; border-radius: 6rpx; margin: 20rpx 0; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #3498DB, #27AE60); border-radius: 6rpx; transition: width 0.3s; }
/* 设备摘要 */
.device-summary { background: #f5f5f5; border-radius: 12rpx; padding: 20rpx; margin-top: 20rpx; }
.summary-row { display: flex; justify-content: space-between; padding: 12rpx 0; }
.summary-label { font-size: 26rpx; color: #666; }
.summary-value { font-size: 26rpx; color: #333; }
.summary-value.paired { color: #27AE60; }
/* 手动配对确认 */
.pair-manual { text-align: left; }
.pair-desc { font-size: 28rpx; color: #333; display: block; margin-bottom: 20rpx; }
.pair-tips { background: #f8f9fa; border-radius: 12rpx; padding: 20rpx; margin-bottom: 30rpx; }
.tip-item { font-size: 26rpx; color: #666; display: block; padding: 8rpx 0; }
.pair-confirm-btns { margin-top: 20rpx; }
</style>