changes
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<DashboardLayout>
|
||||
<template #header-icon>
|
||||
<DashboardIcon name="photo" size="5" class="text-primary" />
|
||||
</template>
|
||||
<template #header-title>Создание слайдера</template>
|
||||
<template #header-subtitle>Заполните основные параметры</template>
|
||||
<template #header-actions>
|
||||
<a
|
||||
:href="route('dashboard.sliders.index')"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-all"
|
||||
>
|
||||
<DashboardIcon name="arrow-left" size="4" />
|
||||
Назад
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-line-2">
|
||||
<h3 class="text-base font-semibold text-foreground">Основные настройки</h3>
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="p-6 space-y-4">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Название слайдера *
|
||||
</label>
|
||||
<input
|
||||
id="title"
|
||||
v-model="form.title"
|
||||
type="text"
|
||||
required
|
||||
@input="generateSlug"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
placeholder="Например: Главный слайдер"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="slug" class="block text-sm font-medium text-foreground mb-1.5">
|
||||
URL-идентификатор *
|
||||
</label>
|
||||
<input
|
||||
id="slug"
|
||||
v-model="form.slug"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
placeholder="main-slider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
id="is_active"
|
||||
v-model="form.is_active"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-primary border-layer-line rounded focus:ring-primary/20"
|
||||
/>
|
||||
<label for="is_active" class="text-sm font-medium text-foreground">
|
||||
Активный слайдер
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg v-if="form.processing" class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Создать слайдер
|
||||
</button>
|
||||
<a
|
||||
:href="route('dashboard.sliders.index')"
|
||||
class="px-4 py-2 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-colors"
|
||||
>
|
||||
Отмена
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DashboardLayout from '../Components/DashboardLayout.vue';
|
||||
import DashboardIcon from '../Components/DashboardIcon.vue';
|
||||
|
||||
export default {
|
||||
name: 'CreateSlider',
|
||||
components: {
|
||||
DashboardLayout,
|
||||
DashboardIcon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
title: '',
|
||||
slug: '',
|
||||
is_active: true,
|
||||
processing: false
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.SET_DOCUMENT_TITLE('Создание слайдера');
|
||||
},
|
||||
methods: {
|
||||
generateSlug() {
|
||||
if (this.form.title && !this.form.slug) {
|
||||
this.form.slug = this.GENERATE_SLUG(this.form.title);
|
||||
}
|
||||
},
|
||||
submit() {
|
||||
this.form.processing = true
|
||||
this.$inertia.post(route('dashboard.sliders.store'), this.form, {
|
||||
onSuccess: () => {
|
||||
this.form.processing = false
|
||||
},
|
||||
onError: () => {
|
||||
this.form.processing = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,645 @@
|
||||
<template>
|
||||
<DashboardLayout>
|
||||
<template #header-icon>
|
||||
<DashboardIcon name="photo" size="5" class="text-primary" />
|
||||
</template>
|
||||
<template #header-title>Редактирование слайдера</template>
|
||||
<template #header-subtitle>{{ slider.title }}</template>
|
||||
<template #header-actions>
|
||||
<a
|
||||
:href="route('dashboard.sliders.index')"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-all"
|
||||
>
|
||||
<DashboardIcon name="arrow-left" size="4" />
|
||||
Назад
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-if="$page.props.flash?.success" class="mb-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<DashboardIcon name="check" size="5" class="text-emerald-600 flex-shrink-0 mt-0.5" />
|
||||
<span class="text-sm text-foreground font-medium">{{ $page.props.flash.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Slider Settings -->
|
||||
<div class="bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-line-2">
|
||||
<h3 class="text-base font-semibold text-foreground">Настройки слайдера</h3>
|
||||
</div>
|
||||
<form @submit.prevent="updateSlider" class="p-6 space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Название слайдера *
|
||||
</label>
|
||||
<input
|
||||
id="title"
|
||||
v-model="form.title"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="slug" class="block text-sm font-medium text-foreground mb-1.5">
|
||||
URL-идентификатор *
|
||||
</label>
|
||||
<input
|
||||
id="slug"
|
||||
v-model="form.slug"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
id="is_active"
|
||||
v-model="form.is_active"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-primary border-layer-line rounded focus:ring-primary/20"
|
||||
/>
|
||||
<label for="is_active" class="text-sm font-medium text-foreground">
|
||||
Активный слайдер
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg v-if="form.processing" class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Slides Section -->
|
||||
<div class="bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-line-2 flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold text-foreground">Слайды</h3>
|
||||
<button
|
||||
@click="showAddSlide = true"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all"
|
||||
>
|
||||
<DashboardIcon name="plus" size="4" />
|
||||
Добавить слайд
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Slide Form -->
|
||||
<div v-if="showAddSlide" class="p-6 border-b border-line-2 bg-surface/50 space-y-6">
|
||||
<h4 class="text-sm font-semibold text-foreground">
|
||||
{{ editingSlideId ? 'Редактировать слайд' : 'Новый слайд' }}
|
||||
</h4>
|
||||
|
||||
<!-- Quick Setup -->
|
||||
<div class="border border-layer-line rounded-lg p-4 space-y-4">
|
||||
<h5 class="text-sm font-medium text-foreground">Быстрая настройка слайда</h5>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Тип контента
|
||||
</label>
|
||||
<select
|
||||
v-model="slideForm.model_select"
|
||||
@change="onModelSelectChange"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
>
|
||||
<option value="Custom">Кастомная ссылка</option>
|
||||
<option value="Post">Новость</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="slideForm.model_select === 'Post'">
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Выбор элемента
|
||||
</label>
|
||||
<select
|
||||
v-model="slideForm.model"
|
||||
@change="onModelChange"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
>
|
||||
<option value="">Выберите новость</option>
|
||||
<option v-for="post in posts" :key="post.id" :value="post.id">{{ post.title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Content -->
|
||||
<div class="border border-layer-line rounded-lg p-4 space-y-4">
|
||||
<h5 class="text-sm font-medium text-foreground">Текстовая часть</h5>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Заголовок слайда
|
||||
</label>
|
||||
<input
|
||||
v-model="slideForm.title"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Цвет текста *
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="slideForm.color_theme"
|
||||
type="color"
|
||||
class="w-10 h-10 rounded border border-layer-line cursor-pointer"
|
||||
/>
|
||||
<span class="text-sm font-mono text-muted-foreground-1">{{ slideForm.color_theme }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Описание слайда
|
||||
</label>
|
||||
<textarea
|
||||
v-model="slideForm.content"
|
||||
rows="2"
|
||||
maxlength="1000"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Позиция текста
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="pos in ['left', 'center', 'right']"
|
||||
:key="pos"
|
||||
type="button"
|
||||
@click="slideForm.settings.text_position = pos"
|
||||
:class="[
|
||||
'flex-1 px-3 py-2 text-sm font-medium rounded-lg border transition-all',
|
||||
slideForm.settings.text_position === pos
|
||||
? 'bg-primary text-white border-primary'
|
||||
: 'bg-surface text-foreground border-layer-line hover:border-primary/50'
|
||||
]"
|
||||
>
|
||||
{{ pos === 'left' ? 'Слева' : pos === 'center' ? 'По центру' : 'Справа' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
id="active_button"
|
||||
v-model="slideForm.active_button"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-primary border-layer-line rounded focus:ring-primary/20"
|
||||
/>
|
||||
<label for="active_button" class="text-sm font-medium text-foreground">
|
||||
Показывать кнопку
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
v-if="slideForm.active_button"
|
||||
v-model="slideForm.settings.link_text"
|
||||
type="text"
|
||||
maxlength="50"
|
||||
placeholder="Текст кнопки"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image -->
|
||||
<div class="border border-layer-line rounded-lg p-4 space-y-4">
|
||||
<h5 class="text-sm font-medium text-foreground">Изображение</h5>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Изображение слайда *
|
||||
</label>
|
||||
<div v-if="slideForm.image_preview" class="mb-3">
|
||||
<img :src="slideForm.image_preview" alt="Preview" class="h-48 w-auto object-cover rounded-lg border border-layer-line shadow-sm" />
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
@change="handleImageUpload"
|
||||
accept="image/*"
|
||||
class="w-full text-sm text-foreground file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-primary/10 file:text-primary hover:file:bg-primary/20 transition-all"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-muted-foreground-1">Рекомендуемое соотношение сторон: 16:9</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Затемнение фона
|
||||
</label>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button
|
||||
v-for="opt in [{value: '1', label: 'Нет'}, {value: '0.7', label: 'Слабое'}, {value: '0.5', label: 'Среднее'}, {value: '0.3', label: 'Сильное'}]"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
@click="slideForm.image_shading = opt.value"
|
||||
:class="[
|
||||
'px-4 py-2 text-sm font-medium rounded-lg border transition-all',
|
||||
slideForm.image_shading === opt.value
|
||||
? 'bg-primary text-white border-primary'
|
||||
: 'bg-surface text-foreground border-layer-line hover:border-primary/50'
|
||||
]"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Settings -->
|
||||
<div class="border border-layer-line rounded-lg p-4 space-y-4">
|
||||
<h5 class="text-sm font-medium text-foreground">Настройки отображения</h5>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Целевая ссылка *
|
||||
</label>
|
||||
<input
|
||||
v-model="slideForm.link"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
required
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 pt-6">
|
||||
<input
|
||||
id="slide_is_active"
|
||||
v-model="slideForm.is_active"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-primary border-layer-line rounded focus:ring-primary/20"
|
||||
/>
|
||||
<label for="slide_is_active" class="text-sm font-medium text-foreground">
|
||||
Активный слайд
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Дата начала показа
|
||||
</label>
|
||||
<input
|
||||
v-model="slideForm.start_time"
|
||||
type="datetime-local"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1.5">
|
||||
Дата окончания показа
|
||||
</label>
|
||||
<input
|
||||
v-model="slideForm.end_time"
|
||||
type="datetime-local"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="createSlide"
|
||||
:disabled="slideForm.processing"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg v-if="slideForm.processing" class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ editingSlideId ? 'Сохранить изменения' : 'Создать слайд' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="showAddSlide = false; editingSlideId = null; resetSlideForm()"
|
||||
class="px-4 py-2 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-colors"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slides List -->
|
||||
<div class="divide-y divide-line-2">
|
||||
<div v-for="slide in slides" :key="slide.id" class="p-4 hover:bg-muted-hover transition-colors">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-20 h-12 rounded overflow-hidden border border-layer-line flex-shrink-0">
|
||||
<img v-if="slide.image?.url" :src="getImageUrl(slide.image.url)" :alt="slide.title" class="w-full h-full object-cover" />
|
||||
<div v-else class="w-full h-full bg-gray-200 flex items-center justify-center">
|
||||
<DashboardIcon name="photo" size="6" class="text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs font-medium text-muted-foreground-1">#{{ slide.sort }}</span>
|
||||
<h5 class="text-sm font-semibold text-foreground truncate">{{ slide.title || 'Без заголовка' }}</h5>
|
||||
<span :class="slide.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium">
|
||||
{{ slide.is_active ? 'Активен' : 'Неактивен' }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="slide.content" class="text-xs text-muted-foreground-1 line-clamp-1 mb-1">{{ slide.content }}</p>
|
||||
<div class="flex items-center gap-3 text-xs text-muted-foreground-1">
|
||||
<span class="truncate">{{ slide.link }}</span>
|
||||
<span v-if="slide.start_time" class="flex-shrink-0">с {{ new Date(slide.start_time).toLocaleDateString('ru-RU') }}</span>
|
||||
<span v-if="slide.end_time" class="flex-shrink-0">по {{ new Date(slide.end_time).toLocaleDateString('ru-RU') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="editSlide(slide)"
|
||||
class="p-2 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title="Редактировать"
|
||||
>
|
||||
<DashboardIcon name="pencil-square" size="4" />
|
||||
</button>
|
||||
<button
|
||||
@click="toggleSlideActive(slide)"
|
||||
class="p-2 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
:title="slide.is_active ? 'Деактивировать' : 'Активировать'"
|
||||
>
|
||||
<DashboardIcon name="arrows-up-down" size="4" />
|
||||
</button>
|
||||
<button
|
||||
@click="deleteSlide(slide)"
|
||||
class="p-2 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Удалить"
|
||||
>
|
||||
<DashboardIcon name="trash" size="4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="slides.length === 0 && !showAddSlide" class="p-12 text-center">
|
||||
<DashboardIcon name="photo" size="12" class="text-gray-400 mx-auto mb-4" />
|
||||
<p class="text-sm text-muted-foreground-1">Слайды еще не добавлены</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DashboardLayout from '../Components/DashboardLayout.vue';
|
||||
import DashboardIcon from '../Components/DashboardIcon.vue';
|
||||
|
||||
export default {
|
||||
name: 'EditSlider',
|
||||
components: {
|
||||
DashboardLayout,
|
||||
DashboardIcon
|
||||
},
|
||||
props: {
|
||||
slider: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
slides: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
posts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
title: this.slider.title,
|
||||
slug: this.slider.slug,
|
||||
is_active: this.slider.is_active ?? true,
|
||||
processing: false
|
||||
},
|
||||
slideForm: {
|
||||
model_select: 'Custom',
|
||||
model: null,
|
||||
title: '',
|
||||
content: '',
|
||||
image: null,
|
||||
image_preview: null,
|
||||
link: '',
|
||||
color_theme: '#ffffff',
|
||||
settings: {
|
||||
text_position: 'left',
|
||||
link_text: 'Читать'
|
||||
},
|
||||
image_shading: '0.5',
|
||||
active_button: true,
|
||||
is_active: true,
|
||||
start_time: this.formatDateTime(new Date()),
|
||||
end_time: this.formatDateTime(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)),
|
||||
processing: false
|
||||
},
|
||||
showAddSlide: false,
|
||||
editingSlideId: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.SET_DOCUMENT_TITLE(`Редактирование слайдера - ${this.slider.title}`);
|
||||
},
|
||||
methods: {
|
||||
getImageUrl(path) {
|
||||
if (!path) return null
|
||||
// Если уже полный URL, возвращаем как есть
|
||||
if (path.startsWith('http')) return path
|
||||
// Добавляем /storage/ для относительных путей
|
||||
return '/storage/' + path
|
||||
},
|
||||
formatDateTime(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||
},
|
||||
updateSlider() {
|
||||
this.form.processing = true
|
||||
this.$inertia.put(route('dashboard.sliders.update', this.slider.id), this.form, {
|
||||
onSuccess: () => {
|
||||
this.form.processing = false
|
||||
},
|
||||
onError: () => {
|
||||
this.form.processing = false
|
||||
}
|
||||
})
|
||||
},
|
||||
onModelSelectChange() {
|
||||
if (this.slideForm.model_select === 'Custom') {
|
||||
this.slideForm.model = null
|
||||
this.slideForm.title = ''
|
||||
this.slideForm.content = ''
|
||||
this.slideForm.link = ''
|
||||
}
|
||||
},
|
||||
onModelChange() {
|
||||
const post = this.posts.find(p => p.id == this.slideForm.model)
|
||||
if (post) {
|
||||
this.slideForm.title = post.title
|
||||
this.slideForm.link = `/news/${post.slug}`
|
||||
}
|
||||
},
|
||||
handleImageUpload(event) {
|
||||
const file = event.target.files[0]
|
||||
if (file) {
|
||||
this.slideForm.image = file
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
this.slideForm.image_preview = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
},
|
||||
createSlide() {
|
||||
this.slideForm.processing = true
|
||||
const formData = new FormData()
|
||||
|
||||
// Добавляем файл
|
||||
if (this.slideForm.image) {
|
||||
formData.append('image', this.slideForm.image)
|
||||
}
|
||||
|
||||
// Добавляем shading к изображению
|
||||
formData.append('image_shading', this.slideForm.image_shading)
|
||||
formData.append('active_button', this.slideForm.active_button ? 1 : 0)
|
||||
|
||||
// Добавляем остальные поля
|
||||
Object.keys(this.slideForm).forEach(key => {
|
||||
if (key === 'processing' || key === 'image' || key === 'image_preview' || key === 'model_select' || key === 'model' || key === 'image_shading' || key === 'active_button') return
|
||||
if (key === 'settings') {
|
||||
Object.keys(this.slideForm.settings).forEach(settingKey => {
|
||||
formData.append(`settings[${settingKey}]`, this.slideForm.settings[settingKey])
|
||||
})
|
||||
} else if (this.slideForm[key] !== null && this.slideForm[key] !== undefined) {
|
||||
formData.append(key, this.slideForm[key])
|
||||
}
|
||||
})
|
||||
|
||||
const url = this.editingSlideId
|
||||
? route('dashboard.slides.update', this.editingSlideId)
|
||||
: route('dashboard.slides.store', this.slider.id)
|
||||
|
||||
const method = this.editingSlideId ? 'post' : 'post'
|
||||
|
||||
// Для обновления добавляем _method
|
||||
if (this.editingSlideId) {
|
||||
formData.append('_method', 'PUT')
|
||||
}
|
||||
|
||||
axios.post(url, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
.then(response => {
|
||||
this.showAddSlide = false
|
||||
this.editingSlideId = null
|
||||
this.resetSlideForm()
|
||||
window.location.reload()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error creating slide:', error)
|
||||
this.slideForm.processing = false
|
||||
if (error.response?.data?.errors) {
|
||||
alert('Ошибка валидации: ' + JSON.stringify(error.response.data.errors))
|
||||
}
|
||||
})
|
||||
},
|
||||
editSlide(slide) {
|
||||
this.editingSlideId = slide.id
|
||||
this.showAddSlide = true
|
||||
this.slideForm = {
|
||||
model_select: 'Custom',
|
||||
model: null,
|
||||
title: slide.title || '',
|
||||
content: slide.content || '',
|
||||
image: null,
|
||||
image_preview: slide.image?.url ? this.getImageUrl(slide.image.url) : null,
|
||||
link: slide.link || '',
|
||||
color_theme: slide.color_theme || '#ffffff',
|
||||
settings: {
|
||||
text_position: slide.settings?.text_position || 'left',
|
||||
link_text: slide.settings?.link_text || 'Читать'
|
||||
},
|
||||
image_shading: slide.image?.shading || '0.5',
|
||||
active_button: slide.settings?.active_button ?? true,
|
||||
is_active: slide.is_active ?? true,
|
||||
start_time: slide.start_time ? this.formatDateTime(new Date(slide.start_time)) : this.formatDateTime(new Date()),
|
||||
end_time: slide.end_time ? this.formatDateTime(new Date(slide.end_time)) : this.formatDateTime(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)),
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
resetSlideForm() {
|
||||
this.editingSlideId = null
|
||||
this.slideForm = {
|
||||
model_select: 'Custom',
|
||||
model: null,
|
||||
title: '',
|
||||
content: '',
|
||||
image: null,
|
||||
image_preview: null,
|
||||
link: '',
|
||||
color_theme: '#ffffff',
|
||||
settings: {
|
||||
text_position: 'left',
|
||||
link_text: 'Читать'
|
||||
},
|
||||
image_shading: '0.5',
|
||||
active_button: true,
|
||||
is_active: true,
|
||||
start_time: this.formatDateTime(new Date()),
|
||||
end_time: this.formatDateTime(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)),
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
toggleSlideActive(slide) {
|
||||
axios.put(route('dashboard.slides.update', slide.id), {
|
||||
is_active: slide.is_active ? 0 : 1
|
||||
})
|
||||
.then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
},
|
||||
deleteSlide(slide) {
|
||||
if (!confirm('Удалить этот слайд?')) return
|
||||
|
||||
axios.delete(route('dashboard.slides.destroy', slide.id))
|
||||
.then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<DashboardLayout>
|
||||
<template #header-icon>
|
||||
<DashboardIcon name="photo" size="5" class="text-primary" />
|
||||
</template>
|
||||
<template #header-title>Слайдеры</template>
|
||||
<template #header-subtitle>Управление слайдерами</template>
|
||||
<template #header-actions>
|
||||
<a
|
||||
:href="route('dashboard.sliders.create')"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all duration-200 shadow-sm hover:shadow-md"
|
||||
>
|
||||
<DashboardIcon name="plus" size="4" />
|
||||
Создать слайдер
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<FlashMessages />
|
||||
|
||||
<!-- Filters -->
|
||||
<DataFilters title="Фильтры" @reset="resetFilters">
|
||||
<div class="col-span-2">
|
||||
<SearchInput
|
||||
v-model="search"
|
||||
label="Поиск"
|
||||
placeholder="Поиск по названию или slug..."
|
||||
id="slider_search"
|
||||
@search="applyFilters"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="is_active_filter" class="block text-xs font-medium text-muted-foreground-1 mb-1.5">
|
||||
Статус
|
||||
</label>
|
||||
<select
|
||||
id="is_active_filter"
|
||||
v-model="isActive"
|
||||
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
@change="applyFilters"
|
||||
>
|
||||
<option value="">Все статусы</option>
|
||||
<option value="1">Активные</option>
|
||||
<option value="0">Неактивные</option>
|
||||
</select>
|
||||
</div>
|
||||
</DataFilters>
|
||||
|
||||
<!-- Sliders Table -->
|
||||
<div class="bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-line-2">
|
||||
<thead class="bg-surface/50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
||||
Название
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
||||
Кол-во слайдов
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
||||
Статус
|
||||
</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
||||
Действия
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-layer divide-y divide-line-2">
|
||||
<tr v-for="slider in sliders.data" :key="slider.id" class="hover:bg-muted-hover transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-foreground">{{ slider.title }}</div>
|
||||
<div class="text-xs text-muted-foreground-1">{{ slider.slug }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||
slider.slides_count > 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
]"
|
||||
>
|
||||
{{ slider.slides_count }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||
slider.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
]"
|
||||
>
|
||||
{{ slider.is_active ? 'Активен' : 'Неактивен' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<a
|
||||
:href="route('dashboard.sliders.edit', slider.id)"
|
||||
class="text-blue-600 hover:text-blue-900 transition-colors"
|
||||
title="Редактировать"
|
||||
>
|
||||
<DashboardIcon name="pencil-square" size="5" />
|
||||
</a>
|
||||
<button
|
||||
@click="confirmDelete(slider)"
|
||||
class="text-red-600 hover:text-red-900 transition-colors"
|
||||
title="Удалить"
|
||||
>
|
||||
<DashboardIcon name="trash" size="5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<EmptyState
|
||||
v-if="sliders.data.length === 0"
|
||||
:columns="4"
|
||||
title="Слайдеры не найдены"
|
||||
description="Создайте первый слайдер или измените параметры поиска"
|
||||
:action-url="route('dashboard.sliders.create')"
|
||||
action-text="Создать слайдер"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
<Pagination :data="sliders" />
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div v-if="showDeleteModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-layer border border-layer-line rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4">Подтверждение удаления</h3>
|
||||
<p class="text-sm text-muted-foreground-1 mb-6">
|
||||
Вы уверены, что хотите удалить слайдер "{{ sliderToDelete?.title }}"? Все слайды будут удалены.
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
@click="showDeleteModal = false"
|
||||
class="px-4 py-2 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-colors"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
@click="deleteSlider"
|
||||
class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DashboardLayout from '../Components/DashboardLayout.vue';
|
||||
import DashboardIcon from '../Components/DashboardIcon.vue';
|
||||
import FlashMessages from '../Components/shared/FlashMessages.vue';
|
||||
import DataFilters from '../Components/shared/DataFilters.vue';
|
||||
import SearchInput from '../Components/shared/SearchInput.vue';
|
||||
import EmptyState from '../Components/shared/EmptyState.vue';
|
||||
import Pagination from '../Components/shared/Pagination.vue';
|
||||
|
||||
export default {
|
||||
name: 'SlidersIndex',
|
||||
components: {
|
||||
DashboardLayout,
|
||||
DashboardIcon,
|
||||
FlashMessages,
|
||||
DataFilters,
|
||||
SearchInput,
|
||||
EmptyState,
|
||||
Pagination
|
||||
},
|
||||
props: {
|
||||
sliders: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: this.filters.search || '',
|
||||
isActive: this.filters.is_active || '',
|
||||
showDeleteModal: false,
|
||||
sliderToDelete: null,
|
||||
searchTimeout: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.SET_DOCUMENT_TITLE('Слайдеры');
|
||||
},
|
||||
methods: {
|
||||
resetFilters() {
|
||||
this.search = ''
|
||||
this.isActive = ''
|
||||
this.$inertia.get(route('dashboard.sliders.index'), {}, {
|
||||
preserveState: true,
|
||||
replace: true
|
||||
})
|
||||
},
|
||||
debounceSearch() {
|
||||
clearTimeout(this.searchTimeout)
|
||||
this.searchTimeout = setTimeout(this.applyFilters, 300)
|
||||
},
|
||||
applyFilters() {
|
||||
this.$inertia.get(route('dashboard.sliders.index'), {
|
||||
search: this.search,
|
||||
is_active: this.isActive
|
||||
}, {
|
||||
preserveState: true,
|
||||
replace: true
|
||||
})
|
||||
},
|
||||
changePage(url) {
|
||||
if (url) {
|
||||
this.$inertia.get(url, {
|
||||
search: this.search,
|
||||
is_active: this.isActive
|
||||
}, {
|
||||
preserveState: true
|
||||
})
|
||||
}
|
||||
},
|
||||
confirmDelete(slider) {
|
||||
this.sliderToDelete = slider
|
||||
this.showDeleteModal = true
|
||||
},
|
||||
deleteSlider() {
|
||||
this.$inertia.delete(route('dashboard.sliders.destroy', this.sliderToDelete.id), {
|
||||
onSuccess: () => {
|
||||
this.showDeleteModal = false
|
||||
this.sliderToDelete = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user