uniapp连接低功耗蓝牙设备(微信小程序同样适用)

一、环境说明

  • uni-app
  • Vue3
  • 开发工具:HBuilder X 3.4.7
  • 以安卓App的方式运行(iOS和小程序同理)

uni-app 提供的低功耗蓝牙 的 api ,和微信小程序提供的api是一样的,所以本文的讲解也适用于微信小程序。

二、低功耗蓝牙设备的特点

蓝牙收发数据的逻辑和我们常用的 AJAX 进行的网络请求是有一丢丢不同的。

其中较大的区别是: 蓝牙接收数据不是那么的稳定,相比起网络请求,蓝牙更容易出现丢包的情况。

在开发中,AJAX 发起的请求不管成功还是失败,浏览器基本都会给你一个答复。但uni-app提供的 api 来看,蓝牙接收数据会显得更加“异步”。

三、思路

  • 初始化:打开蓝牙模块
  • 搜寻:检测附近存在的设备
  • 连接:找到目标设备进行
  • 监听:开启监听功能,接收其他设备传过来的数据
  • 发送指令:不管发送数据还是读取数据,都可以理解为向外发送指令

上面整理出使用蓝牙传输数据的5大动作,但每个动作其实都是由 uni-app 提供的一个或者多个 api 组合而成。

3.1. 初始化阶段

使用蓝牙之前,需要初始化蓝牙模块,这是最最最开始就要做的!

使用 uni.openBluetoothAdapter 这个 api 就可以初始化蓝牙模块。其他蓝牙相关 API 必须在 uni.openBluetoothAdapter 调用之后使用。否则API会返回错误( errCode=10000 )。

错误代码可以查阅 《错误码文档》

<template>
	<view>
		<button @click="initBlue">初始化蓝牙</button>
	</view>
</template>

<script setup>
// 【1】初始化蓝牙
function initBlue() {
	uni.openBluetoothAdapter({
		success(res) {
			console.log('初始化蓝牙成功');
			console.log(res);
		},
		fail(err) {
			console.log('初始化蓝牙失败');
			console.error(err);
		}
	});
}
</script>

如果你手机开启了蓝牙,点击页面上的按钮后,控制台就会输出如下内容

初始化蓝牙成功
{"errMsg":"openBluetoothAdapter:ok"}

如果手机没开启蓝牙,就会返回如下内容

初始化蓝牙失败
{"errMsg":"openBluetoothAdapter:fail not available","code":10001}
  • 根据文档提示,10001代表当前蓝牙适配器不可用。
  • 如果你的控制台能打印出 {“errMsg”:“openBluetoothAdapter:ok”} 证明第一步已经成功了。

3.2. 搜寻附近设备

这一步需要2个api 配合完成。所以可以分解成以下2步:

  • 开启搜寻功能:uni.startBluetoothDevicesDiscovery
  • 监听搜寻到新设备:uni.onBluetoothDeviceFound

开发蓝牙相关功能时,操作逻辑更像是推送,所以“开启搜索”和“监听新设备”是分开操作的。

uni.startBluetoothDevicesDiscovery 可以让设备开始搜索附近蓝牙设备,但这个方法比较耗费系统资源,建议在连接到设备之后就使用 uni.stopBluetoothDevicesDiscovery 停止继续搜索。

uni.startBluetoothDevicesDiscovery 方法里可以传入一个对象OBJECT,该对象接收几个参数,但初学的话我们只关注 success fail。某些蓝牙设备会广播自己的主 service uuid。如果设置此参数,则只搜索广播包有对应 uuid 的主服务的蓝牙设备。建议主要通过该参数过滤掉周边不需要处理的其他蓝牙设备。

OBJECT 参数说明

在使用 uni.startBluetoothDevicesDiscovery (开始搜索)后,可以使用uni.onBluetoothDeviceFound监听搜寻到新设备,这个方法里面接收一个回调函数。

  • 代码示例
<template>
	<view>
		<scroll-view scroll-y class="box">
			<view class="item" v-for="item in blueDeviceList">
				<view>
					<text>id: {{ item.deviceId }}</text>
				</view>
				<view>
					<text>name: {{ item.name }}</text>
				</view>
			</view>
		</scroll-view>

		<button @click="initBlue">初始化蓝牙</button>

		<button @click="discovery">搜索附近蓝牙设备</button>
	</view>
</template>

<script setup>
import { ref } from 'vue';

// 搜索到的蓝牙设备列表
const blueDeviceList = ref([]);

// 【1】初始化蓝牙
function initBlue() {
	uni.openBluetoothAdapter({
		success(res) {
			console.log('初始化蓝牙成功');
			console.log(res);
		},
		fail(err) {
			console.log('初始化蓝牙失败');
			console.error(err);
		}
	});
}

// 【2】开始搜寻附近设备
function discovery() {
	uni.startBluetoothDevicesDiscovery({
		success(res) {
			console.log('开始搜索');

			// 开启监听回调
			uni.onBluetoothDeviceFound(found);
		},
		fail(err) {
			console.log('搜索失败');
			console.error(err);
		}
	});
}

// 【3】找到新设备就触发该方法
function found(res) {
	console.log(res);
	blueDeviceList.value.push(res.devices[0]);
}
</script>

<style>
.box {
	width: 100%;
	height: 400rpx;
	box-sizing: border-box;
	margin-bottom: 20rpx;
	border: 2px solid dodgerblue;
}
.item {
	box-sizing: border-box;
	padding: 10rpx;
	border-bottom: 1px solid #ccc;
}
button {
	margin-bottom: 20rpx;
}
</style>

上面代码的逻辑是,如果开启 “寻找附近设备” 功能成功,接着就开启 “监听寻找到新设备的事件” 。

搜索到的设备会返回以下数据:

{
    "devices": [{
        "deviceId": "B4:10:7B:C4:83:14",
        "name": "蓝牙设备名",
        "RSSI": -58,
        "localName": "",
        "advertisServiceUUIDs": ["0000FFF0-0000-1000-8000-00805F9B34FB"],
        "advertisData": {}
    }]
}

每监听到一个新的设备,我都会将其添加到蓝牙设备列表(blueDeviceList) 里,最后讲这个列表的数据渲染到页面上。

在这里插入图片描述

3.3. 连接目标设备

连接目标设备只需要1个 api 就能完成。但根据文档提示,我们连接后还需要关闭 “搜索附近设备” 的功能,这个很好理解,既然找到了,再继续找就是浪费资源。

流程如下:

  • 获取设备ID:根据 uni.onBluetoothDeviceFound 回调,拿到设备ID
  • 连接设备:使用设备ID进行连接 uni.createBLEConnection
  • 停止搜索:uni.stopBluetoothDevicesDiscovery

我给每条搜索到的蓝牙结果添加一个click 事件,会向目标设备发送连接请求。

  • 代码示例
<template>
	<view>
		<scroll-view scroll-y class="box">
			<view class="item" v-for="item in blueDeviceList" @click="connect(item)">
				<view>
					<text>id: {{ item.deviceId }}</text>
				</view>
				<view>
					<text>name: {{ item.name }}</text>
				</view>
			</view>
		</scroll-view>

		<button @click="initBlue">初始化蓝牙</button>

		<button @click="discovery">搜索附近蓝牙设备</button>
	</view>
</template>

<script setup>
import { ref } from 'vue';

// 搜索到的蓝牙设备列表
const blueDeviceList = ref([]);

// 【1】初始化蓝牙
function initBlue() {
	uni.openBluetoothAdapter({
		success(res) {
			console.log('初始化蓝牙成功');
			console.log(res);
		},
		fail(err) {
			console.log('初始化蓝牙失败');
			console.error(err);
		}
	});
}

// 【2】开始搜寻附近设备
function discovery() {
	uni.startBluetoothDevicesDiscovery({
		success(res) {
			console.log('开始搜索');
			// 开启监听回调
			uni.onBluetoothDeviceFound(found);
		},
		fail(err) {
			console.log('搜索失败');
			console.error(err);
		}
	});
}

// 【3】找到新设备就触发该方法
function found(res) {
	console.log(res);
	blueDeviceList.value.push(res.devices[0]);
}

// 蓝牙设备的id
const deviceId = ref('');

// 【4】连接设备
function connect(data) {
	console.log(data);

	deviceId.value = data.deviceId;

	uni.createBLEConnection({
		deviceId: deviceId.value,
		success(res) {
			console.log('连接成功');
			console.log(res);
			// 停止搜索
			stopDiscovery();
		},
		fail(err) {
			console.log('连接失败');
			console.error(err);
		}
	});
}

// 【5】停止搜索
function stopDiscovery() {
	uni.stopBluetoothDevicesDiscovery({
		success(res) {
			console.log('停止成功');
			console.log(res);
		},
		fail(err) {
			console.log('停止失败');
			console.error(err);
		}
	});
}
</script>

<style>
.box {
	width: 100%;
	height: 400rpx;
	box-sizing: border-box;
	margin-bottom: 20rpx;
	border: 2px solid dodgerblue;
}
.item {
	box-sizing: border-box;
	padding: 10rpx;
	border-bottom: 1px solid #ccc;
}
button {
	margin-bottom: 20rpx;
}
</style>

连接成功后在控制台会输出

连接成功
{"errMsg":"createBLEConnection:ok"}

在连接成功后就立刻调用 uni.stopBluetoothDevicesDiscovery方法停止继续搜索附近其他设备,停止成功后会输出

停止成功
{"errMsg":"stopBluetoothDevicesDiscovery:ok"}

连接成功后,设备的指示灯会亮起

3.4. 监听

在连接完设备后,就要先开启监听数据的功能。这样才能接收到发送读写指令后设备给你回调的信息。

要开启监听,首先需要知道蓝牙设备提供了那些服务,然后通过服务获取特征值,特征值会告诉你哪个可读,哪个可写。最后根据特征值进行消息监听

步骤如下:

  • 获取蓝牙设备服务:uni.getBLEDeviceServices
  • 获取特征值:uni.getBLEDeviceCharacteristics
  • 开启消息监听:uni.notifyBLECharacteristicValueChange
  • 接收消息监听传来的数据:uni.onBLECharacteristicValueChange

正常情况下,硬件佬会提前把蓝牙设备的指定服务还有特征值告诉你。

3.4.1. 第一步,获取蓝牙服务

<template>
	<view>
		<!-- 省略上一步的代码 -->
		<button @click="getServices">获取蓝牙服务</button>
	</view>
</template>

<script setup>
import { ref } from 'vue';

// 省略上一步的代码……

// 【6】获取服务
function getServices() {
	uni.getBLEDeviceServices({
		deviceId: deviceId.value, // 设备ID,在上一步【4】里获取
		success(res) {
			console.log(res);
		},
		fail(err) {
			console.error(err);
		}
	});
}
</script>

此时点击按钮,将会获取到已连接设备的所有服务。

{
    "services": [{
        "uuid": "00001800-0000-1000-8000-00805F9B34FB",
        "isPrimary": true
    }, {
        "uuid": "00001801-0000-1000-8000-00805F9B34FB",
        "isPrimary": true
    }, {
        "uuid": "0000180A-0000-1000-8000-00805F9B34FB",
        "isPrimary": true
    }, {
        "uuid": "0000FFF0-0000-1000-8000-00805F9B34FB",
        "isPrimary": true
    }, {
        "uuid": "0000FFE0-0000-1000-8000-00805F9B34FB",
        "isPrimary": true
    }],
    "errMsg": "getBLEDeviceServices:ok"
}

3.4.1. 获取指定服务的特征值

获取特征值,需要传设备ID服务ID

<template>
	<view>
		<!-- 省略前面几步代码 -->
		<button @click="getCharacteristics">获取特征值</button>
	</view>
</template>

<script setup>
import { ref } from 'vue';

// 省略前面几步代码

// 【7】获取特征值
function getCharacteristics() {
	uni.getBLEDeviceCharacteristics({
		deviceId: deviceId.value, // 设备ID,在【4】里获取到
		serviceId: '0000FFE0-0000-1000-8000-00805F9B34FB', // 服务UUID,在【6】里能获取到
		success(res) {
			console.log(res);
		},
		fail(err) {
			console.error(err);
		}
	});
}
</script>

最后成功输出

{
    "characteristics": [{
        "uuid": "0000FFE1-0000-1000-8000-00805F9B34FB",
        "properties": {
            "read": true,
            "write": true,
            "notify": true,
            "indicate": false
        }
    }],
    "errMsg": "getBLEDeviceCharacteristics:ok"
}

characteristics 字段里保存了该服务的所有特征值,我的设备这个服务只有1个特征值,并且读、写、消息推送都为 true。

你的设备可能不止一条特征值,需要监听那条特征值这需要你和硬件佬协商的(通常也是硬件佬直接和你说要监听哪条)。

3.4.3. 第三、四步,开启消息监听 并 接收消息监听传来的数据

根据已经拿到的 设备ID、服务ID、特征值,就可以开启对应的监听功能。

使用uni.notifyBLECharacteristicValueChange 开启消息监听;

并在 uni.onBLECharacteristicValueChange 方法触发监听到的消息。

<template>
	<view>
		<!-- 省略前面几步代码 -->
		<button @click="notify">开启消息监听</button>
	</view>
</template>

<script setup>
import { ref } from 'vue';

// 省略前面几步代码

// 【8】开启消息监听
function notify() {
	uni.notifyBLECharacteristicValueChange({
		deviceId: deviceId.value, // 设备ID,在【4】里获取到
		serviceId: '0000FFE0-0000-1000-8000-00805F9B34FB', // 服务UUID,在【6】里能获取到
		characteristicId: '0000FFE1-0000-1000-8000-00805F9B34FB', // 特征值,在【7】里能获取到
		success(res) {
			console.log(res);

			// 接受消息的方法
			listenValueChange();
		},
		fail(err) {
			console.error(err);
		}
	});
}

// ArrayBuffer转16进度字符串示例
function ab2hex(buffer) {
	const hexArr = Array.prototype.map.call(
		new Uint8Array(buffer),
		function (bit) {
			return ('00' + bit.toString(16)).slice(-2);
		}
	);
	return hexArr.join('');
}

// 将16进制的内容转成我们看得懂的字符串内容
function hexCharCodeToStr(hexCharCodeStr) {
	var trimedStr = hexCharCodeStr.trim();
	var rawStr =
		trimedStr.substr(0, 2).toLowerCase() === '0x'
			? trimedStr.substr(2)
			: trimedStr;
	var len = rawStr.length;
	if (len % 2 !== 0) {
		alert('存在非法字符!');
		return '';
	}
	var curCharCode;
	var resultStr = [];
	for (var i = 0; i < len; i = i + 2) {
		curCharCode = parseInt(rawStr.substr(i, 2), 16);
		resultStr.push(String.fromCharCode(curCharCode));
	}
	return resultStr.join('');
}

// 【9】监听消息变化
function listenValueChange() {
	uni.onBLECharacteristicValueChange(res => {
		// 结果
		console.log(res);

		// 结果里有个value值,该值为 ArrayBuffer 类型,所以在控制台无法用肉眼观察到,必须将该值转换为16进制
		let resHex = ab2hex(res.value);
		console.log(resHex);

		// 最后将16进制转换为ascii码,就能看到对应的结果
		let result = hexCharCodeToStr(resHex);
		console.log(result);
	});
}
</script>

listenValueChange 方法是用来接收设备传过来的消息。

上面的例子中,res 的结果是

{
    "deviceId": "B4:10:7B:C4:83:14",
    "serviceId": "0000FFE0-0000-1000-8000-00805F9B34FB",
    "characteristicId": "0000FFE1-0000-1000-8000-00805F9B34FB",
    "value": {}
}

设备传过来的内容就放在value字段里,但因为该字段的类型是 ArrayBuffer,所以无法在控制台用肉眼直接观察。于是就通过ab2hex方法将该值转成 16进制 ,最后再用 hexCharCodeToStr 方法将 16进制 转成 ASCII码。

3.5. 发送指令

uni-app 和 微信小程序 提供的蓝牙api 来看

uni.writeBLECharacteristicValue:向低功耗蓝牙设备特征值中写入二进制数据,这个 api 是可以发送一些数据给蓝牙设备,但发送的值要转成 ArrayBuffer

  • 代码示例
<template>
	<view>
		<!-- 省略前面几步代码 -->
		<button @click="send">发送数据</button>
	</view>
</template>

<script setup>
import { ref } from 'vue';

// 省略前面几步代码

// 【10】发送数据
function send() {
	// 向蓝牙设备发送一个0x00的16进制数据

	let msg = 'hello';

	const buffer = new ArrayBuffer(msg.length);
	const dataView = new DataView(buffer);
	// dataView.setUint8(0, 0)

	for (var i = 0; i < msg.length; i++) {
		dataView.setUint8(i, msg.charAt(i).charCodeAt());
	}

	uni.writeBLECharacteristicValue({
		deviceId: deviceId.value, // 设备ID,在【4】里获取到
		serviceId: '0000FFE0-0000-1000-8000-00805F9B34FB', // 服务UUID,在【6】里能获取到
		characteristicId: '0000FFE1-0000-1000-8000-00805F9B34FB', // 特征值,在【7】里能获取到
		value: buffer,
		success(res) {
			console.log(res);
		},
		fail(err) {
			console.error(err);
		}
	});
}
</script>

此时,如果 uni.writeBLECharacteristicValuesuccess ,证明你已经把数据向外成功发送了,但不代表设备一定就收到了。

通常设备收到你发送过去的信息,会返回一条消息给你,而这个回调消息会在 uni.onBLECharacteristicValueChange 触发,也就是 第【9】步 那里。但这是蓝牙设备那边控制的,你作为前端佬,人家“已读不回”你也拿人家没办法。

四、完整代码

<template>
	<view>
		<scroll-view scroll-y class="box">
			<view class="item" v-for="item in blueDeviceList" @click="connect(item)">
				<view>
					<text>id: {{ item.deviceId }}</text>
				</view>
				<view>
					<text>name: {{ item.name }}</text>
				</view>
			</view>
		</scroll-view>

		<button @click="initBlue">1 初始化蓝牙</button>

		<button @click="discovery">2 搜索附近蓝牙设备</button>

		<button @click="getServices">3 获取蓝牙服务</button>

		<button @click="getCharacteristics">4 获取特征值</button>

		<button @click="notify">5 开启消息监听</button>

		<button @click="send">6 发送数据</button>

		<button @click="read">7 读取数据</button>

		<view class="msg_x">
			<view class="msg_txt"> 监听到的内容:{{ message }} </view>
			<view class="msg_hex"> 监听到的内容(十六进制):{{ messageHex }} </view>
		</view>
	</view>
</template>

<script setup>
import { ref } from 'vue';

// 搜索到的蓝牙设备列表
const blueDeviceList = ref([]);

// 【1】初始化蓝牙
function initBlue() {
	uni.openBluetoothAdapter({
		success(res) {
			console.log('初始化蓝牙成功');
			console.log(res);
		},
		fail(err) {
			console.log('初始化蓝牙失败');
			console.error(err);
		}
	});
}

// 【2】开始搜寻附近设备
function discovery() {
	uni.startBluetoothDevicesDiscovery({
		success(res) {
			console.log('开始搜索');
			// 开启监听回调
			uni.onBluetoothDeviceFound(found);
		},
		fail(err) {
			console.log('搜索失败');
			console.error(err);
		}
	});
}

// 【3】找到新设备就触发该方法
function found(res) {
	console.log(res);
	blueDeviceList.value.push(res.devices[0]);
}

// 蓝牙设备的id
const deviceId = ref('');

// 【4】连接设备
function connect(data) {
	console.log(data);

	deviceId.value = data.deviceId; // 将获取到的设备ID存起来

	uni.createBLEConnection({
		deviceId: deviceId.value,
		success(res) {
			console.log('连接成功');
			console.log(res);
			// 停止搜索
			stopDiscovery();
			uni.showToast({
				title: '连接成功'
			});
		},
		fail(err) {
			console.log('连接失败');
			console.error(err);
			uni.showToast({
				title: '连接成功',
				icon: 'error'
			});
		}
	});
}

// 【5】停止搜索
function stopDiscovery() {
	uni.stopBluetoothDevicesDiscovery({
		success(res) {
			console.log('停止成功');
			console.log(res);
		},
		fail(err) {
			console.log('停止失败');
			console.error(err);
		}
	});
}

// 【6】获取服务
function getServices() {
	// 如果是自动链接的话,uni.getBLEDeviceServices方法建议使用setTimeout延迟1秒后再执行
	uni.getBLEDeviceServices({
		deviceId: deviceId.value,
		success(res) {
			console.log(res); // 可以在res里判断有没有硬件佬给你的服务
			uni.showToast({
				title: '获取服务成功'
			});
		},
		fail(err) {
			console.error(err);
			uni.showToast({
				title: '获取服务失败',
				icon: 'error'
			});
		}
	});
}

// 硬件提供的服务id,开发中需要问硬件佬获取该id
const serviceId = ref('0000FFE0-0000-1000-8000-00805F9B34FB');

// 【7】获取特征值
function getCharacteristics() {
	// 如果是自动链接的话,uni.getBLEDeviceCharacteristics方法建议使用setTimeout延迟1秒后再执行
	uni.getBLEDeviceCharacteristics({
		deviceId: deviceId.value,
		serviceId: serviceId.value,
		success(res) {
			console.log(res); // 可以在此判断特征值是否支持读写等操作,特征值其实也需要提前向硬件佬索取的
			uni.showToast({
				title: '获取特征值成功'
			});
		},
		fail(err) {
			console.error(err);
			uni.showToast({
				title: '获取特征值失败',
				icon: 'error'
			});
		}
	});
}

const characteristicId = ref('0000FFE1-0000-1000-8000-00805F9B34FB');

// 【8】开启消息监听
function notify() {
	uni.notifyBLECharacteristicValueChange({
		deviceId: deviceId.value, // 设备id
		serviceId: serviceId.value, // 监听指定的服务
		characteristicId: characteristicId.value, // 监听对应的特征值
		success(res) {
			console.log(res);
			listenValueChange();
			uni.showToast({
				title: '已开启监听'
			});
		},
		fail(err) {
			console.error(err);
			uni.showToast({
				title: '监听失败',
				icon: 'error'
			});
		}
	});
}

// ArrayBuffer转16进度字符串示例
function ab2hex(buffer) {
	const hexArr = Array.prototype.map.call(
		new Uint8Array(buffer),
		function (bit) {
			return ('00' + bit.toString(16)).slice(-2);
		}
	);
	return hexArr.join('');
}

// 将16进制的内容转成我们看得懂的字符串内容
function hexCharCodeToStr(hexCharCodeStr) {
	var trimedStr = hexCharCodeStr.trim();
	var rawStr =
		trimedStr.substr(0, 2).toLowerCase() === '0x'
			? trimedStr.substr(2)
			: trimedStr;
	var len = rawStr.length;
	if (len % 2 !== 0) {
		alert('存在非法字符!');
		return '';
	}
	var curCharCode;
	var resultStr = [];
	for (var i = 0; i < len; i = i + 2) {
		curCharCode = parseInt(rawStr.substr(i, 2), 16);
		resultStr.push(String.fromCharCode(curCharCode));
	}
	return resultStr.join('');
}

// 监听到的内容
const message = ref('');
const messageHex = ref(''); // 十六进制

// 【9】监听消息变化
function listenValueChange() {
	uni.onBLECharacteristicValueChange(res => {
		console.log(res);
		let resHex = ab2hex(res.value);
		console.log(resHex);
		messageHex.value = resHex;
		let result = hexCharCodeToStr(resHex);
		console.log(String(result));
		message.value = String(result);
	});
}

// 【10】发送数据
function send() {
	// 向蓝牙设备发送一个0x00的16进制数据
	let msg = 'hello';

	const buffer = new ArrayBuffer(msg.length);
	const dataView = new DataView(buffer);
	// dataView.setUint8(0, 0)

	for (var i = 0; i < msg.length; i++) {
		dataView.setUint8(i, msg.charAt(i).charCodeAt());
	}

	uni.writeBLECharacteristicValue({
		deviceId: deviceId.value,
		serviceId: serviceId.value,
		characteristicId: characteristicId.value,
		value: buffer,
		success(res) {
			console.log('writeBLECharacteristicValue success', res.errMsg);
			uni.showToast({
				title: 'write指令发送成功'
			});
		},
		fail(err) {
			console.error(err);
			uni.showToast({
				title: 'write指令发送失败',
				icon: 'error'
			});
		}
	});
}

// 【11】读取数据
function read() {
	uni.readBLECharacteristicValue({
		deviceId: deviceId.value,
		serviceId: serviceId.value,
		characteristicId: characteristicId.value,
		success(res) {
			console.log(res);
			uni.showToast({
				title: 'read指令发送成功'
			});
		},
		fail(err) {
			console.error(err);
			uni.showToast({
				title: 'read指令发送失败',
				icon: 'error'
			});
		}
	});
}
</script>

<style>
.box {
	width: 98%;
	height: 400rpx;
	box-sizing: border-box;
	margin: 0 auto 20rpx;
	border: 2px solid dodgerblue;
}
.item {
	box-sizing: border-box;
	padding: 10rpx;
	border-bottom: 1px solid #ccc;
}
button {
	margin-bottom: 20rpx;
}

.msg_x {
	border: 2px solid seagreen;
	width: 98%;
	margin: 10rpx auto;
	box-sizing: border-box;
	padding: 20rpx;
}

.msg_x .msg_txt {
	margin-bottom: 20rpx;
}
</style>

  • 23
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值