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

前言

大家好,我是奈德丽。

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

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

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

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

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

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

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

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

初版实现:静态页面

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

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>法语语法总结</title>
</head>
<body>
    <div class="container">
        <h1>🇫🇷 法语语法总结</h1>
        
        <!-- 冠词部分 -->
        <section class="grammar-section">
            <h2>冠词 (Les Articles)</h2>
            <div class="rule-card">
                <h3>定冠词</h3>
                <div class="examples">
                    <span class="article masculine">le</span> livre (阳性单数)
                    <span class="article feminine">la</span> table (阴性单数)  
                    <span class="article plural">les</span> livres (复数)
                </div>
            </div>
        </section>
        
        <!-- 过去时复合部分 -->
        <section class="grammar-section">
            <h2>过去时复合 (Le Passé Composé)</h2>
            <div class="rule-card">
                <h3>构成公式</h3>
                <div class="formula">
                    助动词(avoir/être) + 过去分词
                </div>
            </div>
        </section>
    </div>
</body>
</html>

配上一些基础样式:

css 复制代码
.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: 0 10px 30px rgba(0,0,0,0.1);
    transition: transform 0.3s ease;
}

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

.article {
    padding: 4px 12px;
    border-radius: 20px;
    font-weight: bold;
    margin: 0 5px;
}

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

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

好吧,需求升级了。

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

核心数据结构设计

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

javascript 复制代码
// 不规则动词数据
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翻转卡片实现

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

css 复制代码
.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: 0 10px 30px rgba(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控制逻辑:

javascript 复制代码
class VerbCardManager {
    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();
        }
    }
}

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

需求又升级了...

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

测试题生成算法

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

javascript 复制代码
function generateQuizOptions() {
    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);
    });
}

function selectOption(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);
}

选项样式:

css 复制代码
.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,因为我不太懂这些专业词汇

javascript 复制代码
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'
    },
    // ... 更多练习
];

交互式填空实现

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

检查答案的逻辑:

javascript 复制代码
function checkArticle() {
    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,正好可以用来实现朗读功能:

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

// 加载可用语音
function loadVoices() {
    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;
    }
}

// 朗读文本
function speakText(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去写一个小工具,说不定你也一样可以俘获另一半的心。

奥利给!

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

相关推荐
我的心巴6 分钟前
Vue2 ElementUI Tree 拖动目标节点能否被放置及获取放置位置
前端·vue.js·elementui
ze_juejin18 分钟前
Subject、BehaviorSubject、ReplaySubject、AsyncSubject、VoidSubject比较
前端·angular.js
每天吃饭的羊23 分钟前
面试-TypeScript 场景类面试题
前端
CRMEB定制开发35 分钟前
CRMEB 注释规范:多端适配下的代码可读性提升之道
前端
中雨202536 分钟前
HarmonyOS Next快速入门:TextInput组件
前端
白晓明39 分钟前
HarmonyOS NEXT端云一体化云侧云函数介绍和开发
前端·harmonyos
白晓明1 小时前
HarmonyOS NEXT端侧工程调用云函数能力实现业务功能
前端·harmonyos
锋利的绵羊1 小时前
【小程序】迁移非主包组件以减少主包体积
前端·微信小程序·uni-app
jqq6661 小时前
解析ElementPlus打包源码
前端·javascript·vue.js