From 09f434bbcfa3226f43af5b63fca2373c4b6e7eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Mon, 3 Aug 2026 15:18:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A6=BB=E7=BA=BF=E8=A3=81=E5=89=AA?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=20+=20=E5=A2=9E=E9=87=8F=E9=A2=84=E8=A7=88?= =?UTF-8?q?=20+=20DEV=E9=A2=84=E8=A7=88=E9=A1=B5=20+=20tempUploads?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=20+=20SDK=E7=BC=93=E5=AD=98=E7=A0=B4?= =?UTF-8?q?=E5=9D=8F=20+=20UI=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增离线裁剪页面 /offline-subset(纯浏览器端字体裁剪) - SDK subsetProvider 抽象,离线页面注入本地裁剪 provider - 预览用增量裁剪(SDK自动去重),下载用全量裁剪 - 拖放区域支持点击和拖拽选择字体文件 - 新增 DEV 预览页 /__dev(仅 dev 模式路由可达) - FontDebugPreview 从首页提取为独立页面 - tempUploads 统计字段(全链路+持久化) - webfont-sdk.js 构建时戳缓存破坏 - 首页导航栏新增离线裁剪入口 - UploadSection 离线裁剪入口合并到游客上传卡片 - SDK 文档代码示例补充 HTML 用法 - 导航栏窄屏换行适配 --- backend/routes/stats.ts | 1 + backend/routes/upload.ts | 6 +- backend/shared.ts | 4 + backend/stats_store.ts | 5 +- index.html | 2 +- package.json | 1 + pnpm-lock.yaml | 161 ++++++++++++++++ public/webfont-sdk.js | 87 ++++++--- src/FontDebugPreview.vue | 47 +++-- src/StatsPanel.vue | 1 + src/UploadSection.vue | 14 ++ src/i18n.ts | 4 + src/main.ts | 7 + src/pages/DevPreview.vue | 81 ++++++++ src/pages/Home.vue | 25 ++- src/pages/OfflineSubset.vue | 359 ++++++++++++++++++++++++++++++++++++ src/shims/woff2-shim.ts | 34 ++++ src/subset-client.ts | 174 +++++++++++++++++ vite.config.ts | 127 ++++++++++++- 19 files changed, 1093 insertions(+), 47 deletions(-) create mode 100644 src/pages/DevPreview.vue create mode 100644 src/pages/OfflineSubset.vue create mode 100644 src/shims/woff2-shim.ts create mode 100644 src/subset-client.ts diff --git a/backend/routes/stats.ts b/backend/routes/stats.ts index 0cec8b5..d3f0fd1 100644 --- a/backend/routes/stats.ts +++ b/backend/routes/stats.ts @@ -10,6 +10,7 @@ export async function handleStats(req: Request, _res: Response) { subsetRequests: stats.subsetRequests, subsetCacheHits: stats.subsetCacheHits, totalChars: stats.totalChars, + tempUploads: stats.tempUploads, subsetCacheEntries: subsetCache.size, fontBufferCacheEntries: fontBufferCache.size, }), diff --git a/backend/routes/upload.ts b/backend/routes/upload.ts index d10ca79..e04e7e6 100644 --- a/backend/routes/upload.ts +++ b/backend/routes/upload.ts @@ -1,4 +1,4 @@ -import { jsonResponse, parseUrl } from "../shared"; +import { jsonResponse, parseUrl, stats, markStatsDirty } from "../shared"; import { parseMultipart } from "../multipart"; import { handleTempUpload, handleAdminUpload } from "../upload"; @@ -43,5 +43,9 @@ export async function handleUpload(req: Request, _res: Response) { result = await handleTempUpload({ data: file.data, filename: file.filename }); console.log("[upload] temp result:", result); + if (result.success) { + stats.tempUploads++; + markStatsDirty(); + } return { req, res: jsonResponse(result, result.success ? 200 : 400) }; } diff --git a/backend/shared.ts b/backend/shared.ts index 95cd730..680d924 100644 --- a/backend/shared.ts +++ b/backend/shared.ts @@ -28,6 +28,8 @@ export const stats = { subsetCacheHits: 0, /** 累计裁剪文字字符数(持久化累计) */ totalChars: 0, + /** 临时文件上传次数(持久化累计) */ + tempUploads: 0, }; /** @@ -43,6 +45,7 @@ export async function initStats(): Promise { stats.subsetRequests = persisted.subsetRequests; stats.subsetCacheHits = persisted.subsetCacheHits; stats.totalChars = persisted.totalChars; + stats.tempUploads = persisted.tempUploads; } /** 取出需要持久化的累计计数字段 */ @@ -52,6 +55,7 @@ export function snapshotStats(): PersistedStats { subsetRequests: stats.subsetRequests, subsetCacheHits: stats.subsetCacheHits, totalChars: stats.totalChars, + tempUploads: stats.tempUploads, }; } diff --git a/backend/stats_store.ts b/backend/stats_store.ts index ff51fcb..5a5590c 100644 --- a/backend/stats_store.ts +++ b/backend/stats_store.ts @@ -22,6 +22,8 @@ export interface PersistedStats { subsetRequests: number; subsetCacheHits: number; totalChars: number; + /** 临时文件上传次数(持久化累计) */ + tempUploads: number; } /** 落盘防抖间隔(毫秒):在请求热路径之外的最低频写盘,平衡丢失量与 IO */ @@ -48,11 +50,12 @@ export async function loadPersistedStats(): Promise
- + diff --git a/package.json b/package.json index b813f22..1f01d2e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "typescript": "^7.0.2", "undici": "^8.8.0", "vite": "^8.1.5", + "vite-plugin-commonjs": "^0.10.4", "vite-plugin-pilot": "^1.0.35", "vite-ssg": "^28.3.0", "vitest": "^4.1.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a26afb5..3f5e2d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: vite: specifier: ^8.1.5 version: 8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(terser@5.49.0)(yaml@2.9.0) + vite-plugin-commonjs: + specifier: ^0.10.4 + version: 0.10.4 vite-plugin-pilot: specifier: ^1.0.35 version: 1.0.35(@vue/compiler-dom@3.5.40)(magic-string@1.1.0)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(terser@5.49.0)(yaml@2.9.0)) @@ -379,6 +382,18 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} @@ -1120,6 +1135,10 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1240,6 +1259,9 @@ packages: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} @@ -1265,6 +1287,13 @@ packages: exsolve@1.1.1: resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1274,6 +1303,10 @@ packages: picomatch: optional: true + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1291,6 +1324,10 @@ packages: resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} engines: {node: '>=20.20.0'} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -1321,6 +1358,18 @@ packages: resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} engines: {node: ^22.18.0 || >=24.0.0} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -1454,6 +1503,14 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} @@ -1503,6 +1560,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} @@ -1543,6 +1604,9 @@ packages: quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -1558,6 +1622,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown-plugin-dts@0.27.13: resolution: {integrity: sha512-DeVZJbbB0ajp5q6vABqC8ZCJzxftlxbiV60Bk96GFdQaysGVpgTTVjQu0lUt4Lb+aRCtejfOixtQKDRol7IuVQ==} engines: {node: ^22.18.0 || >=24.11.0} @@ -1592,6 +1660,9 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -1657,6 +1728,10 @@ packages: resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} hasBin: true + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tough-cookie@6.0.2: resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} @@ -1785,6 +1860,12 @@ packages: resolution: {integrity: sha512-WqkT8n3hqizuCu71W3bUzf5fjBmkbXcudsehe/NbxA8PgqoKnSOY5K0Ba2ckg1qaRaSpSz7as/n9K1R9JXjQKg==} engines: {node: '>=18.12.0'} + vite-plugin-commonjs@0.10.4: + resolution: {integrity: sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==} + + vite-plugin-dynamic-import@1.6.0: + resolution: {integrity: sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg==} + vite-plugin-pilot@1.0.35: resolution: {integrity: sha512-ft20yhPqCTHu/d+3+VZusPC5ONzaihDM11jx5cn1WjrNgZzqgmj86P+wsRkN+7lTgMmQWIC1dJ08DNCf1DHhXw==} engines: {node: '>=18.0.0'} @@ -2273,6 +2354,18 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@oxc-project/types@0.127.0': optional: true @@ -2803,6 +2896,10 @@ snapshots: birpc@2.9.0: {} + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + buffer-from@1.1.2: {} cac@6.7.14: {} @@ -2896,6 +2993,8 @@ snapshots: entities@8.0.0: {} + es-module-lexer@1.7.0: {} + es-module-lexer@2.3.1: {} esbuild@0.27.7: @@ -2940,10 +3039,26 @@ snapshots: exsolve@1.1.1: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + fsevents@2.3.3: optional: true @@ -2955,6 +3070,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + hookable@5.5.3: {} hookable@6.1.1: {} @@ -2995,6 +3114,14 @@ snapshots: import-without-cache@0.4.0: {} + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} jsdom@28.1.0: @@ -3132,6 +3259,13 @@ snapshots: mdn-data@2.27.1: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mitt@3.0.1: {} mlly@1.8.2: @@ -3178,6 +3312,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.5: {} pinyin-pro@3.28.2: {} @@ -3236,6 +3372,8 @@ snapshots: quansync@1.0.0: {} + queue-microtask@1.2.3: {} + readdirp@5.0.0: {} relateurl@0.2.7: {} @@ -3244,6 +3382,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + reusify@1.1.0: {} + rolldown-plugin-dts@0.27.13(rolldown@1.2.0)(typescript@7.0.2): dependencies: dts-resolver: 3.0.0 @@ -3322,6 +3462,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.0 '@rolldown/binding-win32-x64-msvc': 1.2.0 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -3379,6 +3523,10 @@ snapshots: dependencies: tldts-core: 7.4.9 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tough-cookie@6.0.2: dependencies: tldts: 7.4.9 @@ -3481,6 +3629,19 @@ snapshots: verkit@0.1.2: {} + vite-plugin-commonjs@0.10.4: + dependencies: + acorn: 8.18.0 + magic-string: 0.30.21 + vite-plugin-dynamic-import: 1.6.0 + + vite-plugin-dynamic-import@1.6.0: + dependencies: + acorn: 8.18.0 + es-module-lexer: 1.7.0 + fast-glob: 3.3.3 + magic-string: 0.30.21 + vite-plugin-pilot@1.0.35(@vue/compiler-dom@3.5.40)(magic-string@1.1.0)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(terser@5.49.0)(yaml@2.9.0)): dependencies: vite: 8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(terser@5.49.0)(yaml@2.9.0) diff --git a/public/webfont-sdk.js b/public/webfont-sdk.js index c0b703d..2bc4c12 100644 --- a/public/webfont-sdk.js +++ b/public/webfont-sdk.js @@ -46,6 +46,29 @@ var WebFont = (function () { /** 待执行的字体加载任务队列(FIFO) */ var fontLoadQueue = []; + /** + * 子集化提供者 —— 决定如何获取增量字体片段 + * + * 默认走 HTTP API(在线模式)。 + * 离线模式下通过 setSubsetProvider 注入本地裁剪函数, + * SDK 负责增量管理(去重、unicode-range),provider 只管"给定文本→返回字体URL"。 + * + * provider 签名:async function(fontName, text, outType) -> { url: string, format: string } + * - url:字体文件的 URL(HTTP URL 或 blob: URL) + * - format:字体格式("woff2" / "truetype") + */ + var subsetProvider = null; + + /** + * 默认在线 provider —— 构造 HTTP API URL + */ + function httpProvider(baseUrl) { + return function (fontName, text, outType) { + var url = baseUrl + "/api?font=" + encodeURIComponent(fontName) + "&text=" + encodeURIComponent(text) + "&outType=" + outType; + return Promise.resolve({ url: url, format: outType === "woff2" ? "woff2" : "truetype" }); + }; + } + /** * 设置最大并发请求数 * @@ -160,33 +183,40 @@ var WebFont = (function () { var text = newChars.join(""); var outType = loader.outType || "woff2"; - var url = baseUrl + "/api?font=" + encodeURIComponent(fontName) + "&text=" + encodeURIComponent(text) + "&outType=" + outType; - var formatStr = outType === "woff2" ? "woff2" : "truetype"; var unicodeRanges = newChars .map(function (c) { return "U+" + c.codePointAt(0).toString(16).padStart(4, "0"); }) .join(", "); - var style = document.createElement("style"); - style.textContent = - '@font-face {\n' + - ' font-family: "' + family + '";\n' + - ' src: url("' + url + '") format("' + formatStr + '");\n' + - ' unicode-range: ' + unicodeRanges + ';\n' + - '}\n'; - document.head.appendChild(style); - loader.injectedStyles.push(style); - /** - * 释放并发槽位的策略: - * - * 优先用 FontFaceSet API 精确追踪字体加载完成; - * 不可用时退化为 setTimeout(3 秒兜底窗口,覆盖绝大多数裁剪+传输时间)。 + * 通过 subsetProvider 获取字体片段 URL。 + * 默认走 HTTP API;离线模式下由本地裁剪 provider 提供 blob: URL。 */ - if (document.fonts && document.fonts.load) { - document.fonts.load(outType === "woff2" ? "16px \"" + family + "\"" : "16px \"" + family + "\"").then(done, function () { done(); }); - } else { - setTimeout(done, 3000); - } + var provider = subsetProvider || httpProvider(baseUrl); + provider(fontName, text, outType).then(function (result) { + var style = document.createElement("style"); + style.textContent = + '@font-face {\n' + + ' font-family: "' + family + '";\n' + + ' src: url("' + result.url + '") format("' + result.format + '");\n' + + ' unicode-range: ' + unicodeRanges + ';\n' + + '}\n'; + document.head.appendChild(style); + loader.injectedStyles.push(style); + + /** + * 释放并发槽位的策略: + * + * 优先用 FontFaceSet API 精确追踪字体加载完成; + * 不可用时退化为 setTimeout(3 秒兜底窗口,覆盖绝大多数裁剪+传输时间)。 + */ + if (document.fonts && document.fonts.load) { + document.fonts.load("16px \"" + family + "\"").then(done, function () { done(); }); + } else { + setTimeout(done, 3000); + } + }).catch(function () { + done(); + }); }); } @@ -486,12 +516,25 @@ var WebFont = (function () { } } + /** + * 注入自定义子集化提供者(离线裁剪等场景) + * + * provider 签名:async function(fontName, text, outType) -> { url: string, format: string } + * - 离线场景:provider 调用本地裁剪函数,生成 blob: URL 返回 + * - 传 null 恢复默认 HTTP 行为 + */ + function setSubsetProvider(provider) { + subsetProvider = provider; + } + return { loadFont: loadFont, observeFont: observeFont, loadText: loadText, disposeAll: disposeAll, /** 设置客户端最大并发字体请求数(默认 4) */ - setMaxConcurrent: setMaxConcurrent + setMaxConcurrent: setMaxConcurrent, + /** 注入自定义子集化提供者(离线裁剪等场景) */ + setSubsetProvider: setSubsetProvider }; })(); diff --git a/src/FontDebugPreview.vue b/src/FontDebugPreview.vue index 8f81645..bf67da4 100644 --- a/src/FontDebugPreview.vue +++ b/src/FontDebugPreview.vue @@ -27,6 +27,16 @@ onMounted(async () => { } }); +/** 面板是否展开 */ +const expanded = ref(false); + +/** 所有字体的字体卡片是否展开(独立折叠) */ +const expandedFonts = ref>({}); + +function toggleFont(name: string) { + expandedFonts.value[name] = !expandedFonts.value[name]; +} + onUnmounted(() => { for (const loader of loaders.values()) loader.dispose(); loaders.clear(); @@ -40,23 +50,34 @@ function fontFamily(font: FontInfo, ot: string) { diff --git a/src/StatsPanel.vue b/src/StatsPanel.vue index b0ac3cd..3ebd3a1 100644 --- a/src/StatsPanel.vue +++ b/src/StatsPanel.vue @@ -119,6 +119,7 @@ onUnmounted(() => { {{ t('requests') }} {{ data.totalRequests }} {{ t('times') }} {{ t('subset') }} {{ data.subsetRequests }} {{ t('times') }} {{ t('chars') }} {{ data.totalChars }} {{ t('charUnit') }} + 上传 {{ data.tempUploads ?? 0 }} 次 {{ t('cacheHit') }} {{ data.subsetRequests > 0 ? ((data.subsetCacheHits / data.subsetRequests) * 100).toFixed(1) : '0.0' }}% diff --git a/src/UploadSection.vue b/src/UploadSection.vue index 26bc866..ad45945 100644 --- a/src/UploadSection.vue +++ b/src/UploadSection.vue @@ -1,5 +1,6 @@ + + diff --git a/src/pages/Home.vue b/src/pages/Home.vue index 989be52..7d363a3 100644 --- a/src/pages/Home.vue +++ b/src/pages/Home.vue @@ -24,6 +24,7 @@ usePageSeo({ }); const router = useRouter(); +/** dev 模式标志 —— 控制 DEV 预览入口可见性 */ const isDev = import.meta.env.DEV; /** 站点 origin —— SSG 构建期为空串,客户端挂载后修正 */ @@ -34,7 +35,6 @@ const showDemoEntry = ref(false); import UploadSection from "../UploadSection.vue"; import StatsPanel from "../StatsPanel.vue"; import SelectorRow from "../FontSelector.vue"; -import FontDebugPreview from "../FontDebugPreview.vue"; import CodeBlock from "../components/CodeBlock.vue"; const text = ref("天地无极,乾坤借法"); @@ -110,7 +110,7 @@ const basicUsageCode = computed(() => { /** JS SDK 代码示例 */ const jsSdkCode = computed(() => { - return ' + +