+
+
{{ $t('common.loading_data') }}
diff --git a/frontend/src/features/vault/composables/useVaultList.js b/frontend/src/features/vault/composables/useVaultList.js
index c0e90fb..ac3c38e 100644
--- a/frontend/src/features/vault/composables/useVaultList.js
+++ b/frontend/src/features/vault/composables/useVaultList.js
@@ -1,5 +1,5 @@
import { ref, shallowRef, computed, watch } from 'vue'
-import { useInfiniteQuery, useQueryClient } from '@tanstack/vue-query'
+import { useInfiniteQuery, useQueryClient, keepPreviousData } from '@tanstack/vue-query'
import { useVaultStore } from '@/features/vault/store/vaultStore'
import { vaultService } from '@/features/vault/service/vaultService'
@@ -35,7 +35,7 @@ export function useVaultList(afterLoadRef = null) {
const firstPage = data.value?.pages?.[0]
const stats = firstPage?.categoryStats || localCategoryStats.value
if (!stats) return []
-
+
return stats.map(s => ({
category: s.category || '',
count: s.count,
@@ -77,6 +77,7 @@ export function useVaultList(afterLoadRef = null) {
// 60 秒内切换回来直接用缓存,不触发逐页重拉(避免多次 PBKDF2 saveData 开销)
// invalidateQueries / resetQueries 调用时会忽略 staleTime,强制立即刷新
staleTime: 60 * 1000,
+ placeholderData: keepPreviousData,
})
// 监听数据变化,同步到本地 vault 引用
@@ -136,13 +137,13 @@ export function useVaultList(afterLoadRef = null) {
if (!searchQuery.value && vaultStore.isUnlocked) {
setTimeout(async () => {
try {
- await vaultStore.saveData({
+ await vaultStore.saveData({
vault: merged,
// 保存当前分类统计原始数据到持久层
categoryStats: newData.pages[0]?.categoryStats || []
})
vaultStore.clearDirty()
- } catch (e) {}
+ } catch (e) { }
}, 0)
}
}, { deep: false })
diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json
index cd26de9..da7035d 100644
--- a/frontend/src/locales/en-US.json
+++ b/frontend/src/locales/en-US.json
@@ -23,6 +23,7 @@
"selected_items": "Selected {count} items",
"select_all_loaded": "Select all loaded",
"loading_data": "Fetching data, please wait...",
+ "resource_loading": "Loading resources...",
"empty_vault": "Empty here, go add your first 2FA account!",
"go_add_vault": "Go to Add Account",
"export_account": "Export",
@@ -131,6 +132,8 @@
"management": "Source Management",
"add_source": "Add",
"fetching_sources": "Fetching backup sources, please wait...",
+ "test_connection": "Test Connection",
+ "connection_normal": "Connection Normal",
"auto_backup_on": "Auto Backup Enabled",
"last_backup": "Last Backup: ",
"never_backed_up": "Never",
@@ -241,7 +244,7 @@
"continue_use_old_pwd": "The system will continue to use the previous auto-backup password.",
"retain_count_label": "Retain Recent Backups",
"retain_zero_tip": "0 means no limits or cleanup for historical backups.",
- "test_connection": "Test Connection",
+ "retain_zero_tip": "0 means no limits or cleanup for historical backups.",
"save": "Save",
"encrypted_backup": "Encrypted Backup",
"backup_security_tip": "Please enter an encryption password to secure the backup file.",
diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json
index 9c9c1a4..aec517c 100644
--- a/frontend/src/locales/zh-CN.json
+++ b/frontend/src/locales/zh-CN.json
@@ -23,6 +23,7 @@
"selected_items": "已选 {count} 项",
"select_all_loaded": "全选已加载",
"loading_data": "数据获取中, 请稍候...",
+ "resource_loading": "资源加载中",
"empty_vault": "空空如也,快去添加你的第一个 2FA 账号吧!",
"go_add_vault": "去添加账号",
"export_account": "账号导出",
@@ -131,6 +132,8 @@
"management": "云端备份管理",
"add_source": "添加备份源",
"fetching_sources": "备份源获取中, 请稍候...",
+ "test_connection": "测试连接",
+ "connection_normal": "连接正常",
"auto_backup_on": "自动备份已开启",
"last_backup": "上次备份: ",
"never_backed_up": "从未",
@@ -241,7 +244,7 @@
"continue_use_old_pwd": "系统将继续使用原有的自动备份密码。",
"retain_count_label": "保留最近份数",
"retain_zero_tip": "填 0 表示不对历史备份做任何限制与清理",
- "test_connection": "测试连接",
+ "retain_zero_tip": "填 0 表示不对历史备份做任何限制与清理",
"save": "保存",
"encrypted_backup": "加密备份",
"backup_security_tip": "请输入加密密码用于保护备份文件。",
diff --git a/frontend/src/shared/utils/asyncHelper.js b/frontend/src/shared/utils/asyncHelper.js
index 15d8d67..54751b3 100644
--- a/frontend/src/shared/utils/asyncHelper.js
+++ b/frontend/src/shared/utils/asyncHelper.js
@@ -19,32 +19,59 @@ export function createAsyncComponent(loader) {
throw err
}
},
-
+
// 加载中占位
loadingComponent: defineComponent({
name: 'AsyncLoading',
setup() {
- const dots = ref('...')
- let timer = null
+ const t = (key) => {
+ try {
+ return i18n.global.t(key)
+ } catch (e) {
+ return key
+ }
+ }
- onMounted(() => {
- timer = setInterval(() => {
- if (dots.value.length >= 10) {
- dots.value = '...'
- } else {
- dots.value += '.'
+ // 使用 CSS 关键帧动画注入(最轻量、无副作用)
+ const injectStyles = () => {
+ if (document.getElementById('async-loading-styles')) return
+ const style = document.createElement('style')
+ style.id = 'async-loading-styles'
+ style.textContent = `
+ @keyframes async-progress-flow {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
}
- }, 400)
- })
+ .async-progress-bar {
+ width: 200px;
+ height: 3px;
+ background: rgba(64, 158, 255, 0.1);
+ border-radius: 4px;
+ overflow: hidden;
+ position: relative;
+ margin-top: 16px;
+ }
+ .async-progress-inner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 50%;
+ height: 100%;
+ background: var(--el-color-primary);
+ border-radius: 4px;
+ animation: async-progress-flow 1.5s infinite ease-in-out;
+ box-shadow: 0 0 8px var(--el-color-primary);
+ }
+ `
+ document.head.appendChild(style)
+ }
- onUnmounted(() => {
- if (timer) clearInterval(timer)
- })
+ onMounted(injectStyles)
- // 使用闭包渲染函数,这是 Vue3 推荐的最稳健方式
return () => h('div', {
style: {
display: 'flex',
+ flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px',
@@ -52,16 +79,19 @@ export function createAsyncComponent(loader) {
backgroundColor: 'transparent'
}
}, [
+ // 文案优化 - 适配多语言
h('div', {
- class: 'is-loading',
style: {
- fontSize: '24px',
- fontFamily: 'monospace',
- color: 'var(--el-color-primary)',
+ fontSize: '16px',
+ color: 'var(--el-text-color-regular)',
fontWeight: '500',
letterSpacing: '1px'
}
- }, `Loading${dots.value}`)
+ }, t('common.resource_loading')),
+ // 线性进度条
+ h('div', { class: 'async-progress-bar' }, [
+ h('div', { class: 'async-progress-inner' })
+ ])
])
}
}),
@@ -72,7 +102,7 @@ export function createAsyncComponent(loader) {
props: ['error'],
setup(props) {
console.error('[AsyncComponent] Rendering error component:', props.error)
-
+
const t = (key) => {
try {
return i18n.global.t(key)
@@ -100,9 +130,9 @@ export function createAsyncComponent(loader) {
timeout: 15000,
// 错误重试逻辑
onError(error, retry, fail, attempts) {
- if (attempts <= 3) {
- console.warn(`[AsyncComponent] Loading failed, retrying (${attempts}/3)...`, error)
- setTimeout(() => retry(), 1000)
+ if (attempts <= 2) {
+ console.warn(`[AsyncComponent] Loading failed, retrying (${attempts}/2)...`, error)
+ setTimeout(() => retry(), 500)
} else {
fail()
}