From e9a2060ca1c853aac137ab0928cefbedbafc1821 Mon Sep 17 00:00:00 2001 From: nap0o Date: Tue, 17 Mar 2026 15:12:03 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E4=BF=AE=E6=94=B9=E4=B8=80=E4=BA=9BUI?= =?UTF-8?q?=E5=92=8CUE,=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- .../app/styles/modules/components/backup.css | 35 +++++++-- .../styles/modules/components/migration.css | 2 +- .../styles/modules/components/security.css | 4 + .../app/styles/modules/components/shared.css | 4 + .../app/styles/modules/components/tools.css | 5 ++ .../app/styles/modules/components/vault.css | 44 +++++++++++ .../backup/components/backupSettings.vue | 15 +++- .../backup/composables/useBackupProviders.js | 55 +++++++++---- .../features/migration/views/dataImport.vue | 2 +- .../features/vault/components/vaultList.vue | 21 ++++- .../vault/composables/useVaultList.js | 9 ++- frontend/src/locales/en-US.json | 5 +- frontend/src/locales/zh-CN.json | 5 +- frontend/src/shared/utils/asyncHelper.js | 78 +++++++++++++------ 15 files changed, 228 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 9767479..77b0e58 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,7 @@ yarn-error.log* .wrangler/ **/.wrangler/ # Gemini/AI assistant state -.gemini/ +.antigravity # Act (local GitHub Actions) configuration and state .actrc .event diff --git a/frontend/src/app/styles/modules/components/backup.css b/frontend/src/app/styles/modules/components/backup.css index f4ba63d..2c7e737 100644 --- a/frontend/src/app/styles/modules/components/backup.css +++ b/frontend/src/app/styles/modules/components/backup.css @@ -1,8 +1,4 @@ /* Backup Feature Styles */ -.backup-container { - padding: 10px; -} - .backup-header-actions { display: flex; justify-content: space-between; @@ -13,6 +9,7 @@ .backup-provider-card { height: 100%; transition: transform 0.2s; + border-radius: 8px; } .backup-provider-card:hover { @@ -41,6 +38,14 @@ .backup-action-buttons { display: flex; gap: 10px; + flex-wrap: wrap; +} + +@media (max-width: 768px) { + .backup-action-buttons .el-button { + margin-left: 0 !important; + margin-right: 8px !important; + } } .backup-form-tip { @@ -115,15 +120,17 @@ text-align: center; } -/* Global Fix for Element Plus Icon & Loading Alignment */ -.el-button { +/* Global Fix for Element Plus Icon & Loading Alignment in Backup Feature */ +.data-backup-wrapper .el-button, +.backup-container .el-button { display: inline-flex !important; align-items: center !important; justify-content: center !important; vertical-align: middle; } -.el-button .el-icon { +.data-backup-wrapper .el-button .el-icon, +.backup-container .el-button .el-icon { display: inline-flex !important; align-items: center !important; justify-content: center !important; @@ -263,4 +270,18 @@ .dark .btn-dropbox-auth:hover { background-color: #00368c; +} + +/* Connection Test Success State Fix */ +.backup-action-buttons .el-button.btn-test-success.is-disabled { + opacity: 1 !important; + color: var(--el-color-primary) !important; + border-color: var(--el-color-primary-light-7) !important; + background-color: var(--el-color-primary-light-9) !important; + cursor: default !important; +} + +.dark .backup-action-buttons .el-button.btn-test-success.is-disabled { + background-color: rgba(64, 158, 255, 0.1) !important; + border-color: rgba(64, 158, 255, 0.2) !important; } \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/migration.css b/frontend/src/app/styles/modules/components/migration.css index da679ae..63ac24e 100644 --- a/frontend/src/app/styles/modules/components/migration.css +++ b/frontend/src/app/styles/modules/components/migration.css @@ -2,7 +2,7 @@ .migration-export-groups { max-width: 100%; - margin: 0 auto; + margin: 10px auto; display: flex; flex-direction: column; gap: 20px; diff --git a/frontend/src/app/styles/modules/components/security.css b/frontend/src/app/styles/modules/components/security.css index 0e1d6bc..3b2f52c 100644 --- a/frontend/src/app/styles/modules/components/security.css +++ b/frontend/src/app/styles/modules/components/security.css @@ -1,3 +1,7 @@ +.security-wrapper .tab-card-wrapper { + padding-bottom: 40px; +} + .security-wrapper .empty-state { padding: 60px 0; } diff --git a/frontend/src/app/styles/modules/components/shared.css b/frontend/src/app/styles/modules/components/shared.css index 2fa9d13..3d01753 100644 --- a/frontend/src/app/styles/modules/components/shared.css +++ b/frontend/src/app/styles/modules/components/shared.css @@ -100,6 +100,10 @@ } /* 3. QrScanner Component */ +.add-vault-wrapper .tab-card-wrapper { + padding-bottom: 40px; +} + .qr-scanner-container { width: 100%; } diff --git a/frontend/src/app/styles/modules/components/tools.css b/frontend/src/app/styles/modules/components/tools.css index 3c598c1..40a3658 100644 --- a/frontend/src/app/styles/modules/components/tools.css +++ b/frontend/src/app/styles/modules/components/tools.css @@ -1,6 +1,11 @@ /* ========================================= 4. UtilityTools 组件 (工具箱) ========================================= */ + +.tools-wrapper .tab-card-wrapper { + padding-bottom: 40px; +} + .tools-header { display: flex; align-items: center; diff --git a/frontend/src/app/styles/modules/components/vault.css b/frontend/src/app/styles/modules/components/vault.css index af12b80..6636d10 100644 --- a/frontend/src/app/styles/modules/components/vault.css +++ b/frontend/src/app/styles/modules/components/vault.css @@ -411,4 +411,48 @@ html.dark .category-tag.is-active { .tag-count { margin-left: 5px; font-size: 11px; +} + +/* --- 分类标签加载动效 (对齐 asyncHelper.js 的朴素风格) --- */ +.category-tag { + position: relative; + /* 移除 overflow: hidden,由下方的 track 容器单独负责裁剪,确保文字显示完全 */ +} + +.tag-loading-track { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: rgba(var(--el-color-primary-rgb, 64, 158, 255), 0.1); + border-radius: 0 0 3px 3px; + overflow: hidden; + /* 在这里进行裁剪 */ + z-index: 10; +} + +.tag-loading-bar { + position: absolute; + top: 0; + left: 0; + width: 40%; + height: 100%; + background: var(--el-color-primary); + border-radius: 2px; + animation: plain-loading-flow 1.5s infinite ease-in-out; +} + +@keyframes plain-loading-flow { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(250%); + } +} + +.vault-list-wrapper { + transition: opacity 0.3s ease; } \ No newline at end of file diff --git a/frontend/src/features/backup/components/backupSettings.vue b/frontend/src/features/backup/components/backupSettings.vue index 99ccb67..c72c73d 100644 --- a/frontend/src/features/backup/components/backupSettings.vue +++ b/frontend/src/features/backup/components/backupSettings.vue @@ -47,6 +47,18 @@

+ + + + + {{ testResults[provider.id] === 'success' ? $t('backup.connection_normal') : $t('backup.test_connection') }} + @@ -554,6 +566,7 @@ const layoutStore = useLayoutStore() const { providers, isLoading, showConfigDialog, isEditing, isTesting, isSaving, + testingProviderIds, testResults, isEditingWebdavPwd, isEditingS3Secret, isEditingTelegramToken, isEditingEmailPwd, form, isAutoBackupPasswordSaved, shouldUseExistingAutoBackupPassword, fetchProviders, openAddDialog, editProvider, testConnection, saveProvider, deleteProvider, diff --git a/frontend/src/features/backup/composables/useBackupProviders.js b/frontend/src/features/backup/composables/useBackupProviders.js index 1c98ebe..4405b8f 100644 --- a/frontend/src/features/backup/composables/useBackupProviders.js +++ b/frontend/src/features/backup/composables/useBackupProviders.js @@ -20,6 +20,9 @@ export function useBackupProviders() { const isEditing = ref(false) const isTesting = ref(false) const isSaving = ref(false) + const testingProviderIds = ref({}) // { providerId: true } + const testResults = ref({}) // { providerId: 'success' | 'error' } + const isEditingWebdavPwd = ref(false) const isEditingS3Secret = ref(false) const isEditingTelegramToken = ref(false) @@ -174,11 +177,22 @@ export function useBackupProviders() { return null } - const testConnection = async () => { - const error = validateForm() - if (error) return ElMessage.warning(error) + const testConnection = async (provider = null) => { + const isIndividual = !!provider + const targetData = isIndividual ? provider : form.value + const targetId = isIndividual ? provider.id : (isEditing.value ? currentProviderId.value : null) + + if (!isIndividual) { + const error = validateForm() + if (error) return ElMessage.warning(error) + } + + if (isIndividual) { + testingProviderIds.value[targetId] = true + } else { + isTesting.value = true + } - isTesting.value = true; // Only reset error states, keep 'success' state if it exists if (authStatusGoogle.value === 'error') authStatusGoogle.value = null; authErrorMessageGoogle.value = ''; @@ -191,14 +205,19 @@ export function useBackupProviders() { try { const res = await backupService.testConnection( - form.value.type, - form.value.config, - isEditing.value ? currentProviderId.value : null + targetData.type, + targetData.config, + targetId ) if (res.success) { ElMessage.success(t('backup.test_success')) + + if (isIndividual) { + testResults.value[targetId] = 'success' + } + // Ensure the success status is reflected in the UI for OAuth providers - const type = form.value.type + const type = targetData.type if (type === 'gdrive') authStatusGoogle.value = 'success' else if (type === 'onedrive') authStatusMicrosoft.value = 'success' else if (type === 'baidu') authStatusBaidu.value = 'success' @@ -211,11 +230,13 @@ export function useBackupProviders() { // Listen for the standard project-level OAuth revocation signal if (errMsg.includes('oauth_token_revoked')) { - const type = form.value.type; + const type = targetData.type; console.error(`[OAuth Auth Check] The authorization token for ${type} is revoked or expired. Attempting UI reset...`, e); // Reset the internal token holder - form.value.config.refreshToken = ''; + if (!isIndividual) { + form.value.config.refreshToken = ''; + } // Switch UI back to authentication state if (type === 'gdrive') { @@ -233,13 +254,19 @@ export function useBackupProviders() { } // If we are editing an existing provider, update the global store to reflect the state - if (isEditing.value && currentProviderId.value) { - backupStore.markAsRevoked(currentProviderId.value); + if (targetId) { + backupStore.markAsRevoked(targetId); } } else { ElMessage.error(t(`api_errors.${errMsg}`) || rawMsg); } - } finally { isTesting.value = false } + } finally { + if (isIndividual) { + delete testingProviderIds.value[targetId] + } else { + isTesting.value = false + } + } } const saveProvider = async () => { @@ -493,6 +520,8 @@ export function useBackupProviders() { isEditing, isTesting, isSaving, + testingProviderIds, + testResults, isEditingWebdavPwd, isEditingS3Secret, isEditingTelegramToken, diff --git a/frontend/src/features/migration/views/dataImport.vue b/frontend/src/features/migration/views/dataImport.vue index 6b666f3..10ac4e0 100644 --- a/frontend/src/features/migration/views/dataImport.vue +++ b/frontend/src/features/migration/views/dataImport.vue @@ -35,7 +35,7 @@ -
+
diff --git a/frontend/src/features/vault/components/vaultList.vue b/frontend/src/features/vault/components/vaultList.vue index b10de41..c0e50cb 100644 --- a/frontend/src/features/vault/components/vaultList.vue +++ b/frontend/src/features/vault/components/vaultList.vue @@ -36,29 +36,42 @@
{{ $t('common.all') }} ({{ absoluteTotalItems }}) + +
+
+
{{ stat.category || $t('common.uncategorized') }} ({{ stat.count }}) +
+
+
- -
+ +

{{ $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() }