diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/LessonPlanController.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/LessonPlanController.java index abda158..971581e 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/LessonPlanController.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/LessonPlanController.java @@ -1,16 +1,23 @@ package com.yinlihupo.enlish.service.controller; +import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO; import com.yinlihupo.enlish.service.model.vo.plan.AddLessonPlanReqVO; +import com.yinlihupo.enlish.service.model.vo.plan.DownLoadLessonPlanReqVO; import com.yinlihupo.enlish.service.service.LessonPlansService; +import com.yinlihupo.enlish.service.utils.WordExportUtil; import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog; import com.yinlihupo.framework.common.response.Response; +import com.yinlihupo.framework.common.util.JsonUtils; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Map; import java.util.concurrent.Executor; @RequestMapping("/plan/") @@ -24,6 +31,11 @@ public class LessonPlanController { @Resource(name = "taskExecutor") private Executor taskExecutor; + @Value("${templates.plan.weekday}") + private String planWeekday; + @Value("${templates.plan.weekend}") + private String planWeekend; + @PostMapping("generate") @ApiOperationLog(description = "生成学案") public Response generateLessonPlan(@RequestBody AddLessonPlanReqVO addLessonPlanReqVO) { @@ -38,4 +50,22 @@ public class LessonPlanController { } } + + @PostMapping("download") + public void downloadLessonPlan(@RequestBody DownLoadLessonPlanReqVO downLoadLessonPlanReqVO, HttpServletResponse response) { + Integer id = downLoadLessonPlanReqVO.getId(); + LessonPlansDO lessonPlanById = lessonPlanService.findLessonPlanById(id); + + try { + Map map = JsonUtils.parseMap(lessonPlanById.getContentDetails(), String.class, Object.class); + if (!lessonPlanById.getTitle().contains("复习")) { + WordExportUtil.generateLessonPlanDocx(map, lessonPlanById.getTitle(), response, planWeekday, true); + } else { + WordExportUtil.generateLessonPlanDocx(map, lessonPlanById.getTitle(), response, planWeekend, false); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + } } diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/LessonPlansDOMapper.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/LessonPlansDOMapper.java index 0593c62..d537c77 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/LessonPlansDOMapper.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/LessonPlansDOMapper.java @@ -12,4 +12,6 @@ public interface LessonPlansDOMapper { LessonPlansDO selectById(Integer id); List findLessonPlansByStudentId(@Param("ids") List ids); + + LessonPlansDO selectByLessonId(@Param("lessonId") Integer lessonId); } \ No newline at end of file diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/plan/DownLoadLessonPlanReqVO.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/plan/DownLoadLessonPlanReqVO.java new file mode 100644 index 0000000..d684209 --- /dev/null +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/plan/DownLoadLessonPlanReqVO.java @@ -0,0 +1,15 @@ +package com.yinlihupo.enlish.service.model.vo.plan; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class DownLoadLessonPlanReqVO { + + private Integer id; +} diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/LessonPlansService.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/LessonPlansService.java index 9bdbcff..a9431ed 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/LessonPlansService.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/LessonPlansService.java @@ -8,4 +8,6 @@ public interface LessonPlansService { void generateLessonPlans(Integer studentId, Integer unitId); List findLessonPlans(List ids); + + LessonPlansDO findLessonPlanById(Integer id); } diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/plan/LessonPlansServiceImpl.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/plan/LessonPlansServiceImpl.java index 6d44ad9..baa3c22 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/plan/LessonPlansServiceImpl.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/plan/LessonPlansServiceImpl.java @@ -154,6 +154,11 @@ public class LessonPlansServiceImpl implements LessonPlansService { return lessonPlansDOMapper.findLessonPlansByStudentId(ids); } + @Override + public LessonPlansDO findLessonPlanById(Integer id) { + return lessonPlansDOMapper.selectByLessonId(id); + } + private Map generateWeekendPlans(List checkList, int day, diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/utils/WordExportUtil.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/utils/WordExportUtil.java index e1e3005..265e40c 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/utils/WordExportUtil.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/utils/WordExportUtil.java @@ -16,6 +16,8 @@ import java.util.zip.ZipOutputStream; public class WordExportUtil { private static final Configure config; + private static final Configure configLessonPlanWeekday; + private static final Configure configLessonPlanWeekend; static { LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); @@ -23,6 +25,24 @@ public class WordExportUtil { .bind("words", policy) .bind("answer", policy) .build(); + + LoopRowTableRenderPolicy policyLessonPlanWeekday = new LoopRowTableRenderPolicy(); + configLessonPlanWeekday = Configure.builder() + .bind("syncVocabList", policyLessonPlanWeekday) + .bind("gapVocabList", policyLessonPlanWeekday) + .bind("reviewVocabList", policyLessonPlanWeekday) + .bind("drillRound1", policyLessonPlanWeekday) + .bind("drillRound2", policyLessonPlanWeekday) + .bind("drillRound3", policyLessonPlanWeekday) + .bind("mixedDrill", policyLessonPlanWeekday) + .bind("checkList", policyLessonPlanWeekday) + .bind("checkListAns", policyLessonPlanWeekday) + .build(); + + LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy(); + configLessonPlanWeekend = Configure.builder() + .bind("checkList", policyLessonPlan) + .build(); } /** @@ -47,6 +67,28 @@ public class WordExportUtil { } } + public static void generateLessonPlanDocx(Map map, String fileName, HttpServletResponse response, String templateWordPath, boolean isWeekday) throws IOException { + fileName = URLEncoder.encode(fileName + ".docx", StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + + // 3. 设置响应头 + response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); + + try (InputStream inputStream = new FileInputStream(templateWordPath)) { + XWPFTemplate template; + if (isWeekday) { + template = XWPFTemplate.compile(inputStream, configLessonPlanWeekday); + } else { + template = XWPFTemplate.compile(inputStream, configLessonPlanWeekend); + } + OutputStream out = response.getOutputStream(); + template.render(map); + template.write(out); + template.close(); + out.flush(); + } + } + /** * 核心补充:批量渲染并打包为 ZIP */ @@ -103,7 +145,7 @@ public class WordExportUtil { */ private static void generateExamWordsDocx(Map data, HttpServletResponse response, String templateWordPath) throws IOException { - String fileName = URLEncoder.encode("摸底测试" + ".docx", StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20"); + String fileName = URLEncoder.encode("摸底测试" + ".docx", StandardCharsets.UTF_8).replaceAll("\\+", "%20"); // 3. 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); diff --git a/enlish-service/src/main/resources/mapper/LessonPlansDOMapper.xml b/enlish-service/src/main/resources/mapper/LessonPlansDOMapper.xml index 21c0b35..5ffe871 100644 --- a/enlish-service/src/main/resources/mapper/LessonPlansDOMapper.xml +++ b/enlish-service/src/main/resources/mapper/LessonPlansDOMapper.xml @@ -33,4 +33,10 @@ + + \ No newline at end of file diff --git a/enlish-vue/src/api/plan.js b/enlish-vue/src/api/plan.js index 74443f1..b192194 100644 --- a/enlish-vue/src/api/plan.js +++ b/enlish-vue/src/api/plan.js @@ -5,4 +5,65 @@ export function generateLessonPlan(studentId, unitId) { studentId: studentId, unitId: unitId }) -} \ No newline at end of file +} + +export function downloadLessonPlan(data) { + return axios.post('/plan/download', data, { + // 1. 重要:必须指定响应类型为 blob,否则下载的文件会损坏(乱码) + responseType: 'blob', + headers: { + 'Content-Type': 'application/json; application/octet-stream' // 根据需要调整 + } + }).then(response => { + // 2. 提取文件名 (处理后端设置的 Content-Disposition) + // 后端示例: header("Content-Disposition", "attachment; filename*=UTF-8''" + fileName) + let fileName = 'download.zip'; // 默认兜底文件名 + + const contentDisposition = response.headers['content-disposition']; + if (contentDisposition) { + // 正则提取 filename*=utf-8''xxx.zip 或 filename="xxx.zip" + const fileNameMatch = contentDisposition.match(/filename\*?=['"]?(?:UTF-\d['"]*)?([^;\r\n"']*)['"]?;?/); + if (fileNameMatch && fileNameMatch[1]) { + // 后端如果用了 URLEncoder,这里需要 decode + fileName = decodeURIComponent(fileNameMatch[1]); + } + } + + // 3. 开始下载流程 + resolveBlob(response.data, fileName); + }).catch(error => { + console.error('下载失败', error); + showMessage('下载失败' + error, 'error'); + // 注意:如果后端报错返回 JSON,因为 responseType 是 blob, + // 这里看到的 error.response.data 也是 blob,需要转回文本才能看到错误信息 + const reader = new FileReader(); + reader.readAsText(error.response.data); + reader.onload = () => { + console.log(JSON.parse(reader.result)); // 打印后端实际报错 + } + }); +} + +const resolveBlob = (res, fileName) => { + // 创建 Blob 对象,可以指定 type,也可以让浏览器自动推断 + const blob = new Blob([res], { type: 'application/octet-stream' }); + + // 兼容 IE/Edge (虽然现在很少用了) + if (window.navigator.msSaveOrOpenBlob) { + navigator.msSaveBlob(blob, fileName); + } else { + // 创建一个临时的 URL 指向 Blob + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = fileName; + + // 触发点击 + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + + // 清理资源 + document.body.removeChild(link); + window.URL.revokeObjectURL(link.href); + } +}; diff --git a/enlish-vue/src/pages/LearningPlan.vue b/enlish-vue/src/pages/LearningPlan.vue index 4ce58e2..0df84ce 100644 --- a/enlish-vue/src/pages/LearningPlan.vue +++ b/enlish-vue/src/pages/LearningPlan.vue @@ -19,16 +19,25 @@
学案
- + + + +
@@ -54,6 +63,8 @@ import Header from '@/layouts/components/Header.vue' import { ref, onMounted } from 'vue' import { findStudentLessonPlans } from '@/api/studentLessonPlans' +import { downloadLessonPlan } from '@/api/plan' +import { showMessage } from '@/composables/util' const rows = ref([]) const loading = ref(false) @@ -62,6 +73,7 @@ const pageSize = ref(10) const totalCount = ref(0) const searchName = ref('') const tableRef = ref(null) +const downloadingIds = ref([]) async function fetchLessonPlans() { loading.value = true @@ -96,6 +108,22 @@ function onReset() { fetchLessonPlans() } +async function onDownload(plan) { + if (!plan?.id) { + showMessage('无效的计划ID', 'error') + return + } + if (!downloadingIds.value.includes(plan.id)) { + downloadingIds.value = [...downloadingIds.value, plan.id] + } + try { + await downloadLessonPlan({ id: plan.id }) + showMessage('开始下载', 'success') + } finally { + downloadingIds.value = downloadingIds.value.filter(id => id !== plan.id) + } +} + onMounted(() => { fetchLessonPlans() })