mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
Improve resource search loading UI
This commit is contained in:
+169
-26
@@ -109,6 +109,24 @@ const progressEnabled = ref(false)
|
|||||||
// 进度是否激活
|
// 进度是否激活
|
||||||
const progressActive = ref(false)
|
const progressActive = ref(false)
|
||||||
|
|
||||||
|
// 是否显示搜索中的页面态
|
||||||
|
const isSearchLoading = computed(
|
||||||
|
() => !isRefreshed.value && (progressActive.value || progressEnabled.value || progressValue.value > 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 归一化搜索进度,避免 SSE 异常值影响显示
|
||||||
|
const searchProgressPercent = computed(() =>
|
||||||
|
Math.min(100, Math.max(0, Math.ceil(Number(progressValue.value) || 0))),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 搜索进度文案
|
||||||
|
const searchProgressLabel = computed(() =>
|
||||||
|
progressEnabled.value || progressValue.value > 0 ? `${searchProgressPercent.value}%` : '...',
|
||||||
|
)
|
||||||
|
|
||||||
|
// 进度未返回前使用不确定态
|
||||||
|
const searchProgressIndeterminate = computed(() => !progressEnabled.value && searchProgressPercent.value <= 0)
|
||||||
|
|
||||||
// 错误标题
|
// 错误标题
|
||||||
const errorTitle = ref(t('resource.noData'))
|
const errorTitle = ref(t('resource.noData'))
|
||||||
|
|
||||||
@@ -561,17 +579,60 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- 加载进度条 -->
|
<!-- 搜索加载状态 -->
|
||||||
<VFadeTransition>
|
<VFadeTransition>
|
||||||
<div v-if="progressValue > 0 || progressEnabled" class="search-progress-container">
|
<div v-if="isSearchLoading" class="search-loading-state">
|
||||||
<VCard elevation="3" class="search-progress-card">
|
<VCard elevation="0" class="search-progress-card">
|
||||||
<div class="progress-header">
|
<div class="progress-header">
|
||||||
<VIcon icon="mdi-movie-search" color="primary" size="small" class="me-2" />
|
<div class="progress-icon-wrap">
|
||||||
<span class="progress-title">{{ progressText }}</span>
|
<VProgressCircular
|
||||||
|
color="primary"
|
||||||
|
:indeterminate="searchProgressIndeterminate"
|
||||||
|
:model-value="searchProgressPercent"
|
||||||
|
:size="56"
|
||||||
|
:width="5"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-movie-search" color="primary" size="24" />
|
||||||
|
</VProgressCircular>
|
||||||
|
</div>
|
||||||
|
<div class="progress-copy">
|
||||||
|
<span class="progress-title">{{ progressText }}</span>
|
||||||
|
<div v-if="hasSearchTags" class="progress-tags d-flex flex-wrap">
|
||||||
|
<VChip v-if="keyword" class="search-tag progress-tag" color="primary" size="small" variant="tonal">
|
||||||
|
{{ t('resource.keyword') }}: {{ keyword }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="title" class="search-tag progress-tag" color="primary" size="small" variant="tonal">
|
||||||
|
{{ t('resource.title') }}: {{ title }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="year" class="search-tag progress-tag" color="primary" size="small" variant="tonal">
|
||||||
|
{{ t('resource.year') }}: {{ year }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="season" class="search-tag progress-tag" color="primary" size="small" variant="tonal">
|
||||||
|
{{ t('resource.season') }}: {{ season }}
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-percentage">{{ searchProgressLabel }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-container">
|
<div class="progress-bar-container">
|
||||||
<VProgressLinear color="primary" rounded :model-value="progressValue" />
|
<VProgressLinear
|
||||||
<div class="progress-percentage">{{ Math.ceil(progressValue) }}%</div>
|
color="primary"
|
||||||
|
rounded
|
||||||
|
:indeterminate="searchProgressIndeterminate"
|
||||||
|
:model-value="searchProgressPercent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
|
<div v-if="viewType === 'card'" class="search-skeleton-grid">
|
||||||
|
<VCard v-for="item in 6" :key="`search-card-skeleton-${item}`" class="search-skeleton-card" elevation="0">
|
||||||
|
<VSkeletonLoader type="image, article" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VCard v-else class="search-skeleton-list" elevation="0">
|
||||||
|
<div v-for="item in 6" :key="`search-row-skeleton-${item}`" class="search-skeleton-row">
|
||||||
|
<VSkeletonLoader type="list-item-avatar-two-line" />
|
||||||
</div>
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -756,7 +817,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 初始加载状态 -->
|
<!-- 初始加载状态 -->
|
||||||
<LoadingBanner v-else-if="!isRefreshed && !(progressEnabled || progressValue > 0)" />
|
<LoadingBanner v-else-if="!isRefreshed && !isSearchLoading" />
|
||||||
<!-- 滚动到顶部按钮 -->
|
<!-- 滚动到顶部按钮 -->
|
||||||
<Teleport to="body" v-if="route.path === '/resource'">
|
<Teleport to="body" v-if="route.path === '/resource'">
|
||||||
<VScrollToTopBtn />
|
<VScrollToTopBtn />
|
||||||
@@ -765,51 +826,94 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.search-progress-container {
|
.search-loading-state {
|
||||||
position: fixed;
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
inset-block-start: env(safe-area-inset-top);
|
gap: 16px;
|
||||||
inset-inline: 0;
|
min-block-size: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-progress-card {
|
.search-progress-card {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid rgba(var(--v-theme-primary), 0.1);
|
border: 1px solid rgba(var(--v-theme-primary), 0.18);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(var(--v-theme-primary), 0.08), transparent 42%),
|
||||||
|
rgb(var(--v-theme-surface));
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 10%);
|
inline-size: 100%;
|
||||||
inline-size: 90%;
|
|
||||||
max-inline-size: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-header {
|
.progress-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-block-end: 12px;
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-icon-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-copy {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-inline-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-title {
|
.progress-title {
|
||||||
color: rgb(var(--v-theme-on-surface));
|
color: rgb(var(--v-theme-on-surface));
|
||||||
font-size: 0.9rem;
|
display: block;
|
||||||
font-weight: 500;
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tags {
|
||||||
|
gap: 6px;
|
||||||
|
margin-block-start: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tag {
|
||||||
|
max-inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
margin-block-start: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-percentage {
|
.progress-percentage {
|
||||||
color: rgb(var(--v-theme-primary));
|
color: rgb(var(--v-theme-primary));
|
||||||
font-size: 0.8rem;
|
flex: 0 0 auto;
|
||||||
font-weight: 600;
|
font-size: 0.95rem;
|
||||||
min-inline-size: 36px;
|
font-weight: 700;
|
||||||
|
min-inline-size: 44px;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-skeleton-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-skeleton-card,
|
||||||
|
.search-skeleton-list {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgb(var(--v-theme-surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-skeleton-row + .search-skeleton-row {
|
||||||
|
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
/* 精简标题栏样式 */
|
/* 精简标题栏样式 */
|
||||||
.search-header {
|
.search-header {
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
@@ -1012,6 +1116,45 @@ onUnmounted(() => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-loading-state {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-progress-card {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-icon-wrap {
|
||||||
|
padding-block-start: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
min-inline-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tags {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tags::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-skeleton-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.view-toggle-container {
|
.view-toggle-container {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user