feat(slider): add slider feature

This commit is contained in:
2025-11-01 17:32:28 +03:00
parent 0cccc7e3d7
commit 3049bd3101
37 changed files with 685 additions and 256 deletions

View File

@@ -0,0 +1,21 @@
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme) prefix(tw);
@import "tailwindcss/utilities.css" layer(utilities) prefix(tw);
@plugin "daisyui" {
prefix: 'd-'
}
@layer components {
.tw\:d-toggle {
width: calc((var(--d-size) * 2) - (var(--border) + var(--d-toggle-p)) * 2) !important;
height: var(--d-size) !important;
border: var(--border) solid currentColor !important;
color: var(--d-input-color) !important;
border-radius: calc(var(--radius-selector) + min(var(--d-toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max))) !important;
padding: var(--d-toggle-p) !important;
}
.tw\:d-toggle:after {
all: unset !important;
}
}

View File

@@ -1,82 +0,0 @@
<template>
<section>
<pre>{{ banners }}</pre>
<input type="text" name="module_tgshop_mainpage_banners" :value="JSON.stringify(banners)">
<table id="banners" class="table table-striped table-bordered table-hover">
<thead>
<tr>
<td class="text-left">Заголовок</td>
<td class="text-left">Ссылка</td>
<td class="text-center">Изображение</td>
<td>Действия</td>
</tr>
</thead>
<tbody>
<tr v-for="(banner, index) in banners">
<td class="text-left">
<input v-model="banner.title" type="text" placeholder="Заголовок слайда"
class="form-control"/>
</td>
<td class="text-left" style="width: 30%;">
<LinkSelector v-model="banner.link"/>
</td>
<td class="text-center">
<OcImagePIcker v-model="banner.image"/>
<div class="alert alert-info">
Минимальный размер: 370×200 <br>
Рекомендуется: 740×400 или больше, в тех же пропорциях (1.85:1) <br>
Картинка будет автоматически обрезана под нужный формат.
</div>
</td>
<td class="text-left">
<button type="button" class="btn btn-danger" @click="removeBanner(index)">
<i class="fa fa-minus-circle"></i>
</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3"></td>
<td class="text-left">
<button @click="addBanner" type="button" class="btn btn-primary">
<i class="fa fa-plus-circle"></i>
</button>
</td>
</tr>
</tfoot>
</table>
</section>
</template>
<script setup>
import {onMounted, ref} from "vue";
import OcImagePIcker from "@/components/OcImagePIcker.vue";
import LinkSelector from "@/components/Banners/LinkSelector.vue";
const banners = ref([]);
function removeBanner(index) {
banners.value.splice(index, 1);
}
function addBanner() {
banners.value.push({
title: '',
link: {
type: 'none',
value: null,
},
image: '',
});
}
onMounted(() => {
banners.value = JSON.parse(window.TeleCart.banners || '[]');
});
</script>
<style scoped>
</style>

View File

@@ -1,9 +1,15 @@
<template>
<div>
<a href="#" data-toggle="image" class="img-thumbnail" :id="`thumb-image-${id}`">
<img :src="thumb"
<div class="oc-image">
<div v-if="isLoaded === false" class="loader">
<i class="fa fa-spinner fa-spin"></i>
</div>
<a v-show="isLoaded" href="#" data-toggle="image" class="img-thumbnail" :id="`thumb-image-${id}`">
<img
:src="thumb"
data-placeholder="/image/cache/no_image-100x100.png"
alt="Image"
@load="isLoaded = true"
>
</a>
<input ref="inputRef" type="hidden" value="" :id="`input-image-${id}`">
@@ -17,6 +23,7 @@ const id = useId();
const model = defineModel();
const emit = defineEmits(['update:modelValue']);
const inputRef = ref(null);
const isLoaded = ref(false);
const thumb = computed(() => {
if (!model.value) return '/image/cache/no_image-100x100.png';
@@ -39,3 +46,18 @@ onMounted(() => {
observer.observe(input, {attributes: true, attributeFilter: ['value']});
});
</script>
<style scoped>
.oc-image {
display: flex;
justify-content: center;
align-items: center;
}
.loader {
width: 100px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div class="form-group">
<label class="col-sm-2 control-label" for="module_tgshop_status">
{{ label }}
</label>
<div class="col-sm-10">
<slot name="default"></slot>
<div class="help-block">
<slot name="help"></slot>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
label: {
type: String,
default: '',
},
});
</script>
<style scoped lang="scss">
</style>

View File

@@ -3,7 +3,7 @@
<input
type="search"
name="category"
:value="`${category?.name}`"
:value="`${category?.name || ''}`"
placeholder="Начните вводить название категории..."
class="form-control"
ref="categoryRef"

View File

@@ -34,18 +34,16 @@
</template>
<script setup>
import CategorySelect from "@/components/Banners/CategorySelect.vue";
import ProductSelect from "@/components/Banners/ProductSelect.vue";
import CategorySelect from "@/components/Slider/CategorySelect.vue";
import ProductSelect from "@/components/Slider/ProductSelect.vue";
const link = defineModel();
function setLink(value) {
if (Object.is(link.value)) {
if (link.value?.value) {
link.value.value.url = value;
} else {
link.value.value = {
url: value,
};
link.value.value = { url: value };
}
}
</script>

View File

@@ -2,7 +2,7 @@
<div>
<input
type="search"
:value="`${model?.name}`"
:value="`${model?.name || ''}`"
placeholder="Начните вводить название товара..."
class="form-control"
ref="inputRef"

View File

@@ -0,0 +1,186 @@
<template>
<input type="hidden" name="module_tgshop_mainpage_slider" :value="JSON.stringify(slider)">
<div class="alert alert-info">
<p>Здесь настраивается слайдер, который выводится на главной странице.</p>
<p>Рекомендуемые размеры изображений: <span class="text-bold">370×200px</span>, <span
class="text-bold">740×400px</span>,
<span class="text-bold">1110×600px</span> либо другие, в тех же пропорциях (1.85:1)<br>
Изображение будет автоматически обрезана под нужный формат. <br>
Заголовок можно оставить пустым, но рекомендуется заполнить для корректной работы целей
Яндекс.Метрики.</p>
</div>
<section>
<SettingsItem label="Статус">
<template #default>
<Switcher v-model="slider.is_enabled"/>
</template>
<template #help>
Показывать слайдер на главной странице.
Для отображения слайдера нужно добавить минимум 1 слайд.
</template>
</SettingsItem>
<SettingsItem label="Эффект смены слайдов">
<template #default>
<select v-model="slider.effect" class="form-control">
<option value="slide">Слайд</option>
<option value="flip">Переворот</option>
<option value="cards">Карточки</option>
<option value="cube">Куб</option>
<option value="coverflow">Перекрывающиеся слайды</option>
</select>
</template>
</SettingsItem>
<SettingsItem label="Пагинация">
<template #default>
<Switcher v-model="slider.pagination"/>
</template>
<template #help>
Показывать точки под слайдером для индикации текущего слайда.
</template>
</SettingsItem>
<SettingsItem label="Полоса прокрутки">
<template #default>
<Switcher v-model="slider.scrollbar"/>
</template>
<template #help>
Показывать полосу прокрутки под слайдером для навигации между слайдами.
</template>
</SettingsItem>
<SettingsItem label="Расстояние между слайдами">
<template #default>
<div class="tw:max-w-2xl">
<div class="input-group">
<input
v-model="slider.space_between"
type="number"
min="0"
max="100"
class="form-control"
placeholder="30"
/>
<span class="input-group-addon">px</span>
</div>
</div>
</template>
<template #help>
Расстояние между слайдами в пикселях. По умолчанию - 30.
</template>
</SettingsItem>
<SettingsItem label="Свободный режим">
<template #default>
<Switcher v-model="slider.free_mode"/>
</template>
<template #help>
Позволяет свободно прокручивать слайды без привязки к конкретным позициям.
</template>
</SettingsItem>
<SettingsItem label="Бесконечная прокрутка">
<template #default>
<Switcher v-model="slider.loop"/>
</template>
<template #help>
Включите этот режим, чтобы после последнего слайда слайдер продолжал прокрутку с первого, создавая бесконечный цикл.
</template>
</SettingsItem>
<SettingsItem label="Автоматическая прокрутка">
<template #default>
<Switcher v-model="slider.autoplay"/>
</template>
<template #help>
Слайдер будет автоматически листать изображения каждые 3 секунды
</template>
</SettingsItem>
</section>
<section>
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<td class="text-left">Заголовок</td>
<td class="text-left">Ссылка</td>
<td class="text-center">Изображение</td>
<td>Действия</td>
</tr>
</thead>
<tbody>
<tr v-for="(slide, index) in slider.slides">
<td class="text-left">
<input v-model="slide.title" type="text" placeholder="Заголовок слайда"
class="form-control"/>
</td>
<td class="text-left" style="width: 30%;">
<LinkSelector v-model="slide.link"/>
</td>
<td class="text-center">
<OcImagePicker v-model="slide.image"/>
</td>
<td class="text-left">
<button type="button" class="btn btn-danger" @click="removeSlide(index)">
<i class="fa fa-minus-circle"></i>
</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3"></td>
<td class="text-left">
<button @click="addSlide" type="button" class="btn btn-primary">
<i class="fa fa-plus-circle"></i>
</button>
</td>
</tr>
</tfoot>
</table>
</section>
</template>
<script setup>
import {onMounted, ref} from "vue";
import OcImagePIcker from "@/components/OcImagePIcker.vue";
import LinkSelector from "@/components/Slider/LinkSelector.vue";
import SettingsItem from "@/components/SettingsItem.vue";
import Switcher from "@/components/Switcher.vue";
const slider = ref({});
function removeSlide(index) {
slider.value.slides.splice(index, 1);
}
function addSlide() {
slider.value.slides.push({
title: '',
link: {
type: 'none',
value: null,
},
image: '',
});
}
onMounted(() => {
slider.value = JSON.parse(window.TeleCart.mainpage_slider);
});
</script>
<style scoped>
.text-bold {
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<div class="btn-group btn-toggle tw:mt-3">
<button
class="btn btn-xs"
:class="{active: model === true, 'btn-success': model === true, 'btn-default' : model === false }"
@click.prevent="model = true"
>
Вкл
</button>
<button
class="btn btn-xs"
:class="{active: model === false, 'btn-danger': model === false, 'btn-default' : model === true }"
@click.prevent="model = false"
>
Выкл
</button>
</div>
</template>
<script setup>
const model = defineModel({
default: false,
});
</script>
<style scoped lang="scss">
</style>

View File

@@ -1,14 +1,20 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
function onReady(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
fn();
}
}
app.use(createPinia())
app.use(router)
app.mount('#app')
onReady(() => {
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');
});

View File

@@ -1,7 +1,7 @@
<template>
<Banners/>
<Slider/>
</template>
<script setup>
import Banners from "@/components/Banners/Banners.vue";
import Slider from "@/components/Slider/Slider.vue";
</script>