稀土掘金技术社区

程序员自己开发的法语学习工具,帮我收获了爱情


图片

(金石瓜分计划强势上线,速戳上图了解详情)

前言

大家好,我是奈德丽。

今天女朋友突然跟我说:"我下周就要法语考试了,但是完全不知道该怎么复习,冠词和过去时复合搞得我头都大了..." 女朋友让我帮她背法语,这我一个工科男怎么会法语啊,太高估我了吧,但记住咱是程序员呀,秉着程序为人而服务的理念,我想着能不能给她做个小工具,帮她来复习呢,现在Ai这么强大,刚好年费的Cursor不能闲着,让它来帮我辅助开发了一款工具,让女朋友学起来就跟玩游戏一样,虽然会有点耗费时间,但是最终也是如愿以偿,得到了不错的效果。

先来看下受到女朋友连连好评的程序长什么样吧!

Image

那就简单做一下需求分析吧

女朋友向我抱怨了一堆话,总结起来就是:

  1. 「语法规则记不住」 - 法语冠词有定冠词、不定冠词、部分冠词,各种变化规则
  2. 「动词变位太复杂」 - 不规则动词的过去分词总是记混
  3. 「缺乏练习工具」 - 课本上的练习题做完就没了,想多练都没地方
  4. 「学习效率低」 - 翻书查资料浪费时间,没有系统性

哪有那么难,说到底还是上课没好好听! [狗头保命 !!]

那么怎么去实现这样的工具呢?我把它分成了五步

第一步:语法知识库 - 把复杂的规则可视化

初版实现:静态页面

我先从最基础的开始,把她需要的语法知识整理成一个结构化的页面:

<!DOCTYPE html>
<htmllang="zh-CN">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>法语语法总结</title>
</head>
<body>
<divclass="container">
<h1>🇫🇷 法语语法总结</h1>

<!-- 冠词部分 -->
<sectionclass="grammar-section">
<h2>冠词 (Les Articles)</h2>
<divclass="rule-card">
<h3>定冠词</h3>
<divclass="examples">
<spanclass="article masculine">le</span> livre (阳性单数)
<spanclass="article feminine">la</span> table (阴性单数)  
<spanclass="article plural">les</span> livres (复数)
</div>
</div>
</section>

<!-- 过去时复合部分 -->
<sectionclass="grammar-section">
<h2>过去时复合 (Le Passé Composé)</h2>
<divclass="rule-card">
<h3>构成公式</h3>
<divclass="formula">
                    助动词(avoir/être) + 过去分词
</div>
</div>
</section>
</div>
</body>
</html>

配上一些基础样式:

.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}

.rule-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
color: white;
box-shadow: 010px30pxrgba(0,0,0,0.1);
transition: transform 0.3s ease;
}

.rule-card:hover {
transform: translateY(-5px);
}

.article {
padding: 4px12px;
border-radius: 20px;
font-weight: bold;
margin: 05px;
}

.masculine { background: #3498db; }
.feminine { background: #e74c3c; }
.plural { background: #f39c12; }

女朋友看了第一版说:"哇,这个比课本清楚多了!但是能不能加点练习?光看不练还是记不住。"

好吧,需求升级了。

第二步:动词卡片系统 - 让学习变成游戏

核心数据结构设计

既然要做练习,那就得先把数据结构设计好:

// 不规则动词数据
const irregularVerbs = [
    { 
infinitive: 'avoir', 
participle: 'eu', 
example: "J'ai eu de la chance", 
difficulty: 'easy',
pronunciation: '[a.vwaʁ]'
    },
    { 
infinitive: 'être', 
participle: 'été', 
example: "Il a été malade", 
difficulty: 'easy',
pronunciation: '[ɛtʁ]'
    },
// ... 更多动词
];

// être动词(需要用être作助动词的动词)
const etreVerbs = [
    { 
infinitive: 'aller', 
participle: 'allé(e)', 
example: "Je suis allé(e) au cinéma", 
difficulty: 'easy'
    },
// ... 更多动词
];

3D翻转卡片实现

这里是最有趣的部分,我想做一个可以翻转的卡片,正面显示动词原形,背面显示过去分词和例句:

.card {
width: 400px;
height: 250px;
position: relative;
transform-style: preserve-3d;
transition: transform 0.6s;
cursor: pointer;
}

.card.flipped {
transform: rotateY(180deg);
}

.card-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-shadow: 010px30pxrgba(0,0,0,0.3);
}

.card-front {
background: linear-gradient(135deg, #74b9ff, #0984e3);
color: white;
}

.card-back {
background: linear-gradient(135deg, #00b894, #00a085);
color: white;
transform: rotateY(180deg);
}

JavaScript控制逻辑:

classVerbCardManager{
constructor() {
this.currentMode = 'irregular';
this.currentCards = irregularVerbs;
this.currentIndex = 0;
this.isFlipped = false;
this.stats = {
correct: 0,
total: 0
        };
    }

// 翻转卡片
    flipCard() {
const card = document.getElementById('flashcard');
        card.classList.toggle('flipped');
this.isFlipped = !this.isFlipped;
    }

// 更新卡片内容
    updateCard() {
const card = this.currentCards[this.currentIndex];
document.getElementById('verbInfinitive').textContent = card.infinitive;
document.getElementById('pastParticiple').textContent = card.participle;
document.getElementById('example').textContent = card.example;

// 重置翻转状态
document.getElementById('flashcard').classList.remove('flipped');
this.isFlipped = false;

this.updateStats();
this.updateProgress();
    }

// 下一张卡片
    nextCard() {
if (this.currentIndex < this.currentCards.length - 1) {
this.currentIndex++;
this.updateCard();
        }
    }

// 上一张卡片
    previousCard() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.updateCard();
        }
    }
}

女朋友试用后兴奋地说:"这个翻转效果太酷了!但是我想测试一下自己到底记住了多少,能不能加个测试模式?"

需求又升级了...

第三步:智能测试系统 - 四选一选择题

测试题生成算法

为了让测试更有挑战性,我设计了一个干扰项生成算法:

functiongenerateQuizOptions() {
const currentCard = currentCards[currentIndex];
const correctAnswer = currentCard.participle;

// 从所有动词中筛选出错误选项
const allParticiples = currentCards.map(card => card.participle);
const wrongAnswers = allParticiples.filter(p => p !== correctAnswer);

// 随机选择3个错误答案
const shuffledWrong = wrongAnswers.sort(() =>0.5 - Math.random()).slice(0, 3);
const options = [correctAnswer, ...shuffledWrong].sort(() =>0.5 - Math.random());

// 更新UI
document.getElementById('verbInfinitive').textContent = currentCard.infinitive;
document.getElementById('cardInfo').textContent = `${currentCard.infinitive} 的过去分词是?`;

// 生成选项HTML
const optionsContainer = document.getElementById('quizOptions');
    optionsContainer.innerHTML = '';

    options.forEach(option => {
const optionElement = document.createElement('div');
        optionElement.className = 'option';
        optionElement.textContent = option;
        optionElement.onclick = () => selectOption(option, correctAnswer, optionElement);
        optionsContainer.appendChild(optionElement);
    });
}

functionselectOption(selected, correct, element) {
    totalAttempts++;

// 禁用所有选项
const allOptions = document.querySelectorAll('.option');
    allOptions.forEach(opt => opt.style.pointerEvents = 'none');

if (selected === correct) {
        element.classList.add('correct');
        correctAnswers++;
    } else {
        element.classList.add('incorrect');
// 高亮正确答案
        allOptions.forEach(opt => {
if (opt.textContent === correct) {
                opt.classList.add('correct');
            }
        });
    }

    updateStats();

// 3秒后自动下一题
    setTimeout(() => {
if (currentIndex < currentCards.length - 1) {
            nextCard();
        } else {
            showTestResult();
        }
    }, 3000);
}

选项样式:

.options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
max-width: 500px;
margin: 20px auto;
}

.option {
padding: 15px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 10px;
background: rgba(255,255,255,0.1);
color: white;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
font-weight: bold;
}

.option:hover {
background: rgba(255,255,255,0.2);
border-color: rgba(255,255,255,0.6);
}

.option.correct {
background: #00b894;
border-color: #00b894;
animation: correctPulse 0.5s ease;
}

.option.incorrect {
background: #e17055;
border-color: #e17055;
animation: shake 0.5s ease;
}

@keyframes correctPulse {
    0% { transform: scale(1); }
    50% { transform: scale(1.05); }
    100% { transform: scale(1); }
}

@keyframes shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-5px); }
    75% { transform: translateX(5px); }
}

女朋友做了几道题后说:"这个测试很有意思!但是我发现冠词的练习还没有,能不能也加上?"

好家伙,需求还在继续...

第四步:冠词填空练习 - 语法规则的实战应用

练习数据设计

冠词练习比动词练习复杂一些,因为需要考虑语境,当然这部分大多是借助的Ai,因为我不太懂这些专业词汇

const articleExercises = [
    {
before: "Je vais à",
after: "cinéma ce soir.",
correct: "au",
explanation: "au = à + le (缩合形式,阳性单数)",
translation: "我今晚去电影院。",
difficulty: 'easy'
    },
    {
before: "Elle parle de",
after: "enfants dans le parc.",
correct: "des", 
explanation: "des = de + les (缩合形式,复数)",
translation: "她谈论公园里的孩子们。",
difficulty: 'easy'
    },
    {
before: "Il boit",
after: "café tous les matins.",
correct: "du",
explanation: "du (部分冠词,阳性,不可数名词)",
translation: "他每天早上都喝咖啡。",
difficulty: 'medium'
    },
// ... 更多练习
];

交互式填空实现

<divclass="article-question">
<h3>请选择正确的冠词</h3>
<divclass="sentence-container">
<spanid="sentenceBefore"></span>
<selectid="articleSelect"onchange="checkArticle()">
<optionvalue="">选择冠词</option>
<optionvalue="le">le</option>
<optionvalue="la">la</option>
<optionvalue="les">les</option>
<optionvalue="l'">l'</option>
<optionvalue="un">un</option>
<optionvalue="une">une</option>
<optionvalue="des">des</option>
<optionvalue="du">du</option>
<optionvalue="de la">de la</option>
<optionvalue="de l'">de l'</option>
<optionvalue="au">au</option>
<optionvalue="aux">aux</option>
</select>
<spanid="sentenceAfter"></span>
</div>
<divclass="article-feedback"id="articleFeedback"></div>
<divclass="article-translation"id="articleTranslation"></div>
<divclass="article-explanation"id="articleExplanation"></div>
</div>

检查答案的逻辑:

functioncheckArticle() {
const selectedArticle = document.getElementById('articleSelect').value;
const exercise = currentCards[currentIndex];
const feedback = document.getElementById('articleFeedback');
const explanation = document.getElementById('articleExplanation');

if (!selectedArticle) return;

    totalAttempts++;

if (selectedArticle === exercise.correct) {
        feedback.textContent = "✅ 正确!";
        feedback.className = "article-feedback correct";
        correctAnswers++;
    } else {
        feedback.textContent = `❌ 错误!正确答案是: ${exercise.correct}`;
        feedback.className = "article-feedback incorrect";
    }

// 显示解释和翻译
    explanation.textContent = exercise.explanation;
    explanation.classList.add('show');

const translation = document.getElementById('articleTranslation');
    translation.textContent = `中文:${exercise.translation}`;

    updateStats();

// 3秒后自动下一题
    setTimeout(() => {
if (currentIndex < currentCards.length - 1) {
            nextCard();
        } else {
            showCompletionMessage();
        }
    }, 3000);
}

女朋友练习了一会儿说:"太棒了,你这个程序里面有很多刚好也是我老师之前讲到的内容,有一个程序员男朋友太棒了!"

HAH, 受了一番表扬,我心里很舒服,特别开心,都要成翘嘴了,这谁听了谁不迷糊啊,于是我又主动给她优化了一下这个工具,增加了朗读功能

第五步:语音朗读功能 - 让学习更生动

Web Speech API的使用

现代浏览器都支持Web Speech API,正好可以用来实现朗读功能:

// 语音相关变量
let speechSynthesis = window.speechSynthesis;
let voices = [];
let selectedVoice = null;
let speechRate = 0.8;
let isAutoSpeechEnabled = true; // 默认开启
let currentSpeech = null;

// 加载可用语音
functionloadVoices() {
    voices = speechSynthesis.getVoices();
const voiceSelect = document.getElementById('voiceSelect');
    voiceSelect.innerHTML = '<option value="">选择语音</option>';

// 优先显示法语语音
const frenchVoices = voices.filter(voice =>
        voice.lang.startsWith('fr') || 
        voice.name.toLowerCase().includes('french') ||
        voice.name.toLowerCase().includes('français')
    );

    frenchVoices.forEach((voice, index) => {
const option = document.createElement('option');
        option.value = index;
        option.textContent = `${voice.name} (${voice.lang})`;
        voiceSelect.appendChild(option);
    });

// 默认选择第一个法语语音
if (frenchVoices.length > 0) {
        voices = frenchVoices;
        selectedVoice = voices[0];
        voiceSelect.value = 0;
    }
}

// 朗读文本
functionspeakText(event, side) {
    event.stopPropagation();

if (!selectedVoice) {
        alert('请先选择一个语音!');
return;
    }

// 停止当前播放
if (currentSpeech) {
        speechSynthesis.cancel();
    }

let textToSpeak = '';
const currentCard = currentCards[currentIndex];

if (currentMode === 'articles') {
// 冠词模式:朗读完整句子
const exercise = currentCards[currentIndex];
const selectedArticle = document.getElementById('articleSelect').value || '[冠词]';
        textToSpeak = `${exercise.before}${selectedArticle}${exercise.after}`;
    } else {
// 动词模式
if (side === 'front') {
            textToSpeak = currentCard.infinitive;
        } else {
            textToSpeak = `${currentCard.participle}. ${currentCard.example}`;
        }
    }

    currentSpeech = new SpeechSynthesisUtterance(textToSpeak);
    currentSpeech.voice = selectedVoice;
    currentSpeech.rate = speechRate;
    currentSpeech.lang = 'fr-FR';

// 添加视觉反馈
const speechBtn = event.target;
    speechBtn.classList.add('speaking');

    currentSpeech.onend = () => {
        speechBtn.classList.remove('speaking');
        currentSpeech = null;
    };

    speechSynthesis.speak(currentSpeech);
}

结语

这个法语学习工具的开发过程,让我重新思考了技术的意义。 为什么我会想到用程序去解决女朋友的烦恼,第一点当然是我不会法语,哈哈,第二点是我可能具备一些将需求抽象化的思想,(撒一波狗粮)当然最主要的还是我想让她开心一些,不要有那么多烦恼,不就一门考试嘛,就算没过的话可以补考。

「技术不是为了炫技,而是为了解决实际问题。」

对象已经拿着我写工具在给好闺蜜炫耀了,我的脸上也是有了藏不住的笑。我想说一下,作为程序员,我们有能力用代码改变身边人的生活,哪怕只是一个小小的学习工具,也能带来实实在在的帮助。

有对象没对象的朋友们,都快来开动自己的脑筋,给Ta去写一个小工具,说不定你也一样可以俘获另一半的心。

奥利给!

恩恩……女朋友驱动开发,真香!

AI编程资讯AI Coding专区指南:

https://aicoding.juejin.cn/aicoding

图片
点击"阅读原文"了解详情~