feat: UI/UX, add reset cache to admin

This commit is contained in:
2025-11-16 20:34:03 +03:00
parent 6ac6a42e21
commit 09f1e514a9
21 changed files with 227 additions and 886 deletions

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

View File

@@ -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();

View File

@@ -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 }">

View File

@@ -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({

View File

@@ -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">

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import {ref} from 'vue';
*/
export function useHapticScroll(
threshold = 20,
type = 'impactOccurred',
type = 'selectionChanged',
feedback = 'soft'
) {
const lastTranslate = ref(0);

View File

@@ -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_)}`;
}

View File

@@ -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 => {

View File

@@ -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 {

View File

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

View File

@@ -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'