From 8d2180940b9c13c658dd89a0213fd8aadab120a9 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Thu, 12 Mar 2026 22:53:57 +0800 Subject: [PATCH] feat: enhance BottomNav component and update project configuration --- AGENTS.md | 61 +----- config/dev.ts | 6 +- config/index.ts | 11 +- package.json | 3 +- pnpm-lock.yaml | 145 +++++++++++++ project.config.json | 54 +++-- public/update.json | 0 src/app.config.ts | 1 + src/components/BottomNav/index.css | 23 +- src/components/BottomNav/index.tsx | 15 +- src/components/Icons/index.tsx | 74 +++++++ src/config.tsx | 14 +- src/modules/taro-request.ts | 10 + src/pages/index/index.css | 21 +- src/pages/index/index.tsx | 5 +- src/pages/mine/index.config.ts | 1 + src/pages/mine/index.css | 62 ++++-- src/pages/mine/index.tsx | 64 +++++- src/pages/nfc-read/index.config.ts | 5 + src/pages/nfc-read/index.css | 56 +++++ src/pages/nfc-read/index.tsx | 155 ++++++++++++++ src/pages/nfc-read/lib/app.ts | 94 ++++++++ src/pages/nfc-read/lib/nfc.ts | 331 +++++++++++++++++++++++++++++ src/pages/nfc-read/readme.md | 3 + tsconfig.json | 4 +- 25 files changed, 1083 insertions(+), 135 deletions(-) create mode 100644 public/update.json create mode 100644 src/components/Icons/index.tsx create mode 100644 src/modules/taro-request.ts create mode 100644 src/pages/nfc-read/index.config.ts create mode 100644 src/pages/nfc-read/index.css create mode 100644 src/pages/nfc-read/index.tsx create mode 100644 src/pages/nfc-read/lib/app.ts create mode 100644 src/pages/nfc-read/lib/nfc.ts create mode 100644 src/pages/nfc-read/readme.md diff --git a/AGENTS.md b/AGENTS.md index ac1dc20..2497fab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## 项目概述 -这是一个基于 **Taro 框架的多端小程序开发模板**项目。它提供了一个统一的开发框架,用于构建跨平台小程序,可以编译到微信、小红书、支付宝、百度、字节跳动、H5、React Native、QQ、京东等多个平台。 +微信小程序开发模板,基于Taro框架。 ## 技术栈 @@ -48,52 +48,6 @@ taro-template/ ## 开发指南 -### 创建新页面 - -使用 Taro CLI 创建新页面: - -```bash -npm run new -- [pageName] -``` - -### 添加平台特定代码 - -使用 `Taro.getEnv()` 检测当前平台: - -```typescript -import Taro from "@tarojs/taro"; - -const env = Taro.getEnv(); -if (env === Taro.ENV_TYPE.WEAPP) { - // 微信小程序特定代码 -} else if (env === "xhs") { - // 小红书特定代码 -} -``` - -或使用提供的工具函数: - -```typescript -import { isXHS } from './pages/xhs/utils/is-xhs'; - -if (isXHS()) { - // 小红书特定代码 -} -``` - -### 样式 - -项目使用标准 CSS,页面样式与组件放在一起: - -- 全局样式: `src/app.css` -- 页面样式: `src/pages/{page}/{page}.css` - -### 环境变量 - -- `.env.development` - 开发环境 -- `.env.test` - 测试环境 -- `.env.production` - 生产环境 - ## 配置文件说明 ### app.config.ts @@ -104,14 +58,6 @@ if (isXHS()) { 应用入口组件,包含 `useLaunch` 生命周期钩子,用于应用初始化。在微信环境下会自动调用 `Taro.login`。 -### project.xhs.json - -小红书 IDE 特定配置(appid、编译设置等)。 - -### tsconfig.json - -TypeScript 编译器选项,包括路径别名配置(`@/*` → `./src/*`)。 - ## AI 代理注意事项 1. 修改平台特定代码时,使用环境检测确保跨平台兼容性 @@ -119,3 +65,8 @@ TypeScript 编译器选项,包括路径别名配置(`@/*` → `./src/*`) 3. 遵循现有的代码风格和目录结构 4. 添加新依赖时,确保与所有目标平台兼容 5. 项目使用 pnpm 作为包管理器 + +## 避免 +1. 不能使用 `?.` 和 `??` 操作符, 因为不支持 +2. 不能使用 TextDecoder 和 TextEncoder, 因为不支持 +3. 不能使用 Buffer, 因为不支持 \ No newline at end of file diff --git a/config/dev.ts b/config/dev.ts index aa5a594..aee03a1 100644 --- a/config/dev.ts +++ b/config/dev.ts @@ -6,5 +6,9 @@ export default { stats: true }, mini: {}, - h5: {} + h5: { + devServer: { + host: '0.0.0.0' + } + } } satisfies UserConfigExport<'webpack5'> diff --git a/config/index.ts b/config/index.ts index c92ce76..a382d95 100644 --- a/config/index.ts +++ b/config/index.ts @@ -7,8 +7,8 @@ import prodConfig from "./prod"; // @ts-ignore export default defineConfig<"webpack5">(async (merge, { command, mode }) => { const baseConfig: UserConfigExport<"webpack5"> = { - projectName: "2025-09-14-webpack-demo", - date: "2025-9-14", + projectName: "taro-template", + date: "2026-03-12", designWidth: 750, deviceRatio: { 640: 2.34 / 2, @@ -31,6 +31,12 @@ export default defineConfig<"webpack5">(async (merge, { command, mode }) => { enable: false, // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache }, mini: { + compiler: { + type: 'webpack5', + prebundle: { + enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache + }, + }, postcss: { pxtransform: { enable: true, @@ -51,6 +57,7 @@ export default defineConfig<"webpack5">(async (merge, { command, mode }) => { h5: { publicPath: "/", staticDirectory: "static", + esnextModules: ["@stencil/core"], output: { filename: "js/[name].[hash:8].js", chunkFilename: "js/[name].[chunkhash:8].js", diff --git a/package.json b/package.json index a188fbd..3c54e3b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "author": "", "dependencies": { "@babel/runtime": "^7.28.6", - "@nutui/icons-react-taro": "3.0.2-cpp.3.beta.5", + "@kevisual/api": "^0.0.64", + "@kevisual/query": "^0.0.53", "@nutui/nutui-react-taro": "^2.7.15", "@tarojs/components": "4.1.11", "@tarojs/helper": "4.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b80ab7f..f55a5dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@babel/runtime': specifier: ^7.28.6 version: 7.28.6 + '@kevisual/api': + specifier: ^0.0.64 + version: 0.0.64(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@kevisual/query': + specifier: ^0.0.53 + version: 0.0.53 '@nutui/icons-react-taro': specifier: 3.0.2-cpp.3.beta.5 version: 3.0.2-cpp.3.beta.5 @@ -1143,12 +1149,31 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@kevisual/api@0.0.64': + resolution: {integrity: sha512-y7wP8ucvi/rflVGd6uJpvuEUTwI7wMef8+ITQzv4flg7a2pwWZYe/DT0TOyaqDAqKOTlXaVIdBeI15jXuUxIIg==} + + '@kevisual/context@0.0.8': + resolution: {integrity: sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA==} + + '@kevisual/js-filter@0.0.6': + resolution: {integrity: sha512-FcbOsmS1inhwrfgXMM/XLFTGTHUxBCss32JEMYdEFWQDYCar5rN8cxD1W8FuKDTVRlpA+zBpQ/BE6XT4UaeljA==} + + '@kevisual/load@0.0.6': + resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==} + + '@kevisual/query@0.0.53': + resolution: {integrity: sha512-PAhpCLBr0emz0lGNlTVHMbJiC5wrtGLbInPddRzgKE35fiyNt+SWSsUWABiD0DeNrLN/OxWyAFobt880Z/e5MQ==} + '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} '@napi-rs/triples@1.2.0': resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==} + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1178,6 +1203,10 @@ packages: '@nutui/touch-emulator@1.0.0': resolution: {integrity: sha512-k2hvI/9LlRA7Ph1Chni27pTuvPmKPt+/I10sWWd2sWzqiCOYRerD79eIwCMRGUF/q6WVDEKVnv00t9CEUL4sPA==} + '@paralleldrive/cuid2@3.3.0': + resolution: {integrity: sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==} + hasBin: true + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -2218,6 +2247,9 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2869,6 +2901,9 @@ packages: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true + error-causes@3.0.2: + resolution: {integrity: sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -2897,6 +2932,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + esbuild-loader@4.4.2: resolution: {integrity: sha512-8LdoT9sC7fzfvhxhsIAiWhzLJr9yT3ggmckXxsgvM07wgrRxhuT98XhLn3E7VczU5W5AFsPKv9DdWcZIubbWkQ==} peerDependencies: @@ -2976,6 +3014,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -3159,6 +3200,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4058,6 +4103,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + native-request@1.1.2: resolution: {integrity: sha512-/etjwrK0J4Ebbcnt35VMWnfiUX/B04uwGJxyJInagxDqf2z5drSt/lsOvEMWGYunz1kaLZAFrV4NDAbOoDKvAQ==} @@ -4251,6 +4301,9 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + path-browserify-esm@1.0.6: + resolution: {integrity: sha512-9nUwYvvu/yq1PYrUyYCihNWmpzacaRYF6gGbjLWErrZ4MRDWyfPN7RpE8E7tsw8eqBU/rr7mcoTXbS+Vih8uUA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -5019,6 +5072,12 @@ packages: solid-js@1.9.11: resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + sort-keys-length@1.0.1: resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} engines: {node: '>=0.10.0'} @@ -5053,6 +5112,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + spdy-transport@3.0.0: resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} @@ -5622,6 +5684,24 @@ packages: yup@1.7.1: resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@adobe/css-tools@4.3.3': {} @@ -6713,10 +6793,43 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kevisual/api@0.0.64(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@kevisual/context': 0.0.8 + '@kevisual/js-filter': 0.0.6 + '@kevisual/load': 0.0.6 + '@paralleldrive/cuid2': 3.3.0 + es-toolkit: 1.45.1 + eventemitter3: 5.0.4 + fuse.js: 7.1.0 + nanoid: 5.1.6 + path-browserify-esm: 1.0.6 + sonner: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + spark-md5: 3.0.2 + zustand: 5.0.11(@types/react@18.3.28)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + - react + - react-dom + - use-sync-external-store + + '@kevisual/context@0.0.8': {} + + '@kevisual/js-filter@0.0.6': {} + + '@kevisual/load@0.0.6': + dependencies: + eventemitter3: 5.0.4 + + '@kevisual/query@0.0.53': {} + '@leichtgewicht/ip-codec@2.0.5': {} '@napi-rs/triples@1.2.0': {} + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6754,6 +6867,12 @@ snapshots: '@nutui/touch-emulator@1.0.0': {} + '@paralleldrive/cuid2@3.3.0': + dependencies: + '@noble/hashes': 2.0.1 + bignumber.js: 9.3.1 + error-causes: 3.0.2 + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -7954,6 +8073,8 @@ snapshots: big.js@5.2.2: {} + bignumber.js@9.3.1: {} + binary-extensions@2.3.0: {} bl@1.2.3: @@ -8690,6 +8811,8 @@ snapshots: prr: 1.0.1 optional: true + error-causes@3.0.2: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -8717,6 +8840,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-toolkit@1.45.1: {} + esbuild-loader@4.4.2(webpack@5.105.4(@swc/core@1.3.96)): dependencies: esbuild: 0.27.3 @@ -8868,6 +8993,8 @@ snapshots: eventemitter3@4.0.7: {} + eventemitter3@5.0.4: {} + events@3.3.0: {} execa@5.1.1: @@ -9083,6 +9210,8 @@ snapshots: function-bind@1.1.2: {} + fuse.js@7.1.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -9974,6 +10103,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@5.1.6: {} + native-request@1.1.2: optional: true @@ -10171,6 +10302,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + path-browserify-esm@1.0.6: {} + path-browserify@1.0.1: {} path-case@3.0.4: @@ -10933,6 +11066,11 @@ snapshots: seroval: 1.5.1 seroval-plugins: 1.5.1(seroval@1.5.1) + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + sort-keys-length@1.0.1: dependencies: sort-keys: 1.1.2 @@ -10960,6 +11098,8 @@ snapshots: source-map@0.7.6: {} + spark-md5@3.0.2: {} + spdy-transport@3.0.0: dependencies: debug: 4.4.3 @@ -11548,3 +11688,8 @@ snapshots: tiny-case: 1.0.3 toposort: 2.0.2 type-fest: 2.19.0 + + zustand@5.0.11(@types/react@18.3.28)(react@18.3.1): + optionalDependencies: + '@types/react': 18.3.28 + react: 18.3.1 diff --git a/project.config.json b/project.config.json index d195171..bd788b4 100644 --- a/project.config.json +++ b/project.config.json @@ -1,15 +1,39 @@ -{ - "miniprogramRoot": "./dist", - "projectname": "2026-03-12-taro-template", - "description": "taro-template", - "appid": "touristappid", - "setting": { - "urlCheck": true, - "es6": false, - "enhance": false, - "compileHotReLoad": false, - "postcss": false, - "minified": false - }, - "compileType": "miniprogram" -} +{ + "miniprogramRoot": "dist/", + "projectname": "taro-template", + "description": "taro-template", + "appid": "wx5464d820d8c2e4ad", + "setting": { + "urlCheck": true, + "es6": false, + "enhance": false, + "compileHotReLoad": false, + "postcss": false, + "minified": false, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "packNpmRelationList": [], + "minifyWXSS": false, + "minifyWXML": true, + "localPlugins": false, + "disableUseStrict": false, + "useCompilerPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + } + }, + "compileType": "miniprogram", + "simulatorPluginLibVersion": {}, + "packOptions": { + "ignore": [], + "include": [] + }, + "editorSetting": {} +} \ No newline at end of file diff --git a/public/update.json b/public/update.json new file mode 100644 index 0000000..e69de29 diff --git a/src/app.config.ts b/src/app.config.ts index d1a849f..3c65a50 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,6 +1,7 @@ export default defineAppConfig({ pages: [ 'pages/index/index', + 'pages/nfc-read/index', 'pages/mine/index' ], window: { diff --git a/src/components/BottomNav/index.css b/src/components/BottomNav/index.css index 816c94c..e9b979e 100644 --- a/src/components/BottomNav/index.css +++ b/src/components/BottomNav/index.css @@ -1,8 +1,19 @@ +.bottom-nav-wrapper { + display: flex; + flex-direction: column; + height: 100vh; + width: 100%; + overflow: hidden; +} + +.bottom-nav-content { + flex: 1; + overflow-y: auto; + min-height: 0; +} + .bottom-nav { - position: fixed; - bottom: 0; - left: 0; - right: 0; + flex-shrink: 0; height: 100px; background: #ffffff; border-top: 1px solid #e8e8e8; @@ -17,12 +28,12 @@ .bottom-nav-item { display: flex; - flex-direction: column; + flex-direction: row; align-items: center; justify-content: center; flex: 1; height: 100%; - gap: 4px; + gap: 6px; } diff --git a/src/components/BottomNav/index.tsx b/src/components/BottomNav/index.tsx index e3fa320..5881aab 100644 --- a/src/components/BottomNav/index.tsx +++ b/src/components/BottomNav/index.tsx @@ -1,5 +1,6 @@ import { View, Text } from "@tarojs/components"; import Taro from "@tarojs/taro"; +import type { ReactNode } from "react"; import type { NavItem, NavKey } from "../../config"; import "./index.css"; @@ -10,33 +11,35 @@ const ICON_SIZE = 24; interface BottomNavProps { active: NavKey; navItems: NavItem[]; + children?: ReactNode; } -export default function BottomNav({ active, navItems }: BottomNavProps) { +export default function BottomNav({ active, navItems, children }: BottomNavProps) { const handleNavigate = (item: NavItem) => { if (item.key === active) return; Taro.redirectTo({ url: item.path }); }; return ( - + + {children} + {navItems.map((item) => { const isActive = active === item.key; const IconComp = item.icon; + const iconColor = isActive ? ACTIVE_COLOR : DEFAULT_COLOR; return ( handleNavigate(item)} > - + {item.label} ); })} + ); } diff --git a/src/components/Icons/index.tsx b/src/components/Icons/index.tsx new file mode 100644 index 0000000..00c30f1 --- /dev/null +++ b/src/components/Icons/index.tsx @@ -0,0 +1,74 @@ +import { Image } from "@tarojs/components"; +import type { CSSProperties } from "react"; + +// SVG path 与颜色的映射 +const svgPaths: Record = { + home: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z", + user: "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z", + nfc: "M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 18H4V4h16v16zM6 6h12v2H6zm0 4h12v2H6zm0 4h8v2H6z", +}; + +// 手动将字符串转为 base64(不依赖 Buffer) +function toBase64(str: string): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let result = ""; + let i = 0; + + while (i < str.length) { + const a = str.charCodeAt(i++); + const b = i < str.length ? str.charCodeAt(i++) : NaN; + const c = i < str.length ? str.charCodeAt(i++) : NaN; + + const triplet = (a << 16) | (b << 8) | c; + + result += chars[(triplet >> 18) & 0x3f]; + result += chars[(triplet >> 12) & 0x3f]; + result += isNaN(b) ? "=" : chars[(triplet >> 6) & 0x3f]; + result += isNaN(c) ? "=" : chars[triplet & 0x3f]; + } + + return result; +} + +interface IconProps { + name: "home" | "user" | "nfc"; + size?: number; + color?: string; + style?: CSSProperties; +} + +export default function SvgIcon({ name, size = 24, color, style }: IconProps) { + const path = svgPaths[name]; + + // 将 SVG 转为 data URL + const svgString = ``; + const dataUrl = `data:image/svg+xml;base64,${toBase64(svgString)}`; + + return ( + + ); +} + +// Home 图标 +export function IconHome(props: { size?: number; color?: string; style?: CSSProperties }) { + return ; +} + +// User 图标 +export function IconUser(props: { size?: number; color?: string; style?: CSSProperties }) { + return ; +} + +// NFC 图标 +export function IconNfc(props: { size?: number; color?: string; style?: CSSProperties }) { + return ; +} diff --git a/src/config.tsx b/src/config.tsx index 24b6950..49d5595 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -1,4 +1,4 @@ -import { Home, User } from "@nutui/icons-react-taro"; +import { IconHome, IconUser, IconNfc } from "./components/Icons"; import type { ComponentType } from "react"; export type NavKey = string; @@ -7,7 +7,7 @@ export interface NavItem { key: NavKey; label: string; path: string; - icon: ComponentType<{ size?: string | number; color?: string }>; + icon: ComponentType<{ size?: number; color?: string }>; } export const navItems: NavItem[] = [ @@ -15,12 +15,18 @@ export const navItems: NavItem[] = [ key: "index", label: "主页", path: "/pages/index/index", - icon: Home, + icon: IconHome, + }, + { + key: "nfc-read", + label: "NFC", + path: "/pages/nfc-read/index", + icon: IconNfc, }, { key: "mine", label: "我的", path: "/pages/mine/index", - icon: User, + icon: IconUser, }, ]; diff --git a/src/modules/taro-request.ts b/src/modules/taro-request.ts new file mode 100644 index 0000000..5dd4c17 --- /dev/null +++ b/src/modules/taro-request.ts @@ -0,0 +1,10 @@ +import Taro from "@tarojs/taro"; +import { Query } from '@kevisual/query' + +export const query = new Query({ + adapter: (config) => { + // + console.log("Request config:", config); + return Taro.request(config as any); + } +}); \ No newline at end of file diff --git a/src/pages/index/index.css b/src/pages/index/index.css index b00bc62..74d318a 100644 --- a/src/pages/index/index.css +++ b/src/pages/index/index.css @@ -1,27 +1,10 @@ -.nutui-react-demo { - height: 100vh; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.index { - height: 100vh; - background: #f5f5f5; - display: flex; - flex-direction: column; - overflow: hidden; -} - .index-content { - flex: 1; - overflow-y: auto; + height: 100%; + box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 80px; - padding-bottom: 100px; gap: 20px; } diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 7eaf814..5ae5396 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -10,7 +10,7 @@ export default function Index() { }); return ( - + Hello world! + {/* 昵称输入框,type="nickname" 可唤起微信昵称键盘,用 onBlur 接收昵称选择结果 */} + + - - + ); } diff --git a/src/pages/nfc-read/index.config.ts b/src/pages/nfc-read/index.config.ts new file mode 100644 index 0000000..ed90221 --- /dev/null +++ b/src/pages/nfc-read/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: "NFC 读取", + enablePullDownRefresh: false, + navigationStyle: "custom", +}); diff --git a/src/pages/nfc-read/index.css b/src/pages/nfc-read/index.css new file mode 100644 index 0000000..4a30d95 --- /dev/null +++ b/src/pages/nfc-read/index.css @@ -0,0 +1,56 @@ +.nfc-container { + height: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 80px; + padding-left: 32px; + padding-right: 32px; + padding-bottom: 32px; + background-color: #f5f5f5; + overflow: hidden; +} + +.nfc-icon { + width: 120px; + height: 120px; + margin-bottom: 32px; +} + +.nfc-status { + font-size: 18px; + color: #333; + margin-bottom: 16px; + text-align: center; +} + +.nfc-result { + width: 100%; + padding: 24px; + background-color: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.nfc-result-label { + font-size: 14px; + color: #666; + margin-bottom: 8px; +} + +.nfc-result-content { + font-size: 14px; + color: #333; + word-break: break-all; + line-height: 1.6; + white-space: pre-wrap; + font-family: monospace; +} + +.nfc-placeholder { + font-size: 14px; + color: #999; + text-align: center; +} \ No newline at end of file diff --git a/src/pages/nfc-read/index.tsx b/src/pages/nfc-read/index.tsx new file mode 100644 index 0000000..0327042 --- /dev/null +++ b/src/pages/nfc-read/index.tsx @@ -0,0 +1,155 @@ +import { View, Text } from "@tarojs/components"; +import { useDidShow, useDidHide } from "@tarojs/taro"; +import { useState, useRef, useEffect } from "react"; +import BottomNav from "../../components/BottomNav"; +import { navItems } from "../../config"; +import "./index.css"; +import Taro from "@tarojs/taro"; +import { parseNfcReadResult } from "./lib/nfc"; +export default function NfcRead() { + const [nfcData, setNfcData] = useState(""); + const [status, setStatus] = useState("等待靠近NFC标签..."); + const [isSupported, setIsSupported] = useState(true); + const nfcAdapterRef = useRef(null); + const isReadingRef = useRef(false); + + // 初始化 NFC 适配器并开始读取 + const startNfcRead = async () => { + try { + // #ifdef MP-WEIXIN + const wx = (window as any).wx; + if (!wx) { + setStatus("当前环境不支持 NFC"); + return; + } + + // 获取 NFC 适配器 + const adapter = Taro.getNFCAdapter(); + nfcAdapterRef.current = adapter; + + if (!adapter) { + setStatus("当前设备不支持 NFC"); + return; + } + + // 监听 NDEF 消息 + adapter.onDiscovered((res: any) => { + console.log("NFC discovered:", res); + + const result = parseNfcReadResult(res); + console.log("Parsed NFC result:", result); + if (result) { + setNfcData(JSON.stringify(result, null, 2)); + setStatus("读取成功!"); + } else { + setStatus("未读取到有效数据"); + } + }); + + // 开始监听 + adapter.startDiscovery({ + success: () => { + isReadingRef.current = true; + setStatus("请将设备靠近 NFC 标签"); + }, + fail: (err: any) => { + console.error("NFC discovery failed:", err); + // 检查是否是平台不支持的错误 + if (err.errMsg && err.errMsg.includes("current platform is not supported")) { + setIsSupported(false); + setStatus("当前平台不支持 NFC"); + } else { + setStatus("NFC 启动失败: " + (err.errMsg || "未知错误")); + } + }, + }); + // #endif + + // #ifndef MP-WEIXIN + setStatus("请在微信小程序中使用 NFC 功能"); + // #endif + } catch (error) { + console.error("NFC error:", error); + setStatus("NFC 初始化失败"); + } + }; + + // 停止 NFC 读取 + const stopNfcRead = () => { + try { + // #ifdef MP-WEIXIN + if (nfcAdapterRef.current && isReadingRef.current) { + nfcAdapterRef.current.stopDiscovery({ + success: () => { + console.log("NFC discovery stopped"); + isReadingRef.current = false; + }, + fail: (err: any) => { + console.error("Stop NFC failed:", err); + }, + }); + } + // #endif + } catch (error) { + console.error("Stop NFC error:", error); + } + }; + + // 页面显示时启动 NFC + useDidShow(() => { + setNfcData(""); + setStatus("等待靠近NFC标签..."); + setIsSupported(true); + startNfcRead(); + }); + + // 页面隐藏时停止 NFC + useDidHide(() => { + stopNfcRead(); + }); + + // 组件卸载时清理 + useEffect(() => { + return () => { + stopNfcRead(); + }; + }, []); + + return ( + + + {/* NFC 图标 */} + + + N + + + + {/* 状态提示 */} + {status} + + {/* NFC 读取结果 */} + {!isSupported ? null : ( + + NFC 内容 + {nfcData ? ( + {nfcData} + ) : ( + + 将设备靠近 NFC 标签即可读取内容 + + )} + + )} + + + ); +} diff --git a/src/pages/nfc-read/lib/app.ts b/src/pages/nfc-read/lib/app.ts new file mode 100644 index 0000000..43fa847 --- /dev/null +++ b/src/pages/nfc-read/lib/app.ts @@ -0,0 +1,94 @@ +export const nfcAppSchema = [ + +] + +// 微博 +// 主页/帖子/超话 + +// 小红书 +// 主页/笔记/视频 +// 搜索 + +// 抖音 +// 主页/视频/直播 +// 搜索 + +// 哔哩哔哩 +// 主页/视频/专栏/课程/番剧/影视 + +// 快手 +// 视频/主页 + +// 网易云音乐 +// 音乐/歌手/歌单/专辑/播客/播客合集/视频/笔记 +// 漫游模式 + +// QQ +// 加好友 + +// QQ音乐 +// 歌曲/歌单/专辑/歌手/视频 + +// 微信 +// 公众号/小程序/个人号/朋友圈/微信支付 + +// 酷狗 +// 歌曲/歌单/专辑/歌手/视频 + +// 汽水音乐 +// 歌曲/歌单/专辑/歌手/视频 + +// 全民K歌 +// 作品 + +// 波点音乐 +// 歌曲/歌单/专辑/歌手/视频 + +// Apple Music +// 歌曲 + +// 华为音乐 +// 歌曲 + +// 番茄畅听 +// 书籍/专栏/课程 + +// 美团 +// 美团外卖红包 +// 扫一扫 + +// 淘宝闪购 +// 支付宝红包 +// 扫一扫 + +// 高德地图 +// 位置 + +// KEEP + +// 原神 +// 自动打开 com.MiHoYo.YuanShen + +// 王者荣耀 +// 自动打开 com.tencent.tmgp.sgame + +// 和平精英 +// 自动打开 com.tencent.ig + +// 米游社 +// 自动打开 com.mihoyo.hyperion + +// 王者营地 +// 签到 smobagamehelper://web?url= + +// 腾讯视频 +// 视频/专栏/课程/番剧/影视 + +// 爱奇艺 +// 视频/专栏/课程/番剧/影视 + +// 优酷 +// 视频/专栏/课程/番剧/影视 + +// 芒果TV +// 视频/专栏/课程/番剧/影视 diff --git a/src/pages/nfc-read/lib/nfc.ts b/src/pages/nfc-read/lib/nfc.ts new file mode 100644 index 0000000..f17ff91 --- /dev/null +++ b/src/pages/nfc-read/lib/nfc.ts @@ -0,0 +1,331 @@ +// WiFi 认证类型 +const WIFI_AUTH_TYPES: Record = { + 0x0000: "Open", + 0x0001: "WPA-Personal", + 0x0002: "Shared", + 0x0003: "WPA-Auto-Personal", + 0x0004: "WPA2-Personal", + 0x0005: "WPA2-Auto-Personal", + 0x0006: "WPA-Enterprise", + 0x0007: "WPA2-Enterprise", + 0x0008: "WPA-Enterprise-V2", + 0x0009: "WPA2-Enterprise-V2", + 0x0010: "WPA3-Personal", + 0x0011: "WPA3-Enterprise", + 0x0012: "WPA3-Transition", +}; + +// WiFi 加密类型 +const WIFI_ENCRYPTION_TYPES: Record = { + 0x0000: "None", + 0x0001: "WEP", + 0x0002: "TKIP", + 0x0003: "AES", + 0x0004: "TKIP+AES", + 0x0005: "AES-CCM", + 0x0006: "WPA3-SAE", + 0x0007: "WPA3-Enterprise-192bit", +}; + +// WiFi TLV 类型 +const WIFI_TLV_TYPES: Record = { + 0x100E: "SSID", + 0x1027: "Password", + 0x1023: "AuthType", + 0x1025: "EncryptionType", + 0x100F: "MACAddress", + 0x1010: "VendorExtension", + 0x1011: "VendorSpecific", + 0x1020: "NetworkKeyIndex", + 0x1021: "Channel", +}; + +const parseWifiTlv = (buffer: Uint8Array): { type: number; length: number; value: Uint8Array } | null => { + if (buffer.length < 3) return null; + const type = (buffer[0] << 8) | buffer[1]; + const length = buffer[2]; + if (buffer.length < 3 + length) return null; + const value = buffer.slice(3, 3 + length); + return { type, length, value }; +}; + +const parseWifiAuthType = (value: Uint8Array): string => { + if (value.length < 2) return "Unknown"; + const authType = (value[0] << 8) | value[1]; + return WIFI_AUTH_TYPES[authType] || `Unknown (0x${authType.toString(16)})`; +}; + +const parseWifiEncryptionType = (value: Uint8Array): string => { + if (value.length < 2) return "Unknown"; + const encType = (value[0] << 8) | value[1]; + return WIFI_ENCRYPTION_TYPES[encType] || `Unknown (0x${encType.toString(16)})`; +}; + +const parseWifiPayload = (payload: Uint8Array): Record => { + const result: Record = {}; + let offset = 0; + + // WiFi payload 前面可能有 2 字节的前缀 (00 00 或 10 00 等) + if (payload.length > 2 && payload[0] === 0x10 && payload[1] === 0x00) { + offset = 2; + } + + while (offset < payload.length) { + const remaining = payload.slice(offset); + const tlv = parseWifiTlv(remaining); + if (!tlv) break; + + const typeName = WIFI_TLV_TYPES[tlv.type] || `Unknown(0x${tlv.type.toString(16)})`; + + switch (tlv.type) { + case 0x100E: // SSID + result.ssid = textDecode(tlv.value); + break; + case 0x1027: // Password + result.password = textDecode(tlv.value); + break; + case 0x1023: // AuthType + result.authType = parseWifiAuthType(tlv.value); + break; + case 0x1025: // EncryptionType + result.encryptionType = parseWifiEncryptionType(tlv.value); + break; + case 0x1021: // Channel + result.channel = tlv.value[0].toString(); + break; + case 0x100F: // MAC Address + result.macAddress = Array.from(tlv.value) + .map((b) => b.toString(16).padStart(2, "0").toUpperCase()) + .join(":"); + break; + default: + result[typeName] = arrayBufferToHex(tlv.value.buffer); + break; + } + + offset += 3 + tlv.length; + } + + return result; +}; + +// ArrayBuffer 转十六进制字符串 +export const arrayBufferToHex = (buffer: ArrayBufferLike): string => { + if (!buffer) return ""; + const bytes = new Uint8Array(buffer); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0").toUpperCase()) + .join(" "); +}; +// URL 协议前缀映射 (NDEF RTD 规范) +const URL_PREFIXES = [ + "", "http://www.", "https://www.", "http://", "https://", + "tel:", "mailto:", "ftp://anonymous:anonymous@", "ftp://ftp.", + "file://", "news:", "telnet://", "imap:", "rtsp://", + "urn:", "pop:", "sip:", "sips:", "tftp:", "btspp://", + "btl2cap://", "btgoep://", "tcpobex://", "irdaobex://", "file://" +]; + +const urlDecode = (buffer: Uint8Array): string => { + if (!buffer || buffer.length === 0) return ""; + const prefixCode = buffer[0]; + const prefix = URL_PREFIXES[prefixCode] || ""; + const urlBytes = buffer.slice(1); + const urlPart = textDecode(urlBytes); + return prefix + urlPart; +}; + +const textDecode = (buffer: Uint8Array): string => { + const encoded = Array.from(buffer).reduce((s, b) => s + "%" + b.toString(16).padStart(2, "0"), ""); + const text = decodeURIComponent(encoded); + return text; +} +// 将 ArrayBuffer 解析为字符串 +export const arrayBufferToString = (buffer: ArrayBufferLike): string => { + if (!buffer) return ""; + console.log("Parsing ArrayBuffer to string:", arrayBufferToHex(buffer)); + const bytes = new Uint8Array(buffer); + + // 检查是否是 02 65 6E 开头 + if (bytes.length >= 3 && bytes[0] === 0x02 && bytes[1] === 0x65 && bytes[2] === 0x6E) { + // 去掉前3个字节,解析后面的内容 + const textBytes = bytes.slice(3); + const text = textDecode(textBytes); + console.log("Parsed text (with 02 65 6E prefix):", text); + return text; + } + console.log("No 02 65 6E prefix found, parsing entire buffer as UTF-8 string."); + // 其他情况直接按 UTF-8 解析 + const text = textDecode(bytes); + console.log("Parsed text:", text); + return text; +}; + +export type NfcReadMessageRecordBuffer = { + id: ArrayBuffer; + payload: ArrayBuffer; + tnf: number; + type: ArrayBuffer; +}; +export type NfcReadResultBuffer = { + id: ArrayBuffer; + messages: { records: NfcReadMessageRecordBuffer[] }[]; + techs: string[]; +}; + +export type NfcText = { + id: string; + messages: { + records: { + id: string; + payload: string; + tnf: number; + type: string; + }[]; + }[]; + techs: string[]; +}; + +/** + * 解析NFC记录 + * @param record NFC记录数据 + * @returns 解析后的记录对象 + */ +export const parseNfcRecord = (record: NfcReadMessageRecordBuffer) => { + // 将type字节转为十六进制字符串用于比较 + const typeHex = arrayBufferToHex(record.type); + // 解析payload为Uint8Array + const payloadBytes = new Uint8Array(record.payload); + let payloadText = ""; + + switch (typeHex) { + case "00": + // 空记录 - 无payload + payloadText = "[空记录]"; + break; + + case "01": + // NFC论坛外部类型 - 通常是自定义格式 + payloadText = `[外部类型]: ${arrayBufferToHex(record.payload)}`; + break; + + case "53": + // 智能海报 - 包含URL、文本等嵌套记录 + payloadText = `[智能海报]: ${urlDecode(payloadBytes)}`; + break; + + case "54": + // 文本记录 - 常见类型 + payloadText = arrayBufferToString(record.payload); + break; + + case "55": + // URL记录 - 网页链接 + payloadText = urlDecode(payloadBytes); + break; + + case "56": + // VCard记录 - 联系人信息 + payloadText = parseVCard(payloadBytes); + break; + + case "57": + // WiFi配置记录 - 网络连接信息 + const wifiData = parseWifiPayload(payloadBytes); + payloadText = Object.entries(wifiData) + .map(([key, value]) => `${key}: ${value}`) + .join("\n"); + break; + + case "61": + // Android应用记录 - 包名或URL + payloadText = textDecode(payloadBytes); + break; + + case "4D": + // MIME媒体类型 - 可能包含图片、音频等 + const mimeType = textDecode(payloadBytes.slice(0, Math.min(50, payloadBytes.length))); + payloadText = `[MIME]: ${mimeType}\n[数据]: ${arrayBufferToHex(record.payload)}`; + break; + + case "44": + // 设备信息 - 设备厂商、型号等 + payloadText = parseDeviceInfo(payloadBytes); + break; + + default: + // 未知类型 - 转为十六进制显示 + payloadText = `[未知类型 ${typeHex}]: ${arrayBufferToHex(record.payload)}`; + break; + } + + return { + id: arrayBufferToHex(record.id), + payload: payloadText, + tnf: record.tnf, + type: typeHex, + }; +}; + +/** + * 解析VCard联系人信息 + * @param buffer VCard数据 + * @returns 格式化的联系人信息 + */ +const parseVCard = (buffer: Uint8Array): string => { + const text = textDecode(buffer); + // 提取常用字段 + const lines = text.split(/\r?\n/); + const result: string[] = []; + + for (const line of lines) { + if (line.startsWith("FN:") || line.startsWith("fn:")) { + result.push(`姓名: ${line.substring(3)}`); + } else if (line.startsWith("TEL:") || line.startsWith("tel:")) { + result.push(`电话: ${line.substring(4)}`); + } else if (line.startsWith("EMAIL:") || line.startsWith("email:")) { + result.push(`邮箱: ${line.substring(6)}`); + } else if (line.startsWith("ORG:") || line.startsWith("org:")) { + result.push(`公司: ${line.substring(4)}`); + } else if (line.startsWith("TITLE:") || line.startsWith("title:")) { + result.push(`职位: ${line.substring(6)}`); + } else if (line.startsWith("URL:") || line.startsWith("url:")) { + result.push(`网站: ${line.substring(4)}`); + } + } + + return result.length > 0 ? result.join("\n") : text; +}; + +/** + * 解析设备信息 + * @param buffer 设备信息数据 + * @returns 格式化的设备信息 + */ +const parseDeviceInfo = (buffer: Uint8Array): string => { + const text = textDecode(buffer); + const lines = text.split(/\r?\n/); + const result: string[] = []; + + for (const line of lines) { + const parts = line.split(":"); + if (line.startsWith("manufacturer:") || line.startsWith("Manufacturer:")) { + result.push(`厂商: ${parts[1] ? parts[1].trim() : ""}`); + } else if (line.startsWith("model:") || line.startsWith("Model:")) { + result.push(`型号: ${parts[1] ? parts[1].trim() : ""}`); + } else if (line.startsWith("serial:") || line.startsWith("Serial:")) { + result.push(`序列号: ${parts[1] ? parts[1].trim() : ""}`); + } + } + + return result.length > 0 ? result.join("\n") : `[设备信息]: ${text}`; +}; +export const parseNfcReadResult = (result: NfcReadResultBuffer): NfcText => { + return { + id: arrayBufferToHex(result.id), + messages: result.messages.map((msg) => ({ + records: msg.records.map((rec) => parseNfcRecord(rec)), + })), + techs: result.techs || [], + }; +}; \ No newline at end of file diff --git a/src/pages/nfc-read/readme.md b/src/pages/nfc-read/readme.md new file mode 100644 index 0000000..2bb818e --- /dev/null +++ b/src/pages/nfc-read/readme.md @@ -0,0 +1,3 @@ +# 注意事项 + +必须是 type:55 的标签才是网页跳转,如果是55,buffer里面为00的时候 \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1a4753b..99ecfeb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { "target": "es2017", - "module": "commonjs", + "module": "esnext", "removeComments": false, "preserveConstEnums": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "experimentalDecorators": true, "noImplicitAny": false, "allowSyntheticDefaultImports": true,