diff --git a/frontend/package.json b/frontend/package.json
index e74fe48..fb5a612 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -21,6 +21,7 @@
"jspdf": "^4.2.0",
"jsqr": "^1.4.0",
"libsodium-wrappers-sumo": "^0.8.2",
+ "openpgp": "^6.3.0",
"pinia": "^3.0.4",
"qrcode": "^1.5.4",
"scrypt-js": "^3.0.1",
@@ -44,4 +45,4 @@
"overrides": {
"serialize-javascript": "^7.0.3"
}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/app/styles/modules/components/vault.css b/frontend/src/app/styles/modules/components/vault.css
index 03d0ce3..af12b80 100644
--- a/frontend/src/app/styles/modules/components/vault.css
+++ b/frontend/src/app/styles/modules/components/vault.css
@@ -377,41 +377,35 @@
background: var(--el-fill-color-light);
color: var(--el-text-color-regular);
padding: 3px 10px 4px 10px;
- border-radius: 4px;
+ border-radius: 3px;
font-size: 13px;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
- transition: all 0.2s;
- border: 1px solid transparent;
+ border: 1px solid var(--el-fill-color-light);
}
.category-tag:hover {
background: var(--el-fill-color);
}
-/* Final Choice: Option 1 - Subtle Glassmorphism (毛玻璃高亮) */
+/* 分类 Chips 选中状态对齐 .vault-card.is-selected */
.category-tag.is-active {
- background-color: var(--el-color-primary-light-9) !important;
+ background-color: var(--vault-selected-bg) !important;
+ border: 1px solid #409EFF !important;
color: var(--el-color-primary) !important;
- border-color: var(--el-color-primary-light-5) !important;
-}
-
-/* 适配深色模式下的透明度感 */
-html.dark .category-tag.is-active {
- background-color: rgba(64, 158, 255, 0.15) !important;
- border-color: rgba(64, 158, 255, 0.4) !important;
- color: var(--el-color-primary-light-3) !important;
}
.category-tag.is-active .tag-count {
color: var(--el-color-primary) !important;
- opacity: 0.7;
+ opacity: 0.9;
}
-html.dark .category-tag.is-active .tag-count {
- color: var(--el-color-primary-light-3) !important;
+/* 适配深色模式下的透明度感 */
+html.dark .category-tag.is-active {
+ border: none !important;
+ background-color: rgba(64, 158, 255, 0.2) !important;
}
.tag-count {
diff --git a/frontend/src/features/migration/composables/useDataImport.js b/frontend/src/features/migration/composables/useDataImport.js
index c18269e..ba41095 100644
--- a/frontend/src/features/migration/composables/useDataImport.js
+++ b/frontend/src/features/migration/composables/useDataImport.js
@@ -182,7 +182,7 @@ export function useDataImport(emitFn) {
}
}
- if (detectedType === '2fauth_encrypted' || detectedType === 'aegis_encrypted' || detectedType === 'proton_encrypted' || detectedType === '2fas_encrypted' || detectedType === 'ente_encrypted' || detectedType === 'bitwarden_vault_encrypted') {
+ if (detectedType === '2fauth_encrypted' || detectedType === 'aegis_encrypted' || detectedType === 'proton_auth_encrypted' || detectedType === 'proton_pass_pgp' || detectedType === '2fas_encrypted' || detectedType === 'ente_encrypted' || detectedType === 'bitwarden_vault_encrypted') {
importPassword.value = ''
isDialogHandled.value = false
showDecryptDialog.value = true
diff --git a/frontend/src/features/migration/service/dataMigrationService.js b/frontend/src/features/migration/service/dataMigrationService.js
index 3354003..a6b4e1f 100644
--- a/frontend/src/features/migration/service/dataMigrationService.js
+++ b/frontend/src/features/migration/service/dataMigrationService.js
@@ -5,7 +5,8 @@ import { tryParseJSON } from '@/shared/utils/encoding'
import { gaMigrationStrategy } from '@/shared/utils/serializers/gauthStrategy'
import { csvStrategy } from '@/shared/utils/serializers/csvStrategy'
import { aegisStrategy } from '@/shared/utils/serializers/aegisStrategy'
-import protonStrategy from '@/shared/utils/serializers/protonStrategy'
+import { protonAuthStrategy } from '@/shared/utils/serializers/protonAuthStrategy'
+import { protonPassStrategy } from '@/shared/utils/serializers/protonPassStrategy'
import { enteStrategy } from '@/shared/utils/serializers/enteStrategy'
import { migrationError } from '@/shared/utils/errors/migrationError'
import { unzipSync } from 'fflate'
@@ -75,7 +76,9 @@ export const dataMigrationService = {
const firstLine = typeof textContent === 'string' ? textContent.split('\n')[0].toLowerCase() : ''
if (firstLine.includes('login_totp')) return 'bitwarden_vault_csv'
if (firstLine.includes('title') && firstLine.includes('otpauth')) return '1password_csv'
- if (firstLine.includes('otpauth')) return 'bitwarden_auth_csv' // Bitwarden Auth standalone typically exports simpler CSV
+ if (firstLine.includes('otpauth')) return 'bitwarden_auth_csv'
+ if (firstLine.includes('totp') && firstLine.includes('vault') && firstLine.includes('createtime')) return 'proton_pass_csv'
+ if (firstLine.includes('otpurl') && firstLine.includes('title') && firstLine.includes('username')) return 'dashlane_csv'
return 'generic_csv'
}
@@ -99,7 +102,7 @@ export const dataMigrationService = {
if (json.schemaVersion && Array.isArray(json.services)) return '2fas'
if (json.version === 1 && json.db && typeof json.db === 'object' && Array.isArray(json.db.entries)) return 'aegis' // Aegis unencrypted
if (json.version === 1 && json.header && json.db && typeof json.db === 'string') return 'aegis_encrypted'
- if (json.version === 1 && typeof json.salt === 'string' && typeof json.content === 'string') return 'proton_encrypted'
+ if (json.version === 1 && typeof json.salt === 'string' && typeof json.content === 'string') return 'proton_auth_encrypted'
// Ente Auth 加密导出:同时包含 kdfParams 和 encryptedData 两个关键字段
if (json.kdfParams && typeof json.encryptedData === 'string') return 'ente_encrypted'
// Bitwarden password-protected export (Vault)
@@ -107,6 +110,10 @@ export const dataMigrationService = {
}
}
+ if (typeof textContent === 'string' && textContent.includes('-----BEGIN PGP MESSAGE-----')) {
+ return 'proton_pass_pgp'
+ }
+
if (filename) {
const ext = filename.toLowerCase()
if (ext.endsWith('.2fas')) return '2fas'
@@ -1091,8 +1098,17 @@ export const dataMigrationService = {
type = 'raw'
}
- if (type === 'proton_encrypted') {
- rawVault = await protonStrategy.parse(content, password)
+ if (type === 'proton_auth_encrypted') {
+ if (!password) throw new migrationError('导入 Proton Authenticator 备份需要密码', 'MISSING_PASSWORD')
+ rawVault = await protonAuthStrategy.parse(content, password)
+ type = 'raw'
+ content = JSON.stringify(rawVault)
+ password = undefined
+ }
+
+ if (type === 'proton_pass_pgp') {
+ if (!password) throw new migrationError('导入 Proton Pass 备份需要密码', 'MISSING_PASSWORD')
+ rawVault = await protonPassStrategy.parse(content, password)
type = 'raw'
content = JSON.stringify(rawVault)
password = undefined
@@ -1272,7 +1288,7 @@ export const dataMigrationService = {
}))
}
}
- else if (type === 'bitwarden_vault_csv' || type === 'bitwarden_auth_csv' || type === '1password_csv' || type === 'generic_csv') rawVault = csvStrategy.parseCsv(content)
+ else if (type === 'bitwarden_vault_csv' || type === 'bitwarden_auth_csv' || type === '1password_csv' || type === 'proton_pass_csv' || type === 'dashlane_csv' || type === 'generic_csv') rawVault = csvStrategy.parseCsv(content)
return rawVault.map(acc => {
if (typeof acc.account === 'string' && acc.account.includes(':')) {
diff --git a/frontend/src/features/migration/views/dataImport.vue b/frontend/src/features/migration/views/dataImport.vue
index 274c9d3..689ff95 100644
--- a/frontend/src/features/migration/views/dataImport.vue
+++ b/frontend/src/features/migration/views/dataImport.vue
@@ -61,7 +61,9 @@
Bitwarden Vault (.json/.csv)
- {{ $t('migration.onepassword_format') }}
+ 1Password (.1pux/.csv)
+ Proton Pass (.pgp/.csv)
+ Dashlane (.csv)
@@ -99,7 +101,7 @@
- {{ $t('migration.ga_ms_import_guide') }}
+ {{ $t('migration.import_guide') }}
@@ -149,7 +151,8 @@
-
+
+
@@ -187,7 +190,9 @@ import iconAegis from '@/shared/components/icons/iconAegis.vue'
import iconGoogleAuth from '@/shared/components/icons/iconGoogleAuth.vue'
import iconBitwarden from '@/shared/components/icons/iconBitwarden.vue'
import iconMicrosoftAuth from '@/shared/components/icons/iconMicrosoftAuth.vue'
+import iconDashlanePass from '@/shared/components/icons/iconDashlanePass.vue'
import iconProtonAuth from '@/shared/components/icons/iconProtonAuth.vue'
+import iconProtonPass from '@/shared/components/icons/iconProtonPass.vue'
import iconEnte from '@/shared/components/icons/iconEnte.vue'
import icon1Password from '@/shared/components/icons/icon1Password.vue'
import iconLastPassAuth from '@/shared/components/icons/iconLastPassAuth.vue'
diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json
index c93539e..7671aa7 100644
--- a/frontend/src/locales/en-US.json
+++ b/frontend/src/locales/en-US.json
@@ -15,6 +15,7 @@
"update_available": "Update Available",
"pwa_update_ready": "🎉 New Cloudflare Worker deployment is ready!",
"refresh_now": "Refresh Now",
+ "loading_failed": "Component loading failed, please check your network connection",
"copy_success": "Copy success",
"copy_fail": "Copy failed",
"offline_mode": "Network disconnected, the application is currently in offline cache mode",
@@ -371,16 +372,17 @@
"center_desc": "Support restoring data from various 2FA apps or system backups.",
"auto_identify_tip": "💡 Automatically recognize file types",
"drag_drop_tip": "Drag backup files or migration QR screenshots here, or click to upload",
- "support_desc": "Supported Formats",
+ "support_desc": "Supported List",
"ga_auth_desc": "Export 'Transfer QR code' in the App and upload the screenshot (.png / .jpg)",
"ms_auth_desc": "Microsoft Authenticator (Android Only)",
"ms_auth_detail": "Export the following 3 files from Android and upload them together",
- "ga_ms_import_guide": "Google / Microsoft Import Guide",
+ "import_guide": "Import Guide",
"batch_import_processing": "📦 Batch Import Processing",
"batch_progress": "{processed} / {total} tasks completed",
"decrypt_backup_title": "🔓 Decrypt Backup File",
"detect_aegis": "Aegis encrypted backup detected",
- "detect_proton": "Proton Auth encrypted backup detected",
+ "detect_proton_auth": "Proton Authenticator encrypted backup detected",
+ "detect_proton_pass": "Proton Pass encrypted backup detected (.pgp)",
"detect_2fas": "2FAS Auth encrypted backup detected",
"detect_ente": "Ente Auth encrypted backup detected",
"detect_bitwarden_vault": "Bitwarden Vault encrypted backup detected",
@@ -390,7 +392,6 @@
"confirm_decrypt_import": "Confirm Decrypt & Import",
"system_backup_format": "System Backup",
"password_manager_format": "Password Manager (Vault)",
- "onepassword_format": "1Password (.1pux/.csv)",
"encrypted_backup_json": "Encrypted Backup (.json)",
"plaintext_backup_json": "Plaintext Backup (.json)",
"mobile_app_format": "Mobile 2FA Apps",
diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json
index 62efed9..d841c95 100644
--- a/frontend/src/locales/zh-CN.json
+++ b/frontend/src/locales/zh-CN.json
@@ -15,6 +15,7 @@
"update_available": "更新可用",
"pwa_update_ready": "🎉 Cloudflare Worker 新部署已就绪!",
"refresh_now": "立即刷新",
+ "loading_failed": "组件加载失败,请检查网络连接",
"copy_success": "复制成功",
"copy_fail": "复制失败",
"offline_mode": "网络已断开,当前应用正处于离线缓存模式",
@@ -371,16 +372,17 @@
"center_desc": "支持从各类 2FA 软件或本系统的备份文件中恢复数据。",
"auto_identify_tip": "💡 自动识别文件类型导入",
"drag_drop_tip": "将备份文件或迁移二维码截图拖到此处,或 点击上传",
- "support_desc": "支持情况说明",
+ "support_desc": "支持列表",
"ga_auth_desc": "请在 App 内导出\"迁移二维码\",将截图直接上传 (.png / .jpg)",
"ms_auth_desc": "Microsoft Authenticator (仅支持 Android)",
"ms_auth_detail": "Android 手机导出以下3个文件后,同时选中上传即可导入",
- "ga_ms_import_guide": "Google / Microsoft Auth 导入指南",
+ "import_guide": "导入指南",
"batch_import_processing": "📦 批量导入处理中",
"batch_progress": "{processed} / {total} 任务已完成",
"decrypt_backup_title": "🔓 解密备份文件",
"detect_aegis": "检测到 Aegis 加密备份",
- "detect_proton": "检测到 Proton Auth 加密备份",
+ "detect_proton_auth": "检测到 Proton Authenticator 加密备份",
+ "detect_proton_pass": "检测到 Proton Pass 加密备份 (.pgp)",
"detect_2fas": "检测到 2FAS Auth 加密备份",
"detect_ente": "检测到 Ente Auth 加密备份",
"detect_bitwarden_vault": "检测到 Bitwarden Vault 加密备份",
@@ -390,7 +392,6 @@
"confirm_decrypt_import": "确认解密并导入",
"system_backup_format": "本系统备份",
"password_manager_format": "密码管理器 (Vault)",
- "onepassword_format": "1Password (.1pux/.csv)",
"encrypted_backup_json": "加密备份 (.json)",
"plaintext_backup_json": "明文备份 (.json)",
"mobile_app_format": "移动端 2FA App",
diff --git a/frontend/src/shared/components/icons/iconDashlanePass.vue b/frontend/src/shared/components/icons/iconDashlanePass.vue
new file mode 100644
index 0000000..7434bbd
--- /dev/null
+++ b/frontend/src/shared/components/icons/iconDashlanePass.vue
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/shared/components/icons/iconProtonPass.vue b/frontend/src/shared/components/icons/iconProtonPass.vue
new file mode 100644
index 0000000..39cabb9
--- /dev/null
+++ b/frontend/src/shared/components/icons/iconProtonPass.vue
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/frontend/src/shared/utils/asyncHelper.js b/frontend/src/shared/utils/asyncHelper.js
index 125c718..5d0e2b6 100644
--- a/frontend/src/shared/utils/asyncHelper.js
+++ b/frontend/src/shared/utils/asyncHelper.js
@@ -1,5 +1,6 @@
-import { defineAsyncComponent, h } from 'vue'
+import { defineAsyncComponent, h, ref, onMounted, onUnmounted } from 'vue'
import { ElEmpty, ElButton } from 'element-plus'
+import { i18n } from '@/locales'
/**
* 增强的异步组件工厂函数
@@ -10,14 +11,37 @@ export function createAsyncComponent(loader) {
loader,
// 加载中占位
loadingComponent: {
- render() {
- return h('div', {
+ setup() {
+ const dots = ref('...')
+ let timer = null
+
+ onMounted(() => {
+ timer = setInterval(() => {
+ if (dots.value.length >= 10) {
+ dots.value = '...'
+ } else {
+ dots.value += '.'
+ }
+ }, 400)
+ })
+
+ onUnmounted(() => {
+ if (timer) clearInterval(timer)
+ })
+
+ return () => h('div', {
style: 'display: flex; justify-content: center; align-items: center; min-height: 400px; width: 100%;'
}, [
h('div', {
class: 'is-loading',
- style: 'font-size: 24px;'
- }, 'Loading...')
+ style: `
+ font-size: 24px;
+ font-family: monospace;
+ color: var(--el-color-primary);
+ font-weight: 500;
+ letter-spacing: 1px;
+ `
+ }, `Loading${dots.value}`)
])
}
},
@@ -25,14 +49,15 @@ export function createAsyncComponent(loader) {
errorComponent: {
props: ['error'],
render() {
+ const { t } = i18n.global
return h('div', {
style: 'padding: 40px; text-align: center;'
}, [
- h(ElEmpty, { description: '组件加载失败,请检查网络连接' }, {
+ h(ElEmpty, { description: t('common.loading_failed') }, {
default: () => h(ElButton, {
type: 'primary',
onClick: () => window.location.reload()
- }, '刷新页面')
+ }, t('common.refresh_now'))
})
])
}
diff --git a/frontend/src/shared/utils/serializers/csvStrategy.js b/frontend/src/shared/utils/serializers/csvStrategy.js
index 3852581..9973914 100644
--- a/frontend/src/shared/utils/serializers/csvStrategy.js
+++ b/frontend/src/shared/utils/serializers/csvStrategy.js
@@ -66,9 +66,11 @@ export const csvStrategy = {
const isBitwardenVault = headers.includes('login_totp')
const isBitwardenAuth = headers.includes('otpauth') && !headers.includes('title')
const is1Password = headers.includes('otpauth') && headers.includes('title')
+ const isProtonPass = headers.includes('totp') && headers.includes('vault') && headers.includes('createtime')
+ const isDashlane = headers.includes('otpurl') && headers.includes('title') && headers.includes('username')
const isGeneric = headers.includes('issuer') || headers.includes('secret') || headers.includes('name')
- if (!isBitwardenVault && !isBitwardenAuth && !is1Password && !isGeneric) return []
+ if (!isBitwardenVault && !isBitwardenAuth && !is1Password && !isProtonPass && !isDashlane && !isGeneric) return []
for (let i = 1; i < lines.length; i++) {
const row = this._splitCsvLine(lines[i])
@@ -114,6 +116,28 @@ export const csvStrategy = {
rawVault.push(accData)
}
}
+ } else if (isProtonPass) {
+ const totpStr = (rowData['totp'] || '').trim()
+ if (totpStr && totpStr.startsWith('otpauth://')) {
+ const accData = parseOtpUri(totpStr)
+ if (accData) {
+ accData.service = rowData['name'] || accData.service
+ accData.account = rowData['username'] || accData.account
+ accData.category = rowData['vault'] || ''
+ rawVault.push(accData)
+ }
+ }
+ } else if (isDashlane) {
+ const totpStr = (rowData['otpurl'] || '').trim()
+ if (totpStr && totpStr.startsWith('otpauth://')) {
+ const accData = parseOtpUri(totpStr)
+ if (accData) {
+ accData.service = rowData['title'] || accData.service
+ accData.account = rowData['username'] || accData.account
+ accData.category = rowData['category'] || ''
+ rawVault.push(accData)
+ }
+ }
} else {
const secret = rowData['secret'] || ''
if (secret) {
diff --git a/frontend/src/shared/utils/serializers/protonStrategy.js b/frontend/src/shared/utils/serializers/protonAuthStrategy.js
similarity index 89%
rename from frontend/src/shared/utils/serializers/protonStrategy.js
rename to frontend/src/shared/utils/serializers/protonAuthStrategy.js
index 97b6e8c..cc5886f 100644
--- a/frontend/src/shared/utils/serializers/protonStrategy.js
+++ b/frontend/src/shared/utils/serializers/protonAuthStrategy.js
@@ -1,8 +1,12 @@
import argon2 from 'argon2-browser/dist/argon2-bundled.min.js';
import { parseOtpUri } from '@/shared/utils/totp';
-export default {
- name: 'Proton Pass (.json)',
+/**
+ * Proton Authenticator (.json) Strategy
+ * Uses Argon2id + AES-GCM
+ */
+export const protonAuthStrategy = {
+ name: 'Proton Authenticator (.json)',
fileType: 'application/json, text/plain',
async parse(fileContent, password) {
@@ -52,8 +56,6 @@ export default {
distPath: '/'
});
- // 2. Decrypt via Web Crypto API
- // argon2-browser returns Uint8Array in argon2Result.hash
const keyMaterial = argon2Result.hash;
const cryptoKey = await window.crypto.subtle.importKey(
@@ -64,7 +66,7 @@ export default {
['decrypt']
);
- // AAD defined in Rust
+ // AAD defined in Rust source of Proton Auth
const aad = new TextEncoder().encode('proton.authenticator.export.v1');
const decryptedBuffer = await window.crypto.subtle.decrypt(
@@ -81,7 +83,7 @@ export default {
const decryptedString = new TextDecoder().decode(decryptedBuffer);
const decryptedJson = JSON.parse(decryptedString);
- // 3. Parse items
+ // Parse items
const entries = decryptedJson.entries || [];
const results = [];
@@ -97,8 +99,7 @@ export default {
return results;
} catch (err) {
- console.error('Proton decryption failed:', err);
- // Must throw this precise message so UI prompts for password again if wrong
+ console.error('Proton Authenticator decryption failed:', err);
throw new Error('INVALID_FORMAT_OR_PASSWORD');
}
}
diff --git a/frontend/src/shared/utils/serializers/protonPassStrategy.js b/frontend/src/shared/utils/serializers/protonPassStrategy.js
new file mode 100644
index 0000000..70838d9
--- /dev/null
+++ b/frontend/src/shared/utils/serializers/protonPassStrategy.js
@@ -0,0 +1,55 @@
+import { parseOtpUri } from '@/shared/utils/totp';
+import * as openpgp from 'openpgp';
+
+/**
+ * Proton Pass (.pgp) Strategy
+ * Uses OpenPGP (Armored Message)
+ */
+export const protonPassStrategy = {
+ name: 'Proton Pass (.pgp)',
+ fileType: 'text/plain',
+
+ async parse(armoredContent, password) {
+ try {
+ const message = await openpgp.readMessage({
+ armoredMessage: armoredContent
+ });
+
+ const { data: decrypted } = await openpgp.decrypt({
+ message,
+ passwords: [password],
+ format: 'utf8'
+ });
+
+ const decryptedJson = JSON.parse(decrypted);
+ const results = [];
+
+ const vaults = decryptedJson.vaults || {};
+
+ for (const vaultId in vaults) {
+ const vault = vaults[vaultId];
+ const items = vault.items || [];
+ for (const item of items) {
+ const data = item.data || {};
+ const content = data.content || {};
+ const metadata = data.metadata || {};
+
+ if (content.totpUri) {
+ const parsed = parseOtpUri(content.totpUri);
+ if (parsed) {
+ // Use metadata name if available for service
+ if (metadata.name) parsed.service = metadata.name;
+ if (content.itemUsername) parsed.account = content.itemUsername;
+ results.push(parsed);
+ }
+ }
+ }
+ }
+
+ return results;
+ } catch (err) {
+ console.error('Proton Pass PGP decryption failed:', err);
+ throw new Error('INVALID_FORMAT_OR_PASSWORD');
+ }
+ }
+};
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 86818a6..902cdbc 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -84,7 +84,8 @@ export default defineConfig({
'**/assets/pdf-utils*.js',
'**/assets/compression-utils*.js',
'**/assets/dataImport*.js',
- '**/assets/dataMigrationService*.js'
+ '**/assets/dataMigrationService*.js',
+ '**/assets/openpgp*.js'
],
cleanupOutdatedCaches: true,
clientsClaim: true,
@@ -117,7 +118,7 @@ export default defineConfig({
},
{
// 为被 globIgnores 忽略的大体积工具 JS chunk 配置动态缓存
- urlPattern: /assets\/(wa-sqlite|argon2|hash-wasm|libsodium-wrappers|sql|jsQR|pdf-utils|compression-utils|dataImport|dataMigrationService).*\.js$/i,
+ urlPattern: /assets\/(wa-sqlite|argon2|hash-wasm|libsodium-wrappers|sql|jsQR|pdf-utils|compression-utils|dataImport|dataMigrationService|openpgp).*\.js$/i,
handler: 'CacheFirst',
options: {
cacheName: 'lazy-tools-js-cache',
@@ -190,6 +191,9 @@ export default defineConfig({
// 二维码处理相关 (体积较大且仅特定页面使用)
if (id.includes('/node_modules/qrcode/') || id.includes('/node_modules/jsqr/')) return 'qr-utils'
+ // 加密相关:OpenPGP (体积大且仅特定导入场景使用)
+ if (id.includes('/node_modules/openpgp/')) return 'openpgp'
+
// 大型 UI 支撑库
if (id.includes('/node_modules/@tanstack/')) return 'tanstack-query'