vite-uni-dev-tool 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +8 -0
  2. package/dist/const.cjs +1 -1
  3. package/dist/const.d.ts +1 -0
  4. package/dist/const.d.ts.map +1 -1
  5. package/dist/const.js +1 -1
  6. package/dist/core.d.ts.map +1 -1
  7. package/dist/core.js +2 -2
  8. package/dist/i18n/locales/en.cjs +1 -1
  9. package/dist/i18n/locales/en.d.ts +5 -0
  10. package/dist/i18n/locales/en.d.ts.map +1 -1
  11. package/dist/i18n/locales/en.js +1 -1
  12. package/dist/i18n/locales/zh-Hans.cjs +1 -1
  13. package/dist/i18n/locales/zh-Hans.d.ts +5 -0
  14. package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
  15. package/dist/i18n/locales/zh-Hans.js +1 -1
  16. package/dist/modules/devIntercept/index.cjs +9 -9
  17. package/dist/modules/devIntercept/index.d.ts +1 -1
  18. package/dist/modules/devIntercept/index.d.ts.map +1 -1
  19. package/dist/modules/devIntercept/index.js +9 -9
  20. package/dist/modules/devStore/index.cjs +1 -1
  21. package/dist/modules/devStore/index.d.ts.map +1 -1
  22. package/dist/modules/devStore/index.js +1 -1
  23. package/dist/plugins/uniDevTool/transform/transformMain.cjs +3 -3
  24. package/dist/plugins/uniDevTool/transform/transformMain.d.ts +2 -1
  25. package/dist/plugins/uniDevTool/transform/transformMain.d.ts.map +1 -1
  26. package/dist/plugins/uniDevTool/transform/transformMain.js +3 -3
  27. package/dist/plugins/uniDevTool/transform/transformVue.cjs +31 -25
  28. package/dist/plugins/uniDevTool/transform/transformVue.d.ts +2 -1
  29. package/dist/plugins/uniDevTool/transform/transformVue.d.ts.map +1 -1
  30. package/dist/plugins/uniDevTool/transform/transformVue.js +30 -24
  31. package/dist/plugins/uniDevTool/uniDevTool.cjs +3 -3
  32. package/dist/plugins/uniDevTool/uniDevTool.d.ts +3 -1
  33. package/dist/plugins/uniDevTool/uniDevTool.d.ts.map +1 -1
  34. package/dist/plugins/uniDevTool/uniDevTool.js +3 -3
  35. package/dist/type.d.ts +3 -0
  36. package/dist/type.d.ts.map +1 -1
  37. package/dist/v3/DevTool/components/BluetoothList/BluetoothItem.vue +199 -199
  38. package/dist/v3/DevTool/components/BluetoothList/BluetoothTool.vue +730 -730
  39. package/dist/v3/DevTool/components/BluetoothList/index.vue +167 -167
  40. package/dist/v3/DevTool/components/CaptureScreen/index.vue +109 -109
  41. package/dist/v3/DevTool/components/ConsoleList/ConsoleItem.vue +230 -225
  42. package/dist/v3/DevTool/components/ConsoleList/RunJSInput.vue +247 -247
  43. package/dist/v3/DevTool/components/ConsoleList/index.vue +171 -171
  44. package/dist/v3/DevTool/components/ConsoleList/staticTips.ts +1145 -1145
  45. package/dist/v3/DevTool/components/DevToolTitle/index.vue +24 -24
  46. package/dist/v3/DevTool/components/DevToolWindow/DevToolOverlay.vue +197 -197
  47. package/dist/v3/DevTool/components/DevToolWindow/hooks/dataUtils.ts +48 -48
  48. package/dist/v3/DevTool/components/DevToolWindow/hooks/useDevToolData.ts +387 -387
  49. package/dist/v3/DevTool/components/DevToolWindow/hooks/useDevToolHandlers.ts +629 -629
  50. package/dist/v3/DevTool/components/DevToolWindow/hooks/useDevToolOverlay.ts +201 -197
  51. package/dist/v3/DevTool/components/ElEvent/ElEventItem.vue +105 -105
  52. package/dist/v3/DevTool/components/ElEvent/index.vue +106 -106
  53. package/dist/v3/DevTool/components/Instance/components/InstanceTreeNode.vue +265 -265
  54. package/dist/v3/DevTool/components/Instance/flatten.ts +226 -226
  55. package/dist/v3/DevTool/components/Instance/index.vue +94 -94
  56. package/dist/v3/DevTool/components/Instance/registry.ts +49 -49
  57. package/dist/v3/DevTool/components/Instance/transformTree.ts +375 -375
  58. package/dist/v3/DevTool/components/Instance/transformTreeCtx.ts +268 -268
  59. package/dist/v3/DevTool/components/Instance/typing.d.ts +43 -43
  60. package/dist/v3/DevTool/components/InstanceDetail/index.vue +485 -485
  61. package/dist/v3/DevTool/components/JsonDetail/index.vue +70 -70
  62. package/dist/v3/DevTool/components/NFCList/NFCItem.vue +112 -112
  63. package/dist/v3/DevTool/components/NFCList/NFCTool.vue +454 -454
  64. package/dist/v3/DevTool/components/NFCList/const.ts +56 -56
  65. package/dist/v3/DevTool/components/NFCList/index.vue +94 -94
  66. package/dist/v3/DevTool/components/NetworkList/InterceptConfig.vue +624 -624
  67. package/dist/v3/DevTool/components/NetworkList/InterceptItem.vue +140 -140
  68. package/dist/v3/DevTool/components/NetworkList/NetworkDetail.vue +287 -287
  69. package/dist/v3/DevTool/components/NetworkList/NetworkIntercept.vue +88 -88
  70. package/dist/v3/DevTool/components/NetworkList/NetworkItem.vue +163 -163
  71. package/dist/v3/DevTool/components/NetworkList/NetworkSend.vue +589 -589
  72. package/dist/v3/DevTool/components/NetworkList/const.ts +4 -4
  73. package/dist/v3/DevTool/components/NetworkList/hooks/useNetworkForm.ts +86 -86
  74. package/dist/v3/DevTool/components/NetworkList/index.vue +160 -160
  75. package/dist/v3/DevTool/components/NetworkList/utils.ts +101 -101
  76. package/dist/v3/DevTool/components/Performance/index.vue +498 -498
  77. package/dist/v3/DevTool/components/Performance/modules/PerformanceMetrics.vue +153 -153
  78. package/dist/v3/DevTool/components/Performance/modules/usePerformanceChart.ts +460 -460
  79. package/dist/v3/DevTool/components/Performance/modules/usePerformanceData.ts +258 -258
  80. package/dist/v3/DevTool/components/PiniaList/index.vue +93 -93
  81. package/dist/v3/DevTool/components/RunJS/index.vue +148 -148
  82. package/dist/v3/DevTool/components/ScanCodeList/ScanCodeItem.vue +97 -97
  83. package/dist/v3/DevTool/components/ScanCodeList/index.vue +100 -100
  84. package/dist/v3/DevTool/components/SettingButton/index.vue +45 -45
  85. package/dist/v3/DevTool/components/SettingList/index.vue +218 -218
  86. package/dist/v3/DevTool/components/SettingList/modules/SettingBarrage.vue +304 -304
  87. package/dist/v3/DevTool/components/SettingList/modules/SettingDevTool.vue +212 -212
  88. package/dist/v3/DevTool/components/SettingList/modules/SettingInfo.vue +157 -157
  89. package/dist/v3/DevTool/components/SettingList/modules/SettingLanguage.vue +74 -74
  90. package/dist/v3/DevTool/components/SettingList/modules/SettingLog.vue +230 -230
  91. package/dist/v3/DevTool/components/SettingList/typing.d.ts +2 -2
  92. package/dist/v3/DevTool/components/SourceCode/Line.vue +127 -127
  93. package/dist/v3/DevTool/components/SourceCode/parseCode.ts +609 -609
  94. package/dist/v3/DevTool/components/StorageList/index.vue +174 -174
  95. package/dist/v3/DevTool/components/TransferList/TransferDetail.vue +268 -268
  96. package/dist/v3/DevTool/components/VuexList/index.vue +84 -84
  97. package/dist/v3/DevTool/index.vue +1 -0
  98. package/dist/v3/components/AppTransition/index.vue +176 -176
  99. package/dist/v3/components/AutoSizer/index.vue +192 -192
  100. package/dist/v3/components/AutoSizer/index1.vue +184 -184
  101. package/dist/v3/components/AutoSizer/utils.ts +49 -49
  102. package/dist/v3/components/Barrage/BarrageItem.vue +137 -137
  103. package/dist/v3/components/Barrage/index.vue +202 -202
  104. package/dist/v3/components/CircularButton/index.vue +84 -84
  105. package/dist/v3/components/CustomSwiper/CustomSwiperItem.vue +49 -49
  106. package/dist/v3/components/CustomSwiper/index.vue +104 -104
  107. package/dist/v3/components/DevErrorBoundary/index.vue +380 -0
  108. package/dist/v3/components/Empty/index.vue +29 -29
  109. package/dist/v3/components/FilterSelect/index.vue +179 -179
  110. package/dist/v3/components/JsonPretty/components/Brackets/index.vue +27 -27
  111. package/dist/v3/components/JsonPretty/components/Carets/index.vue +59 -59
  112. package/dist/v3/components/JsonPretty/components/CheckController/index.vue +136 -136
  113. package/dist/v3/components/JsonPretty/components/TreeNode/index.vue +387 -387
  114. package/dist/v3/components/JsonPretty/hooks/useClipboard.ts +21 -21
  115. package/dist/v3/components/JsonPretty/hooks/useError.ts +21 -21
  116. package/dist/v3/components/JsonPretty/type.ts +127 -127
  117. package/dist/v3/components/JsonPretty/utils/index.ts +169 -169
  118. package/dist/v3/components/MovableContainer/index.vue +8 -4
  119. package/dist/v3/components/Pick/index.vue +322 -322
  120. package/dist/v3/components/Tag/index.vue +113 -113
  121. package/dist/v3/components/VirtualList/AutoSize.vue +40 -40
  122. package/dist/v3/components/VirtualList/index.vue +416 -416
  123. package/dist/v3/hooks/useBluetooth/index.ts +561 -561
  124. package/dist/v3/hooks/useContainerStyle.ts +153 -153
  125. package/dist/v3/hooks/useNFC/index.ts +107 -107
  126. package/dist/v3/hooks/useNFC/typing.d.ts +396 -396
  127. package/dist/v3/hooks/useNFC/useNFCAndroid.ts +966 -966
  128. package/dist/v3/hooks/useNFC/useNFCMpWeiXin.ts +812 -812
  129. package/dist/v3/hooks/useNFC/utils.ts +754 -754
  130. package/dist/v3/hooks/useRequest/index.ts +586 -586
  131. package/dist/v3/hooks/useRequest/utils.ts +267 -267
  132. package/dist/v3/hooks/useScanCode/index.ts +206 -206
  133. package/dist/v3/hooks/useWebsocket/index.ts +253 -253
  134. package/dist/v3/styles/theme.ts +12 -12
  135. package/package.json +9 -1
@@ -1,754 +1,754 @@
1
- // 创建一个 ArrayBuffer 并查看其值
2
- export function transformArrayBuffer(buffer: ArrayBuffer): NFC.TransformId {
3
- if (!buffer) {
4
- return {
5
- byteArray: [],
6
- hexArray: [],
7
- hexString: '',
8
- };
9
- }
10
-
11
- // 将 ArrayBuffer 转为 Uint8Array(按字节读取)
12
- const uint8Array = new Uint8Array(buffer);
13
-
14
- // 方式1:转为普通数组
15
- const byteArray = Array.from(uint8Array);
16
- // console.log('ArrayBuffer 字节值(十进制):', byteArray);
17
-
18
- // 方式2:转为十六进制(NFC 场景常用,因为 NFC 数据多以十六进制展示)
19
- const hexArray = byteArray.map((byte) =>
20
- byte.toString(16).padStart(2, '0').toUpperCase(),
21
- );
22
-
23
- const hexString = hexArray.join(':');
24
-
25
- return {
26
- byteArray,
27
- hexArray,
28
-
29
- hexString,
30
- };
31
- }
32
-
33
- /**
34
- * 将字节数组转换为字符串
35
- *
36
- * @export
37
- * @param {number[]} bytes
38
- * @return {*}
39
- */
40
- export function byteToString(bytes: number[]) {
41
- return bytes.map((byte) => String.fromCharCode(byte)).join('');
42
- }
43
-
44
- /**
45
- * 将字符串转换为字节数组
46
- *
47
- * @export
48
- * @param {string} str
49
- * @return {*} {number[]}
50
- */
51
- export function stringToBytes(str: string): number[] {
52
- return Array.from(str).map((char) => char.charCodeAt(0));
53
- }
54
-
55
- export function stringToArrayBuffer(str: string): ArrayBuffer {
56
- const bytes = stringToBytes(str); // 使用现有工具函数
57
- const uint8Array = new Uint8Array(bytes);
58
- return uint8Array.buffer.slice(
59
- uint8Array.byteOffset,
60
- uint8Array.byteOffset + uint8Array.byteLength,
61
- );
62
- }
63
-
64
- export function arrayNumberToArrayBuffer(numbers: number[]): ArrayBuffer {
65
- const buffer = new ArrayBuffer(numbers.length);
66
- const view = new DataView(buffer);
67
- numbers.forEach((number, index) => {
68
- view.setUint8(index, number);
69
- });
70
- return buffer;
71
- }
72
-
73
- // 可以添加解析函数来解析 ATQA 信息
74
- export function parseAtqa(atqaBuffer: ArrayBuffer) {
75
- const uint8Array = new Uint8Array(atqaBuffer);
76
- if (uint8Array.length < 2) return null;
77
-
78
- const byte1 = uint8Array[0];
79
- const byte2 = uint8Array[1];
80
-
81
- return {
82
- uidType: (byte1 >> 7) & 0x01, // UID 类型
83
- storageSize: (byte1 >> 4) & 0x07, // 存储大小编码
84
- manufacturer: byte1 & 0x0f, // 制造商代码
85
- cardFeatures: (byte2 >> 4) & 0x0f, // 功能特性
86
- reserved: byte2 & 0x0f, // 保留位
87
- };
88
- }
89
-
90
- export function parseAtqaAndroid(atqa: number[]) {
91
- const byte1 = atqa[0];
92
- const byte2 = atqa[1];
93
- return {
94
- uidType: (byte1 >> 7) & 0x01, // UID 类型
95
- storageSize: (byte1 >> 4) & 0x07, // 存储大小编码
96
- manufacturer: byte1 & 0x0f, // 制造商代码
97
- cardFeatures: (byte2 >> 4) & 0x0f, // 功能特性
98
- reserved: byte2 & 0x0f, // 保留位
99
- };
100
- }
101
-
102
- /**
103
- * 获取 SAK 类型
104
- *
105
- * @export
106
- * @param {number} sak
107
- * @return {*}
108
- */
109
- export function getSakType(sak: number) {
110
- switch (sak) {
111
- case 0x00:
112
- return '未指定类型';
113
- case 0x08:
114
- return 'Mifare Classic 1K';
115
- case 0x09:
116
- return 'Mifare Classic Mini';
117
- case 0x10:
118
- return 'Mifare Plus';
119
- case 0x11:
120
- return 'Mifare Classic 4K';
121
- case 0x18:
122
- return 'Mifare Ultralight / Ultralight C';
123
- case 0x20:
124
- return 'DESFire';
125
- default:
126
- return '未知';
127
- }
128
- }
129
-
130
- export function getSakHex(sak: number) {
131
- return '0x' + (sak < 16 ? '0' : '') + sak.toString(16).toUpperCase();
132
- }
133
-
134
- /** 获取读写标准 */
135
- export function getReadWriteStandard(nfcTypes: NFC.NfcType[]) {
136
- return nfcTypes.map((nfcType) => {
137
- switch (nfcType) {
138
- case 'NFC-A':
139
- case 'MIFARE Classic':
140
- case 'MIFARE Ultralight':
141
- return 'ISO 14443-3A';
142
- case 'NFC-B':
143
- return 'ISO 14443-3B';
144
- case 'NFC-F':
145
- return 'ISO 14443-3F';
146
- case 'NFC-V':
147
- return 'ISO 15693';
148
- case 'Ndef':
149
- case 'NDEF':
150
- return 'NDEF';
151
- case 'ISO-DEP':
152
- return 'ISO 14443-4';
153
- case 'Ndef Formatable':
154
- return 'NDEF Formatable';
155
- default:
156
- return '未知';
157
- }
158
- });
159
- }
160
-
161
- export function getAndroidTechs(techs: string[]) {
162
- return techs.map((tech) => {
163
- switch (tech) {
164
- case 'android.nfc.tech.IsoDep':
165
- return 'ISO-DEP';
166
- case 'android.nfc.tech.MifareClassic':
167
- return 'MIFARE Classic';
168
- case 'android.nfc.tech.MifareUltralight':
169
- return 'MIFARE Ultralight';
170
- case 'android.nfc.tech.NfcA':
171
- return 'NFC-A';
172
- case 'android.nfc.tech.NfcB':
173
- return 'NFC-B';
174
- case 'android.nfc.tech.NfcF':
175
- return 'NFC-F';
176
- case 'android.nfc.tech.NfcV':
177
- return 'NFC-V';
178
- case 'android.nfc.tech.Ndef':
179
- return 'Ndef';
180
- case 'android.nfc.tech.NdefFormatable':
181
- return 'Ndef Formatable';
182
- default:
183
- return '';
184
- }
185
- });
186
- }
187
-
188
- export function getTnfDescription(tnf: string) {
189
- switch (tnf) {
190
- case '0':
191
- return 'TNF_EMPTY';
192
- case '1':
193
- return 'TNF_WELL_KNOWN';
194
- case '2':
195
- return 'TNF_MIME_MEDIA';
196
- case '3':
197
- return 'TNF_ABSOLUTE_URI';
198
- case '4':
199
- return 'TNF_EXTERNAL_TYPE';
200
- case '5':
201
- return 'TNF_UNKNOWN';
202
- case '6':
203
- return 'TNF_UNCHANGED';
204
- case '7':
205
- return 'TNF_RESERVED';
206
- default:
207
- return '未知';
208
- }
209
- }
210
-
211
- /**
212
- * 获取URI记录的具体类型描述
213
- * @param prefixCode - URI前缀代码(payload的第一个字节)
214
- * @returns URI类型描述
215
- */
216
- function getUriTypeDescription(prefixCode: number): string {
217
- const uriPrefixes: { [key: number]: string } = {
218
- 0x00: '未指定(纯URI)',
219
- 0x01: 'http://www.', // http://www.
220
- 0x02: 'https://www.', // https://www.
221
- 0x03: 'http://', // http://
222
- 0x04: 'https://', // https://
223
- 0x05: 'tel:', // tel: (电话号码)
224
- 0x06: 'mailto:', // mailto: (邮箱地址)
225
- 0x07: 'ftp://anonymous:anonymous@', // ftp://anonymous:anonymous@
226
- 0x08: 'ftp://ftp.', // ftp://ftp.
227
- 0x09: 'ftps://', // ftps://
228
- 0x0a: 'sftp://', // sftp://
229
- 0x0b: 'smb://', // smb://
230
- 0x0c: 'nfs://', // nfs://
231
- 0x0d: 'ftp://', // ftp://
232
- 0x0e: 'dav://', // dav://
233
- 0x0f: 'news:', // news:
234
- 0x10: 'telnet://', // telnet://
235
- 0x11: 'imap:', // imap:
236
- 0x12: 'rtsp://', // rtsp://
237
- 0x13: 'urn:', // urn:
238
- 0x14: 'pop:', // pop:
239
- 0x15: 'sip:', // sip:
240
- 0x16: 'sips:', // sips:
241
- 0x17: 'tftp:', // tftp:
242
- 0x18: 'btspp://', // btspp:// (Bluetooth)
243
- 0x19: 'btl2cap://', // btl2cap:// (Bluetooth)
244
- 0x1a: 'btgoep://', // btgoep:// (Bluetooth)
245
- 0x1b: 'tcpobex://', // tcpobex://
246
- 0x1c: 'irdaobex://', // irdaobex://
247
- 0x1d: 'file://', // file://
248
- 0x1e: 'urn:epc:id:', // urn:epc:id:
249
- 0x1f: 'urn:epc:tag:', // urn:epc:tag:
250
- 0x20: 'urn:epc:pat:', // urn:epc:pat:
251
- 0x21: 'urn:epc:raw:', // urn:epc:raw:
252
- 0x22: 'urn:epc:', // urn:epc:
253
- 0x23: 'urn:nfc:', // urn:nfc:
254
- };
255
-
256
- return uriPrefixes[prefixCode] || '未知';
257
- }
258
-
259
- /** 定义 URI 前缀映射表 */
260
- const uriPrefixMap: { [key: string]: number } = {
261
- '': 0x00, // 未指定协议
262
- 'http://www.': 0x01,
263
- 'https://www.': 0x02,
264
- 'http://': 0x03,
265
- 'https://': 0x04,
266
- 'tel:': 0x05, // 电话号码
267
- 'mailto:': 0x06, // 邮箱地址
268
- 'ftp://anonymous:anonymous@': 0x07,
269
- 'ftp://ftp.': 0x08,
270
- 'ftps://': 0x09,
271
- 'sftp://': 0x0a,
272
- 'smb://': 0x0b,
273
- 'nfs://': 0x0c,
274
- 'ftp://': 0x0d,
275
- 'dav://': 0x0e,
276
- 'news:': 0x0f,
277
- 'telnet://': 0x10,
278
- 'imap:': 0x11,
279
- 'rtsp://': 0x12,
280
- 'urn:': 0x13,
281
- 'pop:': 0x14,
282
- 'sip:': 0x15,
283
- 'sips:': 0x16,
284
- 'tftp:': 0x17,
285
- 'btspp://': 0x18,
286
- 'btl2cap://': 0x19,
287
- 'btgoep://': 0x1a,
288
- 'tcpobex://': 0x1b,
289
- 'irdaobex://': 0x1c,
290
- 'file://': 0x1d,
291
- 'urn:epc:id:': 0x1e,
292
- 'urn:epc:tag:': 0x1f,
293
- 'urn:epc:pat:': 0x20,
294
- 'urn:epc:raw:': 0x21,
295
- 'urn:epc:': 0x22,
296
- 'urn:nfc:': 0x23,
297
- };
298
-
299
- /**
300
- * 创建 URI 类型的 NDEF 记录
301
- * 根据 NFC Forum URI Record Type Definition 规范
302
- *
303
- * @param uri 完整的 URI 字符串,如 "tel:18888888888", "https://www.example.com"
304
- * @returns NDEF 记录对象
305
- */
306
- export function createUriNdefRecord(uri: string): NFC.NdefRecord {
307
- let prefix = '';
308
- let suffix = uri;
309
-
310
- // 查找匹配的前缀
311
- for (const [protocol, _code] of Object.entries(uriPrefixMap)) {
312
- if (uri.startsWith(protocol) && protocol.length > prefix.length) {
313
- prefix = protocol;
314
- }
315
- }
316
-
317
- // 提取 URI 后缀部分
318
- if (prefix) {
319
- suffix = uri.substring(prefix.length);
320
- }
321
-
322
- // 获取前缀代码
323
- const prefixCode = uriPrefixMap[prefix] || 0x00;
324
-
325
- // 构建 payload:前缀代码 + 剩余 URI
326
- const payloadArray = [prefixCode];
327
- if (suffix) {
328
- // 将后缀字符串转换为字节数组
329
- const suffixBytes = stringToBytes(suffix);
330
- payloadArray.push(...suffixBytes);
331
- }
332
-
333
- return {
334
- tnf: 1, // TNF_WELL_KNOWN
335
- type: 'U', // URI 类型
336
- payload: arrayNumberToArrayBuffer(payloadArray),
337
- };
338
- }
339
-
340
- export function getTypeDescription(type: string, payload?: number[]) {
341
- switch (type) {
342
- case 'T':
343
- return '文本记录';
344
- case 'U':
345
- // URI记录需要根据payload的第一个字节确定具体类型
346
- if (payload && payload.length > 0) {
347
- const uriPrefixCode = payload[0]; // 第一个字节是URI前缀代码
348
- return getUriTypeDescription(uriPrefixCode);
349
- }
350
- return 'URI记录';
351
- case 'Sp':
352
- return 'Smart Poster记录';
353
- case 'Sig':
354
- return '签名记录';
355
- case 'application/vnd.wfa.wsc':
356
- return 'Wi-Fi Simple Configuration记录';
357
- case 'android.com:pkg':
358
- return 'Android 包名记录';
359
- case 'text/vcard':
360
- return 'VCard 记录';
361
- default:
362
- // 如果是 MIME 类型格式,可以返回更具体的描述
363
- if (type.startsWith('application/')) {
364
- return `应用数据类型: ${type}`;
365
- } else if (type.startsWith('text/')) {
366
- return `文本数据类型: ${type}`;
367
- } else if (
368
- type.startsWith('image/') ||
369
- type.startsWith('video/') ||
370
- type.startsWith('audio/')
371
- ) {
372
- return `媒体数据类型: ${type}`;
373
- }
374
- return '未知';
375
- }
376
- }
377
-
378
- /**
379
- * 通用 TLV 解析配置项接口
380
- * 支持自定义各类 TLV 解析规则,适配不同格式数据
381
- */
382
- interface TLVParseConfig {
383
- // 合法 TLV 的 Type 高字节标识(默认:0x10,对应 NFC WiFi 数据)
384
- legalTypeHigh?: number;
385
- // 需要跳过的无效 TLV 类型(低字节)列表
386
- skipTlvTypes?: number[];
387
- // 长度解析策略(支持自定义,适配标准大端/低字节有效/高字节有效等)
388
- lengthParseStrategy: (lengthHigh: number, lengthLow: number) => number;
389
- // 目标 TLV 类型映射(键:TLV 低字节类型,值:字段名称 + 数据转换函数)
390
- targetTlvMap: Record<
391
- number,
392
- {
393
- fieldName: string;
394
- converter: (valueBytes: number[]) => any;
395
- }
396
- >;
397
- }
398
-
399
- /**
400
- * 通用 TLV 解析工具函数
401
- * 可适配 NFC 门禁卡、公交卡、WiFi 数据等各类 TLV 格式数据
402
- * @param tlvBytes 原始 TLV 字节数组
403
- * @param config 自定义解析配置项
404
- * @returns 解析后的结果对象(键:配置中定义的 fieldName,值:转换后的数据)
405
- */
406
- export function parseUniversalTLV(
407
- tlvBytes: number[],
408
- config: TLVParseConfig,
409
- ): Record<string, any> {
410
- // 配置项默认值填充
411
- const finalConfig = {
412
- legalTypeHigh: 0x10,
413
- skipTlvTypes: [],
414
- ...config,
415
- };
416
-
417
- // 存储解析结果
418
- const parseResult: Record<string, any> = {
419
- raw: tlvBytes,
420
- };
421
- const byteLength = tlvBytes.length;
422
- let index = 0;
423
-
424
- // 循环解析 TLV 数据(核心:按配置项灵活解析)
425
- while (index < byteLength) {
426
- // 1. 校验是否有足够字节读取 Type(2 字节)
427
- if (index + 1 >= byteLength) {
428
- break;
429
- }
430
-
431
- const typeHigh = tlvBytes[index];
432
- const typeLow = tlvBytes[index + 1];
433
- const currentType = typeLow;
434
-
435
- // 2. 过滤非法 TLV(非配置指定的高字节标识)
436
- if (typeHigh !== finalConfig.legalTypeHigh) {
437
- index++;
438
- continue;
439
- }
440
-
441
- // 3. 跳过配置中指定的无效 TLV(直接跳过头部 4 字节:Type2 + Length2)
442
- if (finalConfig.skipTlvTypes?.includes(currentType)) {
443
- index += 4;
444
- continue;
445
- }
446
-
447
- // 4. 步进 Type 字节(2 字节),准备读取 Length
448
- index += 2;
449
- if (index + 1 >= byteLength) {
450
- break;
451
- }
452
-
453
- // 5. 按自定义策略解析 Length(灵活适配不同格式)
454
- const lengthHigh = tlvBytes[index];
455
- const lengthLow = tlvBytes[index + 1];
456
- const currentLength = finalConfig.lengthParseStrategy(
457
- lengthHigh,
458
- lengthLow,
459
- );
460
-
461
- // 6. 步进 Length 字节(2 字节),准备读取 Value
462
- index += 2;
463
- if (index + currentLength > byteLength) {
464
- break;
465
- }
466
-
467
- // 7. 读取 Value 部分(按自定义长度截取)
468
- const currentValue = tlvBytes.slice(index, index + currentLength);
469
-
470
- // 8. 强制步进 Value 字节(避免索引卡死,确保遍历完整性)
471
- index += currentLength;
472
-
473
- // 9. 按自定义映射转换目标 TLV 数据
474
- const targetTlvConfig = finalConfig.targetTlvMap[currentType];
475
- if (targetTlvConfig) {
476
- try {
477
- const convertedData = targetTlvConfig.converter(currentValue);
478
- parseResult[targetTlvConfig.fieldName] = convertedData;
479
- } catch (_e) {
480
- parseResult[targetTlvConfig.fieldName] = null;
481
- }
482
- }
483
- }
484
-
485
- return parseResult;
486
- }
487
-
488
- /**
489
- * 辅助工具:修正 authType 转换逻辑,适配 32(0x20)等新值
490
- */
491
- const TLVConverterTools = {
492
- // 1. 字节数组 → UTF-8 字符串(保持原有)
493
- bytesToUtf8String: (bytes: number[]): string => {
494
- try {
495
- return new TextDecoder('utf-8', { fatal: false }).decode(
496
- new Uint8Array(bytes),
497
- );
498
- } catch (_e) {
499
- return bytes.map((byte) => String.fromCharCode(byte)).join('');
500
- }
501
- },
502
-
503
- // 2. 字节数组 → WIFI 认证类型(核心修正:添加 32(0x20)的映射)
504
- bytesToWifiAuthType: (bytes: number[]): string => {
505
- // 适配当前字节数组的 [0, 32],取有效数据(低字节优先,与格式一致)
506
- const authValue = bytes.length >= 2 ? bytes[1] || bytes[0] : bytes[0];
507
- console.log(
508
- `[authType 解析] 原始标识值:${authValue}(十六进制:0x${authValue.toString(16).padStart(2, '0')}`,
509
- );
510
-
511
- switch (authValue) {
512
- case 1:
513
- return 'OPEN (无认证)';
514
- case 2:
515
- case 34: // 0x22:原有 WPA2-Personal
516
- return 'WPA2-Personal';
517
- case 32: // 0x20:新增,适配当前字节数组的 authType 值
518
- return 'WPA-Personal (兼容 WPA2)';
519
- case 3:
520
- return 'WPA-Personal';
521
- default:
522
- return `未知认证类型(0x${authValue.toString(16).padStart(4, '0')}`;
523
- }
524
- },
525
-
526
- // 3. 其他工具函数(保持原有,可选)
527
- bytesToHexString: (bytes: number[], separator = ''): string => {
528
- return bytes
529
- .map((byte) => byte.toString(16).padStart(2, '0').toUpperCase())
530
- .join(separator);
531
- },
532
- bytesToDecimal: (bytes: number[], isBigEndian = true): number => {
533
- let result = 0;
534
- if (isBigEndian) {
535
- bytes.forEach((byte) => {
536
- result = (result << 8) | byte;
537
- });
538
- } else {
539
- bytes.reverse().forEach((byte) => {
540
- result = (result << 8) | byte;
541
- });
542
- }
543
- return result;
544
- },
545
- bytesToWifiEncryptionType: (bytes: number[]): string => {
546
- const isAllFf = bytes.every((byte) => byte === 255);
547
- if (isAllFf) {
548
- return 'AES/TKIP (WPA2 兼容)';
549
- }
550
- const encryptValue = TLVConverterTools.bytesToDecimal(bytes);
551
- return encryptValue === 0
552
- ? 'NONE (无加密)'
553
- : `WEP(0x${encryptValue.toString(16).padStart(4, '0')}`;
554
- },
555
- };
556
-
557
- // 定义 WiFi 数据的自定义解析配置
558
- export const wifiTLVConfig: TLVParseConfig = {
559
- legalTypeHigh: 0x10, // 合法 TLV 高字节为 16
560
- skipTlvTypes: [14], // 跳过无效 TLV 类型 14
561
- // 长度解析策略:低字节有效(适配非标准 WiFi 数据)
562
- lengthParseStrategy: (lengthHigh, lengthLow) => lengthLow,
563
- // 目标 TLV 映射(按需扩展)
564
- targetTlvMap: {
565
- 69: {
566
- // SSID 类型
567
- fieldName: 'ssid',
568
- converter: TLVConverterTools.bytesToUtf8String,
569
- },
570
- 39: {
571
- // 密码类型
572
- fieldName: 'password',
573
- converter: TLVConverterTools.bytesToUtf8String,
574
- },
575
- 3: {
576
- // 认证类型
577
- fieldName: 'authType',
578
- converter: TLVConverterTools.bytesToWifiAuthType,
579
- },
580
- 32: {
581
- // 加密类型
582
- fieldName: 'encryptionType',
583
- converter: TLVConverterTools.bytesToWifiEncryptionType,
584
- },
585
- },
586
- };
587
-
588
- // WSC 配置接口
589
-
590
- // 生成 WSC 数据 (Android/Windows 更兼容的格式)
591
- export function generateWSCBytesForAndroid(config: NFC.WSCConfig): number[] {
592
- const bytes: number[] = [];
593
-
594
- // 这是 Android 和 Windows 都接受的简化格式
595
- // 参考: Wi-Fi Alliance WSC Specification 2.0
596
-
597
- // 1. Credential 属性 (0x100e)
598
- // 这是一个容器属性,包含其他属性
599
-
600
- // 先收集所有属性
601
- const credentialAttrs: number[] = [];
602
-
603
- // SSID
604
- const ssidBytes = Array.from(config.ssid, (c) => c.charCodeAt(0));
605
- credentialAttrs.push(0x10, 0x45); // SSID
606
- credentialAttrs.push(0x00, ssidBytes.length);
607
- credentialAttrs.push(...ssidBytes);
608
-
609
- // 认证类型
610
- let authValue = 0x0020; // 默认 WPA2-Personal
611
- if (config.authType === 'OPEN') authValue = 0x0001;
612
- if (config.authType === 'WPA') authValue = 0x0022;
613
-
614
- credentialAttrs.push(0x10, 0x03); // Authentication Type
615
- credentialAttrs.push(0x00, 0x02);
616
- credentialAttrs.push((authValue >> 8) & 0xff, authValue & 0xff);
617
-
618
- // 加密类型
619
- let encValue = 0x0008; // 默认 AES
620
- if (config.encryptionType === 'NONE') encValue = 0x0001;
621
- if (config.encryptionType === 'TKIP') encValue = 0x0004;
622
- if (config.encryptionType === 'WEP') encValue = 0x0008;
623
-
624
- credentialAttrs.push(0x10, 0x0f); // Encryption Type
625
- credentialAttrs.push(0x00, 0x02);
626
- credentialAttrs.push((encValue >> 8) & 0xff, encValue & 0xff);
627
-
628
- // 网络密钥
629
- if (config.authType !== 'OPEN') {
630
- const keyBytes = Array.from(config.password, (c) => c.charCodeAt(0));
631
- credentialAttrs.push(0x10, 0x27); // Network Key
632
- credentialAttrs.push(0x00, keyBytes.length);
633
- credentialAttrs.push(...keyBytes);
634
- }
635
-
636
- // 网络密钥索引 (WEP 需要)
637
- if (config.encryptionType === 'WEP') {
638
- const idx = config.networkKeyIndex || 1;
639
- credentialAttrs.push(0x10, 0x28); // Network Key Index
640
- credentialAttrs.push(0x00, 0x01);
641
- credentialAttrs.push(idx);
642
- }
643
-
644
- // 将 Credential 属性添加到主字节数组
645
- bytes.push(0x10, 0x0e); // Credential
646
- bytes.push(0x00, credentialAttrs.length);
647
- bytes.push(...credentialAttrs);
648
-
649
- return bytes;
650
- }
651
-
652
- /**
653
- * vCard (text/vcard) 数据解析工具
654
- * 适配 vCard 3.0 格式,支持提取核心联系人字段,具备容错性
655
- */
656
-
657
- const vcardFieldMap: Record<string, string> = {
658
- FN: 'fullName',
659
- VERSION: 'version',
660
- ORG: 'organization',
661
- TEL: 'telephone',
662
- ADR: 'address',
663
- URL: 'url',
664
- };
665
- const reverseVcardFieldMap: Record<string, string> = Object.entries(
666
- vcardFieldMap,
667
- ).reduce(
668
- (acc, [key, value]) => {
669
- acc[value] = key;
670
- return acc;
671
- },
672
- {} as Record<string, string>,
673
- );
674
-
675
- /**
676
- * 解析 vCard 格式的 payload 字符串,提取联系人信息
677
- * @param vcardPayload NFC 读取到的 vCard 原始字符串(含 \n 分隔符)
678
- * BEGIN:VCARD\nVERSION:3.0\nFN:any\nORG:any\nTEL:18888888888\nADR:杭州\nURL:www.baidu.com\nEND:VCARD\n
679
- * @returns 结构化的联系人信息对象
680
- */
681
- export function parseVCardPayload(vcardPayload: string): NFC.VCardContactInfo {
682
- // 初始化默认结果
683
- const contactInfo: NFC.VCardContactInfo = {
684
- fullName: '',
685
- version: '',
686
- organization: '',
687
- telephone: '',
688
- address: '',
689
- url: '',
690
- };
691
-
692
- // 步骤1:预处理 payload(去除首尾空白、处理不同分隔符 \n/\r\n/\r,拆分行为数组)
693
- const processedPayload = vcardPayload
694
- .trim()
695
- .replace(/\r\n/g, '\n')
696
- .replace(/\r/g, '\n');
697
- const vcardLines = processedPayload
698
- .split('\n')
699
- .filter((line) => line.trim() !== '');
700
-
701
- // 步骤2:验证是否为合法 vCard 格式
702
- const isLegalVCard =
703
- vcardLines.some((line) => line.startsWith('BEGIN:VCARD')) &&
704
- vcardLines.some((line) => line.startsWith('END:VCARD'));
705
- if (!isLegalVCard) {
706
- console.warn(
707
- '[vCard 解析] 无效的 vCard 格式,缺少 BEGIN:VCARD 或 END:VCARD 标记',
708
- );
709
- return contactInfo;
710
- }
711
-
712
- // 步骤3:遍历每行,解析关键字段(KEY:VALUE 格式)
713
-
714
- for (const line of vcardLines) {
715
- // 拆分 KEY 和 VALUE(按第一个冒号分割,避免 VALUE 中包含冒号的情况)
716
- const colonIndex = line.indexOf(':');
717
- if (colonIndex === -1) continue;
718
-
719
- const fieldKey = line.substring(0, colonIndex).trim().toUpperCase();
720
- const fieldValue = line.substring(colonIndex + 1).trim();
721
-
722
- // 映射到结构化对象字段
723
- const targetField = vcardFieldMap[fieldKey];
724
- if (targetField) {
725
- contactInfo[targetField] = fieldValue;
726
- } else {
727
- // 支持扩展字段(直接挂载到对象上)
728
- contactInfo[fieldKey.toLowerCase()] = fieldValue;
729
- }
730
- }
731
-
732
- return contactInfo;
733
- }
734
-
735
- /**
736
- * 格式化 vCard 联系人信息
737
- *
738
- * @export
739
- * @param {NFC.VCardContactInfo} cardInfo
740
- * @return {*}
741
- */
742
- export function formatCardInfo(cardInfo: NFC.VCardContactInfo) {
743
- const list = Object.keys(cardInfo)
744
- .filter((key) => {
745
- return cardInfo[key];
746
- })
747
- .map((key) => {
748
- return `${reverseVcardFieldMap[key]}:${cardInfo[key]}`;
749
- });
750
-
751
- list.unshift('BEGIN:VCARD');
752
- list.push('END:VCARD');
753
- return list.join('\n');
754
- }
1
+ // 创建一个 ArrayBuffer 并查看其值
2
+ export function transformArrayBuffer(buffer: ArrayBuffer): NFC.TransformId {
3
+ if (!buffer) {
4
+ return {
5
+ byteArray: [],
6
+ hexArray: [],
7
+ hexString: '',
8
+ };
9
+ }
10
+
11
+ // 将 ArrayBuffer 转为 Uint8Array(按字节读取)
12
+ const uint8Array = new Uint8Array(buffer);
13
+
14
+ // 方式1:转为普通数组
15
+ const byteArray = Array.from(uint8Array);
16
+ // console.log('ArrayBuffer 字节值(十进制):', byteArray);
17
+
18
+ // 方式2:转为十六进制(NFC 场景常用,因为 NFC 数据多以十六进制展示)
19
+ const hexArray = byteArray.map((byte) =>
20
+ byte.toString(16).padStart(2, '0').toUpperCase(),
21
+ );
22
+
23
+ const hexString = hexArray.join(':');
24
+
25
+ return {
26
+ byteArray,
27
+ hexArray,
28
+
29
+ hexString,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * 将字节数组转换为字符串
35
+ *
36
+ * @export
37
+ * @param {number[]} bytes
38
+ * @return {*}
39
+ */
40
+ export function byteToString(bytes: number[]) {
41
+ return bytes.map((byte) => String.fromCharCode(byte)).join('');
42
+ }
43
+
44
+ /**
45
+ * 将字符串转换为字节数组
46
+ *
47
+ * @export
48
+ * @param {string} str
49
+ * @return {*} {number[]}
50
+ */
51
+ export function stringToBytes(str: string): number[] {
52
+ return Array.from(str).map((char) => char.charCodeAt(0));
53
+ }
54
+
55
+ export function stringToArrayBuffer(str: string): ArrayBuffer {
56
+ const bytes = stringToBytes(str); // 使用现有工具函数
57
+ const uint8Array = new Uint8Array(bytes);
58
+ return uint8Array.buffer.slice(
59
+ uint8Array.byteOffset,
60
+ uint8Array.byteOffset + uint8Array.byteLength,
61
+ );
62
+ }
63
+
64
+ export function arrayNumberToArrayBuffer(numbers: number[]): ArrayBuffer {
65
+ const buffer = new ArrayBuffer(numbers.length);
66
+ const view = new DataView(buffer);
67
+ numbers.forEach((number, index) => {
68
+ view.setUint8(index, number);
69
+ });
70
+ return buffer;
71
+ }
72
+
73
+ // 可以添加解析函数来解析 ATQA 信息
74
+ export function parseAtqa(atqaBuffer: ArrayBuffer) {
75
+ const uint8Array = new Uint8Array(atqaBuffer);
76
+ if (uint8Array.length < 2) return null;
77
+
78
+ const byte1 = uint8Array[0];
79
+ const byte2 = uint8Array[1];
80
+
81
+ return {
82
+ uidType: (byte1 >> 7) & 0x01, // UID 类型
83
+ storageSize: (byte1 >> 4) & 0x07, // 存储大小编码
84
+ manufacturer: byte1 & 0x0f, // 制造商代码
85
+ cardFeatures: (byte2 >> 4) & 0x0f, // 功能特性
86
+ reserved: byte2 & 0x0f, // 保留位
87
+ };
88
+ }
89
+
90
+ export function parseAtqaAndroid(atqa: number[]) {
91
+ const byte1 = atqa[0];
92
+ const byte2 = atqa[1];
93
+ return {
94
+ uidType: (byte1 >> 7) & 0x01, // UID 类型
95
+ storageSize: (byte1 >> 4) & 0x07, // 存储大小编码
96
+ manufacturer: byte1 & 0x0f, // 制造商代码
97
+ cardFeatures: (byte2 >> 4) & 0x0f, // 功能特性
98
+ reserved: byte2 & 0x0f, // 保留位
99
+ };
100
+ }
101
+
102
+ /**
103
+ * 获取 SAK 类型
104
+ *
105
+ * @export
106
+ * @param {number} sak
107
+ * @return {*}
108
+ */
109
+ export function getSakType(sak: number) {
110
+ switch (sak) {
111
+ case 0x00:
112
+ return '未指定类型';
113
+ case 0x08:
114
+ return 'Mifare Classic 1K';
115
+ case 0x09:
116
+ return 'Mifare Classic Mini';
117
+ case 0x10:
118
+ return 'Mifare Plus';
119
+ case 0x11:
120
+ return 'Mifare Classic 4K';
121
+ case 0x18:
122
+ return 'Mifare Ultralight / Ultralight C';
123
+ case 0x20:
124
+ return 'DESFire';
125
+ default:
126
+ return '未知';
127
+ }
128
+ }
129
+
130
+ export function getSakHex(sak: number) {
131
+ return '0x' + (sak < 16 ? '0' : '') + sak.toString(16).toUpperCase();
132
+ }
133
+
134
+ /** 获取读写标准 */
135
+ export function getReadWriteStandard(nfcTypes: NFC.NfcType[]) {
136
+ return nfcTypes.map((nfcType) => {
137
+ switch (nfcType) {
138
+ case 'NFC-A':
139
+ case 'MIFARE Classic':
140
+ case 'MIFARE Ultralight':
141
+ return 'ISO 14443-3A';
142
+ case 'NFC-B':
143
+ return 'ISO 14443-3B';
144
+ case 'NFC-F':
145
+ return 'ISO 14443-3F';
146
+ case 'NFC-V':
147
+ return 'ISO 15693';
148
+ case 'Ndef':
149
+ case 'NDEF':
150
+ return 'NDEF';
151
+ case 'ISO-DEP':
152
+ return 'ISO 14443-4';
153
+ case 'Ndef Formatable':
154
+ return 'NDEF Formatable';
155
+ default:
156
+ return '未知';
157
+ }
158
+ });
159
+ }
160
+
161
+ export function getAndroidTechs(techs: string[]) {
162
+ return techs.map((tech) => {
163
+ switch (tech) {
164
+ case 'android.nfc.tech.IsoDep':
165
+ return 'ISO-DEP';
166
+ case 'android.nfc.tech.MifareClassic':
167
+ return 'MIFARE Classic';
168
+ case 'android.nfc.tech.MifareUltralight':
169
+ return 'MIFARE Ultralight';
170
+ case 'android.nfc.tech.NfcA':
171
+ return 'NFC-A';
172
+ case 'android.nfc.tech.NfcB':
173
+ return 'NFC-B';
174
+ case 'android.nfc.tech.NfcF':
175
+ return 'NFC-F';
176
+ case 'android.nfc.tech.NfcV':
177
+ return 'NFC-V';
178
+ case 'android.nfc.tech.Ndef':
179
+ return 'Ndef';
180
+ case 'android.nfc.tech.NdefFormatable':
181
+ return 'Ndef Formatable';
182
+ default:
183
+ return '';
184
+ }
185
+ });
186
+ }
187
+
188
+ export function getTnfDescription(tnf: string) {
189
+ switch (tnf) {
190
+ case '0':
191
+ return 'TNF_EMPTY';
192
+ case '1':
193
+ return 'TNF_WELL_KNOWN';
194
+ case '2':
195
+ return 'TNF_MIME_MEDIA';
196
+ case '3':
197
+ return 'TNF_ABSOLUTE_URI';
198
+ case '4':
199
+ return 'TNF_EXTERNAL_TYPE';
200
+ case '5':
201
+ return 'TNF_UNKNOWN';
202
+ case '6':
203
+ return 'TNF_UNCHANGED';
204
+ case '7':
205
+ return 'TNF_RESERVED';
206
+ default:
207
+ return '未知';
208
+ }
209
+ }
210
+
211
+ /**
212
+ * 获取URI记录的具体类型描述
213
+ * @param prefixCode - URI前缀代码(payload的第一个字节)
214
+ * @returns URI类型描述
215
+ */
216
+ function getUriTypeDescription(prefixCode: number): string {
217
+ const uriPrefixes: { [key: number]: string } = {
218
+ 0x00: '未指定(纯URI)',
219
+ 0x01: 'http://www.', // http://www.
220
+ 0x02: 'https://www.', // https://www.
221
+ 0x03: 'http://', // http://
222
+ 0x04: 'https://', // https://
223
+ 0x05: 'tel:', // tel: (电话号码)
224
+ 0x06: 'mailto:', // mailto: (邮箱地址)
225
+ 0x07: 'ftp://anonymous:anonymous@', // ftp://anonymous:anonymous@
226
+ 0x08: 'ftp://ftp.', // ftp://ftp.
227
+ 0x09: 'ftps://', // ftps://
228
+ 0x0a: 'sftp://', // sftp://
229
+ 0x0b: 'smb://', // smb://
230
+ 0x0c: 'nfs://', // nfs://
231
+ 0x0d: 'ftp://', // ftp://
232
+ 0x0e: 'dav://', // dav://
233
+ 0x0f: 'news:', // news:
234
+ 0x10: 'telnet://', // telnet://
235
+ 0x11: 'imap:', // imap:
236
+ 0x12: 'rtsp://', // rtsp://
237
+ 0x13: 'urn:', // urn:
238
+ 0x14: 'pop:', // pop:
239
+ 0x15: 'sip:', // sip:
240
+ 0x16: 'sips:', // sips:
241
+ 0x17: 'tftp:', // tftp:
242
+ 0x18: 'btspp://', // btspp:// (Bluetooth)
243
+ 0x19: 'btl2cap://', // btl2cap:// (Bluetooth)
244
+ 0x1a: 'btgoep://', // btgoep:// (Bluetooth)
245
+ 0x1b: 'tcpobex://', // tcpobex://
246
+ 0x1c: 'irdaobex://', // irdaobex://
247
+ 0x1d: 'file://', // file://
248
+ 0x1e: 'urn:epc:id:', // urn:epc:id:
249
+ 0x1f: 'urn:epc:tag:', // urn:epc:tag:
250
+ 0x20: 'urn:epc:pat:', // urn:epc:pat:
251
+ 0x21: 'urn:epc:raw:', // urn:epc:raw:
252
+ 0x22: 'urn:epc:', // urn:epc:
253
+ 0x23: 'urn:nfc:', // urn:nfc:
254
+ };
255
+
256
+ return uriPrefixes[prefixCode] || '未知';
257
+ }
258
+
259
+ /** 定义 URI 前缀映射表 */
260
+ const uriPrefixMap: { [key: string]: number } = {
261
+ '': 0x00, // 未指定协议
262
+ 'http://www.': 0x01,
263
+ 'https://www.': 0x02,
264
+ 'http://': 0x03,
265
+ 'https://': 0x04,
266
+ 'tel:': 0x05, // 电话号码
267
+ 'mailto:': 0x06, // 邮箱地址
268
+ 'ftp://anonymous:anonymous@': 0x07,
269
+ 'ftp://ftp.': 0x08,
270
+ 'ftps://': 0x09,
271
+ 'sftp://': 0x0a,
272
+ 'smb://': 0x0b,
273
+ 'nfs://': 0x0c,
274
+ 'ftp://': 0x0d,
275
+ 'dav://': 0x0e,
276
+ 'news:': 0x0f,
277
+ 'telnet://': 0x10,
278
+ 'imap:': 0x11,
279
+ 'rtsp://': 0x12,
280
+ 'urn:': 0x13,
281
+ 'pop:': 0x14,
282
+ 'sip:': 0x15,
283
+ 'sips:': 0x16,
284
+ 'tftp:': 0x17,
285
+ 'btspp://': 0x18,
286
+ 'btl2cap://': 0x19,
287
+ 'btgoep://': 0x1a,
288
+ 'tcpobex://': 0x1b,
289
+ 'irdaobex://': 0x1c,
290
+ 'file://': 0x1d,
291
+ 'urn:epc:id:': 0x1e,
292
+ 'urn:epc:tag:': 0x1f,
293
+ 'urn:epc:pat:': 0x20,
294
+ 'urn:epc:raw:': 0x21,
295
+ 'urn:epc:': 0x22,
296
+ 'urn:nfc:': 0x23,
297
+ };
298
+
299
+ /**
300
+ * 创建 URI 类型的 NDEF 记录
301
+ * 根据 NFC Forum URI Record Type Definition 规范
302
+ *
303
+ * @param uri 完整的 URI 字符串,如 "tel:18888888888", "https://www.example.com"
304
+ * @returns NDEF 记录对象
305
+ */
306
+ export function createUriNdefRecord(uri: string): NFC.NdefRecord {
307
+ let prefix = '';
308
+ let suffix = uri;
309
+
310
+ // 查找匹配的前缀
311
+ for (const [protocol, _code] of Object.entries(uriPrefixMap)) {
312
+ if (uri.startsWith(protocol) && protocol.length > prefix.length) {
313
+ prefix = protocol;
314
+ }
315
+ }
316
+
317
+ // 提取 URI 后缀部分
318
+ if (prefix) {
319
+ suffix = uri.substring(prefix.length);
320
+ }
321
+
322
+ // 获取前缀代码
323
+ const prefixCode = uriPrefixMap[prefix] || 0x00;
324
+
325
+ // 构建 payload:前缀代码 + 剩余 URI
326
+ const payloadArray = [prefixCode];
327
+ if (suffix) {
328
+ // 将后缀字符串转换为字节数组
329
+ const suffixBytes = stringToBytes(suffix);
330
+ payloadArray.push(...suffixBytes);
331
+ }
332
+
333
+ return {
334
+ tnf: 1, // TNF_WELL_KNOWN
335
+ type: 'U', // URI 类型
336
+ payload: arrayNumberToArrayBuffer(payloadArray),
337
+ };
338
+ }
339
+
340
+ export function getTypeDescription(type: string, payload?: number[]) {
341
+ switch (type) {
342
+ case 'T':
343
+ return '文本记录';
344
+ case 'U':
345
+ // URI记录需要根据payload的第一个字节确定具体类型
346
+ if (payload && payload.length > 0) {
347
+ const uriPrefixCode = payload[0]; // 第一个字节是URI前缀代码
348
+ return getUriTypeDescription(uriPrefixCode);
349
+ }
350
+ return 'URI记录';
351
+ case 'Sp':
352
+ return 'Smart Poster记录';
353
+ case 'Sig':
354
+ return '签名记录';
355
+ case 'application/vnd.wfa.wsc':
356
+ return 'Wi-Fi Simple Configuration记录';
357
+ case 'android.com:pkg':
358
+ return 'Android 包名记录';
359
+ case 'text/vcard':
360
+ return 'VCard 记录';
361
+ default:
362
+ // 如果是 MIME 类型格式,可以返回更具体的描述
363
+ if (type.startsWith('application/')) {
364
+ return `应用数据类型: ${type}`;
365
+ } else if (type.startsWith('text/')) {
366
+ return `文本数据类型: ${type}`;
367
+ } else if (
368
+ type.startsWith('image/') ||
369
+ type.startsWith('video/') ||
370
+ type.startsWith('audio/')
371
+ ) {
372
+ return `媒体数据类型: ${type}`;
373
+ }
374
+ return '未知';
375
+ }
376
+ }
377
+
378
+ /**
379
+ * 通用 TLV 解析配置项接口
380
+ * 支持自定义各类 TLV 解析规则,适配不同格式数据
381
+ */
382
+ interface TLVParseConfig {
383
+ // 合法 TLV 的 Type 高字节标识(默认:0x10,对应 NFC WiFi 数据)
384
+ legalTypeHigh?: number;
385
+ // 需要跳过的无效 TLV 类型(低字节)列表
386
+ skipTlvTypes?: number[];
387
+ // 长度解析策略(支持自定义,适配标准大端/低字节有效/高字节有效等)
388
+ lengthParseStrategy: (lengthHigh: number, lengthLow: number) => number;
389
+ // 目标 TLV 类型映射(键:TLV 低字节类型,值:字段名称 + 数据转换函数)
390
+ targetTlvMap: Record<
391
+ number,
392
+ {
393
+ fieldName: string;
394
+ converter: (valueBytes: number[]) => any;
395
+ }
396
+ >;
397
+ }
398
+
399
+ /**
400
+ * 通用 TLV 解析工具函数
401
+ * 可适配 NFC 门禁卡、公交卡、WiFi 数据等各类 TLV 格式数据
402
+ * @param tlvBytes 原始 TLV 字节数组
403
+ * @param config 自定义解析配置项
404
+ * @returns 解析后的结果对象(键:配置中定义的 fieldName,值:转换后的数据)
405
+ */
406
+ export function parseUniversalTLV(
407
+ tlvBytes: number[],
408
+ config: TLVParseConfig,
409
+ ): Record<string, any> {
410
+ // 配置项默认值填充
411
+ const finalConfig = {
412
+ legalTypeHigh: 0x10,
413
+ skipTlvTypes: [],
414
+ ...config,
415
+ };
416
+
417
+ // 存储解析结果
418
+ const parseResult: Record<string, any> = {
419
+ raw: tlvBytes,
420
+ };
421
+ const byteLength = tlvBytes.length;
422
+ let index = 0;
423
+
424
+ // 循环解析 TLV 数据(核心:按配置项灵活解析)
425
+ while (index < byteLength) {
426
+ // 1. 校验是否有足够字节读取 Type(2 字节)
427
+ if (index + 1 >= byteLength) {
428
+ break;
429
+ }
430
+
431
+ const typeHigh = tlvBytes[index];
432
+ const typeLow = tlvBytes[index + 1];
433
+ const currentType = typeLow;
434
+
435
+ // 2. 过滤非法 TLV(非配置指定的高字节标识)
436
+ if (typeHigh !== finalConfig.legalTypeHigh) {
437
+ index++;
438
+ continue;
439
+ }
440
+
441
+ // 3. 跳过配置中指定的无效 TLV(直接跳过头部 4 字节:Type2 + Length2)
442
+ if (finalConfig.skipTlvTypes?.includes(currentType)) {
443
+ index += 4;
444
+ continue;
445
+ }
446
+
447
+ // 4. 步进 Type 字节(2 字节),准备读取 Length
448
+ index += 2;
449
+ if (index + 1 >= byteLength) {
450
+ break;
451
+ }
452
+
453
+ // 5. 按自定义策略解析 Length(灵活适配不同格式)
454
+ const lengthHigh = tlvBytes[index];
455
+ const lengthLow = tlvBytes[index + 1];
456
+ const currentLength = finalConfig.lengthParseStrategy(
457
+ lengthHigh,
458
+ lengthLow,
459
+ );
460
+
461
+ // 6. 步进 Length 字节(2 字节),准备读取 Value
462
+ index += 2;
463
+ if (index + currentLength > byteLength) {
464
+ break;
465
+ }
466
+
467
+ // 7. 读取 Value 部分(按自定义长度截取)
468
+ const currentValue = tlvBytes.slice(index, index + currentLength);
469
+
470
+ // 8. 强制步进 Value 字节(避免索引卡死,确保遍历完整性)
471
+ index += currentLength;
472
+
473
+ // 9. 按自定义映射转换目标 TLV 数据
474
+ const targetTlvConfig = finalConfig.targetTlvMap[currentType];
475
+ if (targetTlvConfig) {
476
+ try {
477
+ const convertedData = targetTlvConfig.converter(currentValue);
478
+ parseResult[targetTlvConfig.fieldName] = convertedData;
479
+ } catch (_e) {
480
+ parseResult[targetTlvConfig.fieldName] = null;
481
+ }
482
+ }
483
+ }
484
+
485
+ return parseResult;
486
+ }
487
+
488
+ /**
489
+ * 辅助工具:修正 authType 转换逻辑,适配 32(0x20)等新值
490
+ */
491
+ const TLVConverterTools = {
492
+ // 1. 字节数组 → UTF-8 字符串(保持原有)
493
+ bytesToUtf8String: (bytes: number[]): string => {
494
+ try {
495
+ return new TextDecoder('utf-8', { fatal: false }).decode(
496
+ new Uint8Array(bytes),
497
+ );
498
+ } catch (_e) {
499
+ return bytes.map((byte) => String.fromCharCode(byte)).join('');
500
+ }
501
+ },
502
+
503
+ // 2. 字节数组 → WIFI 认证类型(核心修正:添加 32(0x20)的映射)
504
+ bytesToWifiAuthType: (bytes: number[]): string => {
505
+ // 适配当前字节数组的 [0, 32],取有效数据(低字节优先,与格式一致)
506
+ const authValue = bytes.length >= 2 ? bytes[1] || bytes[0] : bytes[0];
507
+ console.log(
508
+ `[authType 解析] 原始标识值:${authValue}(十六进制:0x${authValue.toString(16).padStart(2, '0')}`,
509
+ );
510
+
511
+ switch (authValue) {
512
+ case 1:
513
+ return 'OPEN (无认证)';
514
+ case 2:
515
+ case 34: // 0x22:原有 WPA2-Personal
516
+ return 'WPA2-Personal';
517
+ case 32: // 0x20:新增,适配当前字节数组的 authType 值
518
+ return 'WPA-Personal (兼容 WPA2)';
519
+ case 3:
520
+ return 'WPA-Personal';
521
+ default:
522
+ return `未知认证类型(0x${authValue.toString(16).padStart(4, '0')}`;
523
+ }
524
+ },
525
+
526
+ // 3. 其他工具函数(保持原有,可选)
527
+ bytesToHexString: (bytes: number[], separator = ''): string => {
528
+ return bytes
529
+ .map((byte) => byte.toString(16).padStart(2, '0').toUpperCase())
530
+ .join(separator);
531
+ },
532
+ bytesToDecimal: (bytes: number[], isBigEndian = true): number => {
533
+ let result = 0;
534
+ if (isBigEndian) {
535
+ bytes.forEach((byte) => {
536
+ result = (result << 8) | byte;
537
+ });
538
+ } else {
539
+ bytes.reverse().forEach((byte) => {
540
+ result = (result << 8) | byte;
541
+ });
542
+ }
543
+ return result;
544
+ },
545
+ bytesToWifiEncryptionType: (bytes: number[]): string => {
546
+ const isAllFf = bytes.every((byte) => byte === 255);
547
+ if (isAllFf) {
548
+ return 'AES/TKIP (WPA2 兼容)';
549
+ }
550
+ const encryptValue = TLVConverterTools.bytesToDecimal(bytes);
551
+ return encryptValue === 0
552
+ ? 'NONE (无加密)'
553
+ : `WEP(0x${encryptValue.toString(16).padStart(4, '0')}`;
554
+ },
555
+ };
556
+
557
+ // 定义 WiFi 数据的自定义解析配置
558
+ export const wifiTLVConfig: TLVParseConfig = {
559
+ legalTypeHigh: 0x10, // 合法 TLV 高字节为 16
560
+ skipTlvTypes: [14], // 跳过无效 TLV 类型 14
561
+ // 长度解析策略:低字节有效(适配非标准 WiFi 数据)
562
+ lengthParseStrategy: (lengthHigh, lengthLow) => lengthLow,
563
+ // 目标 TLV 映射(按需扩展)
564
+ targetTlvMap: {
565
+ 69: {
566
+ // SSID 类型
567
+ fieldName: 'ssid',
568
+ converter: TLVConverterTools.bytesToUtf8String,
569
+ },
570
+ 39: {
571
+ // 密码类型
572
+ fieldName: 'password',
573
+ converter: TLVConverterTools.bytesToUtf8String,
574
+ },
575
+ 3: {
576
+ // 认证类型
577
+ fieldName: 'authType',
578
+ converter: TLVConverterTools.bytesToWifiAuthType,
579
+ },
580
+ 32: {
581
+ // 加密类型
582
+ fieldName: 'encryptionType',
583
+ converter: TLVConverterTools.bytesToWifiEncryptionType,
584
+ },
585
+ },
586
+ };
587
+
588
+ // WSC 配置接口
589
+
590
+ // 生成 WSC 数据 (Android/Windows 更兼容的格式)
591
+ export function generateWSCBytesForAndroid(config: NFC.WSCConfig): number[] {
592
+ const bytes: number[] = [];
593
+
594
+ // 这是 Android 和 Windows 都接受的简化格式
595
+ // 参考: Wi-Fi Alliance WSC Specification 2.0
596
+
597
+ // 1. Credential 属性 (0x100e)
598
+ // 这是一个容器属性,包含其他属性
599
+
600
+ // 先收集所有属性
601
+ const credentialAttrs: number[] = [];
602
+
603
+ // SSID
604
+ const ssidBytes = Array.from(config.ssid, (c) => c.charCodeAt(0));
605
+ credentialAttrs.push(0x10, 0x45); // SSID
606
+ credentialAttrs.push(0x00, ssidBytes.length);
607
+ credentialAttrs.push(...ssidBytes);
608
+
609
+ // 认证类型
610
+ let authValue = 0x0020; // 默认 WPA2-Personal
611
+ if (config.authType === 'OPEN') authValue = 0x0001;
612
+ if (config.authType === 'WPA') authValue = 0x0022;
613
+
614
+ credentialAttrs.push(0x10, 0x03); // Authentication Type
615
+ credentialAttrs.push(0x00, 0x02);
616
+ credentialAttrs.push((authValue >> 8) & 0xff, authValue & 0xff);
617
+
618
+ // 加密类型
619
+ let encValue = 0x0008; // 默认 AES
620
+ if (config.encryptionType === 'NONE') encValue = 0x0001;
621
+ if (config.encryptionType === 'TKIP') encValue = 0x0004;
622
+ if (config.encryptionType === 'WEP') encValue = 0x0008;
623
+
624
+ credentialAttrs.push(0x10, 0x0f); // Encryption Type
625
+ credentialAttrs.push(0x00, 0x02);
626
+ credentialAttrs.push((encValue >> 8) & 0xff, encValue & 0xff);
627
+
628
+ // 网络密钥
629
+ if (config.authType !== 'OPEN') {
630
+ const keyBytes = Array.from(config.password, (c) => c.charCodeAt(0));
631
+ credentialAttrs.push(0x10, 0x27); // Network Key
632
+ credentialAttrs.push(0x00, keyBytes.length);
633
+ credentialAttrs.push(...keyBytes);
634
+ }
635
+
636
+ // 网络密钥索引 (WEP 需要)
637
+ if (config.encryptionType === 'WEP') {
638
+ const idx = config.networkKeyIndex || 1;
639
+ credentialAttrs.push(0x10, 0x28); // Network Key Index
640
+ credentialAttrs.push(0x00, 0x01);
641
+ credentialAttrs.push(idx);
642
+ }
643
+
644
+ // 将 Credential 属性添加到主字节数组
645
+ bytes.push(0x10, 0x0e); // Credential
646
+ bytes.push(0x00, credentialAttrs.length);
647
+ bytes.push(...credentialAttrs);
648
+
649
+ return bytes;
650
+ }
651
+
652
+ /**
653
+ * vCard (text/vcard) 数据解析工具
654
+ * 适配 vCard 3.0 格式,支持提取核心联系人字段,具备容错性
655
+ */
656
+
657
+ const vcardFieldMap: Record<string, string> = {
658
+ FN: 'fullName',
659
+ VERSION: 'version',
660
+ ORG: 'organization',
661
+ TEL: 'telephone',
662
+ ADR: 'address',
663
+ URL: 'url',
664
+ };
665
+ const reverseVcardFieldMap: Record<string, string> = Object.entries(
666
+ vcardFieldMap,
667
+ ).reduce(
668
+ (acc, [key, value]) => {
669
+ acc[value] = key;
670
+ return acc;
671
+ },
672
+ {} as Record<string, string>,
673
+ );
674
+
675
+ /**
676
+ * 解析 vCard 格式的 payload 字符串,提取联系人信息
677
+ * @param vcardPayload NFC 读取到的 vCard 原始字符串(含 \n 分隔符)
678
+ * BEGIN:VCARD\nVERSION:3.0\nFN:any\nORG:any\nTEL:18888888888\nADR:杭州\nURL:www.baidu.com\nEND:VCARD\n
679
+ * @returns 结构化的联系人信息对象
680
+ */
681
+ export function parseVCardPayload(vcardPayload: string): NFC.VCardContactInfo {
682
+ // 初始化默认结果
683
+ const contactInfo: NFC.VCardContactInfo = {
684
+ fullName: '',
685
+ version: '',
686
+ organization: '',
687
+ telephone: '',
688
+ address: '',
689
+ url: '',
690
+ };
691
+
692
+ // 步骤1:预处理 payload(去除首尾空白、处理不同分隔符 \n/\r\n/\r,拆分行为数组)
693
+ const processedPayload = vcardPayload
694
+ .trim()
695
+ .replace(/\r\n/g, '\n')
696
+ .replace(/\r/g, '\n');
697
+ const vcardLines = processedPayload
698
+ .split('\n')
699
+ .filter((line) => line.trim() !== '');
700
+
701
+ // 步骤2:验证是否为合法 vCard 格式
702
+ const isLegalVCard =
703
+ vcardLines.some((line) => line.startsWith('BEGIN:VCARD')) &&
704
+ vcardLines.some((line) => line.startsWith('END:VCARD'));
705
+ if (!isLegalVCard) {
706
+ console.warn(
707
+ '[vCard 解析] 无效的 vCard 格式,缺少 BEGIN:VCARD 或 END:VCARD 标记',
708
+ );
709
+ return contactInfo;
710
+ }
711
+
712
+ // 步骤3:遍历每行,解析关键字段(KEY:VALUE 格式)
713
+
714
+ for (const line of vcardLines) {
715
+ // 拆分 KEY 和 VALUE(按第一个冒号分割,避免 VALUE 中包含冒号的情况)
716
+ const colonIndex = line.indexOf(':');
717
+ if (colonIndex === -1) continue;
718
+
719
+ const fieldKey = line.substring(0, colonIndex).trim().toUpperCase();
720
+ const fieldValue = line.substring(colonIndex + 1).trim();
721
+
722
+ // 映射到结构化对象字段
723
+ const targetField = vcardFieldMap[fieldKey];
724
+ if (targetField) {
725
+ contactInfo[targetField] = fieldValue;
726
+ } else {
727
+ // 支持扩展字段(直接挂载到对象上)
728
+ contactInfo[fieldKey.toLowerCase()] = fieldValue;
729
+ }
730
+ }
731
+
732
+ return contactInfo;
733
+ }
734
+
735
+ /**
736
+ * 格式化 vCard 联系人信息
737
+ *
738
+ * @export
739
+ * @param {NFC.VCardContactInfo} cardInfo
740
+ * @return {*}
741
+ */
742
+ export function formatCardInfo(cardInfo: NFC.VCardContactInfo) {
743
+ const list = Object.keys(cardInfo)
744
+ .filter((key) => {
745
+ return cardInfo[key];
746
+ })
747
+ .map((key) => {
748
+ return `${reverseVcardFieldMap[key]}:${cardInfo[key]}`;
749
+ });
750
+
751
+ list.unshift('BEGIN:VCARD');
752
+ list.push('END:VCARD');
753
+ return list.join('\n');
754
+ }