Prenons l’exemple d’une simple application d’apprentissage du vietnamien : l’interface présente les consonnes vietnamiennes, avec un menu en bas de l’écran.
Nous utilisons Tailwind comme modèle de page web.
Pour en savoir plus sur Tailwind, vous pouvez consulter nos autres tutoriels.
Modifiez le contenu du fichier index.html comme ci‑dessous
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Bảng phụ âm tiếng Việt</title>
<!-- CDN intégré de Tailwind : préférez la version locale après le build pour une utilisation hors‑ligne -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#2563eb',
bottomNav: '#ffffff',
cardBg: '#f8fafc'
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.safe-bottom {
padding-bottom: calc(4rem + env(safe-area-inset-bottom));
}
.card-shadow {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
}
body {
/* Empêche l’effet de rebond du scroll sur Cordova */
overscroll-behavior: none;
}
/* Style de l’onglet actif du menu bas */
.tab-active {
color: #2563eb;
}
</style>
</head>
<body class="bg-gray-100 font-sans text-gray-800">
<!-- Conteneur principal de la page -->
<div id="app" class="max-w-md mx-auto bg-white min-h-screen relative safe-bottom">
<!-- Barre de titre en haut -->
<header class="sticky top-0 bg-primary text-white px-4 py-3 shadow z-20">
<h1 class="text-lg font-bold text-center">Phụ Âm Tiếng Việt</h1>
</header>
<!-- Zone de contenu principal -->
<main class="p-4">
<!-- Page 1 : Consonnes -->
<div id="page-consonant" class="page">
<h2 class="text-base font-medium mb-3 text-gray-600">Cliquez sur une fiche pour afficher les détails</h2>
<div id="consonant-grid" class="grid grid-cols-3 gap-3">
<!-- Fiches de consonnes générées par JavaScript -->
</div>
</div>
<!-- Page 2 : Favoris -->
<div id="page-favorite" class="page hidden mt-4">
<h2 class="text-lg font-bold mb-4">Liste des favoris</h2>
<div id="favorite-grid" class="grid grid-cols-3 gap-3">
<p class="col-span-3 text-center text-gray-400 py-10">Aucun favori enregistré</p>
</div>
</div>
<!-- Page 3 : Paramètres -->
<div id="page-setting" class="page hidden mt-4">
<h2 class="text-lg font-bold mb-4">Paramètres</h2>
<div class="space-y-3">
<div class="bg-cardBg p-4 rounded-lg card-shadow">
<p>Application d’apprentissage des consonnes vietnamiennes</p>
<p class="text-sm text-gray-500 mt-1">Cordova + TailwindCSS</p>
</div>
</div>
</div>
</main>
<!-- ========== Barre de navigation inférieure ========== -->
<nav class="fixed bottom-0 left-1/2 -translate-x-1/2 w-full max-w-md bg-bottomNav border-t border-gray-200 flex z-30" style="padding-bottom: env(safe-area-inset-bottom);">
<button class="tab-btn flex-1 py-3 flex flex-col items-center tab-active" data-target="page-consonant">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18M9 21V9"/>
</svg>
<span class="text-xs mt-1">Consonnes</span>
</button>
<button class="tab-btn flex-1 py-3 flex flex-col items-center text-gray-500" data-target="page-favorite">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
<span class="text-xs mt-1">Favoris</span>
</button>
<button class="tab-btn flex-1 py-3 flex flex-col items-center text-gray-500" data-target="page-setting">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span class="text-xs mt-1">Paramètres</span>
</button>
</nav>
</div>
<!-- ========== Fenêtre modale pour afficher les détails de la consonne ========== -->
<div id="modal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center p-4">
<div class="bg-white rounded-xl w-full max-w-sm p-5 relative">
<button id="close-modal" class="absolute top-3 right-3 text-gray-400 text-xl">×</button>
<h3 id="modal-char" class="text-4xl font-bold text-primary text-center py-4"></h3>
<div class="space-y-2 text-gray-700">
<p><span class="font-semibold">Transcription :</span> <span id="modal-pronounce"></span></p>
<p><span class="font-semibold">Explication :</span> <span id="modal-desc"></span></p>
</div>
<button id="btn-fav" class="w-full mt-5 bg-primary text-white py-2 rounded-lg">Ajouter aux favoris</button>
</div>
</div>
<!-- Script Cordova (ne s’exécute que sur appareil physique) -->
<script src="cordova.js"></script>
<script src="js/main.js"></script>
</body>
</html>Langage du code : HTML, XML (xml)
Créez le fichier main.js dans le dossier www/js
Voici son contenu
// Données des consonnes vietnamiennes
const consonants = [
{char: "b", pronounce: "/ɓ/", desc: "Consonne implosive bilabiale"},
{char: "c", pronounce: "/k/", desc: "Consonne vélaire"},
{char: "ch", pronounce: "/tɕ/", desc: "Consonne affriquée palatale"},
{char: "d", pronounce: "/ɗ/", desc: "Consonne implosive alvéolaire"},
{char: "đ", pronounce: "/ɗ/", desc: "Consonne pré‑glottalisée alvéolaire"},
{char: "g", pronounce: "/ɣ/", desc: "Consonne fricative vélaire"},
{char: "gh", pronounce: "/ɣ/", desc: "Utilisé devant e, i"},
{char: "h", pronounce: "/h/", desc: "Consonne fricative glottale"},
{char: "k", pronounce: "/k/", desc: "Consonne occlusive vélaire"},
{char: "kh", pronounce: "/x/", desc: "Consonne fricative vélaire sourde"},
{char: "l", pronounce: "/l/", desc: "Consonne latérale alvéolaire"},
{char: "m", pronounce: "/m/", desc: "Consonne nasale bilabiale"},
{char: "n", pronounce: "/n/", desc: "Consonne nasale alvéolaire"},
{char: "ng", pronounce: "/ŋ/", desc: "Consonne nasale vélaire"},
{char: "ngh", pronounce: "/ŋ/", desc: "Utilisé devant e,i"},
{char: "nh", pronounce: "/ɲ/", desc: "Consonne nasale palatale"},
{char: "p", pronounce: "/p/", desc: "Consonne occlusive bilabiale non implosive"},
{char: "ph", pronounce: "/f/", desc: "Consonne fricative labio‑dentale"},
{char: "q", pronounce: "/kw/", desc: "Toujours associé à u"},
{char: "r", pronounce: "/r/", desc: "Consonne battue alvéolaire"},
{char: "s", pronounce: "/s/", desc: "Consonne fricative alvéolaire sourde"},
{char: "t", pronounce: "/t/", desc: "Consonne occlusive alvéolaire"},
{char: "th", pronounce: "/θ/", desc: "Consonne fricative dentale sourde"},
{char: "v", pronounce: "/v/", desc: "Consonne fricative labio‑dentale voisée"},
{char: "x", pronounce: "/s/", desc: "Consonne fricative alvéolaire sourde"}
];
// Sauvegarde de la liste des favoris dans localStorage
let favList = JSON.parse(localStorage.getItem("vn_consonant_fav")) || [];
// Éléments du DOM
const gridEl = document.getElementById("consonant-grid");
const favGridEl = document.getElementById("favorite-grid");
const modal = document.getElementById("modal");
const modalChar = document.getElementById("modal-char");
const modalPro = document.getElementById("modal-pronounce");
const modalDesc = document.getElementById("modal-desc");
const btnFav = document.getElementById("btn-fav");
const closeModal = document.getElementById("close-modal");
let currentItem = null;
// Génération des fiches de consonnes
function renderCards(){
gridEl.innerHTML = "";
consonants.forEach(item=>{
const div = document.createElement("div");
div.className = "bg-cardBg rounded-lg py-5 text-center card-shadow active:scale-95 transition-transform";
div.innerHTML = `<span class="text-2xl font-bold text-gray-800">${item.char}</span>`;
div.onclick = ()=> openModal(item);
gridEl.appendChild(div);
})
}
// Affichage de la page des favoris
function renderFav(){
favGridEl.innerHTML = "";
if(favList.length === 0){
favGridEl.innerHTML = `<p class="col-span-3 text-center text-gray-400 py-10">Aucun favori enregistré</p>`;
return;
}
favList.forEach(item=>{
const div = document.createElement("div");
div.className = "bg-cardBg rounded-lg py-5 text-center card-shadow active:scale-95 transition-transform";
div.innerHTML = `<span class="text-2xl font-bold text-gray-800">${item.char}</span>`;
div.onclick = ()=> openModal(item);
favGridEl.appendChild(div);
})
}
// Ouverture de la fenêtre détaillée
function openModal(item){
currentItem = item;
modalChar.innerText = item.char;
modalPro.innerText = item.pronounce;
modalDesc.innerText = item.desc;
updateFavButton();
modal.classList.remove("hidden");
}
// Fermeture de la fenêtre modale
closeModal.onclick = ()=>{
modal.classList.add("hidden");
}
modal.onclick = (e)=>{
if(e.target === modal) modal.classList.add("hidden");
}
// Bouton ajouter/supprimer des favoris
function updateFavButton(){
const exist = favList.find(x=>x.char === currentItem.char);
if(exist){
btnFav.innerText = "Retirer des favoris";
}else{
btnFav.innerText = "Ajouter aux favoris";
}
}
btnFav.onclick = ()=>{
const idx = favList.findIndex(x=>x.char === currentItem.char);
if(idx >=0){
favList.splice(idx,1);
}else{
favList.push(currentItem);
}
localStorage.setItem("vn_consonant_fav", JSON.stringify(favList));
updateFavButton();
renderFav();
}
// ========== Gestion des onglets de navigation ==========
const tabBtns = document.querySelectorAll(".tab-btn");
const pages = document.querySelectorAll(".page");
tabBtns.forEach(btn=>{
btn.onclick = ()=>{
const targetId = btn.dataset.target;
// Supprime l’état actif de tous les onglets
tabBtns.forEach(b=>{
b.classList.remove("tab-active");
b.querySelector("svg").classList.add("text-gray-500");
})
btn.classList.add("tab-active");
btn.querySelector("svg").classList.remove("text-gray-500");
// Masque toutes les pages
pages.forEach(p=>p.classList.add("hidden"));
document.getElementById(targetId).classList.remove("hidden");
}
})
// Lancement de l’application
document.addEventListener("DOMContentLoaded", ()=>{
renderCards();
renderFav();
})
// Événement deviceready de Cordova (pour téléphone)
document.addEventListener("deviceready", ()=>{
console.log("Cordova prêt");
}, false);Langage du code : JavaScript (javascript)
Recompilez et lancez l’application
D:\cordovademo\hello>cordova run android
Vous devriez voir ce contenu dans l’application

Les fiches sont cliquables et le menu du bas permet de naviguer entre les pages.
Dans l’étape suivante, nous allons ajouter la lecture audio au clic sur une fiche. Reportez‑vous à la page suivante.
Exemple d’interface d’application d’apprentissage du vietnamien