简介:这是一套基于UniApp框架开发的Vue智能居家页面设计源码,面向前端初学者与智能家居应用开发者,解决跨端智能界面快速搭建与UI组件复用问题。资源共83个文件,含15个Vue组件(如tab-bar、top-menu、gdPicker等)、20个JS文件(涵盖MQTT通信、AES/RSA加解密、HTTP请求封装等核心逻辑)、27个PNG素材及11个JSON配置(pages.json、manifest.json等),辅以SCSS全局样式与Markdown文档说明,压缩包仅4.75MB,轻量易上手。已有229人学习下载,适合用于理解UniApp多端适配机制、智能家居交互流程与模块化组件设计实践。读者可直接运行index.html或main.js启动项目,参考readme.txt部署,复用popup、time-range、temp-range等成熟UI组件,并基于smh-、rhliu-等命名空间的uni_modules快速集成设备控制与状态展示功能。
1. 这不是普通 Vue 页面——UniApp 构建的智能居家界面,天生跨端、可直连 IoT 设备、能离线渲染温控/灯光/安防状态
当你在微信小程序里滑动调节空调温度,在安卓 App 中点击开关窗帘,又在 H5 页面上实时查看门锁开合记录——这些操作背后,很可能跑着同一套代码。UniApp 不是“Vue 的小程序版”,而是以 Vue 语法为基底、通过一套源码编译出 iOS/Android/微信小程序/支付宝小程序/H5 五端的跨平台框架。本项目标题中的“基于 UniApp 的 Vue 智能居家页面设计源码”,核心价值不在“页面美观”,而在于用标准 Vue 语法声明式绑定真实家居设备状态,并通过 UniApp 提供的原生能力桥接蓝牙/WiFi/HTTP/MQTT 等协议通道。它面向的是智能家居厂商前端工程师、IoT 项目集成商、以及需要快速交付多端控制面板的嵌入式系统配套团队。新手可直接运行调试 UI 交互逻辑;有经验者会重点关注uni.getConnectedWifi()、uni.connectSocket()、uni.openBluetoothAdapter()等 API 在不同端的兼容性边界与降级策略——比如 iOS 微信小程序不支持蓝牙直连,但可通过“附近设备”扫码跳转原生 App 完成配网;而 Android App 则必须处理蓝牙权限动态申请与后台保活问题。这不是玩具 Demo,而是生产级设备控制面板的最小可行实现。
2. 从 Vue 单文件组件到五端可运行:UniApp 工程结构与智能居家模块拆解
2.1 为什么选 UniApp 而非纯 Vue 或 React Native?三类典型场景下的技术权衡
在智能居家场景中,设备控制端需同时满足:① 用户已在微信生态内(小程序入口最轻);② 高频操作需原生性能(如滑动调光、实时视频流 overlay);③ 企业需统一管理设备数据(H5 后台运维页)。纯 Vue CLI 项目仅能输出 Web,无法调用蓝牙或获取 WiFi 列表;React Native 虽支持原生模块,但 iOS/Android 双端需分别维护桥接代码,且小程序端完全不可行。UniApp 的核心优势在于其编译时抽象层:.vue文件中写的uni.scanCode()在微信小程序中编译为wx.scanCode(),在 App 中编译为plus.barcode.scan(),在 H5 中则 fallback 为navigator.mediaDevices.getUserMedia()+ canvas 解码。这种“一次编写、多端编译”的能力,使本项目能复用同一套设备状态管理逻辑(如store/modules/device.js),仅需在api/目录下按平台提供适配器:
// api/device-adapter.js export const getDeviceStatus = () => { // 微信小程序:调用云函数查询设备影子 if (process.env.UNI_PLATFORM === 'mp-weixin') { return uniCloud.callFunction({ name: 'getDeviceShadow', data: { deviceId: 'light-001' } }) } // App 端:直连本地 MQTT Broker(如 Mosquitto) if (process.env.UNI_PLATFORM === 'app') { return new Promise((resolve) => { const client = mqtt.connect('mqtt://192.168.1.100:1883') client.on('connect', () => { client.subscribe('device/light-001/status') client.on('message', (topic, payload) => { resolve(JSON.parse(payload.toString())) }) }) }) } // H5:通过 REST API 查询设备服务 return fetch('/api/v1/devices/light-001/status').then(r => r.json()) }提示:
process.env.UNI_PLATFORM是 UniApp 编译期注入的环境变量,值为'h5'、'mp-weixin'、'app'等,不可在运行时动态修改。所有平台特有逻辑必须在此变量判断下分支,否则 H5 端可能因调用uni.connectSocket()报错中断。
2.2 智能居家页面的核心组件分层:从设备卡片到状态同步管道
本源码的页面结构严格遵循“状态驱动 UI”原则,分为三层:
- 视图层(View):
pages/index/index.vue中的<device-card>组件,接收device对象作为 prop,内部用v-if控制空调/灯光/安防等不同设备模板; - 状态层(Store):
store/modules/device.js使用 Vuex 模块化管理设备列表、当前选中设备、连接状态,关键 action 如syncDeviceStatus每 3 秒轮询或监听 WebSocket; - 通信层(API):
api/目录下按协议划分,mqtt.js封装 MQTT 连接与 topic 订阅,ble.js封装蓝牙设备发现与特征值读写,http.js统一处理 REST 请求拦截与 token 刷新。
下面是一个典型的设备卡片组件实现,重点展示如何响应式同步设备状态:
<!-- components/device-card.vue --> <template> <view class="device-card" @click="toggleDevice"> <text class="device-name">{{ device.name }}</text> <view class="status-indicator" :class="{ active: device.power }"></view> <!-- 温控设备显示滑块 --> <slider v-if="device.type === 'thermostat'" :value="device.temperature" @changing="onTempChange" @change="onTempConfirm" min="16" max="30" step="0.5" /> <!-- 灯光设备显示色温选择 --> <picker v-else-if="device.type === 'light'" mode="selector" :range="['暖光', '白光', '彩光']" @change="onLightModeChange" > <view class="light-mode">{{ device.mode }}</view> </picker> </view> </template> <script> export default { name: 'DeviceCard', props: { device: { type: Object, required: true, // 注意:此处 device 是响应式对象,来自 store.state.devices[0] // 修改 device.power 会触发视图更新,但不会自动同步到硬件 } }, methods: { toggleDevice() { // 触发 store action,而非直接修改 prop this.$store.dispatch('device/togglePower', this.device.id) }, onTempChange(e) { // 滑动中预览,不立即发送指令 this.$emit('tempPreview', e.detail.value) }, onTempConfirm(e) { // 松手后确认,调用 API 发送 MQTT 指令 this.$store.dispatch('device/setTemperature', { id: this.device.id, temperature: e.detail.value }) } } } </script>2.2.1 关键参数说明:v-model在 UniApp 中的特殊处理
Vue 原生v-model在 UniApp 中对input、textarea等基础组件有效,但对slider、picker等平台原生组件不生效。必须显式使用@change事件并手动触发this.$emit('update:xxx')才能实现双向绑定。例如slider组件需配合:value和@change实现受控模式,否则在 iOS App 中可能出现滑块位置与实际值不同步的问题。
2.2.2 设备状态同步的三种模式对比表
| 同步方式 | 适用场景 | 实现要点 | 典型延迟 |
|---|---|---|---|
| 轮询 HTTP | H5 后台管理页、低频设备(如门锁) | setInterval(() => api.getDevice(), 5000) | 3–8s |
| WebSocket | App/小程序实时监控(如摄像头在线状态) | uni.connectSocket()+uni.onSocketMessage() | <500ms |
| MQTT 订阅 | App 端直连本地设备(推荐) | client.subscribe('device/+/status'),通配符匹配所有设备 | <100ms |
注意:微信小程序不支持原生 MQTT,必须通过云函数中转;而 App 端若使用
uni-app内置的uni.connectSocket(),需确保服务端 WebSocket 协议与 MQTT over WS 兼容,否则需改用uni-app插件市场中的mqtt-plus插件。
3. 设备连接与状态驱动:蓝牙配网、WiFi 列表扫描与 MQTT 指令下发实战
3.1 微信小程序端实现“一键配网”:扫码 + WiFi 列表 + AP 模式切换全流程
智能设备首次联网(如新买的智能灯泡),用户需将其接入家庭 WiFi。UniApp 在小程序端通过三步完成:
- 扫码进入配网页:调用
uni.scanCode({ onlyFromCamera: true })获取设备序列号(SN); - 获取当前 WiFi 名称与密码:
uni.getConnectedWifi()返回 SSID,但无法直接获取密码,需引导用户手动输入; - 向设备发送配网指令:设备处于 AP 模式时会广播一个热点(如
SmartBulb-XXXX),小程序通过uni.connectWifi()连接该热点,再uni.request()向http://192.168.4.1/configPOST WiFi 凭据。
完整代码如下:
// pages/configure/configure.vue export default { data() { return { ssid: '', password: '', deviceSn: '' } }, methods: { async startConfig() { try { // 步骤1:扫码获取设备 SN const res = await uni.scanCode() this.deviceSn = res.result // 步骤2:获取当前连接的 WiFi 名称(iOS/Android 均支持) const wifi = await uni.getConnectedWifi() this.ssid = wifi.SSID // 步骤3:提示用户输入密码(小程序无法读取) uni.showModal({ title: '请输入家庭 WiFi 密码', showCancel: false, success: () => { this.triggerConfig() } }) } catch (e) { uni.showToast({ title: '配网失败:' + e.message, icon: 'none' }) } }, async triggerConfig() { try { // 连接设备 AP 热点(需提前在 manifest.json 中配置 wifi permissions) await uni.connectWifi({ SSID: `SmartBulb-${this.deviceSn.substring(0,4)}`, password: '' // AP 模式热点通常无密码 }) // 向设备配置接口发送 WiFi 凭据 const response = await uni.request({ url: 'http://192.168.4.1/config', method: 'POST', data: { ssid: this.ssid, password: this.password, sn: this.deviceSn } }) if (response.data.code === 0) { uni.showToast({ title: '配网成功!设备将重启连接家庭网络' }) } } catch (e) { uni.showToast({ title: '配网失败:' + e.message, icon: 'none' }) } } } }3.1.1 manifest.json 必配项说明(微信小程序)
在manifest.json的"mp-weixin"节点下,必须声明以下权限,否则uni.connectWifi()会静默失败:
{ "mp-weixin": { "usingComponents": true, "permission": { "scope.userLocation": { "desc": "用于获取当前位置以便搜索附近设备" }, "scope.writeContacts": { "desc": "用于保存设备配网记录" } } } }注意:
uni.getConnectedWifi()在 iOS 微信 8.0.30+ 才支持,旧版本需降级为“手动输入 WiFi 名称”。
3.2 App 端直连蓝牙设备:发现、连接、读写特征值全链路
当设备已接入家庭网络后,App 端可绕过云端,通过蓝牙直连进行低延迟控制(如调节 RGB 灯颜色)。UniApp 提供uni.openBluetoothAdapter()系列 API,但需严格遵循平台差异:
- Android:需动态申请
android.permission.BLUETOOTH_ADMIN和android.permission.ACCESS_FINE_LOCATION(蓝牙扫描需定位权限); - iOS:
info.plist中必须添加NSBluetoothAlwaysUsageDescription描述,且仅支持 BLE(Bluetooth Low Energy)。
下面是一个完整的蓝牙设备控制流程:
// utils/ble-controller.js export class BLEController { constructor() { this.deviceId = null this.serviceId = null this.characteristicId = null } async init() { try { await uni.openBluetoothAdapter() await uni.startBluetoothDiscovery({ services: ['0000FF00-0000-1000-8000-00805F9B34FB'] }) const devices = await uni.getConnectedBluetoothDevices() if (devices.length > 0) { this.deviceId = devices[0].deviceId await this.connectDevice() } } catch (e) { console.error('蓝牙初始化失败', e) } } async connectDevice() { await uni.createBLEConnection({ deviceId: this.deviceId }) const services = await uni.getBLEDeviceServices({ deviceId: this.deviceId }) this.serviceId = services.services.find(s => s.uuid.includes('FF00')).uuid const chars = await uni.getBLEDeviceCharacteristics({ deviceId: this.deviceId, serviceId: this.serviceId }) this.characteristicId = chars.characteristics.find(c => c.properties.write).uuid } async sendCommand(command) { // command 示例:{ type: 'color', value: [255, 128, 0] } const buffer = new ArrayBuffer(4) const view = new DataView(buffer) view.setUint8(0, command.type === 'color' ? 0x01 : 0x02) command.value.forEach((v, i) => view.setUint8(i + 1, v)) await uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.characteristicId, value: buffer }) } }3.2.1 蓝牙特征值写入的字节序陷阱
上述sendCommand中,new DataView(buffer)默认使用大端序(Big Endian),但多数 BLE 设备固件期望小端序(Little Endian)解析 RGB 值。若发现颜色设置异常,需改为:
// 小端序写入 RGB view.setUint8(1, command.value[0]) // R view.setUint8(2, command.value[1]) // G view.setUint8(3, command.value[2]) // B4. 生产环境关键配置:条件编译、多端样式隔离与 MQTT 连接保活策略
4.1 用条件编译解决“同一份 CSS 在小程序和 App 中渲染错位”问题
UniApp 支持/* #ifdef MP-WEIXIN */等条件编译区块,但CSS 中不能直接写#ifdef。正确做法是在style标签内用@import引入平台专属样式:
<!-- pages/index/index.vue --> <style> @import './index.common.css'; /* 通用样式 */ /* #ifdef MP-WEIXIN */ @import './index.mp.css'; /* 小程序特有样式 */ /* #endif */ /* #ifdef APP-PLUS */ @import './index.app.css'; /* App 特有样式 */ /* #endif */ </style>index.mp.css中可覆盖小程序限制:
/* index.mp.css */ .device-card { /* 小程序不支持 flex: 1,改用固定高度 */ height: 120px; } .status-indicator { /* 小程序中 border-radius 大于 height 时失效,改用 background-image 模拟圆点 */ background-image: radial-gradient(circle, #4CAF50, #2E7D32); width: 20px; height: 20px; }4.1.1 条件编译常用平台标识对照表
| 标识符 | 对应平台 | 典型用途 |
|---|---|---|
MP-WEIXIN | 微信小程序 | 调用wx.login()、wx.chooseAddress() |
APP-PLUS | 5+ App | 调用plus.bluetooth.*、plus.network.* |
H5 | H5 页面 | 使用fetch、localStorage |
MP-ALIPAY | 支付宝小程序 | 调用my.scan()、my.getNetworkType() |
注意:
/* #ifndef H5 */表示“除 H5 外所有平台”,常用于屏蔽 H5 不支持的 API 调用。
4.2 MQTT 连接保活:心跳包、断线重连与离线消息缓存
在 App 端直连 MQTT 时,网络抖动会导致连接中断。UniApp 本身不提供 MQTT 自动重连,需自行实现:
// utils/mqtt-client.js export class MQTTClient { constructor(options) { this.options = options this.client = null this.reconnectTimer = null this.messageQueue = [] // 离线期间的待发消息 } connect() { this.client = mqtt.connect(this.options.url, { username: this.options.username, password: this.options.password, keepalive: 60, // 心跳间隔(秒) reconnectPeriod: 1000 // 首次重连延迟(毫秒) }) this.client.on('connect', () => { console.log('MQTT connected') this.flushQueue() // 连接成功后发送缓存消息 this.startHeartbeat() }) this.client.on('reconnect', () => { console.log('MQTT reconnecting...') }) this.client.on('error', (err) => { console.error('MQTT error', err) this.scheduleReconnect() }) } scheduleReconnect() { if (this.reconnectTimer) clearTimeout(this.reconnectTimer) this.reconnectTimer = setTimeout(() => { this.connect() }, 5000) // 指数退避可在此处增强 } publish(topic, payload) { if (this.client && this.client.connected) { this.client.publish(topic, JSON.stringify(payload)) } else { this.messageQueue.push({ topic, payload }) } } flushQueue() { this.messageQueue.forEach(msg => this.publish(msg.topic, msg.payload)) this.messageQueue = [] } }4.2.1 心跳包与 QoS 级别选择建议
keepalive: 60表示客户端每 60 秒向 Broker 发送一次心跳(PINGREQ),Broker 若 1.5 倍时间未收到则断开连接;- 指令类消息(如“打开灯光”)应设
qos: 1,确保至少送达一次; - 状态上报类消息(如“温度 26.5℃”)可设
qos: 0,避免重复推送造成 UI 闪烁。
5. 验证与调试技巧:用真机日志定位“小程序能连 WiFi 但 App 蓝牙找不到设备”类问题
5.1 三端日志统一收集:在main.js中注入全局错误处理器
UniApp 的console.log在不同端输出位置不同:H5 显示在浏览器控制台,小程序在开发者工具 Console,App 则需通过uni.getProvider()获取原生日志。统一方案是在main.js中重写console方法:
// main.js if (process.env.UNI_PLATFORM === 'app') { const originalLog = console.log console.log = function(...args) { // 同时输出到原生日志和 H5 控制台(便于调试) plus.nativeObj.toast({ content: args.map(String).join(' '), duration: 'short' }) originalLog.apply(console, args) } } // 全局错误捕获 window.addEventListener('error', (e) => { uni.reportAnalytics('js_error', { message: e.message, filename: e.filename, lineno: e.lineno }) })5.2 蓝牙调试黄金组合:uni.getConnectedBluetoothDevices()+uni.getBLEDeviceServices()+uni.getBLEDeviceCharacteristics()
当 App 端“扫描不到设备”时,按此顺序执行诊断:
确认蓝牙适配器已开启:
const adapter = await uni.getBluetoothAdapterState() if (!adapter.available) throw new Error('蓝牙未开启')检查是否已连接设备(避免重复连接):
const connected = await uni.getConnectedBluetoothDevices() console.log('已连接设备:', connected) // 若有结果,直接走 connectDevice 流程扫描后未发现设备?强制刷新扫描缓存:
await uni.stopBluetoothDiscovery() await uni.startBluetoothDiscovery() // 等待 3 秒后再次 getConnectedBluetoothDevices()发现设备但无法获取服务?检查 UUID 大小写与格式:
// 错误写法(小写 uuid 无法匹配) await uni.getBLEDeviceServices({ deviceId, services: ['ff00'] }) // 正确写法(全大写 + 标准格式) await uni.getBLEDeviceServices({ deviceId, services: ['0000FF00-0000-1000-8000-00805F9B34FB'] })
提示:iOS 设备对蓝牙服务 UUID 校验极严,必须与固件广播的 UUID 完全一致(包括连字符位置),而 Android 相对宽松。调试时优先用 LightBlue(iOS)或 nRF Connect(Android)验证设备广播内容。
5.3 小程序配网失败的三个高频原因及对应检查命令
| 现象 | 可能原因 | 检查命令/方法 |
|---|---|---|
| 扫码后无反应 | 设备 SN 格式不匹配正则 | console.log(res.result)查看扫码返回值是否含SN:前缀 |
| 连接 AP 热点失败 | 设备未进入 AP 模式 | 用手机 WiFi 列表手动搜索SmartBulb-XXXX是否存在 |
| POST 配网请求超时 | 设备 IP 地址错误 | ping 192.168.4.1确认设备是否响应 ICMP |
最后一步:在uni.request()中添加timeout: 10000参数,并捕获statusCode: 0错误——这表示网络层未建立连接,大概率是设备未响应或 IP 错误。
本文还有配套的精品资源,点击获取