1
0

Доработка сессий

This commit is contained in:
2026-01-18 21:29:28 +07:00
parent 6928687982
commit 7d7b817d7e
7 changed files with 126 additions and 67 deletions

View File

@@ -54,7 +54,13 @@ export const authApi = {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'logout' })
}, true) // skipSessionCheck — это выход
}, true), // skipSessionCheck — это выход
logoutAll: () => request('/api/user', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'logout_all' })
}, true) // skipSessionCheck — выход со всех устройств
}
// ==================== PROJECTS ====================

View File

@@ -11,10 +11,12 @@
<button
v-if="dialogShowDiscard"
class="btn-discard"
:class="dialogDiscardVariant"
@click="handleDiscard"
:disabled="loading"
>
{{ dialogDiscardText }}
<span v-if="loading === 'discard'" class="btn-loader"></span>
<span v-else>{{ dialogDiscardText }}</span>
</button>
<button
class="btn-confirm"
@@ -22,7 +24,7 @@
@click="handleConfirm"
:disabled="loading"
>
<span v-if="loading" class="btn-loader"></span>
<span v-if="loading === 'confirm'" class="btn-loader"></span>
<span v-else>{{ dialogConfirmText }}</span>
</button>
</div>
@@ -56,10 +58,16 @@ const props = defineProps({
default: undefined
},
variant: String,
discardVariant: String,
// Async callback для подтверждения — сам управляет loading
action: {
type: Function,
default: null
},
// Async callback для discard (опционально)
discardAction: {
type: Function,
default: null
}
})
@@ -71,19 +79,20 @@ 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 dialogDiscardText = computed(() => props.discardText ?? config.value.discardText ?? 'Не сохранять')
const dialogShowDiscard = computed(() => props.showDiscard ?? config.value.showDiscard ?? false)
const dialogVariant = computed(() => props.variant ?? config.value.variant ?? 'default')
const dialogDiscardVariant = computed(() => props.discardVariant ?? config.value.discardVariant ?? 'default')
const emit = defineEmits(['confirm', 'cancel', 'discard'])
// Внутреннее состояние загрузки
const loading = ref(false)
// Внутреннее состояние загрузки: null | 'confirm' | 'discard'
const loading = ref(null)
// Сброс состояния при закрытии диалога
watch(() => props.show, (newVal) => {
if (!newVal) {
loading.value = false
loading.value = null
}
})
@@ -92,7 +101,7 @@ const handleConfirm = async () => {
// Если есть async action — вызываем его и управляем loading
if (props.action) {
loading.value = true
loading.value = 'confirm'
try {
await props.action()
// Успех — эмитим confirm для закрытия
@@ -101,7 +110,7 @@ const handleConfirm = async () => {
console.error('ConfirmDialog action failed:', e)
// При ошибке — не закрываем диалог
} finally {
loading.value = false
loading.value = null
}
} else {
// Простой режим — просто эмитим
@@ -114,9 +123,26 @@ const handleCancel = () => {
emit('cancel')
}
const handleDiscard = () => {
const handleDiscard = async () => {
if (loading.value) return
emit('discard')
// Если есть async discardAction — вызываем его и управляем loading
if (props.discardAction) {
loading.value = 'discard'
try {
await props.discardAction()
// Успех — эмитим discard для закрытия
emit('discard')
} catch (e) {
console.error('ConfirmDialog discardAction failed:', e)
// При ошибке — не закрываем диалог
} finally {
loading.value = null
}
} else {
// Простой режим — просто эмитим
emit('discard')
}
}
</script>
@@ -196,6 +222,30 @@ const handleDiscard = () => {
background: rgba(239, 68, 68, 0.25);
}
.btn-discard.warning {
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
}
.btn-discard.warning:hover {
background: rgba(245, 158, 11, 0.25);
color: #fcd34d;
}
.btn-discard.danger {
background: #ef4444;
color: #fff;
}
.btn-discard.danger:hover {
background: #dc2626;
}
.btn-discard:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-confirm {
background: var(--accent);
color: #000;
@@ -206,21 +256,23 @@ const handleDiscard = () => {
}
.btn-confirm.danger {
background: #ef4444;
color: #fff;
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
.btn-confirm.danger:hover {
background: #dc2626;
background: rgba(239, 68, 68, 0.25);
color: #fca5a5;
}
.btn-confirm.warning {
background: #f59e0b;
color: #000;
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
}
.btn-confirm.warning:hover {
background: #d97706;
background: rgba(245, 158, 11, 0.25);
color: #fcd34d;
}
.btn-confirm:disabled {

View File

@@ -11,7 +11,8 @@
<ConfirmDialog
v-model:show="showDialog"
type="logout"
:action="logout"
:action="logoutAll"
:discard-action="logoutCurrent"
/>
</template>
@@ -38,13 +39,22 @@ const router = useRouter()
const store = useProjectsStore()
const showDialog = ref(false)
const logout = async () => {
// Выход с текущей сессии (discard action)
const logoutCurrent = async () => {
clearAuthCache()
await authApi.logout()
store.reset()
router.push('/login')
}
// Выход со всех сессий (confirm action)
const logoutAll = async () => {
clearAuthCache()
await authApi.logoutAll()
store.reset()
router.push('/login')
}
const refreshIcons = () => {
if (window.lucide) window.lucide.createIcons()
}

View File

@@ -92,12 +92,15 @@ export const DIALOGS = {
variant: 'danger'
},
// Выход из системы
// Выход из системы (3 кнопки)
logout: {
title: 'Выйти из аккаунта?',
message: 'Вы будете перенаправлены<br>на страницу входа.',
confirmText: 'Выйти',
variant: 'warning'
message: 'Выберите, откуда хотите выйти',
confirmText: 'Все сессии',
discardText: 'Текущая сессия',
showDiscard: true,
variant: 'danger',
discardVariant: 'warning'
},
// Удаление отдела

View File

@@ -16,10 +16,6 @@
<div class="float-icon icon-12"><i data-lucide="message-circle"></i></div>
</div>
<!-- Градиентные сферы на фоне -->
<div class="bg-glow glow-2"></div>
<div class="bg-glow glow-3"></div>
<!-- Контент авторизации -->
<div class="login-content" :class="{ 'is-loading': isProcessing, 'is-success': showSuccess }">
@@ -489,44 +485,6 @@ watch([showSuccess, isRegisterMode], () => {
padding: 20px;
}
/* Градиентные сферы на фоне */
.bg-glow {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.4;
pointer-events: none;
}
.glow-2 {
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(96, 165, 250, 0.25) 0%, transparent 70%);
bottom: -150px;
left: -100px;
animation: glowFloat2 10s ease-in-out infinite;
}
.glow-3 {
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(244, 114, 182, 0.2) 0%, transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: glowFloat3 12s ease-in-out infinite;
}
@keyframes glowFloat2 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(40px, -20px) scale(1.05); }
}
@keyframes glowFloat3 {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; }
50% { transform: translate(-50%, -50%) scale(1.2); opacity: 0.5; }
}
/* Левитирующие иконки */
.floating-icons {
position: absolute;