mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
重构离线页面组件
This commit is contained in:
@@ -57,159 +57,78 @@ 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"
|
<div class="status-icon-wrapper">
|
||||||
@leave="onLeave"
|
<div class="status-icon-bg">
|
||||||
>
|
<VIcon :icon="statusIcon" size="48" :color="colorTheme" />
|
||||||
<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-bg">
|
|
||||||
<VIcon :icon="statusIcon" size="64" :color="colorTheme" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主要信息 -->
|
|
||||||
<div class="content-section">
|
|
||||||
<h1 class="offline-title">
|
|
||||||
{{ props.type === 'online' ? t('app.online') : t('app.offline') }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p class="offline-message">
|
|
||||||
{{ statusText }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- 重试按钮 -->
|
|
||||||
<div class="action-section">
|
|
||||||
<VBtn
|
|
||||||
v-if="props.type === 'offline'"
|
|
||||||
:loading="retrying"
|
|
||||||
:color="colorTheme"
|
|
||||||
size="large"
|
|
||||||
variant="flat"
|
|
||||||
@click="handleRetry"
|
|
||||||
>
|
|
||||||
<VIcon icon="mdi-refresh" class="me-2" />
|
|
||||||
{{ retrying ? t('common.checking') : t('common.retry') }}
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 状态指示器 -->
|
|
||||||
<div class="status-indicators">
|
|
||||||
<VChip
|
|
||||||
:color="isOnline ? 'success' : 'error'"
|
|
||||||
:prepend-icon="isOnline ? 'mdi-wifi' : 'mdi-wifi-off'"
|
|
||||||
variant="tonal"
|
|
||||||
class="me-2"
|
|
||||||
>
|
|
||||||
{{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }}
|
|
||||||
</VChip>
|
|
||||||
|
|
||||||
<VChip
|
|
||||||
:color="canPerformNetworkAction ? 'success' : 'warning'"
|
|
||||||
:prepend-icon="canPerformNetworkAction ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
|
||||||
variant="tonal"
|
|
||||||
>
|
|
||||||
{{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }}
|
|
||||||
</VChip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部信息 -->
|
|
||||||
<div class="footer-section">
|
|
||||||
<p class="app-info">{{ t('app.moviepilot') }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Transition>
|
<!-- 主要信息 -->
|
||||||
</Teleport>
|
<VCardText class="text-center">
|
||||||
|
<h2 class="offline-title mb-4">
|
||||||
|
{{ props.type === 'online' ? t('app.online') : t('app.offline') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="offline-message mb-6">
|
||||||
|
{{ statusText }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 重试按钮 -->
|
||||||
|
<div class="action-section mb-6">
|
||||||
|
<VBtn
|
||||||
|
v-if="props.type === 'offline'"
|
||||||
|
:loading="retrying"
|
||||||
|
:color="colorTheme"
|
||||||
|
size="default"
|
||||||
|
variant="flat"
|
||||||
|
@click="handleRetry"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-refresh" class="me-2" />
|
||||||
|
{{ retrying ? t('common.checking') : t('common.retry') }}
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态指示器 -->
|
||||||
|
<div class="status-indicators">
|
||||||
|
<VChip
|
||||||
|
:color="isOnline ? 'success' : 'error'"
|
||||||
|
:prepend-icon="isOnline ? 'mdi-wifi' : 'mdi-wifi-off'"
|
||||||
|
variant="tonal"
|
||||||
|
size="small"
|
||||||
|
class="me-2"
|
||||||
|
>
|
||||||
|
{{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }}
|
||||||
|
</VChip>
|
||||||
|
|
||||||
|
<VChip
|
||||||
|
:color="canPerformNetworkAction ? 'success' : 'warning'"
|
||||||
|
:prepend-icon="canPerformNetworkAction ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||||
|
variant="tonal"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }}
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
</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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user