Vue
<template>
  <el-row style="height: 100vh; padding: 20px;" :gutter="20">

    <!-- 左侧：短期记忆区域 -->
    <el-col :span="12" class="left-panel" style="height: 100%;">

      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
        <h2>短期记忆区域</h2>
        <div>
          <el-button type="primary" @click="openSelectedDialog">查看已选单词</el-button>
          <el-button type="success" @click="openDialog" style="margin-left: 10px;">选择单词</el-button>
        </div>
      </div>

      <!-- 单词卡片 -->
   
      <div class="card-container" v-if="selectedWords.length">
        <div class="card-wrapper">
          <div class="card" :class="{ flipped: isFlipped }">
            <div class="card-face card-front">
              <h1>{{ currentWord.word }}</h1>
              <el-button
                  @click.stop="speakWord(currentWord.word)"
                  style="background-color: #ffffff; border-color: #ffffff; color: #000000; margin-top: 20px;"
              >
                🔊 发音
              </el-button>

            </div>
            <div class="card-face card-back">
              <h1>{{ currentWord.word }}</h1>
              <p><strong>释义：</strong>{{ currentWord.definition }}</p>
              <p><strong>发音：</strong>{{ currentWord.pronunciation || '暂无' }}</p>
              <el-button
                  @click.stop="speakWord(currentWord.word)"
                  style="background-color: #ffffff; border-color: #ffffff; color: #000000; margin-top: 20px;"
              >
                🔊 发音
              </el-button>
            </div>
          </div>

          <div class="btn-group">
            <el-button type="danger" @click="handleButtonClick('forget')">忘记</el-button>
            <el-button type="success" @click="handleButtonClick('know')">认识</el-button>
          </div>
        </div>
      </div>
      
      <!-- 已选单词弹窗 -->
      <el-dialog v-model="selectedDialogVisible" title="已选单词列表" width="800px">
        <el-table :data="selectedWords" style="width: 100%;" border @selection-change="handleSelectedChange">
          <el-table-column type="selection" width="55" />
          <el-table-column prop="id" label="ID" width="80" />
          <el-table-column prop="word" label="单词" />
          <el-table-column prop="definition" label="释义" />
          <el-table-column label="操作" width="100">
            <template #default="scope">
              <el-button type="danger" size="small" @click="removeWord(scope.row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="selectedDialogVisible = false">关闭</el-button>
            <el-button type="danger" :disabled="!selectedToDelete.length" @click="batchRemoveWords">
              批量删除（{{ selectedToDelete.length }}）
            </el-button>
          </span>
        </template>
      </el-dialog>
    </el-col>

    <!-- 右侧：暂时不开发 -->
    <el-col :span="12" class="right-panel" style="border: 1px solid #eee; padding: 20px;">
      <h2>暂未开发</h2>
      <p>暂未开发</p>
    </el-col>

    <!-- 弹窗：选择待学习单词 -->
    <el-dialog v-model="dialogVisible" title="选择待学习单词" width="600px">
      <el-table :data="pendingWords" style="width: 100%;" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" />
        <el-table-column prop="word" label="单词" />
        <el-table-column prop="definition" label="释义" />
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitSelectedWords">
            确认选择（{{ selectedToAdd.length }}）
          </el-button>
        </span>
      </template>
    </el-dialog>
  </el-row>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'

axios.defaults.baseURL = 'http://localhost:8080'

// 状态
const selectedWords = ref([])
const pendingWords = ref([])
const selectedToAdd = ref([])
const selectedToDelete = ref([])
const dialogVisible = ref(false)
const selectedDialogVisible = ref(false)

// 当前学习卡片
const currentIndex = ref(0)
const isFlipped = ref(false)

const currentWord = computed(() => {
  return selectedWords.value[currentIndex.value] || {}
})

// 语音合成器缓存
const utterance = new SpeechSynthesisUtterance()
utterance.lang = 'en-US'

// 页面加载时预热语音合成器，减少首次朗读延迟
onMounted(() => {
  speechSynthesis.speak(new SpeechSynthesisUtterance(' '))
})

// 获取已选单词
const fetchSelectedWords = async () => {
  const userId = localStorage.getItem('userId')
  if (!userId) return ElMessage.warning("请先登录或设置 userId")
  try {
    const res = await axios.get('/study/select-words', { params: { userId } })
    selectedWords.value = res.data
    currentIndex.value = 0
    isFlipped.value = false
    if (selectedWords.value.length > 0) {
      speakWord(selectedWords.value[0].word)
    }
  } catch {
    ElMessage.error("获取已选单词失败")
  }
}


// 查看已选单词弹窗
const openSelectedDialog = async () => {
  await fetchSelectedWords()
  selectedToDelete.value = []
  selectedDialogVisible.value = true
}

// 打开选择单词弹窗
const openDialog = async () => {
  const userId = localStorage.getItem('userId')
  if (!userId) return ElMessage.warning("请先登录或设置 userId")
  try {
    const res = await axios.get('/study/pending-words', {
      params: { userId, limit: 100 }
    })
    pendingWords.value = res.data
    dialogVisible.value = true
  } catch {
    ElMessage.error("获取待学习单词失败")
  }
}

// 选择表格单词
const handleSelectionChange = (val) => {
  selectedToAdd.value = val
}

const handleSelectedChange = (val) => {
  selectedToDelete.value = val
}

// 确认添加单词
const submitSelectedWords = async () => {
  const userId = localStorage.getItem('userId')
  const wordIds = selectedToAdd.value.map(item => item.wordId || item.id)
  if (wordIds.length < 10) return ElMessage.warning("请至少选择10个单词")
  try {
    await axios.post('/study/select-words', { userId: Number(userId), wordIds })
    dialogVisible.value = false
    await fetchSelectedWords()
    ElMessage.success("添加成功")
  } catch {
    ElMessage.error("添加单词失败")
  }
}

// 单个删除
const removeWord = async (row) => {
  const userId = localStorage.getItem('userId')
  try {
    await axios.post('/study/remove-word', {
      userId: Number(userId),
      wordId: row.wordId || row.id
    })
    await fetchSelectedWords()
    ElMessage.success("删除成功")
  } catch {
    ElMessage.error("删除失败")
  }
}

// 批量删除
const batchRemoveWords = async () => {
  const userId = localStorage.getItem('userId')
  const wordIds = selectedToDelete.value.map(item => item.wordId || item.id)
  if (!wordIds.length) return ElMessage.warning("请选择要删除的单词")
  try {
    await axios.post('/study/remove-words', { userId: Number(userId), wordIds })
    await fetchSelectedWords()
    selectedToDelete.value = []
    ElMessage.success("批量删除成功")
  } catch {
    ElMessage.error("批量删除失败")
  }
}

// 优化后的发音函数
const speakWord = (text) => {
  if (!text) return
  // 如果当前正在朗读，取消后再朗读
  if (speechSynthesis.speaking) {
    speechSynthesis.cancel()
  }
  utterance.text = text
  speechSynthesis.speak(utterance)
}

// 卡片按钮点击
const handleButtonClick = (type) => {
  if (!isFlipped.value) {
    // 正面 -> 背面前发音
    speakWord(currentWord.value.word)
    isFlipped.value = true
  } else {
    if (currentIndex.value < selectedWords.value.length - 1) {
      currentIndex.value++
    } else {
      currentIndex.value = 0
      ElMessage.info("已是最后一个，返回第一个")
    }
    isFlipped.value = false
    // 当展示新正面时也发音
    speakWord(currentWord.value.word)
  }
}

</script>


<style scoped>

.left-panel {
  display: flex;
  flex-direction: column;
  border: 1px solid #eee;
  height: 100%;
}

.card-container {
  flex-grow: 1;
  margin-top: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  perspective: 1000px;
  width: 100%;
}

/* 卡片尺寸你可以自己调整 */
.card {
  width: 100%;
  max-width: 550px;
  height: 500px; /* ← 你可以根据需要自行改这个高度 */
  position: relative;
  transition: transform 0.6s;
  transform-style: preserve-3d;
  border-radius: 16px;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
  background-color: #fff;
}

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

.card-face {
  position: absolute;
  width: 100%;
  height: 100%;
  padding: 20px;
  box-sizing: border-box;
  backface-visibility: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.card-front {
  font-size: 1em;
  font-weight: bold;
}

.card-back {
  transform: rotateY(180deg);
  font-size: 1.2em;
  text-align: center;
  line-height: 1.6;
}

.card-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
}


.btn-group {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 30px;
  margin-top: 30px;
}

/* 按钮样式更大更显眼 */
.btn-group .el-button {
  padding: 18px 40px;
  font-size: 18px;
  border-radius: 10px;
  font-weight: bold;
  min-width: 140px;
  transition: all 0.2s ease-in-out;
}

.btn-group .el-button:hover {
  transform: scale(1.05);
}
</style>

Entity
package com.example.springboot.entity;

public class Study {
    private Integer id;
    private Integer userId;
    private Integer wordId;
    private Integer score;
    private Integer lastSeen;
    private String word;        // 单词文本
    private String definition;  // 单词释义

    // Getters and setters
    public String getWord() { return word; }
    public void setWord(String word) { this.word = word; }

    public String getDefinition() { return definition; }
    public void setDefinition(String definition) { this.definition = definition; }



    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }

    public Integer getUserId() { return userId; }
    public void setUserId(Integer userId) { this.userId = userId; }

    public Integer getWordId() { return wordId; }
    public void setWordId(Integer wordId) { this.wordId = wordId; }

    public Integer getScore() { return score; }
    public void setScore(Integer score) { this.score = score; }

    public Integer getLastSeen() { return lastSeen; }
    public void setLastSeen(Integer lastSeen) { this.lastSeen = lastSeen; }
}

Controller
package com.example.springboot.controller;

import com.example.springboot.service.StudyService;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/study")
public class StudyController {

    @Resource
    private StudyService studyService;
    //  用户选择单词
    @PostMapping("/select-words")
    public ResponseEntity<?> userSelectWords(@RequestBody Map<String, Object> payload) {
        Integer userId = (Integer) payload.get("userId");
        List<Integer> wordIds = (List<Integer>) payload.get("wordIds");

        if (userId == null || wordIds == null || wordIds.isEmpty()) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("参数错误");
        }

        int insertedCount = studyService.insertUserSelectedWords(userId, wordIds);

        if (insertedCount == 0) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body("所有单词已存在，无需再添加");
        }

        return ResponseEntity.ok("成功添加 " + insertedCount + " 个新单词");
    }

   //  获取已经选择的单词列表
    @GetMapping("/select-words")
    public ResponseEntity<?> getSelectedWords(@RequestParam(value = "userId", required = false) String userIdStr) {
        if (userIdStr == null || !userIdStr.matches("\\d+")) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("缺少或非法的 userId");
        }

        Integer userId = Integer.valueOf(userIdStr);
        return ResponseEntity.ok(studyService.getSelectedWordsByUserId(userId));
    }

    // 获取用户可选的未学习单词（用于前端弹窗选择）
    @GetMapping("/pending-words")
    public ResponseEntity<?> getPendingWords(
            @RequestParam("userId") Integer userId,
            @RequestParam(value = "limit", defaultValue = "100") Integer limit) {
        return ResponseEntity.ok(studyService.getPendingWords(userId, limit));
    }

    // 删除用户选择的单词
    @PostMapping("/remove-word")
    public ResponseEntity<?> removeWord(@RequestBody Map<String, Object> payload) {
        Integer userId = (Integer) payload.get("userId");
        Integer wordId = (Integer) payload.get("wordId");
        if (userId == null || wordId == null) {
            return ResponseEntity.badRequest().body("参数错误");
        }
        int rows = studyService.deleteWord(userId, wordId);
        if (rows > 0) {
            return ResponseEntity.ok("删除成功");
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("未找到单词");
        }
    }

    //  批量删除用户选择的单词
    @PostMapping("/remove-words")
    public ResponseEntity<?> removeWords(@RequestBody Map<String, Object> payload) {
        Integer userId = (Integer) payload.get("userId");
        List<Integer> wordIds = (List<Integer>) payload.get("wordIds");
        if (userId == null || wordIds == null || wordIds.isEmpty()) {
            return ResponseEntity.badRequest().body("参数错误");
        }
        int rows = studyService.deleteWords(userId, wordIds);
        if (rows > 0) {
            return ResponseEntity.ok("批量删除成功");
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("未找到单词");
        }
    }



}

Service
package com.example.springboot.service;

import com.example.springboot.entity.Study;
import com.example.springboot.entity.Word;
import com.example.springboot.mapper.StudyMapper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class StudyService {

    @Resource
    private StudyMapper studyMapper;


    // 用户手动选择单词加入记忆
    public int insertUserSelectedWords(Integer userId, List<Integer> wordIds) {
        int count = 0;
        for (Integer wordId : wordIds) {
            Study exist = studyMapper.selectStudyByUserIdAndWordId(userId, wordId);
            if (exist == null) {
                Study study = new Study();
                study.setUserId(userId);
                study.setWordId(wordId);
                study.setScore(0);
                study.setLastSeen(-1);
                studyMapper.insertStudy(study);
                count++;
            }
        }
        return count;
    }

    // 获取用户选择的单词
    public List<Word> getSelectedWordsByUserId(Integer userId) {
        return studyMapper.selectSelectedWordsByUserId(userId);
    }

    //  获取待背单词
    public List<Study> getPendingWords(Integer userId, int limit) {
        return studyMapper.selectPendingWordsFromPlan(userId, limit);
    }

    // 删除单词
    public int deleteWord(Integer userId, Integer wordId) {
        return studyMapper.deleteStudyByUserIdAndWordId(userId, wordId);
    }

    // 批量删除
    public int deleteWords(Integer userId, List<Integer> wordIds) {
        return studyMapper.deleteStudiesByUserIdAndWordIds(userId, wordIds);
    }





}

Mapper
package com.example.springboot.mapper;

import com.example.springboot.entity.Study;
import com.example.springboot.entity.Word;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface StudyMapper {

    // 从user_word_plan抽取指定数量且user_word_memory中不存在的单词
    List<Study> selectPendingWordsFromPlan(@Param("userId") Integer userId, @Param("limit") int limit);

    // 查询 user_word_memory 中对应记录
    Study selectStudyByUserIdAndWordId(@Param("userId") Integer userId, @Param("wordId") Integer wordId);

    // 插入 user_word_memory
    int insertStudy(Study study);

    // 更新 user_word_memory
    int updateStudy(Study study);

    // 查询单词详情
    Word selectWordById(@Param("id") Integer id);

    // 查询某个用户的所有已选单词详细信息
    List<Word> selectSelectedWordsByUserId(@Param("userId") Integer userId);

    //  单个删除
    int deleteStudyByUserIdAndWordId(@Param("userId") Integer userId, @Param("wordId") Integer wordId);

    //  批量删除
    int deleteStudiesByUserIdAndWordIds(@Param("userId") Integer userId, @Param("wordIds") List<Integer> wordIds);



}

Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.springboot.mapper.StudyMapper">

    <!-- 从 user_word_plan 中抽取待学习且 user_word_memory 中不存在的单词 -->
    <select id="selectPendingWordsFromPlan" parameterType="map" resultType="com.example.springboot.entity.Study">
        SELECT
            NULL AS id,
            up.user_id AS userId,
            up.word_id AS wordId,
            w.word AS word,
            w.definition AS definition,
            0 AS score,
            -1 AS lastSeen
        FROM user_word_plan up
                 JOIN words w ON up.word_id = w.id
                 LEFT JOIN user_word_memory um ON up.user_id = um.user_id AND up.word_id = um.word_id
        WHERE up.user_id = #{userId} AND up.status = 'pending' AND um.id IS NULL
        ORDER BY up.added_at ASC
        LIMIT #{limit}
    </select>


    <!-- 查询 user_word_memory -->
    <select id="selectStudyByUserIdAndWordId" parameterType="map" resultType="com.example.springboot.entity.Study">
        SELECT id, user_id AS userId, word_id AS wordId, score, last_seen AS lastSeen
        FROM user_word_memory
        WHERE user_id = #{userId} AND word_id = #{wordId}
    </select>

    <!-- 插入 user_word_memory -->
    <insert id="insertStudy" parameterType="com.example.springboot.entity.Study" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user_word_memory(user_id, word_id, score, last_seen)
        VALUES(#{userId}, #{wordId}, #{score}, #{lastSeen})
    </insert>

    <!-- 更新 user_word_memory -->
    <update id="updateStudy" parameterType="com.example.springboot.entity.Study">
        UPDATE user_word_memory
        SET score = #{score},
            last_seen = #{lastSeen}
        WHERE id = #{id}
    </update>

    <!-- 查询单词详情 -->
    <select id="selectWordById" parameterType="int" resultType="com.example.springboot.entity.Word">
        SELECT id, word, definition, pronunciation, custom_content AS customContent, created_at AS createdAt
        FROM words
        WHERE id = #{id}
    </select>

    <!-- 查询用户已选择的单词 -->
    <select id="selectSelectedWordsByUserId" parameterType="int" resultType="com.example.springboot.entity.Word">
        SELECT
            w.id,
            w.word,
            w.definition,
            w.pronunciation,
            w.custom_content AS customContent,
            w.created_at AS createdAt
        FROM user_word_memory uwm
                 JOIN words w ON uwm.word_id = w.id
        WHERE uwm.user_id = #{userId}
    </select>

    <!--  删除单个单词 -->
    <delete id="deleteStudyByUserIdAndWordId" parameterType="map">
        DELETE FROM user_word_memory WHERE user_id = #{userId} AND word_id = #{wordId}
    </delete>

    <!--  批量删除单词 -->
    <delete id="deleteStudiesByUserIdAndWordIds" parameterType="map">
        DELETE FROM user_word_memory WHERE user_id = #{userId} AND word_id IN
        <foreach item="wordId" collection="wordIds" open="(" separator="," close=")">
            #{wordId}
        </foreach>
    </delete>



</mapper>
帮忙把这个算法逻辑，加入我上面的代码里面
一、算法核心逻辑总结
1. 初始化阶段
加载词库，为每个单词设置初始值：
score=0（分数，衡量熟练度），last_seen=-1（最后复习轮数，-1 表示未复习）。
全局参数：
cooldown=2（冷却轮数，复习后需间隔至少 2 轮才会再次出现）
pass_score=3（掌握阈值，分数≥3 时标记为 “已掌握”）
N=3（每轮复习前 N 个单词，可动态调整）
2. 主循环逻辑（轮次迭代）
筛选冷却单词：
条件：当前轮数(round) - 单词.last_seen ≥ cooldown
含义：确保单词距离上一次复习已过足够轮数，避免频繁重复。
排序规则：
一级优先级：按score升序（低分优先，薄弱单词优先复习）。
二级优先级：若分数相同，按last_seen升序（冷却时间越久越优先）。
复习处理：
选取排序后的前 N 个单词，逐个展示并获取用户回答。
认识：score+1，若score≥pass_score则将已掌握单词放入“长期复习表”，每隔若干轮重新抽查；否则保留，记录last_seen=当前轮数。 
忘记：score-1，记录last_seen=当前轮数，冷却结束后因低分优先复习。
终止条件：
当复习列表（review_list）为空时，流程结束。
短期记忆
CREATE TABLE user_word_memory (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    word_id INT NOT NULL,
    score INT DEFAULT 0,
    last_seen INT DEFAULT -1,
    FOREIGN KEY (user_id) REFERENCES user(id),
    FOREIGN KEY (word_id) REFERENCES words(id),
    UNIQUE(user_id, word_id)
);
长期记忆
CREATE TABLE long_term_memory (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    word_id INT NOT NULL,
    review_count INT DEFAULT 0,          -- 总复习次数
    ease_factor FLOAT DEFAULT 2.5,       -- 学习难度因子（如 SuperMemo 算法所需）
    interval_days INT DEFAULT 0,         -- 下次复习间隔（单位：天）
    next_review_date DATE,               -- 下一次复习的目标日期
    last_review_date DATE,               -- 上一次复习日期
    last_score INT DEFAULT NULL,         -- 上一次复习的记忆分数（例如 0-5）
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES user(id),
    FOREIGN KEY (word_id) REFERENCES words(id),
    UNIQUE(user_id, word_id)
);