Agregar modal personalizado de confirmación de salida
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 55s

- Reemplazar prompt del navegador por modal personalizado
- Modal muestra lista de campos modificados con estilo de la app
- Distingue entre almacenamiento local y servidor
- Incluye soporte para modo oscuro
- Mejora UX con diseño responsive
This commit is contained in:
2025-10-17 17:24:44 -06:00
parent 454d98eb97
commit 58a410c51d

View File

@@ -10,7 +10,7 @@
color="neutral" color="neutral"
variant="ghost" variant="ghost"
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
@click="$emit('close')" @click="handleClose"
> >
Cancelar Cancelar
</UButton> </UButton>
@@ -262,7 +262,7 @@
color="neutral" color="neutral"
variant="ghost" variant="ghost"
size="lg" size="lg"
@click="$emit('close')" @click="handleClose"
:disabled="isSubmitting" :disabled="isSubmitting"
> >
Cancelar Cancelar
@@ -311,6 +311,63 @@
</div> </div>
</template> </template>
</UModal> </UModal>
<!-- Modal de confirmación de salida -->
<UModal v-model:open="showExitConfirm">
<template #content>
<div class="exit-confirm-modal">
<div class="exit-modal-header">
<UIcon name="i-heroicons-exclamation-triangle" class="w-12 h-12 text-warning-500" />
<h3 class="exit-modal-title">¿Estás seguro de salir?</h3>
</div>
<div class="exit-modal-body">
<p class="exit-modal-message">
Tienes cambios pendientes en:
</p>
<ul class="exit-modal-fields">
<li v-for="field in modifiedFieldsList" :key="field" class="exit-field-item">
<UIcon name="i-heroicons-pencil-square" class="w-4 h-4 text-warning-500" />
{{ field }}
</li>
</ul>
<div class="exit-modal-notice">
<UIcon name="i-heroicons-information-circle" class="w-5 h-5" />
<p>
Estos cambios quedarán guardados <strong>localmente en tu navegador</strong>,
pero <strong class="text-error-600 dark:text-error-400">NO han sido enviados al servidor</strong>.
</p>
</div>
<div class="exit-modal-help">
<UIcon name="i-heroicons-light-bulb" class="w-5 h-5 text-primary-500" />
<p>
Para guardar definitivamente, haz clic en <strong>"Guardar cambios"</strong>.
</p>
</div>
</div>
<div class="exit-modal-actions">
<UButton
color="neutral"
variant="soft"
size="lg"
@click="showExitConfirm = false"
>
<UIcon name="i-heroicons-arrow-left" class="w-5 h-5" />
Continuar editando
</UButton>
<UButton
color="error"
size="lg"
@click="confirmExit"
>
<UIcon name="i-heroicons-arrow-right-on-rectangle" class="w-5 h-5" />
Salir sin guardar
</UButton>
</div>
</div>
</template>
</UModal>
</div> </div>
</template> </template>
@@ -319,12 +376,13 @@ const { user } = useAuthentik()
const toast = useToast() const toast = useToast()
// Emits // Emits
defineEmits(['close']) const emit = defineEmits(['close'])
// Estado del formulario // Estado del formulario
const isSubmitting = ref(false) const isSubmitting = ref(false)
const isUploading = ref(false) const isUploading = ref(false)
const showCamera = ref(false) const showCamera = ref(false)
const showExitConfirm = ref(false)
// Cookie para persistir cambios del formulario // Cookie para persistir cambios del formulario
const formCookie = useCookie<Record<string, string>>('profile-form-draft', { const formCookie = useCookie<Record<string, string>>('profile-form-draft', {
@@ -536,6 +594,23 @@ const handleAvatarCapture = async (imageBlob: Blob) => {
} }
} }
// Manejar intento de cerrar formulario
const handleClose = () => {
if (hasChanges.value) {
// Si hay cambios, mostrar modal de confirmación
showExitConfirm.value = true
} else {
// Si no hay cambios, cerrar directamente
emit('close')
}
}
// Confirmar salida sin guardar
const confirmExit = () => {
showExitConfirm.value = false
emit('close')
}
// Reiniciar formulario a valores originales // Reiniciar formulario a valores originales
const resetForm = () => { const resetForm = () => {
if (!confirm('¿Estás seguro de descartar todos los cambios?\n\nSe restaurarán los valores originales y se eliminará el borrador guardado.')) { if (!confirm('¿Estás seguro de descartar todos los cambios?\n\nSe restaurarán los valores originales y se eliminará el borrador guardado.')) {
@@ -746,6 +821,120 @@ const removeAvatar = async () => {
overflow-y: auto; overflow-y: auto;
} }
/* Modal de confirmación de salida */
.exit-confirm-modal {
padding: 2rem;
max-width: 600px;
margin: 0 auto;
}
.exit-modal-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.exit-modal-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-gray-900);
margin: 0;
}
.exit-modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.exit-modal-message {
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
margin: 0;
}
.exit-modal-fields {
list-style: none;
padding: 1rem;
margin: 0;
background: rgba(var(--color-warning-500), 0.1);
border-radius: 0.75rem;
border: 1px solid rgba(var(--color-warning-500), 0.3);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.exit-field-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gray-700);
}
.exit-modal-notice {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: rgba(var(--color-error-500), 0.1);
border-radius: 0.75rem;
border: 1px solid rgba(var(--color-error-500), 0.3);
}
.exit-modal-notice p {
margin: 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-700);
}
.exit-modal-help {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: rgba(var(--color-primary-500), 0.1);
border-radius: 0.75rem;
border: 1px solid rgba(var(--color-primary-500), 0.3);
}
.exit-modal-help p {
margin: 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-700);
}
.exit-modal-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
@media (max-width: 640px) {
.exit-confirm-modal {
padding: 1.5rem;
}
.exit-modal-title {
font-size: 1.25rem;
}
.exit-modal-actions {
flex-direction: column-reverse;
}
.exit-modal-actions button {
width: 100%;
}
}
.form-actions { .form-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -827,4 +1016,39 @@ const removeAvatar = async () => {
.dark .modal-title { .dark .modal-title {
color: var(--color-gray-100) !important; color: var(--color-gray-100) !important;
} }
.dark .exit-modal-title {
color: var(--color-gray-100) !important;
}
.dark .exit-modal-message {
color: var(--color-gray-200) !important;
}
.dark .exit-field-item {
color: var(--color-gray-300) !important;
}
.dark .exit-modal-fields {
background: rgba(var(--color-warning-500), 0.15) !important;
border-color: rgba(var(--color-warning-500), 0.4) !important;
}
.dark .exit-modal-notice {
background: rgba(var(--color-error-500), 0.15) !important;
border-color: rgba(var(--color-error-500), 0.4) !important;
}
.dark .exit-modal-notice p {
color: var(--color-gray-300) !important;
}
.dark .exit-modal-help {
background: rgba(var(--color-primary-500), 0.15) !important;
border-color: rgba(var(--color-primary-500), 0.4) !important;
}
.dark .exit-modal-help p {
color: var(--color-gray-300) !important;
}
</style> </style>