feat: product options, speedup home page, themes
This commit is contained in:
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageTool.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageTool.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageToolInterface.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageToolInterface.php
Normal file → Executable file
@@ -1,10 +1,9 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="ru">
|
<html lang="ru" data-theme="cyberpunk">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
|
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
|
||||||
<title>Vite + Vue</title>
|
<title>Vite + Vue</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ watch(
|
|||||||
backButton.hide?.();
|
backButton.hide?.();
|
||||||
} else {
|
} else {
|
||||||
backButton.show?.();
|
backButton.show?.();
|
||||||
backButton.onClick?.(() => router.back());
|
backButton.onClick?.(() => {
|
||||||
|
window.Telegram.WebApp.HapticFeedback.impactOccurred('light');
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{immediate: true, deep: true}
|
{immediate: true, deep: true}
|
||||||
|
|||||||
@@ -7,22 +7,13 @@
|
|||||||
Каталог
|
Каталог
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<RouterLink v-for="category in categories" class="btn" :to="`/category/${category.id}`">
|
<RouterLink v-for="category in categoriesStore.topCategories" class="btn" :to="`/category/${category.id}`">
|
||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
|
||||||
import ftch from "../utils/ftch.js";
|
const categoriesStore = useCategoriesStore();
|
||||||
|
|
||||||
const categories = ref([]);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const {data} = await ftch('categoriesList', {
|
|
||||||
perPage: 7,
|
|
||||||
});
|
|
||||||
categories.value = data;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<label
|
<label
|
||||||
v-for="value in model.values"
|
v-for="value in model.values"
|
||||||
class="group relative flex items-center justify-center rounded-md border border-gray-300 bg-white p-2 has-checked:border-indigo-600 has-checked:bg-indigo-600 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-indigo-600 has-disabled:border-gray-400 has-disabled:bg-gray-200 has-disabled:opacity-25">
|
class="group relative flex items-center justify-center btn btn-soft btn-secondary btn-sm"
|
||||||
|
:class="value.selected ? 'btn-active' : ''"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:value="value.product_option_value_id"
|
:value="value.product_option_value_id"
|
||||||
@@ -36,6 +37,8 @@ function select(toggledValue) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
model.value.value = model.value.values.filter(item => item.selected === true);
|
||||||
|
|
||||||
emit('update:modelValue', model.value);
|
emit('update:modelValue', model.value);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<label
|
<label
|
||||||
v-for="value in model.values"
|
v-for="value in model.values"
|
||||||
class="group relative flex items-center justify-center rounded-md border border-gray-300 bg-base-200 p-2 has-checked:border-indigo-600 has-checked:bg-primary has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-indigo-600 has-disabled:border-gray-400 has-disabled:bg-gray-200 has-disabled:opacity-25">
|
class="group relative flex items-center justify-center btn btn-soft btn-secondary btn-sm"
|
||||||
|
:class="value.selected ? 'btn-active' : ''"
|
||||||
|
>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
class="absolute inset-0 appearance-none focus:outline-none disabled:cursor-not-allowed"
|
class="absolute inset-0 appearance-none focus:outline-none disabled:cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span class="text-xs font-medium group-has-checked:text-white">
|
<span class="text-xs font-medium">
|
||||||
{{ value.name }}<span v-if="value.price"> ({{ value.price_prefix }}{{ value.price }})</span>
|
{{ value.name }}<span v-if="value.price"> ({{ value.price_prefix }}{{ value.price }})</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -33,6 +35,8 @@ function select(selectedValue) {
|
|||||||
value.selected = (value === selectedValue);
|
value.selected = (value === selectedValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
model.value.value = selectedValue;
|
||||||
|
|
||||||
emit('update:modelValue', model);
|
emit('update:modelValue', model);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
class="select"
|
class="select"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
|
<option value="" disabled>Выберите значение</option>
|
||||||
<option
|
<option
|
||||||
v-for="value in model.values"
|
v-for="value in model.values"
|
||||||
:key="value.product_option_value_id"
|
:key="value.product_option_value_id"
|
||||||
@@ -30,6 +31,8 @@ function onChange(event) {
|
|||||||
value.selected = (value.product_option_value_id === selectedId);
|
value.selected = (value.product_option_value_id === selectedId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
model.value.value = model.value.values.find(value => value.product_option_value_id === selectedId);
|
||||||
|
|
||||||
emit('update:modelValue', model.value);
|
emit('update:modelValue', model.value);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,10 +10,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h2 class="text-lg font-bold mb-5 text-center">{{ productsMeta.currentCategoryName }}</h2>
|
<h2 class="text-lg font-bold mb-5 text-center">{{ meta.currentCategoryName }}</h2>
|
||||||
|
|
||||||
<div v-if="products.length > 0" class="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
|
<div v-if="products.length > 0" class="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
|
||||||
<RouterLink v-for="product in products" :key="product.id" class="group" :to="`/product/${product.id}`">
|
<RouterLink
|
||||||
|
v-for="product in products"
|
||||||
|
:key="product.id"
|
||||||
|
class="group"
|
||||||
|
:to="`/product/${product.id}`"
|
||||||
|
@click="haptic"
|
||||||
|
>
|
||||||
<ProductImageSwiper :images="product.images"/>
|
<ProductImageSwiper :images="product.images"/>
|
||||||
<h3 class="mt-4 text-sm">{{ product.name }}</h3>
|
<h3 class="mt-4 text-sm">{{ product.name }}</h3>
|
||||||
<p class="mt-1 text-lg font-medium">{{ product.price }}</p>
|
<p class="mt-1 text-lg font-medium">{{ product.price }}</p>
|
||||||
@@ -26,21 +32,34 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {useHapticFeedback} from 'vue-tg';
|
import {useHapticFeedback} from 'vue-tg';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import ftch from "../utils/ftch.js";
|
|
||||||
import NoProducts from "../components/NoProducts.vue";
|
import NoProducts from "../components/NoProducts.vue";
|
||||||
import ProductImageSwiper from "../components/ProductImageSwiper.vue";
|
import ProductImageSwiper from "../components/ProductImageSwiper.vue";
|
||||||
|
|
||||||
const hapticFeedback = useHapticFeedback();
|
const hapticFeedback = useHapticFeedback();
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const categoryId = route.params.id;
|
|
||||||
|
|
||||||
const isLoading = ref(true);
|
const props = defineProps({
|
||||||
const products = ref([]);
|
products: {
|
||||||
const productsMeta = ref([]);
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
|
||||||
|
meta: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function haptic() {
|
||||||
|
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
const carouselRef = ref();
|
const carouselRef = ref();
|
||||||
let lastScrollLeft = 0;
|
let lastScrollLeft = 0;
|
||||||
@@ -53,13 +72,4 @@ function onScroll(e) {
|
|||||||
lastScrollLeft = scrollLeft;
|
lastScrollLeft = scrollLeft;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const {data, meta} = await ftch('products', {
|
|
||||||
categoryId: categoryId,
|
|
||||||
});
|
|
||||||
productsMeta.value = meta;
|
|
||||||
products.value = data;
|
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center text-center">
|
<div class="flex items-center text-center">
|
||||||
<button class="btn btn-lg btn-neutral" @click="inc">-</button>
|
<button class="btn btn-lg" @click="inc">-</button>
|
||||||
<div class="w-10 h-10 flex items-center justify-center bg-neutral font-bold">{{ model }}</div>
|
<div class="w-10 h-10 flex items-center justify-center font-bold">{{ model }}</div>
|
||||||
<button class="btn btn-lg btn-neutral" @click="dec">+</button>
|
<button class="btn btn-lg" @click="dec">+</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -5,22 +5,38 @@ import { VueTelegramPlugin } from 'vue-tg';
|
|||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
night_auto: false,
|
||||||
|
theme: {
|
||||||
|
light: 'fantasy',
|
||||||
|
dark: 'dark',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
app
|
app
|
||||||
.use(pinia)
|
.use(pinia)
|
||||||
.use(router)
|
.use(router)
|
||||||
.use(VueTelegramPlugin);
|
.use(VueTelegramPlugin);
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|
||||||
import { useMiniApp, useTheme } from 'vue-tg';
|
const productsStore = useProductsStore();
|
||||||
|
productsStore.fetchHomeProducts();
|
||||||
|
const categoriesStore = useCategoriesStore();
|
||||||
|
categoriesStore.fetchTopCategories();
|
||||||
|
|
||||||
const theme = useTheme();
|
import {useProductsStore} from "@/stores/ProductsStore.js";
|
||||||
const tg = useMiniApp();
|
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
|
||||||
|
|
||||||
theme.onChange(() => {
|
if (config.night_auto) {
|
||||||
document.documentElement.setAttribute('data-theme', theme.colorScheme.value);
|
window.Telegram.WebApp.onEvent('themeChanged', function () {
|
||||||
});
|
document.documentElement.setAttribute('data-theme', config.theme[this.colorScheme]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-theme', config.theme.light);
|
||||||
|
}
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-theme', theme.colorScheme.value);
|
|
||||||
window.Telegram.WebApp.ready();
|
window.Telegram.WebApp.ready();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {createMemoryHistory, createRouter} from 'vue-router';
|
|||||||
import Home from './views/Home.vue';
|
import Home from './views/Home.vue';
|
||||||
import Product from './views/Product.vue';
|
import Product from './views/Product.vue';
|
||||||
import CategoriesList from "./views/CategoriesList.vue";
|
import CategoriesList from "./views/CategoriesList.vue";
|
||||||
import ProductsList from "./views/ProductsList.vue";
|
import ProductsList from "@/components/ProductsList.vue";
|
||||||
import Cart from "./views/Cart.vue";
|
import Cart from "./views/Cart.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
|||||||
25
spa/src/stores/CategoriesStore.js
Normal file
25
spa/src/stores/CategoriesStore.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {defineStore} from "pinia";
|
||||||
|
import ftch from "../utils/ftch.js";
|
||||||
|
|
||||||
|
export const useCategoriesStore = defineStore('categories', {
|
||||||
|
state: () => ({
|
||||||
|
topCategories: [],
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchTopCategories() {
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
const response = await ftch('categoriesList', {
|
||||||
|
perPage: 7,
|
||||||
|
});
|
||||||
|
this.topCategories = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
25
spa/src/stores/ProductsStore.js
Normal file
25
spa/src/stores/ProductsStore.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {defineStore} from "pinia";
|
||||||
|
import ftch from "../utils/ftch.js";
|
||||||
|
|
||||||
|
export const useProductsStore = defineStore('products', {
|
||||||
|
state: () => ({
|
||||||
|
homeProducts: {
|
||||||
|
data: [],
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchHomeProducts() {
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.homeProducts = await ftch('products');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
themes: light --default, dark --prefersdark;
|
themes: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="goodsRef">
|
<div ref="goodsRef">
|
||||||
<CategoriesInline/>
|
<CategoriesInline/>
|
||||||
<ProductsList/>
|
<ProductsList
|
||||||
|
:products="productsStore.homeProducts.data"
|
||||||
|
:meta="productsStore.homeProducts.meta"
|
||||||
|
:isLoading="productsStore.isLoading"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
|
import ProductsList from "@/components/ProductsList.vue";
|
||||||
|
import CategoriesInline from "../components/CategoriesInline.vue";
|
||||||
|
import {useProductsStore} from "@/stores/ProductsStore.js";
|
||||||
|
|
||||||
|
const productsStore = useProductsStore();
|
||||||
|
|
||||||
const goodsRef = ref();
|
const goodsRef = ref();
|
||||||
function scrollToProducts() {
|
function scrollToProducts() {
|
||||||
goodsRef.value?.scrollIntoView({ behavior: 'smooth' });
|
goodsRef.value?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
import ProductsList from "./ProductsList.vue";
|
|
||||||
import CategoriesInline from "../components/CategoriesInline.vue";
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="pb-10">
|
||||||
<div>
|
<div>
|
||||||
<ProductImageSwiper :images="product.images"/>
|
<ProductImageSwiper :images="product.images"/>
|
||||||
|
|
||||||
<!-- Product info -->
|
<!-- Product info -->
|
||||||
<div
|
<div
|
||||||
class="mx-auto max-w-2xl px-4 pt-3 pb-16 sm:px-6 lg:grid lg:max-w-7xl lg:grid-cols-3 lg:grid-rows-[auto_auto_1fr] lg:gap-x-8 lg:px-8 lg:pt-16 lg:pb-24">
|
class="mx-auto max-w-2xl px-4 pt-3 pb-16 sm:px-6 lg:grid lg:max-w-7xl lg:grid-cols-3 lg:grid-rows-[auto_auto_1fr] lg:gap-x-8 lg:px-8 lg:pt-16 lg:pb-24 rounded-t-lg">
|
||||||
<div class="lg:col-span-2 lg:border-r lg:pr-8">
|
<div class="lg:col-span-2 lg:border-r lg:pr-8">
|
||||||
<h1 class="text-2xl font-bold tracking-tight sm:text-3xl">{{ product.name }}</h1>
|
<h1 class="text-2xl font-bold tracking-tight sm:text-3xl">{{ product.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,14 +34,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-4 pb-10 pt-4 fixed bottom-0 left-0 w-full bg-info-content z-50 flex justify-between gap-2 border-t-1 border-t-success-content">
|
<div v-if="product.id" class="px-4 pb-10 pt-4 fixed bottom-0 left-0 w-full bg-base-200 z-50 flex justify-between gap-2 border-t-1 border-t-base-300">
|
||||||
<button
|
<div class="flex-1">
|
||||||
class="btn btn-lg flex-1"
|
<button
|
||||||
:class="isInCartNow ? 'btn-success' : 'btn-primary'"
|
class="btn btn-lg w-full"
|
||||||
@click="actionBtnClick"
|
:class="isInCartNow ? 'btn-success' : 'btn-primary'"
|
||||||
>
|
:disabled="canAddToCart === false"
|
||||||
{{ buttonText }}
|
@click="actionBtnClick"
|
||||||
</button>
|
>
|
||||||
|
<span>{{ buttonText }}</span><br>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="canAddToCart === false" class="text-error text-center text-xs mt-1">
|
||||||
|
Выберите обязательные опции
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Quantity
|
<Quantity
|
||||||
v-if="quantity > 0"
|
v-if="quantity > 0"
|
||||||
@@ -76,6 +83,22 @@ const buttonText = computed(() => {
|
|||||||
: 'Добавить в корзину'
|
: 'Добавить в корзину'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canAddToCart = computed(() => {
|
||||||
|
if (!product.value || product.value.options === undefined || product.value.options?.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const required = product.value.options.filter(item => {
|
||||||
|
return ['checkbox', 'radio', 'select', 'text', 'textarea'].indexOf(item.type) !== -1
|
||||||
|
&& item.required === true
|
||||||
|
&& !item.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(required);
|
||||||
|
|
||||||
|
return required.length === 0;
|
||||||
|
});
|
||||||
|
|
||||||
const isInCartNow = computed(() => {
|
const isInCartNow = computed(() => {
|
||||||
return cart.hasProduct(productId.value);
|
return cart.hasProduct(productId.value);
|
||||||
});
|
});
|
||||||
@@ -108,17 +131,4 @@ onMounted(async () => {
|
|||||||
const {data} = await $fetch(`/index.php?route=extension/tgshop/handle&api_action=product_show&id=${productId.value}`);
|
const {data} = await $fetch(`/index.php?route=extension/tgshop/handle&api_action=product_show&id=${productId.value}`);
|
||||||
product.value = data;
|
product.value = data;
|
||||||
});
|
});
|
||||||
|
</script>
|
||||||
const carouselRef = ref();
|
|
||||||
let lastScrollLeft = 0;
|
|
||||||
|
|
||||||
function onScroll(e) {
|
|
||||||
const scrollLeft = e.target.scrollLeft;
|
|
||||||
const delta = Math.abs(scrollLeft - lastScrollLeft);
|
|
||||||
|
|
||||||
if (delta > 30) {
|
|
||||||
hapticFeedback.impactOccurred('soft');
|
|
||||||
lastScrollLeft = scrollLeft;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import {fileURLToPath, URL} from 'node:url';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), vue()],
|
plugins: [tailwindcss(), vue()],
|
||||||
@@ -13,6 +14,12 @@ export default defineConfig({
|
|||||||
manifest: true,
|
manifest: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
allowedHosts: ["tg.nikitakiselev.ru"],
|
allowedHosts: ["tg.nikitakiselev.ru"],
|
||||||
|
|||||||
Reference in New Issue
Block a user