Un calendrier académique est un élément d'interface essentiel pour les applications universitaires. Ce guide détaille la création d'un composant personnalisé pour les WeChat Mini Programs, permettant d'afficher les semaines de cours, les dates de rentrée et les périodes de vacences de manière dynamique.
Configuration et Intégration
Pour utiliser ce composant, il suffit d'importer le code dans votre environnement de développement WeChat et de l'appeler avec les paramètres spécifiques à votre établissement. Notez que les dates doivent impérativement respecter le format yyyy-MM-dd.
<!-- Déclaration du composant dans la page -->
<calendrier-universitaire
session="2023-2024-1"
dateDebut="2023-09-04"
totalSemaines="20"
debutConges="18">
</calendrier-universitaire>
Les propriétés configurables sont les suivantes :
- session : L'identifiant du semestre en cours.
- dateDebut : La date officielle de la rentrée.
- totalSemaines : Le nombre total de semaines d'enseignement.
- debutConges : Le numéro de la semaine où débutent les vacances.
Logique de manipulation des dates
Avant de construire la vue, il est nécessaire de créer un module utilitaire pour gérer le formatage, l'extension des objets Date et le calcul des écarts temporels.
/**
* Module de gestion temporelle (calendarUtils.js)
*/
const formaterDate = (format = "yyyy-MM-dd", dateObj = new Date()) => {
const joursSemaine = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
const correspondances = {
"M+": dateObj.getMonth() + 1,
"d+": dateObj.getDate(),
"h+": dateObj.getHours(),
"m+": dateObj.getMinutes(),
"s+": dateObj.getSeconds(),
"S": dateObj.getMilliseconds(),
"K": joursSemaine[dateObj.getDay()]
};
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (dateObj.getFullYear() + "").substring(4 - RegExp.$1.length));
}
for (let cle in correspondances) {
if (new RegExp("(" + cle + ")").test(format)) {
const val = correspondances[cle];
format = format.replace(RegExp.$1, (RegExp.$1.length === 1) ? val : ("00" + val).substring(("" + val).length));
}
}
return format;
};
const etendreObjetDate = () => {
Date.prototype.ajouterTemps = function (annees = 0, mois = 0, jours = 0) {
if (jours !== 0) this.setDate(this.getDate() + jours);
if (mois !== 0) this.setMonth(this.getMonth() + mois);
if (annees !== 0) this.setFullYear(this.getFullYear() + annees);
};
};
const calculerDifferenceJours = (dateDebutStr, dateFinStr) => {
const [an1, mois1, jour1] = dateDebutStr.split("-").map(Number);
const [an2, mois2, jour2] = dateFinStr.split("-").map(Number);
const d1 = new Date(an1, mois1 - 1, jour1);
const d2 = new Date(an2, mois2 - 1, jour2);
return Math.floor((d2 - d1) / (1000 * 60 * 60 * 24));
};
module.exports = { formaterDate, etendreObjetDate, calculerDifferenceJours };
Contrôleur du composant
Le fichier JavaScript du composant orchestre la génération de la grille mensuelle. Il calcule le décalage nécessaire pour que chaque semaine débute un lundi et attribue des classes CSS dynamiques en fonction du statut de la journée (jour férié, week-end, jour de cours).
const outilsDate = require("calendarUtils.js");
const dateCourante = new Date();
Component({
properties: {
session: { type: String, value: '2023-2024-1' },
dateDebut: { type: String, value: '2023-09-04' },
totalSemaines: { type: Number, value: 20 },
debutConges: { type: Number, value: 18 }
},
data: {
grilleMois: [],
joursAvantConges: 0,
dateDebutConges: "",
moisEnCours: outilsDate.formaterDate("MM", dateCourante),
anneeEnCours: outilsDate.formaterDate("yyyy", dateCourante),
dateAujourdhui: outilsDate.formaterDate("yyyy-MM-dd", dateCourante)
},
created() {
outilsDate.etendreObjetDate();
},
ready() {
this.evaluerConges();
this.actualiserVue(dateCourante);
},
methods: {
naviguerVersDate(e) {
const dateCible = new Date(e.currentTarget.dataset.date);
this.data.moisEnCours = outilsDate.formaterDate("MM", dateCible);
this.data.anneeEnCours = outilsDate.formaterDate("yyyy", dateCible);
this.actualiserVue(dateCible);
this.setData({
moisEnCours: this.data.moisEnCours,
anneeEnCours: this.data.anneeEnCours
});
},
changerMois(e) {
const d = new Date(`${this.data.anneeEnCours}-${this.data.moisEnCours}-01`);
e.currentTarget.dataset.direction === "prev" ? d.ajouterTemps(0, -1) : d.ajouterTemps(0, 1);
this.data.moisEnCours = outilsDate.formaterDate("MM", d);
this.data.anneeEnCours = outilsDate.formaterDate("yyyy", d);
this.actualiserVue(d);
this.setData({
moisEnCours: this.data.moisEnCours,
anneeEnCours: this.data.anneeEnCours
});
},
actualiserVue(dateRef) {
const premierJourStr = outilsDate.formaterDate("yyyy-MM-01", dateRef);
const datePremier = new Date(premierJourStr);
let jourSemaine = datePremier.getDay();
jourSemaine = jourSemaine === 0 ? 7 : jourSemaine; // Normalisation : Lundi = 1
datePremier.ajouterTemps(0, 0, -(jourSemaine - 1));
this.construireCalendrier(datePremier);
},
construireCalendrier(dateInitiale) {
const semaines = [];
for (let i = 0; i < 6; i++) {
const jours = [];
let semaineAcademique = 0;
for (let j = 0; j < 7; j++) {
const dateStr = outilsDate.formaterDate("yyyy-MM-dd", dateInitiale);
if (j === 0) {
semaineAcademique = Math.floor(outilsDate.calculerDifferenceJours(this.data.dateDebut, dateStr) / 7) + 1;
semaineAcademique = semaineAcademique > 0 ? semaineAcademique : 0;
jours.push({ label: semaineAcademique, style: "indicateur-semaine", statut: "Semaine", extra: "" });
}
const cellule = { label: dateStr.split("-")[2], style: "hors-mois ", statut: "--", extra: "" };
if (outilsDate.formaterDate("MM", dateInitiale) === this.data.moisEnCours) cellule.style = "mois-actuel ";
if (dateStr === this.data.dateAujourdhui) cellule.style += "jour-actuel ";
if (dateStr === this.data.dateDebut) cellule.style += "debut-session ";
if (dateStr === this.data.dateDebutConges) cellule.style += "debut-conges ";
if (j >= 5) {
cellule.statut = "Week-end";
cellule.style += "weekend ";
} else if (semaineAcademique > 0 && semaineAcademique < this.data.totalSemaines) {
let couleurStatut = "en-cours ";
cellule.statut = "Cours";
cellule.extra = "detail-cours";
if (semaineAcademique >= this.data.debutConges) {
cellule.statut = "Congé";
couleurStatut = "conges ";
cellule.extra = "";
}
cellule.style += couleurStatut;
}
jours.push(cellule);
dateInitiale.ajouterTemps(0, 0, 1);
}
semaines.push(jours);
}
this.setData({ grilleMois: semaines });
},
evaluerConges() {
const d = new Date(this.data.dateDebut);
d.ajouterTemps(0, 0, (this.data.debutConges - 1) * 7);
const dateCongesStr = outilsDate.formaterDate("yyyy-MM-dd", d);
this.setData({
dateDebutConges: dateCongesStr,
joursAvantConges: outilsDate.calculerDifferenceJours(this.data.dateAujourdhui, dateCongesStr)
});
}
}
});
Structure de l'interface (WXML)
La vue exploite les directives wx:for pour itérer sur les semaines et les jours générés par le contrôleur. Des boutons de navigation permettent de sauter rapidement vers des dates clés comme la rentrée ou le début des vacances.
<view class="conteneur-principal">
<view class="entete flex-center">
<view class="navigation-mois flex-center">
<view class="fleche-gauche" bindtap="changerMois" data-direction="prev"></view>
<view class="affichage-date">{{anneeEnCours}} - {{moisEnCours}}</view>
<view class="fleche-droite" bindtap="changerMois" data-direction="next"></view>
</view>
<view class="actions-rapides flex-center">
<view class="bouton-action btn-aujourdhui" bindtap="naviguerVersDate" data-date="{{dateAujourdhui}}">Aujourd'hui</view>
<view class="bouton-action btn-rentree" bindtap="naviguerVersDate" data-date="{{dateDebut}}">Rentrée</view>
<view class="bouton-action btn-vacances" bindtap="naviguerVersDate" data-date="{{dateDebutConges}}">Vacances</view>
</view>
</view>
<view class="grille-calendrier">
<view class="ligne-jours flex-center">
<view wx:for='{{["Sem", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]}}' wx:key="index" class="cellule-entete">{{item}}</view>
</view>
<view wx:for="{{grilleMois}}" wx:key="index" class="ligne-semaine">
<view wx:for="{{item}}" wx:for-item="jour" wx:for-index="idx" wx:key="idx">
<view class="cellule-jour {{jour.style}}">
<view class="numero-jour">{{jour.label}}</view>
<view class="etiquette-statut {{jour.extra}}">{{jour.statut}}</view>
</view>
</view>
</view>
</view>
</view>
<view class="carte-info flex-center">
<view class="bloc-info">
<view class="point-vert"></view>
<view>Session : {{session}}</view>
</view>
<view class="bloc-info">
<view class="point-violet"></view>
<view>Semaines : {{totalSemaines}}</view>
</view>
<view class="bloc-info">
<view class="point-rouge"></view>
<view>Rentrée : {{dateDebut}}</view>
</view>
</view>
<view class="carte-info flex-center">
<view class="bloc-info">
<view class="point-vert"></view>
<view>Vacances : {{dateDebutConges}}</view>
</view>
<view class="bloc-info">
<view class="point-violet"></view>
<view>Semaine : {{debutConges}}</view>
</view>
<view class="bloc-info">
<view class="point-rouge"></view>
<view>J-{{joursAvantConges}} avant vacances</view>
</view>
</view>
Feuille de styles (WXSS)
Le style repose fortement sur le modèle de boîte flexible (Flexbox) pour l'alignement. Les spécificités CSS sont utilisées pour superposer les couleurs en fonction des états multiples d'une même cellule (par exemple, un jour de cours qui est aussi le jour actuel).
page {
margin: 0;
padding: 15px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
background-color: #F4F6F8;
}
.conteneur-principal, .carte-info {
background: #FFFFFF;
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.flex-center {
display: flex;
align-items: center;
}
.entete {
justify-content: space-between;
margin-bottom: 15px;
flex-wrap: wrap;
gap: 10px;
}
.affichage-date {
margin: 0 15px;
font-weight: 700;
font-size: 16px;
color: #333;
}
.bouton-action {
padding: 5px 10px;
margin: 0 5px;
color: #FFF;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.btn-aujourdhui { background-color: #1E9FFF; }
.btn-rentree { background-color: #FF6347; }
.btn-vacances { background-color: #3CB371; }
.ligne-jours, .ligne-semaine {
display: flex;
justify-content: space-around;
margin: 8px 0;
}
.cellule-entete {
width: 35px;
text-align: center;
font-weight: bold;
color: #666;
}
.cellule-jour {
width: 35px;
text-align: center;
color: #333;
}
.numero-jour {
line-height: 30px;
width: 30px;
height: 30px;
margin: 2px auto;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
}
.etiquette-statut {
font-size: 10px;
color: #999;
}
/* États visuels et priorités */
.hors-mois { color: #D3D3D3 !important; }
.jour-actuel .numero-jour,
.debut-session .numero-jour,
.debut-conges .numero-jour {
color: #FFF !important;
background: #1E9FFF;
}
.debut-session .numero-jour { background: #FF6347; }
.debut-conges .numero-jour { background: #3CB371; }
.indicateur-semaine { color: #9F8BEC; font-weight: bold; }
.mois-actuel .detail-cours { color: #777; }
.weekend, .conges { color: #3CB371; }
.fleche-gauche, .fleche-droite {
width: 20px;
height: 20px;
background-size: contain;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%238a8a8a'%3E%3Cpath d='M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z'/%3E%3C/svg%3E");
}
.fleche-droite { transform: rotate(180deg); }
/* Légende inférieure */
.carte-info {
justify-content: space-between;
font-size: 12px;
color: #555;
}
.bloc-info { display: flex; align-items: center; }
.point-vert, .point-violet, .point-rouge {
width: 10px; height: 10px; border-radius: 50%; margin-right: 6px;
}
.point-vert { background: #3CB371; }
.point-violet { background: #9F8BEC; }
.point-rouge { background: #FF6347; }