第35集:uni-app NFC 功能
核心知识点
1. NFC API
uni-app 提供了完整的 NFC API,用于读写 NFC 标签和模拟 NFC 卡片。主要包括以下几类:
- NFC 适配器管理:
uni.getHCEState()、uni.startHCE()、uni.stopHCE() - NFC 标签读写:
uni.onNDEFDiscovered()、uni.readNFCNDEFMessage()、uni.writeNFCNDEFMessage() - HCE 卡片模拟:
uni.onHCEMessage()、uni.sendHCEMessage()
2. NFC 标签类型
NFC 标签主要有以下几种类型:
- NDEF 标签:存储 NDEF 格式的数据,是最常见的 NFC 标签类型
- ISO 14443-4 标签:支持 ISO 14443-4 协议的标签
- MIFARE 标签:包括 MIFARE Classic、MIFARE Ultralight 等
- Felica 标签:日本 Sony 公司开发的标签类型
3. NDEF 数据格式
NDEF (NFC Data Exchange Format) 是 NFC 论坛定义的数据交换格式,主要包括以下几种记录类型:
- URI 记录:存储网址
- 文本记录:存储文本信息
- 智能海报记录:存储包含标题、网址等信息的海报
- 自定义记录:存储自定义格式的数据
4. HCE 卡片模拟
HCE (Host-based Card Emulation) 是一种在移动设备上模拟 NFC 智能卡的技术,主要用于:
- 移动支付:模拟银行卡、交通卡等
- 门禁系统:模拟门禁卡
- 身份认证:模拟身份证、员工卡等
实用案例分析
案例1:实现 NFC 门禁功能
功能需求
实现一个 NFC 门禁应用,包括以下功能:
- 读取 NFC 门禁卡信息
- 模拟 NFC 门禁卡
- 管理门禁卡列表
- 记录刷卡历史
代码实现
<template>
<view class="container">
<view class="header">
<text class="title">NFC 门禁系统</text>
</view>
<view class="tabs">
<view
v-for="(tab, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: activeTab === index }"
@click="switchTab(index)"
>
<text>{{ tab }}</text>
</view>
</view>
<!-- 读取标签 -->
<view v-if="activeTab === 0" class="tab-content">
<view class="section">
<text class="section-title">读取门禁卡</text>
<view class="read-instruction">
<text>请将门禁卡靠近手机背面的 NFC 感应区</text>
</view>
<button @click="startRead" type="primary">开始读取</button>
<button @click="stopRead" type="default">停止读取</button>
</view>
<view v-if="cardInfo" class="card-info">
<text class="info-title">卡片信息</text>
<view class="info-item">
<text class="info-label">卡片类型:</text>
<text class="info-value">{{ cardInfo.type }}</text>
</view>
<view class="info-item">
<text class="info-label">卡片 ID:</text>
<text class="info-value">{{ cardInfo.id }}</text>
</view>
<view class="info-item">
<text class="info-label">数据内容:</text>
<text class="info-value">{{ cardInfo.content }}</text>
</view>
<button @click="saveCard" type="primary">保存卡片</button>
</view>
</view>
<!-- 模拟卡片 -->
<view v-if="activeTab === 1" class="tab-content">
<view class="section">
<text class="section-title">模拟门禁卡</text>
<view v-if="!isSimulating" class="card-list">
<text class="list-title">已保存的卡片</text>
<view v-for="(card, index) in savedCards" :key="index" class="card-item">
<view class="card-details">
<text class="card-name">{{ card.name }}</text>
<text class="card-id">{{ card.id }}</text>
</view>
<button @click="startSimulate(card)" type="primary" size="mini">模拟</button>
</view>
</view>
<view v-else class="simulating">
<text class="simulate-title">正在模拟卡片</text>
<text class="simulate-card">{{ currentSimulateCard.name }}</text>
<text class="simulate-instruction">请将手机靠近门禁读卡器</text>
<button @click="stopSimulate" type="warn">停止模拟</button>
</view>
</view>
</view>
<!-- 刷卡历史 -->
<view v-if="activeTab === 2" class="tab-content">
<view class="section">
<text class="section-title">刷卡历史</text>
<view class="history-list">
<view v-for="(record, index) in historyRecords" :key="index" class="history-item">
<view class="history-details">
<text class="history-card">{{ record.cardName }}</text>
<text class="history-time">{{ record.time }}</text>
</view>
<text class="history-status" :class="record.status">
{{ record.status === 'success' ? '成功' : '失败' }}
</text>
</view>
<view v-if="historyRecords.length === 0" class="empty-history">
<text>暂无刷卡记录</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
activeTab: 0,
tabs: ['读取标签', '模拟卡片', '刷卡历史'],
isReading: false,
cardInfo: null,
savedCards: [],
isSimulating: false,
currentSimulateCard: null,
historyRecords: []
};
},
onLoad() {
// 从本地存储加载数据
this.loadSavedCards();
this.loadHistoryRecords();
// 监听 NFC 标签发现
uni.onNDEFDiscovered((res) => {
this.handleNDEFDiscovered(res);
});
// 监听 HCE 消息
uni.onHCEMessage((res) => {
this.handleHCEMessage(res);
});
},
methods: {
// 切换标签页
switchTab(index) {
this.activeTab = index;
},
// 开始读取 NFC 标签
startRead() {
uni.showToast({ title: '请将卡片靠近手机', icon: 'none' });
this.isReading = true;
},
// 停止读取 NFC 标签
stopRead() {
this.isReading = false;
uni.showToast({ title: '已停止读取', icon: 'success' });
},
// 处理 NDEF 标签发现
handleNDEFDiscovered(res) {
if (!this.isReading) return;
console.log('发现 NFC 标签:', res);
// 解析标签数据
const cardInfo = {
type: 'NDEF',
id: this.arrayBufferToHex(res.id),
content: ''
};
// 解析 NDEF 消息
if (res.ndefMessage && res.ndefMessage.length > 0) {
res.ndefMessage.forEach(record => {
const payload = this.arrayBufferToString(record.payload);
cardInfo.content += payload;
});
}
this.cardInfo = cardInfo;
uni.showToast({ title: '读取成功', icon: 'success' });
},
// 保存卡片信息
saveCard() {
if (!this.cardInfo) return;
uni.showModal({
title: '保存卡片',
content: '请输入卡片名称',
editable: true,
placeholderText: '例如:办公室门禁卡',
success: (res) => {
if (res.confirm && res.content) {
const newCard = {
id: this.cardInfo.id,
name: res.content,
content: this.cardInfo.content,
type: this.cardInfo.type,
createTime: new Date().toISOString()
};
this.savedCards.push(newCard);
this.saveCardsToStorage();
uni.showToast({ title: '保存成功', icon: 'success' });
}
}
});
},
// 开始模拟卡片
startSimulate(card) {
uni.getHCEState({
success: (res) => {
if (res.errCode === 0) {
// HCE 可用
this.currentSimulateCard = card;
// 配置 HCE 服务
const aidList = [{
aid: 'F222222222'
}];
uni.startHCE({
aidList: aidList,
success: (res) => {
console.log('HCE 启动成功:', res);
this.isSimulating = true;
uni.showToast({ title: '开始模拟卡片', icon: 'success' });
},
fail: (err) => {
console.error('HCE 启动失败:', err);
uni.showToast({ title: '模拟失败', icon: 'none' });
}
});
} else {
uni.showToast({ title: '设备不支持 HCE', icon: 'none' });
}
},
fail: (err) => {
console.error('获取 HCE 状态失败:', err);
uni.showToast({ title: 'NFC 不可用', icon: 'none' });
}
});
},
// 停止模拟卡片
stopSimulate() {
uni.stopHCE({
success: (res) => {
console.log('HCE 停止成功:', res);
this.isSimulating = false;
this.currentSimulateCard = null;
uni.showToast({ title: '已停止模拟', icon: 'success' });
},
fail: (err) => {
console.error('HCE 停止失败:', err);
uni.showToast({ title: '停止失败', icon: 'none' });
}
});
},
// 处理 HCE 消息
handleHCEMessage(res) {
console.log('收到 HCE 消息:', res);
// 模拟门禁卡响应
if (res.messageType === 'request') {
// 这里应该根据实际门禁系统的协议返回相应的数据
// 以下是一个简单的示例
const response = new ArrayBuffer(4);
const dataView = new DataView(response);
dataView.setUint32(0, 0x9000); // 成功响应
uni.sendHCEMessage({
data: response,
success: (res) => {
console.log('发送 HCE 响应成功:', res);
// 记录刷卡历史
this.addHistoryRecord(true);
},
fail: (err) => {
console.error('发送 HCE 响应失败:', err);
// 记录刷卡历史
this.addHistoryRecord(false);
}
});
}
},
// 添加刷卡历史记录
addHistoryRecord(success) {
if (!this.currentSimulateCard) return;
const record = {
cardName: this.currentSimulateCard.name,
time: new Date().toLocaleString(),
status: success ? 'success' : 'fail'
};
this.historyRecords.unshift(record);
// 限制历史记录数量
if (this.historyRecords.length > 50) {
this.historyRecords.pop();
}
this.saveHistoryToStorage();
},
// 加载保存的卡片
loadSavedCards() {
const savedCards = uni.getStorageSync('nfc_cards');
if (savedCards) {
this.savedCards = savedCards;
}
},
// 保存卡片到本地存储
saveCardsToStorage() {
uni.setStorageSync('nfc_cards', this.savedCards);
},
// 加载历史记录
loadHistoryRecords() {
const historyRecords = uni.getStorageSync('nfc_history');
if (historyRecords) {
this.historyRecords = historyRecords;
}
},
// 保存历史记录到本地存储
saveHistoryToStorage() {
uni.setStorageSync('nfc_history', this.historyRecords);
},
// ArrayBuffer 转十六进制字符串
arrayBufferToHex(buffer) {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function(byte) {
return ('00' + byte.toString(16)).slice(-2);
}
);
return hexArr.join('');
},
// ArrayBuffer 转字符串
arrayBufferToString(buffer) {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
}
}
};
</script>
<style scoped>
.container {
padding: 20rpx;
}
.header {
margin-bottom: 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
display: block;
}
.tabs {
display: flex;
margin-bottom: 30rpx;
border-bottom: 1rpx solid #eaeaea;
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
border-bottom: 3rpx solid transparent;
}
.tab-item.active {
color: #007aff;
border-bottom-color: #007aff;
}
.tab-content {
min-height: 500rpx;
}
.section {
margin-bottom: 30rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
.read-instruction {
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
margin-bottom: 20rpx;
text-align: center;
}
.card-info {
margin-top: 30rpx;
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
}
.info-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 15rpx;
display: block;
}
.info-item {
display: flex;
margin-bottom: 10rpx;
}
.info-label {
width: 120rpx;
font-size: 26rpx;
color: #666;
}
.info-value {
flex: 1;
font-size: 26rpx;
word-break: break-all;
}
.card-list {
margin-top: 20rpx;
}
.list-title {
font-size: 26rpx;
margin-bottom: 15rpx;
display: block;
}
.card-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
margin-bottom: 15rpx;
}
.card-details {
flex: 1;
}
.card-name {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 5rpx;
display: block;
}
.card-id {
font-size: 24rpx;
color: #666;
}
.simulating {
padding: 30rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
text-align: center;
}
.simulate-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 15rpx;
display: block;
}
.simulate-card {
font-size: 28rpx;
color: #007aff;
margin-bottom: 20rpx;
display: block;
}
.simulate-instruction {
font-size: 26rpx;
color: #666;
margin-bottom: 30rpx;
}
.history-list {
margin-top: 20rpx;
}
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #eaeaea;
}
.history-details {
flex: 1;
}
.history-card {
font-size: 28rpx;
margin-bottom: 5rpx;
display: block;
}
.history-time {
font-size: 24rpx;
color: #999;
}
.history-status {
font-size: 26rpx;
padding: 5rpx 15rpx;
border-radius: 15rpx;
}
.history-status.success {
color: #07c160;
background-color: #e6f7ee;
}
.history-status.fail {
color: #ff3b30;
background-color: #ffebee;
}
.empty-history {
text-align: center;
padding: 50rpx 0;
color: #999;
font-size: 26rpx;
}
</style>案例2:实现 NFC 标签写入功能
功能需求
实现一个 NFC 标签写入应用,包括以下功能:
- 写入文本信息到 NFC 标签
- 写入网址到 NFC 标签
- 写入自定义数据到 NFC 标签
- 读取已写入的标签数据
代码实现
<template>
<view class="container">
<view class="header">
<text class="title">NFC 标签写入工具</text>
</view>
<view class="tabs">
<view
v-for="(tab, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: activeTab === index }"
@click="switchTab(index)"
>
<text>{{ tab }}</text>
</view>
</view>
<!-- 写入文本 -->
<view v-if="activeTab === 0" class="tab-content">
<view class="section">
<text class="section-title">写入文本</text>
<textarea
v-model="textContent"
placeholder="请输入要写入的文本"
class="textarea"
></textarea>
<button @click="writeText" type="primary">写入标签</button>
</view>
</view>
<!-- 写入网址 -->
<view v-if="activeTab === 1" class="tab-content">
<view class="section">
<text class="section-title">写入网址</text>
<input
v-model="urlContent"
placeholder="请输入要写入的网址"
class="input"
></input>
<button @click="writeUrl" type="primary">写入标签</button>
</view>
</view>
<!-- 写入自定义数据 -->
<view v-if="activeTab === 2" class="tab-content">
<view class="section">
<text class="section-title">写入自定义数据</text>
<input
v-model="customType"
placeholder="数据类型"
class="input"
></input>
<textarea
v-model="customContent"
placeholder="请输入要写入的自定义数据"
class="textarea"
></textarea>
<button @click="writeCustom" type="primary">写入标签</button>
</view>
</view>
<!-- 读取标签 -->
<view v-if="activeTab === 3" class="tab-content">
<view class="section">
<text class="section-title">读取标签</text>
<view class="read-instruction">
<text>请将 NFC 标签靠近手机背面的 NFC 感应区</text>
</view>
<button @click="startRead" type="primary">开始读取</button>
</view>
<view v-if="readData" class="read-data">
<text class="data-title">标签数据</text>
<view v-for="(record, index) in readData" :key="index" class="data-record">
<text class="record-type">类型: {{ record.type }}</text>
<text class="record-content">内容: {{ record.content }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
activeTab: 0,
tabs: ['写入文本', '写入网址', '写入自定义数据', '读取标签'],
textContent: '',
urlContent: '',
customType: '',
customContent: '',
readData: null,
isReading: false
};
},
onLoad() {
// 监听 NFC 标签发现
uni.onNDEFDiscovered((res) => {
this.handleNDEFDiscovered(res);
});
},
methods: {
// 切换标签页
switchTab(index) {
this.activeTab = index;
},
// 写入文本到 NFC 标签
writeText() {
if (!this.textContent) {
uni.showToast({ title: '请输入文本内容', icon: 'none' });
return;
}
uni.showToast({ title: '请将标签靠近手机', icon: 'none' });
// 构建 NDEF 消息
const ndefMessage = [{
tn: 'T', // 文本类型
payload: this.stringToBuffer(this.textContent)
}];
// 写入 NDEF 消息
uni.writeNFCNDEFMessage({
ndefMessage: ndefMessage,
success: (res) => {
console.log('写入成功:', res);
uni.showToast({ title: '写入成功', icon: 'success' });
},
fail: (err) => {
console.error('写入失败:', err);
uni.showToast({ title: '写入失败', icon: 'none' });
}
});
},
// 写入网址到 NFC 标签
writeUrl() {
if (!this.urlContent) {
uni.showToast({ title: '请输入网址', icon: 'none' });
return;
}
uni.showToast({ title: '请将标签靠近手机', icon: 'none' });
// 构建 NDEF 消息
const ndefMessage = [{
tn: 'U', // URI 类型
payload: this.stringToBuffer(this.urlContent)
}];
// 写入 NDEF 消息
uni.writeNFCNDEFMessage({
ndefMessage: ndefMessage,
success: (res) => {
console.log('写入成功:', res);
uni.showToast({ title: '写入成功', icon: 'success' });
},
fail: (err) => {
console.error('写入失败:', err);
uni.showToast({ title: '写入失败', icon: 'none' });
}
});
},
// 写入自定义数据到 NFC 标签
writeCustom() {
if (!this.customType || !this.customContent) {
uni.showToast({ title: '请输入类型和内容', icon: 'none' });
return;
}
uni.showToast({ title: '请将标签靠近手机', icon: 'none' });
// 构建 NDEF 消息
const ndefMessage = [{
tn: this.customType,
payload: this.stringToBuffer(this.customContent)
}];
// 写入 NDEF 消息
uni.writeNFCNDEFMessage({
ndefMessage: ndefMessage,
success: (res) => {
console.log('写入成功:', res);
uni.showToast({ title: '写入成功', icon: 'success' });
},
fail: (err) => {
console.error('写入失败:', err);
uni.showToast({ title: '写入失败', icon: 'none' });
}
});
},
// 开始读取 NFC 标签
startRead() {
uni.showToast({ title: '请将标签靠近手机', icon: 'none' });
this.isReading = true;
},
// 处理 NDEF 标签发现
handleNDEFDiscovered(res) {
if (!this.isReading) return;
console.log('发现 NFC 标签:', res);
// 解析 NDEF 消息
const records = [];
if (res.ndefMessage && res.ndefMessage.length > 0) {
res.ndefMessage.forEach(record => {
const payload = this.bufferToString(record.payload);
records.push({
type: record.tn,
content: payload
});
});
}
this.readData = records;
this.isReading = false;
uni.showToast({ title: '读取成功', icon: 'success' });
},
// 字符串转 ArrayBuffer
stringToBuffer(str) {
const encoder = new TextEncoder();
return encoder.encode(str).buffer;
},
// ArrayBuffer 转字符串
bufferToString(buffer) {
const decoder = new TextDecoder();
return decoder.decode(new Uint8Array(buffer));
}
}
};
</script>
<style scoped>
.container {
padding: 20rpx;
}
.header {
margin-bottom: 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
display: block;
}
.tabs {
display: flex;
flex-wrap: wrap;
margin-bottom: 30rpx;
border-bottom: 1rpx solid #eaeaea;
}
.tab-item {
width: 25%;
text-align: center;
padding: 20rpx 0;
font-size: 24rpx;
color: #666;
border-bottom: 3rpx solid transparent;
}
.tab-item.active {
color: #007aff;
border-bottom-color: #007aff;
}
.tab-content {
min-height: 500rpx;
}
.section {
margin-bottom: 30rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
.input {
width: 100%;
padding: 20rpx;
border: 1rpx solid #eaeaea;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.textarea {
width: 100%;
padding: 20rpx;
border: 1rpx solid #eaeaea;
border-radius: 8rpx;
font-size: 28rpx;
min-height: 200rpx;
margin-bottom: 20rpx;
}
.read-instruction {
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
margin-bottom: 20rpx;
text-align: center;
}
.read-data {
margin-top: 30rpx;
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
}
.data-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 15rpx;
display: block;
}
.data-record {
margin-bottom: 15rpx;
padding: 15rpx;
background-color: #fff;
border-radius: 8rpx;
}
.record-type {
font-size: 26rpx;
margin-bottom: 5rpx;
display: block;
}
.record-content {
font-size: 26rpx;
color: #666;
word-break: break-all;
}
</style>学习目标
通过本集的学习,你应该能够:
- 掌握 uni-app NFC API 的使用方法
- 理解 NFC 标签的基本类型和数据格式
- 实现 NFC 标签的读写操作
- 实现基于 HCE 的卡片模拟功能
- 开发基于 NFC 功能的实际应用
小结
本集详细介绍了 uni-app 中的 NFC 功能,包括 NFC API、标签读写、卡片模拟等核心知识点,并通过两个实际案例展示了如何实现 NFC 门禁功能和标签写入功能。
NFC 功能在移动应用开发中有着广泛的应用场景,如门禁系统、移动支付、身份认证等。掌握这些技能可以帮助你开发出更加便捷和安全的应用。在实际开发中,你还需要注意设备兼容性、NFC 权限、数据安全等问题,以确保应用的可靠性和用户体验。
通过本集的学习,你已经具备了在 uni-app 中使用 NFC 功能的基本能力,可以开始开发需要 NFC 功能的应用了。