refactor: move spa to frontend folder
This commit is contained in:
82
frontend/admin/src/components/Banners/Banners.vue
Normal file
82
frontend/admin/src/components/Banners/Banners.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<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>
|
||||
66
frontend/admin/src/components/Banners/CategorySelect.vue
Normal file
66
frontend/admin/src/components/Banners/CategorySelect.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<input
|
||||
type="search"
|
||||
name="category"
|
||||
:value="`${category?.name}`"
|
||||
placeholder="Начните вводить название категории..."
|
||||
class="form-control"
|
||||
ref="categoryRef"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
|
||||
const category = defineModel();
|
||||
const categoryRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
const input = categoryRef.value;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(input).autocomplete({
|
||||
'source': function (request, response) {
|
||||
if ($(input).val().length === 0) {
|
||||
$(input).val(null);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `index.php?route=catalog/category/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=` + encodeURIComponent(request),
|
||||
dataType: 'json',
|
||||
success: function (json) {
|
||||
response($.map(json, function (item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: item['category_id']
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
'select': function (item) {
|
||||
category.value = {
|
||||
category_id: Number(item['value']),
|
||||
name: item['label'],
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
const input = categoryRef.value;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(input).autocomplete('destroy');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
61
frontend/admin/src/components/Banners/LinkSelector.vue
Normal file
61
frontend/admin/src/components/Banners/LinkSelector.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<select v-model="link.type" class="form-control link-type-select" @change="link.value = null">
|
||||
<option value="none">Нет ссылки</option>
|
||||
<option value="category">Ссылка на категорию</option>
|
||||
<option value="product">Ссылка на товар</option>
|
||||
<option value="url">Внешняя ссылка</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="link.type === 'url'" class="mt-10">
|
||||
<input
|
||||
:value="link.value?.url"
|
||||
@input="setLink($event.target.value)"
|
||||
type="text"
|
||||
placeholder="https://example.com"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="link.type === 'category'" class="mt-10">
|
||||
<CategorySelect v-model="link.value"/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="link.type === 'product'" class="mt-10">
|
||||
<ProductSelect v-model="link.value"/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="link.type === 'none'"></div>
|
||||
|
||||
<div v-else class="alert alert-danger">Не поддерживается: {{ link.type }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import CategorySelect from "@/components/Banners/CategorySelect.vue";
|
||||
import ProductSelect from "@/components/Banners/ProductSelect.vue";
|
||||
|
||||
const link = defineModel();
|
||||
|
||||
function setLink(value) {
|
||||
if (Object.is(link.value)) {
|
||||
link.value.value.url = value;
|
||||
} else {
|
||||
link.value.value = {
|
||||
url: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.link-type-select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
65
frontend/admin/src/components/Banners/ProductSelect.vue
Normal file
65
frontend/admin/src/components/Banners/ProductSelect.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<input
|
||||
type="search"
|
||||
:value="`${model?.name}`"
|
||||
placeholder="Начните вводить название товара..."
|
||||
class="form-control"
|
||||
ref="inputRef"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
|
||||
const model = defineModel();
|
||||
const inputRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
const input = inputRef.value;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(input).autocomplete({
|
||||
'source': function (request, response) {
|
||||
if ($(input).val().length === 0) {
|
||||
$(input).val(null);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `index.php?route=catalog/product/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=` + encodeURIComponent(request),
|
||||
dataType: 'json',
|
||||
success: function (json) {
|
||||
response($.map(json, function (item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: item['product_id']
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
'select': function (item) {
|
||||
model.value = {
|
||||
product_id: Number(item['value']),
|
||||
name: item['label'],
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
const input = inputRef.value;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(input).autocomplete('destroy');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
41
frontend/admin/src/components/OcImagePIcker.vue
Normal file
41
frontend/admin/src/components/OcImagePIcker.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<a href="#" data-toggle="image" class="img-thumbnail" :id="`thumb-image-${id}`">
|
||||
<img :src="thumb"
|
||||
data-placeholder="/image/cache/no_image-100x100.png"
|
||||
alt="Image"
|
||||
>
|
||||
</a>
|
||||
<input ref="inputRef" type="hidden" value="" :id="`input-image-${id}`">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, ref, useId} from "vue";
|
||||
|
||||
const id = useId();
|
||||
const model = defineModel();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const inputRef = ref(null);
|
||||
|
||||
const thumb = computed(() => {
|
||||
if (!model.value) return '/image/cache/no_image-100x100.png';
|
||||
const extIndex = model.value.lastIndexOf('.');
|
||||
const ext = model.value.substring(extIndex);
|
||||
const filename = model.value.substring(0, extIndex);
|
||||
return `/image/cache/${filename}-100x100${ext}`;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const input = inputRef.value;
|
||||
const observer = new MutationObserver(() => {
|
||||
const val = input.value;
|
||||
console.log("Updated value: ", val);
|
||||
if (val !== model.value) {
|
||||
emit('update:modelValue', val);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(input, {attributes: true, attributeFilter: ['value']});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user