Files
BYW-VoiceHub/app/components/UI/Common/LoadingState.vue
T

181 lines
5.7 KiB
Vue

<template>
<div
:class="{
'fixed inset-0 z-[9999] bg-bg-primary-80 backdrop-blur-md': fullScreen,
'min-h-[200px] py-12': !fullScreen
}"
class="flex flex-col items-center justify-center animate-in fade-in duration-300"
>
<div class="flex flex-col items-center text-center max-w-md w-full px-6">
<!-- 加载动画 -->
<div class="mb-8">
<!-- 默认圆形加载器 -->
<div v-if="spinnerType === 'circle'">
<AppSpinner :size="48" />
</div>
<!-- 点状加载器 -->
<div v-else-if="spinnerType === 'dots'" class="flex gap-2">
<div
v-for="i in 3"
:key="i"
class="w-3 h-3 bg-primary rounded-full animate-bounce"
:style="{ animationDelay: `${(i - 1) * 0.2}s` }"
/>
</div>
<!-- 脉冲加载器 -->
<div v-else-if="spinnerType === 'pulse'" class="relative w-12 h-12">
<div
v-for="i in 3"
:key="i"
class="absolute inset-0 border-2 border-primary rounded-full animate-ping"
:style="{ animationDelay: `${(i - 1) * 0.4}s`, animationDuration: '2s' }"
/>
</div>
<!-- 条状加载器 -->
<div v-else-if="spinnerType === 'bars'" class="flex items-end gap-1.5 h-8">
<div
v-for="i in 5"
:key="i"
class="w-1.5 bg-primary rounded-full animate-bounce"
:style="{
animationDelay: `${(i - 1) * 0.1}s`,
height: `${40 + Math.random() * 60}%`
}"
/>
</div>
</div>
<!-- 加载文本 -->
<div class="w-full space-y-2">
<h3 v-if="title" class="text-xl font-black text-text-primary tracking-tight">{{ title }}</h3>
<p class="text-[10px] font-black text-text-tertiary uppercase tracking-widest">{{ displayMessage }}</p>
<!-- 进度条 -->
<div v-if="showProgress" class="mt-8 space-y-2">
<div class="h-1.5 w-full bg-bg-secondary rounded-full overflow-hidden border border-border-secondary">
<div
class="h-full bg-primary transition-all duration-300 shadow-[0_0_10px_var(--primary-50)]"
:style="{ width: `${progress}%` }"
/>
</div>
<div class="flex justify-between items-center px-1">
<span class="text-[10px] font-black text-text-disabled uppercase tracking-widest"
>{{ locale.progress }}</span
>
<span class="text-[10px] font-black text-primary tracking-widest"
>{{ progress }}%</span
>
</div>
</div>
<!-- 加载步骤 -->
<div v-if="steps && steps.length > 0" class="mt-8 space-y-3 text-left">
<div
v-for="(step, index) in steps"
:key="index"
class="flex items-center gap-3 transition-all duration-300"
:class="{
'opacity-100': index <= currentStep,
'opacity-30': index > currentStep
}"
>
<div class="relative flex items-center justify-center w-6 h-6">
<div
v-if="index < currentStep"
class="flex items-center justify-center w-6 h-6 bg-primary-10 border border-primary-20 rounded-lg"
>
<Check :size="12" class="text-primary" />
</div>
<div
v-else-if="index === currentStep"
class="w-6 h-6 flex items-center justify-center"
>
<AppSpinner :size="24" />
</div>
<div
v-else
class="flex items-center justify-center w-6 h-6 bg-bg-secondary border border-border-secondary rounded-lg"
>
<span class="text-[10px] font-black text-text-disabled">{{ index + 1 }}</span>
</div>
</div>
<span
class="text-[10px] font-black uppercase tracking-widest transition-colors"
:class="index === currentStep ? 'text-text-primary' : 'text-text-disabled'"
>
{{ step }}
</span>
</div>
</div>
</div>
<!-- 取消按钮 -->
<button
v-if="showCancel && onCancel"
class="mt-10 px-6 py-2 bg-bg-secondary border border-border-secondary hover:border-border-tertiary text-text-tertiary hover:text-text-primary text-[10px] font-black uppercase tracking-widest rounded-xl transition-all"
@click="onCancel"
>
{{ locale.cancel }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { Check } from '@lucide/vue'
import AppSpinner from '~/components/UI/Common/AppSpinner.vue'
import { computed } from 'vue'
import { useLocale } from '~/utils/locale'
const { ui } = useLocale()
const locale = computed(() => ui.value?.loadingState || {})
interface Props {
title?: string
message?: string
spinnerType?: 'circle' | 'dots' | 'pulse' | 'bars'
fullScreen?: boolean
showProgress?: boolean
progress?: number
steps?: string[]
currentStep?: number
showCancel?: boolean
onCancel?: () => void
}
const props = withDefaults(defineProps<Props>(), {
title: '',
message: undefined,
spinnerType: 'circle',
fullScreen: false,
showProgress: false,
progress: 0,
steps: () => [],
currentStep: 0,
showCancel: false,
onCancel: undefined
})
const displayMessage = computed(() => props.message || locale.value.defaultMessage)
</script>
<style scoped>
/* 保持必要的动画定义,如果 Tailwind 无法完全覆盖的话 */
@keyframes pulse-ring {
0% {
transform: scale(0.33);
opacity: 0;
}
80%,
100% {
opacity: 0;
}
50% {
opacity: 0.5;
}
}
</style>