1
0
Files
TaskBoard/front_vue/src/components/ConfirmDialog.vue
Falknat 3258fa9137 Исправления фронта
Множество оптимизаций по фронту
2026-01-16 10:15:33 +07:00

261 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: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.dialog {
background: var(--bg-secondary);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 32px;
max-width: 420px;
text-align: center;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4);
}
.dialog h3 {
margin: 0 0 12px;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.dialog p {
margin: 0 0 24px;
font-size: 14px;
color: var(--text-muted);
line-height: 1.5;
}
.dialog-buttons {
display: flex;
gap: 12px;
justify-content: center;
}
.dialog-buttons button {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
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: 16px;
height: 16px;
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: all 0.2s ease;
}
.dialog-enter-active .dialog,
.dialog-leave-active .dialog {
transition: transform 0.2s ease;
}
.dialog-enter-from,
.dialog-leave-to {
opacity: 0;
}
.dialog-enter-from .dialog,
.dialog-leave-to .dialog {
transform: scale(0.95);
}
</style>