201 lines
4.4 KiB
Vue
201 lines
4.4 KiB
Vue
<template>
|
|
<div class="flex items-center gap-2">
|
|
<div class="relative flex items-center justify-center">
|
|
<svg class="shield-ring" :class="ringClass" width="48" height="48" viewBox="0 0 48 48">
|
|
<circle
|
|
class="ring-background"
|
|
cx="24"
|
|
cy="24"
|
|
r="20"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
opacity="0.1"
|
|
/>
|
|
<circle
|
|
class="ring-progress"
|
|
cx="24"
|
|
cy="24"
|
|
r="20"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
stroke-linecap="round"
|
|
:stroke-dasharray="circumference"
|
|
:stroke-dashoffset="progressOffset"
|
|
transform="rotate(-90 24 24)"
|
|
/>
|
|
</svg>
|
|
|
|
<Transition name="shield-pulse" mode="out-in">
|
|
<UIcon :key="status" :name="shieldIcon" class="absolute shield-icon" :class="iconClass" />
|
|
</Transition>
|
|
</div>
|
|
|
|
<UBadge :label="statusLabel" :color="badgeColor" size="md" variant="subtle" class="status-badge" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ComplianceStatus } from '~~/.api-client'
|
|
|
|
const props = defineProps<{
|
|
status: ComplianceStatus
|
|
}>()
|
|
|
|
const circumference = 2 * Math.PI * 20
|
|
|
|
const shieldIcon = computed(() => {
|
|
switch (props.status) {
|
|
case ComplianceStatus.Critical:
|
|
return 'i-lucide-shield-alert'
|
|
case ComplianceStatus.Warning:
|
|
return 'i-lucide-shield-x'
|
|
case ComplianceStatus.NonCritical:
|
|
return 'i-lucide-shield-check'
|
|
default:
|
|
return 'i-lucide-shield-check'
|
|
}
|
|
})
|
|
|
|
const { t: $t } = useI18n()
|
|
|
|
const statusLabel = computed(() => {
|
|
switch (props.status) {
|
|
case ComplianceStatus.Critical:
|
|
return $t('compliance.critical')
|
|
case ComplianceStatus.Warning:
|
|
return $t('compliance.warning')
|
|
case ComplianceStatus.NonCritical:
|
|
return $t('compliance.nonCritical')
|
|
default:
|
|
return $t('compliance.nonCritical')
|
|
}
|
|
})
|
|
|
|
const badgeColor = computed(() => {
|
|
switch (props.status) {
|
|
case ComplianceStatus.Critical:
|
|
return 'error' as const
|
|
case ComplianceStatus.Warning:
|
|
return 'warning' as const
|
|
case ComplianceStatus.NonCritical:
|
|
return 'success' as const
|
|
default:
|
|
return 'success' as const
|
|
}
|
|
})
|
|
|
|
const ringClass = computed(() => {
|
|
switch (props.status) {
|
|
case ComplianceStatus.Critical:
|
|
return 'text-red-500'
|
|
case ComplianceStatus.Warning:
|
|
return 'text-yellow-500'
|
|
case ComplianceStatus.NonCritical:
|
|
return 'text-green-500'
|
|
default:
|
|
return 'text-green-500'
|
|
}
|
|
})
|
|
|
|
const iconClass = computed(() => {
|
|
switch (props.status) {
|
|
case ComplianceStatus.Critical:
|
|
return 'text-red-500 w-6 h-6'
|
|
case ComplianceStatus.Warning:
|
|
return 'text-yellow-500 w-6 h-6'
|
|
case ComplianceStatus.NonCritical:
|
|
return 'text-green-500 w-6 h-6'
|
|
default:
|
|
return 'text-green-500 w-6 h-6'
|
|
}
|
|
})
|
|
|
|
const progressValue = computed(() => {
|
|
switch (props.status) {
|
|
case ComplianceStatus.Critical:
|
|
return 100
|
|
case ComplianceStatus.Warning:
|
|
return 60
|
|
case ComplianceStatus.NonCritical:
|
|
return 30
|
|
default:
|
|
return 30
|
|
}
|
|
})
|
|
|
|
const progressOffset = computed(() => {
|
|
return circumference - (progressValue.value / 100) * circumference
|
|
})
|
|
|
|
watch(
|
|
() => props.status,
|
|
() => {
|
|
const element = document.querySelector('.shield-ring')
|
|
if (element) {
|
|
element.classList.add('status-change-pulse')
|
|
setTimeout(() => {
|
|
element.classList.remove('status-change-pulse')
|
|
}, 600)
|
|
}
|
|
}
|
|
)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.shield-ring {
|
|
transition: color 300ms ease-in-out;
|
|
}
|
|
|
|
.ring-progress {
|
|
transition: stroke-dashoffset 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.shield-icon {
|
|
transition: color 300ms ease-in-out;
|
|
}
|
|
|
|
.status-badge {
|
|
transition: all 300ms ease-in-out;
|
|
}
|
|
|
|
@keyframes pulse-ring {
|
|
0% {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: scale(1.1);
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.status-change-pulse {
|
|
animation: pulse-ring 600ms ease-in-out;
|
|
}
|
|
|
|
.shield-pulse-enter-active,
|
|
.shield-pulse-leave-active {
|
|
transition: all 300ms ease-in-out;
|
|
}
|
|
|
|
.shield-pulse-enter-from {
|
|
opacity: 0;
|
|
transform: scale(0.8);
|
|
}
|
|
|
|
.shield-pulse-leave-to {
|
|
opacity: 0;
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.shield-icon {
|
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
|
}
|
|
</style>
|