109 lines
3.9 KiB
Vue
109 lines
3.9 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<div v-if="visible" class="modal-overlay" @click.self="handleCancel">
|
|
<div class="modal-panel max-w-[400px] w-full text-center p-6">
|
|
<!-- Icon -->
|
|
<div
|
|
class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4"
|
|
:class="{
|
|
'bg-red-500/10 text-red-500': variant === 'danger',
|
|
'bg-amber-500/10 text-amber-500': variant === 'warning',
|
|
'bg-brand-500/10 text-brand-500': variant === 'info'
|
|
}"
|
|
>
|
|
<svg v-if="variant === 'danger'" class="w-8 h-8" viewBox="0 0 24 24" fill="none">
|
|
<path d="M12 9V13M12 17H12.01M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
<svg v-else-if="variant === 'warning'" class="w-8 h-8" viewBox="0 0 24 24" fill="none">
|
|
<path d="M12 9V13M12 17H12.01M10.29 3.86L1.82 18C1.64 18.3 1.55 18.64 1.55 19C1.55 19.36 1.64 19.7 1.82 20C2 20.3 2.26 20.56 2.56 20.73C2.86 20.91 3.2 21 3.56 21H20.44C20.8 21 21.14 20.91 21.44 20.73C21.74 20.56 22 20.3 22.18 20C22.36 19.7 22.45 19.36 22.45 19C22.45 18.64 22.36 18.3 22.18 18L13.71 3.86C13.53 3.56 13.27 3.31 12.97 3.14C12.67 2.97 12.34 2.88 12 2.88C11.66 2.88 11.33 2.97 11.03 3.14C10.73 3.31 10.47 3.56 10.29 3.86Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
<svg v-else class="w-8 h-8" viewBox="0 0 24 24" fill="none">
|
|
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-width="2"/>
|
|
<path d="M12 8V12M12 16H12.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<h3 class="mb-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100">{{ title }}</h3>
|
|
<p class="mb-6 text-sm text-zinc-500 dark:text-zinc-400 leading-relaxed">{{ message }}</p>
|
|
|
|
<div class="flex gap-3 justify-center">
|
|
<button
|
|
type="button"
|
|
class="btn-secondary flex-1 max-w-[150px]"
|
|
@click="handleCancel"
|
|
:disabled="loading"
|
|
>
|
|
{{ cancelText }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="flex-1 max-w-[150px]"
|
|
:class="confirmButtonClass"
|
|
@click="handleConfirm"
|
|
:disabled="loading"
|
|
>
|
|
<span v-if="loading" class="inline-block w-4 h-4 border-2 border-current border-r-transparent rounded-full animate-spin mr-1"></span>
|
|
{{ confirmText }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, defineProps, defineEmits } from 'vue'
|
|
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
title: {
|
|
type: String,
|
|
default: 'Confirm Action'
|
|
},
|
|
message: {
|
|
type: String,
|
|
default: 'Are you sure you want to proceed?'
|
|
},
|
|
confirmText: {
|
|
type: String,
|
|
default: 'Confirm'
|
|
},
|
|
cancelText: {
|
|
type: String,
|
|
default: 'Cancel'
|
|
},
|
|
variant: {
|
|
type: String,
|
|
default: 'info',
|
|
validator: (value) => ['info', 'warning', 'danger'].includes(value)
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['confirm', 'cancel'])
|
|
|
|
const confirmButtonClass = computed(() => {
|
|
switch (props.variant) {
|
|
case 'danger': return 'btn-danger'
|
|
case 'warning': return 'inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-amber-500 rounded-lg hover:bg-amber-600 transition-colors'
|
|
default: return 'btn-primary'
|
|
}
|
|
})
|
|
|
|
function handleConfirm() {
|
|
emit('confirm')
|
|
}
|
|
|
|
function handleCancel() {
|
|
if (!props.loading) {
|
|
emit('cancel')
|
|
}
|
|
}
|
|
</script>
|