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

- Создан отдельный 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

@@ -1,8 +1,13 @@
{% extends 'base.html' %}
{% load quality_tags %}
{% load static %}
{% block title %}{{ product.name }}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'products/css/gallery.css' %}">
{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row">
@@ -40,9 +45,6 @@
<img src="{{ photo.get_thumbnail_url }}"
alt="Фото товара"
style="max-width: 100%; max-height: 100%; object-fit: contain;">
<!-- Индикатор качества в углу -->
{% quality_indicator photo %}
</div>
<div class="card-body p-2 text-center">
{% if photo.order == 0 %}
@@ -70,54 +72,58 @@
<h5 class="modal-title" id="photoGalleryModalLabel">Галерея фотографий товара</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div id="photoCarousel" class="carousel slide" data-bs-ride="false">
<div class="modal-body p-2 p-md-3">
<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">
{% for photo in product_photos %}
<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;">
<!-- Large 1200x1200 WebP для полного размера просмотра -->
<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 class="text-center" style="min-height: 50vh; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa;">
<img src="{{ photo.get_large_url }}" class="d-block" alt="Фото товара" style="height: auto; width: auto; max-height: 65vh; max-width: 90vw; object-fit: contain;">
</div>
</div>
{% endfor %}
</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>
<!-- Навигация и индикаторы под фото -->
<!-- Дополнительная информация под каруселью -->
{% 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">
<small class="text-muted">
<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>
</div>
{% endif %}
</div>
<div class="modal-footer">
<div id="galleryQualityStatus" class="me-auto">
<div class="modal-footer py-2 flex-wrap">
<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>
<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>
@@ -315,7 +321,6 @@ document.addEventListener('DOMContentLoaded', function() {
const photoCarousel = document.getElementById('photoCarousel');
if (photoGalleryModal && photoCarousel) {
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
const currentSlideEl = document.getElementById('currentSlide');
const mainBadgeEl = document.getElementById('mainBadge');
@@ -338,11 +343,12 @@ document.addEventListener('DOMContentLoaded', function() {
const button = event.relatedTarget;
const slideIndex = button.getAttribute('data-bs-slide-to');
if (slideIndex !== null) {
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
carousel.to(parseInt(slideIndex));
}
});
// Обновление счетчика, бейджа и статуса качества при переключении слайдов
// Обновление статуса при переключении слайдов
photoCarousel.addEventListener('slid.bs.carousel', function (event) {
const activeIndex = event.to;
const photoInfo = photos[activeIndex];
@@ -360,26 +366,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Обновляем статус качества
const qualityStatusEl = document.getElementById('galleryQualityStatus');
if (qualityStatusEl && 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>`;
}
qualityStatusEl.innerHTML = qualityHTML;
updateQualityStatus(qualityStatusEl, photoInfo);
}
});
@@ -387,9 +374,11 @@ document.addEventListener('DOMContentLoaded', function() {
const handleKeydown = function(event) {
if (event.key === 'ArrowLeft') {
event.preventDefault();
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
carousel.prev();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
carousel.next();
}
};
@@ -401,27 +390,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Инициализируем статус качества первого фото
const qualityStatusEl = document.getElementById('galleryQualityStatus');
if (qualityStatusEl && photos[0]) {
const photoInfo = 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;
updateQualityStatus(qualityStatusEl, photos[0]);
}
});
@@ -429,6 +398,30 @@ document.addEventListener('DOMContentLoaded', function() {
photoGalleryModal.addEventListener('hidden.bs.modal', function () {
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>

View File

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

View File

@@ -142,15 +142,8 @@
<td>
{% if item.photos.all %}
{% with photo=item.photos.first %}
<div class="photo-list-item">
<img src="{{ photo.get_thumbnail_url }}" alt="{{ item.name }}"
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>
<img src="{{ photo.get_thumbnail_url }}" alt="{{ item.name }}"
class="img-thumbnail rounded" style="width: 60px; height: 60px; object-fit: cover;">
{% endwith %}
{% else %}
<span class="text-muted small">Нет фото</span>
@@ -320,27 +313,4 @@
</div>
{% endif %}
</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 %}