mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-04 14:01:27 +08:00
Refine offline status toasts and theme styling
This commit is contained in:
@@ -467,7 +467,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<!-- 👉 Offline Page -->
|
||||
<OfflinePage :navbar-extra-height="navbarExtraHeight" />
|
||||
<OfflinePage />
|
||||
|
||||
<!-- 👉 Pull Down Indicator -->
|
||||
<div
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
navbarExtraHeight?: string
|
||||
}>(),
|
||||
{
|
||||
navbarExtraHeight: '0rem',
|
||||
},
|
||||
)
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { connectionStatus, connectionReason, requestConnectionCheck } = useGlobalOfflineStatus()
|
||||
const dismissed = ref(false)
|
||||
const toast = useToast()
|
||||
const { connectionStatus, connectionReason } = useGlobalOfflineStatus()
|
||||
const lastConnectionPromptKey = ref('')
|
||||
|
||||
const shouldShow = computed(() => connectionStatus.value !== 'online' && !dismissed.value)
|
||||
const isChecking = computed(() => connectionStatus.value === 'checking')
|
||||
const alertType = computed(() => (isChecking.value ? 'warning' : 'error'))
|
||||
const statusTitle = computed(() => (isChecking.value ? t('app.connectionChecking') : t('app.serviceUnavailable')))
|
||||
const statusMessage = computed(() => {
|
||||
if (connectionReason.value === 'browser-offline') return t('app.browserOfflineMessage')
|
||||
@@ -25,133 +16,45 @@ const statusMessage = computed(() => {
|
||||
return t('app.serviceUnavailableMessage')
|
||||
})
|
||||
|
||||
/** 立即请求重新探测 MoviePilot 服务。 */
|
||||
function handleRetry() {
|
||||
requestConnectionCheck()
|
||||
/** 拼接离线状态提示文案,供 Toast 或 Agent 助手气泡展示。 */
|
||||
function buildConnectionPromptMessage() {
|
||||
return `${statusTitle.value}:${statusMessage.value}`
|
||||
}
|
||||
|
||||
/** 隐藏本次连接提示并允许用户继续浏览。 */
|
||||
function handleContinueBrowsing() {
|
||||
dismissed.value = true
|
||||
}
|
||||
|
||||
watch(connectionStatus, (status, previousStatus) => {
|
||||
if (status === 'online' || (status === 'offline' && previousStatus === 'checking')) {
|
||||
dismissed.value = false
|
||||
/** 根据当前连接状态选择 Toast 级别,Agent 助手可用时会由全局 Toast 路由接管。 */
|
||||
function showConnectionPrompt() {
|
||||
const message = buildConnectionPromptMessage()
|
||||
const options = {
|
||||
timeout: isChecking.value ? 5000 : 7000,
|
||||
}
|
||||
|
||||
if (isChecking.value) {
|
||||
toast.warning(message, options)
|
||||
return
|
||||
}
|
||||
|
||||
toast.error(message, options)
|
||||
}
|
||||
|
||||
/** 在连接状态变化时发出一次离线提示,并在恢复在线后允许下一轮提示重新出现。 */
|
||||
function handleConnectionStatusChange() {
|
||||
if (connectionStatus.value === 'online') {
|
||||
lastConnectionPromptKey.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const promptKey = `${connectionStatus.value}:${connectionReason.value || 'unknown'}`
|
||||
if (promptKey === lastConnectionPromptKey.value) return
|
||||
|
||||
lastConnectionPromptKey.value = promptKey
|
||||
showConnectionPrompt()
|
||||
}
|
||||
|
||||
watch([connectionStatus, connectionReason], handleConnectionStatusChange, {
|
||||
flush: 'post',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="connection-status">
|
||||
<div
|
||||
v-if="shouldShow"
|
||||
class="connection-status-host"
|
||||
:style="{ '--connection-status-navbar-extra-height': props.navbarExtraHeight }"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<VAlert :type="alertType" variant="elevated" density="comfortable" class="connection-status-alert">
|
||||
<div class="connection-status-content">
|
||||
<div class="connection-status-copy">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
{{ statusTitle }}
|
||||
</div>
|
||||
<div class="text-body-2 mt-1">
|
||||
{{ statusMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-status-actions">
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="text"
|
||||
:loading="isChecking"
|
||||
:disabled="isChecking"
|
||||
@click="handleRetry"
|
||||
>
|
||||
{{ isChecking ? t('common.checking') : t('common.retry') }}
|
||||
</VBtn>
|
||||
<VBtn size="small" variant="text" @click="handleContinueBrowsing">
|
||||
{{ t('app.continueBrowsing') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VAlert>
|
||||
</div>
|
||||
</Transition>
|
||||
<span class="d-none" aria-hidden="true" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.connection-status-host {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
inline-size: min(44rem, calc(100vw - 2rem));
|
||||
inset-block-start: calc(
|
||||
env(safe-area-inset-top, 0px) + 4rem + var(--connection-status-navbar-extra-height, 0rem) + 0.75rem
|
||||
);
|
||||
inset-inline-start: 50%;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.connection-status-alert {
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: var(--app-overlay-radius);
|
||||
box-shadow: var(--app-overlay-shadow);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.connection-status-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.connection-status-copy {
|
||||
flex: 1;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.connection-status-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.connection-status-enter-active,
|
||||
.connection-status-leave-active {
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.connection-status-enter-from,
|
||||
.connection-status-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -0.75rem);
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.connection-status-host {
|
||||
inline-size: calc(100vw - 1rem);
|
||||
}
|
||||
|
||||
.connection-status-content {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.connection-status-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.connection-status-enter-active,
|
||||
.connection-status-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -847,8 +847,52 @@ html[data-theme="transparent"].transparent-glass-realtime .v-theme--transparent
|
||||
}
|
||||
|
||||
.Vue-Toastification__toast {
|
||||
--mp-toast-accent-rgb: var(--v-theme-primary);
|
||||
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-inline-start: 0.25rem solid rgba(var(--mp-toast-accent-rgb), 0.88);
|
||||
border-radius: var(--app-overlay-radius);
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(var(--mp-toast-accent-rgb), 0.14),
|
||||
rgba(var(--mp-toast-accent-rgb), 0.05) 42%,
|
||||
rgba(var(--v-theme-surface), 0.96)
|
||||
),
|
||||
rgb(var(--v-theme-surface)) !important;
|
||||
box-shadow: var(--app-overlay-shadow);
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.Vue-Toastification__toast--default,
|
||||
.Vue-Toastification__toast--info {
|
||||
--mp-toast-accent-rgb: var(--v-theme-info);
|
||||
}
|
||||
|
||||
.Vue-Toastification__toast--success {
|
||||
--mp-toast-accent-rgb: var(--v-theme-success);
|
||||
}
|
||||
|
||||
.Vue-Toastification__toast--error {
|
||||
--mp-toast-accent-rgb: var(--v-theme-error);
|
||||
}
|
||||
|
||||
.Vue-Toastification__toast--warning {
|
||||
--mp-toast-accent-rgb: var(--v-theme-warning);
|
||||
}
|
||||
|
||||
.Vue-Toastification__icon,
|
||||
.Vue-Toastification__close-button {
|
||||
color: rgba(var(--mp-toast-accent-rgb), 0.95);
|
||||
}
|
||||
|
||||
.Vue-Toastification__close-button {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.Vue-Toastification__progress-bar {
|
||||
background-color: rgba(var(--mp-toast-accent-rgb), 0.35);
|
||||
}
|
||||
|
||||
// 对话框样式
|
||||
|
||||
@@ -221,6 +221,18 @@ html[data-theme="transparent"] {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
||||
}
|
||||
|
||||
.Vue-Toastification__toast {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(var(--mp-toast-accent-rgb), 0.16),
|
||||
rgba(var(--mp-toast-accent-rgb), 0.06) 44%,
|
||||
rgba(var(--v-theme-surface), var(--transparent-opacity-heavy))
|
||||
),
|
||||
rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="transparent"].transparent-background-blur-disabled {
|
||||
|
||||
Reference in New Issue
Block a user