Refine offline status toasts and theme styling

This commit is contained in:
jxxghp
2026-07-02 12:36:49 +08:00
parent af604d0c5c
commit d06ce5d984
4 changed files with 95 additions and 136 deletions

View File

@@ -467,7 +467,7 @@ onMounted(async () => {
<template>
<!-- 👉 Offline Page -->
<OfflinePage :navbar-extra-height="navbarExtraHeight" />
<OfflinePage />
<!-- 👉 Pull Down Indicator -->
<div

View File

@@ -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>

View File

@@ -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);
}
// 对话框样式

View File

@@ -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 {