Fonctionnalités Principales
Cette page de navigation guide l'utilisateur à travers les étapes de cuisson, similaire à un système de navigation GPS. Les fonctionnalités clés incluent :
- Affichage des étapes avec descriptions textuelles (l'intégration d'images est en cours)
- Minuteur pour chaque étape avec rappels
- Synthèse vocale pour un suivi mains libres
- Gestion de plusieurs plats simultanés (version initiale sans algorithme d'optimisation temporelle)
Structure du Template
Voici le code WXML de la page, avec un en-tête personnalisé et une disposition responsive :
<!-- pages/meal/meal.wxml -->
<view class="navigation-header" style="padding-top: {{statusBarHeight}}px">
<button class="back-btn" bindtap="goBack">Retour</button>
<text class="timer-text">{{tempsAffiche}}</text>
</view>
<view class="container">
<!-- Sélecteur de plat -->
<scroll-view wx:if="{{listePlats.length > 1}}" scroll-x class="plat-tabs">
<view wx:for="{{listePlats}}" wx:key="identifiant"
class="tab {{indexPlatCourant === index ? 'selected' : ''}}"
bindtap="changerPlat" data-idx="{{index}}">
{{item.nom}}
</view>
</scroll-view>
<!-- Barre de progression -->
<view class="progress-container">
<view class="progress-bar" style="width: {{pourcentageProgression}}%></view>
<text class="step-count">Étape {{etapeActuelle + 1}} sur {{totalEtapes}}</text>
</view>
<!-- Contenu de l'étape courante -->
<view class="etape-section">
<text class="etape-description">{{descriptionEtape}}</text>
<image wx:if="{{imageEtape}}" src="{{imageEtape}}" mode="aspectFill" class="etape-image"/>
<view wx:if="{{conseilEtape}}" class="conseil-box">
<text class="conseil-icon">💡</text>
<text class="conseil-text">{{conseilEtape}}</text>
</view>
<text wx:if="{{dureeEstimee}}" class="duree-text">Durée estimée : {{dureeEstimee}} secondes</text>
</view>
<!-- Contrôle du minuteur -->
<view class="minuteur-section" wx:if="{{dureeEtape > 0}}">
<view class="minuteur-display">
<text class="minuteur-value">{{secondesRestantes}}</text>
<text class="minuteur-unit">sec</text>
</view>
<view class="minuteur-controls">
<button wx:if="{{!minuteurActif}}" class="ctrl-btn start" bindtap="lancerMinuteur">Démarrer</button>
<button wx:if="{{minuteurActif}}" class="ctrl-btn pause" bindtap="mettreEnPause">Pause</button>
<button wx:if="{{minuteurActif && !enPause}}" class="ctrl-btn reprendre" bindtap="reprendreMinuteur">Reprendre</button>
</view>
</view>
<!-- Boutons d'action -->
<view class="action-buttons">
<button class="btn-precedent" bindtap="etapePrecedente" disabled="{{etapeActuelle === 0}}">Précédent</button>
<button class="btn-suivant" bindtap="etapeSuivante">
{{etapeActuelle === totalEtapes - 1 ? 'Terminer' : 'Suivant'}}
</button>
</view>
</view>
Logique Métier Principale
Initialisation de la Page
// pages/meal/meal.js
Page({
data: {
listePlats: [],
indexPlatCourant: 0,
etapeActuelle: 0,
minuteurActif: false,
secondesRestantes: 0,
identifiantMinuteur: null,
statusBarHeight: 0
},
onLoad(params) {
const identifiantsPlats = params.ids.split('-')
this.chargerPlats(identifiantsPlats)
const app = getApp()
this.setData({ statusBarHeight: app.globalData.barHeight })
},
async chargerPlats(ids) {
const plats = []
for (const id of ids) {
const plat = await app.callCloudFunction('recupererRecette', { idRecette: id })
plats.push(plat)
}
this.setData({
listePlats: plats,
totalEtapes: plats[0].etapes.length,
descriptionEtape: plats[0].etapes[0].texte,
dureeEtape: plats[0].etapes[0].duree
})
},
onUnload() {
if (this.data.identifiantMinuteur) {
clearInterval(this.data.identifiantMinuteur)
}
}
})
Navigation entre Étapes
// Méthodes de navigation
etapePrecedente() {
if (this.data.etapeActuelle > 0) {
this.naviguerVersEtape(this.data.etapeActuelle - 1)
}
},
etapeSuivante() {
const { etapeActuelle, totalEtapes } = this.data
if (etapeActuelle < totalEtapes - 1) {
this.naviguerVersEtape(etapeActuelle + 1)
} else {
this.finaliserCuisson()
}
},
naviguerVersEtape(index) {
this.arreterMinuteur()
const plat = this.data.listePlats[this.data.indexPlatCourant]
const etape = plat.etapes[index]
this.setData({
etapeActuelle: index,
descriptionEtape: etape.texte,
imageEtape: etape.lienImage,
conseilEtape: etape.note,
dureeEtape: etape.duree,
secondesRestantes: etape.duree || 0,
pourcentageProgression: ((index + 1) / plat.etapes.length) * 100
})
this.prononcerTexte(etape.texte)
}
Implémentation du Minuteur
// Gestion du minuteur
lancerMinuteur() {
const duree = this.data.dureeEtape
this.setData({ minuteurActif: true, secondesRestantes: duree, enPause: false })
this.data.identifiantMinuteur = setInterval(() => {
const secondes = this.data.secondesRestantes - 1
this.setData({ secondesRestantes: secondes })
if (secondes <= 0) this.terminerMinuteur()
}, 1000)
},
mettreEnPause() {
if (this.data.identifiantMinuteur) {
clearInterval(this.data.identifiantMinuteur)
this.setData({ minuteurActif: false, enPause: true })
}
},
reprendreMinuteur() {
if (this.data.secondesRestantes > 0) {
this.setData({ minuteurActif: true, enPause: false })
this.data.identifiantMinuteur = setInterval(() => {
const secondes = this.data.secondesRestantes - 1
this.setData({ secondesRestantes: secondes })
if (secondes <= 0) this.terminerMinuteur()
}, 1000)
}
},
terminerMinuteur() {
clearInterval(this.data.identifiantMinuteur)
this.setData({ minuteurActif: false })
wx.vibrateLong()
wx.showToast({ title: 'Temps écoulé !', icon: 'success' })
}
Synthèse Vocale
// Utilisation du plugin TTS
const plugin = requirePlugin('WechatSI')
const moteurTTS = plugin.textToSpeech
prononcerTexte(contenu) {
moteurTTS.speak({
content: contenu,
lang: 'fr-FR',
success: () => console.log('Lecture réussie'),
fail: (erreur) => console.error('Erreur TTS:', erreur)
})
}
// Alternative avec lecture audio
const audioPlayer = wx.createInnerAudioContext()
prononcerTexte(contenu) {
const url = `https://api.tts.fr/parler?text=${encodeURIComponent(contenu)}`
audioPlayer.src = url
audioPlayer.play()
}
Gestion Multi-Plats
// Changement de plat actif
changerPlat(evenement) {
const idx = evenement.currentTarget.dataset.idx
const plat = this.data.listePlats[idx]
this.setData({
indexPlatCourant: idx,
etapeActuelle: 0,
descriptionEtape: plat.etapes[0].texte,
pourcentageProgression: 0
})
}
// Calcul de progression globale
calculerProgressionTotale() {
const { listePlats, indexPlatCourant, etapeActuelle } = this.data
const etapesTerminees = listePlats.slice(0, indexPlatCourant)
.reduce((somme, plat) => somme + plat.etapes.length, 0)
const etapesTotales = listePlats.reduce((somme, plat) => somme + plat.etapes.length, 0)
return ((etapesTerminees + etapeActuelle + 1) / etapesTotales) * 100
}
Problèmes Courants et Solutions
Problème 1 : Réinitialisation des secondes après pause
Symptôme : Les secondes restantes reviennent à 0 lors de la mise en pause.
Solution : Séparer les états de pause et de temps.
// Mauvaise implémentation
this.setData({ minuteurActif: false, secondesRestantes: 0 })
// Bonne implémentation
this.setData({ minuteurActif: false })
// secondesRestantes conservé
Problème 2 : Minuteur actif après fermeture de page
Symptôme : Le minuteur continue de fonctionner en arrière-plan.
Solution : Nettoyer dans onUnload.
onUnload() {
if (this.data.identifiantMinuteur) {
clearInterval(this.data.identifiantMinuteur)
}
audioPlayer.stop()
}
Problème 3 : Conflit de barre de navigation
Symptôme : Double barre de navigation (custom et système).
Solution : Définir le style de navigation dans meal.json.
// pages/meal/meal.json
{
"navigationStyle": "custom",
"usingComponents": {}
}
Finalisation de la Cuisson
// Terminer la session
finaliserCuisson() {
this.arreterTout()
this.reinitialiserDonnees()
wx.showToast({ title: 'Cuisson terminée !', duration: 1500 })
setTimeout(() => {
wx.switchTab({ url: '/pages/index/index' })
}, 1500)
}
arreterTout() {
if (this.data.identifiantMinuteur) clearInterval(this.data.identifiantMinuteur)
audioPlayer.stop()
}
reinitialiserDonnees() {
this.setData({
listePlats: [],
indexPlatCourant: 0,
etapeActuelle: 0,
minuteurActif: false,
secondesRestantes: 0
})
}