feat: UI/UX, add reset cache to admin
This commit is contained in:
40
frontend/admin/src/components/Form/ResetCacheBtn.vue
Normal file
40
frontend/admin/src/components/Form/ResetCacheBtn.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<Button
|
||||
icon="fa fa-refresh"
|
||||
severity="warn"
|
||||
v-tooltip.top="'Сбросить кеш модуля'"
|
||||
:loading="isLoading"
|
||||
@click="resetCache"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {Button, useToast} from "primevue";
|
||||
import {ref} from "vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const isLoading = ref(false);
|
||||
const toast = useToast();
|
||||
|
||||
async function resetCache() {
|
||||
isLoading.value = true;
|
||||
|
||||
const response = await apiPost('resetCache');
|
||||
if (response.success) {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Выполнено',
|
||||
detail: 'Кеш модуля сброшен.',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: 'Ошибка при сбросе кеша.',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
isLoading.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -65,6 +65,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw:mt-6 tw:lg:mt-0 tw:flex tw:items-center tw:gap-4">
|
||||
<ButtonGroup>
|
||||
<ResetCacheBtn/>
|
||||
</ButtonGroup>
|
||||
<div class="btn-group">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
@@ -89,12 +92,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Button from "primevue/button";
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {useStatsStore} from "@/stores/stats.js";
|
||||
import {onMounted, ref} from "vue";
|
||||
import OcImagePicker from "@/components/OcImagePicker.vue";
|
||||
import {apiGet} from "@/utils/http.js";
|
||||
import ResetCacheBtn from "@/components/Form/ResetCacheBtn.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const stats = useStatsStore();
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
|
||||
<div class="drawer-content">
|
||||
<div class="app-container">
|
||||
<header class="app-header bg-neutral text-neutral-content w-full" v-if="platform === 'ios'"></header>
|
||||
<header class="app-header bg-base-100 w-full"></header>
|
||||
|
||||
<section class="telecart-main-section">
|
||||
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
|
||||
|
||||
<Navbar @drawer="toggleDrawer"/>
|
||||
|
||||
<AppDebugMessage v-if="settings.app_debug"/>
|
||||
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:moreText="block.data.all_text"
|
||||
>
|
||||
<Swiper
|
||||
class="select-none"
|
||||
class="select-none block-products-carousel"
|
||||
:slides-per-view="block.data?.carousel?.slides_per_view || 2.5"
|
||||
:space-between="block.data?.carousel?.space_between || 20"
|
||||
:autoplay="block.data?.carousel?.autoplay || false"
|
||||
@@ -14,16 +14,24 @@
|
||||
:lazy="true"
|
||||
@sliderMove="hapticScroll"
|
||||
>
|
||||
<SwiperSlide v-for="product in block.data.products.data" :key="product.id">
|
||||
<RouterLink
|
||||
:to="{name: 'product.show', params: {id: product.id}}"
|
||||
@click="slideClick(product)"
|
||||
>
|
||||
<div class="text-center">
|
||||
<img :src="product.images[0].url" :alt="product.name" loading="lazy" class="product-image"/>
|
||||
<PriceTitle :product="product"/>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<SwiperSlide
|
||||
v-for="product in block.data.products.data"
|
||||
:key="product.id"
|
||||
|
||||
>
|
||||
<div class="will-change-transform active:scale-97 transition-transform">
|
||||
<RouterLink
|
||||
:to="{name: 'product.show', params: {id: product.id}}"
|
||||
@click="slideClick(product)"
|
||||
|
||||
>
|
||||
<div class="text-center">
|
||||
<img :src="product.images[0].url" :alt="product.name" loading="lazy" class="product-image"/>
|
||||
<PriceTitle :product="product"/>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
</BaseBlock>
|
||||
@@ -32,13 +40,11 @@
|
||||
<script setup>
|
||||
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
|
||||
import {Swiper, SwiperSlide} from "swiper/vue";
|
||||
import ProductTitle from "@/components/ProductItem/ProductTitle.vue";
|
||||
import {useHapticScroll} from "@/composables/useHapticScroll.js";
|
||||
import Price from "@/components/ProductItem/Price.vue";
|
||||
import BaseBlock from "@/components/MainPage/Blocks/BaseBlock.vue";
|
||||
import PriceTitle from "@/components/ProductItem/PriceTitle.vue";
|
||||
|
||||
const hapticScroll = useHapticScroll(20, 'selectionChanged');
|
||||
const hapticScroll = useHapticScroll();
|
||||
const yaMetrika = useYaMetrikaStore();
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="navbar bg-neutral text-neutral-content">
|
||||
<div class="navbar">
|
||||
<div class="navbar-start">
|
||||
<div v-if="false" class="dropdown">
|
||||
<button class="btn btn-ghost btn-circle" @click="toggleDrawer">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-if="special">
|
||||
<span class="old-price text-stone-500 line-through mr-1">{{ price }}</span>
|
||||
<span class="old-price text-neutral-content/70 line-through mr-1">{{ price }}</span>
|
||||
<span class="curr-price font-medium">{{ special }}</span>
|
||||
</div>
|
||||
<div v-else class="font-medium">{{ price }}</div>
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
<RouterLink
|
||||
v-for="(product, index) in products"
|
||||
:key="product.id"
|
||||
class="product-grid-card group"
|
||||
class="product-grid-card group will-change-transform active:scale-97 transition-transform"
|
||||
:to="`/product/${product.id}`"
|
||||
@click="productClick(product, index)"
|
||||
>
|
||||
|
||||
<ProductImageSwiper :images="product.images"/>
|
||||
<PriceTitle :product="product"/>
|
||||
</RouterLink>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div v-if="config.slides.length > 0" class="app-banner" :class="classList">
|
||||
<Swiper
|
||||
:effect="slideEffect"
|
||||
class="select-none"
|
||||
class="mainpage-slider select-none"
|
||||
:slides-per-view="1"
|
||||
:space-between="config.space_between"
|
||||
:pagination="pagination"
|
||||
@@ -77,7 +77,7 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const hapticScroll = useHapticScroll(20, 'impactOccurred', 'soft');
|
||||
const hapticScroll = useHapticScroll();
|
||||
const yaMetrika = useYaMetrikaStore();
|
||||
const modules = [
|
||||
Autoplay,
|
||||
@@ -105,7 +105,7 @@ const classList = computed(() => {
|
||||
});
|
||||
|
||||
const onSwiper = (swiper) => {
|
||||
console.log(swiper);
|
||||
|
||||
};
|
||||
const onSlideChange = () => {
|
||||
|
||||
@@ -195,4 +195,15 @@ onMounted(() => {
|
||||
.app-banner .swiper-horizontal .swiper-slide img {
|
||||
border-radius: var(--radius-box);
|
||||
}
|
||||
|
||||
|
||||
.app-banner .mainpage-slider .swiper-pagination-bullet {
|
||||
background-color: var(--color-neutral-content); /* неактивные точки */
|
||||
opacity: 0.6; /* чуть прозрачнее, чтобы не отвлекали */
|
||||
}
|
||||
|
||||
.app-banner .mainpage-slider .swiper-pagination-bullet-active {
|
||||
background-color: var(--color-primary); /* активная точка */
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@ import {ref} from 'vue';
|
||||
*/
|
||||
export function useHapticScroll(
|
||||
threshold = 20,
|
||||
type = 'impactOccurred',
|
||||
type = 'selectionChanged',
|
||||
feedback = 'soft'
|
||||
) {
|
||||
const lastTranslate = ref(0);
|
||||
|
||||
@@ -28,3 +28,62 @@ export function formatPrice(raw) {
|
||||
|
||||
return `${sign}${formatted}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить CSS-переменную DaisyUI OKLH/OKLCH и вернуть HEX для Telegram
|
||||
* @param {string} cssVarName - например '--color-base-100'
|
||||
* @returns {string} #RRGGBB
|
||||
*/
|
||||
export function getCssVarOklchRgb(cssVarName) {
|
||||
// Получаем значение CSS-переменной
|
||||
const cssVar = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(cssVarName)
|
||||
.trim();
|
||||
|
||||
// Проверяем, что это OKLCH
|
||||
const match = cssVar.match(/^oklch\(\s*([\d.]+)%?\s+([\d.]+)\s+([\d.]+)\s*\)$/);
|
||||
if (!match) {
|
||||
console.warn(`CSS variable ${cssVarName} is not a valid OKLCH`);
|
||||
return { r:0, g:0, b:0 };
|
||||
}
|
||||
|
||||
// Парсим L, C, H
|
||||
const L = parseFloat(match[1]) / 100; // L в daisyUI в процентах
|
||||
const C = parseFloat(match[2]);
|
||||
const H = parseFloat(match[3]);
|
||||
|
||||
// --- OKLCH -> OKLab ---
|
||||
const hRad = (H * Math.PI) / 180;
|
||||
const a = C * Math.cos(hRad);
|
||||
const b = C * Math.sin(hRad);
|
||||
const l = L;
|
||||
|
||||
// --- OKLab -> Linear RGB ---
|
||||
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
|
||||
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
|
||||
const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
|
||||
|
||||
const lCubed = l_ ** 3;
|
||||
const mCubed = m_ ** 3;
|
||||
const sCubed = s_ ** 3;
|
||||
|
||||
let r = 4.0767416621 * lCubed - 3.3077115913 * mCubed + 0.2309699292 * sCubed;
|
||||
let g = -1.2684380046 * lCubed + 2.6097574011 * mCubed - 0.3413193965 * sCubed;
|
||||
let b_ = -0.0041960863 * lCubed - 0.7034186147 * mCubed + 1.7076147010 * sCubed;
|
||||
|
||||
// --- Линейный RGB -> sRGB ---
|
||||
const gammaCorrect = c => {
|
||||
c = Math.min(Math.max(c, 0), 1); // обрезаем 0..1
|
||||
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1/2.4) - 0.055;
|
||||
};
|
||||
|
||||
r = Math.round(gammaCorrect(r) * 255);
|
||||
g = Math.round(gammaCorrect(g) * 255);
|
||||
b_ = Math.round(gammaCorrect(b_) * 255);
|
||||
|
||||
// --- Преобразуем в HEX ---
|
||||
const toHex = n => n.toString(16).padStart(2, '0');
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b_)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
import {createApp} from 'vue';
|
||||
import App from './App.vue';
|
||||
import './style.css';
|
||||
import {VueTelegramPlugin} from 'vue-tg';
|
||||
import {router} from './router';
|
||||
import {createPinia} from 'pinia';
|
||||
|
||||
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
|
||||
import {useSettingsStore} from "@/stores/SettingsStore.js";
|
||||
import ApplicationError from "@/ApplicationError.vue";
|
||||
import AppMetaInitializer from "@/utils/AppMetaInitializer.ts";
|
||||
import {injectYaMetrika} from "@/utils/yaMetrika.js";
|
||||
|
||||
import { register } from 'swiper/element/bundle';
|
||||
import {register} from 'swiper/element/bundle';
|
||||
import 'swiper/element/bundle';
|
||||
import 'swiper/css/bundle';
|
||||
import AppLoading from "@/AppLoading.vue";
|
||||
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
|
||||
import {useBlocksStore} from "@/stores/BlocksStore.js";
|
||||
import {getCssVarOklchRgb} from "@/helpers.js";
|
||||
|
||||
register();
|
||||
|
||||
const pinia = createPinia();
|
||||
@@ -46,20 +46,29 @@ settings.load()
|
||||
filtersStore.applied = await filtersStore.fetchFiltersForMainPage();
|
||||
})
|
||||
.then(() => {
|
||||
console.debug('[Init] Set theme attributes');
|
||||
document.documentElement.setAttribute('data-theme', settings.theme[window.Telegram.WebApp.colorScheme]);
|
||||
if (settings.night_auto) {
|
||||
window.Telegram.WebApp.onEvent('themeChanged', function () {
|
||||
document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]);
|
||||
});
|
||||
}
|
||||
console.debug('[Init] Set theme attributes');
|
||||
document.documentElement.setAttribute('data-theme', settings.theme[window.Telegram.WebApp.colorScheme]);
|
||||
if (settings.night_auto) {
|
||||
window.Telegram.WebApp.onEvent('themeChanged', function () {
|
||||
document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]);
|
||||
});
|
||||
}
|
||||
|
||||
for (const key in settings.theme.variables) {
|
||||
document.documentElement.style.setProperty(key, settings.theme.variables[key]);
|
||||
for (const key in settings.theme.variables) {
|
||||
document.documentElement.style.setProperty(key, settings.theme.variables[key]);
|
||||
|
||||
}
|
||||
|
||||
const daisyUIBgColor = getCssVarOklchRgb('--color-base-100');
|
||||
window.Telegram.WebApp.setHeaderColor(daisyUIBgColor);
|
||||
window.Telegram.WebApp.setBackgroundColor(daisyUIBgColor);
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(() => new AppMetaInitializer(settings).init())
|
||||
.then(() => { appLoading.unmount(); app.mount('#app'); })
|
||||
.then(() => {
|
||||
appLoading.unmount();
|
||||
app.mount('#app');
|
||||
})
|
||||
.then(() => window.Telegram.WebApp.ready())
|
||||
.then(() => settings.ya_metrika_enabled && injectYaMetrika())
|
||||
.catch(error => {
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
themes: all;
|
||||
}
|
||||
|
||||
/**
|
||||
--color-base-100 - DaisyUI background
|
||||
*/
|
||||
|
||||
html, body, #app {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -11,16 +15,11 @@ html, body, #app {
|
||||
--swiper-pagination-bullet-horizontal-gap: 1px;
|
||||
--swiper-pagination-bullet-size: 6px;
|
||||
--swiper-pagination-color: #777;
|
||||
--swiper-pagination-bottom: -5px;
|
||||
--swiper-pagination-bottom: 0px;
|
||||
--product_list_title_max_lines: 2;
|
||||
--tc-navbar-min-height: 3rem;
|
||||
}
|
||||
|
||||
.swiper-pagination-bullets {
|
||||
border-radius: var(--radius-selector);
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: relative;
|
||||
/*padding-top: var(--tg-content-safe-area-inset-top);*/
|
||||
@@ -46,15 +45,14 @@ html, body, #app {
|
||||
.app-header {
|
||||
z-index: 60;
|
||||
position: fixed;
|
||||
height: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));
|
||||
min-height: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));
|
||||
max-height: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));
|
||||
height: calc(var(--tg-content-safe-area-inset-top, 0px) + var(--tg-safe-area-inset-top, 0px));
|
||||
min-height: calc(var(--tg-content-safe-area-inset-top, 0px) + var(--tg-safe-area-inset-top, 0px));
|
||||
max-height: calc(var(--tg-content-safe-area-inset-top, 0px) + var(--tg-safe-area-inset-top, 0px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
color: white;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.telecart-main-section {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<main class="px-4 mt-4">
|
||||
<header v-if="title" class="font-bold uppercase mb-4">{{ title }}</header>
|
||||
<header v-if="title" class="font-bold uppercase mb-4 text-center">{{ title }}</header>
|
||||
<section>
|
||||
<slot></slot>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div ref="goodsRef" class="space-y-8 mt-4">
|
||||
<MainPage/>
|
||||
<div>
|
||||
<Navbar/>
|
||||
<div ref="goodsRef" class="space-y-8 mt-4">
|
||||
<MainPage/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,6 +16,7 @@ import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
|
||||
import {useSettingsStore} from "@/stores/SettingsStore.js";
|
||||
import MainPage from "@/components/MainPage/MainPage.vue";
|
||||
import {useBlocksStore} from "@/stores/BlocksStore.js";
|
||||
import Navbar from "@/components/Navbar.vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'Home'
|
||||
|
||||
Reference in New Issue
Block a user