Page de Navigation Cuisinière : Étapes, Minuteur et Synthèse Vocale

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
  })
}

Étiquettes: WeChatMiniProgram SynthèseVocale Minuteur InterfaceUtilisateur DéveloppementMobile

Publié le 15 juin à 04h45