feat: enhance BottomNav component and update project configuration
This commit is contained in:
61
AGENTS.md
61
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
|
### app.config.ts
|
||||||
@@ -104,14 +58,6 @@ if (isXHS()) {
|
|||||||
|
|
||||||
应用入口组件,包含 `useLaunch` 生命周期钩子,用于应用初始化。在微信环境下会自动调用 `Taro.login`。
|
应用入口组件,包含 `useLaunch` 生命周期钩子,用于应用初始化。在微信环境下会自动调用 `Taro.login`。
|
||||||
|
|
||||||
### project.xhs.json
|
|
||||||
|
|
||||||
小红书 IDE 特定配置(appid、编译设置等)。
|
|
||||||
|
|
||||||
### tsconfig.json
|
|
||||||
|
|
||||||
TypeScript 编译器选项,包括路径别名配置(`@/*` → `./src/*`)。
|
|
||||||
|
|
||||||
## AI 代理注意事项
|
## AI 代理注意事项
|
||||||
|
|
||||||
1. 修改平台特定代码时,使用环境检测确保跨平台兼容性
|
1. 修改平台特定代码时,使用环境检测确保跨平台兼容性
|
||||||
@@ -119,3 +65,8 @@ TypeScript 编译器选项,包括路径别名配置(`@/*` → `./src/*`)
|
|||||||
3. 遵循现有的代码风格和目录结构
|
3. 遵循现有的代码风格和目录结构
|
||||||
4. 添加新依赖时,确保与所有目标平台兼容
|
4. 添加新依赖时,确保与所有目标平台兼容
|
||||||
5. 项目使用 pnpm 作为包管理器
|
5. 项目使用 pnpm 作为包管理器
|
||||||
|
|
||||||
|
## 避免
|
||||||
|
1. 不能使用 `?.` 和 `??` 操作符, 因为不支持
|
||||||
|
2. 不能使用 TextDecoder 和 TextEncoder, 因为不支持
|
||||||
|
3. 不能使用 Buffer, 因为不支持
|
||||||
@@ -6,5 +6,9 @@ export default {
|
|||||||
stats: true
|
stats: true
|
||||||
},
|
},
|
||||||
mini: {},
|
mini: {},
|
||||||
h5: {}
|
h5: {
|
||||||
|
devServer: {
|
||||||
|
host: '0.0.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
} satisfies UserConfigExport<'webpack5'>
|
} satisfies UserConfigExport<'webpack5'>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import prodConfig from "./prod";
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default defineConfig<"webpack5">(async (merge, { command, mode }) => {
|
export default defineConfig<"webpack5">(async (merge, { command, mode }) => {
|
||||||
const baseConfig: UserConfigExport<"webpack5"> = {
|
const baseConfig: UserConfigExport<"webpack5"> = {
|
||||||
projectName: "2025-09-14-webpack-demo",
|
projectName: "taro-template",
|
||||||
date: "2025-9-14",
|
date: "2026-03-12",
|
||||||
designWidth: 750,
|
designWidth: 750,
|
||||||
deviceRatio: {
|
deviceRatio: {
|
||||||
640: 2.34 / 2,
|
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
|
enable: false, // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
|
||||||
},
|
},
|
||||||
mini: {
|
mini: {
|
||||||
|
compiler: {
|
||||||
|
type: 'webpack5',
|
||||||
|
prebundle: {
|
||||||
|
enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
|
||||||
|
},
|
||||||
|
},
|
||||||
postcss: {
|
postcss: {
|
||||||
pxtransform: {
|
pxtransform: {
|
||||||
enable: true,
|
enable: true,
|
||||||
@@ -51,6 +57,7 @@ export default defineConfig<"webpack5">(async (merge, { command, mode }) => {
|
|||||||
h5: {
|
h5: {
|
||||||
publicPath: "/",
|
publicPath: "/",
|
||||||
staticDirectory: "static",
|
staticDirectory: "static",
|
||||||
|
esnextModules: ["@stencil/core"],
|
||||||
output: {
|
output: {
|
||||||
filename: "js/[name].[hash:8].js",
|
filename: "js/[name].[hash:8].js",
|
||||||
chunkFilename: "js/[name].[chunkhash:8].js",
|
chunkFilename: "js/[name].[chunkhash:8].js",
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.6",
|
"@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",
|
"@nutui/nutui-react-taro": "^2.7.15",
|
||||||
"@tarojs/components": "4.1.11",
|
"@tarojs/components": "4.1.11",
|
||||||
"@tarojs/helper": "4.1.11",
|
"@tarojs/helper": "4.1.11",
|
||||||
|
|||||||
145
pnpm-lock.yaml
generated
145
pnpm-lock.yaml
generated
@@ -11,6 +11,12 @@ importers:
|
|||||||
'@babel/runtime':
|
'@babel/runtime':
|
||||||
specifier: ^7.28.6
|
specifier: ^7.28.6
|
||||||
version: 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':
|
'@nutui/icons-react-taro':
|
||||||
specifier: 3.0.2-cpp.3.beta.5
|
specifier: 3.0.2-cpp.3.beta.5
|
||||||
version: 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':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
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':
|
'@leichtgewicht/ip-codec@2.0.5':
|
||||||
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
||||||
|
|
||||||
'@napi-rs/triples@1.2.0':
|
'@napi-rs/triples@1.2.0':
|
||||||
resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==}
|
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':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1178,6 +1203,10 @@ packages:
|
|||||||
'@nutui/touch-emulator@1.0.0':
|
'@nutui/touch-emulator@1.0.0':
|
||||||
resolution: {integrity: sha512-k2hvI/9LlRA7Ph1Chni27pTuvPmKPt+/I10sWWd2sWzqiCOYRerD79eIwCMRGUF/q6WVDEKVnv00t9CEUL4sPA==}
|
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':
|
'@parcel/watcher-android-arm64@2.5.6':
|
||||||
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
|
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
@@ -2218,6 +2247,9 @@ packages:
|
|||||||
big.js@5.2.2:
|
big.js@5.2.2:
|
||||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||||
|
|
||||||
|
bignumber.js@9.3.1:
|
||||||
|
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||||
|
|
||||||
binary-extensions@2.3.0:
|
binary-extensions@2.3.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2869,6 +2901,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
|
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
error-causes@3.0.2:
|
||||||
|
resolution: {integrity: sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==}
|
||||||
|
|
||||||
error-ex@1.3.4:
|
error-ex@1.3.4:
|
||||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||||
|
|
||||||
@@ -2897,6 +2932,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-toolkit@1.45.1:
|
||||||
|
resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==}
|
||||||
|
|
||||||
esbuild-loader@4.4.2:
|
esbuild-loader@4.4.2:
|
||||||
resolution: {integrity: sha512-8LdoT9sC7fzfvhxhsIAiWhzLJr9yT3ggmckXxsgvM07wgrRxhuT98XhLn3E7VczU5W5AFsPKv9DdWcZIubbWkQ==}
|
resolution: {integrity: sha512-8LdoT9sC7fzfvhxhsIAiWhzLJr9yT3ggmckXxsgvM07wgrRxhuT98XhLn3E7VczU5W5AFsPKv9DdWcZIubbWkQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2976,6 +3014,9 @@ packages:
|
|||||||
eventemitter3@4.0.7:
|
eventemitter3@4.0.7:
|
||||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||||
|
|
||||||
|
eventemitter3@5.0.4:
|
||||||
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
events@3.3.0:
|
events@3.3.0:
|
||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
@@ -3159,6 +3200,10 @@ packages:
|
|||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
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:
|
gensync@1.0.0-beta.2:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -4058,6 +4103,11 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
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:
|
native-request@1.1.2:
|
||||||
resolution: {integrity: sha512-/etjwrK0J4Ebbcnt35VMWnfiUX/B04uwGJxyJInagxDqf2z5drSt/lsOvEMWGYunz1kaLZAFrV4NDAbOoDKvAQ==}
|
resolution: {integrity: sha512-/etjwrK0J4Ebbcnt35VMWnfiUX/B04uwGJxyJInagxDqf2z5drSt/lsOvEMWGYunz1kaLZAFrV4NDAbOoDKvAQ==}
|
||||||
|
|
||||||
@@ -4251,6 +4301,9 @@ packages:
|
|||||||
pascal-case@3.1.2:
|
pascal-case@3.1.2:
|
||||||
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
|
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
|
||||||
|
|
||||||
|
path-browserify-esm@1.0.6:
|
||||||
|
resolution: {integrity: sha512-9nUwYvvu/yq1PYrUyYCihNWmpzacaRYF6gGbjLWErrZ4MRDWyfPN7RpE8E7tsw8eqBU/rr7mcoTXbS+Vih8uUA==}
|
||||||
|
|
||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
@@ -5019,6 +5072,12 @@ packages:
|
|||||||
solid-js@1.9.11:
|
solid-js@1.9.11:
|
||||||
resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==}
|
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:
|
sort-keys-length@1.0.1:
|
||||||
resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
|
resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -5053,6 +5112,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
|
spark-md5@3.0.2:
|
||||||
|
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||||
|
|
||||||
spdy-transport@3.0.0:
|
spdy-transport@3.0.0:
|
||||||
resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==}
|
resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==}
|
||||||
|
|
||||||
@@ -5622,6 +5684,24 @@ packages:
|
|||||||
yup@1.7.1:
|
yup@1.7.1:
|
||||||
resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==}
|
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:
|
snapshots:
|
||||||
|
|
||||||
'@adobe/css-tools@4.3.3': {}
|
'@adobe/css-tools@4.3.3': {}
|
||||||
@@ -6713,10 +6793,43 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@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': {}
|
'@leichtgewicht/ip-codec@2.0.5': {}
|
||||||
|
|
||||||
'@napi-rs/triples@1.2.0': {}
|
'@napi-rs/triples@1.2.0': {}
|
||||||
|
|
||||||
|
'@noble/hashes@2.0.1': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@@ -6754,6 +6867,12 @@ snapshots:
|
|||||||
|
|
||||||
'@nutui/touch-emulator@1.0.0': {}
|
'@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':
|
'@parcel/watcher-android-arm64@2.5.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -7954,6 +8073,8 @@ snapshots:
|
|||||||
|
|
||||||
big.js@5.2.2: {}
|
big.js@5.2.2: {}
|
||||||
|
|
||||||
|
bignumber.js@9.3.1: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
bl@1.2.3:
|
bl@1.2.3:
|
||||||
@@ -8690,6 +8811,8 @@ snapshots:
|
|||||||
prr: 1.0.1
|
prr: 1.0.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
error-causes@3.0.2: {}
|
||||||
|
|
||||||
error-ex@1.3.4:
|
error-ex@1.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish: 0.2.1
|
is-arrayish: 0.2.1
|
||||||
@@ -8717,6 +8840,8 @@ snapshots:
|
|||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
hasown: 2.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)):
|
esbuild-loader@4.4.2(webpack@5.105.4(@swc/core@1.3.96)):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.3
|
esbuild: 0.27.3
|
||||||
@@ -8868,6 +8993,8 @@ snapshots:
|
|||||||
|
|
||||||
eventemitter3@4.0.7: {}
|
eventemitter3@4.0.7: {}
|
||||||
|
|
||||||
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
execa@5.1.1:
|
execa@5.1.1:
|
||||||
@@ -9083,6 +9210,8 @@ snapshots:
|
|||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
|
fuse.js@7.1.0: {}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
@@ -9974,6 +10103,8 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
nanoid@5.1.6: {}
|
||||||
|
|
||||||
native-request@1.1.2:
|
native-request@1.1.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -10171,6 +10302,8 @@ snapshots:
|
|||||||
no-case: 3.0.4
|
no-case: 3.0.4
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
path-browserify-esm@1.0.6: {}
|
||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
path-case@3.0.4:
|
path-case@3.0.4:
|
||||||
@@ -10933,6 +11066,11 @@ snapshots:
|
|||||||
seroval: 1.5.1
|
seroval: 1.5.1
|
||||||
seroval-plugins: 1.5.1(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:
|
sort-keys-length@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
sort-keys: 1.1.2
|
sort-keys: 1.1.2
|
||||||
@@ -10960,6 +11098,8 @@ snapshots:
|
|||||||
|
|
||||||
source-map@0.7.6: {}
|
source-map@0.7.6: {}
|
||||||
|
|
||||||
|
spark-md5@3.0.2: {}
|
||||||
|
|
||||||
spdy-transport@3.0.0:
|
spdy-transport@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -11548,3 +11688,8 @@ snapshots:
|
|||||||
tiny-case: 1.0.3
|
tiny-case: 1.0.3
|
||||||
toposort: 2.0.2
|
toposort: 2.0.2
|
||||||
type-fest: 2.19.0
|
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
|
||||||
|
|||||||
@@ -1,15 +1,39 @@
|
|||||||
{
|
{
|
||||||
"miniprogramRoot": "./dist",
|
"miniprogramRoot": "dist/",
|
||||||
"projectname": "2026-03-12-taro-template",
|
"projectname": "taro-template",
|
||||||
"description": "taro-template",
|
"description": "taro-template",
|
||||||
"appid": "touristappid",
|
"appid": "wx5464d820d8c2e4ad",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": true,
|
"urlCheck": true,
|
||||||
"es6": false,
|
"es6": false,
|
||||||
"enhance": false,
|
"enhance": false,
|
||||||
"compileHotReLoad": false,
|
"compileHotReLoad": false,
|
||||||
"postcss": false,
|
"postcss": false,
|
||||||
"minified": false
|
"minified": false,
|
||||||
},
|
"compileWorklet": false,
|
||||||
"compileType": "miniprogram"
|
"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": {}
|
||||||
}
|
}
|
||||||
0
public/update.json
Normal file
0
public/update.json
Normal file
@@ -1,6 +1,7 @@
|
|||||||
export default defineAppConfig({
|
export default defineAppConfig({
|
||||||
pages: [
|
pages: [
|
||||||
'pages/index/index',
|
'pages/index/index',
|
||||||
|
'pages/nfc-read/index',
|
||||||
'pages/mine/index'
|
'pages/mine/index'
|
||||||
],
|
],
|
||||||
window: {
|
window: {
|
||||||
|
|||||||
@@ -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 {
|
.bottom-nav {
|
||||||
position: fixed;
|
flex-shrink: 0;
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 100px;
|
height: 100px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-top: 1px solid #e8e8e8;
|
border-top: 1px solid #e8e8e8;
|
||||||
@@ -17,12 +28,12 @@
|
|||||||
|
|
||||||
.bottom-nav-item {
|
.bottom-nav-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { View, Text } from "@tarojs/components";
|
import { View, Text } from "@tarojs/components";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import type { NavItem, NavKey } from "../../config";
|
import type { NavItem, NavKey } from "../../config";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
@@ -10,33 +11,35 @@ const ICON_SIZE = 24;
|
|||||||
interface BottomNavProps {
|
interface BottomNavProps {
|
||||||
active: NavKey;
|
active: NavKey;
|
||||||
navItems: NavItem[];
|
navItems: NavItem[];
|
||||||
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BottomNav({ active, navItems }: BottomNavProps) {
|
export default function BottomNav({ active, navItems, children }: BottomNavProps) {
|
||||||
const handleNavigate = (item: NavItem) => {
|
const handleNavigate = (item: NavItem) => {
|
||||||
if (item.key === active) return;
|
if (item.key === active) return;
|
||||||
Taro.redirectTo({ url: item.path });
|
Taro.redirectTo({ url: item.path });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<View className="bottom-nav-wrapper">
|
||||||
|
<View className="bottom-nav-content">{children}</View>
|
||||||
<View className="bottom-nav">
|
<View className="bottom-nav">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const isActive = active === item.key;
|
const isActive = active === item.key;
|
||||||
const IconComp = item.icon;
|
const IconComp = item.icon;
|
||||||
|
const iconColor = isActive ? ACTIVE_COLOR : DEFAULT_COLOR;
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={item.key}
|
key={item.key}
|
||||||
className={`bottom-nav-item${isActive ? " active" : ""}`}
|
className={`bottom-nav-item${isActive ? " active" : ""}`}
|
||||||
onClick={() => handleNavigate(item)}
|
onClick={() => handleNavigate(item)}
|
||||||
>
|
>
|
||||||
<IconComp
|
<IconComp size={ICON_SIZE} color={iconColor} />
|
||||||
size={ICON_SIZE}
|
|
||||||
color={isActive ? ACTIVE_COLOR : DEFAULT_COLOR}
|
|
||||||
/>
|
|
||||||
<Text className="bottom-nav-label">{item.label}</Text>
|
<Text className="bottom-nav-label">{item.label}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/components/Icons/index.tsx
Normal file
74
src/components/Icons/index.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { Image } from "@tarojs/components";
|
||||||
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
|
// SVG path 与颜色的映射
|
||||||
|
const svgPaths: Record<string, string> = {
|
||||||
|
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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="${color || "currentColor"}"><path d="${path}"/></svg>`;
|
||||||
|
const dataUrl = `data:image/svg+xml;base64,${toBase64(svgString)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={dataUrl}
|
||||||
|
mode="aspectFit"
|
||||||
|
style={{
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home 图标
|
||||||
|
export function IconHome(props: { size?: number; color?: string; style?: CSSProperties }) {
|
||||||
|
return <SvgIcon name="home" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User 图标
|
||||||
|
export function IconUser(props: { size?: number; color?: string; style?: CSSProperties }) {
|
||||||
|
return <SvgIcon name="user" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NFC 图标
|
||||||
|
export function IconNfc(props: { size?: number; color?: string; style?: CSSProperties }) {
|
||||||
|
return <SvgIcon name="nfc" {...props} />;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Home, User } from "@nutui/icons-react-taro";
|
import { IconHome, IconUser, IconNfc } from "./components/Icons";
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
|
|
||||||
export type NavKey = string;
|
export type NavKey = string;
|
||||||
@@ -7,7 +7,7 @@ export interface NavItem {
|
|||||||
key: NavKey;
|
key: NavKey;
|
||||||
label: string;
|
label: string;
|
||||||
path: string;
|
path: string;
|
||||||
icon: ComponentType<{ size?: string | number; color?: string }>;
|
icon: ComponentType<{ size?: number; color?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const navItems: NavItem[] = [
|
export const navItems: NavItem[] = [
|
||||||
@@ -15,12 +15,18 @@ export const navItems: NavItem[] = [
|
|||||||
key: "index",
|
key: "index",
|
||||||
label: "主页",
|
label: "主页",
|
||||||
path: "/pages/index/index",
|
path: "/pages/index/index",
|
||||||
icon: Home,
|
icon: IconHome,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "nfc-read",
|
||||||
|
label: "NFC",
|
||||||
|
path: "/pages/nfc-read/index",
|
||||||
|
icon: IconNfc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "mine",
|
key: "mine",
|
||||||
label: "我的",
|
label: "我的",
|
||||||
path: "/pages/mine/index",
|
path: "/pages/mine/index",
|
||||||
icon: User,
|
icon: IconUser,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
10
src/modules/taro-request.ts
Normal file
10
src/modules/taro-request.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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 {
|
.index-content {
|
||||||
flex: 1;
|
height: 100%;
|
||||||
overflow-y: auto;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 80px;
|
padding-top: 80px;
|
||||||
padding-bottom: 100px;
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function Index() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="index">
|
<BottomNav active="index" navItems={navItems}>
|
||||||
<View className="index-content">
|
<View className="index-content">
|
||||||
<Text>Hello world!</Text>
|
<Text>Hello world!</Text>
|
||||||
<Button
|
<Button
|
||||||
@@ -22,7 +22,6 @@ export default function Index() {
|
|||||||
User Info
|
User Info
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<BottomNav active="index" navItems={navItems} />
|
</BottomNav>
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: "我的",
|
navigationBarTitleText: "我的",
|
||||||
|
navigationStyle: "custom",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +1,59 @@
|
|||||||
.mine-page {
|
.mine-content {
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
background: #f5f5f5;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
padding: 30px 32px;
|
||||||
|
padding-top: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-content {
|
.mine-profile-row {
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 80px;
|
gap: 24px;
|
||||||
padding-bottom: 100px;
|
background: #fff;
|
||||||
gap: 20px;
|
border-radius: 16px;
|
||||||
|
padding: 24px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-avatar-btn {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-avatar-btn::after {
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-avatar {
|
.mine-avatar {
|
||||||
width: 120px;
|
width: 100px;
|
||||||
height: 120px;
|
height: 100px;
|
||||||
border-radius: 60px;
|
border-radius: 50px;
|
||||||
background: #1976d2;
|
}
|
||||||
|
|
||||||
|
.mine-avatar-placeholder {
|
||||||
|
background: #e0e0e0;
|
||||||
|
display: block;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-nickname-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-name {
|
.mine-name {
|
||||||
|
|||||||
@@ -1,16 +1,68 @@
|
|||||||
import { View, Text } from "@tarojs/components";
|
import { View, Button, Image, Input } from "@tarojs/components";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import Taro from "@tarojs/taro";
|
||||||
import BottomNav from "../../components/BottomNav";
|
import BottomNav from "../../components/BottomNav";
|
||||||
import { navItems } from "../../config";
|
import { navItems } from "../../config";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
|
const STORAGE_KEY_AVATAR = "mine_avatarUrl";
|
||||||
|
const STORAGE_KEY_NICKNAME = "mine_nickName";
|
||||||
|
|
||||||
export default function Mine() {
|
export default function Mine() {
|
||||||
|
const [avatarUrl, setAvatarUrl] = useState("");
|
||||||
|
const [nickName, setNickName] = useState("");
|
||||||
|
|
||||||
|
// 初始化时从缓存读取
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const cachedAvatar = Taro.getStorageSync(STORAGE_KEY_AVATAR);
|
||||||
|
const cachedNick = Taro.getStorageSync(STORAGE_KEY_NICKNAME);
|
||||||
|
if (cachedAvatar) setAvatarUrl(cachedAvatar);
|
||||||
|
if (cachedNick) setNickName(cachedNick);
|
||||||
|
} catch (e) {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChooseAvatar = (e: any) => {
|
||||||
|
const url = e && e.detail && e.detail.avatarUrl;
|
||||||
|
if (url) {
|
||||||
|
setAvatarUrl(url);
|
||||||
|
try { Taro.setStorageSync(STORAGE_KEY_AVATAR, url); } catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNickNameBlur = (e: any) => {
|
||||||
|
const val = (e && e.detail && e.detail.value) || "";
|
||||||
|
if (val) {
|
||||||
|
setNickName(val);
|
||||||
|
try { Taro.setStorageSync(STORAGE_KEY_NICKNAME, val); } catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="mine-page">
|
<BottomNav active="mine" navItems={navItems}>
|
||||||
<View className="mine-content">
|
<View className="mine-content">
|
||||||
<View className="mine-avatar" />
|
<View className="mine-profile-row">
|
||||||
<Text className="mine-name">用户昵称</Text>
|
{/* 头像选择按钮 */}
|
||||||
|
<Button
|
||||||
|
className="mine-avatar-btn"
|
||||||
|
openType="chooseAvatar"
|
||||||
|
onChooseAvatar={handleChooseAvatar}
|
||||||
|
>
|
||||||
|
{avatarUrl ? (
|
||||||
|
<Image className="mine-avatar" src={avatarUrl} />
|
||||||
|
) : (
|
||||||
|
<View className="mine-avatar mine-avatar-placeholder" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{/* 昵称输入框,type="nickname" 可唤起微信昵称键盘,用 onBlur 接收昵称选择结果 */}
|
||||||
|
<Input
|
||||||
|
className="mine-nickname-input"
|
||||||
|
type="nickname"
|
||||||
|
placeholder={nickName || "点击设置昵称"}
|
||||||
|
onBlur={handleNickNameBlur}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<BottomNav active="mine" navItems={navItems} />
|
|
||||||
</View>
|
</View>
|
||||||
|
</BottomNav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/pages/nfc-read/index.config.ts
Normal file
5
src/pages/nfc-read/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: "NFC 读取",
|
||||||
|
enablePullDownRefresh: false,
|
||||||
|
navigationStyle: "custom",
|
||||||
|
});
|
||||||
56
src/pages/nfc-read/index.css
Normal file
56
src/pages/nfc-read/index.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
155
src/pages/nfc-read/index.tsx
Normal file
155
src/pages/nfc-read/index.tsx
Normal file
@@ -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<string>("");
|
||||||
|
const [status, setStatus] = useState<string>("等待靠近NFC标签...");
|
||||||
|
const [isSupported, setIsSupported] = useState<boolean>(true);
|
||||||
|
const nfcAdapterRef = useRef<Taro.NFCAdapter | null>(null);
|
||||||
|
const isReadingRef = useRef<boolean>(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 (
|
||||||
|
<BottomNav active="nfc-read" navItems={navItems}>
|
||||||
|
<View className="nfc-container">
|
||||||
|
{/* NFC 图标 */}
|
||||||
|
<View className="nfc-icon">
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: isSupported ? "#1890ff" : "#999",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: "48px", color: "#fff" }}>N</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 状态提示 */}
|
||||||
|
<Text className="nfc-status">{status}</Text>
|
||||||
|
|
||||||
|
{/* NFC 读取结果 */}
|
||||||
|
{!isSupported ? null : (
|
||||||
|
<View className="nfc-result">
|
||||||
|
<Text className="nfc-result-label">NFC 内容</Text>
|
||||||
|
{nfcData ? (
|
||||||
|
<Text className="nfc-result-content">{nfcData}</Text>
|
||||||
|
) : (
|
||||||
|
<Text className="nfc-placeholder">
|
||||||
|
将设备靠近 NFC 标签即可读取内容
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</BottomNav>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
src/pages/nfc-read/lib/app.ts
Normal file
94
src/pages/nfc-read/lib/app.ts
Normal file
@@ -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
|
||||||
|
// 视频/专栏/课程/番剧/影视
|
||||||
331
src/pages/nfc-read/lib/nfc.ts
Normal file
331
src/pages/nfc-read/lib/nfc.ts
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
// WiFi 认证类型
|
||||||
|
const WIFI_AUTH_TYPES: Record<number, string> = {
|
||||||
|
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<number, string> = {
|
||||||
|
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<number, string> = {
|
||||||
|
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<string, string> => {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
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 || [],
|
||||||
|
};
|
||||||
|
};
|
||||||
3
src/pages/nfc-read/readme.md
Normal file
3
src/pages/nfc-read/readme.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 注意事项
|
||||||
|
|
||||||
|
必须是 type:55 的标签才是网页跳转,如果是55,buffer里面为00的时候
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"module": "commonjs",
|
"module": "esnext",
|
||||||
"removeComments": false,
|
"removeComments": false,
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user