程序员自己开发的法语学习工具,帮我收获了爱情
(金石瓜分计划强势上线,速戳上图了解详情)前言大家好,我是奈德丽。
今天女朋友突然跟我说:"我下周就要法语考试了,但是完全不知道该怎么复习,冠词和过去时复合搞得我头都大了..." 女朋友让我帮她背法语,这我一个工科男怎么会法语啊,太高估我了吧,但记住咱是程序员呀,秉着程序为人而服务的理念,我想着能不能给她做个小工具,帮她来复习呢,现在Ai这么强大,刚好年费的Cursor不能闲着,让它来帮我辅助开发了一款工具,让女朋友学起来就跟玩游戏一样,虽然会有点耗费时间,但是最终也是如愿以偿,得到了不错的效果。
先来看下受到女朋友连连好评的程序长什么样吧!
那就简单做一下需求分析吧女朋友向我抱怨了一堆话,总结起来就是:
「语法规则记不住」- 法语冠词有定冠词、不定冠词、部分冠词,各种变化规则「动词变位太复杂」- 不规则动词的过去分词总是记混「缺乏练习工具」- 课本上的练习题做完就没了,想多练都没地方「学习效率低」- 翻书查资料浪费时间,没有系统性哪有那么难,说到底还是上课没好好听! [狗头保命 !!]
那么怎么去实现这样的工具呢?我把它分成了五步
第一步:语法知识库 - 把复杂的规则可视化初版实现:静态页面我先从最基础的开始,把她需要的语法知识整理成一个结构化的页面:
!DOCTYPEhtml
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/spanlivre (阳性单数)
spanclass="article feminine"la/spantable (阴性单数)
spanclass="article plural"les/spanlivres (复数)
/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:0auto;
padding:20px;
}
.rule-card{
background:linear-gradient(135deg, #667eea0%, #764ba2100%);
border-radius:15px;
padding:25px;
margin-bottom:20px;
color: white;
box-shadow:010px30pxrgba(0,0,0,0.1);
transition: transform0.3sease;
}
.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; }
女朋友看了第一版说:"哇,这个比课本清楚多了!但是能不能加点练习?光看不练还是记不住。"
好吧,需求升级了。
第二步:动词卡片系统 - 让学习变成游戏核心数据结构设计既然要做练习,那就得先把数据结构设计好:
// 不规则动词数据
constirregularVerbs = [
{
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作助动词的动词)
constetreVerbs = [
{
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: transform0.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() {
constcard =document.getElementById('flashcard');
card.classList.toggle('flipped');
this.isFlipped = !this.isFlipped;
}
// 更新卡片内容
updateCard() {
constcard =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(){
constcurrentCard = currentCards[currentIndex];
constcorrectAnswer = currentCard.participle;
// 从所有动词中筛选出错误选项
constallParticiples = currentCards.map(card=card.participle);
constwrongAnswers = allParticiples.filter(p=p !== correctAnswer);
// 随机选择3个错误答案
constshuffledWrong = wrongAnswers.sort(()=0.5-Math.random()).slice(0,3);
constoptions = [correctAnswer, ...shuffledWrong].sort(()=0.5-Math.random());
// 更新UI
document.getElementById('verbInfinitive').textContent = currentCard.infinitive;
document.getElementById('cardInfo').textContent =`${currentCard.infinitive}的过去分词是?`;
// 生成选项HTML
constoptionsContainer =document.getElementById('quizOptions');
optionsContainer.innerHTML ='';
options.forEach(option={
constoptionElement =document.createElement('div');
optionElement.className ='option';
optionElement.textContent = option;
optionElement.onclick =()=selectOption(option, correctAnswer, optionElement);
optionsContainer.appendChild(optionElement);
}
functionselectOption(selected, correct, element){
totalAttempts++;
// 禁用所有选项
constallOptions =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:1fr1fr;
gap:15px;
max-width:500px;
margin:20pxauto;
}
.option{
padding:15px;
border:2pxsolidrgba(255,255,255,0.3);
border-radius:10px;
background:rgba(255,255,255,0.1);
color: white;
cursor: pointer;
transition: all0.3sease;
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: correctPulse0.5sease;
}
.option.incorrect{
background:#e17055;
border-color:#e17055;
animation: shake0.5sease;
}
@keyframescorrectPulse {
0% {transform:scale(1); }
50% {transform:scale(1.05); }
100% {transform:scale(1); }
}
@keyframesshake {
0%, 100% {transform:translateX(0); }
25% {transform:translateX(-5px); }
75% {transform:translateX(5px); }
}
女朋友做了几道题后说:"这个测试很有意思!但是我发现冠词的练习还没有,能不能也加上?"
好家伙,需求还在继续...
第四步:冠词填空练习 - 语法规则的实战应用练习数据设计冠词练习比动词练习复杂一些,因为需要考虑语境,当然这部分大多是借助的Ai,因为我不太懂这些专业词汇
constarticleExercises = [
{
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(){
constselectedArticle =document.getElementById('articleSelect').value;
constexercise = currentCards[currentIndex];
constfeedback =document.getElementById('articleFeedback');
constexplanation =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');
consttranslation =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,正好可以用来实现朗读功能:
// 语音相关变量
letspeechSynthesis =window.speechSynthesis;
letvoices = [];
letselectedVoice =null;
letspeechRate =0.8;
letisAutoSpeechEnabled =true;// 默认开启
letcurrentSpeech =null;
// 加载可用语音
functionloadVoices(){
voices = speechSynthesis.getVoices();
constvoiceSelect =document.getElementById('voiceSelect');
voiceSelect.innerHTML ='option value=""选择语音/option';
// 优先显示法语语音
constfrenchVoices = voices.filter(voice=
voice.lang.startsWith('fr') ||
voice.name.toLowerCase().includes('french') ||
voice.name.toLowerCase().includes('fran?ais')
frenchVoices.forEach((voice, index) ={
constoption =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();
}
lettextToSpeak ='';
constcurrentCard = currentCards[currentIndex];
if(currentMode ==='articles') {
// 冠词模式:朗读完整句子
constexercise = currentCards[currentIndex];
constselectedArticle =document.getElementById('articleSelect').value ||'[冠词]';
textToSpeak =`${exercise.before}${selectedArticle}${exercise.after}`;
}else{
// 动词模式
if(side ==='front') {
textToSpeak = currentCard.infinitive;
}else{
textToSpeak =`${currentCard.participle}.${currentCard.example}`;
}
}
currentSpeech =newSpeechSynthesisUtterance(textToSpeak);
currentSpeech.voice = selectedVoice;
currentSpeech.rate = speechRate;
currentSpeech.lang ='fr-FR';
// 添加视觉反馈
constspeechBtn = 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
点击"阅读原文"了解详情~
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线