EDMS/components/base/LoadingStates.vue
2025-06-05 14:57:08 +08:00

269 lines
7.6 KiB
Vue

<script setup>
import { computed } from 'vue';
import { useDesignSystem } from '~/composables/useDesignSystem';
const props = defineProps({
// Loading type
type: {
type: String,
default: 'spinner',
validator: (value) => [
'spinner', 'skeleton', 'skeleton-table', 'skeleton-card',
'skeleton-list', 'skeleton-tree', 'pulse', 'overlay'
].includes(value)
},
// Size variants
size: {
type: String,
default: 'md',
validator: (value) => ['sm', 'md', 'lg', 'xl'].includes(value)
},
// Loading message
message: {
type: String,
default: ''
},
// Number of skeleton items
count: {
type: Number,
default: 3
},
// Show as overlay
overlay: {
type: Boolean,
default: false
},
// Animation speed
speed: {
type: String,
default: 'normal',
validator: (value) => ['slow', 'normal', 'fast'].includes(value)
},
// Custom classes
class: {
type: String,
default: ''
}
});
// Design system
const { tokens, utils } = useDesignSystem();
// Computed classes
const containerClasses = computed(() => {
const classes = ['loading-container'];
if (props.overlay) {
classes.push('loading-overlay');
}
if (props.class) {
classes.push(props.class);
}
return classes.join(' ');
});
const spinnerClasses = computed(() => {
const sizeMap = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8',
xl: 'w-12 h-12'
};
const speedMap = {
slow: 'animate-spin-slow',
normal: 'animate-spin',
fast: 'animate-spin-fast'
};
return utils.composeClasses(
'border-2 border-current border-t-transparent rounded-full',
sizeMap[props.size],
speedMap[props.speed]
);
});
const pulseClasses = computed(() => {
const speedMap = {
slow: 'animate-pulse-slow',
normal: 'animate-pulse',
fast: 'animate-pulse-fast'
};
return utils.composeClasses(
'bg-gray-300 dark:bg-gray-600 rounded',
speedMap[props.speed]
);
});
</script>
<template>
<div :class="containerClasses">
<!-- Spinner Loading -->
<div v-if="type === 'spinner'" class="flex flex-col items-center justify-center space-y-3">
<div :class="spinnerClasses" role="status" aria-label="Loading"></div>
<p v-if="message" class="text-sm text-gray-600 dark:text-gray-400">
{{ message }}
</p>
</div>
<!-- Pulse Loading -->
<div v-else-if="type === 'pulse'" class="flex items-center justify-center">
<div class="flex space-x-2">
<div class="w-2 h-2 bg-primary rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
</div>
<!-- Skeleton Table -->
<div v-else-if="type === 'skeleton-table'" class="space-y-4">
<!-- Table Header -->
<div class="grid grid-cols-4 gap-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div v-for="i in 4" :key="`header-${i}`" :class="pulseClasses" class="h-4"></div>
</div>
<!-- Table Rows -->
<div v-for="row in count" :key="`row-${row}`" class="grid grid-cols-4 gap-4 p-4 border border-gray-200 dark:border-gray-600 rounded-lg">
<div v-for="col in 4" :key="`cell-${row}-${col}`" :class="pulseClasses" class="h-4"></div>
</div>
</div>
<!-- Skeleton Cards -->
<div v-else-if="type === 'skeleton-card'" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="card in count" :key="`card-${card}`" class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg p-6 space-y-4">
<!-- Card Header -->
<div class="flex items-center space-x-3">
<div :class="pulseClasses" class="w-10 h-10 rounded-full"></div>
<div class="flex-1 space-y-2">
<div :class="pulseClasses" class="h-4 w-3/4"></div>
<div :class="pulseClasses" class="h-3 w-1/2"></div>
</div>
</div>
<!-- Card Content -->
<div class="space-y-2">
<div :class="pulseClasses" class="h-3 w-full"></div>
<div :class="pulseClasses" class="h-3 w-5/6"></div>
<div :class="pulseClasses" class="h-3 w-4/6"></div>
</div>
<!-- Card Footer -->
<div class="flex justify-between">
<div :class="pulseClasses" class="h-8 w-20 rounded"></div>
<div :class="pulseClasses" class="h-8 w-16 rounded"></div>
</div>
</div>
</div>
<!-- Skeleton List -->
<div v-else-if="type === 'skeleton-list'" class="space-y-3">
<div v-for="item in count" :key="`list-${item}`" class="flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-600 rounded-lg">
<div :class="pulseClasses" class="w-8 h-8 rounded"></div>
<div class="flex-1 space-y-2">
<div :class="pulseClasses" class="h-4 w-3/4"></div>
<div :class="pulseClasses" class="h-3 w-1/2"></div>
</div>
<div :class="pulseClasses" class="w-6 h-6 rounded"></div>
</div>
</div>
<!-- Skeleton Tree -->
<div v-else-if="type === 'skeleton-tree'" class="space-y-2">
<div v-for="level in count" :key="`tree-${level}`">
<!-- Parent Node -->
<div class="flex items-center space-x-2 p-2">
<div :class="pulseClasses" class="w-4 h-4 rounded"></div>
<div :class="pulseClasses" class="w-5 h-5 rounded"></div>
<div :class="pulseClasses" class="h-4 w-40"></div>
</div>
<!-- Child Nodes -->
<div class="ml-6 space-y-1">
<div v-for="child in 2" :key="`child-${level}-${child}`" class="flex items-center space-x-2 p-1">
<div :class="pulseClasses" class="w-4 h-4 rounded"></div>
<div :class="pulseClasses" class="h-3 w-32"></div>
</div>
</div>
</div>
</div>
<!-- Basic Skeleton -->
<div v-else-if="type === 'skeleton'" class="space-y-3">
<div v-for="item in count" :key="`skeleton-${item}`" class="space-y-2">
<div :class="pulseClasses" class="h-4 w-full"></div>
<div :class="pulseClasses" class="h-4 w-5/6"></div>
<div :class="pulseClasses" class="h-4 w-4/6"></div>
</div>
</div>
<!-- Overlay Loading -->
<div v-else-if="type === 'overlay'" class="overlay-content">
<div class="flex flex-col items-center justify-center space-y-4">
<div :class="spinnerClasses" role="status" aria-label="Loading"></div>
<p v-if="message" class="text-lg font-medium text-gray-700 dark:text-gray-300">
{{ message }}
</p>
</div>
</div>
</div>
</template>
<style scoped>
.loading-container {
@apply w-full;
}
.loading-overlay {
@apply fixed inset-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm;
}
.overlay-content {
@apply h-full flex items-center justify-center;
}
/* Custom animation speeds */
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-fast {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse-slow {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes pulse-fast {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.animate-spin-slow {
animation: spin-slow 2s linear infinite;
}
.animate-spin-fast {
animation: spin-fast 0.5s linear infinite;
}
.animate-pulse-slow {
animation: pulse-slow 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-pulse-fast {
animation: pulse-fast 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
</style>