重构离线页面组件

This commit is contained in:
jxxghp
2025-09-13 14:00:03 +08:00
parent e37bde77a1
commit 1c019cd5c8
2 changed files with 81 additions and 203 deletions
+38 -160
View File
@@ -57,85 +57,35 @@ const statusIcon = computed(() => {
const colorTheme = computed(() => { const colorTheme = computed(() => {
return props.type === 'online' ? 'success' : 'error' return props.type === 'online' ? 'success' : 'error'
}) })
// 动画时长
const ENTER_DURATION = 600
const LEAVE_DURATION = 400
// 进入动画
function onEnter(el: HTMLElement, done: () => void) {
// 初始状态
el.style.opacity = '0'
el.style.transform = 'scale(0.9)'
el.style.filter = 'blur(10px)'
// 强制重绘
el.offsetHeight
// 应用过渡
el.style.transition = `all ${ENTER_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`
// 目标状态
requestAnimationFrame(() => {
el.style.opacity = '1'
el.style.transform = 'scale(1)'
el.style.filter = 'blur(0)'
})
// 动画完成
setTimeout(done, ENTER_DURATION)
}
// 离开动画
function onLeave(el: HTMLElement, done: () => void) {
// 应用过渡
el.style.transition = `all ${LEAVE_DURATION}ms cubic-bezier(0.4, 0, 1, 1)`
// 目标状态
requestAnimationFrame(() => {
el.style.opacity = '0'
el.style.transform = 'scale(1.1)'
el.style.filter = 'blur(20px)'
})
// 动画完成
setTimeout(done, LEAVE_DURATION)
}
</script> </script>
<template> <template>
<Teleport to="body"> <VDialog :model-value="shouldShow" persistent max-width="420" scrollable>
<Transition <VCard class="offline-dialog">
:css="false"
@enter="onEnter"
@leave="onLeave"
>
<div v-if="shouldShow" class="offline-page" ref="offlinePage">
<div class="offline-container" :class="{ 'container-animate': shouldShow }">
<!-- 状态图标 --> <!-- 状态图标 -->
<div class="status-icon-wrapper"> <div class="status-icon-wrapper">
<div class="status-icon-bg"> <div class="status-icon-bg">
<VIcon :icon="statusIcon" size="64" :color="colorTheme" /> <VIcon :icon="statusIcon" size="48" :color="colorTheme" />
</div> </div>
</div> </div>
<!-- 主要信息 --> <!-- 主要信息 -->
<div class="content-section"> <VCardText class="text-center">
<h1 class="offline-title"> <h2 class="offline-title mb-4">
{{ props.type === 'online' ? t('app.online') : t('app.offline') }} {{ props.type === 'online' ? t('app.online') : t('app.offline') }}
</h1> </h2>
<p class="offline-message"> <p class="offline-message mb-6">
{{ statusText }} {{ statusText }}
</p> </p>
<!-- 重试按钮 --> <!-- 重试按钮 -->
<div class="action-section"> <div class="action-section mb-6">
<VBtn <VBtn
v-if="props.type === 'offline'" v-if="props.type === 'offline'"
:loading="retrying" :loading="retrying"
:color="colorTheme" :color="colorTheme"
size="large" size="default"
variant="flat" variant="flat"
@click="handleRetry" @click="handleRetry"
> >
@@ -150,6 +100,7 @@ function onLeave(el: HTMLElement, done: () => void) {
:color="isOnline ? 'success' : 'error'" :color="isOnline ? 'success' : 'error'"
:prepend-icon="isOnline ? 'mdi-wifi' : 'mdi-wifi-off'" :prepend-icon="isOnline ? 'mdi-wifi' : 'mdi-wifi-off'"
variant="tonal" variant="tonal"
size="small"
class="me-2" class="me-2"
> >
{{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }} {{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }}
@@ -159,57 +110,25 @@ function onLeave(el: HTMLElement, done: () => void) {
:color="canPerformNetworkAction ? 'success' : 'warning'" :color="canPerformNetworkAction ? 'success' : 'warning'"
:prepend-icon="canPerformNetworkAction ? 'mdi-check-circle' : 'mdi-alert-circle'" :prepend-icon="canPerformNetworkAction ? 'mdi-check-circle' : 'mdi-alert-circle'"
variant="tonal" variant="tonal"
size="small"
> >
{{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }} {{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }}
</VChip> </VChip>
</div> </div>
</div> </VCardText>
</VCard>
<!-- 底部信息 --> </VDialog>
<div class="footer-section">
<p class="app-info">{{ t('app.moviepilot') }}</p>
</div>
</div>
</div>
</Transition>
</Teleport>
</template> </template>
<style scoped> <style scoped>
.offline-page { .offline-dialog {
position: fixed; border-radius: 16px;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
backdrop-filter: blur(10px);
background: linear-gradient(135deg, rgb(var(--v-theme-surface)) 0%, rgb(var(--v-theme-surface-variant)) 100%);
inset: 0;
will-change: transform, opacity, filter;
}
.offline-container {
padding: 40px;
border-radius: 24px;
background: rgb(var(--v-theme-surface));
box-shadow: 0 20px 40px rgba(0, 0, 0, 10%), 0 0 0 1px rgba(var(--v-border-color), var(--v-border-opacity));
inline-size: 100%;
max-inline-size: 500px;
text-align: center;
opacity: 0;
transform: translateY(20px);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.offline-page .offline-container.container-animate {
opacity: 1;
transform: translateY(0);
transition-delay: 0.2s;
} }
.status-icon-wrapper { .status-icon-wrapper {
margin-block-end: 32px; padding-block: 24px 0;
padding-inline: 24px;
text-align: center;
} }
.status-icon-bg { .status-icon-bg {
@@ -218,71 +137,61 @@ function onLeave(el: HTMLElement, done: () => void) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 50%; border-radius: 50%;
animation: icon-pulse 3s ease-in-out infinite;
background: rgba(var(--v-theme-surface-variant), 0.5); background: rgba(var(--v-theme-surface-variant), 0.5);
block-size: 120px; block-size: 80px;
inline-size: 120px; inline-size: 80px;
margin-block: 0; margin-block: 0;
margin-inline: auto; margin-inline: auto;
} }
.status-icon-bg {
animation: iconPulse 3s ease-in-out infinite;
}
.status-icon-bg::before { .status-icon-bg::before {
position: absolute; position: absolute;
z-index: -1; z-index: -1;
border-radius: 50%; border-radius: 50%;
animation: icon-glow 2s ease-in-out infinite alternate;
background: linear-gradient(45deg, rgb(var(--v-theme-primary)), rgb(var(--v-theme-secondary))); background: linear-gradient(45deg, rgb(var(--v-theme-primary)), rgb(var(--v-theme-secondary)));
content: ''; content: '';
inset: -4px; inset: -3px;
opacity: 0.1; opacity: 0.1;
animation: iconGlow 2s ease-in-out infinite alternate;
} }
@keyframes iconPulse { @keyframes icon-pulse {
0%, 100% { 0%,
100% {
transform: scale(1); transform: scale(1);
} }
50% { 50% {
transform: scale(1.05); transform: scale(1.05);
} }
} }
@keyframes iconGlow { @keyframes icon-glow {
0% { 0% {
opacity: 0.1; opacity: 0.1;
transform: scale(1); transform: scale(1);
} }
100% { 100% {
opacity: 0.3; opacity: 0.3;
transform: scale(1.1); transform: scale(1.1);
} }
} }
.content-section {
margin-block-end: 32px;
}
.offline-title { .offline-title {
color: rgb(var(--v-theme-on-surface)); color: rgb(var(--v-theme-on-surface));
font-size: 2rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
margin-block-end: 16px;
} }
.offline-message { .offline-message {
color: rgb(var(--v-theme-on-surface)); color: rgb(var(--v-theme-on-surface));
font-size: 1.1rem; font-size: 1rem;
line-height: 1.6; line-height: 1.5;
margin-block-end: 32px;
opacity: 0.7; opacity: 0.7;
} }
.action-section {
margin-block-end: 32px;
}
.status-indicators { .status-indicators {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -290,41 +199,19 @@ function onLeave(el: HTMLElement, done: () => void) {
gap: 8px; gap: 8px;
} }
.help-section {
margin-block-end: 32px;
}
.help-panels {
text-align: start;
}
.footer-section {
opacity: 0.7;
}
.app-info {
color: rgb(var(--v-theme-on-surface));
font-size: 0.875rem;
}
/* 移动端优化 */ /* 移动端优化 */
@media (width <= 600px) { @media (width <= 600px) {
.offline-container { .status-icon-bg {
padding: 24px; block-size: 70px;
margin: 16px; inline-size: 70px;
} }
.offline-title { .offline-title {
font-size: 1.5rem; font-size: 1.25rem;
} }
.offline-message { .offline-message {
font-size: 1rem; font-size: 0.9rem;
}
.status-icon-bg {
block-size: 100px;
inline-size: 100px;
} }
.status-indicators { .status-indicators {
@@ -332,13 +219,4 @@ function onLeave(el: HTMLElement, done: () => void) {
align-items: center; align-items: center;
} }
} }
/* 暗黑模式优化 */
.v-theme--dark .offline-page {
background: linear-gradient(135deg, rgb(var(--v-theme-surface)) 0%, rgba(var(--v-theme-surface-variant), 0.8) 100%);
}
.v-theme--dark .offline-container {
box-shadow: 0 20px 40px rgba(0, 0, 0, 30%), 0 0 0 1px rgba(var(--v-border-color), var(--v-border-opacity));
}
</style> </style>
+1 -1
View File
@@ -49,7 +49,7 @@ const dataList = ref<Array<Context>>([])
const isRefreshed = ref(false) const isRefreshed = ref(false)
// 加载进度文本 // 加载进度文本
const progressText = ref('') const progressText = ref(t('common.pleaseWait'))
// 加载进度 // 加载进度
const progressValue = ref(0) const progressValue = ref(0)