refactor: move spa to frontend folder
This commit is contained in:
11
frontend/admin/src/App.vue
Normal file
11
frontend/admin/src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
86
frontend/admin/src/assets/base.css
Normal file
86
frontend/admin/src/assets/base.css
Normal 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;
|
||||
}
|
||||
1
frontend/admin/src/assets/logo.svg
Normal file
1
frontend/admin/src/assets/logo.svg
Normal 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 |
0
frontend/admin/src/assets/main.css
Normal file
0
frontend/admin/src/assets/main.css
Normal file
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>
|
||||
14
frontend/admin/src/main.js
Normal file
14
frontend/admin/src/main.js
Normal 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')
|
||||
15
frontend/admin/src/router/index.js
Normal file
15
frontend/admin/src/router/index.js
Normal 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
|
||||
12
frontend/admin/src/stores/counter.js
Normal file
12
frontend/admin/src/stores/counter.js
Normal 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 }
|
||||
})
|
||||
7
frontend/admin/src/views/HomeView.vue
Normal file
7
frontend/admin/src/views/HomeView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<Banners/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Banners from "@/components/Banners/Banners.vue";
|
||||
</script>
|
||||
Reference in New Issue
Block a user