From 0d5b0919dac6c7b1e854208d42189fbbe1602d10 Mon Sep 17 00:00:00 2001 From: guoqingshan Date: Fri, 31 Jul 2026 14:50:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A5=BF=E7=8F=AD=E7=89=99=E8=AF=AD?= =?UTF-8?q?=E6=97=A5=E8=AE=B0=E5=AD=A6=E4=B9=A0=E5=B7=A5=E5=85=B7=E4=B8=8E?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 ++++ app.js | 329 ++++++++++++++++++++++++++++++++++++++ db.js | 87 ++++++++++ index.html | 100 ++++++++++++ knowledge.html | 63 ++++++++ knowledge/adjetivo.html | 93 +++++++++++ knowledge/fonetica.html | 189 ++++++++++++++++++++++ knowledge/otros.html | 211 ++++++++++++++++++++++++ knowledge/pronombre.html | 108 +++++++++++++ knowledge/style.css | 204 +++++++++++++++++++++++ knowledge/sustantivo.html | 99 ++++++++++++ knowledge/verbo.html | 165 +++++++++++++++++++ speech.js | 33 ++++ style.css | 228 ++++++++++++++++++++++++++ translate.js | 199 +++++++++++++++++++++++ 15 files changed, 2140 insertions(+) create mode 100644 README.md create mode 100644 app.js create mode 100644 db.js create mode 100644 index.html create mode 100644 knowledge.html create mode 100644 knowledge/adjetivo.html create mode 100644 knowledge/fonetica.html create mode 100644 knowledge/otros.html create mode 100644 knowledge/pronombre.html create mode 100644 knowledge/style.css create mode 100644 knowledge/sustantivo.html create mode 100644 knowledge/verbo.html create mode 100644 speech.js create mode 100644 style.css create mode 100644 translate.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..c31d1ef --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Mi Diario — 西班牙语日记学习工具 + +一个纯前端的西班牙语学习应用,基于外研社《现代西班牙语1》整理知识库。 + +## 功能 + +- 📝 西班牙语日记:新建、编辑、删除、按日期管理 +- 🌐 实时翻译:输入西语自动显示英语和中文对照 +- 🔊 发音朗读:支持西语、英语、中文三种语言(Web Speech API) +- 📖 单词本:自动提取输入中的关键词并给出解释,支持点击发音 +- 📚 语法知识库:按教材整理的 40 个语音和语法知识点 + +## 使用 + +直接打开 `index.html` 即可使用(推荐 Edge / Chrome)。 + +## 文件结构 + +``` +index.html — 主页面 +style.css — 主页面样式 +app.js — 应用逻辑 +db.js — IndexedDB 数据存储 +translate.js — 翻译模块 +speech.js — 发音模块 +knowledge.html — 知识库首页 +knowledge/ — 6 个分类知识页面 +``` + +## 数据存储 + +阶段 1 使用浏览器 IndexedDB 本地存储,无需后端。后续计划迁移到 Python + SQLite。 diff --git a/app.js b/app.js new file mode 100644 index 0000000..9966cbf --- /dev/null +++ b/app.js @@ -0,0 +1,329 @@ +// Alias db.js functions +const dbGetAllEntries = getAllEntries; +const dbGetEntry = getEntry; +const dbAddEntry = addEntry; +const dbUpdateEntry = updateEntry; +const dbDeleteEntry = deleteEntry; + +var savedSelStart = 0; +var savedSelEnd = 0; + +let state = { + entries: [], + currentId: null +}; + +function uuid() { + return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); +} + +function todayStr() { + const d = new Date(); + return d.getFullYear() + '-' + + String(d.getMonth() + 1).padStart(2, '0') + '-' + + String(d.getDate()).padStart(2, '0'); +} + +// ----- Render functions ----- + +function renderList() { + const ul = document.getElementById('entry-list'); + ul.innerHTML = state.entries.map(e => { + const preview = e.spanish.slice(0, 30) + (e.spanish.length > 30 ? '...' : ''); + const active = e.id === state.currentId ? 'active' : ''; + return '
  • ' + + '' + + '
    ' + escHtml(preview) + '
    ' + + '
    ' + + '' + + '
    ' + + '
  • '; + }).join(''); +} + +function escHtml(s) { + const div = document.createElement('div'); + div.textContent = s; + return div.innerHTML; +} + +function renderTranslate(entry) { + document.getElementById('translate-en').textContent = entry ? entry.english : '-'; + document.getElementById('translate-cn').textContent = entry ? entry.chinese : '-'; +} + +function setEditor(entry) { + document.getElementById('entry-date').value = entry ? entry.date : todayStr(); + document.getElementById('entry-text').value = entry ? entry.spanish : ''; + const hint = document.getElementById('entry-hint'); + hint.textContent = entry && entry.spanish + ? '已保存。修改后点击保存更新。' + : '输入西语后实时显示翻译'; + updateLiveTranslation(); +} + +function clearEditor() { + state.currentId = null; + document.querySelectorAll('#entry-list li').forEach(function(el) { el.classList.remove('active'); }); + setEditor(null); + renderTranslate(null); + clearLiveTranslation(); +} + +// ----- Live translation & vocabulary ----- + +function updateLiveTranslation() { + var text = document.getElementById('entry-text').value; + if (!text.trim()) { + clearLiveTranslation(); + return; + } + var result = translateText(text); + document.getElementById('editor-translate-en').textContent = result.english; + document.getElementById('editor-translate-cn').textContent = result.chinese; + + updateVocabulary(text); +} + +function clearLiveTranslation() { + document.getElementById('editor-translate-en').textContent = '-'; + document.getElementById('editor-translate-cn').textContent = '-'; + document.getElementById('vocab-list').innerHTML = ''; + document.getElementById('selection-translate').style.display = 'none'; +} + +function updateVocabulary(text) { + var vocab = getVocabulary(text); + var container = document.getElementById('vocab-list'); + if (vocab.length === 0) { + container.innerHTML = '
    输入西语后显示单词
    '; + return; + } + container.innerHTML = vocab.map(function(v) { + return '
    ' + + '' + escHtml(v.word) + '' + + '' + + '' + escHtml(v.english) + '' + + '' + + '' + escHtml(v.chinese) + '' + + '' + + '
    '; + }).join(''); +} + +function handleVocabSpeech(word, lang) { + var entry = getVocabForWord(word); + if (entry) { + if (lang === 'en') speakText(entry.en, 'en'); + else if (lang === 'zh') speakText(entry.zh, 'zh'); + else speakText(word, 'es'); + } +} + +function handleSelection() { + var ta = document.getElementById('entry-text'); + var selText = ta.value.substring(ta.selectionStart, ta.selectionEnd).trim(); + var el = document.getElementById('selection-translate'); + if (selText && selText.length > 0) { + var result = translateSelection(selText); + document.getElementById('selected-translation').textContent = + result.english + ' / ' + result.chinese; + el.style.display = 'block'; + } else { + el.style.display = 'none'; + } +} + +// ----- Event handlers ----- + +function selectEntry(id) { + state.currentId = id; + dbGetEntry(id).then(function(entry) { + if (entry) { + setEditor(entry); + renderTranslate(entry); + renderList(); + } + }); +} + +function saveCurrentEntry() { + var date = document.getElementById('entry-date').value; + var spanish = document.getElementById('entry-text').value.trim(); + if (!spanish) { alert('请输入西语内容'); return; } + + var translation = translateText(spanish); + + var savePromise; + if (state.currentId) { + savePromise = dbGetEntry(state.currentId).then(function(existing) { + existing.date = date; + existing.spanish = spanish; + existing.english = translation.english; + existing.chinese = translation.chinese; + existing.updatedAt = new Date().toISOString(); + return dbUpdateEntry(existing); + }); + } else { + var entry = { + id: uuid(), + date: date, + spanish: spanish, + english: translation.english, + chinese: translation.chinese, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + savePromise = dbAddEntry(entry).then(function() { + state.currentId = entry.id; + }); + } + + savePromise.then(function() { + return dbGetAllEntries(); + }).then(function(entries) { + state.entries = entries; + renderList(); + renderTranslate({ english: translation.english, chinese: translation.chinese }); + var found = entries.find(function(e) { return e.id === state.currentId; }); + if (found) setEditor(found); + document.getElementById('btn-save').textContent = '\uD83D\uDCBE 已保存 \u2713'; + setTimeout(function() { document.getElementById('btn-save').textContent = '\uD83D\uDCBE 保存'; }, 1500); + }); +} + +function newEntry() { + clearEditor(); + document.getElementById('entry-date').value = todayStr(); + document.getElementById('entry-text').focus(); +} + +// ----- Modal ----- + +var pendingDeleteId = null; + +function showDeleteModal(id) { + pendingDeleteId = id; + document.getElementById('modal-overlay').classList.add('show'); +} + +function hideDeleteModal() { + pendingDeleteId = null; + document.getElementById('modal-overlay').classList.remove('show'); +} + +function confirmDelete(id) { + dbDeleteEntry(id).then(function() { + if (state.currentId === id) clearEditor(); + return dbGetAllEntries(); + }).then(function(entries) { + state.entries = entries; + renderList(); + hideDeleteModal(); + }); +} + +function saveTextareaSelection() { + var ta = document.getElementById('entry-text'); + savedSelStart = ta.selectionStart; + savedSelEnd = ta.selectionEnd; +} + +// ----- Speech handler ----- + +function handleSpeech(lang) { + var text = lang === 'en' + ? document.getElementById('translate-en').textContent + : document.getElementById('translate-cn').textContent; + if (text && text !== '-') speakText(text, lang); +} + +// ----- Initialization ----- + +function init() { + dbGetAllEntries().then(function(entries) { + state.entries = entries; + renderList(); + setEditor(null); + renderTranslate(null); + }); + + document.getElementById('entry-list').addEventListener('click', function(e) { + var li = e.target.closest('li[data-id]'); + if (!li) return; + if (e.target.closest('.btn-delete')) { + showDeleteModal(li.dataset.id); + return; + } + selectEntry(li.dataset.id); + }); + + document.getElementById('btn-new').addEventListener('click', newEntry); + document.getElementById('btn-save').addEventListener('click', saveCurrentEntry); + + document.querySelectorAll('.btn-speech').forEach(function(btn) { + btn.addEventListener('click', function() { handleSpeech(btn.dataset.lang); }); + }); + + // Live translation on input + document.getElementById('entry-text').addEventListener('input', updateLiveTranslation); + + // Selection translation + document.getElementById('entry-text').addEventListener('mouseup', function(e) { handleSelection(); saveTextareaSelection(); }); + document.getElementById('entry-text').addEventListener('keyup', function(e) { handleSelection(); saveTextareaSelection(); }); + + // Vocabulary speech delegation + document.getElementById('editor-vocab').addEventListener('click', function(e) { + var btn = e.target.closest('.vocab-speech'); + if (btn) handleVocabSpeech(btn.dataset.word, btn.dataset.lang); + }); + + // Vocabulary double-click to auto-read + document.getElementById('editor-vocab').addEventListener('dblclick', function(e) { + var item = e.target.closest('.vocab-item'); + if (!item) return; + var word = item.querySelector('.vocab-word').textContent; + var el = e.target; + if (el.classList.contains('vocab-en') || el.closest('.vocab-en')) { + speakText(item.querySelector('.vocab-en').textContent, 'en'); + } else if (el.classList.contains('vocab-zh') || el.closest('.vocab-zh')) { + speakText(item.querySelector('.vocab-zh').textContent, 'zh'); + } else { + speakText(word, 'es'); + } + }); + + // Textarea double-click auto-read + document.getElementById('entry-text').addEventListener('dblclick', function() { + var ta = document.getElementById('entry-text'); + setTimeout(function() { + var txt = ta.value.substring(ta.selectionStart, ta.selectionEnd).trim(); + if (txt) speakText(txt, 'es'); + }, 50); + }); + + // Speak selected button - reads selection aloud + document.getElementById('btn-speak-selected').addEventListener('click', function() { + var ta = document.getElementById('entry-text'); + var start = savedSelStart || ta.selectionStart; + var end = savedSelEnd || ta.selectionEnd; + if (start === end) { start = 0; end = ta.value.length; } + var sel = ta.value.substring(start, end).trim(); + if (sel) { + speakText(sel, 'es'); + document.getElementById('selected-translation').textContent = sel; + document.getElementById('selection-translate').style.display = 'block'; + } + }); + + document.querySelector('#modal-box .btn-cancel').addEventListener('click', hideDeleteModal); + document.querySelector('#modal-box .btn-confirm').addEventListener('click', function() { + if (pendingDeleteId) confirmDelete(pendingDeleteId); + }); + + document.addEventListener('keydown', function(e) { + if (e.ctrlKey && e.key === 'Enter') saveCurrentEntry(); + }); +} + +document.addEventListener('DOMContentLoaded', init); diff --git a/db.js b/db.js new file mode 100644 index 0000000..f0e2872 --- /dev/null +++ b/db.js @@ -0,0 +1,87 @@ +const DB_NAME = 'SpanishDiary'; +const DB_VERSION = 1; +const STORE_NAME = 'entries'; + +let db = null; + +function openDb() { + return new Promise((resolve, reject) => { + if (db) { resolve(db); return; } + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onupgradeneeded = (e) => { + const d = e.target.result; + if (!d.objectStoreNames.contains(STORE_NAME)) { + const store = d.createObjectStore(STORE_NAME, { keyPath: 'id' }); + store.createIndex('date', 'date', { unique: false }); + } + }; + req.onsuccess = (e) => { db = e.target.result; resolve(db); }; + req.onerror = (e) => reject(e.target.error); + }); +} + +function getAllEntries() { + return openDb().then(db => { + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readonly'); + const store = tx.objectStore(STORE_NAME); + const index = store.index('date'); + const req = index.openCursor(null, 'prev'); + const results = []; + req.onsuccess = (e) => { + const cursor = e.target.result; + if (cursor) { results.push(cursor.value); cursor.continue(); } + else resolve(results); + }; + req.onerror = (e) => reject(e.target.error); + }); + }); +} + +function getEntry(id) { + return openDb().then(db => { + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readonly'); + const store = tx.objectStore(STORE_NAME); + const req = store.get(id); + req.onsuccess = () => resolve(req.result || null); + req.onerror = (e) => reject(e.target.error); + }); + }); +} + +function addEntry(entry) { + return openDb().then(db => { + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const req = store.add(entry); + req.onsuccess = () => resolve(entry); + req.onerror = (e) => reject(e.target.error); + }); + }); +} + +function updateEntry(entry) { + return openDb().then(db => { + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const req = store.put(entry); + req.onsuccess = () => resolve(entry); + req.onerror = (e) => reject(e.target.error); + }); + }); +} + +function deleteEntry(id) { + return openDb().then(db => { + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const req = store.delete(id); + req.onsuccess = () => resolve(); + req.onerror = (e) => reject(e.target.error); + }); + }); +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..59f850f --- /dev/null +++ b/index.html @@ -0,0 +1,100 @@ + + + + + + Mi Diario — 西班牙语日记 + + + +
    + + +
    +
    + + + +
    + +
    输入西语后实时显示翻译
    + +
    +
    🌐 实时翻译
    +
    + EN: + - +
    +
    + CN: + - +
    + +
    + +
    +
    📖 单词本
    +
    +
    +
    + + +
    + + + + + + + + + + diff --git a/knowledge.html b/knowledge.html new file mode 100644 index 0000000..0ad71db --- /dev/null +++ b/knowledge.html @@ -0,0 +1,63 @@ + + + + + + 西班牙语知识库 — 现代西班牙语1 + + + + + +

    现代西班牙语 1 知识库

    +

    按外研社教材整理,语音 + 语法共 40 个知识点,支持点击发音。

    + +
    + +

    🔊 语音发音

    +

    字母表、元音、辅音、音节与重音

    + 8 个知识点 +
    + +

    📦 名词与冠词

    +

    名词的性数、定冠词、不定冠词

    + 4 个知识点 +
    + +

    🎨 形容词

    +

    形容词变化、位置、指示与物主形容词

    + 4 个知识点 +
    + +

    👤 代词

    +

    人称、宾格、与格、指示、疑问代词

    + 5 个知识点 +
    + +

    ⚡ 动词与时态

    +

    规则变位、现在时、完成时、过去时

    + 7 个知识点 +
    + +

    🧩 词类与句型

    +

    副词、介词、数词、常用句型

    + 12 个知识点 +
    +
    + + + + + diff --git a/knowledge/adjetivo.html b/knowledge/adjetivo.html new file mode 100644 index 0000000..642e48e --- /dev/null +++ b/knowledge/adjetivo.html @@ -0,0 +1,93 @@ + + + + + + 形容词 — 西班牙语知识库 + + + + + +

    形容词

    +

    《现代西班牙语1》形容词用法,共 4 个知识点。

    + +
    +

    1. 形容词的性与数

    +
    Género y número del adjetivo
    +

    形容词必须与所修饰名词在性和数上保持一致。

    + + + + + + +
    规则阳性阴性复数
    以 -o 结尾:变 -a 为阴性,加 -sbuenobuenabuenos / buenas
    以 -e 结尾:单复数同形,加 -sgrandegrandegrandes
    以辅音结尾:复数加 -esfelizfelizfelices
    以 -dor/-or 结尾:阴性加 -atrabajadortrabajadoratrabajadores
    +
    un libro bueno, una amiga buena, unos libros buenos一本好书,一位好朋友,一些好书
    +
    易错提醒:形容词的性数要与名词一致,尤其是复数中"阳性复数 + 阴性名词"时各自变。
    +
    + +
    +

    2. 形容词的位置与短尾形式

    +
    Posición del adjetivo y apócope
    +

    一般位置:西班牙语形容词通常放在名词之后,强调客观属性。

    +
    una casa blanca, un libro interesante一座白房子,一本有趣的书
    +

    前置情况:数量词、限定词和一些常见形容词置于名词前,且发生短尾。

    + + + + + + + + +
    短尾形式原形示例
    ununoun libro
    buenbuenoun buen amigo
    malmaloun mal día
    grangrandeun gran hombre
    primer / tercerprimero / terceroel primer libro
    algún / ningúnalguno / ningunoalgún día
    +
    易错提醒:bueno 前置短尾为 buen,但后置不变:un hombre bueno。
    +
    + +
    +

    3. 指示形容词

    +
    Los adjetivos demostrativos
    + + + + + +
    距离阳性阴性阳性复数阴性复数
    近(这里)esteestaestosestas
    中(那里)eseesaesosesas
    远(那边)aquelaquellaaquellosaquellas
    +
    este libro, esa casa, aquellos años这本书,那座房子,那些年代
    +
    易错提醒:指示形容词修饰名词时不带重音;指示代词带重音(éste、ése、aquél)。
    +
    + +
    +

    4. 物主形容词

    +
    Los adjetivos posesivos
    +

    非重读物主形容词(置于名词前,最常用):

    + + + + + + + +
    人称单数复数
    mi / mismi libromis libros
    tu / tustu librotus libros
    su / sussu librosus libros
    nuestro/a / nuestros/asnuestro libronuestros libros
    vuestro/a / vuestros/asvuestro librovuestros libros
    +

    重读物主形容词(置于名词后,表强调):mío/mía、tuyo/tuya、suyo/suya、nuestro/a、vuestro/a。

    +
    mi amigo, tus ideas, su casa, nuestros padres我的朋友,你的想法,他/她的家,我们的父母
    +
    易错提醒:su/sus 含义多(他的/她的/您的/他们的),需靠上下文判断;mi/tu/su 单数不随名词的性变化。
    +
    + + + + + diff --git a/knowledge/fonetica.html b/knowledge/fonetica.html new file mode 100644 index 0000000..6d1986f --- /dev/null +++ b/knowledge/fonetica.html @@ -0,0 +1,189 @@ + + + + + + 语音发音 — 西班牙语知识库 + + + + + +

    语音发音

    +

    《现代西班牙语1》语音部分,共 8 个知识点。点击 🔊 可听发音。

    + +
    +

    1. 字母表与发音

    +
    El alfabeto español
    +

    西班牙语字母表共 29 个字母,其中 ch、ll、ñ 为西班牙语特有字母(1994 年西班牙皇家语言学院将 ch、ll 并入字母表,但传统教材仍单独列示)。

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    字母名称示例发音
    A aaamigo
    B bbebueno
    C ccecasa
    Ch chchechico
    D ddedía
    E eeescuela
    F fefefamilia
    G ggegracias
    H hhachehola
    I iiidea
    J jjotajoven
    K kkakilo
    L lelelibro
    Ll llellellamar
    M mememadre
    N nenenoche
    Ñ ñeñeniño
    O ooobra
    P ppepadre
    Q qcuqueso
    R rerrepero
    Rr rrerre dobleperro
    S sesesí
    T ttetiempo
    U uuuniversidad
    V vuvevivir
    W wuve dobleweb
    X xequisexamen
    Y yyeyo
    Z zzetazapato
    +
    易错提醒:H 不发音;ñ 发"ni"的鼻音;rr 为颤音,不能出现在词首,词首 r 直接读颤音。
    +
    + +
    +

    2. 元音发音

    +
    Las vocales
    +

    西班牙语只有 5 个元音:a、e、i、o、u。发音固定,不因拼写变化。

    + + + + + + + +
    元音分类发音提示示例发音
    a强元音类似"啊",口型大casa
    e强元音类似"诶",比英语短促mesa
    i弱元音类似"衣",短促sí
    o强元音类似"哦",圆唇todo
    u弱元音类似"乌",圆唇luna
    +
    易错提醒:西班牙语元音没有长音/短音区分,发音时不要拉长。
    +
    + +
    +

    3. 辅音发音①(b/v、c、d、g)

    +
    Las consonantes (1)
    +

    b / v:发音相同。在停顿或词首读爆破音 [b],位于元音间读摩擦音 [β](双唇轻微摩擦)。

    +
    bien, vivir, haba好,生活,豆子
    + +

    c:在 e/i 前读 [θ](类似英语 think 的 th)或拉美地区读 [s];在 a/o/u 前读 [k]。

    +
    cero, casa, como零,房子,我吃
    + +

    d:词首/停顿后读 [d];元音间读 [ð](舌尖轻触上齿)。

    +
    día, nada, ciudad天,没有什么,城市
    + +

    g:在 e/i 前读 [x](类似"哈");在 a/o/u 前读 [g]。gui/gue 中 u 不发音。

    +
    gente, gato, guía人们,猫,指南
    +
    易错提醒:gue/gui 的 u 不发音;üe/üi(如 pingüino)中 ü 才发音。
    +
    + +
    +

    4. 辅音发音②(h、j、ll、ñ、q、r/rr、z)

    +
    Las consonantes (2)
    +

    h:永远不发音。

    +
    hola, ahora你好,现在
    + +

    j:读 [x],类似中文"哈"但更用力。

    +
    joven, jefe年轻的,老板
    + +

    ll:传统读 [ʎ],多数地区读 [j] 或 [ʒ],类似"衣"或"日"。

    +
    llamar, calle呼叫,街道
    + +

    ñ:读 [ɲ],类似"ni"连读。

    +
    niño, España男孩,西班牙
    + +

    q:只出现在 que/qui 中,u 不发音,读 [k]。

    +
    queso, química奶酪,化学
    + +

    r / rr:r 在词中读单颤 [ɾ];词首、n/l/s 后及 rr 读多颤 [r]。

    +
    pero, perro, río但是,狗,河
    + +

    z:在西班牙读 [θ],拉美多读 [s],与 c+e/i 相同。

    +
    zapato, zorro鞋,狐狸
    +
    易错提醒:r 与 rr 是语义对立音位,pero(但是)与 perro(狗)必须区分。
    +
    + +
    +

    5. 二重元音与三重元音

    +
    Diptongos y triptongos
    +

    二重元音:一个强元音 + 一个弱元音,或两个弱元音组成一个音节。

    + + + + + + + + + + +
    组合示例发音
    ai / ayaire, hay
    ei / eyseis, rey
    oi / oyhoy, soy
    iafamilia
    ietiempo
    iodiario
    ua / ue / uoagua, bueno, cuota
    ui / uymuy, cuidado
    +

    三重元音:弱 + 强 + 弱,如 Uruguay、Paraguay。

    +
    Uruguay, Paraguay乌拉圭,巴拉圭
    +
    易错提醒:两个强元音不能组成二重元音,须分属两个音节,如 cae(ca-e)。
    +
    + +
    +

    6. 分音节规则

    +
    La división silábica
    +
      +
    1. 一个辅音 + 一个元音组成音节:ca-sa, pe-rro
    2. +
    3. 两个元音之间有一个辅音时,辅音跟后:a-mi-go
    4. +
    5. 两个辅音之间一般分属两个音节:es-cue-la, can-tar
    6. +
    7. 辅音连缀 pr、pl、tr、tl、dr、gr、br、cr、fr、fl、gl、cl 不分开:pa-dre, li-bro, cla-se
    8. +
    9. ch、ll、rr 作为单个辅音,不分开:chi-co, ca-lle, pe-rro
    10. +
    11. 二重元音属于同一音节:tie-mpo, cau-sa
    12. +
    13. 两个强元音分属不同音节:le-er, ca-er
    14. +
    +
    es-cue-la, li-bro, tie-mpo学校,书,时间
    +
    + +
    +

    7. 重音规则

    +
    Las reglas de acentuación
    + + + + + +
    情况规则示例
    以元音或 n/s 结尾重音在倒数第二个音节casa, joven, lunes
    以其他辅音结尾重音在最后一个音节ciudad, español, mujer
    不符合上述规则必须加重音符号música, teléfono, lápiz
    +

    重音符号还用于区分同形词:si(如果)与 sí(是),el(定冠词)与 él(他),tu(你的)与 tú(你)。

    +
    música, español, lápiz音乐,西班牙语,铅笔
    +
    易错提醒:疑问词 qué、dónde、cuándo 永远带重音;关系词 que、donde、cuando 不带。
    +
    + +
    +

    8. 连读与语调

    +
    Enlace y entonación
    +

    连读:词尾辅音与下一词首元音连接:los amigos → lo-sa-mi-gos。

    +
    los amigos, un hombre朋友们,一个男人
    +

    陈述句语调:句末下降。

    +
    Hoy estudio español.今天我学习西班牙语。
    +

    一般疑问句语调:句末上升。

    +
    ¿Estudias español?你学习西班牙语吗?
    +
    易错提醒:问句首尾需写倒问号 ¿ 和正问号 ?;选择问句中最后一个选项前语调开始下降。
    +
    + + + + + diff --git a/knowledge/otros.html b/knowledge/otros.html new file mode 100644 index 0000000..eefb83a --- /dev/null +++ b/knowledge/otros.html @@ -0,0 +1,211 @@ + + + + + + 词类与句型 — 西班牙语知识库 + + + + + +

    词类与句型

    +

    《现代西班牙语1》常用词类与句型,共 12 个知识点。

    + +
    +

    1. 常用副词

    +
    Los adverbios comunes
    + + + + + + + + + + +
    副词含义示例
    muy非常(修饰形容词/副词)muy bien(很好)
    mucho很多(修饰动词/名词)Estudio mucho.
    poco少poco a poco(一点一点)
    siempre总是Siempre llego temprano.
    nunca从不Nunca como carne.
    ahora现在Ahora estudio.
    ya已经Ya he terminado.
    todavía仍然Todavía no llego.
    +
    Estudio mucho. Ahora vivo en Madrid. Nunca como tarde.我学习很多。现在我住在马德里。我从不晚吃饭。
    +
    易错提醒:muy 修饰形容词/副词;mucho 修饰动词或作名词/形容词,二者不可混用。
    +
    + +
    +

    2. 常用介词

    +
    Las preposiciones comunes
    + + + + + + + + + + + + +
    介词含义示例
    a到、向Voy a casa.
    de的、从el libro de Juan
    en在(地点/时间)en casa
    con和、用con mis amigos
    sin没有sin azúcar
    para为了、给para ti
    por由于、经过、为了por favor
    sobre关于、在…上面sobre la mesa
    desde自从、从desde enero
    hasta直到hasta mañana
    +
    Voy a casa con mis amigos. El libro está sobre la mesa.我和朋友们回家。书在桌子上。
    +
    + +
    +

    3. 介词冠词缩合

    +
    Las contracciones al y del
    +

    西班牙语只有两组强制缩合:

    + + + + +
    缩合构成示例
    ala + elVoy al parque.(我去公园)
    delde + elel libro del profesor(老师的书)
    +

    其他介词 + el 不缩合:en el parque, para el niño。

    +
    Voy al cine. Salgo del trabajo a las seis.我去电影院。我六点下班。
    +
    易错提醒:a el 和 de el 必须写 al / del;但 de + él(他)不缩合,因 él 是人称代词。
    +
    + +
    +

    4. 基数词

    +
    Los numerales cardinales
    + + + + + + + + + + +
    0 cero1 uno2 dos3 tres
    4 cuatro5 cinco6 seis7 siete
    8 ocho9 nueve10 diez11 once
    12 doce13 trece14 catorce15 quince
    16 dieciséis17 diecisiete18 dieciocho19 diecinueve
    20 veinte21 veintiuno30 treinta40 cuarenta
    50 cincuenta60 sesenta70 setenta80 ochenta
    90 noventa100 cien101 ciento uno200 doscientos
    1000 mil1,000,000 un millón
    +
    Tengo veinte años. Hay cien estudiantes.我二十岁。有一百名学生。
    +
    易错提醒:cien 只用于精确 100 或 100 后接名词;101 以上用 ciento(ciento uno)。
    +
    + +
    +

    5. 序数词

    +
    Los numerales ordinales
    + + + + +
    1º primero2º segundo3º tercero4º cuarto
    5º quinto6º sexto7º séptimo8º octavo
    9º noveno10º décimo11º undécimo12º duodécimo
    +

    序数词有性数变化:la primera lección, los primeros días。

    +
    La primera lección es fácil. Vivo en el tercer piso.第一课很简单。我住在三楼。
    +
    易错提醒:primero / tercero 在阳性单数名词前短尾为 primer / tercer。
    +
    + +
    +

    6. 日期表达

    +
    Las fechas
    + +

    格式:Hoy es lunes, 31 de julio de 2026(今天是 2026 年 7 月 31 日星期一)。

    +
    Hoy es lunes, 31 de julio de 2026. Mi cumpleaños es el 5 de mayo.今天是 2026 年 7 月 31 日星期一。我的生日是 5 月 5 日。
    +
    易错提醒:日期中月份不用大写;"在星期几"用 el + 星期(el lunes),"在某日"也用 el + 数字(el 31)。
    +
    + +
    +

    7. 时间表达

    +
    La hora
    +

    整点:Es la una. / Son las dos.(一点/两点)

    +

    半点:Son las tres y media.(三点半)

    +

    刻钟:Son las cuatro y cuarto.(四点一刻)

    +

    差几分:Son las cinco menos diez.(差十分五点)

    +

    下午/晚上:de la tarde / de la noche;中午 de la mañana。

    +
    Son las tres y media de la tarde. Son las cinco menos diez.现在是下午三点半。差十分五点。
    +
    易错提醒:只有 1 点用 es la una,其他都用 son las;问时间用 ¿Qué hora es?
    +
    + +
    +

    8. 疑问句

    +
    Las oraciones interrogativas
    + +
    ¿Estudias español? ¿Qué haces? ¿Café o té?你学西班牙语吗?你在做什么?咖啡还是茶?
    +
    易错提醒:疑问句首必须加倒问号 ¿,句末用正问号 ?。
    +
    + +
    +

    9. 否定句

    +
    Las oraciones negativas
    + +
    No estudio hoy. Nunca fumo. No tengo ningún libro.今天我不学习。我从不抽烟。我没有一本书。
    +
    易错提醒:西班牙语双重否定是正常用法:No tengo nada(我什么都没有),不要改成 No tengo algo。
    +
    + +
    +

    10. hay 存在句

    +
    Hay
    +

    hay 表示"有、存在",无人称,不随主语变化,后接单数或复数名词。

    + +

    区分:hay 表示存在;tener 表示拥有:Hay dos perros.(有两只狗存在)/ Tengo dos perros.(我拥有两只狗)

    +
    Hay un libro en la mesa. No hay agua. Hay dos supermercados aquí.桌上有一本书。没有水。这里有两家超市。
    +
    易错提醒:hay 永远不变位为 han/hayn;"有许多"用 hay muchos / muchas。
    +
    + +
    +

    11. ser / estar 区别

    +
    Ser vs. Estar
    + + + + + + + + + + +
    serestar
    本质/身份Soy estudiante.(我是学生)—
    国籍/来源Ella es de China.(她来自中国)—
    时间/日期Hoy es lunes.(今天是星期一)—
    职业Es médico.(他是医生)—
    位置—Estoy en casa.(我在家)
    临时状态—Estoy cansado.(我累了)
    进行时—Estoy leyendo.(我正在读)
    性质Es bonito.(它很漂亮)Está bonito.(现在看起来漂亮)
    +
    Soy estudiante. Estoy en casa. Estoy cansado. Hoy es lunes.我是学生。我在家。我累了。今天是星期一。
    +
    易错提醒:ser + 形容词表固有性质,estar + 形容词表临时状态:Es listo(聪明)≠ Está listo(准备好了)。
    +
    + +
    +

    12. 基本语序

    +
    El orden de las palabras
    +

    陈述句基本语序:主语 + 动词 + 宾语 / 补语。

    +
    Yo estudio español en la universidad.我在大学学习西班牙语。
    +

    常见变体:

    + +
    Ahora estudio español en la universidad. ¿Vives en Madrid?现在我在大学学习西班牙语。你住在马德里吗?
    +
    + + + + + diff --git a/knowledge/pronombre.html b/knowledge/pronombre.html new file mode 100644 index 0000000..d4a5474 --- /dev/null +++ b/knowledge/pronombre.html @@ -0,0 +1,108 @@ + + + + + + 代词 — 西班牙语知识库 + + + + + +

    代词

    +

    《现代西班牙语1》代词体系,共 5 个知识点。

    + +
    +

    1. 人称代词(主格)

    +
    Los pronombres personales
    + + + + + +
    人称单数复数
    第一人称yo(我)nosotros / nosotras(我们)
    第二人称tú(你), usted(您)vosotros / vosotras(你们), ustedes(您们)
    第三人称él(他), ella(她)ellos / ellas(他们/她们)
    +

    用法要点:

    + +
    Yo soy estudiante. Tú eres profesor. Él es médico.我是学生。你是老师。他是医生。
    +
    易错提醒:省略主语是常态,但为强调或避免歧义时可保留:Ella trabaja aquí.
    +
    + +
    +

    2. 宾格代词(直接宾语)

    +
    Los pronombres de objeto directo
    + + + + + +
    人称单数复数
    第一人称me(我)nos(我们)
    第二人称te(你)os(你们)
    第三人称lo / la(他/她/它)los / las(他们/她们/它们)
    +

    位置:变位动词前,或紧跟在原形动词后:Lo veo. / Voy a verlo.

    +
    Veo a María. → La veo. Leo libros. → Los leo.我看见玛丽亚 → 我看见她;我读书 → 我读它们
    +
    易错提醒:指人直接宾语前要加 a(Veo a María);宾格代词 lo/la 要随性数变化。
    +
    + +
    +

    3. 与格代词(间接宾语)

    +
    Los pronombres de objeto indirecto
    + + + + + +
    人称单数复数
    第一人称me(给我)nos(给我们)
    第二人称te(给你)os(给你们)
    第三人称le(给他/她/您)les(给他们/她们/您们)
    +

    与格 + 宾格同现:le/les 变 se:Se lo doy.(我把它给他)

    +
    Me das el libro. → Me lo das. Te escribo una carta. → Te la escribo.你给我书 → 你把它给我;我给你写信 → 我把它写给你
    +
    易错提醒:le/les + lo/la/los/las 时,le/les 必须改为 se,不能写 le lo。
    +
    + +
    +

    4. 指示代词

    +
    Los pronombres demostrativos
    + + + + + +
    距离阳性阴性中性
    近éste / éstosésta / éstasesto
    中ése / ésosésa / ésaseso
    远aquél / aquéllosaquélla / aquéllasaquello
    +

    中性形式 esto/eso/aquello 指代事物或概念,无复数、无性变化。

    +
    ¿Qué es esto? Eso es importante. Aquello no me gusta.这是什么?那很重要。那个我不喜欢。
    +
    易错提醒:现代西班牙语皇家学院已允许指示代词不加重音,但教材中 éste/ése 通常仍带重音以区别形容词。
    +
    + +
    +

    5. 疑问代词

    +
    Los pronombres interrogativos
    + + + + + + + + + +
    疑问词含义示例
    qué什么¿Qué estudias?
    quién / quiénes谁¿Quién es ella?
    cuál / cuáles哪一个¿Cuál prefieres?
    cuánto/a/os/as多少¿Cuánto cuesta?
    dónde哪里¿Dónde vives?
    cuándo什么时候¿Cuándo llegas?
    cómo怎样¿Cómo estás?
    +
    ¿Qué haces? ¿Quién es tu amigo? ¿Dónde está la biblioteca?你在做什么?谁是你的朋友?图书馆在哪里?
    +
    易错提醒:直接/间接疑问句中的疑问词都必须带重音:No sé qué hacer.(我不知道做什么)
    +
    + + + + + diff --git a/knowledge/style.css b/knowledge/style.css new file mode 100644 index 0000000..2953641 --- /dev/null +++ b/knowledge/style.css @@ -0,0 +1,204 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #f8f9fa; + --panel-bg: #ffffff; + --border: #dee2e6; + --text: #212529; + --text-muted: #6c757d; + --accent: #0d6efd; + --accent-hover: #0b5ed7; + --danger: #dc3545; + --success: #198754; + --warning-bg: #fff3cd; + --warning-border: #ffc107; + --code-bg: #f1f3f5; + --radius: 8px; + --font-mono: 'Consolas', 'Courier New', monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #1a1d23; + --panel-bg: #212529; + --border: #373b3e; + --text: #e9ecef; + --text-muted: #9ca3af; + --accent: #3b82f6; + --accent-hover: #60a5fa; + --danger: #ef4444; + --success: #22c55e; + --warning-bg: #3d3520; + --warning-border: #b8860b; + --code-bg: #2d2d2f; + } +} + +body { + font-family: system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.6; + padding: 1.5rem; + max-width: 960px; + margin: 0 auto; +} + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +.top-nav { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--border); +} +.top-nav .back { font-size: 0.9rem; } +.top-nav .home { font-weight: 600; } + +h1 { + font-size: 1.6rem; + margin-bottom: 0.25rem; +} +.page-subtitle { + color: var(--text-muted); + margin-bottom: 1.5rem; +} + +.category-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + margin: 1.5rem 0; +} +.category-card { + display: block; + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + color: var(--text); + transition: border-color 0.15s, box-shadow 0.15s; +} +.category-card:hover { + border-color: var(--accent); + box-shadow: 0 4px 12px rgba(0,0,0,0.08); + text-decoration: none; +} +.category-card h2 { font-size: 1.1rem; margin-bottom: 0.3rem; } +.category-card p { color: var(--text-muted); font-size: 0.85rem; } +.category-card .count { + display: inline-block; + margin-top: 0.6rem; + font-size: 0.75rem; + color: var(--accent); + background: var(--code-bg); + padding: 0.15rem 0.5rem; + border-radius: 4px; +} + +/* Article sections */ +.article { + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; + margin-bottom: 1.5rem; +} +.article h2 { + font-size: 1.2rem; + margin-bottom: 0.2rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} +.article .article-title-en { + color: var(--text-muted); + font-size: 0.9rem; + margin-bottom: 0.75rem; +} +.article h3 { + font-size: 1rem; + margin: 1rem 0 0.4rem; + color: var(--accent); +} +.article p { margin-bottom: 0.5rem; } +.article ul, .article ol { margin: 0.4rem 0 0.75rem 1.4rem; } +.article li { margin-bottom: 0.25rem; } + +.example { + background: var(--code-bg); + border-radius: 6px; + padding: 0.75rem 1rem; + margin: 0.5rem 0 0.75rem; + font-family: var(--font-mono); + font-size: 0.95rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.75rem; +} +.example .zh { + color: var(--text-muted); + font-family: system-ui, sans-serif; + font-size: 0.85rem; +} +.speak-btn { + background: none; + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.25rem 0.6rem; + font-size: 0.8rem; + cursor: pointer; + color: var(--text-muted); + flex-shrink: 0; + line-height: 1; +} +.speak-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +.tip { + background: var(--warning-bg); + border-left: 4px solid var(--warning-border); + border-radius: 0 6px 6px 0; + padding: 0.6rem 0.9rem; + margin: 0.75rem 0; + font-size: 0.9rem; +} +.tip strong { color: var(--warning-border); } + +table { + width: 100%; + border-collapse: collapse; + margin: 0.6rem 0 0.9rem; + font-size: 0.9rem; +} +th, td { + border: 1px solid var(--border); + padding: 0.45rem 0.6rem; + text-align: left; +} +th { + background: var(--code-bg); + font-weight: 600; +} + +.badge { + display: inline-block; + font-size: 0.7rem; + padding: 0.1rem 0.45rem; + border-radius: 4px; + background: var(--code-bg); + color: var(--text-muted); + margin-left: 0.4rem; + vertical-align: middle; +} + +@media (max-width: 700px) { + body { padding: 0.75rem; } + .example { flex-direction: column; align-items: flex-start; } + .category-grid { grid-template-columns: 1fr; } +} diff --git a/knowledge/sustantivo.html b/knowledge/sustantivo.html new file mode 100644 index 0000000..4c8e24a --- /dev/null +++ b/knowledge/sustantivo.html @@ -0,0 +1,99 @@ + + + + + + 名词与冠词 — 西班牙语知识库 + + + + + +

    名词与冠词

    +

    《现代西班牙语1》基础词法,共 4 个知识点。

    + +
    +

    1. 名词的性

    +
    El género del sustantivo
    +

    西班牙语名词分阳性和阴性,且名词的性影响冠词和形容词的形式。

    + + + + + + +
    规则阳性阴性
    以 -o 结尾多为阳性libro(书), perro(狗)—
    以 -a 结尾多为阴性—mesa(桌子), casa(房子)
    以 -or、-ón 结尾多为阳性profesor(男老师)—
    以 -ción、-sión、-dad 结尾多为阴性—canción(歌曲), ciudad(城市)
    +

    例外:el día(天)、la mano(手)、el mapa(地图)、el problema(问题)、el programa(节目)。

    +
    el libro, la mesa, el problema书,桌子,问题
    +
    易错提醒:以 -a 结尾不一定是阴性,如 el día、el mapa、el programa。
    +
    + +
    +

    2. 名词的数

    +
    El número del sustantivo
    + + + + + + +
    规则单数复数
    以元音结尾加 -slibrolibros
    以辅音结尾加 -esciudadciudades
    以 z 结尾变 z → c 再加 -eslápizlápices
    以 -s 结尾且末音节不重读,单复数同形luneslunes
    +
    un libro, dos libros; una ciudad, tres ciudades一本书、两本书;一座城、三座城
    +
    易错提醒:z 结尾变复数先变 c(lápiz → lápices),不是直接加 es。
    +
    + +
    +

    3. 定冠词

    +
    El artículo determinado
    +

    定冠词表示特指,相当于"这个/那个"。

    + + + + +
    阳性阴性
    单数ella
    复数loslas
    +

    用法:

    + +
    el libro, la casa, los amigos, las mujeres书,房子,朋友们,女人们
    +
    易错提醒:el agua 是"阴性名词 + 阳性冠词",形容阴性词仍用阴性形式:el agua fría。
    +
    + +
    +

    4. 不定冠词

    +
    El artículo indeterminado
    +

    不定冠词表示泛指,相当于"一个/一些"。

    + + + + +
    阳性阴性
    单数ununa
    复数unosunas
    + +
    un amigo, una amiga, unos amigos, unas amigas一个男/女朋友,一些男/女朋友
    +
    易错提醒:表示职业的句子常用 un/una(Es médico),否定存在常省略冠词(No hay agua)。
    +
    + + + + + diff --git a/knowledge/verbo.html b/knowledge/verbo.html new file mode 100644 index 0000000..9326eab --- /dev/null +++ b/knowledge/verbo.html @@ -0,0 +1,165 @@ + + + + + + 动词与时态 — 西班牙语知识库 + + + + + +

    动词与时态

    +

    《现代西班牙语1》核心动词体系,共 7 个知识点。

    + +
    +

    1. 规则动词变位

    +
    La conjugación regular
    +

    西班牙语动词按词尾分为 -ar、-er、-ir 三类。规则动词变位时去掉词尾,加对应人称词尾。

    +

    -ar 动词(hablar 说)

    + + + + + + + + +
    人称词尾变位
    yo-ohablo
    tú-ashablas
    él/ella/usted-ahabla
    nosotros-amoshablamos
    vosotros-áishabláis
    ellos/ustedes-anhablan
    +

    -er 动词(comer 吃)

    + + + + + + + + +
    人称词尾变位
    yo-ocomo
    tú-escomes
    él/ella/usted-ecome
    nosotros-emoscomemos
    vosotros-éiscoméis
    ellos/ustedes-encomen
    +

    -ir 动词(vivir 居住)

    + + + + + + + + +
    人称词尾变位
    yo-ovivo
    tú-esvives
    él/ella/usted-evive
    nosotros-imosvivimos
    vosotros-ísvivís
    ellos/ustedes-enviven
    +
    Hablo español. Comemos juntos. Viven en Madrid.我说西班牙语。我们一起吃饭。他们住在马德里。
    +
    + +
    +

    2. 陈述式现在时

    +
    El presente de indicativo
    +

    用法:

    + +
    Estudio español todos los días. Ahora leo un libro.我每天学习西班牙语。现在我正在读一本书。
    +
    易错提醒:现在时也可表达很近的将来,尤其与 ir a + inf 同义但更口语化。
    +
    + +
    +

    3. 不规则动词

    +
    Los verbos irregulares
    +

    ser(是)

    +
    soy, eres, es, somos, sois, son
    +

    estar(在/处于)

    +
    estoy, estás, está, estamos, estáis, están
    +

    tener(有)

    +
    tengo, tienes, tiene, tenemos, tenéis, tienen
    +

    ir(去)

    +
    voy, vas, va, vamos, vais, van
    +

    hacer(做)

    +
    hago, haces, hace, hacemos, hacéis, hacen
    +

    其他常见不规则:venir(来)、poder(能够)、querer(想要)、decir(说)、ver(看见)。

    +
    Soy estudiante. Estoy cansado. Tengo un libro. Voy a casa. Hago la tarea.我是学生。我累了。我有一本书。我回家。我做作业。
    +
    易错提醒:tener 第一人称单数特殊(tengo);ir 与 ser 的变位有相似点,注意区分。
    +
    + +
    +

    4. 现在完成时

    +
    El pretérito perfecto compuesto
    +

    构成:haber 的现在时 + 过去分词(-ado / -ido)。

    + + + + + + + + +
    人称haber示例(hablar)
    yohehe hablado
    túhashas hablado
    él/ellahaha hablado
    nosotroshemoshemos hablado
    vosotroshabéishabéis hablado
    elloshanhan hablado
    +

    用法:表示刚刚完成的、与现在有关的动作:Hoy he estudiado.(今天我学了)

    +
    Hoy he comido en casa. Ya he hecho la tarea.今天我吃了饭。我已经做完作业。
    +
    易错提醒:不规则过去分词:hacer→hecho、decir→dicho、escribir→escrito、ver→visto、abrir→abierto。
    +
    + +
    +

    5. 简单过去时

    +
    El pretérito indefinido
    +

    规则变位:

    + + + + + + + + +
    人称hablarcomervivir
    yohablécomíviví
    túhablastecomisteviviste
    él/ellahablócomióvivió
    nosotroshablamoscomimosvivimos
    vosotroshablasteiscomisteisvivisteis
    elloshablaroncomieronvivieron
    +

    用法:表示过去已结束的动作:Ayer estudié dos horas.(昨天我学了两个小时)

    +
    Ayer fui al parque. Compré un libro. Vi una película.昨天我去了公园。我买了一本书。我看了一部电影。
    +
    易错提醒:fui 既是 ir 也是 ser 的简单过去时第一/三人称单数,靠上下文判断。
    +
    + +
    +

    6. 动词短语

    +
    Las perífrasis verbales
    + + + + + + + +
    结构含义示例
    ir a + inf将要去做Voy a estudiar.(我要学习)
    tener que + inf必须做Tengo que trabajar.(我必须工作)
    estar + gerundio正在做Estoy leyendo.(我正在读)
    acabar de + inf刚做完Acabo de llegar.(我刚到)
    hay que + inf必须(无人称)Hay que estudiar.(必须学习)
    +

    现在分词构成:-ar → -ando(hablar → hablando);-er/-ir → -iendo(comer → comiendo, vivir → viviendo)。

    +
    Voy a estudiar. Tengo que trabajar. Estoy leyendo un libro.我要学习。我必须工作。我正在读一本书。
    +
    易错提醒:estar + gerundio 中 estar 随主语变位,gerundio 不变。
    +
    + +
    +

    7. gustar 类动词

    +
    El verbo gustar
    +

    gustar 字面义"使喜欢",因此受事作主语,喜欢的人用间接宾语代词。

    + + + + +
    受事结构示例
    单数名词/动词me/te/le/nos/os/les + gustaMe gusta el café.
    复数名词me/te/le/nos/os/les + gustanMe gustan los libros.
    +

    同类动词:encantar(非常喜欢)、interesar(感兴趣)、faltar(缺少)、importar(重要)。

    +
    Me gusta el café. Me gustan los libros. Te interesa el español.我喜欢咖啡。我喜欢书。你对西班牙语感兴趣。
    +
    易错提醒:Me gusta los libros 是错的;受事为复数时 gustar 必须用 gustan。
    +
    + + + + + diff --git a/speech.js b/speech.js new file mode 100644 index 0000000..92197b2 --- /dev/null +++ b/speech.js @@ -0,0 +1,33 @@ +function initSpeech() { + if (!window.speechSynthesis) return; + var voices = window.speechSynthesis.getVoices(); + if (voices.length > 0) return; + window.speechSynthesis.onvoiceschanged = function() {}; +} + +function speakText(text, lang) { + if (!text) return; + if (!window.speechSynthesis) { return; } + if (window.speechSynthesis.getVoices().length === 0) { + window.speechSynthesis.onvoiceschanged = function() { + doSpeak(text, lang); + window.speechSynthesis.onvoiceschanged = null; + }; + window.speechSynthesis.getVoices(); + return; + } + doSpeak(text, lang); +} + +function doSpeak(text, lang) { + window.speechSynthesis.cancel(); + var utterance = new SpeechSynthesisUtterance(text); + var langMap = { 'es': 'es-ES', 'en': 'en-US', 'zh': 'zh-CN' }; + utterance.lang = langMap[lang] || lang; + utterance.rate = 0.9; + utterance.pitch = 1; + window.speechSynthesis.speak(utterance); +} + +if (document.readyState === 'complete') initSpeech(); +else document.addEventListener('DOMContentLoaded', initSpeech); diff --git a/style.css b/style.css new file mode 100644 index 0000000..8c33fe6 --- /dev/null +++ b/style.css @@ -0,0 +1,228 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #f8f9fa; + --panel-bg: #ffffff; + --border: #dee2e6; + --text: #212529; + --text-muted: #6c757d; + --accent: #0d6efd; + --accent-hover: #0b5ed7; + --danger: #dc3545; + --success: #198754; + --highlight: #fff3cd; + --shadow: 0 1px 3px rgba(0,0,0,0.08); + --radius: 8px; + --font-mono: 'Consolas', 'Courier New', monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #1a1d23; + --panel-bg: #212529; + --border: #373b3e; + --text: #e9ecef; + --text-muted: #9ca3af; + --accent: #3b82f6; + --accent-hover: #60a5fa; + --danger: #ef4444; + --success: #22c55e; + --highlight: #3d3520; + } +} + +html, body { height: 100%; } +body { + font-family: system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + display: flex; + justify-content: center; + align-items: stretch; + padding: 1rem; +} + +#app { + display: grid; + grid-template-columns: 240px 1fr 280px; + gap: 1rem; + width: 100%; + max-width: 1200px; + min-height: 0; +} + +#app > * { + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); + gap: 0.5rem; +} + +.panel-header h2 { font-size: 0.95rem; font-weight: 600; } + +/* Left panel — entry list */ +#panel-list { overflow-y: auto; } +#entry-list { + list-style: none; + flex: 1; + overflow-y: auto; +} +#entry-list li { + padding: 0.75rem 1rem; + cursor: pointer; + border-bottom: 1px solid var(--border); + transition: background 0.1s; +} +#entry-list li:hover { background: var(--highlight); } +#entry-list li.active { background: var(--highlight); border-left: 3px solid var(--accent); } +#entry-list .entry-date { font-size: 0.75rem; color: var(--text-muted); } +#entry-list .entry-preview { font-size: 0.85rem; margin-top: 0.15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.entry-actions { display: none; gap: 0.3rem; margin-top: 0.3rem; } +#entry-list li:hover .entry-actions { display: flex; } +.entry-actions button { + background: none; border: 1px solid var(--border); border-radius: 4px; + padding: 0.15rem 0.4rem; font-size: 0.7rem; cursor: pointer; color: var(--text-muted); +} +.entry-actions .btn-delete { color: var(--danger); border-color: var(--danger); } + +#btn-new { + background: var(--accent); color: #fff; border: none; + width: 28px; height: 28px; border-radius: 6px; + font-size: 1.2rem; cursor: pointer; line-height: 1; +} +#btn-new:hover { background: var(--accent-hover); } + +/* Grammar quick access */ +#grammar-quick { + border-top: 1px solid var(--border); + flex-shrink: 0; +} +.grammar-header { + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--border); +} +.more-link { + font-size: 0.75rem; + color: var(--accent); + text-decoration: none; + white-space: nowrap; +} +.more-link:hover { text-decoration: underline; } +#grammar-list { + list-style: none; + padding: 0.25rem 0; + max-height: 220px; + overflow-y: auto; +} +#grammar-list li { + padding: 0.35rem 1rem; + font-size: 0.8rem; +} +#grammar-list a { + color: var(--text); + text-decoration: none; + display: block; +} +#grammar-list a:hover { color: var(--accent); } + +/* Center panel — editor */ +#panel-editor { display: flex; flex-direction: column; } +#entry-date { + border: 1px solid var(--border); border-radius: 4px; + padding: 0.3rem 0.5rem; font-size: 0.85rem; + background: var(--panel-bg); color: var(--text); +} +#btn-save { + background: var(--accent); color: #fff; border: none; + padding: 0.35rem 0.75rem; border-radius: 6px; + font-size: 0.85rem; cursor: pointer; white-space: nowrap; +} +#btn-save:hover { background: var(--accent-hover); } +#entry-text { + flex: 1; resize: none; border: none; padding: 1rem; + font-size: 1.05rem; line-height: 1.7; + font-family: var(--font-mono); + background: transparent; color: var(--text); + outline: none; +} +#entry-hint { padding: 0.5rem 1rem; font-size: 0.75rem; color: var(--text-muted); border-top: 1px solid var(--border); } + +/* Editor panel - live translation section */ +#editor-translate { border-top: 1px solid var(--border); padding: 0.5rem 1rem; } +.editor-section-title { font-size: 0.75rem; font-weight: 600; color: var(--text-muted); margin-bottom: 0.4rem; } +.editor-translate-line { display: flex; gap: 0.4rem; margin-bottom: 0.25rem; font-size: 0.85rem; line-height: 1.5; } +.et-label { font-weight: 600; color: var(--text-muted); white-space: nowrap; min-width: 2.5rem; } +.et-text { color: var(--text); flex: 1; } +#selection-translate { margin-top: 0.3rem; padding: 0.3rem 0.5rem; background: var(--highlight); border-radius: 4px; } + +/* Editor panel - vocabulary section */ +#editor-vocab { border-top: 1px solid var(--border); padding: 0.5rem 1rem; max-height: 180px; overflow-y: auto; } +.vocab-item { display: flex; align-items: center; gap: 0.4rem; padding: 0.25rem 0; font-size: 0.85rem; border-bottom: 1px solid var(--border); } +.vocab-item:last-child { border-bottom: none; } +.vocab-word { font-weight: 600; font-family: var(--font-mono); color: var(--accent); min-width: 6rem; } +.vocab-en { color: var(--text-muted); flex: 1; } +.vocab-zh { color: var(--text); min-width: 3rem; } +.vocab-speech { background: none; border: 1px solid var(--border); border-radius: 4px; padding: 0.15rem 0.4rem; font-size: 0.7rem; cursor: pointer; color: var(--text-muted); line-height: 1; } +.vocab-speech:hover { border-color: var(--accent); color: var(--accent); } + +/* Right panel — translation */ +#panel-translate { overflow-y: auto; } +.translate-section { + padding: 1rem; + border-bottom: 1px solid var(--border); +} +.translate-label { font-size: 0.8rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-muted); } +.translate-text { + font-size: 0.95rem; line-height: 1.6; + min-height: 3rem; padding: 0.5rem; + background: var(--bg); border-radius: 4px; +} +.btn-speech { + background: none; border: 1px solid var(--border); border-radius: 4px; + padding: 0.25rem 0.6rem; font-size: 0.75rem; cursor: pointer; + margin-top: 0.5rem; color: var(--text-muted); +} +.btn-speech:hover { border-color: var(--accent); color: var(--accent); } + +/* Modal overlay for delete confirmation */ +#modal-overlay { + display: none; + position: fixed; inset: 0; + background: rgba(0,0,0,0.4); + align-items: center; justify-content: center; + z-index: 100; +} +#modal-overlay.show { display: flex; } +#modal-box { + background: var(--panel-bg); border-radius: var(--radius); + padding: 1.5rem; max-width: 360px; width: 90%; + box-shadow: 0 8px 24px rgba(0,0,0,0.15); +} +#modal-box p { margin-bottom: 1rem; font-size: 0.9rem; } +#modal-box .modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; } +#modal-box button { + padding: 0.4rem 1rem; border-radius: 6px; border: 1px solid var(--border); + cursor: pointer; font-size: 0.85rem; +} +#modal-box .btn-cancel { background: var(--panel-bg); color: var(--text); } +#modal-box .btn-confirm { background: var(--danger); color: #fff; border-color: var(--danger); } + +@media (max-width: 768px) { + body { padding: 0.5rem; } + #app { grid-template-columns: 1fr; } + #panel-list { max-height: 30vh; } + #panel-editor { min-height: 40vh; } + #panel-translate { max-height: 30vh; } +} diff --git a/translate.js b/translate.js new file mode 100644 index 0000000..793daaf --- /dev/null +++ b/translate.js @@ -0,0 +1,199 @@ +const MOCK_TRANSLATIONS = { + 'hola': { en: 'Hello', zh: '你好' }, + 'buenos días': { en: 'Good morning', zh: '早上好' }, + 'buenas tardes': { en: 'Good afternoon', zh: '下午好' }, + 'buenas noches': { en: 'Good night', zh: '晚安' }, + 'hoy': { en: 'today', zh: '今天' }, + 'ayer': { en: 'yesterday', zh: '昨天' }, + 'mañana': { en: 'tomorrow', zh: '明天' }, + 'fui': { en: 'I went', zh: '我去了' }, + 'fui a': { en: 'I went to', zh: '我去了' }, + 'voy': { en: 'I go', zh: '我去' }, + 'voy a': { en: 'I am going to', zh: '我要去' }, + 'estoy': { en: 'I am', zh: '我是' }, + 'estoy muy': { en: 'I am very', zh: '我非常' }, + 'tengo': { en: 'I have', zh: '我有' }, + 'quiero': { en: 'I want', zh: '我想要' }, + 'me gusta': { en: 'I like', zh: '我喜欢' }, + 'me gustó': { en: 'I liked', zh: '我喜欢了' }, + 'aprendí': { en: 'I learned', zh: '我学了' }, + 'estudié': { en: 'I studied', zh: '我学习了' }, + 'comí': { en: 'I ate', zh: '我吃了' }, + 'bebí': { en: 'I drank', zh: '我喝了' }, + 'leí': { en: 'I read', zh: '我读了' }, + 'escribí': { en: 'I wrote', zh: '我写了' }, + 'trabajé': { en: 'I worked', zh: '我工作了' }, + 'descansé': { en: 'I rested', zh: '我休息了' }, + 'caminé': { en: 'I walked', zh: '我散步了' }, + 'corrí': { en: 'I ran', zh: '我跑步了' }, + 'compré': { en: 'I bought', zh: '我买了' }, + 'vi': { en: 'I saw', zh: '我看了' }, + 'el': { en: 'the', zh: '' }, + 'la': { en: 'the', zh: '' }, + 'los': { en: 'the', zh: '' }, + 'las': { en: 'the', zh: '' }, + 'un': { en: 'a', zh: '' }, + 'una': { en: 'a', zh: '' }, + 'unos': { en: 'some', zh: '一些' }, + 'unas': { en: 'some', zh: '一些' }, + 'al': { en: 'to the', zh: '' }, + 'del': { en: 'of the', zh: '' }, + 'a': { en: 'to', zh: '' }, + 'de': { en: 'of', zh: '' }, + 'en': { en: 'in', zh: '在' }, + 'para': { en: 'for', zh: '为了' }, + 'por': { en: 'for / by', zh: '' }, + 'con': { en: 'with', zh: '和' }, + 'sin': { en: 'without', zh: '没有' }, + 'sobre': { en: 'about', zh: '关于' }, + 'mi': { en: 'my', zh: '我的' }, + 'tu': { en: 'your', zh: '你的' }, + 'su': { en: 'his / her / their', zh: '' }, + 'nuestro': { en: 'our', zh: '我们的' }, + 'amigo': { en: 'friend', zh: '朋友' }, + 'amiga': { en: 'friend', zh: '朋友' }, + 'familia': { en: 'family', zh: '家人' }, + 'casa': { en: 'home', zh: '家' }, + 'trabajo': { en: 'work', zh: '工作' }, + 'escuela': { en: 'school', zh: '学校' }, + 'parque': { en: 'park', zh: '公园' }, + 'restaurante': { en: 'restaurant', zh: '餐厅' }, + 'café': { en: 'cafe', zh: '咖啡馆' }, + 'libro': { en: 'book', zh: '书' }, + 'película': { en: 'movie', zh: '电影' }, + 'música': { en: 'music', zh: '音乐' }, + 'día': { en: 'day', zh: '天' }, + 'noche': { en: 'night', zh: '夜晚' }, + 'comida': { en: 'food', zh: '食物' }, + 'agua': { en: 'water', zh: '水' }, + 'tiempo': { en: 'time / weather', zh: '时间' }, + 'feliz': { en: 'happy', zh: '开心的' }, + 'cansado': { en: 'tired', zh: '累的' }, + 'enfermo': { en: 'sick', zh: '生病的' }, + 'bien': { en: 'well', zh: '好' }, + 'mal': { en: 'bad', zh: '不好' }, + 'muy': { en: 'very', zh: '非常' }, + 'un poco': { en: 'a little', zh: '一点' }, + 'mucho': { en: 'a lot', zh: '很多' }, + 'y': { en: 'and', zh: '和' }, + 'pero': { en: 'but', zh: '但是' }, + 'porque': { en: 'because', zh: '因为' }, + 'también': { en: 'also', zh: '也' }, + 'no': { en: 'not', zh: '不' }, + 'sí': { en: 'yes', zh: '是的' }, + 'gracias': { en: 'thank you', zh: '谢谢' }, + 'por favor': { en: 'please', zh: '请' }, + 'lo siento': { en: 'sorry', zh: '对不起' }, + 'nuevo': { en: 'new', zh: '新的' }, + 'viejo': { en: 'old', zh: '旧的' }, + 'grande': { en: 'big', zh: '大' }, + 'pequeño': { en: 'small', zh: '小' }, + 'bueno': { en: 'good', zh: '好的' }, + 'malo': { en: 'bad', zh: '坏的' }, + 'bonito': { en: 'pretty', zh: '漂亮的' }, + 'rápido': { en: 'fast', zh: '快的' }, + 'lento': { en: 'slow', zh: '慢的' }, + 'aquí': { en: 'here', zh: '这里' }, + 'allí': { en: 'there', zh: '那里' }, + 'ahora': { en: 'now', zh: '现在' }, + 'siempre': { en: 'always', zh: '总是' }, + 'nunca': { en: 'never', zh: '从不' }, + 'ya': { en: 'already', zh: '已经' }, + 'todavía': { en: 'still', zh: '仍然' }, + 'después': { en: 'after', zh: '之后' }, + 'antes': { en: 'before', zh: '之前' }, + 'más': { en: 'more', zh: '更多' }, + 'menos': { en: 'less', zh: '更少' }, + 'tan': { en: 'so', zh: '如此' }, + 'como': { en: 'as / like', zh: '像' }, + 'que': { en: 'that / than', zh: '比' }, + 'este': { en: 'this', zh: '这个' }, + 'ese': { en: 'that', zh: '那个' }, + 'qué': { en: 'what', zh: '什么' }, + 'quién': { en: 'who', zh: '谁' }, + 'cuándo': { en: 'when', zh: '什么时候' }, + 'dónde': { en: 'where', zh: '哪里' }, + 'por qué': { en: 'why', zh: '为什么' }, + 'cómo': { en: 'how', zh: '如何' }, + 'mucho gusto': { en: 'nice to meet you', zh: '很高兴认识你' }, + 'buena suerte': { en: 'good luck', zh: '好运' }, + 'tener que': { en: 'have to', zh: '必须' }, + 'tener hambre': { en: 'be hungry', zh: '饿了' }, + 'tener sed': { en: 'be thirsty', zh: '渴了' }, + 'tener sueño': { en: 'be sleepy', zh: '困了' }, + 'tener razón': { en: 'be right', zh: '是对的' }, + 'hacer calor': { en: 'be hot (weather)', zh: '热' }, + 'hacer frío': { en: 'be cold (weather)', zh: '冷' }, + 'estar bien': { en: 'be fine', zh: '没问题' } +}; + +const SORTED_KEYS = Object.keys(MOCK_TRANSLATIONS).sort(function(a, b) { + return b.length - a.length; +}); + +function translateText(text) { + if (!text || !text.trim()) return { english: '', chinese: '' }; + var lower = text.toLowerCase().trim(); + var enParts = []; + var zhParts = []; + var i = 0; + while (i < lower.length) { + var matched = false; + for (var k = 0; k < SORTED_KEYS.length; k++) { + var key = SORTED_KEYS[k]; + if (lower.indexOf(key, i) === i) { + var entry = MOCK_TRANSLATIONS[key]; + if (entry.en) enParts.push(entry.en); + if (entry.zh) zhParts.push(entry.zh); + i += key.length; + matched = true; + break; + } + } + if (!matched) { + var ch = lower[i]; + if (/[a-zà-ÿñ]/.test(ch)) { enParts.push(ch); } + i++; + } + } + var enStr = enParts.join(' ').replace(/\s+/g, ' ').trim(); + var zhStr = zhParts.join('').trim(); + enStr = enStr.charAt(0).toUpperCase() + enStr.slice(1); + return { english: enStr || text, chinese: zhStr || text }; +} + +function getVocabulary(text) { + if (!text || !text.trim()) return []; + var lower = text.toLowerCase().trim(); + var seen = {}; + var result = []; + var i = 0; + while (i < lower.length) { + for (var k = 0; k < SORTED_KEYS.length; k++) { + var key = SORTED_KEYS[k]; + if (lower.indexOf(key, i) === i) { + var entry = MOCK_TRANSLATIONS[key]; + if (!seen[key] && entry.zh && key.indexOf(' ') === -1) { + seen[key] = true; + result.push({ word: key, english: entry.en, chinese: entry.zh }); + } + i += key.length; + break; + } + } + i++; + } + result.sort(function(a, b) { return b.word.length - a.word.length; }); + return result; +} + +function translateSelection(text) { + if (!text || !text.trim()) return { english: '', chinese: '' }; + return translateText(text); +} + +function getVocabForWord(word) { + var w = word.toLowerCase().trim(); + var entry = MOCK_TRANSLATIONS[w]; + return entry || null; +}