本项目在将 Web 版 Markdown 阅读器打包为 Tauri v2 桌面应用 + Capacitor 移动应用的过程中,遇到了大量"看似正确但实际不工作"的问题。本文档记录所有 root cause 及教训,避免重蹈覆辙。
完整打包技能文档:见
skills/tauri-capacitor-build.md,包含架构概览、完整 workflow 模板、常见问题速查表、发版流程、检查清单等。
{ "identifier": "fs:allow-read", "allow": [{ "path": "**" }] }
{ "identifier": "fs:allow-write", "allow": [{ "path": "**" }] }{ "identifier": "fs:allow-read-file", "allow": [{ "path": "**" }] }
{ "identifier": "fs:allow-write-file", "allow": [{ "path": "**" }] }
{ "identifier": "fs:allow-read-text-file", "allow": [{ "path": "**" }] }Tauri v2 tauri-plugin-fs 的权限 identifier 与 Rust 命令名一一对应,且命令名非常容易被混淆:
| 前端调用的命令 | 所需权限 identifier | 容易错写成 |
|---|---|---|
plugin:fs|read_file (读取整个文件) |
fs:allow-read-file |
❌ fs:allow-read(对应 read 命令——文件描述符式读取,完全不同) |
plugin:fs|read_text_file |
fs:allow-read-text-file |
❌ 未加 |
plugin:fs|write_file (写整个文件) |
fs:allow-write-file |
❌ fs:allow-write(对应 write 命令——文件描述符式写入) |
plugin:fs|read_dir |
fs:allow-read-dir |
✅ 恰好正确 |
plugin:fs|exists |
fs:allow-exists |
✅ 恰好正确 |
自动生成的权限文件位于插件目录下的 permissions/autogenerated/commands/,命名规则为:
- 命令
read_file→ identifierallow-read-file(全称加连字符)
- 永远不要猜测 permission identifier。必须到插件源码的
permissions/autogenerated/commands/下查看实际生成的文件名。 fs:allow-read≠fs:allow-read-file,前者是 "读取文件描述符"的权限,后者才是"读取整个文件"的权限。dialog:allow-confirm在 v2.7.1 中已废弃,实际是allow-message的别名。
dialog.confirm not allowed. Command not found
检查 dialog-2.7.1/permissions/default.toml:
permissions = ["allow-message", "allow-save", "allow-open"]dialog:default 不包含 allow-ask 和 allow-confirm。需要显式添加。
*:default权限集不等于"所有权限",只包含最基础的子集。使用前必须检查其定义。
拖拽文件到窗口,tauri://drag-drop / tauri://drag-enter / tauri://drag-leave 事件在 Windows 上不触发。
Tauri v2 的 IPC 拖拽事件实现存在平台兼容性问题,Windows 上经常收不到事件。
- 在
tauri.conf.json中设置"dragDropEnabled": false - 使用 HTML5 原生 DOM 事件:
document.addEventListener('dragover', handler)document.addEventListener('drop', handler)
- 通过
e.dataTransfer.files获取 File 对象,用FileReader读取
- Tauri IPC 事件(
tauri://*)不是 100% 可靠的,尤其是拖拽相关事件在 Windows 上存在问题。 - 优先使用 HTML5 原生 API,只有在原生 API 无法满足需求时再退而求其次使用 Tauri IPC。
图片格式检测后,isBinary 变量无法更新。
const isBinary = false;
// ... later ...
isBinary = true; // TypeError: Assignment to constant variable因为后续检测发现是图片格式,需要将 isBinary 改为 true,但 const 禁止重新赋值。
- 任何设计为「先检测再可能更新」的标志变量,必须使用
let而非const。 - 代码审查时特别注意
const/let的选择。
Session restore 恢复的是旧的缓存数据,而不是当前正确数据。
Service Worker 清理(caches.delete())是异步操作,而 restoreLastSession() 在 SW 清理完成之前就开始执行。清理和恢复之间没有同步保证。
await swCleanup(); // 先等待清理完成
await restoreLastSession(); // 再恢复- 涉及异步操作时,必须显式使用
await保证执行顺序。 - 不能假设两个异步函数的执行顺序(即使它们在代码中相邻)。
点击"清空文档"后,旧文件的导入列表、图片列表、图片索引仍然残留。
clearDocument() 只清空了 state.fileContent、state.fileName 等显示相关字段,但遗漏了:
state.importedFilesstate.imageFilesstate.imageIndexstate._currentImgDataUrl
下次打开文件时,这些残留数据会被用于导航和渲染,导致显示异常。
- 重置函数必须逐一对照
state对象的所有字段,确保无遗漏。 - 建议将 state 初始化提取为独立函数,重置时直接调用。
图片文件有左右箭头导航(navigateImage),但 Markdown/PDF/Word 等其他文件没有。
初始设计只考虑了图片的"同目录兄弟文件"导航,没有实现通用的"导入文件列表"导航。
添加 navigateDoc(idx) 和 updateDocNav() 函数,基于 state.importedFiles 数组实现通用的 prev/next 导航,适用于所有文件类型。
- 功能设计时应该先考虑通用方案,再为特殊类型(如图片)做特化优化。
- 导航功能应基于统一的文件列表,而不是为每种文件类型单独实现。
左上菜单"打开文件"只能选择一个文件。
FileAPI.pickFile(ACCEPT_EXTS) // 单文件应改为:
FileAPI.pickFiles(ACCEPT_EXTS) // 多文件还要遍历返回的数组,逐个调用 loadFile()。
- API 命名中
pickFile(单数)和pickFiles(复数)暗示了它们的区别。 - 调用前应检查 API 返回类型:
pickFile返回单个对象或 null,pickFiles返回数组。
导航到某些同目录图片时显示空白。
navigateImage() 中 readAsArrayBuffer 返回了 byteLength = 0 的缓冲区,但没有做有效性检查,直接传入渲染函数。
if (!raw || raw.byteLength === 0) {
showToast('无法加载图片');
return;
}- 所有 I/O 操作结果都必须做空值/空长度检查。
- "forbidden path" 和"空缓冲区"是不同的问题,需要分别处理。
在 Tauri v2 项目中遇到"功能不工作"时,按以下顺序排查:
- 权限检查 — 确认 permission identifier 与实际调用的 Rust 命令名完全匹配(到插件源码
permissions/autogenerated/commands/下核实) - scope 检查 — scope 中的
allow/deny路径模式是否正确(**通配) *:default检查 — 查看该插件的default.toml确认默认权限集实际包含哪些子权限- 命令名检查 — 前端
plugin:<plugin>|<command>中的 command 名是否与 Rust 端#[tauri::command]函数的名称一致 - console 日志 — 用
_d()输出关键变量的值(如权限、路径、缓冲区长度),打开调试面板对比 - 异步顺序 — 检查
async/await执行链,确认没有遗漏await - 常量和变量 — 确认需要修改的标志变量用
let而非const - 状态完整性 — 重置函数应逐个字段清空,最好用独立的初始化函数
- 通用 vs 特化 — 优先实现通用方案(如统一导航),再为特殊类型做优化
GitHub Actions macOS/Linux runner 打包时 curl SSL 连接超时。
src-tauri/.cargo/config.toml 配置了 rsproxy.cn(国内镜像源)作为 crates.io 替代。GitHub runner 位于海外,无法访问该镜像。
- 镜像源配置不要提交到仓库。
.cargo/config.toml属于开发者本地环境配置,应加入.gitignore。 - 需要镜像的开发者在本地单独配置
~/.cargo/config.toml(全局)或src-tauri/.cargo/config.toml(项目级但不入库)。
npm ci 报错 package.json and package-lock.json are in sync. Missing: @capacitor/ios from lock file。
在 package.json 中新增了 @capacitor/ios 依赖,但没有在本地执行 npm install 更新 package-lock.json。npm ci 要求两者严格一致。
- CI 中用
npm install代替npm ci(会自动更新 lock 文件),或将新依赖从package.json移除,在 CI 中内联安装。 - 最佳实践:移动端平台依赖(
@capacitor/ios、@capacitor/android)不要写入package.json,在 CI 中按需安装即可。
gradlew assembleRelease 报错 invalid source release: 21。
Capacitor v8 / Gradle 8.14+ 要求 Java 21+,CI 中配置的 Java 17 不够。
- Capacitor 大版本升级后,检查 Gradle 和 Java 版本要求。
- CI 中 Android 构建配置固定
java-version: 21。
./gradlew: Permission denied。
Git 在 Windows 上克隆时不会保留 Unix 可执行权限位。CI runner(Linux/macOS)需要显式 chmod +x。
- 在 CI 构建 Android 前加
chmod +x gradlew。
pod install 报错 No Podfile found in the project directory。
Capacitor v8 的 iOS 项目改用 Xcode 原生项目管理,不再生成 Podfile,也不再使用 .xcworkspace,而是直接使用 .xcodeproj。
xcodebuild -project App.xcodeproj \
-scheme App \
-configuration Release \
-sdk iphoneos \
-archivePath $PWD/build/App.xcarchive \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
archive- Capacitor 大版本升级后,iOS 构建方式可能根本性变化,必须查阅对应版本文档。
Release 页面上传了几十个内部文件(Info.plist、App、js 等),体积巨大。
.xcarchive 是一个目录(Xcode archive bundle),upload-artifact 遍历目录上传了所有内容。Release 的 ios-app/**/* glob 进一步递归展开了所有文件。
构建后先压缩再上传:
cd ios/App/build
zip -r ios-app.zip App.xcarchive- macOS 的
.xcarchive、.app、.framework都是目录,上传前必须 zip。
git push origin main --tags 后工作流未触发。
git push --tags 只推送本地有但远程没有的 tag。如果 tag 已存在(之前已推送),不会产生新的 push 事件,工作流不会触发。
- 新建 tag:
git tag v1.0.2 && git push origin v1.0.2 - 或在 Actions 页面手动触发(
workflow_dispatch)
两个 workflow 各自创建 Release,产物混在一起或冲突。
将桌面端(Tauri)和移动端(Capacitor)构建合并到同一个 workflow 文件中,用 needs: [build, android, ios] 统一等待所有构建完成后创建 Release。
在 GitHub Actions 中搭建 Tauri + Capacitor 多平台构建时,逐项确认:
- Cargo 镜像 — 项目内无
.cargo/config.toml中的镜像源配置 - package-lock.json — 与
package.json同步(本地跑npm install确认) - Java 版本 — Android 用 Java 21+(Capacitor v8 要求)
- gradlew 权限 — 构建前
chmod +x gradlew - iOS 构建 — Capacitor v8 用
-project App.xcodeproj(非-workspace App.xcworkspace),不需要pod install - xcarchive — 上传前 zip 压缩,避免上传内部文件
- Artifact 路径 — 精确到文件,避免 glob 匹配到目录内容
- Release 触发 — 打新 tag 或手动触发,旧 tag 不会重新触发
- 统一 Release — 多个构建 job 合并到一个 workflow,用
needs串联 - npm ci vs npm install — CI 中优先用
npm ci,若依赖不同步则用npm install - Shell 兼容性 — Windows runner 默认 PowerShell,含 bash 语法的步骤必须加
shell: bash - APK 签名 — CI 每次环境不同,debug keystore 需用
actions/cache缓存保证签名一致 - 版本号自动化 — 从 git tag 自动提取版本号,CI 中用 Node.js 同步更新所有配置文件,避免手动改版本
- NSIS 配置 — Tauri v2 内置 NSIS,无需外部安装;保留 wix 配置,NSIS 和 WiX 配置互不影响
- 版本号格式 — 回退版本号必须符合 WiX 最严格要求(纯数字),不要用
0.0.0-dev,用0.0.0
APK 打开 PDF 后不报错,但内容为空白。
renderPdf() 使用 <iframe> + blob: URL 方案,依赖浏览器内置 PDF 查看器。Android WebView 没有内置 PDF 查看器,blob URL 无法渲染。
集成 pdf.js(Mozilla PDF.js),在 Capacitor 环境下用 <canvas> 逐页渲染:
if (FileAPI.platform === 'capacitor') {
// 使用 pdf.js 渲染
const pdfjsLib = await import(baseUrl + 'lib/pdf.min.mjs');
// ... 渲染到 canvas
} else {
// 桌面端继续用 iframe
}- 不要假设所有平台都有相同的浏览器能力。Android WebView 是精简版 Chromium,缺少很多桌面 Chrome 的功能。
- 移动端文件格式渲染(PDF/Word/Excel)必须有独立的渲染方案,不能依赖浏览器内置能力。
- pdf.js 的 ESM 格式需要用
import()动态加载,路径要用绝对路径(baseUrl + 'lib/pdf.min.mjs'),相对路径在 Capacitor 中可能解析失败。
PDF 在手机上能显示但分辨率很低,文字模糊看不清。
渲染 scale 只考虑了屏幕宽度,没有乘以 window.devicePixelRatio。手机屏幕 DPI 通常是 2-3x,不乘以 dpr 会导致渲染分辨率不足。
var scale = baseScale * (window.devicePixelRatio || 1);- 移动端 canvas 渲染必须考虑
devicePixelRatio,否则在高 DPI 屏幕上模糊。 - 桌面端
devicePixelRatio通常为 1,移动端通常为 2-3。
每次 CI 构建的 APK 签名不同,无法覆盖安装升级。
CI 每次运行都是全新环境,debug keystore 由 keytool 新生成,导致签名不一致。
使用 actions/cache@v4 缓存 ~/.android/debug.keystore:
- name: Setup debug keystore
uses: actions/cache@v4
with:
path: ~/.android/debug.keystore
key: android-debug-keystore
- name: Generate debug keystore (if not cached)
run: |
if [ ! -f "$HOME/.android/debug.keystore" ]; then
keytool -genkeypair -v -keystore $HOME/.android/debug.keystore ...
fi同时在 build.gradle 中配置固定签名路径:
signingConfigs {
debug {
storeFile file(System.getenv("ANDROID_KEYSTORE") ?: "${System.getProperty('user.home')}/.android/debug.keystore")
}
}- CI 环境中任何「自动生成」的文件(keystore、证书、配置)都需要缓存或固定,否则每次构建结果不一致。
- debug keystore 是 APK 签名的基础,必须保证跨构建一致性。
每次发版都要手动修改 tauri.conf.json、Cargo.toml、package.json 三个文件的版本号,容易遗漏或不同步。
在 CI 中自动从 git tag 提取版本号,构建前同步更新所有配置文件:
- name: Resolve version
id: ver
shell: bash
run: |
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Sync version to config files
shell: bash
run: |
VERSION="${{ steps.ver.outputs.version }}"
node -e "
const fs = require('fs');
const tauri = JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8'));
tauri.version = '$VERSION';
fs.writeFileSync('src-tauri/tauri.conf.json', JSON.stringify(tauri, null, 2));
"发版只需:git tag v1.0.9 && git push origin v1.0.9
- 版本号是「单一事实来源」(Single Source of Truth),应该只从一个地方(git tag)派生。
- 用 Node.js 更新 JSON 文件比
sed更可靠(跨平台兼容)。 workflow_dispatch可以加inputs.version参数支持手动指定版本号。
Windows 构建 job 报错 Missing '(' after 'if' in if statement。
GitHub Actions Windows runner 默认使用 PowerShell,而 shell 脚本用的是 bash 语法。没有指定 shell: bash 时,PowerShell 无法解析 bash 语法。
所有包含 bash 语法的步骤必须显式声明 shell: bash:
- name: Resolve version
shell: bash # 必须加!
run: |
if [ ... ]; then- GitHub Actions 的
shell默认值因 runner 而异:Ubuntu/macOS 默认bash,Windows 默认pwsh。 - 跨平台 workflow 中,所有 shell 脚本步骤都应该显式声明
shell: bash,不要依赖默认值。 run: |块中的 bash 语法在 PowerShell 中会直接报错,不会被跳过。
PDF 能显示但无法缩放、翻页,用户体验极差。
初版只渲染了 canvas 页面,没有添加任何交互控件。移动端用户无法缩放查看细节,也无法快速跳转页面。
添加完整的 PDF 查看器工具栏:
- 翻页:上一页 / 下一页按钮 + 滚动时自动更新页码
- 缩放:放大 / 缩小按钮 + 适应宽度按钮
- 页码:显示当前页 / 总页数
- 关闭:关闭 PDF 查看器返回主界面
- 「能用」和「好用」是两回事。文件格式支持不能只停留在「能渲染」,必须有基本的阅读体验。
- 移动端 PDF 查看器的核心功能:缩放、翻页、页码指示。缺少任何一个都严重影响体验。
- 工具栏应该固定在顶部(
position:fixed),不随页面滚动。
在 Tauri + Capacitor 多平台项目中支持文件格式时,逐项确认:
- PDF — 桌面端用 iframe + blob URL,移动端必须用 pdf.js
- PDF 分辨率 — 渲染 scale 必须乘以
devicePixelRatio - PDF 交互 — 必须有缩放、翻页、页码显示
- PDF 双指缩放 — 动态修改 viewport(
user-scalable=yes)+ 手动监听 touch 事件 + CSS transform 实时预览 - PDF 标注 — 双 canvas 架构(渲染层 + 标注层),标注坐标基于 canvas 内部坐标
- PDF 便签 — 用 HTML div 元素(非 canvas),支持事件和 tooltip
- Word/Excel/PPT — 桌面端用 mammoth/docx-preview/xlsx/pptxjs,移动端需验证兼容性
- 图片 — 各平台原生支持,但大图需要压缩处理
- 文件读取 — Capacitor 的
readAsArrayBuffer对_uri路径支持不完整,需要特殊处理 - 文件选择 — Capacitor 回退到 Web
<input type="file">,不支持目录选择 - 保存文件 — Capacitor 不支持原生保存对话框,需要回退到 Blob 下载
MSI 构建报错:optional pre-release identifier in app version must be numeric-only and cannot be greater than 65535 for msi target
WiX/MSI 对版本号格式有严格要求:主版本.次版本.修订号[.预发布],其中预发布标识必须是纯数字(且 ≤ 65535)。0.0.0-dev 中的 dev 是非数字字符串,WiX 拒绝接受。NSIS 没有这个限制。
CI 中非 tag 触发时的版本回退值从 0.0.0-dev 改为 0.0.0(纯数字):
- name: Resolve version
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.version }}" ]; then
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
else
echo "version=0.0.0" >> $GITHUB_OUTPUT # 不是 0.0.0-dev!
fi- WiX 版本号规则:
MAJOR.MINOR.BUILD[.PRERELEASE],PRERELEASE 部分只能是数字。 - NSIS 对版本号宽容得多,所以只构建 NSIS 时不会发现这个问题。
- 项目中所有版本号回退值都必须检查是否符合最严格的格式要求。
恢复了版本号后 MSI 仍然构建失败,报各种 WiX 相关错误。
v1.0.6 提交时为了配置 NSIS 选项,把 tauri.conf.json 中的整个 windows 块替换成了:
"windows": {
"nsis": { "displayLanguageSelector": false, "installerIcon": "icons/icon.ico" }
}删除了原有的 "wix": { "language": "zh-CN" } 配置。没有 wix 配置后,Tauri 构建 MSI 时走了不同的代码路径导致失败。
NSIS 和 WiX 配置需要同时存在:
"windows": {
"nsis": {
"displayLanguageSelector": false,
"installerIcon": "icons/icon.ico"
},
"wix": {
"language": "zh-CN"
}
}tauri.conf.json的bundle.windows下nsis和wix是独立的配置节,互不影响,不能互相替代。- 删除任何配置前,先确认该配置是否影响其他构建目标。
APK 中 PDF 能正常显示,但双指缩放手势无反应。
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">maximum-scale=1.0, user-scalable=no 直接禁止了 WebView 中的所有缩放手势,包括双指缩放。
PDF 查看器打开时动态修改 viewport,关闭时恢复:
// 保存原始值
var origViewport = document.querySelector('meta[name="viewport"]');
var origContent = origViewport.content;
// PDF 查看器打开时
origViewport.content = 'width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=5.0, user-scalable=yes';
// PDF 查看器关闭时
origViewport.content = origContent;user-scalable=no在移动端 web app 中常见,但会阻止所有缩放手势。- 如果只需要在特定页面允许缩放,应该动态修改 viewport 而不是全局放开。
- 动态修改 viewport 是合法的,WebView 会实时响应变化。
即使 viewport 允许缩放了,PDF canvas 内容仍然不会跟随双指手势缩放。
viewport 缩放作用于整个页面(所有元素等比放大),而 PDF 是用 canvas 渲染的固定分辨率图像。用户期望的是 PDF 内容本身缩放(类似原生 PDF 阅读器),不是整个 UI 放大。
手动监听双指触摸事件,计算缩放比例:
var pinchState = null;
var pinchStartScale = currentScale;
// touchstart: 记录初始双指距离
scrollContainer.addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
e.preventDefault();
var dx = e.touches[0].clientX - e.touches[1].clientX;
var dy = e.touches[0].clientY - e.touches[1].clientY;
pinchStartScale = currentScale;
pinchState = { dist: Math.sqrt(dx * dx + dy * dy) };
}
}, { passive: false });
// touchmove: 用 CSS transform 实时预览(零延迟)
scrollContainer.addEventListener('touchmove', function(e) {
if (e.touches.length === 2 && pinchState) {
e.preventDefault();
var dx = e.touches[0].clientX - e.touches[1].clientX;
var dy = e.touches[0].clientY - e.touches[1].clientY;
var dist = Math.sqrt(dx * dx + dy * dy);
var ratio = dist / pinchState.dist;
var previewScale = Math.min(Math.max(pinchStartScale * ratio, 0.25), 5);
var ratioCSS = previewScale / currentScale;
pagesContainer.style.transform = 'scale(' + ratioCSS + ')';
document.getElementById('pdfZoomLevel').textContent = Math.round(previewScale * 100) + '%';
pinchPending = previewScale;
}
}, { passive: false });
// touchend: 松手后用新 scale 重新渲染 canvas(保证清晰度)
scrollContainer.addEventListener('touchend', function(e) {
if (e.touches.length < 2 && pinchState) {
pagesContainer.style.transform = '';
if (pinchPending && Math.abs(pinchPending - currentScale) > 0.01) {
currentScale = pinchPending;
renderAllPages(); // 重新渲染 canvas
}
pinchState = null;
pinchPending = false;
}
});- 双指拖动过程中用 CSS
transform:scale()实时预览(GPU 加速,零延迟) - 松手后才用新 scale 重新渲染 canvas(保证高清晰度,但有短暂渲染时间)
- 这种"CSS 实时预览 + canvas 重新渲染"的两阶段策略是移动端 PDF 缩放的最佳实践
viewport缩放和内容缩放是两个不同的概念,不能混淆。- canvas 元素不会自动跟随 viewport 缩放,需要手动处理触摸事件。
e.preventDefault()+{ passive: false }是拦截触摸事件的必要配置。- 渲染所有页面的 canvas 是耗时操作,只在手势结束时执行,不能在手势过程中执行。
在 canvas 上叠加 annotation overlay(另一个 canvas),而不是直接在 PDF canvas 上绘制:
- PDF canvas:只读渲染,缩放时完全重绘
- Annotation canvas:独立的半透明层,用绝对定位覆盖在 PDF canvas 上
- 便签 marker:HTML 元素(div),用绝对定位放在页面容器上
- 缩放时需要重绘 PDF,annotations 不需要重新计算坐标
- 清除标注不需要重新渲染 PDF 内容
- 标注的坐标基于 PDF 原始尺寸(devicePixelRatio 缩放前),缩放时自动对齐
- 交互式 canvas 应用(标注、绘图)应该用多层 canvas,渲染层和交互层分离。
- 便签/标记等 UI 元素用 HTML div 比 canvas 更方便(支持事件、样式、tooltip)。
- 标注坐标存储时应使用 canvas 内部坐标(高分辨率),而不是 CSS 像素坐标。
只有 PDF 查看器支持双指缩放,其他格式(Markdown/HTML/TXT/图片等)无法双指缩放。
双指缩放只在 PDF 查看器的 wrapper 上实现了触摸事件处理,通用内容区域(contentArea)没有对应的实现。
在 contentArea 上添加触摸事件监听,复用已有的 applyZoom 和 reapplyZoom 函数:
contentArea.addEventListener('touchstart', function(e) {
if (e.touches.length === 2 && !document.getElementById('pdfViewerWrapper')) {
e.preventDefault();
gPinchStartZoom = state.zoomLevel;
gPinchState = { dist: Math.sqrt(dx * dx + dy * dy) };
}
}, { passive: false });
contentArea.addEventListener('touchmove', function(e) {
if (e.touches.length === 2 && gPinchState) {
e.preventDefault();
var target = mdContent.style.display !== 'none' ? mdContent : htmlFrame;
target.style.transform = 'scale(' + previewZoom + ')';
target.style.transformOrigin = 'top left';
}
}, { passive: false });- 检查
!document.getElementById('pdfViewerWrapper')避免与 PDF 查看器的缩放冲突 - 复用
state.zoomLevel和reapplyZoom()保持与按钮缩放一致 - CSS transform 实时预览 + 松手后持久化到
state.zoomLevel
- 功能扩展时要检查是否遗漏了其他内容类型。
- 复用已有状态管理(
state.zoomLevel)比创建独立状态更可靠。
每次打开应用都弹出"恢复上次阅读?"确认框,用户需要手动点击"恢复"。
restoreLastSession() 函数中使用 openSheet() 显示确认对话框,等待用户选择。
去掉确认对话框,直接恢复:
- 有缓存内容(IndexedDB)→ 直接渲染
- 无缓存(二进制文件)→ 在空状态显示"恢复上次阅读"按钮(
showRestoreButtonIfAvailable)
- 移动端用户体验要求"打开即用",减少确认步骤。
- 二进制文件(PDF/图片)无法缓存到 IndexedDB,只能提示重新选择文件。
CI 自动更新了 tauri.conf.json、Cargo.toml、package.json 的版本号,但应用内"关于"对话框仍显示硬编码的 v2.0。
index.html 中有三个硬编码的版本号:
- About 对话框:
v2.0 __BUILD_ID__:20260619-0500- HTML 中的
buildIddiv:20260619-0500
CI 的版本同步脚本没有覆盖 index.html。
- 在
index.html中定义__APP_VERSION__占位符:
const __APP_VERSION__ = '__APP_VERSION__';- About 对话框使用动态版本:
v${typeof __APP_VERSION__ !== 'undefined' && __APP_VERSION__ !== '__APP_VERSION__' ? __APP_VERSION__ : 'dev'}- CI 同步脚本增加
index.html替换:
let html = fs.readFileSync('public/index.html', 'utf8');
html = html.replace(/__APP_VERSION__/g, '$VERSION');
fs.writeFileSync('public/index.html', html);- 启动时更新
buildIddiv 的文本内容。
- 版本号是「单一事实来源」,所有显示版本的地方都必须从同一来源派生。
- 新增配置文件时,检查 CI 同步脚本是否覆盖了该文件。
- 占位符替换比硬编码更可靠,但要确保所有 CI job 都执行替换。