Оптимизирована галерея фотографий и убраны индикаторы качества с фото

- Создан отдельный CSS файл products/static/products/css/gallery.css для стилей галереи
- Перенесены все стили модальной карусели из quality_indicator.css в gallery.css
- Добавлены современные стрелки навигации с широкой областью нажатия (80px)
- Улучшена видимость элементов управления: контрастные обводки, тени, градиенты
- Круглые индикаторы с полупрозрачной подложкой для видимости на любом фоне
- Адаптивные размеры для планшетов (60px) и мобильных (50px)
- Убраны визуальные индикаторы качества фото из углов изображений
- Оставлена только текстовая информация о качестве под фотографиями
- Упрощена разметка списка товаров - удалены ненужные обёртки и стили
This commit is contained in:
2025-11-15 23:31:46 +03:00
parent 0bac86264d
commit 6cb2123a82
4 changed files with 314 additions and 110 deletions

View File

@@ -0,0 +1,244 @@
/**
* Стили для фото-галереи товаров
* Современный дизайн карусели с адаптивной навигацией
*/
/* =========================
Модальная галерея товара
========================= */
/* Компактный modal footer */
.modal-footer.py-2 {
padding-top: 0.5rem !important;
padding-bottom: 0.5rem !important;
}
/* Стандартные индикаторы Bootstrap в стиле круглых точек */
#photoCarousel .carousel-indicators {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
margin: 0;
z-index: 15;
background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
padding: 8px 15px 5px;
border-radius: 20px;
}
#photoCarousel .carousel-indicators button {
width: 10px;
height: 10px;
border-radius: 50%;
margin: 0 4px;
padding: 0;
border: 2px solid rgba(0, 0, 0, 0.3);
background-color: rgba(255, 255, 255, 0.6);
opacity: 0.8;
transition: all 0.3s ease;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
#photoCarousel .carousel-indicators button:hover {
opacity: 1;
background-color: rgba(255, 255, 255, 0.9);
border-color: rgba(0, 0, 0, 0.5);
}
#photoCarousel .carousel-indicators button.active {
width: 12px;
height: 12px;
opacity: 1;
background-color: #fff;
border-color: rgba(0, 0, 0, 0.6);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
}
/* Современные кнопки навигации с широкой областью нажатия */
#photoCarousel .carousel-control-prev,
#photoCarousel .carousel-control-next {
width: 80px;
height: 100%;
opacity: 0;
transition: opacity 0.3s ease, background-color 0.3s ease;
background: linear-gradient(to right, rgba(0, 0, 0, 0.3), transparent);
}
#photoCarousel .carousel-control-next {
background: linear-gradient(to left, rgba(0, 0, 0, 0.3), transparent);
}
#photoCarousel:hover .carousel-control-prev,
#photoCarousel:hover .carousel-control-next {
opacity: 1;
}
#photoCarousel .carousel-control-prev:hover {
background: linear-gradient(to right, rgba(0, 0, 0, 0.5), transparent);
}
#photoCarousel .carousel-control-next:hover {
background: linear-gradient(to left, rgba(0, 0, 0, 0.5), transparent);
}
/* Современные иконки стрелок */
#photoCarousel .carousel-control-prev-icon,
#photoCarousel .carousel-control-next-icon {
width: 3rem;
height: 3rem;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 8px;
border: none;
padding: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
position: relative;
/* Скрываем стандартные SVG иконки Bootstrap */
background-image: none !important;
}
/* Переопределяем стандартные стрелки Bootstrap на современные */
#photoCarousel .carousel-control-prev-icon::before,
#photoCarousel .carousel-control-next-icon::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 14px;
height: 14px;
border-top: 3px solid #333;
border-right: 3px solid #333;
}
#photoCarousel .carousel-control-prev-icon::before {
transform: translate(-40%, -50%) rotate(-135deg);
}
#photoCarousel .carousel-control-next-icon::before {
transform: translate(-60%, -50%) rotate(45deg);
}
/* Эффект при наведении на иконку */
#photoCarousel .carousel-control-prev:hover .carousel-control-prev-icon,
#photoCarousel .carousel-control-next:hover .carousel-control-next-icon {
background-color: #fff;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
transform: scale(1.1);
}
#photoCarousel .carousel-control-prev:hover .carousel-control-prev-icon::before,
#photoCarousel .carousel-control-next:hover .carousel-control-next-icon::before {
border-color: #000;
}
/* =========================
Адаптивность
========================= */
/* Планшеты и средние экраны */
@media (max-width: 768px) {
/* Уменьшаем отступы modal body на мобильных */
.modal-body.p-2 {
padding: 0.5rem !important;
}
/* Уменьшаем высоту изображения на маленьких экранах */
.carousel-item > div {
min-height: 40vh !important;
}
.carousel-item img {
max-height: 55vh !important;
max-width: 95vw !important;
}
/* Компактный footer */
.modal-footer {
padding: 0.5rem;
font-size: 0.85rem;
}
#galleryQualityStatus .badge {
font-size: 0.7rem;
padding: 0.25rem 0.4rem;
}
/* Индикаторы чуть меньше на мобильных */
#photoCarousel .carousel-indicators button {
width: 8px;
height: 8px;
margin: 0 3px;
}
#photoCarousel .carousel-indicators button.active {
width: 10px;
height: 10px;
}
/* Стрелки меньше на планшетах */
#photoCarousel .carousel-control-prev,
#photoCarousel .carousel-control-next {
width: 60px;
}
#photoCarousel .carousel-control-prev-icon,
#photoCarousel .carousel-control-next-icon {
width: 2.5rem;
height: 2.5rem;
}
#photoCarousel .carousel-control-prev-icon::before,
#photoCarousel .carousel-control-next-icon::before {
width: 12px;
height: 12px;
border-width: 2.5px;
}
}
/* Мобильные телефоны */
@media (max-width: 576px) {
#photoCarousel .carousel-indicators button {
width: 7px;
height: 7px;
margin: 0 2px;
}
#photoCarousel .carousel-indicators button.active {
width: 9px;
height: 9px;
}
/* Текст счетчика более компактный */
.text-nowrap {
font-size: 0.75rem;
}
/* Кнопки навигации меньше на мобильных */
#photoCarousel .carousel-control-prev,
#photoCarousel .carousel-control-next {
width: 50px;
/* Стрелки видимы всегда на мобильных */
opacity: 0.6;
}
#photoCarousel:hover .carousel-control-prev,
#photoCarousel:hover .carousel-control-next {
opacity: 1;
}
#photoCarousel .carousel-control-prev-icon,
#photoCarousel .carousel-control-next-icon {
width: 2.2rem;
height: 2.2rem;
border-radius: 6px;
}
#photoCarousel .carousel-control-prev-icon::before,
#photoCarousel .carousel-control-next-icon::before {
width: 10px;
height: 10px;
border-width: 2px;
}
}

View File

@@ -1,8 +1,13 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load quality_tags %} {% load quality_tags %}
{% load static %}
{% block title %}{{ product.name }}{% endblock %} {% block title %}{{ product.name }}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'products/css/gallery.css' %}">
{% endblock %}
{% block content %} {% block content %}
<div class="container mt-5"> <div class="container mt-5">
<div class="row"> <div class="row">
@@ -40,9 +45,6 @@
<img src="{{ photo.get_thumbnail_url }}" <img src="{{ photo.get_thumbnail_url }}"
alt="Фото товара" alt="Фото товара"
style="max-width: 100%; max-height: 100%; object-fit: contain;"> style="max-width: 100%; max-height: 100%; object-fit: contain;">
<!-- Индикатор качества в углу -->
{% quality_indicator photo %}
</div> </div>
<div class="card-body p-2 text-center"> <div class="card-body p-2 text-center">
{% if photo.order == 0 %} {% if photo.order == 0 %}
@@ -70,54 +72,58 @@
<h5 class="modal-title" id="photoGalleryModalLabel">Галерея фотографий товара</h5> <h5 class="modal-title" id="photoGalleryModalLabel">Галерея фотографий товара</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body p-2 p-md-3">
<div id="photoCarousel" class="carousel slide" data-bs-ride="false"> <div id="photoCarousel" class="carousel slide" data-bs-ride="carousel">
<!-- Индикаторы (круглые точки) -->
{% if photos_count > 1 %}
<div class="carousel-indicators">
{% for photo in product_photos %}
<button type="button" data-bs-target="#photoCarousel" data-bs-slide-to="{{ forloop.counter0 }}"
{% if forloop.first %}class="active" aria-current="true"{% endif %}
aria-label="Слайд {{ forloop.counter }}"></button>
{% endfor %}
</div>
{% endif %}
<!-- Слайды с фотографиями -->
<div class="carousel-inner"> <div class="carousel-inner">
{% for photo in product_photos %} {% for photo in product_photos %}
<div class="carousel-item {% if forloop.first %}active{% endif %}"> <div class="carousel-item {% if forloop.first %}active{% endif %}">
<div class="text-center" style="min-height: 60vh; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa;"> <div class="text-center" style="min-height: 50vh; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa;">
<!-- Large 1200x1200 WebP для полного размера просмотра --> <img src="{{ photo.get_large_url }}" class="d-block" alt="Фото товара" style="height: auto; width: auto; max-height: 65vh; max-width: 90vw; object-fit: contain;">
<img src="{{ photo.get_large_url }}" class="d-block" alt="Фото товара" style="height: auto; width: auto; max-height: 75vh; max-width: 85vw; object-fit: contain;">
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<!-- Кнопки навигации -->
{% if photos_count > 1 %}
<button class="carousel-control-prev" type="button" data-bs-target="#photoCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Предыдущее</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#photoCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Следующее</span>
</button>
{% endif %}
</div> </div>
<!-- Навигация и индикаторы под фото --> <!-- Дополнительная информация под каруселью -->
{% if photos_count > 1 %} {% if photos_count > 1 %}
<div class="d-flex justify-content-center align-items-center mt-3 gap-3">
<button class="btn btn-outline-secondary" type="button" data-bs-target="#photoCarousel" data-bs-slide="prev">
<i class="bi bi-chevron-left"></i> Предыдущее
</button>
<div class="carousel-indicators position-static m-0">
{% for photo in product_photos %}
<button type="button" data-bs-target="#photoCarousel" data-bs-slide-to="{{ forloop.counter0 }}"
{% if forloop.first %}class="active" aria-current="true"{% endif %}
aria-label="Слайд {{ forloop.counter }}"
style="background-color: #6c757d;"></button>
{% endfor %}
</div>
<button class="btn btn-outline-secondary" type="button" data-bs-target="#photoCarousel" data-bs-slide="next">
Следующее <i class="bi bi-chevron-right"></i>
</button>
</div>
<div class="text-center mt-2"> <div class="text-center mt-2">
<small class="text-muted"> <small class="text-muted">
<span id="currentSlide">1</span> из {{ photos_count }} <span id="currentSlide">1</span> из {{ photos_count }}
<span id="mainBadge" {% if not product_photos.0.order == 0 %}style="display: none;"{% endif %} class="badge bg-success ms-2">⭐ Главное</span>
</small> </small>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="modal-footer"> <div class="modal-footer py-2 flex-wrap">
<div id="galleryQualityStatus" class="me-auto"> <div id="galleryQualityStatus" class="me-auto d-flex align-items-center gap-2">
<!-- Индикатор качества текущего фото в галерее --> <!-- Индикатор качества текущего фото в галерее -->
<span id="mainBadge" {% if not product_photos.0.order == 0 %}style="display: none;"{% endif %} class="badge bg-success">⭐ Главное</span>
</div> </div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button> <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Закрыть</button>
</div> </div>
</div> </div>
</div> </div>
@@ -315,7 +321,6 @@ document.addEventListener('DOMContentLoaded', function() {
const photoCarousel = document.getElementById('photoCarousel'); const photoCarousel = document.getElementById('photoCarousel');
if (photoGalleryModal && photoCarousel) { if (photoGalleryModal && photoCarousel) {
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
const currentSlideEl = document.getElementById('currentSlide'); const currentSlideEl = document.getElementById('currentSlide');
const mainBadgeEl = document.getElementById('mainBadge'); const mainBadgeEl = document.getElementById('mainBadge');
@@ -338,11 +343,12 @@ document.addEventListener('DOMContentLoaded', function() {
const button = event.relatedTarget; const button = event.relatedTarget;
const slideIndex = button.getAttribute('data-bs-slide-to'); const slideIndex = button.getAttribute('data-bs-slide-to');
if (slideIndex !== null) { if (slideIndex !== null) {
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
carousel.to(parseInt(slideIndex)); carousel.to(parseInt(slideIndex));
} }
}); });
// Обновление счетчика, бейджа и статуса качества при переключении слайдов // Обновление статуса при переключении слайдов
photoCarousel.addEventListener('slid.bs.carousel', function (event) { photoCarousel.addEventListener('slid.bs.carousel', function (event) {
const activeIndex = event.to; const activeIndex = event.to;
const photoInfo = photos[activeIndex]; const photoInfo = photos[activeIndex];
@@ -360,26 +366,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Обновляем статус качества // Обновляем статус качества
const qualityStatusEl = document.getElementById('galleryQualityStatus'); const qualityStatusEl = document.getElementById('galleryQualityStatus');
if (qualityStatusEl && photoInfo) { if (qualityStatusEl && photoInfo) {
let qualityHTML = ''; updateQualityStatus(qualityStatusEl, photoInfo);
if (photoInfo.quality_warning) {
qualityHTML = '<span class="badge bg-danger"><i class="bi bi-exclamation-circle"></i> Требует обновления</span>';
} else {
const qualityInfo = {
'excellent': { symbol: '🟢', label: 'Отлично', color: 'success' },
'good': { symbol: '🟡', label: 'Хорошо', color: 'info' },
'acceptable': { symbol: '🟠', label: 'Приемлемо', color: 'warning' },
'poor': { symbol: '🔴', label: 'Плохо', color: 'danger' },
'very_poor': { symbol: '🔴', label: 'Очень плохо', color: 'danger' },
};
const info = qualityInfo[photoInfo.quality_level] || { symbol: '⚪', label: 'Неизвестно', color: 'secondary' };
const sizeInfo = photoInfo.width && photoInfo.height ? ` (${photoInfo.width}×${photoInfo.height}px)` : '';
qualityHTML = `<span class="badge bg-${info.color}">${info.symbol} ${info.label}${sizeInfo}</span>`;
}
qualityStatusEl.innerHTML = qualityHTML;
} }
}); });
@@ -387,9 +374,11 @@ document.addEventListener('DOMContentLoaded', function() {
const handleKeydown = function(event) { const handleKeydown = function(event) {
if (event.key === 'ArrowLeft') { if (event.key === 'ArrowLeft') {
event.preventDefault(); event.preventDefault();
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
carousel.prev(); carousel.prev();
} else if (event.key === 'ArrowRight') { } else if (event.key === 'ArrowRight') {
event.preventDefault(); event.preventDefault();
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
carousel.next(); carousel.next();
} }
}; };
@@ -401,27 +390,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Инициализируем статус качества первого фото // Инициализируем статус качества первого фото
const qualityStatusEl = document.getElementById('galleryQualityStatus'); const qualityStatusEl = document.getElementById('galleryQualityStatus');
if (qualityStatusEl && photos[0]) { if (qualityStatusEl && photos[0]) {
const photoInfo = photos[0]; updateQualityStatus(qualityStatusEl, photos[0]);
let qualityHTML = '';
if (photoInfo.quality_warning) {
qualityHTML = '<span class="badge bg-danger"><i class="bi bi-exclamation-circle"></i> Требует обновления</span>';
} else {
const qualityInfo = {
'excellent': { symbol: '🟢', label: 'Отлично', color: 'success' },
'good': { symbol: '🟡', label: 'Хорошо', color: 'info' },
'acceptable': { symbol: '🟠', label: 'Приемлемо', color: 'warning' },
'poor': { symbol: '🔴', label: 'Плохо', color: 'danger' },
'very_poor': { symbol: '🔴', label: 'Очень плохо', color: 'danger' },
};
const info = qualityInfo[photoInfo.quality_level] || { symbol: '⚪', label: 'Неизвестно', color: 'secondary' };
const sizeInfo = photoInfo.width && photoInfo.height ? ` (${photoInfo.width}×${photoInfo.height}px)` : '';
qualityHTML = `<span class="badge bg-${info.color}">${info.symbol} ${info.label}${sizeInfo}</span>`;
}
qualityStatusEl.innerHTML = qualityHTML;
} }
}); });
@@ -429,6 +398,30 @@ document.addEventListener('DOMContentLoaded', function() {
photoGalleryModal.addEventListener('hidden.bs.modal', function () { photoGalleryModal.addEventListener('hidden.bs.modal', function () {
document.removeEventListener('keydown', handleKeydown); document.removeEventListener('keydown', handleKeydown);
}); });
// Функция обновления статуса качества
function updateQualityStatus(element, photoInfo) {
let qualityHTML = '';
if (photoInfo.quality_warning) {
qualityHTML = '<span class="badge bg-danger"><i class="bi bi-exclamation-circle"></i> Требует обновления</span>';
} else {
const qualityInfo = {
'excellent': { symbol: '🟢', label: 'Отлично', color: 'success' },
'good': { symbol: '🟡', label: 'Хорошо', color: 'info' },
'acceptable': { symbol: '🟠', label: 'Приемлемо', color: 'warning' },
'poor': { symbol: '🔴', label: 'Плохо', color: 'danger' },
'very_poor': { symbol: '🔴', label: 'Очень плохо', color: 'danger' },
};
const info = qualityInfo[photoInfo.quality_level] || { symbol: '⚪', label: 'Неизвестно', color: 'secondary' };
const sizeInfo = photoInfo.width && photoInfo.height ? ` (${photoInfo.width}×${photoInfo.height}px)` : '';
qualityHTML = `<span class="badge bg-${info.color}">${info.symbol} ${info.label}${sizeInfo}</span>`;
}
element.innerHTML = qualityHTML;
}
} }
}); });
</script> </script>

View File

@@ -188,9 +188,6 @@
title="Нажмите для увеличения"> title="Нажмите для увеличения">
<img src="{{ photo.get_thumbnail_url }}" class="card-img-top" alt="{{ kit.name }}" <img src="{{ photo.get_thumbnail_url }}" class="card-img-top" alt="{{ kit.name }}"
style="height: 100%; width: 100%; object-fit: cover;"> style="height: 100%; width: 100%; object-fit: cover;">
<!-- Индикатор качества в углу -->
{% quality_indicator photo %}
</div> </div>
{% if photo.order == 0 %} {% if photo.order == 0 %}
<div class="card-footer bg-success text-white text-center small">⭐ Главное</div> <div class="card-footer bg-success text-white text-center small">⭐ Главное</div>

View File

@@ -142,15 +142,8 @@
<td> <td>
{% if item.photos.all %} {% if item.photos.all %}
{% with photo=item.photos.first %} {% with photo=item.photos.first %}
<div class="photo-list-item"> <img src="{{ photo.get_thumbnail_url }}" alt="{{ item.name }}"
<img src="{{ photo.get_thumbnail_url }}" alt="{{ item.name }}" class="img-thumbnail rounded" style="width: 60px; height: 60px; object-fit: cover;">
class="img-thumbnail rounded" style="width: 60px; height: 60px; object-fit: cover;">
{% if item.item_type == 'product' %}
<span class="quality-icon" title="{{ photo.get_quality_level_display }}">
{{ photo|quality_icon_only }}
</span>
{% endif %}
</div>
{% endwith %} {% endwith %}
{% else %} {% else %}
<span class="text-muted small">Нет фото</span> <span class="text-muted small">Нет фото</span>
@@ -320,27 +313,4 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<style>
/* Стили для индикатора качества фото */
.photo-list-item {
position: relative;
display: inline-block;
}
.photo-list-item .quality-icon {
position: absolute;
top: 2px;
right: 2px;
font-size: 14px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
</style>
{% endblock %} {% endblock %}