Dev #11

Merged
serega404 merged 87 commits from dev into main 2026-05-25 03:22:55 +03:00
5 changed files with 182 additions and 97 deletions
Showing only changes of commit 32f28898f5 - Show all commits
+4 -38
View File
@@ -175,17 +175,13 @@ onBeforeUnmount(() => {
</div> </div>
</div> </div>
<ModalDialog v-model="isCoinDialogOpen"> <ModalDialog v-model="isCoinDialogOpen" title="Монеты UniVerse" icon="coin" size="sm">
<div class="coin-dialog-content">
<div class="coin-dialog-title">
<AppIcon class="coin-dialog-icon" icon="coin" :size="22" />
<span>Монеты UniVerse</span>
</div>
<p> <p>
В будущем монеты можно будет использовать во внутреннем магазине. Сейчас магазин еще не запущен, поэтому монеты просто копятся на вашем балансе. В будущем монеты можно будет использовать во внутреннем магазине. Сейчас магазин еще не запущен, поэтому монеты просто копятся на вашем балансе.
</p> </p>
<button class="btn-primary coin-dialog-ok" type="button" @click="closeCoinDialog">ОК</button> <template #footer>
</div> <button class="btn-primary" type="button" @click="closeCoinDialog">ОК</button>
</template>
</ModalDialog> </ModalDialog>
</header> </header>
</template> </template>
@@ -232,36 +228,6 @@ onBeforeUnmount(() => {
gap: 12px; gap: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
.coin-dialog-content {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 14px;
}
.coin-dialog-title {
display: flex;
align-items: center;
gap: 9px;
color: var(--color-text);
font-size: 17px;
font-weight: 800;
}
.coin-dialog-icon {
flex: 0 0 auto;
color: var(--color-coin-chip-text);
}
.coin-dialog-content p {
margin: 0;
color: var(--color-text-secondary);
font-size: 14px;
line-height: 1.5;
}
.coin-dialog-ok {
min-width: 92px;
align-self: center;
justify-content: center;
margin-top: 2px;
}
.level-chip { .level-chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -20,36 +20,20 @@ function openMyLectures() {
</script> </script>
<template> <template>
<ModalDialog :model-value="modelValue" title="Лимит записей достигнут" @update:model-value="emit('update:modelValue', $event)"> <ModalDialog
<div class="limit-modal"> :model-value="modelValue"
title="Лимит записей достигнут"
icon="alert-triangle"
size="sm"
@update:model-value="emit('update:modelValue', $event)"
>
<p> <p>
Все доступные слоты записи уже заняты. Чтобы записаться на новую лекцию, отмените одну из текущих записей Все доступные слоты записи уже заняты. Чтобы записаться на новую лекцию, отмените одну из текущих записей
или повысьте уровень. или повысьте уровень.
</p> </p>
<div class="limit-actions"> <template #footer>
<button class="btn-secondary" type="button" @click="close">Понятно</button> <button class="btn-secondary" type="button" @click="close">Понятно</button>
<button class="btn-primary" type="button" @click="openMyLectures">Мои записи</button> <button class="btn-primary" type="button" @click="openMyLectures">Мои записи</button>
</div> </template>
</div>
</ModalDialog> </ModalDialog>
</template> </template>
<style scoped>
.limit-modal {
display: flex;
flex-direction: column;
gap: 16px;
}
.limit-modal p {
margin: 0;
color: var(--color-text-secondary);
font-size: 14px;
line-height: 1.5;
}
.limit-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
</style>
+154 -19
View File
@@ -1,19 +1,75 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ import { onBeforeUnmount, watch } from 'vue'
import AppIcon from '@/components/ui/AppIcon.vue'
const props = withDefaults(defineProps<{
title?: string title?: string
description?: string
icon?: string
modelValue?: boolean modelValue?: boolean
}>() size?: 'sm' | 'md' | 'lg'
showClose?: boolean
closeOnOverlay?: boolean
closeOnEscape?: boolean
}>(), {
size: 'md',
showClose: true,
closeOnOverlay: true,
closeOnEscape: true,
})
const emit = defineEmits<{ 'update:modelValue': [v: boolean] }>() const emit = defineEmits<{ 'update:modelValue': [v: boolean] }>()
function close() {
emit('update:modelValue', false)
}
function closeFromOverlay() {
if (props.closeOnOverlay) close()
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && props.modelValue && props.closeOnEscape) close()
}
watch(
() => props.modelValue,
(isOpen) => {
if (isOpen) document.addEventListener('keydown', handleKeydown)
else document.removeEventListener('keydown', handleKeydown)
},
{ immediate: true }
)
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKeydown)
})
</script> </script>
<template> <template>
<Teleport to="body"> <Teleport to="body">
<Transition name="modal"> <Transition name="modal">
<div v-if="modelValue" class="modal-overlay" @click.self="emit('update:modelValue', false)"> <div v-if="modelValue" class="modal-overlay" @click.self="closeFromOverlay">
<div class="modal-box"> <section
<div class="modal-header" v-if="title"> class="modal-box"
<span class="modal-title">{{ title }}</span> :class="`modal-box-${size}`"
<button class="modal-close" @click="emit('update:modelValue', false)">×</button> role="dialog"
aria-modal="true"
:aria-label="title"
>
<div class="modal-header" v-if="title || description || icon || $slots.header || showClose">
<slot name="header">
<div class="modal-heading">
<span v-if="icon" class="modal-icon" :class="{ 'modal-icon-warning': icon === 'alert-triangle' }">
<AppIcon :icon="icon" :size="20" />
</span>
<div class="modal-heading-text">
<h2 v-if="title" class="modal-title">{{ title }}</h2>
<p v-if="description" class="modal-description">{{ description }}</p>
</div>
</div>
</slot>
<button v-if="showClose" class="modal-close" type="button" aria-label="Закрыть" @click="close">×</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<slot /> <slot />
@@ -21,7 +77,7 @@ const emit = defineEmits<{ 'update:modelValue': [v: boolean] }>()
<div class="modal-footer" v-if="$slots.footer"> <div class="modal-footer" v-if="$slots.footer">
<slot name="footer" /> <slot name="footer" />
</div> </div>
</div> </section>
</div> </div>
</Transition> </Transition>
</Teleport> </Teleport>
@@ -42,39 +98,118 @@ const emit = defineEmits<{ 'update:modelValue': [v: boolean] }>()
.modal-box { .modal-box {
background: var(--color-surface); background: var(--color-surface);
border: 1px solid var(--color-border-glass); border: 1px solid var(--color-border-glass);
border-radius: var(--radius-lg); border-radius: var(--radius-md);
box-shadow: 0 24px 64px var(--color-black-a20); box-shadow: 0 24px 64px var(--color-black-a20);
width: 100%; width: 100%;
max-width: 520px;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
} }
.modal-box-sm { max-width: 420px; }
.modal-box-md { max-width: 520px; }
.modal-box-lg { max-width: 680px; }
.modal-header { .modal-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 18px 22px; gap: 14px;
border-bottom: 1px solid var(--color-border-glass); padding: 18px 22px 16px;
}
.modal-heading {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.modal-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 34px;
width: 34px;
height: 34px;
color: var(--color-primary-dark);
background: var(--color-primary-a10);
border: 1px solid var(--color-primary-a20);
border-radius: var(--radius-sm);
}
.modal-icon-warning {
color: var(--color-warning);
background: var(--color-warning-bg-a90);
border-color: var(--color-warning-border);
}
.modal-heading-text {
min-width: 0;
}
.modal-title {
margin: 0;
font-size: 17px;
font-weight: 700;
line-height: 1.2;
color: var(--color-text);
}
.modal-description {
margin: 5px 0 0;
color: var(--color-text-secondary);
font-size: 13px;
line-height: 1.45;
} }
.modal-title { font-size: 17px; font-weight: 700; color: var(--color-text); }
.modal-close { .modal-close {
background: none; flex: 0 0 auto;
border: none; display: inline-flex;
font-size: 22px; align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: var(--color-white-a70);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-sm);
font-size: 20px;
cursor: pointer; cursor: pointer;
color: var(--color-text-secondary); color: var(--color-text-secondary);
line-height: 1; line-height: 1;
transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.modal-close:hover {
background: var(--color-primary-a10);
border-color: var(--color-primary-a20);
color: var(--color-text);
}
.modal-body {
padding: 20px 22px;
color: var(--color-text-secondary);
font-size: 14px;
line-height: 1.5;
}
.modal-body :deep(p) {
margin: 0;
} }
.modal-close:hover { color: var(--color-text); }
.modal-body { padding: 20px 22px; }
.modal-footer { .modal-footer {
padding: 14px 22px; padding: 14px 22px;
border-top: 1px solid var(--color-border-glass);
display: flex; display: flex;
gap: 10px; gap: 10px;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap;
} }
.modal-enter-active, .modal-leave-active { transition: all 0.25s ease; } .modal-enter-active, .modal-leave-active { transition: all 0.25s ease; }
.modal-enter-from, .modal-leave-to { opacity: 0; } .modal-enter-from, .modal-leave-to { opacity: 0; }
.modal-enter-from .modal-box, .modal-leave-to .modal-box { transform: scale(0.93); } .modal-enter-from .modal-box, .modal-leave-to .modal-box { transform: scale(0.93); }
@media (max-width: 520px) {
.modal-overlay {
align-items: flex-end;
padding: 12px;
}
.modal-box {
max-height: 88vh;
}
.modal-header,
.modal-body,
.modal-footer {
padding-left: 16px;
padding-right: 16px;
}
.modal-footer > :deep(button) {
flex: 1 1 140px;
}
}
</style> </style>
+3 -3
View File
@@ -268,7 +268,7 @@ function isRegistered(id: string) {
</GlassCard> </GlassCard>
</div> </div>
<ModalDialog v-model="filtersOpen" title="Фильтры"> <ModalDialog v-model="filtersOpen" title="Фильтры" icon="search" size="lg">
<div class="modal-filters"> <div class="modal-filters">
<label>Дата</label> <label>Дата</label>
<select v-model="dateFilter" class="glass-input"> <select v-model="dateFilter" class="glass-input">
@@ -303,8 +303,8 @@ function isRegistered(id: string) {
</label> </label>
</div> </div>
<template #footer> <template #footer>
<button class="btn-secondary" @click="filtersOpen = false">Закрыть</button> <button class="btn-secondary" type="button" @click="filtersOpen = false">Закрыть</button>
<button class="btn-primary" @click="filtersOpen = false">Применить</button> <button class="btn-primary" type="button" @click="filtersOpen = false">Применить</button>
</template> </template>
</ModalDialog> </ModalDialog>
@@ -85,11 +85,11 @@ async function confirmCancel() {
</GlassCard> </GlassCard>
</div> </div>
<ModalDialog v-model="cancelModal" title="Отменить запись?"> <ModalDialog v-model="cancelModal" title="Отменить запись?" icon="alert-triangle" size="sm">
<p>Вы уверены, что хотите отменить запись на лекцию? Место будет освобождено для других студентов.</p> <p>Вы уверены, что хотите отменить запись на лекцию? Место будет освобождено для других студентов.</p>
<template #footer> <template #footer>
<button class="btn-secondary" @click="cancelModal = false">Нет</button> <button class="btn-secondary" type="button" @click="cancelModal = false">Нет</button>
<button class="btn-danger" @click="confirmCancel">Да, отменить</button> <button class="btn-danger" type="button" @click="confirmCancel">Да, отменить</button>
</template> </template>
</ModalDialog> </ModalDialog>
</div> </div>