在Windows系统上构建
This commit is contained in:
parent
22ae3ac755
commit
0652323b85
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ node_modules/
|
||||
dist/
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
tests/fixtures/sample-qr.png
|
||||
|
||||
36
doc/构建.md
36
doc/构建.md
@ -69,6 +69,7 @@ npm run tauri android build
|
||||
产物路径:
|
||||
```
|
||||
src-tauri/gen/android/app/build/outputs/apk/
|
||||
|
||||
```
|
||||
|
||||
## 6. 生成签名密钥(release 必需)
|
||||
@ -111,3 +112,38 @@ adb install -r <上面生成的 apk 路径>
|
||||
- 图标缺失导致 `generate_context!` 失败:确认 `src-tauri/icons/icon.png` 存在,可用 `npm run tauri icon <源图>` 生成全套。
|
||||
- NDK 找不到:检查 `NDK_HOME` 是否指向真实存在的版本目录。
|
||||
|
||||
|
||||
|
||||
## 9. Windows 本地工具链(通过 mise 安装)
|
||||
|
||||
本仓库根目录的 `mise.toml` 固定了 Android 构建所需的工具链,均通过 [mise](https://mise.jdx.dev) 管理。
|
||||
|
||||
安装步骤:
|
||||
```powershell
|
||||
mise install # 安装 java(temurin-17) + rust(1.89)
|
||||
mise trust # 首次需信任本目录的 mise.toml 以启用环境变量
|
||||
```
|
||||
|
||||
工具链清单:
|
||||
- JDK 17(`java = "temurin-17"`),Android/Gradle 需要 17+。
|
||||
- Rust 1.89(`rust = "1.89"`),并已 `rustup target add` 四个 Android 目标
|
||||
(aarch64/armv7/i686/x86_64-linux-android)。
|
||||
- Android SDK 安装在 `D:\android-sdk`(平台 android-34、build-tools 34.0.0、
|
||||
platform-tools、NDK 27.2.12479018、emulator + system-images;android-34;google_apis;x86_64)。
|
||||
通过 `mise.toml` 的 `[env]` 注入 `ANDROID_HOME` / `NDK_HOME` 及 PATH。
|
||||
- 模拟器 AVD:`tauri_test`(Pixel 6,Android 14)。启动:`emulator -avd tauri_test`。
|
||||
|
||||
> 说明:Windows 上 mise 的 vfox android-sdk 插件校验会失败,且 C 盘空间紧张,
|
||||
> 因此 SDK 手动安装到 D 盘并用环境变量接入;JDK 因 GitHub 发布 CDN 在本网络不可达,
|
||||
> 改用清华镜像下载后由 mise 离线安装(`MISE_PREFER_OFFLINE=1`)。
|
||||
|
||||
### 已知问题:360 安全卫士拦截构建
|
||||
本机安装了 **360 安全卫士**(`ZhuDongFangYu` 主动防御)。它会拦截 cargo 新编译出的
|
||||
build-script 可执行文件的运行,导致 `cargo build` 报 `拒绝访问 (os error 5)` 或卡死。
|
||||
构建 Android 前需临时退出 360 主动防御,或将本仓库与 `~/.cargo`、`~/.rustup` 加入信任白名单。
|
||||
|
||||
### 前端测试
|
||||
```powershell
|
||||
npm test # 含 autofill 与 qrscan(二维码解码)用例
|
||||
node tests/fixtures/generate-qr.mjs # 生成一张测试二维码图片 tests/fixtures/sample-qr.png
|
||||
```
|
||||
|
||||
16
mise.toml
Normal file
16
mise.toml
Normal file
@ -0,0 +1,16 @@
|
||||
# Tauri Android 工具链(通过 mise 管理)
|
||||
[tools]
|
||||
java = "temurin-17"
|
||||
rust = "1.89"
|
||||
|
||||
# Android SDK 手动安装到 D:\android-sdk(Windows 上 vfox 插件校验失败,
|
||||
# 且 C 盘空间紧张),此处通过环境变量接入 mise。
|
||||
[env]
|
||||
ANDROID_HOME = "D:\\android-sdk"
|
||||
ANDROID_SDK_ROOT = "D:\\android-sdk"
|
||||
NDK_HOME = "D:\\android-sdk\\ndk\\27.2.12479018"
|
||||
_.path = [
|
||||
"D:\\android-sdk\\platform-tools",
|
||||
"D:\\android-sdk\\emulator",
|
||||
"D:\\android-sdk\\cmdline-tools\\latest\\bin",
|
||||
]
|
||||
342
package-lock.json
generated
342
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-barcode-scanner": "^2.0.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
@ -17,6 +18,7 @@
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"vite": "^5.2.0",
|
||||
"vitest": "^1.6.0"
|
||||
}
|
||||
@ -1460,6 +1462,16 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
@ -1514,6 +1526,16 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
|
||||
@ -1546,6 +1568,38 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@ -1640,6 +1694,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
@ -1680,6 +1744,13 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@ -1695,6 +1766,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
@ -1830,6 +1908,20 @@
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
@ -1872,6 +1964,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-func-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||
@ -2053,6 +2155,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
@ -2128,6 +2240,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsqr": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/jsqr/-/jsqr-1.4.0.tgz",
|
||||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/local-pkg": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
|
||||
@ -2145,6 +2263,19 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
|
||||
@ -2337,6 +2468,45 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
@ -2350,6 +2520,16 @@
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@ -2402,6 +2582,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
@ -2468,6 +2658,24 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
@ -2482,6 +2690,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
@ -2561,6 +2786,13 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@ -2627,6 +2859,34 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||
@ -3016,6 +3276,13 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
@ -3033,6 +3300,37 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
@ -3072,6 +3370,50 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-barcode-scanner": "^2.0.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
@ -20,6 +21,7 @@
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"vite": "^5.2.0",
|
||||
"vitest": "^1.6.0"
|
||||
}
|
||||
|
||||
2
src-tauri/.cargo/config.toml
Normal file
2
src-tauri/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64/link.exe"
|
||||
76
src/qrscan.js
Normal file
76
src/qrscan.js
Normal file
@ -0,0 +1,76 @@
|
||||
// 二维码解码核心逻辑:与 UI、相机、Tauri 解耦,便于单元测试。
|
||||
// 相机扫码走原生插件,图片扫码走纯 JS 的 jsQR,桌面端也可用。
|
||||
import jsQR from "jsqr";
|
||||
|
||||
/**
|
||||
* 校验扫码内容是否为有效网址(仅接受 http/https)。
|
||||
* @param {unknown} content 扫码得到的原始内容。
|
||||
* @returns {string} 合法网址(去除首尾空白);非法时返回空字符串。
|
||||
*/
|
||||
export function extractUrl(content) {
|
||||
if (typeof content !== "string") return "";
|
||||
const text = content.trim();
|
||||
return /^https?:\/\//i.test(text) ? text : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从像素数据中解码二维码。
|
||||
* @param {{data: Uint8ClampedArray, width: number, height: number}} imageData
|
||||
* 画布像素数据(RGBA)。
|
||||
* @returns {string|null} 解码到的文本;未识别到返回 null。
|
||||
*/
|
||||
export function decodeQrFromImageData(imageData) {
|
||||
if (!imageData || !imageData.data || !imageData.width || !imageData.height) {
|
||||
return null;
|
||||
}
|
||||
// attemptBoth 允许识别反色(浅底深码或深底浅码)二维码,提升清晰码的识别率。
|
||||
const result = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: "attemptBoth",
|
||||
});
|
||||
return result ? result.data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片元素绘制到离屏画布并取出像素数据(仅浏览器环境可用)。
|
||||
* @param {CanvasImageSource & {width:number,height:number}} image 已加载的图片源。
|
||||
* @param {number} [maxSize=1600] 长边最大像素,过大图片等比缩小以兼顾速度与精度。
|
||||
* @returns {ImageData} 画布像素数据。
|
||||
*/
|
||||
export function imageToImageData(image, maxSize = 1600) {
|
||||
const sw = image.naturalWidth || image.width;
|
||||
const sh = image.naturalHeight || image.height;
|
||||
const scale = Math.min(1, maxSize / Math.max(sw, sh));
|
||||
const w = Math.max(1, Math.round(sw * scale));
|
||||
const h = Math.max(1, Math.round(sh * scale));
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(image, 0, 0, w, h);
|
||||
return ctx.getImageData(0, 0, w, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件(用户选择的图片)解码二维码(仅浏览器环境可用)。
|
||||
* @param {Blob} file 图片文件。
|
||||
* @returns {Promise<string|null>} 解码文本;未识别到返回 null。
|
||||
*/
|
||||
export function decodeQrFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
try {
|
||||
resolve(decodeQrFromImageData(imageToImageData(img)));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error("图片加载失败"));
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup>
|
||||
import { ref, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
@ -9,16 +9,19 @@ import {
|
||||
openAppSettings,
|
||||
Format,
|
||||
} from "@tauri-apps/plugin-barcode-scanner";
|
||||
import { extractUrl, decodeQrFromFile } from "../qrscan.js";
|
||||
|
||||
// 扫码页:调用相机扫描二维码,得到 URL 后跳转 h5 表单页。
|
||||
// Android 上相机预览渲染在 WebView 下层,扫码期间需将页面背景透明化才能看到画面。
|
||||
// 扫码页:既支持相机扫码(原生插件,仅移动端),也支持从图片扫码(纯 JS,全平台)。
|
||||
// Android 上相机预览渲染在 WebView 下层,扫码期间需把页面背景透明化才能看到画面。
|
||||
const router = useRouter();
|
||||
const error = ref("");
|
||||
const denied = ref(false);
|
||||
const scanning = ref(false);
|
||||
const decoding = ref(false);
|
||||
const fileInput = ref(null);
|
||||
|
||||
/**
|
||||
* 将任意异常转换为可读字符串,避免出现 [object Object]。
|
||||
* 将任意异常转为可读字符串,避免出现 [object Object]。
|
||||
* @param {unknown} e 捕获到的异常。
|
||||
* @returns {string} 可读的错误描述。
|
||||
*/
|
||||
@ -34,6 +37,7 @@ function toMessage(e) {
|
||||
|
||||
/**
|
||||
* 切换扫码透明态:透明时可看到 WebView 下层的相机预览。
|
||||
* 同时作用于 html/body 与 #app 挂载点,避免残留不透明背景挡住画面。
|
||||
* @param {boolean} on 是否进入透明态。
|
||||
* @returns {void}
|
||||
*/
|
||||
@ -41,6 +45,23 @@ function setTransparent(on) {
|
||||
const val = on ? "transparent" : "";
|
||||
document.documentElement.style.background = val;
|
||||
document.body.style.background = val;
|
||||
const app = document.getElementById("app");
|
||||
if (app) app.style.background = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命中网址后跳转到 WebForm 页;非网址则提示。
|
||||
* @param {string} content 扫码/解码得到的原始文本。
|
||||
* @returns {boolean} 是否为有效网址并已跳转。
|
||||
*/
|
||||
function handleContent(content) {
|
||||
const url = extractUrl(content);
|
||||
if (url) {
|
||||
router.push({ path: "/web", query: { url } });
|
||||
return true;
|
||||
}
|
||||
error.value = "扫描内容不是有效网址:" + content;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,7 +77,7 @@ async function ensurePermission() {
|
||||
return state === "granted";
|
||||
}
|
||||
|
||||
/** 启动扫码,成功后携带 URL 跳转到 WebForm 页。@returns {Promise<void>} */
|
||||
/** 启动相机扫码,成功后携带 URL 跳转到 WebForm 页。@returns {Promise<void>} */
|
||||
async function startScan() {
|
||||
error.value = "";
|
||||
if (scanning.value) return;
|
||||
@ -68,13 +89,12 @@ async function startScan() {
|
||||
}
|
||||
scanning.value = true;
|
||||
setTransparent(true);
|
||||
const res = await scan({ windowed: true, formats: [Format.QRCode] });
|
||||
const url = res.content;
|
||||
if (/^https?:\/\//.test(url)) {
|
||||
router.push({ path: "/web", query: { url } });
|
||||
} else {
|
||||
error.value = "扫描内容不是有效网址:" + url;
|
||||
}
|
||||
const res = await scan({
|
||||
windowed: true,
|
||||
formats: [Format.QRCode],
|
||||
cameraDirection: "back",
|
||||
});
|
||||
handleContent(res.content);
|
||||
} catch (e) {
|
||||
error.value = "扫码失败或已取消:" + toMessage(e);
|
||||
} finally {
|
||||
@ -83,7 +103,7 @@ async function startScan() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消正在进行的扫码。@returns {Promise<void>} */
|
||||
/** 取消正在进行的相机扫码。@returns {Promise<void>} */
|
||||
async function stopScan() {
|
||||
try {
|
||||
await cancel();
|
||||
@ -94,7 +114,38 @@ async function stopScan() {
|
||||
setTransparent(false);
|
||||
}
|
||||
|
||||
// 离开页面时确保关闭相机并复原背景,避免占用。
|
||||
/** 触发图片选择框。@returns {void} */
|
||||
function pickImage() {
|
||||
error.value = "";
|
||||
if (fileInput.value) fileInput.value.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户选择的图片解码二维码。
|
||||
* @param {Event} evt input change 事件。
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function onFileChange(evt) {
|
||||
const file = evt.target.files && evt.target.files[0];
|
||||
evt.target.value = ""; // 允许连续选择同一文件
|
||||
if (!file) return;
|
||||
error.value = "";
|
||||
decoding.value = true;
|
||||
try {
|
||||
const content = await decodeQrFromFile(file);
|
||||
if (content == null) {
|
||||
error.value = "未在图片中识别到二维码,请换一张更清晰的图片。";
|
||||
return;
|
||||
}
|
||||
handleContent(content);
|
||||
} catch (e) {
|
||||
error.value = "图片识别失败:" + toMessage(e);
|
||||
} finally {
|
||||
decoding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 离开页面时确保关闭相机并复原背景,避免占用与残留透明态。
|
||||
onUnmounted(() => {
|
||||
if (scanning.value) stopScan();
|
||||
setTransparent(false);
|
||||
@ -105,8 +156,18 @@ onUnmounted(() => {
|
||||
<div>
|
||||
<!-- 非扫码态:说明与入口 -->
|
||||
<div v-if="!scanning" class="card">
|
||||
<p>点击下方按钮,将小票二维码对准取景框即可自动识别。</p>
|
||||
<p>点击下方按钮,将小票二维码对准取景框即可自动识别;也可从相册选择二维码图片。</p>
|
||||
<button @click="startScan">开始扫码</button>
|
||||
<button class="plain" style="margin-left:8px" :disabled="decoding" @click="pickImage">
|
||||
{{ decoding ? "识别中..." : "从图片扫描" }}
|
||||
</button>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style="display:none"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
<p v-if="error" style="color:#d32f2f;margin-top:12px">{{ error }}</p>
|
||||
<button v-if="denied" class="plain" style="margin-top:12px" @click="openAppSettings()">
|
||||
去系统设置开启相机权限
|
||||
|
||||
16
tests/fixtures/generate-qr.mjs
vendored
Normal file
16
tests/fixtures/generate-qr.mjs
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
// 生成测试用二维码图片(PNG),便于用「从图片扫描」功能验证。
|
||||
// 用法:node tests/fixtures/generate-qr.mjs [网址] [输出路径]
|
||||
import QRCode from "qrcode";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const url = process.argv[2] || "https://fapiao.example.com/open?id=20240701";
|
||||
const out = resolve(__dirname, process.argv[3] || "sample-qr.png");
|
||||
|
||||
await QRCode.toFile(out, url, {
|
||||
errorCorrectionLevel: "M",
|
||||
margin: 4,
|
||||
scale: 8,
|
||||
});
|
||||
console.log(`已生成二维码图片: ${out}\n内容: ${url}`);
|
||||
71
tests/qrscan.test.js
Normal file
71
tests/qrscan.test.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import QRCode from "qrcode";
|
||||
import {
|
||||
extractUrl,
|
||||
decodeQrFromImageData,
|
||||
} from "../src/qrscan.js";
|
||||
|
||||
/**
|
||||
* 用 qrcode 库把文本渲染成 RGBA 像素数据(放大每个模块以贴近真实拍摄清晰度)。
|
||||
* @param {string} text 待编码文本。
|
||||
* @param {number} scale 每个二维码模块放大的像素倍数。
|
||||
* @param {number} quiet 静区(模块数)。
|
||||
* @returns {{data:Uint8ClampedArray,width:number,height:number}} 画布像素数据。
|
||||
*/
|
||||
function renderQrImageData(text, scale = 8, quiet = 4) {
|
||||
const qr = QRCode.create(text, { errorCorrectionLevel: "M" });
|
||||
const size = qr.modules.size;
|
||||
const modules = qr.modules.data;
|
||||
const dim = (size + quiet * 2) * scale;
|
||||
const data = new Uint8ClampedArray(dim * dim * 4);
|
||||
// 先整体填白(含静区)。
|
||||
data.fill(255);
|
||||
for (let row = 0; row < size; row += 1) {
|
||||
for (let col = 0; col < size; col += 1) {
|
||||
if (!modules[row * size + col]) continue; // 0 = 浅色,跳过
|
||||
const x0 = (col + quiet) * scale;
|
||||
const y0 = (row + quiet) * scale;
|
||||
for (let dy = 0; dy < scale; dy += 1) {
|
||||
for (let dx = 0; dx < scale; dx += 1) {
|
||||
const idx = ((y0 + dy) * dim + (x0 + dx)) * 4;
|
||||
data[idx] = 0;
|
||||
data[idx + 1] = 0;
|
||||
data[idx + 2] = 0;
|
||||
data[idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { data, width: dim, height: dim };
|
||||
}
|
||||
|
||||
describe("qrscan", () => {
|
||||
// 4-1 网址校验:接受 http/https,拒绝其它内容。
|
||||
it("4-1 extractUrl 仅接受 http/https 网址", () => {
|
||||
expect(extractUrl("https://example.com/inv?id=1")).toBe(
|
||||
"https://example.com/inv?id=1"
|
||||
);
|
||||
expect(extractUrl(" http://a.b/c ")).toBe("http://a.b/c");
|
||||
expect(extractUrl("ftp://x")).toBe("");
|
||||
expect(extractUrl("hello")).toBe("");
|
||||
expect(extractUrl(null)).toBe("");
|
||||
});
|
||||
|
||||
// 4-2 从生成的清晰二维码像素中解码,验证扫描核心逻辑正确。
|
||||
it("4-2 decodeQrFromImageData 能识别清晰二维码", () => {
|
||||
const url = "https://fapiao.example.com/open?id=20240701";
|
||||
const imageData = renderQrImageData(url);
|
||||
expect(decodeQrFromImageData(imageData)).toBe(url);
|
||||
});
|
||||
|
||||
// 4-3 无法识别时返回 null,而不是抛错。
|
||||
it("4-3 无二维码时返回 null", () => {
|
||||
const blank = {
|
||||
data: new Uint8ClampedArray(64 * 64 * 4).fill(255),
|
||||
width: 64,
|
||||
height: 64,
|
||||
};
|
||||
expect(decodeQrFromImageData(blank)).toBeNull();
|
||||
expect(decodeQrFromImageData(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user