refactor: move spa to frontend folder

This commit is contained in:
2025-10-27 12:32:38 +03:00
committed by Nikita Kiselev
parent 5681ac592a
commit 0cccc7e3d7
40 changed files with 1566 additions and 35 deletions

View File

@@ -0,0 +1,11 @@
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,14 @@
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)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,15 @@
import {createMemoryHistory, createRouter} from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
],
})
export default router

View File

@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

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