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

前言

大家好,我是奈德丽。

今天女朋友突然跟我说:"我下周就要法语考试了,但是完全不知道该怎么复习,冠词和过去时复合搞得我头都大了..." 女朋友让我帮她背法语,这我一个工科男怎么会法语啊,太高估我了吧,但记住咱是程序员呀,秉着程序为人而服务的理念,我想着能不能给她做个小工具,帮她来复习呢,现在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去写一个小工具,说不定你也一样可以俘获另一半的心。

奥利给!

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

相关推荐
小月鸭7 分钟前
如何理解HTML语义化
前端·html
jump68030 分钟前
url输入到网页展示会发生什么?
前端
诸葛韩信34 分钟前
我们需要了解的Web Workers
前端
brzhang39 分钟前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋1 小时前
场景模拟:基础路由配置
前端
六月的可乐1 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程
一 乐2 小时前
智慧党建|党务学习|基于SprinBoot+vue的智慧党建学习平台(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习
BBB努力学习程序设计2 小时前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html