smart-home/smart-home-app/pages/device/add.vue

1205 lines
36 KiB
Vue
Raw Normal View History

2026-02-26 09:16:34 +08:00
<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>