修改NFC读取

This commit is contained in:
xuli
2025-12-01 18:07:02 +08:00
parent b444bcee57
commit 11990a62da
296 changed files with 3808 additions and 282 deletions

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2020] 雷龙飞[1102228556@qq.com]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,42 +0,0 @@
{
"name": "Android-NFC读卡",
"id": "LF-Sense-Card",
"version": "0.0.2",
"description": "uni-app android原生插件 用来使用nfc读取m1卡",
"_dp_type": "nativeplugin",
"_dp_nativeplugin": {
"android": {
"plugins": [
{
"type": "module",
"name": "LF-Sense-Card-M1",
"class": "com.clubank.uniplugin_m1card.M1CardModule"
}
],
"hooksClass": "",
"integrateType": "aar",
"dependencies": [],
"compileOptions": {
"sourceCompatibility": "1.8",
"targetCompatibility": "1.8"
},
"abis": [
"armeabi-v7a",
"arm64-v8a",
"x86"
],
"minSdkVersion": 19,
"useAndroidX": false,
"permissions": [
"<uses-permission android:name=\"android.permission.NFC\" />"
],
"parameters": {
"插件需要配置的参数名称, 如appid": {
"des": "参数描述",
"key": "AndroidManifest.xml中添加meta-data节点对应android:name属性值, 如GETUI_APPID",
"placeholder": "build.gradle中添加到manifestPlaceholders中的字段名"
}
}
}
}
}

View File

@@ -0,0 +1,421 @@
<template>
<view class="nfc-con">
<view class="nfc-bg">
<view class="nfc-title">
<uni-icons type="closeempty" size="20" class="nfc-close" @click="handleClose"></uni-icons>NFC识别
</view>
<block v-if="!nfcResult">
<view class="nfc-pic" @click="readData" v-if="!isReading">
<img :src="'static/images/polling/nfc-logo.png'" />
</view>
<view class="nfc-pic" v-else>
<img class="nfc-pic-animal" :src="'static/images/polling/nfc-logo.png'" />
</view>
<view class="nfc-desc">请将设备靠近NFC识别</view>
</block>
<!-- 识别成功 -->
<view class="result-section" v-else>
<block v-if="readStatus">
<view class="result-header">
<img class="result-img" :src="'static/images/polling/icon-success.png'" />
<view class="result-label">识别成功</view>
</view>
<view class="result-value">
<view>识别时间</view>
<view>{{parseTime(new Date().getTime(),'{y}年{m}月{d}日 星期{a} {h}:{i}')}}</view>
</view>
<view class="btn-blue" @click="handleConfirm">确定</view>
</block>
<block v-else>
<view class="result-header">
<img class="result-img" :src="'static/images/polling/icon-Alert.png'" />
<view class="result-label result-err">识别失败</view>
</view>
<view class="result-value">
可能的失败原因手机距离NFC设备较远手机未联网NFC设备故障
</view>
<view class="btn-blue" @click="readData">重新识别</view>
</block>
</view>
</view>
</view>
</template>
<script setup>
import { ref,watch } from 'vue'
import { onLoad,onHide} from '@dcloudio/uni-app';
import { parseTime } from '@/utils/datetime.js';
// 包路径
const package_NdefRecord = 'android.nfc.NdefRecord';
const package_NdefMessage = 'android.nfc.NdefMessage';
const package_TECH_DISCOVERED = 'android.nfc.action.TECH_DISCOVERED';
const package_Intent = 'android.content.Intent';
const package_Activity = 'android.app.Activity';
const package_PendingIntent = 'android.app.PendingIntent';
const package_IntentFilter = 'android.content.IntentFilter';
const package_NfcAdapter = 'android.nfc.NfcAdapter';
const package_Ndef = 'android.nfc.tech.Ndef';
const package_NdefFormatable = 'android.nfc.tech.NdefFormatable';
const package_Parcelable = 'android.os.Parcelable';
const package_String = 'java.lang.String';
let NfcAdapter;
let NdefRecord;
let NdefMessage;
let Uri;
let readyWriteData = false;
let readyRead = false;
let noNFC = false;
let techListsArray = [
['android.nfc.tech.IsoDep'],
['android.nfc.tech.NfcA'],
['android.nfc.tech.NfcB'],
['android.nfc.tech.NfcF'],
['android.nfc.tech.Nfcf'],
['android.nfc.tech.NfcV'],
['android.net.Uri'],
['android.nfc.tech.NdefFormatable'],
['android.nfc.tech.MifareClassi'],
['android.nfc.tech.MifareUltralight']
];
let text = '';// 要写入的数据
let readResult = '';
const isReading=ref(false)
const readStatus=ref(false);//读取状态 true 成功
const nfcResult=ref('')
// onLoad(() => {
// listenNFCStatus();
// })
// 监听NFC状态
const listenNFCStatus=()=>{
console.log("listenNFCStatus=====begin")
try {
let main = plus.android.runtimeMainActivity();
let Intent = plus.android.importClass('android.content.Intent');
let Activity = plus.android.importClass('android.app.Activity');
let PendingIntent = plus.android.importClass('android.app.PendingIntent');
let IntentFilter = plus.android.importClass('android.content.IntentFilter');
Uri = plus.android.importClass('android.net.Uri');
NfcAdapter = plus.android.importClass('android.nfc.NfcAdapter');
let nfcAdapter = NfcAdapter.getDefaultAdapter(main);
if(nfcAdapter == null){
uni.showToast({
title: '设备不支持NFC',
icon: 'none'
})
noNFC = true;
return;
}
if (!nfcAdapter.isEnabled()) {
uni.showToast({
title: '请在系统设置中先启用NFC功能',
icon: 'none'
});
noNFC = true;
return;
}else{
noNFC = false;
}
let intent = new Intent(main, main.getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
let pendingIntent = PendingIntent.getActivity(main, 0, intent, 0);
let ndef = new IntentFilter("android.nfc.action.TECH_DISCOVERED");
ndef.addDataType("*/*");
let intentFiltersArray = [ndef];
plus.globalEvent.addEventListener('newintent',function() {
// console.log('newintent running');
// 监听 NFC
setTimeout(nfcRuning(), 1000);
});
plus.globalEvent.addEventListener('pause',function(e) {
// console.log('pause running');
if (nfcAdapter) {
//关闭前台调度系统
//恢复默认状态
nfcAdapter.disableForegroundDispatch(main);
}
});
plus.globalEvent.addEventListener('resume',function(e) {
// console.log('resume running');
if (nfcAdapter) {
//开启前台调度系统
nfcAdapter.enableForegroundDispatch(main, pendingIntent, intentFiltersArray, techListsArray);
}
});
nfcAdapter.enableForegroundDispatch(main, pendingIntent, intentFiltersArray, techListsArray);
} catch (e) {
console.error(e);
}
}
// 运行nfc
const nfcRuning=()=>{
NdefRecord = plus.android.importClass("android.nfc.NdefRecord");
NdefMessage = plus.android.importClass("android.nfc.NdefMessage");
let main = plus.android.runtimeMainActivity();
let intent = main.getIntent();
console.log("action type:" + intent.getAction());
if (package_TECH_DISCOVERED == intent.getAction()) {
if (readyWriteData) {
write(intent);
readyWriteData = false;
} else if (readyRead) {
read(intent);
readyRead = false;
}
}
}
// 写入nfc
const write=(intent)=>{
try {
toast('请勿移开标签 正在写入...');
console.log("text=" + text);
let textBytes = plus.android.invoke(text, "getBytes");
// image/jpeg text/plain
let uri = Uri.parse(text)
let message = new NdefMessage([new NdefRecord.createUri(uri)]);
console.log("====message===>", message)
let Ndef = plus.android.importClass('android.nfc.tech.Ndef');
let NdefFormatable = plus.android.importClass('android.nfc.tech.NdefFormatable');
let tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
let ndef = Ndef.get(tag);
if (ndef != null) {
// 待写入的数据长度
let size = message.toByteArray().length;
ndef.connect();
if (!ndef.isWritable()) {
toast('tag不允许写入');
return;
}
if (ndef.getMaxSize() < size) {
toast('文件大小超出容量!');
return;
}
ndef.writeNdefMessage(message);
toast('写入数据成功!');
return;
} else {
let format = NdefFormatable.get(tag);
if (format != null) {
try {
format.connect();
format.format(message);
toast('格式化tag并且写入message');
return;
} catch (e) {
toast('格式化tag失败.');
return;
}
} else {
toast('Tag不支持NDEF');
return;
}
}
} catch (e) {
toast('写入失败');
console.log("error=" + e);
}
}
// 读取nfc
const read=(intent)=>{
toast('请勿移开标签正在读取数据');
// NFC id
let bytesId = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
let nfc_id = byteArrayToHexString(bytesId);
console.log('nfc_id:', nfc_id);
let Parcelable = plus.android.importClass("android.os.Parcelable");
let rawmsgs = intent.getParcelableArrayExtra("android.nfc.extra.NDEF_MESSAGES");
//let rawmsgs = intent.getParcelableArrayExtra();
// console.log('rawmsgs:', rawmsgs);
if(rawmsgs != null && rawmsgs.length > 0) {
let records = rawmsgs[0].getRecords();
let result = records[0].getPayload();
let data = plus.android.newObject("java.lang.String", result);
console.log('data=>'+data);
readResult = data;
readStatus.value = true;
nfcResult.value = data;
toast('NFC 数据:' + data);
// plus.runtime.openURL(data, function(res) {
// console.log("NFC 数据----", res);
// });
// return data;
}else{
toast('没有读取到数据');
return ""
}
}
// 字节转换
const byteArrayToHexString=(inarray)=>{ // converts byte arrays to string
let i, j, inn;
let hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
let out = "";
for(j = 0; j < inarray.length; ++j) {
inn = inarray[j] & 0xff;
i = (inn >>> 4) & 0x0f;
out += hex[i];
i = inn & 0x0f;
out += hex[i];
}
return out;
}
// 正式调用写入
const writeData= ()=>{
if(noNFC){
toast('请检查设备是否支持并开启 NFC 功能!');
return;
}
// 监听事件,触发条件
readyWriteData = true;
toast('请将NFC标签靠近');
}
// 正式调用读
const readData= ()=>{
if(noNFC){
toast('请检查设备是否支持并开启 NFC 功能!');
return;
}
// 监听事件,触发条件
readyRead = true;
isReading.value = true;
readStatus.value = false;
// toast('请将NFC标签靠近');
}
// 提示
const toast = (content)=>{
uni.showToast({
title: content,
icon: 'none'
})
}
// 回调父组件
const emit = defineEmits(['close','confirm'])
const handleClose = ()=>{
emit('close');
}
const handleConfirm= ()=>{
emit('confirm',nfcResult.value);
}
// 暴露方法给父组件
defineExpose({
listenNFCStatus
});
</script>
<style lang="scss" scoped>
.nfc-con {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 9999;
}
.nfc-con .nfc-bg{
position:absolute;
bottom:0;
left:0;
background-color:#fff;
border-radius:18rpx 18rpx 0 0;
text-align:center;
width:650rpx;
padding:30rpx 50rpx 130rpx;
}
.nfc-bg .nfc-title{
text-align:center;
color:#333333;
font-size:32rpx;
font-weight:bold;
position:relative;
}
.nfc-bg .nfc-title .nfc-close{
position:absolute;
top:0rpx;
left:-20rpx
}
.nfc-con .nfc-bg .nfc-pic{
padding:126rpx 0 86rpx;
}
.nfc-con .nfc-bg .nfc-pic img{
width:260rpx;
height:208rpx;
}
.nfc-con .nfc-bg .nfc-desc{
text-align:center;
color:#0395E0;
font-size:36rpx;
font-weight:bold;
}
.nfc-pic-animal{
animation: elasticLoop 3s ease-in-out infinite;
}
@keyframes elasticLoop {
0%, 100% { transform: scale(1); }
25% { transform: scale(1.2); }
50% { transform: scale(0.95); }
75% { transform: scale(1.1); }
}
.result-section{
.result-header{
padding:70rpx 0 0;
.result-img{
width:120rpx;
height:120rpx;
}
.result-label,.result-err{
text-align:center;
color:#00BF5A;
font-size:36rpx;
font-weight:bold;
padding:20rpx 0;
}
.result-err{
color:#FF9638;
}
}
.result-value{
color:#333;
text-align:center;
font-size:30rpx;
margin-top:20rpx;
}
.btn-blue{
background-color:#05A3F4;
border-radius:40rpx;
width:380rpx;
height:80rpx;
line-height:80rpx;
text-align:center;
color:#fff;
font-size:36rpx;
margin:50rpx auto 0;
}
}
</style>

View File

@@ -112,27 +112,6 @@
"pid" : "",
"parameters" : {}
}
},
"LF-Sense-Card" : {
"插件需要配置的参数名称, 如appid" : "",
"__plugin_info__" : {
"name" : "Android-NFC读卡",
"description" : "uni-app android原生插件 用来使用nfc读取m1卡",
"platforms" : "Android",
"url" : "",
"android_package_name" : "",
"ios_bundle_id" : "",
"isCloud" : false,
"bought" : -1,
"pid" : "",
"parameters" : {
"插件需要配置的参数名称, 如appid" : {
"des" : "参数描述",
"key" : "AndroidManifest.xml中添加meta-data节点对应android:name属性值, 如GETUI_APPID",
"value" : ""
}
}
}
}
}
},

View File

@@ -662,6 +662,18 @@
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/business/polling/nfcTest/nfcTemplate",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/business/polling/nfcTest/index",
"style": {
"navigationBarTitleText": ""
}
}
],
"globalStyle": {

View File

@@ -0,0 +1,78 @@
<template>
<view class="container">
<input class="input-box" v-model="inputText" @input="input" type="text" placeholder="请输入内容"/>
<view class="btn-box">
<button class="btn-write" @click="writeData()">写信息</button>
<button class="btn-read" @click="readData()">读信息</button>
</view>
</view>
</template>
<script>
import nfc from '@/utils/ouu-nfc.js'
export default {
data() {
return {
inputText: 'https://baidu.com'
}
},
onLoad() {
nfc.listenNFCStatus();
},
methods: {
input(e) {
nfc.inputChanage(e.detail.value)
},
writeData() {
nfc.writeData()
},
readData() {
nfc.readData()
}
}
}
</script>
<style scoped lang="scss">
.container {
padding: 15px;
box-sizing: border-box;
.iframe {
height: 100vh;
width: 100vw;
border: none;
margin: 0;
}
.input-box {
border: 1px solid #aaa;
margin: 30px 0;
padding: 10px;
border-radius: 100px;
}
.btn-box {
display: flex;
justify-content: space-between;
align-items: center;
.btn-write {
width: 100%;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.btn-read {
width: 100%;
margin-left: 20px;
height: 100px;
background-color: #1ee176;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,784 @@
<template>
<view class="container" @touchstart="touchStart" @touchend="touchEnd">
<view class="header">
<text class="title">NFC卡片读取器</text>
</view>
<view class="content" :class="{ 'landscape-layout': isLandscape }">
<!-- NFC读取按钮 - 平板适配按钮尺寸不小于48px×48px -->
<button class="nfc-button" @click="startNFCReading" :disabled="isReading">
<text v-if="!isReading">开始NFC读取</text>
<view v-else class="loading-wrapper">
<view class="loading-spinner"></view>
<text>正在读取中...</text>
</view>
</button>
<!-- 读取状态提示 -->
<view class="status-section">
<text class="status-text" :class="statusClass">{{ statusMessage }}</text>
</view>
<!-- 读取结果显示区域 -->
<view class="result-section" v-if="nfcResult">
<view class="result-header">
<text class="result-label">NFC ID:</text>
<button class="copy-button" @click="copyToClipboard">复制</button>
</view>
<text class="result-value">{{ nfcResult }}</text>
</view>
<!-- 操作提示 -->
<view class="instruction">
<text class="instruction-title">操作指南:</text>
<text class="instruction-text">1. 确保设备NFC功能已开启</text>
<text class="instruction-text">2. 将NFC卡片贴近设备感应区域</text>
<text class="instruction-text">3. 听到提示音或感受到震动时表示读取成功</text>
</view>
</view>
<!-- 底部信息 -->
<view class="footer">
<text class="footer-text">支持卡片类型: ISO14443A/B, Mifare, NFC-V, FeliCa</text>
</view>
</view>
</template>
<script>
import nfcUtil from "@/utils/hexiii-nfc.js"
export default {
data() {
return {
isReading: false,
statusMessage: '准备就绪',
statusClass: 'status-ready',
nfcResult: '',
statusHistory: [], // 记录状态历史
isLandscape: false, // 是否横屏
screenWidth: 0, // 屏幕宽度
screenHeight: 0, // 屏幕高度
deviceInfo: null, // 设备信息
touchStartTime: 0, // 触摸开始时间
touchStartX: 0, // 触摸开始X坐标
touchStartY: 0 // 触摸开始Y坐标
}
},
onLoad() {
// 获取设备信息
this._getDeviceInfo();
// 页面加载时检查NFC状态
this._checkNFCStatus();
// 添加屏幕旋转监听
uni.onWindowResize(this._onWindowResize);
},
onShow() {
// 页面显示时重新检查NFC状态
this._checkNFCStatus();
},
onHide() {
// 页面隐藏时暂停NFC监听
if (this.isReading) {
nfcUtil.stopNFCListening();
this.isReading = false;
}
},
onUnload() {
// 页面卸载时清理NFC资源
nfcUtil.cleanupNFC();
// 移除屏幕旋转监听
uni.offWindowResize(this._onWindowResize);
},
methods: {
/**
* 获取设备信息
* @private
*/
_getDeviceInfo() {
uni.getSystemInfo({
success: (res) => {
this.deviceInfo = res;
this.screenWidth = res.screenWidth;
this.screenHeight = res.screenHeight;
// 判断是否为横屏
this.isLandscape = res.windowWidth > res.windowHeight;
console.log('设备信息:', res);
console.log('当前屏幕方向:', this.isLandscape ? '横屏' : '竖屏');
}
});
},
/**
* 监听屏幕旋转
* @private
*/
_onWindowResize(res) {
if (res) {
const isLandscape = res.windowWidth > res.windowHeight;
// 屏幕方向改变时更新状态
if (this.isLandscape !== isLandscape) {
this.isLandscape = isLandscape;
console.log('屏幕旋转为:', isLandscape ? '横屏' : '竖屏');
// 屏幕旋转时重新调整布局
this._adjustLayoutForOrientation();
}
}
},
/**
* 根据屏幕方向调整布局
* @private
*/
_adjustLayoutForOrientation() {
// 可以在这里进行特定的布局调整
// 例如:横屏时调整按钮位置、改变元素大小等
// 目前主要通过CSS媒体查询实现这里预留接口
console.log('调整布局适应屏幕方向');
},
/**
* 开始NFC读取
*/
async startNFCReading() {
// 提供触觉反馈 - 平板用户体验增强
this._provideHapticFeedback();
// 设置读取状态
this.isReading = true;
this.statusMessage = '等待NFC卡片...';
this.statusClass = 'status-waiting';
this._addStatusHistory('开始读取NFC');
try {
// 调用NFC读取功能
this._addStatusHistory('正在监听NFC信号...');
const nfcId = await nfcUtil.listenNFCStatus();
// 确保ID有效
if (nfcId) {
// 成功读取,提供触觉反馈
this._provideHapticFeedback('success');
// 播放成功提示音
this._playBeepSound();
// 显示结果并输出到控制台
this.nfcResult = nfcId;
console.log('NFC读取结果:', nfcId);
// 更新状态
this.statusMessage = '读取成功';
this.statusClass = 'status-success';
this._addStatusHistory(`成功读取ID: ${nfcId}`);
// 显示成功提示框
uni.showModal({
title: '读取成功',
content: `NFC ID: ${nfcId}\n\n是否复制到剪贴板`,
showCancel: true,
cancelText: '取消',
confirmText: '复制',
success: (res) => {
if (res.confirm) {
this.copyToClipboard();
}
}
});
} else {
this.statusMessage = '未读取到NFC数据';
this.statusClass = 'status-error';
this._addStatusHistory('未读取到NFC数据');
}
} catch (error) {
console.error('NFC读取错误:', error);
// 错误情况,提供触觉反馈
this._provideHapticFeedback('error');
// 根据错误类型显示不同的提示
let errorMsg = '读取失败';
if (error.message.includes('不支持NFC')) {
errorMsg = '设备不支持NFC功能';
} else if (error.message.includes('未开启')) {
errorMsg = '请先在系统设置中开启NFC功能';
} else if (error.message.includes('超时')) {
errorMsg = '读取超时,请重试';
} else {
errorMsg = '读取失败: ' + error.message;
}
this.statusMessage = errorMsg;
this.statusClass = 'status-error';
this._addStatusHistory(`错误: ${error.message}`);
// 显示错误提示
uni.showModal({
title: '读取失败',
content: errorMsg,
showCancel: false,
confirmText: '确定'
});
} finally {
// 重置读取状态
this.isReading = false;
// 3秒后重置状态消息
setTimeout(() => {
if (!this.isReading) {
this.statusMessage = '准备就绪';
this.statusClass = 'status-ready';
}
}, 3000);
}
},
/**
* 复制NFC ID到剪贴板
*/
copyToClipboard() {
if (this.nfcResult) {
uni.setClipboardData({
data: this.nfcResult,
success: () => {
uni.showToast({
title: '已复制到剪贴板',
icon: 'success',
duration: 2000
});
}
});
}
},
/**
* 检查NFC状态
* @private
*/
_checkNFCStatus() {
try {
// 尝试初始化NFC适配器检查状态
const main = plus.android.runtimeMainActivity();
const NfcAdapter = plus.android.importClass('android.nfc.NfcAdapter');
const nfcAdapter = NfcAdapter.getDefaultAdapter(main);
if (nfcAdapter == null) {
this.statusMessage = '设备不支持NFC功能';
this.statusClass = 'status-error';
} else if (!nfcAdapter.isEnabled()) {
this.statusMessage = 'NFC功能未开启请在系统设置中开启';
this.statusClass = 'status-warning';
}
} catch (e) {
console.log('NFC状态检查失败可能在非Android环境');
}
},
/**
* 提供触觉反馈
* @private
* @param {string} type - 反馈类型: 'success', 'error'或默认
*/
_provideHapticFeedback(type = 'default') {
try {
// 在Android设备上提供震动反馈
const VIBRATOR_SERVICE = 'vibrator';
const Context = plus.android.importClass('android.content.Context');
const main = plus.android.runtimeMainActivity();
const vibrator = main.getSystemService(VIBRATOR_SERVICE);
// 根据类型设置不同的震动模式
if (vibrator) {
if (type === 'success') {
// 成功震动模式: 短震动
vibrator.vibrate(100);
} else if (type === 'error') {
// 错误震动模式: 长震动
vibrator.vibrate(300);
} else {
// 默认震动模式: 轻微震动
vibrator.vibrate(50);
}
}
} catch (e) {
// 如果设备不支持震动或没有权限,忽略错误
console.log('无法提供震动反馈:', e.message);
}
},
/**
* 播放提示音
* @private
*/
_playBeepSound() {
try {
// 使用系统默认提示音
const AudioManager = plus.android.importClass('android.media.AudioManager');
const ToneGenerator = plus.android.importClass('android.media.ToneGenerator');
const main = plus.android.runtimeMainActivity();
const toneGen = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
toneGen.startTone(ToneGenerator.TONE_PROP_BEEP);
} catch (e) {
// 如果无法播放提示音,忽略错误
console.log('无法播放提示音:', e.message);
}
},
/**
* 添加状态历史记录
* @private
* @param {string} message - 状态消息
*/
_addStatusHistory(message) {
const timestamp = new Date().toLocaleTimeString();
this.statusHistory.push(`[${timestamp}] ${message}`);
// 只保留最近10条记录
if (this.statusHistory.length > 10) {
this.statusHistory.shift();
}
console.log(`[NFC状态] ${message}`);
},
/**
* 触摸开始事件处理(用于长按等手势识别)
* @private
*/
touchStart(event) {
this.touchStartTime = Date.now();
this.touchStartX = event.touches[0].clientX;
this.touchStartY = event.touches[0].clientY;
},
/**
* 触摸结束事件处理(用于长按等手势识别)
* @private
*/
touchEnd(event) {
// 可以在这里添加手势识别逻辑
// 例如:长按识别、滑动检测等
const touchEndTime = Date.now();
const touchEndX = event.changedTouches[0].clientX;
const touchEndY = event.changedTouches[0].clientY;
// 计算触摸时间和移动距离
const touchDuration = touchEndTime - this.touchStartTime;
const distanceX = Math.abs(touchEndX - this.touchStartX);
const distanceY = Math.abs(touchEndY - this.touchStartY);
// 长按检测超过500ms且移动距离小于10px
if (touchDuration > 500 && distanceX < 10 && distanceY < 10) {
console.log('长按操作');
// 长按可以触发特殊功能,如显示详细信息
}
},
}
}
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 32rpx;
background-color: #f5f5f5;
/* 增加背景纹理,提升平板视觉体验 */
background-image: linear-gradient(45deg, rgba(0, 0, 0, 0.02) 25%, transparent 25%, transparent 75%, rgba(0, 0, 0, 0.02) 75%, rgba(0, 0, 0, 0.02)),
linear-gradient(45deg, rgba(0, 0, 0, 0.02) 25%, transparent 25%, transparent 75%, rgba(0, 0, 0, 0.02) 75%, rgba(0, 0, 0, 0.02));
background-size: 200rpx 200rpx;
background-position: 0 0, 100rpx 100rpx;
}
/* 横屏布局 */
.content.landscape-layout {
/* 横屏时优化内容区布局 */
max-width: 960rpx;
margin: 0 auto;
/* 为横屏模式添加微妙的阴影效果 */
box-shadow: 0 0 20rpx rgba(0, 0, 0, 0.05);
background-color: rgba(255, 255, 255, 0.7);
border-radius: 16rpx;
padding: 40rpx;
backdrop-filter: blur(5rpx);
}
.header {
text-align: center;
margin-bottom: 48rpx;
padding: 20rpx 0;
}
.title {
font-size: 48rpx;
font-weight: bold;
color: #212121;
/* 平板适配:增加文字阴影提升可读性 */
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 32rpx;
}
/* NFC按钮 - 平板适配按钮高度不小于60px */
.nfc-button {
width: 600rpx;
height: 120rpx;
line-height: 120rpx;
font-size: 36rpx;
background-color: #1976D2;
color: white;
border-radius: 8rpx;
margin-bottom: 48rpx;
display: flex;
align-items: center;
justify-content: center;
/* 平板适配:增加按钮阴影和触摸反馈 */
box-shadow: 0 4rpx 12rpx rgba(25, 118, 210, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
/* 水波纹效果 - 符合Android设计规范 */
.nfc-button::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.nfc-button:active::after {
width: 120%;
height: 120%;
}
.nfc-button:active {
background-color: #1565C0;
transform: translateY(2rpx);
box-shadow: 0 2rpx 6rpx rgba(25, 118, 210, 0.3);
}
.nfc-button[disabled] {
background-color: #90CAF9;
box-shadow: none;
}
/* 加载动画 */
.loading-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.loading-spinner {
width: 32rpx;
height: 32rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top: 4rpx solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.status-section {
margin-bottom: 32rpx;
padding: 24rpx 32rpx;
border-radius: 8rpx;
background-color: white;
min-width: 500rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.status-text {
font-size: 32rpx;
font-weight: 500;
transition: color 0.3s ease;
}
.status-ready {
color: #616161;
}
.status-waiting {
color: #1976D2;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.status-success {
color: #4CAF50;
}
.status-error {
color: #F44336;
}
.status-warning {
color: #FF9800;
}
.result-section {
background-color: white;
padding: 32rpx;
border-radius: 8rpx;
margin-bottom: 32rpx;
min-width: 500rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.result-section:hover {
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.result-label {
font-size: 28rpx;
color: #757575;
font-weight: 500;
}
.copy-button {
min-width: 120rpx;
height: 60rpx;
line-height: 60rpx;
font-size: 24rpx;
background-color: #E3F2FD;
color: #1976D2;
border-radius: 6rpx;
}
.copy-button:active {
background-color: #BBDEFB;
}
.result-value {
font-size: 36rpx;
font-family: monospace;
color: #212121;
word-break: break-all;
line-height: 1.5;
padding: 16rpx;
background-color: #FAFAFA;
border-radius: 6rpx;
border: 1rpx solid #EEEEEE;
}
.instruction {
margin-top: 32rpx;
background-color: white;
padding: 32rpx;
border-radius: 8rpx;
min-width: 500rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.instruction-title {
font-size: 32rpx;
font-weight: bold;
color: #212121;
margin-bottom: 16rpx;
display: block;
}
.instruction-text {
font-size: 28rpx;
color: #757575;
line-height: 1.6;
display: block;
margin-bottom: 12rpx;
}
.footer {
margin-top: 40rpx;
padding: 24rpx;
text-align: center;
}
.footer-text {
font-size: 24rpx;
color: #9E9E9E;
}
/* 平板横屏适配 */
@media screen and (orientation: landscape) {
/* 横屏时优化元素排布 */
.container {
padding: 24rpx;
}
.header {
margin-bottom: 32rpx;
}
/* 在横屏模式下,核心操作区放在右侧,便于右手操作 */
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 40rpx;
min-height: calc(100vh - 100rpx);
}
/* 横屏时增大按钮点击区域 */
.nfc-button {
width: 500rpx;
height: 130rpx;
line-height: 130rpx;
/* 放在屏幕右侧,便于右手操作 */
align-self: flex-end;
margin-right: 60rpx;
}
/* 状态区域也放在右侧 */
.status-section {
min-width: 600rpx;
align-self: flex-end;
margin-right: 60rpx;
}
/* 结果区域在横屏时更宽,方便查看完整信息 */
.result-section {
min-width: 800rpx;
margin: 32rpx auto;
}
/* 操作指南放在左侧,不干扰核心操作区 */
.instruction {
position: absolute;
left: 40rpx;
top: 180rpx;
min-width: 400rpx;
max-width: 450rpx;
padding: 24rpx;
background-color: rgba(255, 255, 255, 0.95);
}
/* 底部信息在横屏时放在左侧底部 */
.footer {
position: absolute;
left: 40rpx;
bottom: 30rpx;
text-align: left;
}
}
/* 安卓平板特殊适配 */
@media (min-width: 768px) and (min-height: 500px) {
/* 优化触控体验确保所有可点击元素至少有48px x 48px的点击区域 */
.nfc-button {
min-height: 120rpx; /* 约等于48px在rpx中的值 */
min-width: 300rpx;
touch-action: manipulation; /* 防止双击缩放,提高点击响应速度 */
}
.copy-button {
min-height: 80rpx;
min-width: 160rpx;
touch-action: manipulation;
}
/* 平板上增大字体和间距,提升可读性 */
.title {
letter-spacing: 2rpx;
}
.status-text {
letter-spacing: 1rpx;
}
.result-value {
letter-spacing: 2rpx;
font-size: 42rpx; /* 在平板上进一步增大ID显示字体 */
padding: 24rpx;
}
}
/* 大型平板适配10英寸以上 */
@media (min-width: 1024px) {
.container {
padding: 48rpx;
}
.title {
font-size: 64rpx;
}
.nfc-button {
width: 700rpx;
height: 150rpx;
line-height: 150rpx;
font-size: 44rpx;
}
.status-section,
.result-section {
min-width: 900rpx;
padding: 40rpx;
}
/* 大型平板上,优化内容区的最大宽度,避免内容过宽影响阅读 */
.content {
max-width: 70vw;
margin: 0 auto;
}
}
/* 适配不同尺寸的平板 */
@media screen and (min-width: 768px) {
.title {
font-size: 56rpx;
}
.nfc-button {
width: 700rpx;
height: 140rpx;
line-height: 140rpx;
font-size: 40rpx;
}
.status-text {
font-size: 36rpx;
}
.result-value {
font-size: 40rpx;
}
}
</style>

View File

@@ -6,7 +6,9 @@
:leftFlag="true" :rightFlag="true"
>
<template #right>
<view class="head-right" @click="handleQuestion">
<!-- 状态是4-已完成 5-已过期的不能再问题上报了 -->
<block v-if="optionObj.taskStatus==5||optionObj.taskStatus==4"></block>
<view v-else class="head-right" @click="handleQuestion">
<view class="btn-yellow">
<img :src="'static/images/polling/icon-repair.png'" class="img-repair" />新建问题上报
</view>
@@ -92,7 +94,7 @@
<view>{{ item.pointName }}</view>
</view>
<!-- #ifdef APP-PLUS -->
<view class="r-right" @click.stop="initNFC">
<view class="r-right" @click.stop="initNFC(item,index)">
<img :src="'static/images/polling/icon-NFCcode.png'" class="img-nfc" /> 开始识别
</view>
<!-- #endif -->
@@ -156,8 +158,10 @@
</block>
<view class="report-border" :style="{borderColor:index<optionObj.pointList.length-1?'#E7E7E7':'#fff'}"></view>
</view>
<view class="btn-submit">
<button type="primary" @click="handleSubmit" :loading="submitLoading">提交</button>
<!-- 状态是4-已完成 5-已过期的不能再修改了 -->
<block v-if="optionObj.taskStatus==5||optionObj.taskStatus==4"></block>
<view v-else class="btn-submit">
<button type="primary" @click="handleSubmit" :loading="submitLoading" >提交</button>
</view>
</block>
<view v-else class="no-data">
@@ -195,11 +199,20 @@
></customShowModal>
<!-- 图片放大 -->
<mediaPreview :visible="isVisible" :url="mediaUrl" @close="handlePreviewClose"></mediaPreview>
<mediaPreview :visible="isVisible"
:url="mediaUrl"
@close="handlePreviewClose"
></mediaPreview>
<!-- NFC 弹窗 -->
<NFCTemplate v-if="nfcShow" ref="nfcTemplateRef"
@close="nfcClose"
@confirm="nfcConfirm"
></NFCTemplate>
</view>
</template>
<script setup>
import { ref,onMounted,onUnmounted,nextTick,computed,reactive } from 'vue'
import { ref,onMounted,onUnmounted,nextTick,computed,reactive,getCurrentInstance } from 'vue'
import { onLoad,onHide} from '@dcloudio/uni-app';
import { MINIO_KEY } from '@/enums/cacheEnums';
import customHeader from '@/components/customHeader.vue';
@@ -208,14 +221,13 @@ import multipleSelect from "@/components/multipleSelect.vue";
import pollingShowModal from "@/components/pollingShowModal.vue";
import customShowModal from "@/components/customShowModal.vue"
import mediaPreview from "@/components/mediaPreview.vue"
import NFCTemplate from "@/components/NFCTemplate.vue"
import { parseTime } from '@/utils/datetime.js';
import { formatTaskStatus } from '@/utils/status.js';
import { taskGroupDetail,submitResult,minioUpload } from '@/api/polling.js'
import {compressImageUni} from '@/utils/common.js'
// import {uploadFileMinio} from '@/utils/minio.js'
// #ifdef APP-PLUS
let m1CardModule = uni.requireNativePlugin("LF-Sense-Card-M1")
// #endif
const { proxy } = getCurrentInstance();
let taskId = ref(undefined);
let groupId = ref(undefined);
@@ -227,6 +239,8 @@ onLoad(option => {
minioObj = JSON.parse(uni.getStorageSync(MINIO_KEY) || "\{\}")
// console.log(minioObj)
})
// 下拉刷新
@@ -431,15 +445,29 @@ const handlePreviewClose=()=>{
// nfc 处理
let cardUID = ref('');
let cardValue = ref('');
const initNFC = async(item) => {
m1CardModule.openNativeSenseCard({
'keyA': 'TestKey',
'sector': 0,
'block': 0
},ret => {
cardUID.value = ret.uid
cardValue.value = ret.code
})
let nfcShow = ref(false);
const nfcTemplateRef = ref(null);
// optionObj.pointList
let nfcIndex = ref(0);
const initNFC = async(item,index) => {
nfcShow.value = true;
nfcIndex.value=index;
// uni.navigateTo({url:'/pages/business/polling/nfcTest/index'})
// #ifdef APP-PLUS
setTimeout(()=>{
// console.log("nfcTemplateRef==",nfcTemplateRef.value)
if (nfcTemplateRef.value) {
nfcTemplateRef.value.listenNFCStatus();
}
},500)
// #endif
}
const nfcClose = async(item) => {
nfcShow.value = false;
}
const nfcConfirm=(result)=>{
console.log("confirm=>",result)
optionObj.pointList[nfcIndex].resultContent = result
}
// 扫二维码
@@ -576,6 +604,7 @@ onUnmounted(() => {
})
</script>
<style scoped>
.scroll-h{
/* #ifdef APP-PLUS */
height:calc(100vh - 78px) !important;

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,4 @@
## 1.0.22025-08-29
api 变更注意查看最新文档
## 1.0.02025-07-30

Binary file not shown.

View File

@@ -0,0 +1,105 @@
{
"id": "ohyes-nfc",
"displayName": "android NFC ",
"version": "1.0.2",
"description": "ohyes-nfc",
"keywords": [
"android",
"uts",
"nfc"
],
"repository": "",
"engines": {
"HBuilderX": "^4.66",
"uni-app": "^4.27",
"uni-app-x": "^4.27"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "1.00"
},
"sourcecode": {
"price": "3000.00"
}
},
"contact": {
"qq": "1209808782"
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "android.permission.NFC\nandroid.permission.VIBRATE"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": {
"extVersion": "1.0.1",
"minVersion": ""
},
"vue3": {
"extVersion": "1.0.1",
"minVersion": ""
}
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,174 @@
*注意iOS平台的NFC功能暂无支持计划当前版本仅支持Android平台。*
# ohyes-nfc
一个用于uni-app的NFC读取插件支持Android的NFC标签读取功能。
## 免责声明
- 源码无后门代码。
- 由第三方SDK而导致的任何损失,与作者无关。
- 使用本插件而导致的任何损失,与作者无关。
- 使用该插件即代表同意免责声明。
## 是否使用第三方SDK?
-
## 我有需求怎么办?
发送邮件到 helloword202507@163.com 感谢支持
## 功能特性
- 🔍 **NFC标签读取** - 支持多种NFC标签类型的读取
- 🏷️ **多种标签格式** - 支持NDEF、Mifare Classic、Mifare Ultralight、ISO-DEP等
- 📊 **详细信息获取** - 获取标签ID、技术列表、内容、内存使用情况等
- 🔒 **安全验证** - 支持标签签名验证和密码保护检测
## 支持的标签类型
- **NDEF标签** - NFC Forum Type 1-5
- **Mifare Classic** - 1K/4K标签
- **Mifare Ultralight** - 轻量级标签
- **ISO-DEP** - ISO14443-4标准标签
- **NFC-A/B/F/V** - 各种NFC技术标准
## 安装
将插件导入到你的uni-app项目中
1. 下载插件包
2. 将插件放入项目的`uni_modules`目录
3. 重新编译项目
## 权限配置
### Android权限
插件会自动添加以下权限到AndroidManifest.xml
```xml
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.VIBRATE" />
```
## 使用方法
### 基本用法
参考示例工程
## API文档
### enableNFCRead()
启用NFC读取模式
### disableNFCRead()
停用NFC读取模式
### isNFCSupported()
检查当前设备是否支持NFC功能
**返回值:**
- `boolean` - true表示设备支持NFCfalse表示不支持
### isNFCEnabled()
检查当前设备NFC功能是否已打开
**返回值:**
- `boolean` - true表示NFC已启用false表示NFC未启用
### openNFCSettings()
跳转到系统NFC设置页面用户可以手动打开NFC开关
### onNFCListener(callback:(res:NFCInfo)=>void)
监听NFC返回数据,全局唯一,重复调用会覆盖
## NFCInfo数据结构
读取成功后返回的NFC信息对象包含以下字段
```typescript
interface NFCInfo {
id: string // 标签唯一ID
techList: string[] // 支持的技术列表
type: string // 标签类型
size: number // 标签容量(字节)
isWritable: boolean // 是否可写
content: string // 标签内容
atqa: string // ATQA值NFC-A技术
sak: string // SAK值NFC-A技术
signature: string // 签名状态
passwordProtected: boolean // 是否密码保护
dataFormat: string // 数据格式
recordCount: number // NDEF记录数量
usedBytes: number // 已使用字节数
freeBytes: number // 剩余字节数
}
```
### 字段说明
- **id**: 标签的唯一标识符,通常为十六进制字符串
- **techList**: 标签支持的NFC技术如["NfcA", "Ndef", "MifareUltralight"]
- **type**: 标签类型描述,如"Type 2 (Ultralight)"
- **size**: 标签总容量,单位为字节
- **isWritable**: 标签是否支持写入操作
- **content**: 解析后的标签内容包括文本、URL等
- **atqa/sak**: NFC-A技术的技术参数
- **signature**: 标签签名验证状态
- **dataFormat**: 数据格式,如"NFC Forum Type 2"
- **recordCount**: NDEF消息中的记录数量
- **usedBytes/freeBytes**: 内存使用情况
## 支持的内容类型
插件能够解析以下类型的NFC内容
### NDEF记录类型
- **文本记录** - 纯文本内容,支持多语言
- **URI记录** - 网址、电话、邮箱等链接
- **MIME记录** - 多媒体内容
- **外部类型** - 自定义格式
### 非NDEF标签
- **Mifare Classic** - 读取扇区和块数据
- **Mifare Ultralight** - 读取页面数据
- **ISO-DEP** - 读取历史字节和高层响应
## 平台支持
| 平台 | 支持状态 | 说明 |
|------|----------|------|
| Android | ✅ 完全支持 | 支持所有NFC功能包括多种标签类型 |
## 注意事项
1. **设备要求**: 设备必须支持NFC功能
2. **权限**: Android需要NFC权限iOS需要在设置中启用NFC
3. **距离**: NFC读取需要设备与标签距离很近通常<4cm
4. **性能**: 读取大容量标签时可能需要较长时间
5. **兼容性**: 不同厂商的NFC标签可能有细微差异
## 错误处理
常见错误及处理方法
- **"NFC不可用"** - 检查设备是否支持NFC并已启用
- **"读取超时"** - 确保标签与设备距离足够近
- **"标签格式不支持"** - 标签可能损坏或使用了不支持的格式
- **"权限被拒绝"** - 检查应用是否有NFC权限
## 更新日志
### v1.0.0
- 初始版本发布
- 支持Android平台NFC读取
- 支持多种标签类型和格式
- 提供详细的标签信息

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="com.ohyes.nfc">
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.VIBRATE" />
</manifest>

View File

@@ -0,0 +1,3 @@
{
"minSdkVersion": "21"
}

Binary file not shown.

View File

@@ -0,0 +1,3 @@
{
"deploymentTarget": "12"
}

Binary file not shown.

View File

@@ -0,0 +1,29 @@
export type NFCEnableReadMode = () => void
export type NFCDisableReadMode = () => void
export type NFCIsSupported = () => boolean
export type NFCIsEnabled = () => boolean
export type NFCOpenSettings = () => void
export type NFCListener = (info : NFCInfo) => void
export type NFCReadOptions = {
resolve : (res : NFCInfo) => void
reject : (e : String) => void
}
export type NFCInfo = {
id : string,
techList : Array<string>,
type : string,
size : number,
isWritable : boolean,
content : string,
atqa : string,
sak : string,
signature : string,
passwordProtected : boolean,
dataFormat : string,
recordCount : number,
usedBytes : number,
freeBytes : number
}

View File

@@ -0,0 +1,18 @@
## 1.0.82025-07-22
修复uniapp x打包报错AndroidManifest.xml 需要显式声明 android:exported="true"
## 1.0.72025-06-26
修改BUG
## 1.0.62024-07-24
更新刷卡监听界面背景透明化,提高界面友好度
## 1.0.52024-07-24
更新刷新提示窗体
## 1.0.42024-05-14
修改BUG
## 1.0.32024-05-11
优化
## 1.0.22024-05-11
更换图标
## 1.0.12024-04-12
增加设备支持与NFC启用判断
## 1.0.02024-04-12
提交插件

View File

@@ -0,0 +1 @@
<EFBFBD>,<2C>P<EFBFBD><50>=<3D>[&<26>۸<>6<EFBFBD><36>z<EFBFBD>s<EFBFBD>+<2B>g<02><>_<EFBFBD><5F>ϭ<EFBFBD><12><><EFBFBD><EFBFBD><EBA580>e<EFBFBD>$<24>,<14><><EFBFBD><EFBFBD>s`<60>_<><5F><07>B<EFBFBD><42><EFBFBD><EFBFBD>Wa<57><61>8W <0C><><EFBFBD>t<EFBFBD>O<EFBFBD><4F>{_Hf<48><66>ڎ@<15>-u+UX<55><02>*<2A><><EFBFBD>zl0f<30>U8K^G<><47><EFBFBD>0<14><><17><><EFBFBD>hpj<70><6A><EFBFBD><EFBFBD><EFBFBD>]^?

View File

@@ -0,0 +1,102 @@
{
"id": "read-nfc",
"displayName": "read-nfc",
"version": "1.0.8",
"description": "读取NFC卡号 read-nfc",
"keywords": [
"read-nfc"
],
"repository": "",
"engines": {
"uni-app": "^4.06",
"uni-app-x": "^4.07"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "50.00"
},
"sourcecode": {
"price": "200.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "<uses-permission android:name=\"android.permission.NFC\" /> \r\n<uses-feature android:name=\"android.hardware.nfc\" android:required=\"true\" />"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "√",
"nvue": "-",
"android": {
"extVersion": "1.0.0",
"minVersion": "21"
},
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": {
"extVersion": "1.0.0",
"minVersion": "21"
},
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
# read-nfc
## 插件示例
1、通过插件市场“使用 HBuilderX 导入示例项目”下载插件示例项目
2、通过插件市场“试用”导入插件到示例项目中
3、打包并运行自定义基座
使用了NFC的Android原生库需要打包使用自定义基座
## 引入插件对象
```
import * as nfc from "@/uni_modules/read-nfc";
```
## 调起NFC识别
```
data() {
return {
title: '读取NFC监听',
nfcCode:''
}
},
onLoad() {
this.gotoNfcActivity();//调起NFC识别并监听回调
},
methods: {
gotoNfcActivity() {
let that=this;
nfc.gotoNfcActivity(function(result){
that.nfcCode=result.message;
uni.showModal({
title: '提示',
content: result.message,
confirmText:'继续',
success: function (res) {
if (res.confirm) {
that.gotoNfcActivity();//继续识别
console.log('用户点击继续!');
} else if (res.cancel) {
console.log('用户点击取消!');
}
}
});
console.log('NFC',result);
});
}
}
```

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="io.dcloud.uni_modules.read_nfc">
<!--获取NFC权限-->
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<application>
<activity android:name="uts.sdk.modules.readNfc.NfcActivity" android:exported="true" android:launchMode="singleTop" android:theme="@android:style/Theme.Translucent">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,3 @@
{
"minSdkVersion": "21"
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="请靠近NFC卡"
android:gravity="center_horizontal"
android:src="@drawable/nfc" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:text="请靠近NFC卡"
android:textColor="#000"
android:textSize="20sp" />
</LinearLayout>
</RelativeLayout>

View File

@@ -0,0 +1,45 @@
/**
* interface.uts
* uts插件接口定义文件按规范定义接口文件可以在HBuilderX中更好的做到语法提示
*/
/**
* myApi 异步函数的参数在type里定义函数需要的参数以及api成功、失败的相关回调函数。
*/
export type MyApiOptions = {
paramA : boolean
success ?: (res : MyApiResult) => void
fail ?: (res : MyApiFail) => void
complete ?: (res : any) => void
}
/**
* 函数返回结果
* 可以是void, 基本数据类型自定义type, 或者其他类型。
* [可选实现]
*/
export type MyApiResult = {
fieldA : number,
fieldB : boolean,
fieldC : string
}
/**
* 错误码
* 根据uni错误码规范要求建议错误码以90开头以下是错误码示例
* - 9010001 错误信息1
* - 9010002 错误信息2
*/
export type MyApiErrorCode = 9010001 | 9010002;
/**
* myApi 的错误回调参数
*/
export interface MyApiFail extends IUniError {
errCode : MyApiErrorCode
};
/* 异步函数定义 */
export type MyApi = (options : MyApiOptions) => void
/* 同步函数定义 */
export type MyApiSync = (paramA : boolean) => MyApiResult

Binary file not shown.

431
src/utils/hexiii-nfc.js Normal file
View File

@@ -0,0 +1,431 @@
// NFC工具类 - 优化版,适配安卓平板环境
// 包路径定义
const package_TECH_DISCOVERED = 'android.nfc.action.TECH_DISCOVERED';
const package_TAG_DISCOVERED = 'android.nfc.action.TAG_DISCOVERED';
const package_NDEF_DISCOVERED = 'android.nfc.action.NDEF_DISCOVERED';
// 全局变量
let nfcAdapter = null; // NFC适配器实例
let mainActivity = null; // 主Activity实例
let pendingIntent = null; // 用于NFC前台调度的PendingIntent
let currentPromiseResolve = null; // 当前Promise的resolve函数引用
let currentPromiseReject = null; // 当前Promise的reject函数引用
let isNFCInitialized = false; // NFC初始化状态
let noNFC = false; // 无NFC功能标识
let timeoutId = null; // 超时定时器ID
let isListening = false; // 是否正在监听状态
// 支持的NFC技术列表 - 优化以适配更多平板设备
let techListsArray = [
['android.nfc.tech.IsoDep'],
['android.nfc.tech.NfcA'],
['android.nfc.tech.NfcB'],
['android.nfc.tech.NfcF'],
['android.nfc.tech.NfcV'],
['android.nfc.tech.NdefFormatable'],
['android.nfc.tech.MifareClassic'],
['android.nfc.tech.MifareUltralight']
];
/**
* NFC工具类 - 处理安卓平台NFC卡片读取
*/
export default {
/**
* 初始化NFC功能并监听NFC状态
* @param {number} timeoutMs - 超时时间毫秒默认30秒
* @returns {Promise<string>} 返回读取到的NFC卡片ID
*/
listenNFCStatus: function(timeoutMs = 30000) {
console.log("NFC: 开始监听NFC状态超时时间:" + timeoutMs + "ms");
// 先停止之前的监听(如果存在)
this.stopNFCListening();
// 使用Promise封装NFC读取过程
return new Promise((resolve, reject) => {
try {
// 保存当前Promise的引用
currentPromiseResolve = resolve;
currentPromiseReject = reject;
isListening = true;
// 获取主Activity
if (!mainActivity) {
mainActivity = plus.android.runtimeMainActivity();
}
// 初始化NFC适配器
if (!this._initializeNFC()) {
// 如果初始化失败且是因为设备不支持NFC或NFC未开启则直接reject
if (noNFC) {
reject(new Error('设备不支持NFC或NFC功能未开启'));
} else {
reject(new Error('NFC初始化失败'));
}
return;
}
// 设置NFC前台调度
if (!this._setupForegroundDispatch()) {
reject(new Error('NFC前台调度设置失败'));
return;
}
// 设置超时
timeoutId = setTimeout(() => {
if (isListening && currentPromiseReject === reject) {
console.log('NFC: 读取超时');
const errorMsg = 'NFC读取超时请重试';
this._cleanupResources();
reject(new Error(errorMsg));
}
}, timeoutMs);
} catch (error) {
console.error('NFC: 监听NFC状态时发生错误:', error);
this._cleanupResources();
reject(new Error('NFC操作异常: ' + error.message));
}
});
},
/**
* 停止NFC监听
*/
stopNFCListening: function() {
console.log('NFC: 停止监听NFC状态');
this._cleanupResources();
if (currentPromiseReject) {
const reject = currentPromiseReject;
currentPromiseReject = null;
// 不抛出错误,而是安静地停止监听
}
},
/**
* 清理NFC资源
* @private
*/
_cleanupResources: function() {
try {
// 清除超时定时器
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
// 禁用前台调度
if (nfcAdapter && mainActivity) {
try {
nfcAdapter.disableForegroundDispatch(mainActivity);
console.log('NFC: 已禁用前台调度');
} catch (e) {
console.warn('NFC: 禁用前台调度时出错:', e);
}
}
// 重置状态
isListening = false;
currentPromiseResolve = null;
currentPromiseReject = null;
} catch (error) {
console.error('NFC: 清理资源时发生错误:', error);
}
},
/**
* 初始化NFC适配器
* @private
* @returns {boolean} 初始化是否成功
*/
_initializeNFC: function() {
try {
// 如果已经初始化,则直接返回成功
if (isNFCInitialized && nfcAdapter) {
return true;
}
// 导入必要的类
const NfcAdapter = plus.android.importClass('android.nfc.NfcAdapter');
// 获取NFC适配器实例
nfcAdapter = NfcAdapter.getDefaultAdapter(mainActivity);
// 检查设备是否支持NFC
if (nfcAdapter == null) {
console.warn('NFC: 设备不支持NFC');
noNFC = true;
return false;
}
// 检查NFC是否已启用
if (!nfcAdapter.isEnabled()) {
console.warn('NFC: NFC功能未启用');
noNFC = true;
return false;
}
// 设置初始化状态为成功
isNFCInitialized = true;
noNFC = false;
console.log('NFC: 初始化成功');
return true;
} catch (error) {
console.error('NFC: 初始化失败:', error);
return false;
}
},
/**
* 设置NFC前台调度 - 优化以适配平板大屏和不同Android版本
* @private
* @returns {boolean} 设置是否成功
*/
_setupForegroundDispatch: function() {
try {
// 导入必要的类
const Intent = plus.android.importClass('android.content.Intent');
const PendingIntent = plus.android.importClass('android.app.PendingIntent');
const IntentFilter = plus.android.importClass('android.content.IntentFilter');
const NfcAdapter = plus.android.importClass('android.nfc.NfcAdapter');
// 创建Intent和PendingIntent
const intent = new Intent(mainActivity, mainActivity.getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
// 适配不同Android版本的PendingIntent标志
let flags = PendingIntent.FLAG_UPDATE_CURRENT;
// 检查Android版本适配Android 12+
try {
const Build = plus.android.importClass('android.os.Build');
if (Build.VERSION.SDK_INT >= 31) { // Android 12 (API 31)
flags = PendingIntent.FLAG_MUTABLE;
console.log('NFC: 检测到Android 12+使用FLAG_MUTABLE标志');
}
} catch (e) {
console.log('NFC: 无法检测Android版本使用默认标志');
}
pendingIntent = PendingIntent.getActivity(mainActivity, 0, intent, flags);
// 创建多个IntentFilter以提高平板设备上的识别率
const ndef = new IntentFilter(package_TECH_DISCOVERED);
const tag = new IntentFilter(package_TAG_DISCOVERED);
const ndefDiscovered = new IntentFilter(package_NDEF_DISCOVERED);
try {
ndef.addDataType('*/*');
ndefDiscovered.addDataType('*/*');
} catch (e) {
console.warn('NFC: 添加数据类型失败:', e);
}
const intentFiltersArray = [ndef, tag, ndefDiscovered];
// 移除旧的监听器,避免重复添加
try {
plus.globalEvent.removeEventListener('newintent', this._handleNewIntent);
} catch (e) {
// 忽略错误
}
// 设置全局事件监听器
plus.globalEvent.addEventListener('newintent', this._handleNewIntent.bind(this));
// 启用前台调度 - 优化平板设备上的NFC识别
nfcAdapter.enableForegroundDispatch(mainActivity, pendingIntent, intentFiltersArray, techListsArray);
console.log('NFC: 前台调度设置成功已添加多种Intent过滤器');
return true;
} catch (error) {
console.error('NFC: 前台调度设置失败:', error);
return false;
}
},
/**
* 处理新的NFC Intent - 优化以支持多种NFC Intent类型
* @private
*/
_handleNewIntent: function() {
try {
console.log('NFC: 接收到新的Intent');
// 导入必要的类
const NfcAdapter = plus.android.importClass('android.nfc.NfcAdapter');
// 获取当前Intent
const intent = mainActivity.getIntent();
const action = intent.getAction();
// 检查是否是NFC相关的Intent - 支持多种NFC Intent类型
if (action === package_TECH_DISCOVERED ||
action === package_TAG_DISCOVERED ||
action === package_NDEF_DISCOVERED) {
console.log('NFC: 检测到NFC卡片动作类型:', action);
// 读取NFC ID
const nfcId = this._readNFCId(intent, NfcAdapter);
// 如果成功读取到ID且Promise仍在等待则解析Promise
if (nfcId && currentPromiseResolve && isListening) {
console.log('NFC: 成功读取到ID:', nfcId);
const resolve = currentPromiseResolve;
// 清理资源
this._cleanupResources();
// 延迟解析Promise确保资源完全清理
setTimeout(() => {
resolve(nfcId);
}, 100);
}
}
} catch (error) {
console.error('NFC: 处理Intent时发生错误:', error);
if (currentPromiseReject && isListening) {
const reject = currentPromiseReject;
this._cleanupResources();
reject(new Error('处理NFC数据时出错: ' + error.message));
}
}
},
/**
* 读取NFC卡片ID - 优化以适配安卓平板
* @private
* @param {android.content.Intent} intent - Intent对象
* @param {android.nfc.NfcAdapter} adapter - NfcAdapter类
* @returns {string} NFC卡片ID的十六进制字符串
*/
_readNFCId: function(intent, adapter) {
try {
// 获取NFC ID字节数组 - 尝试多种方式获取
let bytesId = null;
// 主要方式
bytesId = intent.getByteArrayExtra(adapter.EXTRA_ID);
// 如果失败,尝试其他方式
if (!bytesId || bytesId.length === 0) {
try {
// 获取标签对象
const tag = intent.getParcelableExtra(adapter.EXTRA_TAG);
if (tag) {
bytesId = tag.getId();
console.log('NFC: 通过Tag对象获取ID');
}
} catch (e) {
console.warn('NFC: 尝试通过Tag对象获取ID失败:', e);
}
}
if (!bytesId || bytesId.length === 0) {
console.warn('NFC: 未读取到ID字节数组');
return null;
}
// 转换为十六进制字符串
const hexId = this.byteArrayToHexString(bytesId);
console.log('NFC: 读取到ID:', hexId, '长度:', bytesId.length);
// 尝试获取更多卡片信息,增强平板使用体验
try {
const tag = intent.getParcelableExtra(adapter.EXTRA_TAG);
if (tag) {
const Tag = plus.android.importClass('android.nfc.Tag');
const techList = tag.getTechList();
console.log('NFC: 卡片支持的技术:', techList);
}
} catch (e) {
console.log('NFC: 获取卡片技术信息失败:', e);
}
return hexId;
} catch (error) {
console.error('NFC: 读取ID时发生错误:', error);
return null;
}
},
/**
* 检查NFC是否可用
* @returns {Object} 包含支持状态和启用状态的对象
*/
checkNfcAvailability: function() {
try {
const main = plus.android.runtimeMainActivity();
const NfcAdapter = plus.android.importClass('android.nfc.NfcAdapter');
const adapter = NfcAdapter.getDefaultAdapter(main);
if (adapter === null) {
return {
supported: false,
enabled: false,
error: '设备不支持NFC'
};
}
const isEnabled = adapter.isEnabled();
return {
supported: true,
enabled: isEnabled,
error: isEnabled ? null : 'NFC未启用'
};
} catch (error) {
console.error('NFC: 检查可用性失败:', error);
return {
supported: false,
enabled: false,
error: error.message || '检查失败'
};
}
},
/**
* 将字节数组转换为十六进制字符串
* @param {Uint8Array|Array} inarray - 字节数组
* @returns {string} 十六进制字符串
*/
byteArrayToHexString: function(inarray) {
const hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
let out = "";
for (let j = 0; j < inarray.length; j++) {
const inn = inarray[j] & 0xff;
const i = (inn >>> 4) & 0x0f;
out += hex[i];
const k = inn & 0x0f;
out += hex[k];
}
return out;
},
/**
* 清理NFC资源
*/
cleanupNFC: function() {
try {
// 使用私有清理方法
this._cleanupResources();
// 移除事件监听器
try {
plus.globalEvent.removeEventListener('newintent', this._handleNewIntent);
console.log('NFC: 已移除newintent事件监听器');
} catch (e) {
console.warn('NFC: 移除事件监听器时出错:', e);
}
// 重置初始化状态
isNFCInitialized = false;
console.log('NFC: 资源已完全清理');
} catch (error) {
console.error('NFC: 清理资源时发生错误:', error);
}
}
};

244
src/utils/ouu-nfc.js Normal file
View File

@@ -0,0 +1,244 @@
// 包路径
const package_NdefRecord = 'android.nfc.NdefRecord';
const package_NdefMessage = 'android.nfc.NdefMessage';
const package_TECH_DISCOVERED = 'android.nfc.action.TECH_DISCOVERED';
const package_Intent = 'android.content.Intent';
const package_Activity = 'android.app.Activity';
const package_PendingIntent = 'android.app.PendingIntent';
const package_IntentFilter = 'android.content.IntentFilter';
const package_NfcAdapter = 'android.nfc.NfcAdapter';
const package_Ndef = 'android.nfc.tech.Ndef';
const package_NdefFormatable = 'android.nfc.tech.NdefFormatable';
const package_Parcelable = 'android.os.Parcelable';
const package_String = 'java.lang.String';
let NfcAdapter;
let NdefRecord;
let NdefMessage;
let Uri;
let readyWriteData = false;
let readyRead = false;
let noNFC = false;
let techListsArray = [
['android.nfc.tech.IsoDep'],
['android.nfc.tech.NfcA'],
['android.nfc.tech.NfcB'],
['android.nfc.tech.NfcF'],
['android.nfc.tech.Nfcf'],
['android.nfc.tech.NfcV'],
['android.net.Uri'],
['android.nfc.tech.NdefFormatable'],
['android.nfc.tech.MifareClassi'],
['android.nfc.tech.MifareUltralight']
];
// 要写入的数据
let text = '';
let readResult = '';
export default {
listenNFCStatus: function () {
let that = this;
try {
let main = plus.android.runtimeMainActivity();
let Intent = plus.android.importClass('android.content.Intent');
let Activity = plus.android.importClass('android.app.Activity');
let PendingIntent = plus.android.importClass('android.app.PendingIntent');
let IntentFilter = plus.android.importClass('android.content.IntentFilter');
Uri = plus.android.importClass('android.net.Uri');
NfcAdapter = plus.android.importClass('android.nfc.NfcAdapter');
let nfcAdapter = NfcAdapter.getDefaultAdapter(main);
if(nfcAdapter == null){
uni.showToast({
title: '设备不支持NFC',
icon: 'none'
})
noNFC = true;
return;
}
if (!nfcAdapter.isEnabled()) {
uni.showToast({
title: '请在系统设置中先启用NFC功能',
icon: 'none'
});
noNFC = true;
return;
}else{
noNFC = false;
}
let intent = new Intent(main, main.getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
let pendingIntent = PendingIntent.getActivity(main, 0, intent, 0);
let ndef = new IntentFilter("android.nfc.action.TECH_DISCOVERED");
ndef.addDataType("*/*");
let intentFiltersArray = [ndef];
plus.globalEvent.addEventListener('newintent',function() {
console.log('newintent running');
// 监听 NFC
setTimeout(that.nfcRuning(), 1000);
});
plus.globalEvent.addEventListener('pause',function(e) {
console.log('pause running');
if (nfcAdapter) {
//关闭前台调度系统
//恢复默认状态
nfcAdapter.disableForegroundDispatch(main);
}
});
plus.globalEvent.addEventListener('resume',function(e) {
console.log('resume running');
if (nfcAdapter) {
//开启前台调度系统
nfcAdapter.enableForegroundDispatch(main, pendingIntent, intentFiltersArray, techListsArray);
}
});
nfcAdapter.enableForegroundDispatch(main, pendingIntent, intentFiltersArray, techListsArray);
} catch (e) {
console.error(e);
}
},
nfcRuning: function () {
NdefRecord = plus.android.importClass("android.nfc.NdefRecord");
NdefMessage = plus.android.importClass("android.nfc.NdefMessage");
let main = plus.android.runtimeMainActivity();
let intent = main.getIntent();
let that = this;
console.log("action type:" + intent.getAction());
if (package_TECH_DISCOVERED == intent.getAction()) {
if (readyWriteData) {
that.write(intent);
readyWriteData = false;
} else if (readyRead) {
that.read(intent);
readyRead = false;
}
}
},
write(intent) {
try {
toast('请勿移开标签 正在写入...');
console.log("text=" + text);
let textBytes = plus.android.invoke(text, "getBytes");
// image/jpeg text/plain
let uri = Uri.parse(text)
let message = new NdefMessage([new NdefRecord.createUri(uri)]);
console.log("====message===>", message)
let Ndef = plus.android.importClass('android.nfc.tech.Ndef');
let NdefFormatable = plus.android.importClass('android.nfc.tech.NdefFormatable');
let tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
let ndef = Ndef.get(tag);
if (ndef != null) {
// 待写入的数据长度
let size = message.toByteArray().length;
ndef.connect();
if (!ndef.isWritable()) {
toast('tag不允许写入');
return;
}
if (ndef.getMaxSize() < size) {
toast('文件大小超出容量!');
return;
}
ndef.writeNdefMessage(message);
toast('写入数据成功!');
return;
} else {
let format = NdefFormatable.get(tag);
if (format != null) {
try {
format.connect();
format.format(message);
toast('格式化tag并且写入message');
return;
} catch (e) {
toast('格式化tag失败.');
return;
}
} else {
toast('Tag不支持NDEF');
return;
}
}
} catch (e) {
toast('写入失败');
console.log("error=" + e);
}
},
read(intent) {
toast('请勿移开标签正在读取数据');
let that = this;
// NFC id
let bytesId = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
let nfc_id = that.byteArrayToHexString(bytesId);
console.log('nfc_id:', nfc_id);
let Parcelable = plus.android.importClass("android.os.Parcelable");
let rawmsgs = intent.getParcelableArrayExtra("android.nfc.extra.NDEF_MESSAGES");
//let rawmsgs = intent.getParcelableArrayExtra();
if(rawmsgs != null && rawmsgs.length > 0) {
let records = rawmsgs[0].getRecords();
let result = records[0].getPayload();
let data = plus.android.newObject("java.lang.String", result);
toast('NFC 数据:' + data);
plus.runtime.openURL(data, function(res) {
console.log("NFC 数据----", res);
});
console.log('NFC 数据:',data);
readResult = data;
// return data;
}else{
toast('没有读取到数据');
return ""
}
},
byteArrayToHexString: function (inarray) { // converts byte arrays to string
let i, j, inn;
let hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
let out = "";
for(j = 0; j < inarray.length; ++j) {
inn = inarray[j] & 0xff;
i = (inn >>> 4) & 0x0f;
out += hex[i];
i = inn & 0x0f;
out += hex[i];
}
return out;
},
writeData: function () {
if(noNFC){
toast('请检查设备是否支持并开启 NFC 功能!');
return;
}
// 监听事件,触发条件
readyWriteData = true;
toast('请将NFC标签靠近');
},
readData: function () {
if(noNFC){
toast('请检查设备是否支持并开启 NFC 功能!');
return;
}
// 监听事件,触发条件
readyRead = true;
toast('请将NFC标签靠近');
},
// 输入文本改变
inputChanage: function (res) {
console.log("正在编辑写入数据...", res)
text = res
}
}
function toast(content){
uni.showToast({
title: content,
icon: 'none'
})
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>View</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="app.css" />
<script>var __uniConfig = {"globalStyle":{},"darkmode":false}</script>
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
</head>
<body>
<div id="app"></div>
<script src="uni-app-view.umd.js"></script>
<script src="app-wxs.js"></script>
<script src="app-renderjs.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
(function(){})();

View File

@@ -0,0 +1,2 @@
var __renderjsModules={};
__renderjsModules["1e78dde8"]=(()=>{var a=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var d=Object.prototype.hasOwnProperty;var p=(t,n)=>{for(var i in n)a(t,i,{get:n[i],enumerable:!0})},f=(t,n,i,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of u(n))!d.call(t,r)&&r!==i&&a(t,r,{get:()=>n[r],enumerable:!(o=l(n,r))||o.enumerable});return t};var g=t=>f(a({},"__esModule",{value:!0}),t);var m={};p(m,{default:()=>w});var e={};window&&!window.$mescrollRenderInit&&(window.$mescrollRenderInit=!0,window.addEventListener("touchstart",function(t){e.disabled()||(e.startPoint=e.getPoint(t))},{passive:!0}),window.addEventListener("touchmove",function(t){if(!e.disabled()&&!(e.getScrollTop()>0)){var n=e.getPoint(t),i=n.y-e.startPoint.y;if(i>0&&!e.isDownScrolling&&!e.optDown.isLock&&(!e.isUpScrolling||e.isUpScrolling&&e.isUpBoth)){for(var o=t.target,r=!1;o&&o.tagName&&o.tagName!=="UNI-PAGE-BODY"&&o.tagName!="BODY";){var s=o.classList;if(s&&s.contains("mescroll-render-touch")){r=!0;break}o=o.parentNode}r&&t.cancelable&&!t.defaultPrevented&&t.preventDefault()}}},{passive:!1}));e.getScrollTop=function(){return e.scrollTop||0};e.disabled=function(){return!e.optDown||!e.optDown.use||e.optDown.native};e.getPoint=function(t){return t?t.touches&&t.touches[0]?{x:t.touches[0].pageX,y:t.touches[0].pageY}:t.changedTouches&&t.changedTouches[0]?{x:t.changedTouches[0].pageX,y:t.changedTouches[0].pageY}:{x:t.clientX,y:t.clientY}:{x:0,y:0}};function h(t){e.optDown=t.optDown,e.scrollTop=t.scrollTop,e.isDownScrolling=t.isDownScrolling,e.isUpScrolling=t.isUpScrolling,e.isUpBoth=t.isUpBoth}var v={data(){return{propObserver:h}}},c=v;var w={mixins:[c]};return g(m);})();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
var __wxsModules={};
__wxsModules["199dc8ed"]=(()=>{var f=(o,e)=>()=>(e||o((e={exports:{}}).exports,e),e.exports);var w=f((m,c)=>{var t={};t.onMoving=function(o,e,n){o.requestAnimationFrame(function(){o.selectComponent(".mescroll-wxs-content").setStyle({"will-change":"transform",transform:"translateY("+n+"px)",transition:""});var r=o.selectComponent(".mescroll-wxs-progress");r&&r.setStyle({transform:"rotate("+360*e+"deg)"})})};t.showLoading=function(o){t.downHight=t.optDown.offset,o.requestAnimationFrame(function(){o.selectComponent(".mescroll-wxs-content").setStyle({"will-change":"auto",transform:"translateY("+t.downHight+"px)",transition:"transform 300ms"})})};t.endDownScroll=function(o){t.downHight=0,t.isDownScrolling=!1,o.requestAnimationFrame(function(){o.selectComponent(".mescroll-wxs-content").setStyle({"will-change":"auto",transform:"translateY(0)",transition:"transform 300ms"})})};t.clearTransform=function(o){o.requestAnimationFrame(function(){o.selectComponent(".mescroll-wxs-content").setStyle({"will-change":"",transform:"",transition:""})})};function h(o){t.optDown=o.optDown,t.scrollTop=o.scrollTop,t.bodyHeight=o.bodyHeight,t.isDownScrolling=o.isDownScrolling,t.isUpScrolling=o.isUpScrolling,t.isUpBoth=o.isUpBoth,t.isScrollBody=o.isScrollBody,t.startTop=o.scrollTop}function u(o,e,n){t.disabled()||o.callType&&(o.callType==="showLoading"?t.showLoading(n):o.callType==="endDownScroll"?t.endDownScroll(n):o.callType==="clearTransform"&&t.clearTransform(n))}function p(o,e){t.downHight=0,t.startPoint=t.getPoint(o),t.startTop=t.getScrollTop(),t.startAngle=0,t.lastPoint=t.startPoint,t.maxTouchmoveY=t.getBodyHeight()-t.optDown.bottomOffset,t.inTouchend=!1,t.callMethod(e,{type:"setWxsProp"})}function d(o,e){var n=!0;if(t.disabled())return n;var r=t.getScrollTop(),l=t.getPoint(o),i=l.y-t.startPoint.y;if(i>0&&(t.isScrollBody&&r<=0||!t.isScrollBody&&(r<=0||r<=t.optDown.startTop&&r===t.startTop))&&!t.inTouchend&&!t.isDownScrolling&&!t.optDown.isLock&&(!t.isUpScrolling||t.isUpScrolling&&t.isUpBoth)){if(t.startAngle||(t.startAngle=t.getAngle(t.lastPoint,l)),t.startAngle<t.optDown.minAngle)return n;if(t.maxTouchmoveY>0&&l.y>=t.maxTouchmoveY)return t.inTouchend=!0,s(o,e),n;n=!1;var a=l.y-t.lastPoint.y;t.downHight<t.optDown.offset?(t.movetype!==1&&(t.movetype=1,t.callMethod(e,{type:"setLoadType",downLoadType:1}),t.isMoveDown=!0),t.downHight+=a*t.optDown.inOffsetRate):(t.movetype!==2&&(t.movetype=2,t.callMethod(e,{type:"setLoadType",downLoadType:2}),t.isMoveDown=!0),a>0?t.downHight+=a*t.optDown.outOffsetRate:t.downHight+=a),t.downHight=Math.round(t.downHight);var g=t.downHight/t.optDown.offset;t.onMoving(e,g,t.downHight)}return t.lastPoint=l,n}function s(o,e){if(t.isMoveDown)t.downHight>=t.optDown.offset?(t.downHight=t.optDown.offset,t.callMethod(e,{type:"triggerDownScroll"})):(t.downHight=0,t.callMethod(e,{type:"endDownScroll"})),t.movetype=0,t.isMoveDown=!1;else if(!t.isScrollBody&&t.getScrollTop()===t.startTop){var n=t.getPoint(o).y-t.startPoint.y<0;if(n){var r=t.getAngle(t.getPoint(o),t.startPoint);r>80&&t.callMethod(e,{type:"triggerUpScroll"})}}t.callMethod(e,{type:"setWxsProp"})}t.disabled=function(){return!t.optDown||!t.optDown.use||t.optDown.native};t.getPoint=function(o){return o?o.touches&&o.touches[0]?{x:o.touches[0].pageX,y:o.touches[0].pageY}:o.changedTouches&&o.changedTouches[0]?{x:o.changedTouches[0].pageX,y:o.changedTouches[0].pageY}:{x:o.clientX,y:o.clientY}:{x:0,y:0}};t.getAngle=function(o,e){var n=Math.abs(o.x-e.x),r=Math.abs(o.y-e.y),l=Math.sqrt(n*n+r*r),i=0;return l!==0&&(i=Math.asin(r/l)/Math.PI*180),i};t.getScrollTop=function(){return t.scrollTop||0};t.getBodyHeight=function(){return t.bodyHeight||0};t.callMethod=function(o,e){o&&o.callMethod("wxsCall",e)};c.exports={propObserver:h,callObserver:u,touchstartEvent:p,touchmoveEvent:d,touchendEvent:s}});return w();})();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"@platforms":["android","iPhone","iPad"],"id":"__UNI__4C459F4","name":"718友晟","version":{"name":"1.0.0","code":"100"},"description":"","developer":{"name":"","email":"","url":""},"permissions":{"Push":{},"Camera":{},"Barcode":{},"VideoPlayer":{},"UniNView":{"description":"UniNView原生渲染"}},"plus":{"useragent":{"value":"uni-app","concatenate":true},"splashscreen":{"target":"id:1","autoclose":true,"waiting":false,"delay":0},"popGesture":"close","launchwebview":{"id":"1","kernel":"WKWebview"},"compatible":{"ignoreVersion":true},"usingComponents":true,"nvueStyleCompiler":"uni-app","compilerVersion":3,"nativePlugins":{"Tm-TmSafeSaveFileModule":{"__plugin_info__":{"name":"TmSafeSaveFileModule隐私数据保存","description":"TmSafeSaveFileModule隐私数据保存","platforms":"Android","url":"","android_package_name":"","ios_bundle_id":"","isCloud":false,"bought":-1,"pid":"","parameters":{}}}},"statusbar":{"immersed":"supportedDevice","style":"dark","background":"#F8F8F8"},"uniStatistics":{"enable":false},"allowsInlineMediaPlayback":true,"safearea":{"background":"#000000","bottom":{"offset":"auto"}},"uni-app":{"control":"uni-v3","vueVersion":"3","compilerVersion":"4.76","nvueCompiler":"uni-app","renderer":"auto","nvue":{"flex-direction":"column"},"nvueLaunchMode":"normal","webView":{"minUserAgentVersion":"49.0"}},"tabBar":{"position":"bottom","color":"#919191","selectedColor":"#ffffff","borderStyle":"#ffffff","blurEffect":"none","fontSize":"12px","iconWidth":"24px","spacing":"3px","height":"50px","list":[{"pagePath":"pages/home/home","iconPath":"/static/images/tabs/menu-home.png","selectedIconPath":"/static/images/tabs/menu-home-on.png","text":"首页"},{"pagePath":"pages/business/business","iconPath":"/static/images/tabs/menu-business.png","selectedIconPath":"/static/images/tabs/menu-business-on.png","text":"业务中心"},{"pagePath":"pages/notice/notice","iconPath":"/static/images/tabs/menu-info.png","selectedIconPath":"/static/images/tabs/menu-info-on.png","text":"消息"},{"pagePath":"pages/userinfo/userinfo","iconPath":"/static/images/tabs/menu-me.png","selectedIconPath":"/static/images/tabs/menu-me-on.png","text":"我的"}],"backgroundColor":"#000000","selectedIndex":0,"shown":true}},"app-harmony":{"useragent":{"value":"uni-app","concatenate":true},"uniStatistics":{"enable":false},"safearea":{"background":"#000000","bottom":{"offset":"auto"}}},"launch_path":"__uniappview.html"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More