addon
This commit is contained in:
386
docs/FEATURE_THUMBNAILS_IN_LIST.md
Executable file
386
docs/FEATURE_THUMBNAILS_IN_LIST.md
Executable file
@@ -0,0 +1,386 @@
|
||||
# Fonctionnalité : Miniatures dans la liste des périphériques
|
||||
|
||||
## 🎯 Objectif
|
||||
|
||||
Afficher les miniatures (thumbnails) de 48px dans la liste des périphériques au lieu de l'icône générique.
|
||||
|
||||
**Comportement** :
|
||||
- Si le périphérique a une photo principale → Afficher la miniature
|
||||
- Si pas de photo → Afficher l'icône `<i class="fas fa-microchip"></i>`
|
||||
- Si l'image ne charge pas (erreur) → Fallback vers l'icône
|
||||
|
||||
---
|
||||
|
||||
## ✅ Implémentation
|
||||
|
||||
### 1. Backend - Schéma API
|
||||
|
||||
**Fichier** : `backend/app/schemas/peripheral.py` (ligne 150)
|
||||
|
||||
**Modification** : Ajout du champ `thumbnail_url` au schéma `PeripheralSummary`
|
||||
|
||||
```python
|
||||
class PeripheralSummary(BaseModel):
|
||||
"""Summary schema for peripheral lists"""
|
||||
id: int
|
||||
nom: str
|
||||
type_principal: str
|
||||
sous_type: Optional[str]
|
||||
marque: Optional[str]
|
||||
modele: Optional[str]
|
||||
etat: str
|
||||
rating: float
|
||||
prix: Optional[float]
|
||||
en_pret: bool
|
||||
is_complete_device: bool
|
||||
quantite_disponible: int
|
||||
thumbnail_url: Optional[str] = None # ← Nouveau champ
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
**Résultat** : L'API retourne maintenant l'URL du thumbnail pour chaque périphérique dans la liste.
|
||||
|
||||
---
|
||||
|
||||
### 2. Backend - Service
|
||||
|
||||
**Fichier** : `backend/app/services/peripheral_service.py` (lignes 179-210)
|
||||
|
||||
**Modification** : Récupération de la photo principale et construction de l'URL
|
||||
|
||||
```python
|
||||
# Import PeripheralPhoto here to avoid circular import
|
||||
from app.models.peripheral import PeripheralPhoto
|
||||
|
||||
# Convert to summary
|
||||
items = []
|
||||
for p in peripherals:
|
||||
# Get primary photo thumbnail
|
||||
thumbnail_url = None
|
||||
primary_photo = db.query(PeripheralPhoto).filter(
|
||||
PeripheralPhoto.peripheral_id == p.id,
|
||||
PeripheralPhoto.is_primary == True
|
||||
).first()
|
||||
|
||||
if primary_photo and primary_photo.thumbnail_path:
|
||||
# Convert file path to URL
|
||||
thumbnail_url = primary_photo.thumbnail_path.replace('/app/uploads/', '/uploads/')
|
||||
|
||||
items.append(PeripheralSummary(
|
||||
id=p.id,
|
||||
nom=p.nom,
|
||||
type_principal=p.type_principal,
|
||||
sous_type=p.sous_type,
|
||||
marque=p.marque,
|
||||
modele=p.modele,
|
||||
etat=p.etat or "Inconnu",
|
||||
rating=p.rating or 0.0,
|
||||
prix=p.prix,
|
||||
en_pret=p.en_pret or False,
|
||||
is_complete_device=p.is_complete_device or False,
|
||||
quantite_disponible=p.quantite_disponible or 0,
|
||||
thumbnail_url=thumbnail_url # ← Ajout du thumbnail
|
||||
))
|
||||
```
|
||||
|
||||
**Logique** :
|
||||
1. Pour chaque périphérique, requête SQL pour trouver la photo avec `is_primary = True`
|
||||
2. Si une photo principale existe et a un `thumbnail_path` :
|
||||
- Convertir le chemin serveur (`/app/uploads/...`) en URL web (`/uploads/...`)
|
||||
3. Sinon : `thumbnail_url = None`
|
||||
|
||||
**Exemple de résultat API** :
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 6,
|
||||
"nom": "USB Receiver",
|
||||
"thumbnail_url": "/uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"nom": "Flash Card Reader/Writer",
|
||||
"thumbnail_url": null
|
||||
}
|
||||
],
|
||||
"total": 46,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Frontend - JavaScript
|
||||
|
||||
**Fichier** : `frontend/js/peripherals.js` (lignes 325-329)
|
||||
|
||||
**Modification** : Affichage conditionnel de l'image ou de l'icône
|
||||
|
||||
```javascript
|
||||
<div class="peripheral-photo">
|
||||
${p.thumbnail_url
|
||||
? `<img src="${escapeHtml(p.thumbnail_url)}" alt="${escapeHtml(p.nom)}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<i class="fas fa-microchip" style="display:none;"></i>`
|
||||
: `<i class="fas fa-microchip"></i>`
|
||||
}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Logique** :
|
||||
- **Si `thumbnail_url` existe** :
|
||||
- Afficher `<img>` avec l'URL du thumbnail
|
||||
- Ajouter un `onerror` handler : si l'image ne charge pas, cacher l'image et afficher l'icône
|
||||
- Icône en `display:none` par défaut (visible seulement en cas d'erreur)
|
||||
|
||||
- **Si pas de `thumbnail_url`** :
|
||||
- Afficher directement l'icône `<i class="fas fa-microchip"></i>`
|
||||
|
||||
**Cas gérés** :
|
||||
1. ✅ Périphérique avec photo → Image affichée
|
||||
2. ✅ Périphérique sans photo → Icône affichée
|
||||
3. ✅ Image qui ne charge pas (404, erreur réseau) → Fallback vers icône
|
||||
|
||||
---
|
||||
|
||||
### 4. Frontend - CSS
|
||||
|
||||
**Fichier** : `frontend/css/peripherals.css` (lignes 156-176)
|
||||
|
||||
**Modification** : Style pour conteneur et images
|
||||
|
||||
```css
|
||||
.peripheral-photo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #232323;
|
||||
border: 1px solid #3e3d32;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #66d9ef;
|
||||
font-size: 1.5rem;
|
||||
overflow: hidden; /* ← Ajouté */
|
||||
}
|
||||
|
||||
.peripheral-photo img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain; /* ← Conserve ratio */
|
||||
}
|
||||
```
|
||||
|
||||
**Caractéristiques** :
|
||||
- **Conteneur** : 50×50px, fond sombre, bordure, centré
|
||||
- **Image** :
|
||||
- `max-width/max-height: 100%` → Ne dépasse jamais le conteneur
|
||||
- `width/height: auto` → Préserve le ratio d'aspect
|
||||
- `object-fit: contain` → L'image entière est visible sans déformation
|
||||
- Centré grâce au `display: flex` du parent
|
||||
|
||||
**Rendu visuel** :
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Nom │ Type │ Photo │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ USB Receiver │ USB │ [🖼️ Thumbnail] │
|
||||
│ Flash Card Reader │ Stockage│ [💾 Icône] │
|
||||
│ ConBee II │ USB │ [🖼️ Thumbnail] │
|
||||
│ CS9711Fingprint │ USB │ [🖼️ Thumbnail] │
|
||||
│ TL-WN823N │ Réseau │ [💾 Icône] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Exemples
|
||||
|
||||
### Exemple 1 : Périphérique avec photo
|
||||
|
||||
**API Response** :
|
||||
```json
|
||||
{
|
||||
"id": 6,
|
||||
"nom": "USB Receiver",
|
||||
"thumbnail_url": "/uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png"
|
||||
}
|
||||
```
|
||||
|
||||
**HTML généré** :
|
||||
```html
|
||||
<div class="peripheral-photo">
|
||||
<img src="/uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png"
|
||||
alt="USB Receiver"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<i class="fas fa-microchip" style="display:none;"></i>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Rendu** : Image du thumbnail (48px de large, ratio conservé)
|
||||
|
||||
---
|
||||
|
||||
### Exemple 2 : Périphérique sans photo
|
||||
|
||||
**API Response** :
|
||||
```json
|
||||
{
|
||||
"id": 5,
|
||||
"nom": "Flash Card Reader/Writer",
|
||||
"thumbnail_url": null
|
||||
}
|
||||
```
|
||||
|
||||
**HTML généré** :
|
||||
```html
|
||||
<div class="peripheral-photo">
|
||||
<i class="fas fa-microchip"></i>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Rendu** : Icône générique de puce électronique
|
||||
|
||||
---
|
||||
|
||||
### Exemple 3 : Image qui ne charge pas (erreur)
|
||||
|
||||
**Scénario** : Le fichier thumbnail est supprimé mais l'URL existe en base
|
||||
|
||||
**API Response** :
|
||||
```json
|
||||
{
|
||||
"id": 7,
|
||||
"nom": "Peripheral Test",
|
||||
"thumbnail_url": "/uploads/peripherals/photos/7/thumbnail/deleted.png"
|
||||
}
|
||||
```
|
||||
|
||||
**HTML généré** :
|
||||
```html
|
||||
<div class="peripheral-photo">
|
||||
<img src="/uploads/peripherals/photos/7/thumbnail/deleted.png"
|
||||
alt="Peripheral Test"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<i class="fas fa-microchip" style="display:none;"></i>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Comportement** :
|
||||
1. Navigateur tente de charger l'image
|
||||
2. Image introuvable → Event `onerror` déclenché
|
||||
3. JavaScript cache l'image (`display:none`)
|
||||
4. JavaScript affiche l'icône (`display:flex`)
|
||||
|
||||
**Rendu final** : Icône générique (fallback automatique)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Flux complet
|
||||
|
||||
```
|
||||
1. User charge page /peripherals.html
|
||||
↓
|
||||
2. JavaScript appelle GET /api/peripherals/?page=1&page_size=10
|
||||
↓
|
||||
3. Backend service list_peripherals()
|
||||
│ ├─> Query périphériques (avec pagination)
|
||||
│ └─> Pour chaque périphérique:
|
||||
│ ├─> Query photo principale (is_primary=True)
|
||||
│ └─> Si photo existe: thumbnail_url = "/uploads/..."
|
||||
↓
|
||||
4. API retourne JSON avec items[].thumbnail_url
|
||||
↓
|
||||
5. JavaScript génère HTML du tableau
|
||||
│ ├─> Si thumbnail_url → <img>
|
||||
│ └─> Sinon → <i class="fas fa-microchip">
|
||||
↓
|
||||
6. Navigateur affiche la liste
|
||||
│ ├─> Charge les images (si URLs présentes)
|
||||
│ └─> Affiche icônes (si pas d'URL ou erreur)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Fichiers modifiés
|
||||
|
||||
### Backend
|
||||
| Fichier | Lignes | Modification |
|
||||
|---------|--------|--------------|
|
||||
| `backend/app/schemas/peripheral.py` | 150 | Ajout `thumbnail_url: Optional[str] = None` |
|
||||
| `backend/app/services/peripheral_service.py` | 179-210 | Query photo principale + construction URL |
|
||||
|
||||
### Frontend
|
||||
| Fichier | Lignes | Modification |
|
||||
|---------|--------|--------------|
|
||||
| `frontend/js/peripherals.js` | 325-329 | Affichage conditionnel image/icône |
|
||||
| `frontend/css/peripherals.css` | 170-176 | Style pour `img` dans `.peripheral-photo` |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
### Test 1 : API retourne thumbnail_url
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8007/api/peripherals/?page=1&page_size=2" | \
|
||||
python3 -c "import sys, json; data=json.load(sys.stdin); print(data['items'][0].get('thumbnail_url'))"
|
||||
```
|
||||
|
||||
**Résultat attendu** :
|
||||
```
|
||||
/uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png
|
||||
```
|
||||
|
||||
### Test 2 : Plusieurs périphériques avec/sans photos
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8007/api/peripherals/?page=1&page_size=10" | \
|
||||
python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
for item in data['items'][:5]:
|
||||
thumb = item.get('thumbnail_url', 'None')
|
||||
print(f'{item[\"nom\"][:30]:30} → {thumb}')
|
||||
"
|
||||
```
|
||||
|
||||
**Résultat attendu** :
|
||||
```
|
||||
USB Receiver → /uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png
|
||||
Flash Card Reader/Writer → None
|
||||
ConBee II → /uploads/peripherals/photos/4/thumbnail/conbee2_thumb_20251231_101147.png
|
||||
CS9711Fingprint → /uploads/peripherals/photos/3/thumbnail/csfingerprint_thumb_20251231_101537.png
|
||||
TL-WN823N v2/v3 → None
|
||||
```
|
||||
|
||||
### Test 3 : Affichage visuel
|
||||
|
||||
1. Ouvrir `http://10.0.0.50:8087/peripherals.html`
|
||||
2. Vérifier la colonne "Photo" :
|
||||
- ✅ Les périphériques avec photos affichent la miniature
|
||||
- ✅ Les périphériques sans photos affichent l'icône puce
|
||||
- ✅ Les images sont bien dimensionnées (max 50×50px)
|
||||
|
||||
---
|
||||
|
||||
## 💡 Améliorations futures
|
||||
|
||||
- [ ] Cache des requêtes thumbnail (éviter N+1 queries)
|
||||
- [ ] Lazy loading des images (`loading="lazy"`)
|
||||
- [ ] Preview au survol (hover) de la miniature
|
||||
- [ ] Optimisation : JOIN au lieu de requête séparée par périphérique
|
||||
- [ ] Placeholder animé pendant chargement de l'image
|
||||
|
||||
---
|
||||
|
||||
**Date** : 31 décembre 2025
|
||||
**Statut** : ✅ Implémenté et testé
|
||||
**Impact** : Affichage des miniatures dans la liste des périphériques avec fallback automatique vers icône
|
||||
Reference in New Issue
Block a user