package com.example.springboot.controller;

import com.example.springboot.dto.AddWordsRequest;
import com.example.springboot.entity.TodayPlan;
import com.example.springboot.service.TodayPlanService;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("/today-plan")
public class TodayPlanController {

    @Resource
    private TodayPlanService todayPlanService;

    @PostMapping("/add")
    public void addWordsToPlan(@RequestBody AddWordsRequest request) {
        todayPlanService.addWordsToPlan(request.getUserId(), request.getWordIds());
    }


    @GetMapping("/todaylist")
    public List<TodayPlan> getTodayPlan(@RequestParam Integer userId) {
        return todayPlanService.getTodayPlan(userId);
    }

    @PostMapping("/delete")
    public void deleteWordsFromPlan(@RequestBody AddWordsRequest request) {
        todayPlanService.deletePlansByIds(request.getUserId(), request.getWordIds());
    }


}
package com.example.springboot.service;

import com.example.springboot.entity.TodayPlan;
import com.example.springboot.mapper.TodayPlanMapper;
import org.springframework.stereotype.Service;

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

@Service
public class TodayPlanService {

    @Resource
    private TodayPlanMapper todayPlanMapper;

    public void addWordsToPlan(Integer userId, List<Integer> wordIds) {
        todayPlanMapper.batchInsertPlans(userId, wordIds);
    }

    public List<TodayPlan> getTodayPlan(Integer userId) {
        return todayPlanMapper.getTodayPlans(userId);
    }



    public List<TodayPlan> exportPlanByIds(Integer userId, List<Integer> ids) {
        return todayPlanMapper.selectPlansByIds(userId, ids);
    }

    public void deletePlansByIds(Integer userId, List<Integer> ids) {
        todayPlanMapper.deletePlansByIds(userId, ids);
    }


}
package com.example.springboot.mapper;

import com.example.springboot.entity.TodayPlan;
import org.apache.ibatis.annotations.Param;
import java.util.List;

public interface TodayPlanMapper {
    void batchInsertPlans(@Param("userId") Integer userId, @Param("wordIds") List<Integer> wordIds);
    List<TodayPlan> getTodayPlans(@Param("userId") Integer userId);
    List<TodayPlan> exportPlans(@Param("userId") Integer userId);

    List<TodayPlan> selectPlansByIds(@Param("userId") Integer userId, @Param("ids") List<Integer> ids);
    void deletePlansByIds(@Param("userId") Integer userId, @Param("ids") List<Integer> ids);


}
<?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.TodayPlanMapper">

    <insert id="batchInsertPlans">
        INSERT IGNORE INTO user_word_plan (user_id, word_id)
        VALUES
        <foreach collection="wordIds" item="wordId" separator=",">
            (#{userId}, #{wordId})
        </foreach>
    </insert>

    <select id="getTodayPlans" resultType="com.example.springboot.entity.TodayPlan">
        SELECT uwp.*,
               w.word AS wordText,
               w.definition AS meaning
        FROM user_word_plan uwp
                 JOIN words w ON uwp.word_id = w.id
        WHERE uwp.user_id = #{userId}
          AND DATE(uwp.added_at) = CURDATE()
        ORDER BY uwp.added_at DESC
    </select>

    <select id="exportPlans" resultType="com.example.springboot.entity.TodayPlan">
        SELECT uwp.*,
               w.word AS wordText,
               w.definition AS meaning
        FROM user_word_plan uwp
                 JOIN words w ON uwp.word_id = w.id
        WHERE uwp.user_id = #{userId}
        ORDER BY uwp.added_at DESC
    </select>

    <select id="selectPlansByIds" resultType="com.example.springboot.entity.TodayPlan">
        SELECT uwp.*,
        w.word AS wordText,
        w.definition AS meaning
        FROM user_word_plan uwp
        JOIN words w ON uwp.word_id = w.id
        WHERE uwp.user_id = #{userId}
        AND uwp.id IN
        <foreach item="id" collection="ids" open="(" separator="," close=")">
            #{id}
        </foreach>
    </select>

    <delete id="deletePlansByIds">
        DELETE FROM user_word_plan
        WHERE user_id = #{userId}
        AND id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>




</mapper>
<template>
  <div>
    <!-- 查询区域 -->
    <div class="card" style="margin-bottom: 10px">
      <el-input
          style="width: 240px; margin-right: 10px"
          v-model="vocabulary.name"
          placeholder="请输入名称查询"
          prefix-icon="Search"
      />
      <el-button type="primary" @click="fetchWords">查 询</el-button>
      <el-button type="warning" @click="reset">重 置</el-button>
      <el-button type="primary" @click="openTodayPlanDialog">今日背诵计划</el-button>
    </div>

    <!-- 操作按钮区域 -->
    <div class="card" style="margin-bottom: 10px">
      <el-button
          type="primary"
          :disabled="!selectedIds.size"
          @click="addSelectedWordsToPlan"
          style="margin-right: 10px"
      >
        加入背诵计划
      </el-button>
    </div>

    <!-- 表格区域 -->
    <div class="card">
      <el-table
          ref="tableRef"
          :data="tableData"
          :row-key="row => row.id"
          stripe
          border
          style="width: 100%"
          @selection-change="handleSelectionChange"
          :row-class-name="getRowClassName"
      >
        <el-table-column type="selection" width="55" :selectable="isSelectable" />
        // 单词列
        <el-table-column prop="word" label="单词">
          <template #default="{ row }">
            <el-tooltip v-if="todayPlanWordIds.has(row.id)" content="已在今日计划中" placement="top">
              <span>{{ row.word }}</span>
            </el-tooltip>
            <span v-else>{{ row.word }}</span>
          </template>
        </el-table-column>

        <el-table-column prop="definition" label="词义" />
        <el-table-column prop="customContent" label="例句" />
        <el-table-column label="操作" width="100">
          <template #default="{ row }">
            <el-button size="small" @click="playWord(row.word)">🔊 发音</el-button>
          </template>
        </el-table-column>
      </el-table>


      <!-- 分页 -->
      <el-pagination
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
          :page-size="pageSize"
          :page-sizes="[10, 20, 30, 50]"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          style="margin-top: 15px; text-align: right"
      />
    </div>

    <!-- 今日背诵计划弹窗 -->
    <el-dialog title="📘 今日背诵计划" v-model="todayPlanDialogVisible" width="60%">
      <div style="margin-bottom: 10px; display: flex; justify-content: space-between;">
        <div>共 {{ todayPlanTotal }} 个单词</div>
      </div>

      <el-table
          :data="todayPlanWords"
          @selection-change="handleTodaySelectionChange"
          stripe
          border
          style="width: 100%"
      >
        <el-table-column type="selection" width="55" />
        <el-table-column prop="wordText" label="单词" />
        <el-table-column prop="meaning" label="词义" />
      </el-table>

      <template #footer>
        <el-button @click="todayPlanDialogVisible = false">关闭</el-button>
        <el-button
            type="success"
            :disabled="!selectedTodayWords.length"
            @click="exportSelectedTodayWords"
        >
          导出选中单词
        </el-button>
        <el-button
            type="danger"
            :disabled="!selectedTodayWords.length"
            @click="deleteSelectedTodayWords"
        >
          删除选中单词
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>

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

// 用户 ID
const userId = localStorage.getItem('userId')

// 表格相关
const vocabulary = reactive({ name: '' })
const tableData = ref([])
const total = ref(0)
const pageSize = ref(10)
const currentPage = ref(1)
const selectedIds = ref(new Set())
const tableRef = ref(null)

// 今日计划相关
const todayPlanDialogVisible = ref(false)
const todayPlanWords = ref([])
const selectedTodayWords = ref([])
const todayPlanTotal = ref(0)
const todayPlanCurrentPage = ref(1)
const todayPlanWordIds = ref(new Set()) // ✅ 新增：保存已加入计划的单词 ID

// 判断某行是否可选中
const isSelectable = (row) => {
  return !todayPlanWordIds.value.has(row.id)
}




// 获取单词数据
const fetchWords = async () => {
  try {
    const params = {
      pageNum: currentPage.value,
      pageSize: pageSize.value,
      keyword: vocabulary.name || undefined
    }
    const res = await axios.get('http://localhost:8080/words', { params })
    tableData.value = res.data.records
    total.value = res.data.total

    await nextTick()
    restoreSelection()
  } catch (err) {
    ElMessage.error('获取单词列表失败')
  }
}

// 多选变化
const handleSelectionChange = (selection) => {
  selection.forEach(item => selectedIds.value.add(item.id))
  tableData.value.forEach(row => {
    if (!selection.find(s => s.id === row.id)) {
      selectedIds.value.delete(row.id)
    }
  })
}

// 恢复勾选状态
const restoreSelection = () => {
  if (!tableRef.value) return
  tableRef.value.clearSelection()
  tableData.value.forEach(row => {
    if (selectedIds.value.has(row.id)) {
      tableRef.value.toggleRowSelection(row, true)
    }
  })
}

// 重置查询
const reset = () => {
  vocabulary.name = ''
  currentPage.value = 1
  fetchWords()
}

// 分页
const handleSizeChange = (val) => {
  pageSize.value = val
  currentPage.value = 1
  fetchWords()
}
const handleCurrentChange = (val) => {
  currentPage.value = val
  fetchWords()
}

// 发音
const playWord = (text) => {
  if (!text) {
    ElMessage.warning('无效的单词发音')
    return
  }
  const utterance = new SpeechSynthesisUtterance(text)
  utterance.lang = 'en-US'
  window.speechSynthesis.speak(utterance)
}

// 加入背诵计划
const addSelectedWordsToPlan = async () => {
  if (!selectedIds.value.size) {
    ElMessage.warning('请先选择单词')
    return
  }
  try {
    const wordIds = Array.from(selectedIds.value)
    await axios.post('http://localhost:8080/today-plan/add', {
      userId,
      wordIds
    })

    ElMessage.success('单词已加入背诵计划')
    if (todayPlanDialogVisible.value) {
      loadTodayPlan(todayPlanCurrentPage.value)
    }

    // 重新加载已加入计划的 ID，刷新禁选状态
    await loadTodayPlan()
    fetchWords()

  } catch (err) {
    ElMessage.error('加入背诵计划失败')
  }
}

// 打开今日计划弹窗
const openTodayPlanDialog = () => {
  todayPlanDialogVisible.value = true
  loadTodayPlan()
}

// 加载今日计划
const loadTodayPlan = async () => {
  try {
    const res = await axios.get('http://localhost:8080/today-plan/todaylist', {
      params: { userId }
    })

    todayPlanWords.value = res.data
    todayPlanTotal.value = res.data.length
    todayPlanWordIds.value = new Set(res.data.map(item => item.wordId || item.id)) // ✅ 更新禁选 ID 集合

  } catch (err) {
    ElMessage.error('加载今日背诵单词失败')
  }
}

// 今日计划选择
const handleTodaySelectionChange = (val) => {
  selectedTodayWords.value = val
}

// 导出选中
const exportSelectedTodayWords = async () => {
  if (!selectedTodayWords.value.length) {
    ElMessage.warning('请先选择单词')
    return
  }
  try {
    const selectedIds = selectedTodayWords.value.map(item => item.id)
    const params = new URLSearchParams()
    params.append('userId', userId)
    selectedIds.forEach(id => params.append('ids', id))

    const res = await axios.get('http://localhost:8080/plan-excel/export-selected', {
      params,
      paramsSerializer: params => params.toString(),
      responseType: 'blob'
    })

    const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
    const url = window.URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    link.setAttribute('download', 'today_words.xlsx')
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    window.URL.revokeObjectURL(url)

  } catch (err) {
    ElMessage.error('导出失败')
  }
}

// 删除今日单词
const deleteSelectedTodayWords = async () => {
  if (!selectedTodayWords.value.length) {
    ElMessage.warning('请先选择要删除的单词')
    return
  }

  try {
    const confirm = await ElMessageBox.confirm(
        '确定要从今日背诵计划中删除选中的单词吗？',
        '提示',
        {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }
    )

    const idsToDelete = selectedTodayWords.value.map(item => item.id)

    await axios.post('http://localhost:8080/today-plan/delete', {
      userId,
      wordIds: idsToDelete
    })

    ElMessage.success('删除成功')
    loadTodayPlan()

  } catch (err) {
    if (err !== 'cancel') {
      ElMessage.error('删除失败')
    }
  }
}

// 初始化加载
onMounted(async () => {
  await loadTodayPlan()
  fetchWords()
})
</script>


在用户点击加入背诵计划时，应该检测一下选中的单词是否已存在user_word_plan，long_term_memory里面，如果存在则不能添加