feat(slider): add slider feature
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
26
frontend/admin/src/components/SettingsItem.vue
Normal file
26
frontend/admin/src/components/SettingsItem.vue
Normal 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>
|
||||
@@ -3,7 +3,7 @@
|
||||
<input
|
||||
type="search"
|
||||
name="category"
|
||||
:value="`${category?.name}`"
|
||||
:value="`${category?.name || ''}`"
|
||||
placeholder="Начните вводить название категории..."
|
||||
class="form-control"
|
||||
ref="categoryRef"
|
||||
@@ -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>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<input
|
||||
type="search"
|
||||
:value="`${model?.name}`"
|
||||
:value="`${model?.name || ''}`"
|
||||
placeholder="Начните вводить название товара..."
|
||||
class="form-control"
|
||||
ref="inputRef"
|
||||
186
frontend/admin/src/components/Slider/Slider.vue
Normal file
186
frontend/admin/src/components/Slider/Slider.vue
Normal 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>
|
||||
28
frontend/admin/src/components/Switcher.vue
Normal file
28
frontend/admin/src/components/Switcher.vue
Normal 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>
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user