Forráskód Böngészése

fix: 修复真机 PDF 预览崩溃与 HTML 预览白屏

## PDF:降级 pdfjs-dist 6.4.299 → 4.10.38
真机报错 `this[#e].getOrInsertComputed is not a function`。
根因:pdfjs 6.4 使用了 Map.prototype.getOrInsertComputed(ES2026 提案),
仅 Chrome 133+ / Safari 18.4+ 支持,真机内核较旧即崩。
电脑浏览器与「模拟手机」(只改视口不改内核)测不出该问题。

## HTML 预览:增加加载态
移动端首开需下载约 1.3MB 资源(主包 + worker + 预览 chunk),
期间 iframe 纯白,用户误以为打不开。现在显示「正在加载页面…」。

## 部署脚本:刷新固定文件名资源
pdf.worker.js 是固定名、不带 hash,CDN 会一直吐旧版本。
实测换了 pdfjs 后线上仍是旧 worker,导致修复不生效。
现在部署时一并刷新 /pdf.worker.js 与 /favicon.ico。

Co-Authored-By: Claude Code <noreply@anthropic.com>
Huccct 3 órája
szülő
commit
6ba2c67c77

+ 54 - 53
package-lock.json

@@ -7,6 +7,7 @@
     "": {
       "name": "fmode-app-v2",
       "version": "0.0.0",
+      "hasInstallScript": true,
       "dependencies": {
         "@angular/common": "^20.3.0",
         "@angular/compiler": "^20.3.0",
@@ -19,7 +20,7 @@
         "fmode-ng": "^0.0.246",
         "matrix-js-sdk": "^42.4.0",
         "ngx-markdown": "^20.1.0",
-        "pdfjs-dist": "^6.4.299",
+        "pdfjs-dist": "4.10.38",
         "prismjs": "^1.30.0",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
@@ -2520,9 +2521,9 @@
       ]
     },
     "node_modules/@napi-rs/canvas": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-1.0.11.tgz",
-      "integrity": "sha512-vt5tog1cr+QKadIddGe+3LrcX0EmjEd0izF717Uwf92mBmgYfq2XRmL2r4bZU40spB69exxhrBn9I91HYEr/7w==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.100.tgz",
+      "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
       "license": "MIT",
       "optional": true,
       "workspaces": [
@@ -2536,23 +2537,23 @@
         "url": "https://github.com/sponsors/Brooooooklyn"
       },
       "optionalDependencies": {
-        "@napi-rs/canvas-android-arm64": "1.0.11",
-        "@napi-rs/canvas-darwin-arm64": "1.0.11",
-        "@napi-rs/canvas-darwin-x64": "1.0.11",
-        "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.11",
-        "@napi-rs/canvas-linux-arm64-gnu": "1.0.11",
-        "@napi-rs/canvas-linux-arm64-musl": "1.0.11",
-        "@napi-rs/canvas-linux-riscv64-gnu": "1.0.11",
-        "@napi-rs/canvas-linux-x64-gnu": "1.0.11",
-        "@napi-rs/canvas-linux-x64-musl": "1.0.11",
-        "@napi-rs/canvas-win32-arm64-msvc": "1.0.11",
-        "@napi-rs/canvas-win32-x64-msvc": "1.0.11"
+        "@napi-rs/canvas-android-arm64": "0.1.100",
+        "@napi-rs/canvas-darwin-arm64": "0.1.100",
+        "@napi-rs/canvas-darwin-x64": "0.1.100",
+        "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
+        "@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
+        "@napi-rs/canvas-linux-arm64-musl": "0.1.100",
+        "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
+        "@napi-rs/canvas-linux-x64-gnu": "0.1.100",
+        "@napi-rs/canvas-linux-x64-musl": "0.1.100",
+        "@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
+        "@napi-rs/canvas-win32-x64-msvc": "0.1.100"
       }
     },
     "node_modules/@napi-rs/canvas-android-arm64": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.11.tgz",
-      "integrity": "sha512-dtJS4FQEfH9OwSYLtMrEnj7T8DzBbJc/97LI/OkiWOYpW/iW5t6R/gcxVrUXS8l45gCVpwMufgucRe/pe0ISqA==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
+      "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
       "cpu": [
         "arm64"
       ],
@@ -2570,9 +2571,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-darwin-arm64": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.11.tgz",
-      "integrity": "sha512-J/JylWoJTG8OQqZaiqly4IODyAxNofUlZo0wtSBJtHk6AdymbdIkPynAM1JvQaZpPaG5h+msk9QuK8y4i1ooqw==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
+      "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
       "cpu": [
         "arm64"
       ],
@@ -2590,9 +2591,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-darwin-x64": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.11.tgz",
-      "integrity": "sha512-nlecCk3xy06eAGC7krZtzNDm9UNTHKuHQYK8ZYegdjzgSL8anGt/eLJZm86OWaNgZkwQwvDqfSmG8Nqe6MPZNQ==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
+      "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
       "cpu": [
         "x64"
       ],
@@ -2610,9 +2611,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.11.tgz",
-      "integrity": "sha512-DKImmG+sTPolE0kGPsYPBxTtOjb3INALp32O2lW2CfSr0bAzSEKKuu+GrQbtilClyoPOahUErS3OrznFDraoWA==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
+      "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
       "cpu": [
         "arm"
       ],
@@ -2630,9 +2631,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.11.tgz",
-      "integrity": "sha512-55sykACGENj5j1rhj+81AO8JbR9HYgGjRZ7/H+pNr4VE1DvINKYNMTcutk8ZmWxk276jAQvUKJ3+Kdj9GXU3lQ==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
+      "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
       "cpu": [
         "arm64"
       ],
@@ -2650,9 +2651,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-arm64-musl": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.11.tgz",
-      "integrity": "sha512-6Jz6jys17yXydO+WdJgu7JhoB4K7/w9sWHByzzTmLjQghUx3TDMWvLv+UcCdnWhg4oprET67EMymnrETdeGNAg==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
+      "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
       "cpu": [
         "arm64"
       ],
@@ -2670,9 +2671,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.11.tgz",
-      "integrity": "sha512-lf2XeEebWiUYVOVEjLXxF7dfXrBBSEOBn9AqBh39VYXrPt5cDqI+1t58IFSjn8npqkg5jIOs8TQbjKABbFW1gQ==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
+      "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
       "cpu": [
         "riscv64"
       ],
@@ -2690,9 +2691,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-x64-gnu": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.11.tgz",
-      "integrity": "sha512-iDapw7nFSE5tsmQxcH2+KotJhyfVPmHd6vUOQN/Em+8WNbkZJwJ/rDowlJMFnMl0SYSDSfgKCNUGi6Sn5++Smg==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
+      "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
       "cpu": [
         "x64"
       ],
@@ -2710,9 +2711,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-x64-musl": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.11.tgz",
-      "integrity": "sha512-26q26AyITo3CGZX4HWFIMgBMA1lu3YN07gxzBm4tW6Hglq/OVyTP50kDrmWuKybCX6dh3m8Ly+c7YhI+sqCgCQ==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
+      "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
       "cpu": [
         "x64"
       ],
@@ -2730,9 +2731,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.11.tgz",
-      "integrity": "sha512-cUO96IdsfvIj2S+2PNcDSkTcLhjPj8pIOEWRV1Kj70MOP4wnE9zSk7N4C1EKHlFGRcgdR5AkbnBZj0dfHK9nbQ==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
+      "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
       "cpu": [
         "arm64"
       ],
@@ -2750,9 +2751,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-win32-x64-msvc": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.11.tgz",
-      "integrity": "sha512-DFYp/ZQRdKunI/LPc0UkRXG1vlNjJKOJf8O8Ctb8+wm2Nt2ELg9Eamc31D2VoakkfGo8L+z0Iejz6WtFbQwyGw==",
+      "version": "0.1.100",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
+      "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
       "cpu": [
         "x64"
       ],
@@ -10185,15 +10186,15 @@
       }
     },
     "node_modules/pdfjs-dist": {
-      "version": "6.4.299",
-      "resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-6.4.299.tgz",
-      "integrity": "sha512-AVl138zALtfaAPvADulE0PZThbYzCBS79nL4pOSL/6Sm/4AH5A21BD9VHt97OlCuzJuCpmeZtAtkinisF4Vb1g==",
+      "version": "4.10.38",
+      "resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz",
+      "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==",
       "license": "Apache-2.0",
       "engines": {
-        "node": ">=22.13.0 || >=24"
+        "node": ">=20"
       },
       "optionalDependencies": {
-        "@napi-rs/canvas": "^1.0.10"
+        "@napi-rs/canvas": "^0.1.65"
       }
     },
     "node_modules/picocolors": {

+ 1 - 1
package.json

@@ -49,7 +49,7 @@
     "fmode-ng": "^0.0.246",
     "matrix-js-sdk": "^42.4.0",
     "ngx-markdown": "^20.1.0",
-    "pdfjs-dist": "^6.4.299",
+    "pdfjs-dist": "4.10.38",
     "prismjs": "^1.30.0",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",

+ 18 - 2
scripts/deploy.mjs

@@ -124,14 +124,30 @@ run('obsutil', ['chattri', BUCKET, '-r', '-f', `-e=${OBS_EP}`, '-acl=public-read
 // ── 4. 刷新 CDN ────────────────────────────────────────────────────────────
 console.log('\n=== [4/5] 刷新 CDN ===');
 const profile = process.env.HCLOUD_PROFILE || 'default';
+/*
+ * 🔴 必须同时刷新「固定文件名的资源」(2026-11-13 实测踩坑)。
+ *
+ * 带 hash 的 chunk(main-XXXX.js)换 hash 就等于换 URL,天然绕过 CDN 缓存。
+ * 但下面这些是**固定文件名**,内容变了 URL 却没变 →
+ * CDN 会一直吐旧版本,用户拿到的还是老代码:
+ *
+ *   · pdf.worker.js  —— pdfjs 升级/降级时最容易中招
+ *     (实测:换了 pdfjs 4.10 后,线上仍是 6.4 的旧 worker,
+ *       真机 PDF 预览一直报 getOrInsertComputed 不是函数)
+ *
+ * 新增固定名资源时,记得同步加到这里。
+ */
+const FIXED_NAME_ASSETS = ['/pdf.worker.js', '/favicon.ico'];
+const refreshUrls = [`${CDN_BASE}/`, `${CDN_BASE}/index.html`, ...FIXED_NAME_ASSETS.map((p) => `${CDN_BASE}${p}`)];
 const refreshArgs = [
   'CDN',
   'CreateRefreshTasks/v2',
   `--cli-region=${CDN_REGION}`,
-  `--refresh_task.urls.1=${CDN_BASE}/`,
-  `--refresh_task.urls.2=${CDN_BASE}/index.html`,
+  ...refreshUrls.map((u, i) => `--refresh_task.urls.${i + 1}=${u}`),
   `--cli-profile=${profile}`,
 ];
+console.log('  刷新 URL:');
+for (const u of refreshUrls) console.log(`    ${u}`);
 // KooCLI 首次运行会交互式询问《隐私政策声明》,无人值守时会卡死并报 USE_ERROR
 run('hcloud', refreshArgs, { input: 'y\ny\ny\n' });
 

+ 51 - 1
src/app/components/preview/file-preview/file-preview.ts

@@ -298,6 +298,17 @@ export type PreviewPresentation = 'rail' | 'full' | 'modal';
                           (click)="$event.stopPropagation()"
                         ></div>
                       }
+                      @if (frameLoading() && !frameStalled()) {
+                        <!--
+                          加载中提示(2026-11-13 真机实测):
+                          移动端首开要下 1.3MB 资源,期间 iframe 纯白,
+                          用户以为打不开。这里给明确反馈。
+                        -->
+                        <div class="fp-html-loading" role="status" aria-live="polite">
+                          <i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>
+                          <p>正在加载页面…</p>
+                        </div>
+                      }
                       @if (frameStalled()) {
                         <div class="fp-html-stall">
                           <i class="fa-solid fa-circle-exclamation" aria-hidden="true"></i>
@@ -807,6 +818,31 @@ export type PreviewPresentation = 'rail' | 'full' | 'modal';
         color: var(--muted);
         text-align: center;
       }
+      /*
+       * 加载中提示(2026-11-13):
+       * 与 stall 同样是覆盖层,但半透明 —— 既给出反馈,又不完全遮挡
+       * (iframe 部分内容可能已经开始渲染)。
+       */
+      .fp-html-loading {
+        position: absolute;
+        inset: 0;
+        display: grid;
+        place-content: center;
+        justify-items: center;
+        gap: var(--sp-3);
+        background: var(--surface);
+        color: var(--muted);
+        text-align: center;
+        pointer-events: none;
+      }
+      .fp-html-loading i {
+        font-size: var(--fs-24);
+        color: var(--primary);
+      }
+      .fp-html-loading p {
+        margin: 0;
+        font-size: var(--fs-13);
+      }
 
       /* markdown / 文本:不缩放容器,只缩放字号(用户要求:外层不变) */
       .fp-paper {
@@ -999,6 +1035,14 @@ export class FilePreviewComponent {
 
   /** iframe 卡住 */
   readonly frameStalled = signal(false);
+  /**
+   * iframe 还在加载中(2026-11-13)。
+   *
+   * 🔴 移动端首次打开时,主包 + pdf worker + 预览 chunk 合计要下 1.3MB,
+   *    期间 iframe 是纯白的 —— 用户以为"打不开"(真机实测)。
+   *    这里给一个明确的加载态,把"白屏无反馈"变成"正在加载"。
+   */
+  readonly frameLoading = signal(false);
   /** 「在新标签打开」被浏览器拦截时的提示(3 秒自动消失) */
   readonly blockedHint = signal(false);
   private frameTimer: ReturnType<typeof setTimeout> | null = null;
@@ -1339,6 +1383,7 @@ export class FilePreviewComponent {
   onFrameLoad(): void {
     this.clearFrameTimer();
     this.frameStalled.set(false);
+    this.frameLoading.set(false);
   }
 
   retryFrame(): void {
@@ -1352,8 +1397,13 @@ export class FilePreviewComponent {
   private startFrameTimer(): void {
     this.clearFrameTimer();
     this.frameStalled.set(false);
+    /* 开始计时即进入「加载中」态,onFrameLoad 时清除 */
+    this.frameLoading.set(true);
     /* 15 秒兜底:只用于「一直没有 load 事件」的情况 */
-    this.frameTimer = setTimeout(() => this.frameStalled.set(true), 15_000);
+    this.frameTimer = setTimeout(() => {
+      this.frameStalled.set(true);
+      this.frameLoading.set(false);
+    }, 15_000);
   }
 
   private clearFrameTimer(): void {