Visualisation des Fonctionnalités
L'application permet aux utilisateurs d'enregistrer leurs apports alimentaires et leurs dépenses énergétiques. En entrant dans la page d'enregistrement, l'utilisateur voit un résumé des données du jour. Un bouton d'ajout vert déclenche l'affichage d'une interface de sélection d'article, suivie d'une interface de saisie de quantité. Une fois la quantité saisie via un clavier numérique virtuel, la soumission met à jour instantanément le résumé des statistiques nutritionnelles et la liste des enregistrements du jour.
Développement de la Couche Métier de Suivi Alimentaire
Objectifs
- Définir un modèle de données nommé
RecapNutritionnelpour centraliser les statistiques nutritionnelles quotidiennes (calories ingérées, dépensées, glucides, protéines, lipides). - Créer une structure
InfoRegroupementpour organiser les données d'enreigstrements récupérées de la base de données par catégorie, facilitant ainsi leur affichage groupé dans l'interface utilisateur (par exemple, "Petit-déjeuner", "Déjeuner", "Dîner", "Activité physique"). Les composants utilisant cette structure, commeListeArticles, seront adaptés en conséquence. - Encapsuler toute la logique métier de manipulation des enregistrements (ajout, suppression, récupération, calcul de statistiques et regroupement) dans un service dédié nommé
ServiceSuiviAlimentaire.
Implémentation
RecapNutritionnel.ets:
export default class RecapNutritionnel {
// Calories consommées ce jour
caloriesIngerees: number;
// Énergie dépensée par l'activité physique
caloriesDepensees: number;
// Quantité totale de glucides consommés
totalGlucides: number;
// Quantité totale de protéines consommées
totalProteines: number;
// Quantité totale de lipides consommés
totalLipides: number;
constructor(ingerees: number = 0, depensees: number = 0, glucides: number = 0, proteines: number = 0, lipides: number = 0) {
this.caloriesIngerees = ingerees;
this.caloriesDepensees = depensees;
this.totalGlucides = glucides;
this.totalProteines = proteines;
this.totalLipides = lipides;
}
}
InfoRegroupement.ets:
export default class InfoRegroupement<T_TYPE, T_ELEMENT> {
// Catégorie du regroupement (ex: Repas, Exercice)
categorie: T_TYPE;
// Liste des éléments appartenant à ce regroupement
elements: T_ELEMENT[];
// Somme des calories pour les éléments de ce groupe
sommeCalories: number;
constructor(typeGroupe: T_TYPE, listeElements: T_ELEMENT[]) {
this.categorie = typeGroupe;
this.elements = listeElements;
this.sommeCalories = 0; // Initialisé à 0
}
}
ListeArticles.ets
La boucle ForEach dans le composant ListeArticles doit être mise à jour pour utiliser le nouveau type InfoRegroupement<CategorieArticle, ArticleEnregistre> pour ses groupes :
// ... dans le composant ListeArticles
ForEach(this.groupes, (groupe: InfoRegroupement<CategorieArticle, ArticleEnregistre>) => {
// ...
})
// ...
ServiceSuiviAlimentaire.ets
import UtilitaireDate from '../common/utils/DateUtil';
import ModeleArticle from '../model/ItemModel';
import ModeleEnregistrement from '../model/RecordModel';
import { TypeEnregistrementEnum, TypesEnregistrement } from '../model/RecordTypeModel';
import InfoRegroupement from '../viewmodel/GroupInfo'; // Assurez-vous que l'import est correct
import CategorieEnregistrement from '../viewmodel/RecordType';
import ValeurObjetEnregistrement from '../viewmodel/RecordVO';
import RecapNutritionnel from '../viewmodel/StatsInfo'; // Assurez-vous que l'import est correct
class ServiceSuiviAlimentaire {
/**
* Ajoute un nouvel enregistrement alimentaire ou d'activité physique.
* @param idType ID de la catégorie d'enregistrement.
* @param idArticle ID de l'article enregistré (aliment ou exercice).
* @param quantite Quantité (poids, durée, etc.).
* @returns Promesse du nombre d'enregistrements ajoutés.
*/
async ajouterEnregistrement(idType: number, idArticle: number, quantite: number): Promise<number> {
// 1. Détermine le moment de la création
const tempsCreation = (AppStorage.Get('dateSelectionnee') || UtilitaireDate.debutJournee(new Date())) as number;
// 2. Insère le nouvel enregistrement
return ModeleEnregistrement.insert({ idType, idArticle, quantite, tempsCreation });
}
/**
* Supprime un enregistrement par son identifiant.
* @param id Identifiant de l'enregistrement à supprimer.
* @returns Promesse du nombre d'enregistrements supprimés.
*/
async supprimerParId(id: number): Promise<number> {
return ModeleEnregistrement.deleteById(id);
}
/**
* Récupère la liste des enregistrements pour une date donnée.
* @param date Timestamp de la date à rechercher.
* @returns Promesse d'une liste d'objets ValeurObjetEnregistrement.
*/
async obtenirEnregistrementsParDate(date: number): Promise<ValeurObjetEnregistrement[]> {
// 1. Récupère les PO (Plain Object) de la base de données
const enregistrementsPO = await ModeleEnregistrement.listByDate(date);
const listeVO: ValeurObjetEnregistrement[] = [];
// 2. Convertit chaque RecordPO en ValeurObjetEnregistrement
for (const po of enregistrementsPO) {
const vo: ValeurObjetEnregistrement = {
id: po.id,
idType: po.idType,
quantite: po.quantite
} as ValeurObjetEnregistrement; // Conversion de type pour initialisation
// 2.2. Récupère les détails de l'article enregistré
vo.articleEnregistre = ModeleArticle.getById(po.idArticle, po.idType !== TypeEnregistrementEnum.ENTRAINEMENT);
// 2.3. Calcule les calories pour cet enregistrement
vo.calories = po.quantite * vo.articleEnregistre.calorie;
listeVO.push(vo);
}
return listeVO;
}
/**
* Calcule les informations statistiques (calories, nutriments) à partir d'une liste d'enregistrements.
* @param enregistrements Liste des enregistrements alimentaires/d'activité.
* @returns Objet RecapNutritionnel contenant les totaux.
*/
calculerRecapNutritionnel(enregistrements: ValeurObjetEnregistrement[]): RecapNutritionnel {
const recap = new RecapNutritionnel();
if (!enregistrements || enregistrements.length === 0) {
return recap;
}
for (const record of enregistrements) {
if (record.idType === TypeEnregistrementEnum.ENTRAINEMENT) {
// Activité physique: accumule les calories dépensées
recap.caloriesDepensees += record.calories;
} else {
// Aliment: accumule les calories ingérées, protéines, glucides, lipides
recap.caloriesIngerees += record.calories;
recap.totalGlucides += record.articleEnregistre.carbon;
recap.totalProteines += record.articleEnregistre.protein;
recap.totalLipides += record.articleEnregistre.fat;
}
}
return recap;
}
/**
* Regroupe une liste d'enregistrements par type d'enregistrement.
* @param enregistrements Liste des enregistrements.
* @returns Liste d'objets InfoRegroupement.
*/
regrouperEnregistrements(enregistrements: ValeurObjetEnregistrement[]): InfoRegroupement<CategorieEnregistrement, ValeurObjetEnregistrement>[] {
// 1. Crée des groupes vides pour chaque type d'enregistrement connu
const groupesInitialises = TypesEnregistrement.map(typeRec => new InfoRegroupement(typeRec, []));
if (!enregistrements || enregistrements.length === 0) {
return groupesInitialises;
}
// 2. Parcourt tous les enregistrements et les assigne à leur groupe respectif
for (const record of enregistrements) {
const groupeCorrespondant = groupesInitialises[record.idType];
if (groupeCorrespondant) {
groupeCorrespondant.elements.push(record);
groupeCorrespondant.sommeCalories += record.calories;
}
}
return groupesInitialises;
}
}
const serviceSuivi = new ServiceSuiviAlimentaire();
export default serviceSuivi;
Persistance des Données et Interaction Utilisateur
Objectifs
- Pour synchroniser les données entre les composants enfants
CarteStatistiquesetListeEnregistrements, la liste des enregistrements sera fournie par leur parent commun,IndexEnregistrements, via la décorateur@Provide. - Les composants d'affichage des statistiques (
StatistiquesCaloriesetStatistiquesNutriments) ainsi que laListeEnregistrementsseront modifiés pour recevoir leurs données via des propriétés réactives (@Prop) afin de réagir aux changements. - Implémenter la navigation depuis les titres de catégories d'enregistrement vers une page de saisie d'article (
IndexArticle) où la logique de persistance des nouvelles données sera gérée. - Ajouter un état réactif (
@State) au composant racine (PagePrincipale) pour informerIndexEnregistrementsde l'état d'affichage de la page, déclenchant ainsi un rechargement des données. - Les fonctions
@Builderutilisées dansCarteArticle.ets,StatistiquesCalories.etsetStatistiquesNutriments.etsseront ajustées pour utiliser des liaisons bidirectionnelles ($$:{}) pour les valeurs qui doivent être modifiées ou réactualisées par le parent ou d'autres logiques.
Implémentation
CarteStatistiques.ets:
import ConstantesPointRupture from '../../common/constants/BreakpointConstants';
import { ConstantesCommunes } from '../../common/constants/CommonConstants';
import UtilitaireDate from '../../common/utils/DateUtil';
import ServiceSuiviAlimentaire from '../../service/RecordService'; // Mise à jour de l'import
import ValeurObjetEnregistrement from '../../viewmodel/RecordVO';
import RecapNutritionnel from '../../viewmodel/StatsInfo'; // Mise à jour de l'import
import StatistiquesCalories from './CalorieState'; // Mise à jour de l'import
import DialogueChoixDate from './DatePickDialog';
import StatistiquesNutriments from './NutrientState'; // Mise à jour de l'import
@Component
export default struct CarteStatistiques {
@StorageProp('dateSelectionnee') dateSelectionnee: number = UtilitaireDate.debutJournee(new Date());
@StorageProp('pointRuptureActuel') pointRuptureActuel: string = ConstantesPointRupture.BREAKPOINT_SM;
// Surveillance des changements dans les enregistrements
@Consume @Watch('gererChangementEnregistrements') enregistrements: ValeurObjetEnregistrement[];
@State informationsRecap: RecapNutritionnel = new RecapNutritionnel();
gererChangementEnregistrements() {
this.informationsRecap = ServiceSuiviAlimentaire.calculerRecapNutritionnel(this.enregistrements);
}
controleurDialogue: CustomDialogController = new CustomDialogController({
builder: DialogueChoixDate({ dateSelectionnee: new Date(this.dateSelectionnee) })
});
build() {
Column() {
// 1. Informations sur la date
Row() {
Text(UtilitaireDate.formatDate(this.dateSelectionnee))
.fontColor($r('app.color.secondary_color'));
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor($r('app.color.secondary_color'));
}
.padding(ConstantesCommunes.ESPACE_8)
.onClick(() => this.controleurDialogue.open());
// 2. Informations statistiques
Swiper() {
// 2.1. Statistiques caloriques
StatistiquesCalories({
caloriesIngerees: this.informationsRecap.caloriesIngerees,
caloriesDepensees: this.informationsRecap.caloriesDepensees
});
// 2.2. Statistiques nutritionnelles
StatistiquesNutriments({
glucides: this.informationsRecap.totalGlucides,
proteines: this.informationsRecap.totalProteines,
lipides: this.informationsRecap.totalLipides
});
}
}
.width(ConstantesCommunes.MILLIEME_940)
.backgroundColor($r('app.color.stats_title_bgc'))
.borderRadius(ConstantesCommunes.RAYON_DEFAUT_18);
}
}
ListeEnregistrements.ets:
import router from '@ohos.router';
import { ConstantesCommunes } from '../../common/constants/CommonConstants';
import ServiceSuiviAlimentaire from '../../service/RecordService'; // Mise à jour de l'import
import InfoRegroupement from '../../viewmodel/GroupInfo'; // Mise à jour de l'import
import CategorieEnregistrement from '../../viewmodel/RecordType';
import ValeurObjetEnregistrement from '../../viewmodel/RecordVO';
// Fonction d'extension pour le style de texte gris
@Extend(Text) function texteGris() {
.fontSize(14)
.fontColor($r('app.color.light_gray'));
}
@Component
export default struct ListeEnregistrements {
// Surveillance des changements dans les enregistrements
@Consume @Watch('gererChangementEnregistrements') enregistrements: ValeurObjetEnregistrement[];
@State groupes: InfoRegroupement<CategorieEnregistrement, ValeurObjetEnregistrement> [] = [];
gererChangementEnregistrements() {
// Regroupe et calcule les statistiques des enregistrements alimentaires
this.groupes = ServiceSuiviAlimentaire.regrouperEnregistrements(this.enregistrements);
}
build() {
List({ space: ConstantesCommunes.ESPACE_10 }) {
ForEach(this.groupes, (groupe: InfoRegroupement<CategorieEnregistrement, ValeurObjetEnregistrement>) => {
ListItem() {
Column({ space: ConstantesCommunes.ESPACE_8 }) {
// 1. Titre du groupe
Row({ space: ConstantesCommunes.ESPACE_4 }) {
Image(groupe.categorie.icon).width(24);
Text(groupe.categorie.name).fontSize(18).fontWeight(ConstantesCommunes.POIDS_POLICE_700);
Text(`Recommandé ${groupe.categorie.min}~${groupe.categorie.max} kcal`).texteGris();
// Espacement
Blank();
Text(groupe.sommeCalories.toFixed(0)).fontSize(14).fontColor($r('app.color.primary_color'));
Text('kcal').texteGris();
Image($r('app.media.ic_public_add_norm_filled'))
.width(20)
.fillColor($r('app.color.primary_color'));
}
.width('100%')
.onClick(() => {
router.pushUrl({
url: 'pages/ItemIndex',
params: { type: groupe.categorie }
});
});
// 2. Liste des articles dans le groupe
List() {
ForEach(groupe.elements, (article: ValeurObjetEnregistrement) => {
ListItem() {
Row({ space: ConstantesCommunes.ESPACE_6 }) {
Image(article.articleEnregistre.image).width(50);
Column({ space: ConstantesCommunes.ESPACE_4 }) {
Text(article.articleEnregistre.name).fontWeight(ConstantesCommunes.POIDS_POLICE_500);
Text(`${article.quantite}${article.articleEnregistre.unit}`).texteGris();
}
Blank();
Text(`${article.calories.toFixed(0)} kcal`).texteGris();
}
.width('100%')
.padding(ConstantesCommunes.ESPACE_6);
}.swipeAction({ end: this.boutonSupprimer.bind(this) }); // Utilisation de bind
});
}
.width('100%');
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(ConstantesCommunes.RAYON_DEFAUT_18)
.padding(ConstantesCommunes.ESPACE_12);
}
});
}
.width(ConstantesCommunes.MILLIEME_940)
.height('100%')
.margin({ top: 10 });
}
@Builder boutonSupprimer() {
Image($r('app.media.ic_public_delete_filled'))
.width(20)
.fillColor(Color.Red)
.margin(5);
}
}
IndexEnregistrements.ets:
import UtilitaireDate from '../../common/utils/DateUtil';
import ServiceSuiviAlimentaire from '../../service/RecordService'; // Mise à jour de l'import
import ValeurObjetEnregistrement from '../../viewmodel/RecordVO';
import ListeEnregistrements from './RecordList'; // Mise à jour de l'import
import EnTeteRecherche from './SearchHeader';
import CarteStatistiques from './StatsCard'; // Mise à jour de l'import
@Component
export default struct IndexEnregistrements {
@Prop @Watch('gererAffichagePage') estPageVisible: boolean;
@Provide enregistrementsFournis: ValeurObjetEnregistrement[] = [];
// Surveillance des changements de date
@Watch('initialiserDonnees')
@StorageProp('dateSelectionnee') dateSelectionnee: number = UtilitaireDate.debutJournee(new Date());
gererAffichagePage() {
if (this.estPageVisible) {
this.initialiserDonnees();
}
}
// Fournit les données lors de l'apparition du composant ou d'un changement de date
async initialiserDonnees() {
this.enregistrementsFournis = await ServiceSuiviAlimentaire.obtenirEnregistrementsParDate(this.dateSelectionnee);
}
build() {
Column() {
// 1. Barre de recherche en haut
EnTeteRecherche();
// 2. Carte des statistiques
CarteStatistiques();
// 3. Liste des enregistrements
ListeEnregistrements()
.layoutWeight(1);
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.index_page_background'));
}
}
StatistiquesCalories.ets:
import { ConstantesCommunes } from '../../common/constants/CommonConstants';
@Component
export default struct StatistiquesCalories {
@Prop caloriesIngerees: number;
@Prop caloriesDepensees: number;
recommandationCalorique: number = ConstantesCommunes.CALORIES_RECOMMANDEES;
calculerCaloriesRestantes(): number {
return this.recommandationCalorique - this.caloriesIngerees + this.caloriesDepensees;
}
// Fonction d'aide pour l'affichage des statistiques
@Builder VueStatistique($$: { label: string, valeur: number, indice?: string }) {
Column({ space: ConstantesCommunes.ESPACE_6 }) {
Text($$.label)
.fontColor($r('app.color.gray'))
.fontWeight(ConstantesCommunes.POIDS_POLICE_600);
Text($$.valeur.toFixed(0))
.fontSize(20)
.fontWeight(ConstantesCommunes.POIDS_POLICE_700);
if ($$.indice) {
Text($$.indice)
.fontSize(12)
.fontColor($r('app.color.light_gray'));
}
}
}
build() {
Row({ space: ConstantesCommunes.ESPACE_6 }) {
// 1. Apport calorique
this.VueStatistique({ label: 'Apport Alimentaire', valeur: this.caloriesIngerees });
// 2. Calories restantes
Stack() {
// 2.1. Barre de progression
Progress({
value: this.caloriesIngerees,
total: this.recommandationCalorique,
type: ProgressType.Ring
})
.width(120)
.style({ strokeWidth: ConstantesCommunes.LARGEUR_DEFAUT_10 })
.color($r('app.color.primary_color'));
// 2.2. Données statistiques
this.VueStatistique({
label: 'Calories Restantes',
valeur: this.calculerCaloriesRestantes(),
indice: `Recommandé ${this.recommandationCalorique}`
});
}
// 3. Dépenses énergétiques
this.VueStatistique({ label: 'Dépense Physique', valeur: this.caloriesDepensees });
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
// Définition des marges
.padding({ top: 30, bottom: 35 });
}
}
StatistiquesNutriments.ets:
import { ConstantesCommunes } from '../../common/constants/CommonConstants';
@Component
export default struct StatistiquesNutriments {
@Prop glucides: number;
@Prop proteines: number;
@Prop lipides: number;
recommandationGlucides: number = ConstantesCommunes.GLUCIDES_RECOMMANDEES;
recommandationProteines: number = ConstantesCommunes.PROTEINES_RECOMMANDEES;
recommandationLipides: number = ConstantesCommunes.LIPIDES_RECOMMANDEES;
// Fonction d'aide pour l'affichage des nutriments
@Builder VueNutriment($$: {
libelle: string,
valeurActuelle: number,
valeurRecommandee: number,
couleurProgress: ResourceStr
}) {
Column({ space: ConstantesCommunes.ESPACE_6 }) {
Stack() {
Progress({
value: $$.valeurActuelle,
total: $$.valeurRecommandee,
type: ProgressType.Ring
})
.width(95)
.style({ strokeWidth: ConstantesCommunes.LARGEUR_DEFAUT_6 })
.color($$.couleurProgress);
Column({ space: ConstantesCommunes.ESPACE_6 }) {
Text('Apport/Recommandation')
.fontSize(12)
.fontColor($r('app.color.gray'));
Text(`${$$.valeurActuelle.toFixed(0)}/${$$.valeurRecommandee.toFixed(0)}`)
.fontSize(18)
.fontWeight(ConstantesCommunes.POIDS_POLICE_600);
}
}
Text(' (grammes)')
.fontSize(12)
.fontColor($r('app.color.light_gray'));
}
}
build() {
Row({ space: ConstantesCommunes.ESPACE_6 }) {
this.VueNutriment({
libelle: 'Glucides',
valeurActuelle: this.glucides,
valeurRecommandee: this.recommandationGlucides,
couleurProgress: $r('app.color.carbon_color')
});
this.VueNutriment({
libelle: 'Protéines',
valeurActuelle: this.proteines,
valeurRecommandee: this.recommandationProteines,
couleurProgress: $r('app.color.protein_color')
});
this.VueNutriment({
libelle: 'Lipides',
valeurActuelle: this.lipides,
valeurRecommandee: this.recommandationLipides,
couleurProgress: $r('app.color.fat_color')
});
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ top: 30, bottom: 35 });
}
}
IndexArticle.ets:
import router from '@ohos.router';
import { ConstantesCommunes } from '../common/constants/CommonConstants';
import { TypeEnregistrementEnum, TypesEnregistrement } from '../model/RecordTypeModel';
import ServiceSuiviAlimentaire from '../service/RecordService'; // Mise à jour de l'import
import CarteArticle from '../view/item/ItemCard'; // Mise à jour de l'import
import ListeArticles from '../view/item/ItemList'; // Mise à jour de l'import
import EnTetePanneauArticle from '../view/item/ItemPanelHeader';
import ClavierNumerique from '../view/item/NumberKeyboard';
import ArticleEnregistre from '../viewmodel/RecordItem';
import CategorieEnregistrement from '../viewmodel/RecordType';
@Entry
@Component
struct IndexArticle {
@State afficherPanneau: boolean = false;
@State quantiteSaisie: number = 1;
@State valeurClavier: string = '';
// item utilisé pour recevoir les données de l'article cliqué
@State articleSelectionne: ArticleEnregistre = null;
@State typeEnregistrement: CategorieEnregistrement = TypesEnregistrement[0];
@State estAliment: boolean = true;
onAfficherPanneau(article: ArticleEnregistre) {
// Contrôle l'affichage du panneau
this.quantiteSaisie = 1;
this.valeurClavier = '';
this.articleSelectionne = article;
this.afficherPanneau = true;
}
onPageShow() {
// 1. Récupère les paramètres de navigation
const params: any = router.getParams();
// 2. Obtient le type d'enregistrement cliqué
this.typeEnregistrement = params.type;
// 3. Détermine si c'est un aliment (différent d'entraînement)
this.estAliment = this.typeEnregistrement.id !== TypeEnregistrementEnum.ENTRAINEMENT;
}
build() {
Column() {
// 1. En-tête de navigation
this.EnTetePage();
// 2. Liste des articles
ListeArticles({ onAfficherPanneau: this.onAfficherPanneau.bind(this), estAliment: this.estAliment })
// Définir la hauteur de la liste pour que le panneau s'affiche correctement
.layoutWeight(1);
// 3. Panneau inférieur
Panel(this.afficherPanneau) {
// 3.1. En-tête du panneau (date)
EnTetePanneauArticle();
// 3.2. Carte de l'article enregistré
if (this.articleSelectionne) {
CarteArticle({ quantite: this.quantiteSaisie, article: $articleSelectionne }); // Utilisation de $
}
// 3.3. Clavier numérique
ClavierNumerique({ quantite: $quantiteSaisie, valeur: $valeurClavier }); // Utilisation de $
// 3.4. Boutons d'action
Row({ space: ConstantesCommunes.ESPACE_6 }) {
Button('Annuler')
.width(120)
.backgroundColor($r('app.color.light_gray'))
.type(ButtonType.Normal)
.borderRadius(6)
.onClick(() => this.afficherPanneau = false);
Button('Soumettre')
.width(120)
.type(ButtonType.Normal)
.borderRadius(6)
.backgroundColor($r('app.color.primary_color'))
.onClick(() => {
// 1. Enregistre les données
ServiceSuiviAlimentaire.ajouterEnregistrement(
this.typeEnregistrement.id,
this.articleSelectionne.id,
this.quantiteSaisie
)
.then(() => {
// 2. Ferme le panneau après succès
this.afficherPanneau = false;
});
});
}
.margin({ top: 10 });
}
.mode(PanelMode.Full)
.dragBar(false)
.backgroundMask($r('app.color.light_gray'))
.backgroundColor(Color.White);
}
.height('100%');
}
// En-tête de la page
@Builder EnTetePage() {
Row() {
Image($r('app.media.ic_public_back'))
.width(30)
.onClick(() => router.back());
Blank();
Text(this.typeEnregistrement.name).fontSize(18).fontWeight(ConstantesCommunes.POIDS_POLICE_600);
}
.width(ConstantesCommunes.MILLIEME_940)
.height(32);
}
}
PagePrincipale.ets:
import TypePointRupture from '../common/bean/BreanpointType';
import ConstantesPointRupture from '../common/constants/BreakpointConstants';
import { ConstantesCommunes } from '../common/constants/CommonConstants';
import SystemePointsRupture from '../common/utils/BreakpointSystem';
import IndexEnregistrements from '../view/record/RecordIndex'; // Mise à jour de l'import
@Entry
@Component
struct PagePrincipale {
@State indexOngletActuel: number = 0;
@State pageEstVisible: boolean = false;
private systemePointsRupture: SystemePointsRupture = new SystemePointsRupture();
@StorageProp('pointRuptureActuel') pointRuptureActuel: string = ConstantesPointRupture.BREAKPOINT_SM;
// Appelé lors de l'affichage de la page
onPageShow() {
this.pageEstVisible = true;
}
// Appelé lors de la masquage de la page
onPageHide() {
this.pageEstVisible = false;
}
aboutToAppear() {
this.systemePointsRupture.register();
}
aboutToDisappear() {
this.systemePointsRupture.unregister();
}
@Builder ConstructeurBarreOnglets(titre: ResourceStr, image: ResourceStr, index: number) {
Column({ space: ConstantesCommunes.ESPACE_8 }) {
Image(image)
.width(22)
.fillColor(this.selectionnerCouleur(index));
Text(titre)
.fontSize(14)
.fontColor(this.selectionnerCouleur(index));
}
}
// Logique de sélection de couleur pour les onglets
selectionnerCouleur(index: number): ResourceColor {
return this.indexOngletActuel === index ? $r('app.color.primary_color') : $r('app.color.gray');
}
build() {
Tabs({ barPosition: ConstantesPointRupture.BAR_POSITION.getValue(this.pointRuptureActuel) }) {
TabContent() {
IndexEnregistrements({ estPageVisible: this.pageEstVisible });
}
.tabBar(this.ConstructeurBarreOnglets($r('app.string.tab_record'), $r('app.media.ic_calendar'), 0));
TabContent() {
Text('Page Découverte');
}
.tabBar(this.ConstructeurBarreOnglets($r('app.string.tab_discover'), $r('app.media.discover'), 1));
TabContent() {
Text('Page Mon Profil');
}
.tabBar(this.ConstructeurBarreOnglets($r('app.string.tab_user'), $r('app.media.ic_user_portrait'), 2));
}
.width('100%')
.height('100%')
.vertical(false)
.onChange(index => this.indexOngletActuel = index)
.vertical(new TypePointRupture({
sm: false,
md: true,
lg: true
}).getValue(this.pointRuptureActuel));
}
}
CarteArticle.ets
import { ConstantesCommunes } from '../../common/constants/CommonConstants';
import ArticleEnregistre from '../../viewmodel/RecordItem'; // Mise à jour de l'import
@Component
export default struct CarteArticle {
@Prop quantite: number;
// Réception de l'article avec lien bidirectionnel
@Link article: ArticleEnregistre;
build() {
Column({ space: ConstantesCommunes.ESPACE_8 }) {
// 1. Image de l'article
Image(this.article.image)
.width(150);
// 2. Nom de l'article
Row() {
Text(this.article.name).fontWeight(ConstantesCommunes.POIDS_POLICE_700);
}
.backgroundColor($r('app.color.lightest_primary_color'))
.padding({ top: 5, bottom: 5, left: 15, right: 12 });
// Ligne de séparation
Divider().width(ConstantesCommunes.MILLIEME_940).opacity(0.6);
// 3. Informations nutritionnelles
Row({ space: ConstantesCommunes.ESPACE_8 }) {
this.InfoNutriment({ libelle: 'Calories (kcal)', valeur: this.article.calorie });
// Vérifie si l'ID est inférieur à 10000, les activités n'ayant pas de nutriments
if (this.article.id < 10000) {
this.InfoNutriment({ libelle: 'Glucides (g)', valeur: this.article.carbon });
this.InfoNutriment({ libelle: 'Protéines (g)', valeur: this.article.protein });
this.InfoNutriment({ libelle: 'Lipides (g)', valeur: this.article.fat });
}
}
Divider().width(ConstantesCommunes.MILLIEME_940).opacity(0.6);
// 4. Quantité
Row() {
Column({ space: ConstantesCommunes.ESPACE_4 }) {
Text(this.quantite.toFixed(1))
.fontSize(50).fontColor($r('app.color.primary_color'))
.fontWeight(ConstantesCommunes.POIDS_POLICE_600);
Divider().color($r('app.color.primary_color'));
}
Text(this.article.unit)
.fontColor($r('app.color.light_gray'))
.fontWeight(ConstantesCommunes.POIDS_POLICE_600);
}.width(150);
}
}
@Builder InfoNutriment($$: { libelle: string, valeur: number }) {
Column({ space: ConstantesCommunes.ESPACE_8 }) {
Text($$.libelle).fontSize(14).fontColor($r('app.color.light_gray'));
Text(($$.valeur * this.quantite).toFixed(1)).fontSize(18).fontWeight(ConstantesCommunes.POIDS_POLICE_700);
}
}
}