1205 lines
36 KiB
Vue
1205 lines
36 KiB
Vue
|
|
<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>
|