feat: улучшил создание лекций

This commit is contained in:
2026-05-24 21:32:22 +03:00
parent 99d25adbb1
commit e56b577772
3 changed files with 360 additions and 177 deletions
+31 -176
View File
@@ -14,6 +14,7 @@ import type {
import GlassCard from '@/components/ui/GlassCard.vue'
import DataTable from '@/components/ui/DataTable.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
import CreateLectureModal from '@/components/admin/CreateLectureModal.vue'
type TabKey = 'lectures' | 'courses' | 'rooms' | 'tags'
type TabConfig = {
@@ -34,19 +35,7 @@ const syncError = ref('')
const syncErrorDetails = ref<string[]>([])
const syncStatus = ref<SyncStatusDto | null>(null)
const syncResult = ref<SyncResultDto | null>(null)
const creatingDummyLecture = ref(false)
const dummyTimeOptions = [
{ label: 'Через 3ч 5м', minutes: 185 },
{ label: 'Через 1ч 5м', minutes: 65 },
{ label: 'Через 5м', minutes: 5 },
]
const dummyLectureForm = ref({
title: 'Фиктивная лекция для проверки уведомлений',
offsetMinutes: 185,
durationMinutes: 60,
maxEnrollments: 100,
courseId: null as number | null,
})
const isCreateLectureModalOpen = ref(false)
const addToast = inject('addToast') as
| ((message: string, type?: 'success' | 'error' | 'info') => void)
| undefined
@@ -197,60 +186,14 @@ async function loadData() {
loading.value = false
}
function getDummyLectureCourseId() {
const selectedCourseId = dummyLectureForm.value.courseId
if (selectedCourseId && courses.value.some((course) => course.id === selectedCourseId))
return selectedCourseId
return courses.value[0]?.id ?? null
async function handleLectureCreated(lecture: LectureDto) {
lectures.value = [lecture, ...lectures.value]
activeTab.value = 'lectures'
await loadData()
}
async function createDummyLecture() {
const courseId = getDummyLectureCourseId()
const title = dummyLectureForm.value.title.trim()
const durationMinutes = Math.max(5, Number(dummyLectureForm.value.durationMinutes) || 60)
const maxEnrollments = Math.max(1, Number(dummyLectureForm.value.maxEnrollments) || 100)
if (!courseId) {
addToast?.('Нужен хотя бы один курс, чтобы создать фиктивную лекцию.', 'error')
activeTab.value = 'courses'
return
}
if (!title) {
addToast?.('Укажите название фиктивной лекции.', 'error')
return
}
creatingDummyLecture.value = true
try {
const location = locations.value[0]
const startsAt = new Date(Date.now() + dummyLectureForm.value.offsetMinutes * 60_000)
const endsAt = new Date(startsAt.getTime() + durationMinutes * 60_000)
const createdLecture = await lecturesApi.create({
courseId,
teacherId: null,
locationId: location?.id ?? null,
title,
description:
'Фиктивная лекция создана из админки для проверки системы отзывов и напоминаний о начале лекции.',
format: location ? 'Offline' : 'Online',
startsAt: startsAt.toISOString(),
endsAt: endsAt.toISOString(),
isOpen: true,
maxEnrollments,
onlineUrl: null,
})
lectures.value = [createdLecture, ...lectures.value]
activeTab.value = 'lectures'
addToast?.(`Фиктивная лекция создана: ${startsAt.toLocaleString('ru-RU')}.`, 'success')
await loadData()
} catch (err) {
const message = err instanceof Error ? err.message : 'Не удалось создать фиктивную лекцию.'
const details = extractApiErrorDetails(err)
addToast?.(details.length ? `${message}: ${details.join('; ')}` : message, 'error')
} finally {
creatingDummyLecture.value = false
}
function handleLectureMissingCourse() {
activeTab.value = 'courses'
}
async function refreshSyncStatus() {
@@ -345,9 +288,14 @@ onMounted(() => {
<div class="admin-lectures page-content">
<div class="header">
<h1 class="page-title">Управление лекциями и справочниками</h1>
<button class="btn-secondary" type="button" :disabled="loading" @click="loadData">
Обновить
</button>
<div class="header-actions">
<button class="btn-primary" type="button" @click="isCreateLectureModalOpen = true">
Создать лекцию
</button>
<button class="btn-secondary" type="button" :disabled="loading" @click="loadData">
Обновить
</button>
</div>
</div>
<div class="tabs">
@@ -374,80 +322,6 @@ onMounted(() => {
<DataTable :columns="current.columns" :rows="current.rows" />
</GlassCard>
<GlassCard>
<div class="section-heading">
<div>
<div class="section-title">Фиктивная лекция</div>
<div class="sync-meta">Для ручной проверки отзывов и уведомлений</div>
</div>
</div>
<form class="form" @submit.prevent="createDummyLecture">
<label>Название</label>
<input
v-model="dummyLectureForm.title"
class="glass-input"
placeholder="Например, тест уведомлений"
/>
<label>Старт</label>
<div class="time-options">
<button
v-for="option in dummyTimeOptions"
:key="option.minutes"
class="time-option"
:class="{
active: dummyLectureForm.offsetMinutes === option.minutes,
}"
type="button"
@click="dummyLectureForm.offsetMinutes = option.minutes"
>
{{ option.label }}
</button>
</div>
<label>Курс</label>
<select v-model="dummyLectureForm.courseId" class="glass-input">
<option :value="null">Первый доступный курс</option>
<option v-for="course in courses" :key="course.id" :value="course.id">
{{ course.name || `Курс #${course.id}` }}
</option>
</select>
<div class="dummy-fields">
<label>
<span>Длительность, мин</span>
<input
v-model.number="dummyLectureForm.durationMinutes"
class="glass-input"
min="5"
type="number"
/>
</label>
<label>
<span>Мест</span>
<input
v-model.number="dummyLectureForm.maxEnrollments"
class="glass-input"
min="1"
type="number"
/>
</label>
</div>
<div class="dummy-note">
Лекция создаётся открытой для записи. Аудитория подставится автоматически, если она уже
есть в справочнике.
</div>
<div class="form-actions">
<button class="btn-primary" type="submit" :disabled="creatingDummyLecture || loading">
{{ creatingDummyLecture ? 'Создаём...' : 'Создать фиктивную лекцию' }}
</button>
</div>
</form>
</GlassCard>
<GlassCard>
<div class="section-heading">
<div>
@@ -511,6 +385,15 @@ onMounted(() => {
</form>
</GlassCard>
</div>
<CreateLectureModal
v-model="isCreateLectureModalOpen"
:courses="courses"
:locations="locations"
:loading="loading"
@created="handleLectureCreated"
@missing-course="handleLectureMissingCourse"
/>
</div>
</template>
@@ -527,6 +410,12 @@ onMounted(() => {
gap: 12px;
flex-wrap: wrap;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.tabs {
display: inline-flex;
width: fit-content;
@@ -574,40 +463,6 @@ onMounted(() => {
gap: 10px;
flex-wrap: wrap;
}
.time-options {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.time-option {
border: 1px solid var(--color-border-glass);
border-radius: 999px;
padding: 8px 12px;
background: var(--color-white-a72);
color: var(--color-text-secondary);
cursor: pointer;
}
.time-option.active {
background: var(--color-primary-a18);
border-color: var(--color-primary-light);
color: var(--color-primary-dark);
font-weight: 600;
}
.dummy-fields {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
}
.dummy-fields label {
display: flex;
flex-direction: column;
gap: 6px;
}
.dummy-note {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.type-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
@@ -49,7 +49,7 @@ watch(() => auth.user?.id, fetchTeacherLectures)
<div class="section-title">Заметность за пределами направления</div>
<div class="visibility">
<div class="visibility-meta">
{{ visibility }}% студентов из других институтов Цель 50%
{{ visibility }}% студентов из других институтов
</div>
<ProgressBar :value="visibility" :max="100" />
</div>