style: 修改一些UI和UE,提升用户体验
This commit is contained in:
1 parent
06a6d0820a
commit
e9a2060ca1
15 files changed
+228
-58
No files matched your search
+1
-1
@@ -56,7 +56,7 @@ yarn-error.log*
|
||||
.wrangler/
|
||||
**/.wrangler/
|
||||
# Gemini/AI assistant state
|
||||
.gemini/
|
||||
.antigravity
|
||||
# Act (local GitHub Actions) configuration and state
|
||||
.actrc
|
||||
.event
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
.migration-export-groups {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
margin: 10px auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.security-wrapper .tab-card-wrapper {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.security-wrapper .empty-state {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
@@ -100,6 +100,10 @@
|
||||
}
|
||||
|
||||
/* 3. QrScanner Component */
|
||||
.add-vault-wrapper .tab-card-wrapper {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.qr-scanner-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/* =========================================
|
||||
4. UtilityTools 组件 (工具箱)
|
||||
========================================= */
|
||||
|
||||
.tools-wrapper .tab-card-wrapper {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.tools-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -47,6 +47,18 @@
|
||||
</p>
|
||||
|
||||
<div class="backup-action-buttons">
|
||||
<el-button
|
||||
type="primary" plain size="small"
|
||||
:loading="!!testingProviderIds[provider.id]"
|
||||
:disabled="testResults[provider.id] === 'success'"
|
||||
:class="{ 'btn-test-success': testResults[provider.id] === 'success' }"
|
||||
@click="testConnection(provider)"
|
||||
>
|
||||
<el-icon v-if="testResults[provider.id] === 'success'" color="#67C23A" style="margin-right: 4px;">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
{{ testResults[provider.id] === 'success' ? $t('backup.connection_normal') : $t('backup.test_connection') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success" plain size="small"
|
||||
:loading="checkingBackupProviderId === provider.id"
|
||||
@@ -471,7 +483,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="testConnection" :loading="isTesting">{{ $t('backup.test_connection') }}</el-button>
|
||||
<el-button @click="() => testConnection()" :loading="isTesting">{{ $t('backup.test_connection') }}</el-button>
|
||||
<el-button type="primary" @click="saveProvider" :loading="isSaving">{{ $t('backup.save') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</span>
|
||||
</el-divider>
|
||||
|
||||
<div class="compatibility-container mt-20">
|
||||
<div class="compatibility-container mt-20 mb-10">
|
||||
<div class="ecosystem-groups">
|
||||
<!-- 组1: 本系统 -->
|
||||
<div class="ecosystem-group-card">
|
||||
|
||||
@@ -36,29 +36,42 @@
|
||||
<div class="category-chips">
|
||||
<div
|
||||
class="category-tag"
|
||||
:class="{ 'is-active': selectedCategory === '' }"
|
||||
:class="{
|
||||
'is-active': selectedCategory === '',
|
||||
'is-loading': isFetching && selectedCategory === '' && !isFetchingNextPage
|
||||
}"
|
||||
@click="selectedCategory = ''"
|
||||
>
|
||||
{{ $t('common.all') }}
|
||||
<span class="tag-count">({{ absoluteTotalItems }})</span>
|
||||
<!-- 独立加载轨道,避免截断文字 -->
|
||||
<div v-if="isFetching && selectedCategory === '' && !isFetchingNextPage" class="tag-loading-track">
|
||||
<div class="tag-loading-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="stat in categoryStats"
|
||||
:key="stat.id"
|
||||
class="category-tag"
|
||||
:class="{ 'is-active': selectedCategory === stat.id }"
|
||||
:class="{
|
||||
'is-active': selectedCategory === stat.id,
|
||||
'is-loading': isFetching && selectedCategory === stat.id && !isFetchingNextPage
|
||||
}"
|
||||
@click="selectedCategory = stat.id"
|
||||
>
|
||||
{{ stat.category || $t('common.uncategorized') }}
|
||||
<span class="tag-count">({{ stat.count }})</span>
|
||||
<div v-if="isFetching && selectedCategory === stat.id && !isFetchingNextPage" class="tag-loading-track">
|
||||
<div class="tag-loading-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-affix>
|
||||
|
||||
<!-- 1. 加载中 -->
|
||||
<div v-if="(isInitializing || isLoading || isFetching) && vault.length === 0" class="flex-column flex-center min-h-400 text-secondary">
|
||||
<!-- 1. 加载中 (仅在真正没有任何数据或初始化时显示大加载态) -->
|
||||
<div v-if="isInitializing || (isLoading && vault.length === 0)" class="flex-column flex-center min-h-400 text-secondary">
|
||||
<el-icon class="is-loading mb-20 text-primary" :size="48"><Loading /></el-icon>
|
||||
<p class="text-16 ls-1">{{ $t('common.loading_data') }}</p>
|
||||
</div>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "请输入加密密码用于保护备份文件。",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user