1
0
Files
TaskBoard/front_vue/src/components/ConfirmDialog.vue
Falknat f856e68ea8 Мелкие правки фронта
Улучшены окна подтверждений
2026-01-16 10:22:32 +07:00

268 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<Transition name="dialog">
<div v-if="show" class="dialog-overlay" @click.self="handleCancel">
<div class="dialog">
<h3>{{ dialogTitle }}</h3>
<p v-html="dialogMessage"></p>
<div class="dialog-buttons">
<button class="btn-cancel" @click="handleCancel" :disabled="loading">
{{ dialogCancelText }}
</button>
<button
v-if="dialogShowDiscard"
class="btn-discard"
@click="handleDiscard"
:disabled="loading"
>
{{ dialogDiscardText }}
</button>
<button
class="btn-confirm"
:class="dialogVariant"
@click="handleConfirm"
:disabled="loading"
>
<span v-if="loading" class="btn-loader"></span>
<span v-else>{{ dialogConfirmText }}</span>
</button>
</div>
</div>
</div>
</Transition>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { DIALOGS } from '../stores/dialogs'
const props = defineProps({
show: {
type: Boolean,
default: false
},
// Тип диалога из конфига (archive, restore, deleteTask, etc.)
type: {
type: String,
default: null
},
// Прямые props (переопределяют type если заданы)
title: String,
message: String,
confirmText: String,
cancelText: String,
discardText: String,
showDiscard: Boolean,
variant: String,
// Async callback для подтверждения — сам управляет loading
action: {
type: Function,
default: null
}
})
// Получаем конфиг по типу
const config = computed(() => props.type ? DIALOGS[props.type] : {})
// Computed свойства с fallback: props → config → default
const dialogTitle = computed(() => props.title ?? config.value.title ?? 'Подтверждение')
const dialogMessage = computed(() => props.message ?? config.value.message ?? 'Вы уверены?')
const dialogConfirmText = computed(() => props.confirmText ?? config.value.confirmText ?? 'Подтвердить')
const dialogCancelText = computed(() => props.cancelText ?? 'Отмена')
const dialogDiscardText = computed(() => props.discardText ?? 'Не сохранять')
const dialogShowDiscard = computed(() => props.showDiscard ?? config.value.showDiscard ?? false)
const dialogVariant = computed(() => props.variant ?? config.value.variant ?? 'default')
const emit = defineEmits(['confirm', 'cancel', 'discard'])
// Внутреннее состояние загрузки
const loading = ref(false)
// Сброс состояния при закрытии диалога
watch(() => props.show, (newVal) => {
if (!newVal) {
loading.value = false
}
})
const handleConfirm = async () => {
if (loading.value) return
// Если есть async action — вызываем его и управляем loading
if (props.action) {
loading.value = true
try {
await props.action()
// Успех — эмитим confirm для закрытия
emit('confirm')
} catch (e) {
console.error('ConfirmDialog action failed:', e)
// При ошибке — не закрываем диалог
} finally {
loading.value = false
}
} else {
// Простой режим — просто эмитим
emit('confirm')
}
}
const handleCancel = () => {
if (loading.value) return
emit('cancel')
}
const handleDiscard = () => {
if (loading.value) return
emit('discard')
}
</script>
<style scoped>
.dialog-overlay {
position: fixed;
inset: 0;
background: var(--bg-body);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.dialog {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
width: 100%;
height: 100%;
padding: 24px;
}
.dialog h3 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.dialog p {
margin: 0 0 48px;
font-size: 16px;
color: var(--text-muted);
line-height: 1.5;
max-width: 320px;
}
.dialog-buttons {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-width: 320px;
}
.dialog-buttons button {
width: 100%;
padding: 16px 24px;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
font-family: inherit;
}
.btn-cancel {
background: rgba(255, 255, 255, 0.08);
color: var(--text-muted);
}
.btn-cancel:hover {
background: rgba(255, 255, 255, 0.12);
color: var(--text-primary);
}
.btn-discard {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.btn-discard:hover {
background: rgba(239, 68, 68, 0.25);
}
.btn-confirm {
background: var(--accent);
color: #000;
}
.btn-confirm:hover {
background: #00e6b8;
}
.btn-confirm.danger {
background: #ef4444;
color: #fff;
}
.btn-confirm.danger:hover {
background: #dc2626;
}
.btn-confirm.warning {
background: #f59e0b;
color: #000;
}
.btn-confirm.warning:hover {
background: #d97706;
}
.btn-confirm:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-loader {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Transition */
.dialog-enter-active,
.dialog-leave-active {
transition: opacity 0.25s ease;
}
.dialog-enter-active .dialog,
.dialog-leave-active .dialog {
transition: transform 0.25s ease;
}
.dialog-enter-from,
.dialog-leave-to {
opacity: 0;
}
.dialog-enter-from .dialog {
transform: translateY(20px);
}
.dialog-leave-to .dialog {
transform: translateY(-20px);
}
</style>