Skip to main content

Кастомизация маркетплейса

Подробное руководство по персонализации внешнего вида и функциональности маркетплейса.

Система тем

Структура темы

themes/
├── my-custom-theme/
│ ├── config.json
│ ├── styles/
│ │ ├── variables.css
│ │ ├── components.css
│ │ └── layouts.css
│ ├── templates/
│ │ ├── homepage.html
│ │ ├── product-page.html
│ │ └── category-page.html
│ ├── components/
│ │ ├── header.html
│ │ ├── footer.html
│ │ └── product-card.html
│ └── assets/
│ ├── images/
│ ├── fonts/
│ └── js/

Конфигурация темы

{
"name": "Моя Кастомная Тема",
"version": "1.0.0",
"description": "Описание темы",
"author": "Ваше имя",
"preview": "preview.jpg",
"compatibility": "^2.0.0",
"features": [
"responsive",
"dark-mode",
"rtl-support"
],
"customization": {
"colors": {
"primary": { "type": "color", "default": "#2563eb" },
"secondary": { "type": "color", "default": "#64748b" },
"accent": { "type": "color", "default": "#f59e0b" }
},
"typography": {
"headingFont": { "type": "font", "default": "Inter" },
"bodyFont": { "type": "font", "default": "Roboto" },
"fontSize": { "type": "range", "min": 12, "max": 20, "default": 16 }
},
"layout": {
"headerStyle": {
"type": "select",
"options": ["classic", "modern", "minimal"],
"default": "modern"
},
"sidebarPosition": {
"type": "select",
"options": ["left", "right", "none"],
"default": "left"
}
}
}
}

CSS кастомизация

Переменные дизайн-системы

/* variables.css */
:root {
/* Цветовая палитра */
--color-primary: #2563eb;
--color-primary-dark: #1d4ed8;
--color-primary-light: #3b82f6;

--color-secondary: #64748b;
--color-accent: #f59e0b;

--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #3b82f6;

/* Нейтральные цвета */
--color-gray-50: #f8fafc;
--color-gray-100: #f1f5f9;
--color-gray-200: #e2e8f0;
--color-gray-300: #cbd5e1;
--color-gray-400: #94a3b8;
--color-gray-500: #64748b;
--color-gray-600: #475569;
--color-gray-700: #334155;
--color-gray-800: #1e293b;
--color-gray-900: #0f172a;

/* Типографика */
--font-family-primary: 'Inter', sans-serif;
--font-family-secondary: 'Roboto', sans-serif;
--font-family-mono: 'Fira Code', monospace;

--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;

--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;

/* Spacing */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-5: 1.25rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
--spacing-10: 2.5rem;
--spacing-12: 3rem;
--spacing-16: 4rem;
--spacing-20: 5rem;
--spacing-24: 6rem;

/* Border radius */
--radius-none: 0;
--radius-sm: 0.125rem;
--radius-base: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-full: 9999px;

/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);

/* Transitions */
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 350ms ease;

/* Z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}

/* Темная тема */
[data-theme="dark"] {
--color-primary: #3b82f6;
--color-secondary: #9ca3af;

--color-gray-50: #0f172a;
--color-gray-100: #1e293b;
--color-gray-200: #334155;
--color-gray-300: #475569;
--color-gray-400: #64748b;
--color-gray-500: #94a3b8;
--color-gray-600: #cbd5e1;
--color-gray-700: #e2e8f0;
--color-gray-800: #f1f5f9;
--color-gray-900: #f8fafc;
}

Стилизация компонентов

/* components.css */

/* Кнопки */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-2) var(--spacing-4);
font-family: var(--font-family-primary);
font-weight: var(--font-weight-medium);
border-radius: var(--radius-md);
border: 1px solid transparent;
transition: var(--transition-fast);
cursor: pointer;
text-decoration: none;
}

.btn-primary {
background-color: var(--color-primary);
color: white;
}

.btn-primary:hover {
background-color: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}

.btn-secondary {
background-color: var(--color-gray-100);
color: var(--color-gray-700);
border-color: var(--color-gray-200);
}

.btn-outline {
background-color: transparent;
color: var(--color-primary);
border-color: var(--color-primary);
}

.btn-outline:hover {
background-color: var(--color-primary);
color: white;
}

/* Карточки товаров */
.product-card {
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
transition: var(--transition-base);
overflow: hidden;
}

.product-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}

.product-card__image {
position: relative;
aspect-ratio: 1;
overflow: hidden;
}

.product-card__image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: var(--transition-slow);
}

.product-card:hover .product-card__image img {
transform: scale(1.05);
}

.product-card__content {
padding: var(--spacing-4);
}

.product-card__title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-900);
margin-bottom: var(--spacing-2);
line-height: 1.2;
}

.product-card__price {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
}

.product-card__old-price {
font-size: var(--font-size-base);
color: var(--color-gray-500);
text-decoration: line-through;
margin-left: var(--spacing-2);
}

/* Хедер */
.header {
background: white;
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: var(--z-sticky);
}

.header__container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-4);
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
}

.header__logo {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
text-decoration: none;
}

.header__nav {
display: flex;
gap: var(--spacing-6);
}

.header__nav-link {
color: var(--color-gray-700);
text-decoration: none;
font-weight: var(--font-weight-medium);
transition: var(--transition-fast);
}

.header__nav-link:hover {
color: var(--color-primary);
}

/* Адаптивность */
@media (max-width: 768px) {
.header__container {
padding: 0 var(--spacing-2);
}

.header__nav {
display: none;
}

.product-card {
margin-bottom: var(--spacing-4);
}
}

Кастомные компоненты

Создание нового компонента

<!-- components/product-card-enhanced.html -->
<div class="product-card-enhanced" data-product-id="{{ product.id }}">
<div class="product-card-enhanced__badge">
{% if product.isNew %}
<span class="badge badge--new">Новинка</span>
{% endif %}
{% if product.discount > 0 %}
<span class="badge badge--discount">-{{ product.discount }}%</span>
{% endif %}
</div>

<div class="product-card-enhanced__image">
<img src="{{ product.image }}" alt="{{ product.name }}" loading="lazy">
<div class="product-card-enhanced__overlay">
<button class="btn btn--icon btn--wishlist" data-action="add-to-wishlist">
<i class="icon-heart"></i>
</button>
<button class="btn btn--icon btn--quick-view" data-action="quick-view">
<i class="icon-eye"></i>
</button>
</div>
</div>

<div class="product-card-enhanced__content">
<div class="product-card-enhanced__category">{{ product.category }}</div>
<h3 class="product-card-enhanced__title">{{ product.name }}</h3>
<div class="product-card-enhanced__rating">
{% for i in range(5) %}
<i class="icon-star {% if i < product.rating %}icon-star--filled{% endif %}"></i>
{% endfor %}
<span class="rating-count">({{ product.reviewCount }})</span>
</div>
<div class="product-card-enhanced__price">
<span class="price price--current">{{ product.price | currency }}</span>
{% if product.oldPrice %}
<span class="price price--old">{{ product.oldPrice | currency }}</span>
{% endif %}
</div>
<button class="btn btn--primary btn--full-width" data-action="add-to-cart">
Добавить в корзину
</button>
</div>
</div>

JavaScript для компонента

// assets/js/components/product-card-enhanced.js
class ProductCardEnhanced {
constructor(element) {
this.element = element;
this.productId = element.dataset.productId;
this.init();
}

init() {
this.bindEvents();
this.setupLazyLoading();
}

bindEvents() {
// Добавление в избранное
const wishlistBtn = this.element.querySelector('[data-action="add-to-wishlist"]');
wishlistBtn?.addEventListener('click', (e) => {
e.preventDefault();
this.toggleWishlist();
});

// Быстрый просмотр
const quickViewBtn = this.element.querySelector('[data-action="quick-view"]');
quickViewBtn?.addEventListener('click', (e) => {
e.preventDefault();
this.openQuickView();
});

// Добавление в корзину
const addToCartBtn = this.element.querySelector('[data-action="add-to-cart"]');
addToCartBtn?.addEventListener('click', (e) => {
e.preventDefault();
this.addToCart();
});
}

async toggleWishlist() {
try {
const response = await fetch(`/api/wishlist/toggle`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId: this.productId })
});

if (response.ok) {
const wishlistBtn = this.element.querySelector('[data-action="add-to-wishlist"]');
wishlistBtn.classList.toggle('btn--active');

// Показать уведомление
this.showNotification('Товар добавлен в избранное');
}
} catch (error) {
console.error('Ошибка при добавлении в избранное:', error);
}
}

openQuickView() {
// Открытие модального окна быстрого просмотра
const modal = new QuickViewModal(this.productId);
modal.open();
}

async addToCart() {
try {
const response = await fetch(`/api/cart/add`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId: this.productId,
quantity: 1
})
});

if (response.ok) {
// Анимация добавления
this.animateAddToCart();

// Обновление счетчика корзины
this.updateCartCounter();

// Показать уведомление
this.showNotification('Товар добавлен в корзину');
}
} catch (error) {
console.error('Ошибка при добавлении в корзину:', error);
}
}

animateAddToCart() {
const button = this.element.querySelector('[data-action="add-to-cart"]');
button.classList.add('btn--loading');

setTimeout(() => {
button.classList.remove('btn--loading');
button.classList.add('btn--success');

setTimeout(() => {
button.classList.remove('btn--success');
}, 2000);
}, 1000);
}

setupLazyLoading() {
const image = this.element.querySelector('img');
if (image && 'IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src || img.src;
img.classList.add('loaded');
observer.unobserve(img);
}
});
});

observer.observe(image);
}
}

showNotification(message) {
// Интеграция с системой уведомлений
if (window.NotificationSystem) {
window.NotificationSystem.show(message, 'success');
}
}

updateCartCounter() {
// Обновление счетчика товаров в корзине
const cartCounter = document.querySelector('.cart-counter');
if (cartCounter) {
const currentCount = parseInt(cartCounter.textContent) || 0;
cartCounter.textContent = currentCount + 1;
}
}
}

// Инициализация компонентов
document.addEventListener('DOMContentLoaded', () => {
const productCards = document.querySelectorAll('.product-card-enhanced');
productCards.forEach(card => new ProductCardEnhanced(card));
});

Кастомизация страниц

Шаблон страницы товара

<!-- templates/product-page.html -->
{% extends "layouts/base.html" %}

{% block title %}{{ product.name }} - {{ site.name }}{% endblock %}

{% block content %}
<div class="product-page">
<div class="container">
<!-- Breadcrumbs -->
<nav class="breadcrumbs">
<a href="/">Главная</a>
<span class="breadcrumbs__separator">/</span>
<a href="/category/{{ product.category.slug }}">{{ product.category.name }}</a>
<span class="breadcrumbs__separator">/</span>
<span class="breadcrumbs__current">{{ product.name }}</span>
</nav>

<div class="product-page__content">
<!-- Галерея изображений -->
<div class="product-gallery">
<div class="product-gallery__main">
<img src="{{ product.images[0] }}" alt="{{ product.name }}" id="main-image">
{% if product.images|length > 1 %}
<div class="product-gallery__zoom">
<i class="icon-zoom-in"></i>
</div>
{% endif %}
</div>

{% if product.images|length > 1 %}
<div class="product-gallery__thumbnails">
{% for image in product.images %}
<button class="thumbnail {% if loop.first %}thumbnail--active{% endif %}"
data-image="{{ image }}">
<img src="{{ image }}" alt="{{ product.name }}">
</button>
{% endfor %}
</div>
{% endif %}
</div>

<!-- Информация о товаре -->
<div class="product-info">
<h1 class="product-info__title">{{ product.name }}</h1>

<div class="product-info__rating">
<div class="rating">
{% for i in range(5) %}
<i class="icon-star {% if i < product.rating %}icon-star--filled{% endif %}"></i>
{% endfor %}
</div>
<span class="rating-text">{{ product.rating }} ({{ product.reviewCount }} отзывов)</span>
</div>

<div class="product-info__price">
<span class="price price--current">{{ product.price | currency }}</span>
{% if product.oldPrice %}
<span class="price price--old">{{ product.oldPrice | currency }}</span>
<span class="discount-badge">-{{ ((product.oldPrice - product.price) / product.oldPrice * 100) | round }}%</span>
{% endif %}
</div>

<div class="product-info__description">
{{ product.description | safe }}
</div>

<!-- Варианты товара -->
{% if product.variants %}
<div class="product-variants">
{% for variantGroup in product.variants %}
<div class="variant-group">
<label class="variant-group__label">{{ variantGroup.name }}:</label>
<div class="variant-group__options">
{% for option in variantGroup.options %}
<button class="variant-option"
data-variant="{{ variantGroup.slug }}"
data-value="{{ option.value }}">
{{ option.name }}
</button>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endif %}

<!-- Действия -->
<div class="product-actions">
<div class="quantity-selector">
<button class="quantity-btn" data-action="decrease">-</button>
<input type="number" value="1" min="1" max="{{ product.stock }}" class="quantity-input">
<button class="quantity-btn" data-action="increase">+</button>
</div>

<button class="btn btn--primary btn--large" data-action="add-to-cart">
Добавить в корзину
</button>

<button class="btn btn--secondary" data-action="add-to-wishlist">
<i class="icon-heart"></i>
В избранное
</button>
</div>

<!-- Дополнительная информация -->
<div class="product-meta">
<div class="meta-item">
<span class="meta-label">Артикул:</span>
<span class="meta-value">{{ product.sku }}</span>
</div>
<div class="meta-item">
<span class="meta-label">Наличие:</span>
<span class="meta-value {% if product.stock > 0 %}in-stock{% else %}out-of-stock{% endif %}">
{% if product.stock > 0 %}В наличии ({{ product.stock }} шт.){% else %}Нет в наличии{% endif %}
</span>
</div>
<div class="meta-item">
<span class="meta-label">Категория:</span>
<a href="/category/{{ product.category.slug }}" class="meta-value">{{ product.category.name }}</a>
</div>
</div>
</div>
</div>

<!-- Вкладки с дополнительной информацией -->
<div class="product-tabs">
<div class="tabs-nav">
<button class="tab-btn tab-btn--active" data-tab="description">Описание</button>
<button class="tab-btn" data-tab="specifications">Характеристики</button>
<button class="tab-btn" data-tab="reviews">Отзывы ({{ product.reviewCount }})</button>
<button class="tab-btn" data-tab="shipping">Доставка</button>
</div>

<div class="tab-content tab-content--active" data-tab="description">
{{ product.fullDescription | safe }}
</div>

<div class="tab-content" data-tab="specifications">
{% if product.specifications %}
<table class="specifications-table">
{% for spec in product.specifications %}
<tr>
<td class="spec-name">{{ spec.name }}</td>
<td class="spec-value">{{ spec.value }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>

<div class="tab-content" data-tab="reviews">
{% include "components/reviews.html" %}
</div>

<div class="tab-content" data-tab="shipping">
{% include "components/shipping-info.html" %}
</div>
</div>

<!-- Рекомендуемые товары -->
{% if relatedProducts %}
<section class="related-products">
<h2>Похожие товары</h2>
<div class="products-grid">
{% for product in relatedProducts %}
{% include "components/product-card.html" %}
{% endfor %}
</div>
</section>
{% endif %}
</div>
</div>
{% endblock %}

Кастомизация через админ-панель

// Интерфейс кастомизации
const customizationPanel = {
// Цветовая схема
colorPicker: {
primary: '#2563eb',
secondary: '#64748b',
accent: '#f59e0b',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444'
},

// Типографика
typography: {
headingFont: 'Inter',
bodyFont: 'Roboto',
fontSize: 16,
lineHeight: 1.5
},

// Компоненты
components: {
buttons: {
borderRadius: 8,
padding: '12px 24px',
fontSize: 16
},
cards: {
borderRadius: 12,
shadow: 'medium',
padding: 24
}
},

// Анимации
animations: {
duration: 250,
easing: 'ease-out',
enabled: true
}
};

// Применение изменений в реальном времени
function applyCustomization(settings) {
const root = document.documentElement;

// Применение цветов
Object.entries(settings.colorPicker).forEach(([key, value]) => {
root.style.setProperty(`--color-${key}`, value);
});

// Применение типографики
root.style.setProperty('--font-family-primary', settings.typography.headingFont);
root.style.setProperty('--font-family-secondary', settings.typography.bodyFont);
root.style.setProperty('--font-size-base', `${settings.typography.fontSize}px`);

// Сохранение настроек
localStorage.setItem('marketplace-customization', JSON.stringify(settings));
}