Compare commits
26 Commits
2d76ed507e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fcf381b8f1 | |||
| fb29acc145 | |||
| 7182371c92 | |||
| 09b326c07a | |||
| 49963bb49c | |||
| bf2a80917c | |||
| deabd5f7f5 | |||
| 0802f6fe70 | |||
| 679241588f | |||
| fe7128dd4e | |||
| 1184ea7895 | |||
| e468be74b7 | |||
| 57166af2f4 | |||
| fcb8ac9c22 | |||
| 7f2fda16ec | |||
| 9cd43c0e74 | |||
| b86f37443c | |||
| 36e5231c6c | |||
| 868e0bb7bd | |||
| 504dd8d964 | |||
| 6277e3ab42 | |||
| 0f5169c1d7 | |||
| 5ebf40101d | |||
| 5858bf2ecc | |||
| bddf6c0936 | |||
| 340bc5b5e3 |
11395
docx/enlish_v3.sql
Normal file
11395
docx/enlish_v3.sql
Normal file
File diff suppressed because one or more lines are too long
@@ -24,24 +24,16 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
.back();
|
.back();
|
||||||
|
|
||||||
SaRouter.match("/**")
|
SaRouter.match("/**")
|
||||||
.notMatch("/class/list")
|
|
||||||
.notMatch("/exam/words/get")
|
|
||||||
.notMatch("/exam/words/detail")
|
|
||||||
.notMatch("/exam/words/student/history")
|
|
||||||
.notMatch("grade/list")
|
|
||||||
.notMatch("/student/list")
|
|
||||||
.notMatch("/student/detail")
|
|
||||||
.notMatch("/studentLessonPlans/list")
|
|
||||||
.notMatch("/studentLessonPlans/history")
|
|
||||||
.notMatch("/student/analyze")
|
|
||||||
.notMatch("/student/mastery/detail")
|
|
||||||
.notMatch("/unit/list")
|
|
||||||
.notMatch("/vocabulary/list")
|
|
||||||
.notMatch("/vocabulary/student/detail")
|
|
||||||
.notMatch("/plan/download")
|
|
||||||
.notMatch("/login/**")
|
.notMatch("/login/**")
|
||||||
|
.notMatch("/plan/word/voice")
|
||||||
|
.notMatch("/plan/word/voice/tts")
|
||||||
.check(r -> StpUtil.checkLogin());
|
.check(r -> StpUtil.checkLogin());
|
||||||
|
|
||||||
|
SaRouter.match("/admin/**")
|
||||||
|
.notMatch("/plan/word/voice")
|
||||||
|
.notMatch("/plan/word/voice/tts")
|
||||||
|
.check(r -> StpUtil.checkRole("root"));
|
||||||
|
|
||||||
}))
|
}))
|
||||||
.addPathPatterns("/**")
|
.addPathPatterns("/**")
|
||||||
.excludePathPatterns("/error");
|
.excludePathPatterns("/error");
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ public class StpInterfaceImpl implements StpInterface {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getRoleList(Object loginId, String loginType) {
|
public List<String> getRoleList(Object loginId, String loginType) {
|
||||||
return userToRole((Long) loginId) ;
|
long l = 0L;
|
||||||
|
if (loginId instanceof String loginIdStr) {
|
||||||
|
l = Long.parseLong(loginIdStr);
|
||||||
|
}
|
||||||
|
return userToRole(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> userToRole(Long userId) {
|
private List<String> userToRole(Long userId) {
|
||||||
|
|||||||
@@ -31,9 +31,15 @@ public class ExamWordsConstant {
|
|||||||
public static final int ZONE_F_SIZE = 7;
|
public static final int ZONE_F_SIZE = 7;
|
||||||
|
|
||||||
|
|
||||||
|
// 摸底
|
||||||
public static final int EXAM_TYPE_BASELINE = 1;
|
public static final int EXAM_TYPE_BASELINE = 1;
|
||||||
|
// 中期
|
||||||
public static final int EXAM_TYPE_MIDTERM = 2;
|
public static final int EXAM_TYPE_MIDTERM = 2;
|
||||||
|
// 期末
|
||||||
public static final int EXAM_TYPE_FINAL = 3;
|
public static final int EXAM_TYPE_FINAL = 3;
|
||||||
|
// 小测
|
||||||
|
public static final int EXAM_TYPE_TEST = 4;
|
||||||
|
|
||||||
|
|
||||||
public static int getZoneA(int gradeId) {
|
public static int getZoneA(int gradeId) {
|
||||||
return switch (gradeId) {
|
return switch (gradeId) {
|
||||||
@@ -131,4 +137,17 @@ public class ExamWordsConstant {
|
|||||||
default -> "";
|
default -> "";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String day2Chinese(int day) {
|
||||||
|
return switch (day) {
|
||||||
|
case 1 -> "一";
|
||||||
|
case 2 -> "二";
|
||||||
|
case 3 -> "三";
|
||||||
|
case 4 -> "四";
|
||||||
|
case 5 -> "五";
|
||||||
|
case 6 -> "六";
|
||||||
|
case 7 -> "七";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
package com.yinlihupo.enlish.service.constant;
|
package com.yinlihupo.enlish.service.constant;
|
||||||
|
|
||||||
public interface LessonPlanConstant {
|
public class LessonPlanConstant {
|
||||||
|
|
||||||
String TITLE = "Title";
|
public static final String TITLE = "Title";
|
||||||
String PASSAGE = "The Passage";
|
public static final String PASSAGE = "ThePassage";
|
||||||
String QUIZ = "Quiz";
|
public static final String QUIZ = "Quiz";
|
||||||
String ANSWER_KEY_EXPLANATION = "Answer Key & Explanation";
|
public static final String ANSWER_KEY_EXPLANATION = "AnswerKey&Explanation";
|
||||||
String FULL_TRANSLATION = "Full Translation";
|
public static final String FULL_TRANSLATION = "FullTranslation";
|
||||||
|
|
||||||
|
// 正在生成学案标识
|
||||||
|
public static final String GENERATING_PLAN = "GeneratingPlan";
|
||||||
|
|
||||||
|
public static String buildGeneratePlanContent(Integer studentId) {
|
||||||
|
return GENERATING_PLAN + ":" + studentId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ public class UserRedisConstants {
|
|||||||
|
|
||||||
public static final String USER_LOGIN_CODE = "user:login:code:";
|
public static final String USER_LOGIN_CODE = "user:login:code:";
|
||||||
|
|
||||||
|
public static final String USER_INVITATION_CODE = "user:invitation:code:";
|
||||||
|
|
||||||
public static String buildUserLoginCode(String phone) {
|
public static String buildUserLoginCode(String phone) {
|
||||||
return USER_LOGIN_CODE + phone;
|
return USER_LOGIN_CODE + phone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String buildUserInvitationCode(String code) {
|
||||||
|
return USER_INVITATION_CODE + code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.yinlihupo.enlish.service.controller;
|
package com.yinlihupo.enlish.service.controller;
|
||||||
|
|
||||||
|
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
|
import com.yinlihupo.enlish.service.model.vo.admin.CreateInvitationCodeReqVO;
|
||||||
|
import com.yinlihupo.enlish.service.model.vo.admin.CreateInvitationCodeRspVO;
|
||||||
import com.yinlihupo.enlish.service.model.vo.user.CreateUserReqVO;
|
import com.yinlihupo.enlish.service.model.vo.user.CreateUserReqVO;
|
||||||
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRepVO;
|
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRepVO;
|
||||||
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRspVO;
|
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRspVO;
|
||||||
@@ -11,6 +14,7 @@ import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
|||||||
import com.yinlihupo.framework.common.response.PageResponse;
|
import com.yinlihupo.framework.common.response.PageResponse;
|
||||||
import com.yinlihupo.framework.common.response.Response;
|
import com.yinlihupo.framework.common.response.Response;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -19,6 +23,8 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@RequestMapping("/admin/")
|
@RequestMapping("/admin/")
|
||||||
@RestController
|
@RestController
|
||||||
@@ -30,6 +36,8 @@ public class AdminController {
|
|||||||
private PasswordEncoder passwordEncoder;
|
private PasswordEncoder passwordEncoder;
|
||||||
@Resource
|
@Resource
|
||||||
private RoleService roleService;
|
private RoleService roleService;
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
@PostMapping("user/list")
|
@PostMapping("user/list")
|
||||||
@ApiOperationLog(description = "查询用户列表")
|
@ApiOperationLog(description = "查询用户列表")
|
||||||
@@ -66,4 +74,18 @@ public class AdminController {
|
|||||||
|
|
||||||
return Response.success();
|
return Response.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("user/create/invitation/code")
|
||||||
|
@ApiOperationLog(description = "创建邀请码")
|
||||||
|
public Response<CreateInvitationCodeRspVO> createInvitationCode(@RequestBody CreateInvitationCodeReqVO createInvitationCodeReqVO) {
|
||||||
|
|
||||||
|
String code = String.valueOf(new Random().nextInt(1000000));
|
||||||
|
Integer limit = createInvitationCodeReqVO.getLimit();
|
||||||
|
Integer expire = createInvitationCodeReqVO.getExpire();
|
||||||
|
|
||||||
|
redisTemplate.opsForValue().set(UserRedisConstants.buildUserInvitationCode(code), limit);
|
||||||
|
redisTemplate.expire(UserRedisConstants.buildUserInvitationCode(code), expire, TimeUnit.DAYS);
|
||||||
|
|
||||||
|
return Response.success(CreateInvitationCodeRspVO.builder().invitationCode(code).build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class ExamWordsController {
|
|||||||
// bug: 获取单词后,单词的id会乱序、 需要重新更新考试记录中的 id
|
// bug: 获取单词后,单词的id会乱序、 需要重新更新考试记录中的 id
|
||||||
examWordsDO.setWordIds(assessmentWords.stream().map(Word::getId).toList());
|
examWordsDO.setWordIds(assessmentWords.stream().map(Word::getId).toList());
|
||||||
examWordsService.updateExamWordsWordIdsOrder(examWordsDO);
|
examWordsService.updateExamWordsWordIdsOrder(examWordsDO);
|
||||||
|
log.info("生成试卷成功 {}", examWordsDO);
|
||||||
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
|
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
|
||||||
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
|
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
@@ -72,6 +72,12 @@ public class ExamWordsController {
|
|||||||
data.put("examStr", examWordsDO.getTitle());
|
data.put("examStr", examWordsDO.getTitle());
|
||||||
data.put("words", assessmentWords);
|
data.put("words", assessmentWords);
|
||||||
data.put("answer", assessmentWords);
|
data.put("answer", assessmentWords);
|
||||||
|
|
||||||
|
List<Word> words1 = assessmentWords.subList(0, assessmentWords.size() / 2);
|
||||||
|
List<Word> words2 = assessmentWords.subList(assessmentWords.size() / 2, assessmentWords.size());
|
||||||
|
data.put("words1", words1);
|
||||||
|
data.put("words2", words2);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@@ -107,12 +113,17 @@ public class ExamWordsController {
|
|||||||
PageResponse<ExamWordsResultRspVO> getExamWordsResult(@RequestBody ExamWordsResultReqVO examWordsResultReqVO) {
|
PageResponse<ExamWordsResultRspVO> getExamWordsResult(@RequestBody ExamWordsResultReqVO examWordsResultReqVO) {
|
||||||
Integer page = examWordsResultReqVO.getPage();
|
Integer page = examWordsResultReqVO.getPage();
|
||||||
Integer size = examWordsResultReqVO.getSize();
|
Integer size = examWordsResultReqVO.getSize();
|
||||||
|
Integer classId = examWordsResultReqVO.getClassId();
|
||||||
|
Integer gradeId = examWordsResultReqVO.getGradeId();
|
||||||
|
String studentName = examWordsResultReqVO.getStudentName();
|
||||||
Integer total = examWordsJudgeService.getExamWordsJudgeResultCount();
|
Integer total = examWordsJudgeService.getExamWordsJudgeResultCount();
|
||||||
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(page, size);
|
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(page, size, classId, gradeId, studentName);
|
||||||
List<ExamWordsResultRspVO> list = examWordsJudgeResult.stream().map(examWordsJudgeResultDO -> ExamWordsResultRspVO
|
List<ExamWordsResultRspVO> list = examWordsJudgeResult.stream().map(examWordsJudgeResultDO -> ExamWordsResultRspVO
|
||||||
.builder()
|
.builder()
|
||||||
.id(examWordsJudgeResultDO.getId())
|
.id(examWordsJudgeResultDO.getId())
|
||||||
.studentId(examWordsJudgeResultDO.getStudentId())
|
.studentId(examWordsJudgeResultDO.getStudentId())
|
||||||
|
.studentName(examWordsJudgeResultDO.getStudentId() != null ? studentService.getStudentById(examWordsJudgeResultDO.getStudentId()).getName() : "")
|
||||||
|
.examWordsTitle(examWordsJudgeResultDO.getExamWordsId() != null ? examWordsService.getExamWordsDOById(examWordsJudgeResultDO.getExamWordsId()).getTitle() : "")
|
||||||
.examWordsId(examWordsJudgeResultDO.getExamWordsId())
|
.examWordsId(examWordsJudgeResultDO.getExamWordsId())
|
||||||
.startDate(examWordsJudgeResultDO.getStartDate())
|
.startDate(examWordsJudgeResultDO.getStartDate())
|
||||||
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
|
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.yinlihupo.enlish.service.controller;
|
package com.yinlihupo.enlish.service.controller;
|
||||||
|
|
||||||
|
import com.yinlihupo.enlish.service.constant.LessonPlanConstant;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
||||||
import com.yinlihupo.enlish.service.model.vo.plan.*;
|
import com.yinlihupo.enlish.service.model.vo.plan.*;
|
||||||
@@ -7,12 +8,14 @@ import com.yinlihupo.enlish.service.service.LessonPlansService;
|
|||||||
import com.yinlihupo.enlish.service.utils.TTSUtil;
|
import com.yinlihupo.enlish.service.utils.TTSUtil;
|
||||||
import com.yinlihupo.enlish.service.utils.WordExportUtil;
|
import com.yinlihupo.enlish.service.utils.WordExportUtil;
|
||||||
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||||
|
import com.yinlihupo.framework.common.response.PageResponse;
|
||||||
import com.yinlihupo.framework.common.response.Response;
|
import com.yinlihupo.framework.common.response.Response;
|
||||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -30,6 +33,9 @@ public class LessonPlanController {
|
|||||||
@Resource
|
@Resource
|
||||||
private LessonPlansService lessonPlanService;
|
private LessonPlansService lessonPlanService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
@Resource(name = "taskExecutor")
|
@Resource(name = "taskExecutor")
|
||||||
private Executor taskExecutor;
|
private Executor taskExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -47,6 +53,9 @@ public class LessonPlanController {
|
|||||||
Integer unitId = addLessonPlanReqVO.getUnitId();
|
Integer unitId = addLessonPlanReqVO.getUnitId();
|
||||||
Integer wordSize = addLessonPlanReqVO.getWordSize();
|
Integer wordSize = addLessonPlanReqVO.getWordSize();
|
||||||
try {
|
try {
|
||||||
|
if (redisTemplate.opsForValue().get(LessonPlanConstant.buildGeneratePlanContent(studentId)) != null) {
|
||||||
|
throw new RuntimeException("学案正在生成,请耐心等待");
|
||||||
|
}
|
||||||
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId, wordSize));
|
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId, wordSize));
|
||||||
return Response.success("生成学案成功,请等待 10 分钟");
|
return Response.success("生成学案成功,请等待 10 分钟");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -96,4 +105,31 @@ public class LessonPlanController {
|
|||||||
public void findPlanWordVoiceTTS(@RequestBody FindWordTTSVoiceReqVO findWordVoiceReqVO, HttpServletResponse response) {
|
public void findPlanWordVoiceTTS(@RequestBody FindWordTTSVoiceReqVO findWordVoiceReqVO, HttpServletResponse response) {
|
||||||
ttsUtil.generateWordVoice(findWordVoiceReqVO.getText(), response);
|
ttsUtil.generateWordVoice(findWordVoiceReqVO.getText(), response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("check")
|
||||||
|
@ApiOperationLog(description = "检测学案是否在生成")
|
||||||
|
public Response<String> checkLessonPlan(@RequestBody FindIsGeneratePlanReqVO findIsGeneratePlanReqVO) {
|
||||||
|
Integer studentId = findIsGeneratePlanReqVO.getStudentId();
|
||||||
|
String key = LessonPlanConstant.buildGeneratePlanContent(studentId);
|
||||||
|
if (redisTemplate.opsForValue().get(key) != null) {
|
||||||
|
return Response.fail();
|
||||||
|
}
|
||||||
|
return Response.success("学案生成完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("student/list")
|
||||||
|
@ApiOperationLog(description = "查询学生学案")
|
||||||
|
public Response<FindPlanStudentListRspVO> findStudentPlans(@RequestBody FindPlanStudentReqVO findPlanStudentReqVO) {
|
||||||
|
List<LessonPlansDO> lessonPlansDOS = lessonPlanService.findLessonPlansByStudentId(findPlanStudentReqVO.getStudentId());
|
||||||
|
List<LessonPlanItem> list = lessonPlansDOS.stream().map(lessonPlansDO -> LessonPlanItem
|
||||||
|
.builder()
|
||||||
|
.id(lessonPlansDO.getId())
|
||||||
|
.isFinished(0)
|
||||||
|
.title(lessonPlansDO.getTitle())
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Response.success(FindPlanStudentListRspVO.builder().lessonPlanItems(list).build());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class LoginController {
|
|||||||
@ApiOperationLog(description = "登录")
|
@ApiOperationLog(description = "登录")
|
||||||
public Response<String> login(@RequestBody LoginReqVO loginReqVO) {
|
public Response<String> login(@RequestBody LoginReqVO loginReqVO) {
|
||||||
try {
|
try {
|
||||||
loginService.login(loginReqVO.getPhone(), loginReqVO.getName(), loginReqVO.getPassword(), loginReqVO.getCode());
|
loginService.login(loginReqVO.getPhone(), loginReqVO.getName(), loginReqVO.getPassword(), loginReqVO.getCode(), loginReqVO.getInvitationCode());
|
||||||
return Response.success(StpUtil.getTokenInfo().getTokenValue());
|
return Response.success(StpUtil.getTokenInfo().getTokenValue());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("注册或登录失败 {}", e.getMessage());
|
log.error("注册或登录失败 {}", e.getMessage());
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ public class StudentController {
|
|||||||
.id(studentDO.getId())
|
.id(studentDO.getId())
|
||||||
.name(studentDO.getName())
|
.name(studentDO.getName())
|
||||||
.classId(studentDO.getClassId())
|
.classId(studentDO.getClassId())
|
||||||
|
.className(classService.findClassById(studentDO.getClassId()).getTitle())
|
||||||
|
.gradeName(gradeService.findByClassId(studentDO.getGradeId()).getTitle())
|
||||||
.gradeId(studentDO.getGradeId())
|
.gradeId(studentDO.getGradeId())
|
||||||
.build()).toList();
|
.build()).toList();
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
package com.yinlihupo.enlish.service.controller;
|
package com.yinlihupo.enlish.service.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
|
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
|
||||||
|
import com.yinlihupo.enlish.service.model.vo.user.UpdateUserInfoReqVO;
|
||||||
import com.yinlihupo.enlish.service.service.UserService;
|
import com.yinlihupo.enlish.service.service.UserService;
|
||||||
|
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||||
import com.yinlihupo.framework.common.response.Response;
|
import com.yinlihupo.framework.common.response.Response;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/user/")
|
@RequestMapping("/user/")
|
||||||
|
@Slf4j
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -27,4 +33,23 @@ public class UserController {
|
|||||||
return Response.success(findUserInfoRspVO);
|
return Response.success(findUserInfoRspVO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("update-user-info")
|
||||||
|
@ApiOperationLog(description = "修改用户信息")
|
||||||
|
public Response<String> updatePassword(@RequestBody UpdateUserInfoReqVO updateUserInfoReqVO) {
|
||||||
|
try {
|
||||||
|
String code = updateUserInfoReqVO.getCode();
|
||||||
|
String newPassword = updateUserInfoReqVO.getNewPassword();
|
||||||
|
String phone = updateUserInfoReqVO.getPhone();
|
||||||
|
String name = updateUserInfoReqVO.getName();
|
||||||
|
userService.updateUserInfo(newPassword, code, phone, name);
|
||||||
|
|
||||||
|
StpUtil.logout();
|
||||||
|
|
||||||
|
return Response.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("修改密码失败 {}", e.getMessage());
|
||||||
|
return Response.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.yinlihupo.enlish.service.domain.dataobject;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class PlanExamDO {
|
||||||
|
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
private Integer planId;
|
||||||
|
|
||||||
|
private Integer examId;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,9 +19,13 @@ public interface ExamWordsJudgeResultDOMapper {
|
|||||||
|
|
||||||
Integer selectCount();
|
Integer selectCount();
|
||||||
|
|
||||||
|
Integer selectUnfinishedCount();
|
||||||
|
|
||||||
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
|
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDO> selectByStudentId(@Param("studentId") Integer studentId);
|
List<ExamWordsJudgeResultDO> selectByStudentId(@Param("studentId") Integer studentId);
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
|
List<ExamWordsJudgeResultDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
|
||||||
|
|
||||||
|
List<ExamWordsJudgeResultDO> selectByPageAndStudentIds(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize, @Param("studentIds") List<Integer> studentIds);
|
||||||
}
|
}
|
||||||
@@ -14,4 +14,6 @@ public interface LessonPlansDOMapper {
|
|||||||
List<LessonPlansDO> findLessonPlansByStudentId(@Param("ids") List<Integer> ids);
|
List<LessonPlansDO> findLessonPlansByStudentId(@Param("ids") List<Integer> ids);
|
||||||
|
|
||||||
LessonPlansDO selectByLessonId(@Param("lessonId") Integer lessonId);
|
LessonPlansDO selectByLessonId(@Param("lessonId") Integer lessonId);
|
||||||
|
|
||||||
|
List<LessonPlansDO> selectByStudentId(@Param("studentId") Integer studentId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.yinlihupo.enlish.service.domain.mapper;
|
||||||
|
|
||||||
|
import com.yinlihupo.enlish.service.domain.dataobject.PlanExamDO;
|
||||||
|
|
||||||
|
public interface PlanExamDOMapper {
|
||||||
|
void insert(PlanExamDO record);
|
||||||
|
|
||||||
|
PlanExamDO selectByExamId(Integer examId);
|
||||||
|
}
|
||||||
@@ -23,4 +23,10 @@ public interface StudentDOMapper {
|
|||||||
int selectStudentCountByClassId(@Param("classId") Integer classId);
|
int selectStudentCountByClassId(@Param("classId") Integer classId);
|
||||||
|
|
||||||
int updateStudentActualGradeId(@Param("studentId") Integer studentId, @Param("gradeId") Integer gradeId);
|
int updateStudentActualGradeId(@Param("studentId") Integer studentId, @Param("gradeId") Integer gradeId);
|
||||||
|
|
||||||
|
List<StudentDO> selectStudentDOListByClassId(@Param("classId") Integer classId);
|
||||||
|
|
||||||
|
List<StudentDO> selectStudentDOListByGradeId(@Param("gradeId") Integer gradeId);
|
||||||
|
|
||||||
|
List<StudentDO> selectStudentDOListByName(@Param("name") String name);
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,10 @@ public interface UserDOMapper {
|
|||||||
|
|
||||||
void insert(UserDO userDO);
|
void insert(UserDO userDO);
|
||||||
|
|
||||||
|
void updatePassword(@Param("id") Long id, @Param("password") String password);
|
||||||
|
|
||||||
|
void updateUserInfo(@Param("id") Long id, @Param("name") String name, @Param("password") String password, @Param("phone") String phone);
|
||||||
|
|
||||||
UserDO selectById(Long id);
|
UserDO selectById(Long id);
|
||||||
|
|
||||||
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ public interface VocabularyBankDOMapper {
|
|||||||
Integer selectWordTotal();
|
Integer selectWordTotal();
|
||||||
|
|
||||||
List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
|
List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
|
||||||
|
|
||||||
|
List<VocabularyBankDO> selectByGradeIdAndNotMatchIds(@Param("gradeId") Integer gradeId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.yinlihupo.enlish.service.model.bo;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class Sentence {
|
||||||
|
|
||||||
|
@JsonProperty("target_word")
|
||||||
|
private String targetWord;
|
||||||
|
@JsonProperty("grade_level")
|
||||||
|
private String gradeLevel;
|
||||||
|
@JsonProperty("question")
|
||||||
|
private String question;
|
||||||
|
@JsonProperty("chinese_clue")
|
||||||
|
private String chineseClue;
|
||||||
|
@JsonProperty("correct_answer")
|
||||||
|
private String correctAnswer;
|
||||||
|
@JsonProperty("grammar_point")
|
||||||
|
private String grammarPoint;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yinlihupo.enlish.service.model.vo.admin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class CreateInvitationCodeReqVO {
|
||||||
|
|
||||||
|
// 限制人数
|
||||||
|
private Integer limit;
|
||||||
|
// 有效期
|
||||||
|
private Integer expire;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.yinlihupo.enlish.service.model.vo.admin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class CreateInvitationCodeRspVO {
|
||||||
|
|
||||||
|
private String invitationCode;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -13,4 +13,8 @@ public class ExamWordsResultReqVO {
|
|||||||
|
|
||||||
private Integer page;
|
private Integer page;
|
||||||
private Integer size;
|
private Integer size;
|
||||||
|
|
||||||
|
private Integer classId;
|
||||||
|
private Integer gradeId;
|
||||||
|
private String studentName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.time.LocalDateTime;
|
|||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
public class ExamWordsResultRspVO {
|
public class ExamWordsResultRspVO {
|
||||||
|
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
||||||
private String ansSheetPath;
|
private String ansSheetPath;
|
||||||
@@ -20,6 +21,10 @@ public class ExamWordsResultRspVO {
|
|||||||
|
|
||||||
private Integer examWordsId;
|
private Integer examWordsId;
|
||||||
|
|
||||||
|
private String studentName;
|
||||||
|
|
||||||
|
private String examWordsTitle;
|
||||||
|
|
||||||
private Integer correctWordCount;
|
private Integer correctWordCount;
|
||||||
|
|
||||||
private Integer wrongWordCount;
|
private Integer wrongWordCount;
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ public class LoginReqVO {
|
|||||||
private String name;
|
private String name;
|
||||||
private String password;
|
private String password;
|
||||||
private String code;
|
private String code;
|
||||||
|
private String invitationCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 FindIsGeneratePlanReqVO {
|
||||||
|
|
||||||
|
private Integer studentId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.yinlihupo.enlish.service.model.vo.plan;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class FindPlanStudentListRspVO {
|
||||||
|
|
||||||
|
List<LessonPlanItem> lessonPlanItems;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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 FindPlanStudentReqVO {
|
||||||
|
private Integer studentId;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.yinlihupo.enlish.service.model.vo.student;
|
package com.yinlihupo.enlish.service.model.vo.student;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -16,5 +17,6 @@ public class AddStudentReqVO {
|
|||||||
private String name;
|
private String name;
|
||||||
private Integer classId;
|
private Integer classId;
|
||||||
private Integer gradeId;
|
private Integer gradeId;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,6 @@ public class StudentItemRspVO {
|
|||||||
private String name;
|
private String name;
|
||||||
private Integer classId;
|
private Integer classId;
|
||||||
private Integer gradeId;
|
private Integer gradeId;
|
||||||
|
private String className;
|
||||||
|
private String gradeName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yinlihupo.enlish.service.model.vo.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Data
|
||||||
|
public class UpdateUserInfoReqVO {
|
||||||
|
|
||||||
|
private String newPassword;
|
||||||
|
private String name;
|
||||||
|
private String phone;
|
||||||
|
private String code;
|
||||||
|
}
|
||||||
@@ -9,10 +9,12 @@ public interface ExamWordsJudgeService {
|
|||||||
|
|
||||||
void judgeExamWords(int count);
|
void judgeExamWords(int count);
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize);
|
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize, Integer classId, Integer gradeId, String studentName);
|
||||||
|
|
||||||
Integer getExamWordsJudgeResultCount();
|
Integer getExamWordsJudgeResultCount();
|
||||||
|
|
||||||
|
Integer getExamUnfinishedCount();
|
||||||
|
|
||||||
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
|
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId);
|
List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId);
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ public interface ExamWordsService {
|
|||||||
int saveExamWordsPngToDbAndLocal(MultipartFile file);
|
int saveExamWordsPngToDbAndLocal(MultipartFile file);
|
||||||
|
|
||||||
void updateExamWordsWordIdsOrder(ExamWordsDO examWordsDO);
|
void updateExamWordsWordIdsOrder(ExamWordsDO examWordsDO);
|
||||||
|
|
||||||
|
ExamWordsDO getExamWordsDOById(Integer id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ public interface LessonPlansService {
|
|||||||
List<LessonPlansDO> findLessonPlans(List<Integer> ids);
|
List<LessonPlansDO> findLessonPlans(List<Integer> ids);
|
||||||
|
|
||||||
LessonPlansDO findLessonPlanById(Integer id);
|
LessonPlansDO findLessonPlanById(Integer id);
|
||||||
|
|
||||||
|
List<LessonPlansDO> findLessonPlansByStudentId(Integer studentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.yinlihupo.enlish.service.service;
|
|||||||
|
|
||||||
public interface LoginService {
|
public interface LoginService {
|
||||||
|
|
||||||
void login(String phone, String name, String reqPassword, String reqCode);
|
void login(String phone, String name, String reqPassword, String reqCode, String invitationCode);
|
||||||
|
|
||||||
void sendVerificationCode(String phone);
|
void sendVerificationCode(String phone);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ public interface UserService {
|
|||||||
Integer findUserTotal();
|
Integer findUserTotal();
|
||||||
|
|
||||||
void createUser(UserDO userDO);
|
void createUser(UserDO userDO);
|
||||||
|
|
||||||
|
void updateUserInfo(String password, String reqCode, String phone, String name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,17 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -47,7 +48,6 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
@Transactional(rollbackFor = RuntimeException.class)
|
@Transactional(rollbackFor = RuntimeException.class)
|
||||||
public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
|
public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
|
||||||
|
|
||||||
|
|
||||||
ExamWordsDO examWordsDO;
|
ExamWordsDO examWordsDO;
|
||||||
|
|
||||||
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
|
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
|
||||||
@@ -61,6 +61,16 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
examWordsDO = generateFinalExamWords(studentId);
|
examWordsDO = generateFinalExamWords(studentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Integer> wordIds = new ArrayList<>(examWordsDO.getWordIds());
|
||||||
|
if (wordIds.size() < wordCount) {
|
||||||
|
log.info("单词数量不足,补充单词");
|
||||||
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
|
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectByGradeIdAndNotMatchIds(studentDO.getGradeId(), wordIds, wordCount - wordIds.size());
|
||||||
|
List<Integer> list = new ArrayList<>(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList());
|
||||||
|
wordIds.addAll(list);
|
||||||
|
examWordsDO.setWordIds(wordIds);
|
||||||
|
}
|
||||||
|
|
||||||
return examWordsDO;
|
return examWordsDO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +119,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
Integer gradeId = studentDO.getGradeId();
|
Integer gradeId = studentDO.getGradeId();
|
||||||
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId) + "上");
|
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId) + "上");
|
||||||
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS);
|
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS, ExamWordsConstant.EXAM_TYPE_MIDTERM);
|
||||||
examWordsDO.setTitle("期中测试" + studentDO.getName());
|
examWordsDO.setTitle("期中测试" + studentDO.getName());
|
||||||
return getExamWordsDO(studentId, examWordsDO);
|
return getExamWordsDO(studentId, examWordsDO);
|
||||||
|
|
||||||
@@ -119,13 +129,13 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
Integer gradeId = studentDO.getGradeId();
|
Integer gradeId = studentDO.getGradeId();
|
||||||
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId));
|
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId));
|
||||||
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS);
|
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS, ExamWordsConstant.EXAM_TYPE_FINAL);
|
||||||
examWordsDO.setTitle("期末测试" + studentDO.getName());
|
examWordsDO.setTitle("期末测试" + studentDO.getName());
|
||||||
return getExamWordsDO(studentId, examWordsDO);
|
return getExamWordsDO(studentId, examWordsDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private ExamWordsDO getExamWordsDO(Integer studentId, StudentDO studentDO, Integer gradeId, List<UnitDO> unitDOS) {
|
private ExamWordsDO getExamWordsDO(Integer studentId, StudentDO studentDO, Integer gradeId, List<UnitDO> unitDOS, Integer type) {
|
||||||
if (unitDOS.isEmpty()) {
|
if (unitDOS.isEmpty()) {
|
||||||
throw new RuntimeException("没有找到对应的单元");
|
throw new RuntimeException("没有找到对应的单元");
|
||||||
}
|
}
|
||||||
@@ -134,7 +144,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
||||||
.gradeId(gradeId)
|
.gradeId(gradeId)
|
||||||
.level(1)
|
.level(1)
|
||||||
.type(ExamWordsConstant.EXAM_TYPE_BASELINE)
|
.type(type)
|
||||||
.title(studentDO.getName())
|
.title(studentDO.getName())
|
||||||
.createdAt(LocalDateTime.now())
|
.createdAt(LocalDateTime.now())
|
||||||
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
|
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
|
||||||
@@ -161,29 +171,55 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = RuntimeException.class)
|
@Transactional(rollbackFor = RuntimeException.class)
|
||||||
public int saveExamWordsPngToDbAndLocal(MultipartFile file) {
|
public int saveExamWordsPngToDbAndLocal(MultipartFile file) {
|
||||||
|
// 1. 基础校验:判空
|
||||||
File dir = new File(tmpPng);
|
if (file == null || file.isEmpty()) {
|
||||||
if (!dir.exists()) {
|
throw new RuntimeException("上传文件不能为空");
|
||||||
dir.mkdirs();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 安全校验:检查后缀名白名单
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String extension = StringUtils.getFilenameExtension(originalFilename); // Spring工具类
|
||||||
|
List<String> allowedExtensions = Arrays.asList("png", "jpg", "jpeg");
|
||||||
|
|
||||||
|
if (extension == null || !allowedExtensions.contains(extension.toLowerCase())) {
|
||||||
|
throw new RuntimeException("不支持的文件格式,仅支持: " + allowedExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 准备目录 (使用 NIO)
|
||||||
|
// 假设 tmpPng 是配置好的基础路径字符串
|
||||||
|
Path directoryPath = Paths.get(tmpPng);
|
||||||
try {
|
try {
|
||||||
String originalFilename = file.getOriginalFilename();
|
if (!Files.exists(directoryPath)) {
|
||||||
String suffix = "";
|
Files.createDirectories(directoryPath);
|
||||||
if (originalFilename != null && originalFilename.contains(".")) {
|
|
||||||
suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
|
|
||||||
}
|
}
|
||||||
String newFileName = UUID.randomUUID() + suffix;
|
|
||||||
String path = tmpPng + newFileName;
|
|
||||||
|
|
||||||
File dest = new File(path);
|
// 4. 生成文件名 (防止文件名冲突)
|
||||||
file.transferTo(dest);
|
String newFileName = UUID.randomUUID().toString() + "." + extension;
|
||||||
|
|
||||||
|
// 5. 组合最终路径 (自动处理分隔符)
|
||||||
|
Path targetPath = directoryPath.resolve(newFileName);
|
||||||
|
|
||||||
|
// 6. 保存文件
|
||||||
|
file.transferTo(targetPath.toAbsolutePath().toFile());
|
||||||
|
String string = targetPath.toAbsolutePath().toFile().toString();
|
||||||
|
log.info("文件上传成功路径为 {}", string);
|
||||||
|
if (!targetPath.toFile().exists()) {
|
||||||
|
log.error("文件上传失败: {}", newFileName);
|
||||||
|
throw new RuntimeException("文件上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 入库
|
||||||
|
// 建议:存相对路径或文件名,不要存 targetPath.toString() 的绝对路径
|
||||||
|
// 这里为了演示,假设 insert 依然接受字符串,建议存 newFileName
|
||||||
|
int insert = examWordsJudgeResultDOMapper.insert(targetPath.toString());
|
||||||
|
|
||||||
|
log.info("上传文件成功: {}", newFileName);
|
||||||
|
return insert;
|
||||||
|
|
||||||
return examWordsJudgeResultDOMapper.insert(path);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("上传失败", e);
|
log.error("文件上传失败: {}", originalFilename, e);
|
||||||
|
throw new RuntimeException("上传失败,请稍后重试", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -191,5 +227,10 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
examWordsDOMapper.updateWordIdsOrder(examWordsDO);
|
examWordsDOMapper.updateWordIdsOrder(examWordsDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExamWordsDO getExamWordsDOById(Integer id) {
|
||||||
|
return examWordsDOMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.yinlihupo.enlish.service.model.bo.exam.ActionType;
|
|||||||
import com.yinlihupo.enlish.service.model.bo.exam.DiagnosisResult;
|
import com.yinlihupo.enlish.service.model.bo.exam.DiagnosisResult;
|
||||||
import com.yinlihupo.enlish.service.model.bo.exam.ZoneStats;
|
import com.yinlihupo.enlish.service.model.bo.exam.ZoneStats;
|
||||||
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
||||||
|
import com.yinlihupo.enlish.service.service.StudentLessonPlansService;
|
||||||
import com.yinlihupo.enlish.service.utils.PngUtil;
|
import com.yinlihupo.enlish.service.utils.PngUtil;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -40,23 +41,29 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
private GradeUnitDOMapper gradeUnitDOMapper;
|
private GradeUnitDOMapper gradeUnitDOMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private StudentDOMapper studentDOMapper;
|
private StudentDOMapper studentDOMapper;
|
||||||
|
@Resource
|
||||||
|
private PlanExamDOMapper planExamDOMapper;
|
||||||
|
@Resource
|
||||||
|
private StudentLessonPlansService studentLessonPlansService;
|
||||||
|
|
||||||
@Value("${templates.data}")
|
@Value("${templates.data}")
|
||||||
private String tessdataPath;
|
private String tessdataPath;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void judgeExamWords(int count) {
|
public void judgeExamWords(int count) {
|
||||||
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
|
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
|
||||||
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
|
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
|
||||||
|
String ansSheetPath = null;
|
||||||
try {
|
try {
|
||||||
String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
|
ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
|
||||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
||||||
|
|
||||||
// 从图片中获取学生 id 和考试 id
|
// 从图片中获取学生 id 和考试 id
|
||||||
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
||||||
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
|
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
|
||||||
if (studentExamId == null) {
|
if (studentExamId == null) {
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生 id 和考试 id");
|
log.info("未找到学生 id 和考试 id");
|
||||||
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生和考试");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +81,7 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
||||||
if(examWordsDO == null) {
|
if (examWordsDO == null) {
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -93,9 +100,13 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
.wrongWordCount(unmemorizedWordIds.size())
|
.wrongWordCount(unmemorizedWordIds.size())
|
||||||
.isFinished(1)
|
.isFinished(1)
|
||||||
.build();
|
.build();
|
||||||
// 判断考试等级
|
|
||||||
judgeExamActualGrade(wordsJudgeResultDO, examWordsDO);
|
|
||||||
|
|
||||||
|
if (examWordsDO.getType().equals(ExamWordsConstant.EXAM_TYPE_BASELINE)) {
|
||||||
|
// 判断考试等级
|
||||||
|
judgeExamActualGrade(wordsJudgeResultDO, examWordsDO);
|
||||||
|
} else {
|
||||||
|
wordsJudgeResultDO.setMsg("此次考试" + examWordsDO.getTitle() + "答对单词数为" + memorizedWordIds.size());
|
||||||
|
}
|
||||||
int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO);
|
int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO);
|
||||||
if (updated != 1) {
|
if (updated != 1) {
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新考试记录失败");
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新考试记录失败");
|
||||||
@@ -103,6 +114,12 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
}
|
}
|
||||||
log.info("更新考试记录成功");
|
log.info("更新考试记录成功");
|
||||||
|
|
||||||
|
PlanExamDO planExamDO = planExamDOMapper.selectByExamId(examWordsId);
|
||||||
|
if (planExamDO != null) {
|
||||||
|
studentLessonPlansService.finishStudentLessonPlan(studentId, planExamDO.getPlanId());
|
||||||
|
log.info("完成学案成功, planId: {}", planExamDO.getPlanId());
|
||||||
|
}
|
||||||
|
|
||||||
List<WordMasteryLogDO> wordMasteryLogDOS = new ArrayList<>(unmemorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder()
|
List<WordMasteryLogDO> wordMasteryLogDOS = new ArrayList<>(unmemorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder()
|
||||||
.wordId(wordId)
|
.wordId(wordId)
|
||||||
.studentId(studentId)
|
.studentId(studentId)
|
||||||
@@ -131,9 +148,14 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
boolean delete = new File(ansSheetPath).delete();
|
boolean delete = new File(ansSheetPath).delete();
|
||||||
if (delete) {
|
if (delete) {
|
||||||
log.info("删除文件成功:{}", ansSheetPath);
|
log.info("删除文件成功:{}", ansSheetPath);
|
||||||
|
} else {
|
||||||
|
log.error("删除文件失败:{}", ansSheetPath);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("识别考试失败 {}", e.getMessage());
|
log.error("识别考试失败 {}", e.getMessage());
|
||||||
|
if (ansSheetPath != null) {
|
||||||
|
new File(ansSheetPath).delete();
|
||||||
|
}
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage());
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,8 +332,24 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize) {
|
public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize, Integer classId, Integer gradeId, String studentName) {
|
||||||
return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize);
|
|
||||||
|
List<Integer> studentIds = new ArrayList<>();
|
||||||
|
if (classId != null) {
|
||||||
|
studentIds.addAll(studentDOMapper.selectStudentDOListByClassId(classId).stream().map(StudentDO::getId).toList());
|
||||||
|
}
|
||||||
|
if (gradeId != null) {
|
||||||
|
studentIds.addAll(studentDOMapper.selectStudentDOListByGradeId(gradeId).stream().map(StudentDO::getId).toList());
|
||||||
|
}
|
||||||
|
if (!studentName.isEmpty()) {
|
||||||
|
studentIds.addAll(studentDOMapper.selectStudentDOListByName(studentName).stream().map(StudentDO::getId).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (studentIds.isEmpty()) {
|
||||||
|
return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize);
|
||||||
|
} else {
|
||||||
|
return examWordsJudgeResultDOMapper.selectByPageAndStudentIds((page - 1) * pageSize, page * pageSize, studentIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -319,6 +357,11 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
return examWordsJudgeResultDOMapper.selectCount();
|
return examWordsJudgeResultDOMapper.selectCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getExamUnfinishedCount() {
|
||||||
|
return examWordsJudgeResultDOMapper.selectUnfinishedCount();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
|
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
|
||||||
return examWordsJudgeResultDOMapper.selectDetailById(id);
|
return examWordsJudgeResultDOMapper.selectDetailById(id);
|
||||||
|
|||||||
@@ -13,11 +13,15 @@ import com.yinlihupo.enlish.service.service.LoginService;
|
|||||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -28,20 +32,32 @@ public class LoginServiceImpl implements LoginService {
|
|||||||
@Resource
|
@Resource
|
||||||
private PasswordEncoder passwordEncoder;
|
private PasswordEncoder passwordEncoder;
|
||||||
@Resource
|
@Resource
|
||||||
private StringRedisTemplate stringRedisTemplate;
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
@Resource
|
@Resource
|
||||||
private Client client;
|
private Client client;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void login(String phone, String name, String reqPassword, String reqCode) {
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void login(String phone, String name, String reqPassword, String reqCode, String invitationCode) {
|
||||||
UserDO userDO = userDOMapper.selectByPhone(phone);
|
UserDO userDO = userDOMapper.selectByPhone(phone);
|
||||||
log.info("userDO:{}", userDO);
|
log.info("userDO:{}", userDO);
|
||||||
|
|
||||||
String code = stringRedisTemplate.opsForValue().get(UserRedisConstants.buildUserLoginCode(phone));
|
String code = JsonUtils.toJsonString(redisTemplate.opsForValue().get(UserRedisConstants.buildUserLoginCode(phone)));
|
||||||
if (userDO == null) {
|
if (userDO == null) {
|
||||||
if (code == null || !code.equals(reqCode)) {
|
if (code == null || !code.equals(reqCode)) {
|
||||||
throw new RuntimeException("验证码错误");
|
throw new RuntimeException("验证码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object invitationObj = redisTemplate.opsForValue().get(UserRedisConstants.buildUserInvitationCode(invitationCode));
|
||||||
|
if (invitationObj == null) {
|
||||||
|
throw new RuntimeException("邀请码错误");
|
||||||
|
}
|
||||||
|
int invitationLimit = Integer.parseInt(JsonUtils.toJsonString(invitationObj));
|
||||||
|
if (invitationLimit <= 0) {
|
||||||
|
throw new RuntimeException("邀请码已使用完毕");
|
||||||
|
}
|
||||||
|
redisTemplate.opsForValue().set(UserRedisConstants.buildUserInvitationCode(invitationCode), invitationLimit - 1);
|
||||||
|
|
||||||
userDO = UserDO.builder()
|
userDO = UserDO.builder()
|
||||||
.phone(phone)
|
.phone(phone)
|
||||||
.name(name)
|
.name(name)
|
||||||
@@ -68,7 +84,9 @@ public class LoginServiceImpl implements LoginService {
|
|||||||
@Override
|
@Override
|
||||||
public void sendVerificationCode(String phone) {
|
public void sendVerificationCode(String phone) {
|
||||||
String code = RandomUtil.randomNumbers(6);
|
String code = RandomUtil.randomNumbers(6);
|
||||||
stringRedisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
|
String key = UserRedisConstants.buildUserLoginCode(phone);
|
||||||
|
redisTemplate.opsForValue().set(key, code);
|
||||||
|
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
|
||||||
String signName = "短信测试";
|
String signName = "短信测试";
|
||||||
String templateCode = "SMS_154950909";
|
String templateCode = "SMS_154950909";
|
||||||
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
package com.yinlihupo.enlish.service.service.plan;
|
package com.yinlihupo.enlish.service.service.plan;
|
||||||
|
|
||||||
|
import com.yinlihupo.enlish.service.constant.ExamWordsConstant;
|
||||||
import com.yinlihupo.enlish.service.constant.LessonPlanConstant;
|
import com.yinlihupo.enlish.service.constant.LessonPlanConstant;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.*;
|
import com.yinlihupo.enlish.service.domain.dataobject.*;
|
||||||
import com.yinlihupo.enlish.service.domain.mapper.*;
|
import com.yinlihupo.enlish.service.domain.mapper.*;
|
||||||
|
import com.yinlihupo.enlish.service.model.bo.Sentence;
|
||||||
|
import com.yinlihupo.enlish.service.model.bo.Word;
|
||||||
import com.yinlihupo.enlish.service.service.LessonPlansService;
|
import com.yinlihupo.enlish.service.service.LessonPlansService;
|
||||||
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
|
import com.yinlihupo.enlish.service.utils.DifyClient;
|
||||||
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
|
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
|
||||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -35,17 +39,34 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
@Resource
|
@Resource
|
||||||
private GradeDOMapper gradeDOMapper;
|
private GradeDOMapper gradeDOMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private DifyArticleClient difyArticleClient;
|
private DifyClient difyClient;
|
||||||
|
@Resource
|
||||||
|
private ExamWordsDOMapper examWordsDOMapper;
|
||||||
|
@Resource
|
||||||
|
private StudentExamWordsDOMapper studentExamWordsDOMapper;
|
||||||
|
@Resource
|
||||||
|
private StudentDOMapper studentDOMapper;
|
||||||
|
@Resource
|
||||||
|
private PlanExamDOMapper planExamDOMapper;
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
@Resource
|
||||||
|
private ClassDOMapper classDOMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) {
|
public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) {
|
||||||
|
String key = LessonPlanConstant.buildGeneratePlanContent(studentId);
|
||||||
|
redisTemplate.opsForValue().set(key, studentId);
|
||||||
|
redisTemplate.expire(key, 7, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
log.info("开始生成计划");
|
||||||
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId);
|
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId);
|
||||||
UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId);
|
UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId);
|
||||||
GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId);
|
GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId);
|
||||||
GradeDO gradeDO = gradeDOMapper.selectById(gradeUnitDO.getGradeId());
|
GradeDO gradeDO = gradeDOMapper.selectById(gradeUnitDO.getGradeId());
|
||||||
|
|
||||||
|
List<VocabularyBankDO> totalWords = new ArrayList<>();
|
||||||
|
|
||||||
// 补差词汇所用词汇的
|
// 补差词汇所用词汇的
|
||||||
List<VocabularyBankDO> vocabularyBankListStudentNotMaster = getVocabListRandom(vocabularyBankDOMapper
|
List<VocabularyBankDO> vocabularyBankListStudentNotMaster = getVocabListRandom(vocabularyBankDOMapper
|
||||||
.selectVocabularyBankListStudentNotMaster(gradeUnitDO.getGradeId(), studentId), 50);
|
.selectVocabularyBankListStudentNotMaster(gradeUnitDO.getGradeId(), studentId), 50);
|
||||||
@@ -57,18 +78,21 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
int checkTotal = 50;
|
int checkTotal = 50;
|
||||||
List<List<VocabularyBankDO>> weeksSync = new ArrayList<>();
|
List<List<VocabularyBankDO>> weeksSync = new ArrayList<>();
|
||||||
List<List<VocabularyBankDO>> weeksGap = new ArrayList<>();
|
List<List<VocabularyBankDO>> weeksGap = new ArrayList<>();
|
||||||
|
int j = 0;
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
List<VocabularyBankDO> syncVocabList;
|
List<VocabularyBankDO> syncVocabList;
|
||||||
if ((i + 1) * wordSize < syncSize) {
|
if ((i + 1) * wordSize < syncSize) {
|
||||||
syncVocabList = vocabularyBankDOS.subList(i * wordSize, (i + 1) * wordSize);
|
syncVocabList = vocabularyBankDOS.subList(i * wordSize, (i + 1) * wordSize);
|
||||||
} else if (i == 4) {
|
|
||||||
syncVocabList = vocabularyBankDOS.subList(i * wordSize, syncSize);
|
|
||||||
} else {
|
} else {
|
||||||
syncVocabList = vocabularyBankDOS.subList((syncSize - i) * wordSize, Math.min((syncSize - i + 1) * wordSize, syncSize));
|
syncVocabList = new ArrayList<>(weeksSync.get(Math.min(j++, weeksSync.size() - 1)));
|
||||||
}
|
}
|
||||||
List<VocabularyBankDO> gapVocabList = vocabularyBankListStudentNotMaster.subList(i * countGap, Math.min(i * countGap + countGap, gapSize));
|
List<VocabularyBankDO> gapVocabList = vocabularyBankListStudentNotMaster.subList(i * countGap, Math.min(i * countGap + countGap, gapSize));
|
||||||
weeksSync.add(syncVocabList);
|
weeksSync.add(syncVocabList);
|
||||||
weeksGap.add(gapVocabList);
|
weeksGap.add(gapVocabList);
|
||||||
|
|
||||||
|
totalWords.addAll(syncVocabList);
|
||||||
|
totalWords.addAll(gapVocabList);
|
||||||
|
|
||||||
List<VocabularyBankDO> reviewVocabList = new ArrayList<>();
|
List<VocabularyBankDO> reviewVocabList = new ArrayList<>();
|
||||||
List<VocabularyBankDO> checkList = new ArrayList<>();
|
List<VocabularyBankDO> checkList = new ArrayList<>();
|
||||||
// 艾宾浩斯遗忘曲线
|
// 艾宾浩斯遗忘曲线
|
||||||
@@ -118,21 +142,29 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
.build();
|
.build();
|
||||||
studentLessonPlansDOMapper.insert(studentLessonPlansDO);
|
studentLessonPlansDOMapper.insert(studentLessonPlansDO);
|
||||||
|
|
||||||
|
Integer examId = (Integer) lessonPlanMap.get("examId");
|
||||||
|
PlanExamDO planExamDO = PlanExamDO.builder()
|
||||||
|
.planId(lessonPlansDO.getId())
|
||||||
|
.examId(examId)
|
||||||
|
.build();
|
||||||
|
planExamDOMapper.insert(planExamDO);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
log.info("生成第{}天计划失败,失败原因 {}", i + 1, e.getMessage());
|
||||||
}
|
}
|
||||||
log.info("生成第{}天计划成功", i + 1);
|
log.info("生成第{}天计划成功", i + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int syncWeekender = syncSize / 2;
|
log.info("开始生成周末计划");
|
||||||
|
int syncWeekendSize = totalWords.size() / 2;
|
||||||
for (int i = 0; i < 2; i++) {
|
for (int i = 0; i < 2; i++) {
|
||||||
List<VocabularyBankDO> checkList = vocabularyBankDOS.subList(i * syncWeekender, Math.min((i + 1) * syncWeekender, syncSize));
|
List<VocabularyBankDO> checkList = totalWords.subList(i * syncWeekendSize, Math.min((i + 1) * syncWeekendSize, syncWeekendSize));
|
||||||
Map<String, Object> map = generateWeekendPlans(checkList, i + 6, gradeDO, unitDO, studentId);
|
Map<String, Object> map = generateWeekendPlans(checkList, i + 6, gradeDO, unitDO, studentId);
|
||||||
|
|
||||||
LessonPlansDO lessonPlansDO = LessonPlansDO.builder()
|
LessonPlansDO lessonPlansDO = LessonPlansDO.builder()
|
||||||
.title(map.get("title").toString())
|
.title(map.get("examStr").toString() + "复习")
|
||||||
.gradeId(gradeDO.getId().toString())
|
.gradeId(gradeDO.getId().toString())
|
||||||
.unitId(unitDO.getId())
|
.unitId(unitDO.getId())
|
||||||
.createdAt(LocalDateTime.now())
|
.createdAt(LocalDateTime.now())
|
||||||
@@ -140,6 +172,13 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
.build();
|
.build();
|
||||||
lessonPlansDOMapper.insert(lessonPlansDO);
|
lessonPlansDOMapper.insert(lessonPlansDO);
|
||||||
|
|
||||||
|
Integer examId = (Integer) map.get("examId");
|
||||||
|
PlanExamDO planExamDO = PlanExamDO.builder()
|
||||||
|
.planId(lessonPlansDO.getId())
|
||||||
|
.examId(examId)
|
||||||
|
.build();
|
||||||
|
planExamDOMapper.insert(planExamDO);
|
||||||
|
|
||||||
StudentLessonPlansDO studentLessonPlansDO = StudentLessonPlansDO.builder()
|
StudentLessonPlansDO studentLessonPlansDO = StudentLessonPlansDO.builder()
|
||||||
.studentId(studentId)
|
.studentId(studentId)
|
||||||
.planId(lessonPlansDO.getId())
|
.planId(lessonPlansDO.getId())
|
||||||
@@ -162,14 +201,43 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
return lessonPlansDOMapper.selectByLessonId(id);
|
return lessonPlansDOMapper.selectByLessonId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LessonPlansDO> findLessonPlansByStudentId(Integer studentId) {
|
||||||
|
return lessonPlansDOMapper.selectByStudentId(studentId);
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> generateWeekendPlans(List<VocabularyBankDO> checkList,
|
|
||||||
|
private Map<String, Object> generateWeekendPlans(List<VocabularyBankDO> words,
|
||||||
int day,
|
int day,
|
||||||
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws IOException {
|
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws IOException {
|
||||||
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("title", "第" + day + "天" + "复习" + gradeDO.getTitle() + unitDO.getTitle() + studentId);
|
words.forEach(word -> word.setDefinition(word.getDefinition().length() > 5 ? word.getDefinition().substring(0, 5) : word.getDefinition()));
|
||||||
data.put("checkList", checkList);
|
List<Integer> wordIds = words.stream().map(VocabularyBankDO::getId).toList();
|
||||||
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
|
|
||||||
|
String ExamTitle = gradeDO.getTitle() + unitDO.getTitle() + "教案小测第" + ExamWordsConstant.day2Chinese(day) + "天" + studentDO.getName();
|
||||||
|
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
||||||
|
.gradeId(gradeDO.getId())
|
||||||
|
.level(1)
|
||||||
|
.wordIds(wordIds)
|
||||||
|
.type(ExamWordsConstant.EXAM_TYPE_TEST)
|
||||||
|
.title(ExamTitle)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
examWordsDOMapper.insert(examWordsDO);
|
||||||
|
studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
|
||||||
|
|
||||||
|
ClassDO classDO = classDOMapper.selectClassDOById(studentDOMapper.selectStudentById(studentId).getClassId());
|
||||||
|
data.put("examId", examWordsDO.getId());
|
||||||
|
data.put("studentId", studentId);
|
||||||
|
data.put("studentStr", gradeDO.getTitle() + " " + classDO.getTitle() + " " + studentDO.getName());
|
||||||
|
data.put("examStr", ExamTitle);
|
||||||
|
|
||||||
|
List<VocabularyBankDO> words1 = words.subList(0, words.size() / 2);
|
||||||
|
List<VocabularyBankDO> words2 = words.subList(words.size() / 2, words.size());
|
||||||
|
data.put("words1", words1);
|
||||||
|
data.put("words2", words2);
|
||||||
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||||
// Configure config = Configure.builder()
|
// Configure config = Configure.builder()
|
||||||
// .bind("checkList", policy)
|
// .bind("checkList", policy)
|
||||||
@@ -188,7 +256,8 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
List<VocabularyBankDO> checkList,
|
List<VocabularyBankDO> checkList,
|
||||||
int day,
|
int day,
|
||||||
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
|
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
|
||||||
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "第" + day + "天" + studentId;
|
|
||||||
|
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "第" + day + "天";
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("title", title);
|
data.put("title", title);
|
||||||
data.put("syncVocabList", syncVocabList);
|
data.put("syncVocabList", syncVocabList);
|
||||||
@@ -196,15 +265,17 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
data.put("reviewVocabList", reviewVocabList);
|
data.put("reviewVocabList", reviewVocabList);
|
||||||
data.put("checkList", checkList);
|
data.put("checkList", checkList);
|
||||||
data.put("checkListAns", checkList);
|
data.put("checkListAns", checkList);
|
||||||
|
|
||||||
// 中译英
|
// 中译英
|
||||||
List<VocabularyBankDO> drillRound1 = new ArrayList<>(syncVocabList);
|
List<Word> list = syncVocabList.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList();
|
||||||
|
list.forEach(word -> word.setTitle(" "));
|
||||||
|
|
||||||
|
List<Word> drillRound1 = new ArrayList<>(list);
|
||||||
Collections.shuffle(drillRound1);
|
Collections.shuffle(drillRound1);
|
||||||
data.put("drillRound1", drillRound1);
|
data.put("drillRound1", drillRound1);
|
||||||
List<VocabularyBankDO> drillRound2 = new ArrayList<>(syncVocabList);
|
List<Word> drillRound2 = new ArrayList<>(list);
|
||||||
Collections.shuffle(drillRound2);
|
Collections.shuffle(drillRound2);
|
||||||
data.put("drillRound2", drillRound2);
|
data.put("drillRound2", drillRound2);
|
||||||
List<VocabularyBankDO> drillRound3 = new ArrayList<>(syncVocabList);
|
List<Word> drillRound3 = new ArrayList<>(list);
|
||||||
Collections.shuffle(drillRound3);
|
Collections.shuffle(drillRound3);
|
||||||
data.put("drillRound3", drillRound3);
|
data.put("drillRound3", drillRound3);
|
||||||
|
|
||||||
@@ -213,8 +284,10 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
mixedDrill.addAll(syncVocabList);
|
mixedDrill.addAll(syncVocabList);
|
||||||
mixedDrill.addAll(gapVocabList);
|
mixedDrill.addAll(gapVocabList);
|
||||||
mixedDrill.addAll(reviewVocabList);
|
mixedDrill.addAll(reviewVocabList);
|
||||||
Collections.shuffle(mixedDrill);
|
List<Word> mixedList = new ArrayList<>(mixedDrill.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList());
|
||||||
data.put("mixedDrill", mixedDrill);
|
mixedList.forEach(word -> word.setDefinition(" "));
|
||||||
|
Collections.shuffle(mixedList);
|
||||||
|
data.put("mixedDrill", mixedList);
|
||||||
|
|
||||||
// 文章 A
|
// 文章 A
|
||||||
log.info("生成文章 A 中文开始");
|
log.info("生成文章 A 中文开始");
|
||||||
@@ -241,6 +314,50 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
data.put("articleBans", mapB.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION));
|
data.put("articleBans", mapB.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION));
|
||||||
data.put("articleBtran", mapB.get(LessonPlanConstant.FULL_TRANSLATION));
|
data.put("articleBtran", mapB.get(LessonPlanConstant.FULL_TRANSLATION));
|
||||||
|
|
||||||
|
// 连词成句
|
||||||
|
List<Sentence> sentences = difyClient.sendSentenceAnalyze(syncVocabList.subList(0, Math.max(10, syncVocabList.size())), gradeDO.getTitle());
|
||||||
|
data.put("sentences", sentences);
|
||||||
|
data.put("sentencesAns", sentences);
|
||||||
|
log.info( "生成连词成句成功");
|
||||||
|
|
||||||
|
|
||||||
|
// 教案小测
|
||||||
|
List<VocabularyBankDO> words = new ArrayList<>(syncVocabList);
|
||||||
|
words.addAll(gapVocabList);
|
||||||
|
words.addAll(reviewVocabList);
|
||||||
|
if (words.size() < 100) {
|
||||||
|
words.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(gradeDO.getId(), 100 - words.size()));
|
||||||
|
} else {
|
||||||
|
words = words.subList(0, 100);
|
||||||
|
}
|
||||||
|
words.forEach(word -> word.setDefinition(word.getDefinition().length() > 5 ? word.getDefinition().substring(0, 5) : word.getDefinition()));
|
||||||
|
List<Integer> wordIds = words.stream().map(VocabularyBankDO::getId).toList();
|
||||||
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
|
|
||||||
|
String ExamTitle = gradeDO.getTitle() + unitDO.getTitle() + "教案小测 第" + ExamWordsConstant.day2Chinese(day) + "天" + studentDO.getName();
|
||||||
|
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
||||||
|
.gradeId(gradeDO.getId())
|
||||||
|
.level(1)
|
||||||
|
.wordIds(wordIds)
|
||||||
|
.type(ExamWordsConstant.EXAM_TYPE_TEST)
|
||||||
|
.title(ExamTitle)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
examWordsDOMapper.insert(examWordsDO);
|
||||||
|
studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
|
||||||
|
|
||||||
|
data.put("examId", examWordsDO.getId());
|
||||||
|
data.put("studentId", studentId);
|
||||||
|
data.put("studentStr", studentDO.getName());
|
||||||
|
data.put("examStr", ExamTitle);
|
||||||
|
|
||||||
|
List<VocabularyBankDO> words1 = words.subList(0, wordIds.size() / 2);
|
||||||
|
List<VocabularyBankDO> words2 = words.subList(wordIds.size() / 2, wordIds.size());
|
||||||
|
data.put("words1", words1);
|
||||||
|
data.put("words2", words2);
|
||||||
|
|
||||||
|
log.info("生成教案小测成功");
|
||||||
|
|
||||||
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||||
// Configure config = Configure.builder()
|
// Configure config = Configure.builder()
|
||||||
// .bind("syncVocabList", policy)
|
// .bind("syncVocabList", policy)
|
||||||
@@ -271,7 +388,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
int i = 0;
|
int i = 0;
|
||||||
do {
|
do {
|
||||||
log.info("第{}次生成文章中文开始", ++i);
|
log.info("第{}次生成文章中文开始", ++i);
|
||||||
String answer = difyArticleClient.sendChat(string, String.valueOf(studentId) + UUID.randomUUID(), null).getAnswer();
|
String answer = difyClient.sendChat(string, String.valueOf(studentId) + UUID.randomUUID(), null).getAnswer();
|
||||||
map = StringToPlanMapUtil.parseTextToMap(answer);
|
map = StringToPlanMapUtil.parseTextToMap(answer);
|
||||||
} while (map.get(LessonPlanConstant.TITLE) == null
|
} while (map.get(LessonPlanConstant.TITLE) == null
|
||||||
|| map.get(LessonPlanConstant.PASSAGE) == null
|
|| map.get(LessonPlanConstant.PASSAGE) == null
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ public class RoleServiceImpl implements RoleService {
|
|||||||
List<RoleDO> roleDOs = roleIds.stream().map(roleId2RoleDO::get).toList();
|
List<RoleDO> roleDOs = roleIds.stream().map(roleId2RoleDO::get).toList();
|
||||||
List<String> user2RoleKeys = roleDOs.stream().map(RoleDO::getRoleKey).toList();
|
List<String> user2RoleKeys = roleDOs.stream().map(RoleDO::getRoleKey).toList();
|
||||||
log.info("将用户 {} 的角色同步到 redis 中, {}", userId, roleKeys);
|
log.info("将用户 {} 的角色同步到 redis 中, {}", userId, roleKeys);
|
||||||
redisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), JsonUtils.toJsonString(user2RoleKeys));
|
// 不要使用 JsonUtils.toJsonString(user2RoleKeys); 会造成二次序列化
|
||||||
|
redisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), user2RoleKeys);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import com.yinlihupo.enlish.service.model.bo.exam.ExamWordsJudgeResultDetail;
|
|||||||
import com.yinlihupo.enlish.service.model.bo.exam.WordMasteryDetail;
|
import com.yinlihupo.enlish.service.model.bo.exam.WordMasteryDetail;
|
||||||
import com.yinlihupo.enlish.service.model.vo.student.AddStudentReqVO;
|
import com.yinlihupo.enlish.service.model.vo.student.AddStudentReqVO;
|
||||||
import com.yinlihupo.enlish.service.service.StudentService;
|
import com.yinlihupo.enlish.service.service.StudentService;
|
||||||
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
|
import com.yinlihupo.enlish.service.utils.DifyClient;
|
||||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -37,7 +36,7 @@ public class StudentServiceImpl implements StudentService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
|
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private DifyArticleClient difyArticleClient;
|
private DifyClient difyClient;
|
||||||
@Resource
|
@Resource
|
||||||
private RedisTemplate<String, Object> redisTemplate;
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -121,7 +120,10 @@ public class StudentServiceImpl implements StudentService {
|
|||||||
|
|
||||||
List<Integer> wordIds = new java.util.ArrayList<>(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getCorrectWordIds).flatMap(List::stream).toList());
|
List<Integer> wordIds = new java.util.ArrayList<>(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getCorrectWordIds).flatMap(List::stream).toList());
|
||||||
wordIds.addAll(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getWrongWordIds).flatMap(List::stream).toList());
|
wordIds.addAll(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getWrongWordIds).flatMap(List::stream).toList());
|
||||||
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordIds);
|
List<VocabularyBankDO> vocabularyBankDOS = new ArrayList<>();
|
||||||
|
if (!wordIds.isEmpty()) {
|
||||||
|
vocabularyBankDOS = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordIds);
|
||||||
|
}
|
||||||
Map<Integer, VocabularyBankDO> id2Word = vocabularyBankDOS.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
|
Map<Integer, VocabularyBankDO> id2Word = vocabularyBankDOS.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDetail> examWordsJudgeResultDetails = new ArrayList<>();
|
List<ExamWordsJudgeResultDetail> examWordsJudgeResultDetails = new ArrayList<>();
|
||||||
@@ -159,7 +161,7 @@ public class StudentServiceImpl implements StudentService {
|
|||||||
studentStudyInfo.put("单词掌握情况", wordMasteryDetails);
|
studentStudyInfo.put("单词掌握情况", wordMasteryDetails);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String analyze = difyArticleClient.sendStudentAnalyze(JsonUtils.toJsonString(studentStudyInfo)).getAnswer();
|
String analyze = difyClient.sendStudentAnalyze(JsonUtils.toJsonString(studentStudyInfo)).getAnswer();
|
||||||
// 设置过期时间 3 天
|
// 设置过期时间 3 天
|
||||||
redisTemplate.opsForValue().set(key, analyze);
|
redisTemplate.opsForValue().set(key, analyze);
|
||||||
redisTemplate.expire(key, 3, TimeUnit.DAYS);
|
redisTemplate.expire(key, 3, TimeUnit.DAYS);
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.yinlihupo.enlish.service.service.user;
|
package com.yinlihupo.enlish.service.service.user;
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
||||||
import com.yinlihupo.enlish.service.service.UserService;
|
import com.yinlihupo.enlish.service.service.UserService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -17,8 +21,10 @@ public class UserServiceImpl implements UserService {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private UserDOMapper userDOMapper;
|
private UserDOMapper userDOMapper;
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
@Resource
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDO findUser() {
|
public UserDO findUser() {
|
||||||
@@ -43,4 +49,21 @@ public class UserServiceImpl implements UserService {
|
|||||||
userDOMapper.insert(userDO);
|
userDOMapper.insert(userDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateUserInfo(String password, String reqCode, String phone, String name) {
|
||||||
|
long id = Integer.parseInt(String.valueOf(StpUtil.getLoginId()));
|
||||||
|
UserDO userDO = userDOMapper.selectById(id);
|
||||||
|
|
||||||
|
String key = UserRedisConstants.buildUserLoginCode(userDO.getPhone());
|
||||||
|
String code = Objects.requireNonNull(redisTemplate.opsForValue().get(key)).toString();
|
||||||
|
if (code == null || !code.equals(reqCode)) {
|
||||||
|
throw new RuntimeException("验证码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password != null) {
|
||||||
|
password = passwordEncoder.encode(password);
|
||||||
|
}
|
||||||
|
userDOMapper.updateUserInfo(id, name, password, phone);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.yinlihupo.enlish.service.job;
|
package com.yinlihupo.enlish.service.task;
|
||||||
|
|
||||||
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@Slf4j
|
||||||
public class AutoJudgeExamWordsTask {
|
public class AutoJudgeExamWordsTask {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -15,7 +15,9 @@ public class AutoJudgeExamWordsTask {
|
|||||||
|
|
||||||
@Scheduled(fixedRate = 5000)
|
@Scheduled(fixedRate = 5000)
|
||||||
public void autoJudgeExamWords() {
|
public void autoJudgeExamWords() {
|
||||||
System.out.println("【固定频率】开始自动判卷,时间:" + LocalDateTime.now());
|
if (examWordsJudgeService.getExamUnfinishedCount() != 0) {
|
||||||
examWordsJudgeService.judgeExamWords(5);
|
log.info("有试卷待检测,开始检测");
|
||||||
|
examWordsJudgeService.judgeExamWords(5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,9 @@ package com.yinlihupo.enlish.service.utils; // 修改为你的包名
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
||||||
|
import com.yinlihupo.enlish.service.model.bo.Sentence;
|
||||||
|
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -14,21 +17,25 @@ import java.net.http.HttpRequest;
|
|||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class DifyArticleClient {
|
public class DifyClient {
|
||||||
|
|
||||||
@Value("${ai.key}")
|
@Value("${ai.key}")
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
private String anaKey = "app-hrUFcopdcpnflsvpHWRuBfCp";
|
@Value("${ai.analyzeKey}")
|
||||||
|
private String analyzeKey;
|
||||||
|
@Value("${ai.sentenceKey}")
|
||||||
|
private String sentenceKey;
|
||||||
@Value("${ai.url}")
|
@Value("${ai.url}")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
// 构造函数
|
// 构造函数
|
||||||
public DifyArticleClient() {
|
public DifyClient() {
|
||||||
|
|
||||||
this.httpClient = HttpClient.newBuilder()
|
this.httpClient = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(10)) // 连接超时
|
.connectTimeout(Duration.ofSeconds(10)) // 连接超时
|
||||||
@@ -36,6 +43,48 @@ public class DifyArticleClient {
|
|||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Sentence> sendSentenceAnalyze(List<VocabularyBankDO> list, String grade) throws Exception {
|
||||||
|
// 1. 构建请求体对象
|
||||||
|
ChatRequest payload = new ChatRequest();
|
||||||
|
payload.setQuery(JsonUtils.toJsonString(list.stream().map(VocabularyBankDO::getWord).toList()));
|
||||||
|
payload.setUser("admin");
|
||||||
|
|
||||||
|
HashMap<String, Object> objectObjectHashMap = new HashMap<>();
|
||||||
|
objectObjectHashMap.put("grade", grade);
|
||||||
|
payload.setResponseMode("blocking"); // 使用阻塞模式,一次性返回
|
||||||
|
payload.setInputs(objectObjectHashMap);
|
||||||
|
|
||||||
|
// 2. 序列化为 JSON 字符串
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(payload);
|
||||||
|
|
||||||
|
// 3. 构建 HTTP 请求
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl))
|
||||||
|
.header("Authorization", "Bearer " + sentenceKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||||
|
.timeout(Duration.ofSeconds(30)) // 读取超时
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 4. 发送请求
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
|
// 5. 检查状态码
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new RuntimeException("Dify 请求失败: HTTP " + response.statusCode() + " | Body: " + response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 反序列化响应体
|
||||||
|
DifyResponse difyResponse = objectMapper.readValue(response.body(), DifyResponse.class);
|
||||||
|
|
||||||
|
String answer = difyResponse.getAnswer();
|
||||||
|
answer = answer.replace("json", "");
|
||||||
|
answer = answer.replace("```", "");
|
||||||
|
|
||||||
|
return JsonUtils.parseList(answer, Sentence.class);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public DifyResponse sendStudentAnalyze(String query) throws Exception {
|
public DifyResponse sendStudentAnalyze(String query) throws Exception {
|
||||||
String endpoint = this.baseUrl;
|
String endpoint = this.baseUrl;
|
||||||
|
|
||||||
@@ -52,7 +101,7 @@ public class DifyArticleClient {
|
|||||||
// 3. 构建 HTTP 请求
|
// 3. 构建 HTTP 请求
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(endpoint))
|
.uri(URI.create(endpoint))
|
||||||
.header("Authorization", "Bearer " + anaKey)
|
.header("Authorization", "Bearer " + analyzeKey)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||||
.timeout(Duration.ofSeconds(30)) // 读取超时
|
.timeout(Duration.ofSeconds(30)) // 读取超时
|
||||||
@@ -15,6 +15,7 @@ import org.opencv.imgproc.Imgproc;
|
|||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferByte;
|
import java.awt.image.DataBufferByte;
|
||||||
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -31,29 +32,11 @@ public class PngUtil {
|
|||||||
// 获取起始坐标
|
// 获取起始坐标
|
||||||
public static List<CoordinatesXY> analysisXY(String imagePath) {
|
public static List<CoordinatesXY> analysisXY(String imagePath) {
|
||||||
|
|
||||||
|
Mat binary = image2BinaryMath(imagePath);
|
||||||
Mat src = Imgcodecs.imread(imagePath);
|
Mat src = Imgcodecs.imread(imagePath);
|
||||||
|
|
||||||
if (src.empty()) {
|
|
||||||
System.out.println("无法读取图片,请检查路径。");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 预处理
|
|
||||||
// 3.1 转换为灰度图
|
|
||||||
Mat gray = new Mat();
|
|
||||||
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
|
||||||
|
|
||||||
// 3.2 二值化处理 (Thresholding)
|
|
||||||
// 使用 THRESH_BINARY_INV (反转二值化),因为我们需要找的是白色背景上的黑色块。
|
|
||||||
// 反转后,黑色块变成白色(255),背景变成黑色(0),方便 findContours 查找。
|
|
||||||
Mat binary = new Mat();
|
|
||||||
// 阈值设为 50 左右即可,因为块是纯黑的
|
|
||||||
Imgproc.threshold(gray, binary, 50, 255, Imgproc.THRESH_BINARY_INV);
|
|
||||||
|
|
||||||
// 4. 查找轮廓
|
// 4. 查找轮廓
|
||||||
List<MatOfPoint> contours = new ArrayList<>();
|
List<MatOfPoint> contours = new ArrayList<>();
|
||||||
Mat hierarchy = new Mat();
|
Mat hierarchy = new Mat();
|
||||||
// RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点
|
|
||||||
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
||||||
|
|
||||||
System.out.println("检测到的轮廓总数: " + contours.size());
|
System.out.println("检测到的轮廓总数: " + contours.size());
|
||||||
@@ -89,67 +72,44 @@ public class PngUtil {
|
|||||||
System.out.println("------------------------------------------------");
|
System.out.println("------------------------------------------------");
|
||||||
list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build());
|
list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build());
|
||||||
// 可选:在原图上画出框,用于调试验证
|
// 可选:在原图上画出框,用于调试验证
|
||||||
// Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
|
Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
|
||||||
// Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
|
Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
|
||||||
// Imgcodecs.imwrite("output_red.png", src);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Imgcodecs.imwrite("output_red.png", src);
|
||||||
System.out.println("找到 " + blockCount + " 个黑色块。");
|
System.out.println("找到 " + blockCount + " 个黑色块。");
|
||||||
|
|
||||||
// 获取每一列的宽度
|
// 计算起始坐标
|
||||||
list.sort(Comparator.comparingInt(CoordinatesXY::getHeight));
|
|
||||||
int height = list.get(list.size() - 1).getHeight() / ExamWordsConstant.PGN_COL;
|
|
||||||
|
|
||||||
// 删除两列答题卡区块
|
|
||||||
list.sort(Comparator.comparingInt(CoordinatesXY::getWidth));
|
|
||||||
list.remove(list.size() - 1);
|
|
||||||
list.remove(list.size() - 1);
|
|
||||||
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
|
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
|
||||||
|
|
||||||
// 计算起始坐标
|
list.forEach(coordinatesXY -> coordinatesXY.setHeight(coordinatesXY.getHeight() / 51));
|
||||||
List<CoordinatesXY> ans = getCoordinatesXIES(list, height);
|
list.forEach(coordinatesXY -> coordinatesXY.setWidth(coordinatesXY.getWidth() / 3));
|
||||||
|
list.forEach(coordinatesXY -> coordinatesXY.setX(coordinatesXY.getX() + coordinatesXY.getWidth() * 2));
|
||||||
|
|
||||||
src.release();
|
log.info("起始坐标: {}", list);
|
||||||
binary.release();
|
|
||||||
hierarchy.release();
|
|
||||||
binary.release();
|
|
||||||
|
|
||||||
return ans;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取(未背熟)单词的 id
|
// 获取(未背熟)单词的 id
|
||||||
public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Integer> wordIds, List<CoordinatesXY> coordinatesXYList) {
|
public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Integer> wordIds, List<CoordinatesXY> coordinatesXYList) {
|
||||||
|
|
||||||
Mat src = Imgcodecs.imread(filePath);
|
|
||||||
if (src.empty()) {
|
|
||||||
log.error("无法读取图片,请检查路径: {}", filePath);
|
|
||||||
throw new RuntimeException("无法读取图片");
|
|
||||||
}
|
|
||||||
|
|
||||||
Mat gray = new Mat();
|
Mat binary = image2BinaryMath(filePath);
|
||||||
Mat binary = new Mat();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
|
||||||
// 建议:如果光照不均匀,考虑使用 THRESH_OTSU 自动阈值,或者自适应阈值
|
|
||||||
Imgproc.threshold(gray, binary, 150, 255, Imgproc.THRESH_BINARY_INV);
|
|
||||||
// 调试时打印
|
|
||||||
// Imgcodecs.imwrite("output_binary.png", binary);
|
|
||||||
List<Integer> answer = new ArrayList<>();
|
List<Integer> answer = new ArrayList<>();
|
||||||
int words_index = 0;
|
int words_index = 0;
|
||||||
|
|
||||||
for (int i = 0; i < coordinatesXYList.size(); i++) {
|
for (CoordinatesXY coordinatesXY : coordinatesXYList) {
|
||||||
CoordinatesXY coordinatesXY = coordinatesXYList.get(i);
|
|
||||||
|
|
||||||
int width = coordinatesXY.getWidth();
|
int width = coordinatesXY.getWidth();
|
||||||
int height = coordinatesXY.getHeight();
|
int height = coordinatesXY.getHeight();
|
||||||
int currentX = coordinatesXY.getX();
|
int currentX = coordinatesXY.getX();
|
||||||
int currentY = coordinatesXY.getY();
|
int currentY = coordinatesXY.getY() + height;
|
||||||
|
|
||||||
int count = i == 0 ? ExamWordsConstant.PGN_COL - 1 : ExamWordsConstant.PGN_COL;
|
|
||||||
|
|
||||||
// 内层循环:遍历这一列的每一行
|
// 内层循环:遍历这一列的每一行
|
||||||
for (int j = 0; j < count; j++) {
|
for (int j = 0; j < 50; j++) {
|
||||||
// 安全检查:防止单词列表比格子少导致越界
|
// 安全检查:防止单词列表比格子少导致越界
|
||||||
if (words_index >= wordIds.size()) {
|
if (words_index >= wordIds.size()) {
|
||||||
log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
|
log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
|
||||||
@@ -168,12 +128,15 @@ public class PngUtil {
|
|||||||
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
|
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
|
||||||
Mat region = binary.submat(rect);
|
Mat region = binary.submat(rect);
|
||||||
int countNonZero = Core.countNonZero(region);
|
int countNonZero = Core.countNonZero(region);
|
||||||
|
log.info("当前位置为 words_index={},坐标为 x={} y={} 当前区域非零像素数: {}", words_index, currentX, currentY, countNonZero);
|
||||||
if (countNonZero > 500) {
|
if (countNonZero > 1000) {
|
||||||
Integer id = wordIds.get(words_index);
|
Integer id = wordIds.get(words_index);
|
||||||
answer.add(id);
|
answer.add(id);
|
||||||
log.info("检测到标记(未背熟):ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
|
log.info("检测到标记(未背熟):ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
|
||||||
}
|
}
|
||||||
|
if (countNonZero == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
region.release();
|
region.release();
|
||||||
words_index++;
|
words_index++;
|
||||||
@@ -185,8 +148,6 @@ public class PngUtil {
|
|||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
src.release();
|
|
||||||
gray.release();
|
|
||||||
binary.release();
|
binary.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,21 +167,21 @@ public class PngUtil {
|
|||||||
Rect roiRect = new Rect(0, 0, left.getX(), left.getY());
|
Rect roiRect = new Rect(0, 0, left.getX(), left.getY());
|
||||||
Mat roi = new Mat(src, roiRect);
|
Mat roi = new Mat(src, roiRect);
|
||||||
|
|
||||||
// 3. 图像预处理 (提高 OCR 准确率)
|
// // 3. 图像预处理 (提高 OCR 准确率)
|
||||||
// 3.1 转为灰度图
|
// // 3.1 转为灰度图
|
||||||
Mat gray = new Mat();
|
// Mat gray = new Mat();
|
||||||
Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
|
// Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
|
||||||
|
//
|
||||||
// 3.2 二值化 (Thresholding)
|
// // 3.2 二值化 (Thresholding)
|
||||||
// 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
|
// // 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
|
||||||
Mat binary = new Mat();
|
// Mat binary = new Mat();
|
||||||
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
|
// Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
|
||||||
|
|
||||||
// 可选:保存预处理后的图片查看效果
|
// 可选:保存预处理后的图片查看效果
|
||||||
// Imgcodecs.imwrite("debug_roi.jpg", binary);
|
Imgcodecs.imwrite("debug_roi.jpg", src);
|
||||||
|
|
||||||
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
|
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
|
||||||
BufferedImage processedImage = matToBufferedImage(binary);
|
BufferedImage processedImage = matToBufferedImage(src);
|
||||||
|
|
||||||
// 5. 使用 Tesseract 进行 OCR 识别
|
// 5. 使用 Tesseract 进行 OCR 识别
|
||||||
ITesseract instance = new Tesseract();
|
ITesseract instance = new Tesseract();
|
||||||
@@ -245,6 +206,50 @@ public class PngUtil {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Mat image2BinaryMath(String imagePath) {
|
||||||
|
|
||||||
|
if (!new File(imagePath).exists()) {
|
||||||
|
log.error("图片不存在,请检查路径: {}", imagePath);
|
||||||
|
throw new RuntimeException("图片不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Mat src = Imgcodecs.imread(imagePath);
|
||||||
|
|
||||||
|
if (src.empty()) {
|
||||||
|
log.info("无法读取图片,请检查路径: {}", imagePath);
|
||||||
|
throw new RuntimeException("无法读取图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
Mat gray = new Mat();
|
||||||
|
//转换为灰度图
|
||||||
|
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
||||||
|
|
||||||
|
Imgproc.GaussianBlur(gray, gray, new Size(5, 5), 0);
|
||||||
|
|
||||||
|
Mat binary = new Mat();
|
||||||
|
Imgproc.adaptiveThreshold(gray, binary, 255,
|
||||||
|
Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
Imgproc.THRESH_BINARY_INV, 25, 10);
|
||||||
|
|
||||||
|
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
|
||||||
|
|
||||||
|
//开运算 (Open):先腐蚀后膨胀,用于去除背景中的微小噪点
|
||||||
|
Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_OPEN, kernel);
|
||||||
|
|
||||||
|
// 闭运算 (Close):先膨胀后腐蚀,用于连接断裂的区域并填充块内部的空洞
|
||||||
|
// 如果块比较大且内部反光严重,可以将 Size(3,3) 改为 Size(5,5) 或更大
|
||||||
|
Mat closeKernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(7, 7));
|
||||||
|
Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_CLOSE, closeKernel);
|
||||||
|
|
||||||
|
// 保存二值化过程图用于调试 (生产环境可注释)
|
||||||
|
Imgcodecs.imwrite("debug_binary_natural.png", binary);
|
||||||
|
|
||||||
|
src.release();
|
||||||
|
gray.release();
|
||||||
|
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
private static @NonNull StudentExamId getStudentExamId(Pattern pattern, String result) {
|
private static @NonNull StudentExamId getStudentExamId(Pattern pattern, String result) {
|
||||||
Matcher matcher = pattern.matcher(result);
|
Matcher matcher = pattern.matcher(result);
|
||||||
StudentExamId studentExamId = new StudentExamId(0, 0);
|
StudentExamId studentExamId = new StudentExamId(0, 0);
|
||||||
@@ -276,21 +281,4 @@ public class PngUtil {
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull List<CoordinatesXY> getCoordinatesXIES(List<CoordinatesXY> list, int height) {
|
|
||||||
List<CoordinatesXY> ans = new ArrayList<>();
|
|
||||||
CoordinatesXY left = new CoordinatesXY();
|
|
||||||
left.setX(list.get(1).getX());
|
|
||||||
left.setWidth(list.get(1).getWidth());
|
|
||||||
left.setHeight(height);
|
|
||||||
left.setY(list.get(0).getY() + left.getHeight());
|
|
||||||
ans.add(left);
|
|
||||||
|
|
||||||
CoordinatesXY right = new CoordinatesXY();
|
|
||||||
right.setX(list.get(2).getX());
|
|
||||||
right.setY(list.get(0).getY());
|
|
||||||
right.setWidth(list.get(1).getWidth());
|
|
||||||
right.setHeight(height);
|
|
||||||
ans.add(right);
|
|
||||||
return ans;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ public class WordExportUtil {
|
|||||||
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||||
config = Configure.builder()
|
config = Configure.builder()
|
||||||
.bind("words", policy)
|
.bind("words", policy)
|
||||||
|
.bind("words1", policy)
|
||||||
|
.bind("words2", policy)
|
||||||
.bind("answer", policy)
|
.bind("answer", policy)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -50,11 +52,16 @@ public class WordExportUtil {
|
|||||||
.bind("mixedDrill", policyLessonPlanWeekday)
|
.bind("mixedDrill", policyLessonPlanWeekday)
|
||||||
.bind("checkList", policyLessonPlanWeekday)
|
.bind("checkList", policyLessonPlanWeekday)
|
||||||
.bind("checkListAns", policyLessonPlanWeekday)
|
.bind("checkListAns", policyLessonPlanWeekday)
|
||||||
|
.bind("sentences", policyLessonPlanWeekday)
|
||||||
|
.bind("sentencesAns", policyLessonPlanWeekday)
|
||||||
|
.bind("words1", policy)
|
||||||
|
.bind("words2", policy)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy();
|
LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy();
|
||||||
configLessonPlanWeekend = Configure.builder()
|
configLessonPlanWeekend = Configure.builder()
|
||||||
.bind("checkList", policyLessonPlan)
|
.bind("words1", policy)
|
||||||
|
.bind("words2", policy)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +101,7 @@ public class WordExportUtil {
|
|||||||
} else {
|
} else {
|
||||||
template = XWPFTemplate.compile(inputStream, configLessonPlanWeekend);
|
template = XWPFTemplate.compile(inputStream, configLessonPlanWeekend);
|
||||||
}
|
}
|
||||||
String url = "http://localhost:5173/#/plan/tts?planId=" + lessonPlan.getId();
|
String url = "http://english.yinlihupo.cn/#/plan/tts?planId=" + lessonPlan.getId();
|
||||||
map.put("img", Pictures.ofBytes(generateQR(url), PictureType.PNG).create());
|
map.put("img", Pictures.ofBytes(generateQR(url), PictureType.PNG).create());
|
||||||
OutputStream out = response.getOutputStream();
|
OutputStream out = response.getOutputStream();
|
||||||
template.render(map);
|
template.render(map);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ spring:
|
|||||||
datasource:
|
datasource:
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver # 指定数据库驱动类
|
driver-class-name: com.mysql.cj.jdbc.Driver # 指定数据库驱动类
|
||||||
# 数据库连接信息
|
# 数据库连接信息
|
||||||
url: jdbc:mysql://124.220.58.5:3306/enlish?allowMultiQueries=true
|
url: jdbc:mysql://124.220.58.5:3306/dev_english?allowMultiQueries=true
|
||||||
username: root # 数据库用户名
|
username: root # 数据库用户名
|
||||||
password: YLHP@admin123 # 数据库密码
|
password: YLHP@admin123 # 数据库密码
|
||||||
data:
|
data:
|
||||||
@@ -24,25 +24,27 @@ spring:
|
|||||||
api-key: your_api_key_here
|
api-key: your_api_key_here
|
||||||
base-url: http://124.220.58.5:2233
|
base-url: http://124.220.58.5:2233
|
||||||
audio:
|
audio:
|
||||||
speech:
|
speech:
|
||||||
options:
|
options:
|
||||||
model: tts-1
|
model: tts-1
|
||||||
voice: alloy
|
voice: alloy
|
||||||
|
|
||||||
|
|
||||||
templates:
|
templates:
|
||||||
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v5.docx
|
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v9.docx
|
||||||
count: 100
|
count: 100
|
||||||
data: C:\project\tess
|
data: C:\project\tess
|
||||||
plan:
|
plan:
|
||||||
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v3.docx
|
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v7.docx
|
||||||
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v1.docx
|
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v3.docx
|
||||||
plan_day: 7
|
plan_day: 7
|
||||||
tmp:
|
tmp:
|
||||||
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\
|
png:
|
||||||
|
|
||||||
ai:
|
ai:
|
||||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||||
|
analyzeKey: app-hrUFcopdcpnflsvpHWRuBfCp
|
||||||
|
sentenceKey: app-Emk5YQBaD2YruRXuE5sK1vEU
|
||||||
url: https://chat.cosonggle.com/v1/chat-messages
|
url: https://chat.cosonggle.com/v1/chat-messages
|
||||||
|
|
||||||
aliyun:
|
aliyun:
|
||||||
|
|||||||
52
enlish-service/src/main/resources/config/application-pro.yml
Normal file
52
enlish-service/src/main/resources/config/application-pro.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver # 指定数据库驱动类
|
||||||
|
# 数据库连接信息
|
||||||
|
url: jdbc:mysql://124.220.58.5:3306/enlish?allowMultiQueries=true
|
||||||
|
username: root # 数据库用户名
|
||||||
|
password: YLHP@admin123 # 数据库密码
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
database: 6 # Redis 数据库索引(默认为 0)
|
||||||
|
host: 124.220.58.5 # Redis 服务器地址
|
||||||
|
port: 6543 # Redis 服务器连接端口
|
||||||
|
password: 741963 # Redis 服务器连接密码(默认为空)
|
||||||
|
timeout: 5s # 读超时时间
|
||||||
|
connect-timeout: 5s # 链接超时时间
|
||||||
|
lettuce:
|
||||||
|
pool:
|
||||||
|
max-active: 200 # 连接池最大连接数
|
||||||
|
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||||
|
min-idle: 0 # 连接池中的最小空闲连接
|
||||||
|
max-idle: 10 # 连接池中的最大空闲连接
|
||||||
|
ai:
|
||||||
|
openai:
|
||||||
|
api-key: your_api_key_here
|
||||||
|
base-url: http://124.220.58.5:2233
|
||||||
|
audio:
|
||||||
|
speech:
|
||||||
|
options:
|
||||||
|
model: tts-1
|
||||||
|
voice: alloy
|
||||||
|
|
||||||
|
|
||||||
|
templates:
|
||||||
|
word: assessment_v9.docx
|
||||||
|
count: 100
|
||||||
|
data:
|
||||||
|
plan:
|
||||||
|
weekday: tem_study_plan_v7.docx
|
||||||
|
weekend: study_plan_review_v3.docx
|
||||||
|
plan_day: 7
|
||||||
|
tmp:
|
||||||
|
png:
|
||||||
|
|
||||||
|
ai:
|
||||||
|
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||||
|
analyzeKey: app-hrUFcopdcpnflsvpHWRuBfCp
|
||||||
|
sentenceKey: app-Emk5YQBaD2YruRXuE5sK1vEU
|
||||||
|
url: https://chat.cosonggle.com/v1/chat-messages
|
||||||
|
|
||||||
|
aliyun:
|
||||||
|
accessKeyId:
|
||||||
|
accessKeySecret:
|
||||||
@@ -3,12 +3,16 @@ server:
|
|||||||
|
|
||||||
spring:
|
spring:
|
||||||
profiles:
|
profiles:
|
||||||
active: dev # 默认激活 dev 本地开发环境
|
active: pro # 默认激活 dev 本地开发环境
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 30MB
|
||||||
|
max-request-size: 30MB
|
||||||
mybatis:
|
mybatis:
|
||||||
# MyBatis xml 配置文件路径
|
# MyBatis xml 配置文件路径
|
||||||
mapper-locations: classpath:/mapper/**/*.xml
|
mapper-locations: classpath:/mapper/**/*.xml
|
||||||
|
|
||||||
|
|
||||||
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
|
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
|
||||||
sa-token:
|
sa-token:
|
||||||
# token 名称(同时也是 cookie 名称)
|
# token 名称(同时也是 cookie 名称)
|
||||||
@@ -26,4 +30,7 @@ sa-token:
|
|||||||
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
||||||
is-share: true
|
is-share: true
|
||||||
# 是否输出操作日志
|
# 是否输出操作日志
|
||||||
is-log: true
|
is-log: true
|
||||||
|
#logging:
|
||||||
|
# level:
|
||||||
|
# com.yinlihupo.enlish.service.domain.mapper: debug
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
targetProject="src/main/java"/>
|
targetProject="src/main/java"/>
|
||||||
|
|
||||||
<!-- 需要生成的表-实体类 -->
|
<!-- 需要生成的表-实体类 -->
|
||||||
<table tableName="student_stage_learning_remark" domainObjectName="StudentStageLearningRemarkDO"
|
<table tableName="plan_exam" domainObjectName="PlanExamDO"
|
||||||
enableCountByExample="false"
|
enableCountByExample="false"
|
||||||
enableUpdateByExample="false"
|
enableUpdateByExample="false"
|
||||||
enableDeleteByExample="false"
|
enableDeleteByExample="false"
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
insert into exam_words (grade_id, level, title, word_ids, created_at)
|
insert into exam_words (grade_id, level, title, word_ids, type, created_at)
|
||||||
VALUES (#{gradeId}, #{level}, #{title}, #{wordIds, typeHandler=com.yinlihupo.enlish.service.config.ListWordIdTypeHandler}, #{createdAt})
|
VALUES (#{gradeId}, #{level}, #{title}, #{wordIds, typeHandler=com.yinlihupo.enlish.service.config.ListWordIdTypeHandler}, #{type}, #{createdAt})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<update id="updateWordIdsOrder">
|
<update id="updateWordIdsOrder">
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
select count(1)
|
select count(1)
|
||||||
from exam_words_judge_result
|
from exam_words_judge_result
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectDetailById" resultMap="ResultMapWithBLOBs">
|
<select id="selectDetailById" resultMap="ResultMapWithBLOBs">
|
||||||
select *
|
select *
|
||||||
from exam_words_judge_result
|
from exam_words_judge_result
|
||||||
@@ -81,4 +82,22 @@
|
|||||||
and start_date between date_sub(now(), interval 7 day) and now()
|
and start_date between date_sub(now(), interval 7 day) and now()
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByPageAndStudentIds" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from exam_words_judge_result
|
||||||
|
where student_id in
|
||||||
|
<foreach item="item" index="index" collection="studentIds"
|
||||||
|
open="(" separator="," close=")">
|
||||||
|
#{item}
|
||||||
|
</foreach>
|
||||||
|
order by start_date
|
||||||
|
limit #{startIndex}, #{pageSize}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectUnfinishedCount" resultType="java.lang.Integer">
|
||||||
|
select count(*)
|
||||||
|
from exam_words_judge_result
|
||||||
|
where is_finished = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -39,4 +39,15 @@
|
|||||||
where id = #{lessonId}
|
where id = #{lessonId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByStudentId" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from lesson_plans
|
||||||
|
where id in (
|
||||||
|
select student_lesson_plans.plan_id
|
||||||
|
from student_lesson_plans
|
||||||
|
where student_id = #{studentId}
|
||||||
|
and is_finished = 0
|
||||||
|
)
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?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.yinlihupo.enlish.service.domain.mapper.PlanExamDOMapper">
|
||||||
|
<resultMap id="BaseResultMap" type="com.yinlihupo.enlish.service.domain.dataobject.PlanExamDO">
|
||||||
|
<id column="id" jdbcType="INTEGER" property="id" />
|
||||||
|
<result column="plan_id" jdbcType="INTEGER" property="planId" />
|
||||||
|
<result column="exam_id" jdbcType="INTEGER" property="examId" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into plan_exam (plan_id, exam_id)
|
||||||
|
values (#{planId}, #{examId})
|
||||||
|
</insert>
|
||||||
|
<select id="selectByExamId" resultMap="BaseResultMap">
|
||||||
|
select * from plan_exam where exam_id = #{examId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -75,4 +75,24 @@
|
|||||||
where class_id = #{classId}
|
where class_id = #{classId}
|
||||||
and is_deleted = 0
|
and is_deleted = 0
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectStudentDOListByClassId" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from student
|
||||||
|
where class_id = #{classId}
|
||||||
|
and is_deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectStudentDOListByGradeId" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from student
|
||||||
|
where grade_id = #{gradeId}
|
||||||
|
and is_deleted = 0
|
||||||
|
</select>
|
||||||
|
<select id="selectStudentDOListByName" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from student
|
||||||
|
where name like concat('%', #{name}, '%')
|
||||||
|
and is_deleted = 0
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
from student_exam_words
|
from student_exam_words
|
||||||
where student_id = #{studentId}
|
where student_id = #{studentId}
|
||||||
and exam_words_id = #{examWordsId}
|
and exam_words_id = #{examWordsId}
|
||||||
and is_completed = 0
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<update id="updateStudentExamWordsFinished">
|
<update id="updateStudentExamWordsFinished">
|
||||||
|
|||||||
@@ -20,6 +20,28 @@
|
|||||||
values (#{phone}, #{name}, #{password})
|
values (#{phone}, #{name}, #{password})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<update id="updatePassword">
|
||||||
|
update user
|
||||||
|
set password = #{password}
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateUserInfo">
|
||||||
|
update user
|
||||||
|
<set>
|
||||||
|
<if test="password != null">
|
||||||
|
password = #{password},
|
||||||
|
</if>
|
||||||
|
<if test="name != null">
|
||||||
|
`name` = #{name},
|
||||||
|
</if>
|
||||||
|
<if test="phone != null">
|
||||||
|
phone = #{phone},
|
||||||
|
</if>
|
||||||
|
</set>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
<select id="selectByPhone" resultMap="BaseResultMap">
|
<select id="selectByPhone" resultMap="BaseResultMap">
|
||||||
select *
|
select *
|
||||||
from user
|
from user
|
||||||
|
|||||||
@@ -145,5 +145,21 @@
|
|||||||
order by rand()
|
order by rand()
|
||||||
limit 100
|
limit 100
|
||||||
</select>
|
</select>
|
||||||
|
<select id="selectByGradeIdAndNotMatchIds"
|
||||||
|
resultType="com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO">
|
||||||
|
select *
|
||||||
|
from vocabulary_bank
|
||||||
|
where unit_id in (
|
||||||
|
select unit_id
|
||||||
|
from grade_unit
|
||||||
|
where grade_id = #{gradeId}
|
||||||
|
)
|
||||||
|
and id not in
|
||||||
|
<foreach item="id" collection="ids" separator="," open="(" close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
order by rand()
|
||||||
|
limit #{wordCount}
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
BIN
enlish-service/src/main/resources/templates/assessment_v7.docx
Normal file
BIN
enlish-service/src/main/resources/templates/assessment_v7.docx
Normal file
Binary file not shown.
BIN
enlish-service/src/main/resources/templates/assessment_v8.docx
Normal file
BIN
enlish-service/src/main/resources/templates/assessment_v8.docx
Normal file
Binary file not shown.
BIN
enlish-service/src/main/resources/templates/assessment_v9.docx
Normal file
BIN
enlish-service/src/main/resources/templates/assessment_v9.docx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,47 +1,32 @@
|
|||||||
package com.yinlihupo.enlish.service.ai;
|
package com.yinlihupo.enlish.service.ai;
|
||||||
|
|
||||||
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
|
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
||||||
|
import com.yinlihupo.enlish.service.domain.mapper.VocabularyBankDOMapper;
|
||||||
|
import com.yinlihupo.enlish.service.model.bo.Sentence;
|
||||||
|
import com.yinlihupo.enlish.service.utils.DifyClient;
|
||||||
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
|
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
|
||||||
|
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
public class AITest {
|
public class AITest {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private DifyArticleClient client;
|
private DifyClient client;
|
||||||
|
@Resource
|
||||||
|
private VocabularyBankDOMapper vocabularyBankDOMapper;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test1() throws IOException {
|
public void test1() throws IOException {
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. 第一轮对话 (没有 conversation_id)
|
|
||||||
System.out.println("--- Round 1 ---");
|
|
||||||
String userId = "user-1001";
|
|
||||||
DifyArticleClient.DifyResponse response1 = client.sendChat("ruler, pencil, eraser, crayon, bag, pen, book, red, green, yellow, blue, face, ear, eye, nose, mouth, duck, pig, cat, bear, dog, elephant, monkey, bird, tiger, panda, bread, juice, egg, milk", userId, null);
|
|
||||||
|
|
||||||
//System.out.println("AI 回复: " + response1.getAnswer());
|
|
||||||
System.out.println("当前会话ID: " + response1.getConversationId());
|
|
||||||
|
|
||||||
// // 3. 第二轮对话 (传入上一轮的 conversation_id 以保持记忆)
|
|
||||||
// System.out.println("\n--- Round 2 ---");
|
|
||||||
// // 注意这里传入了 response1.getConversationId()
|
|
||||||
// DifyClient.DifyResponse response2 = client.sendChat("我刚才说了我叫什么?", userId, response1.getConversationId());
|
|
||||||
//
|
|
||||||
// System.out.println("AI 回复: " + response2.getAnswer());
|
|
||||||
|
|
||||||
System.out.println("\n--- Round 2 ---");
|
|
||||||
Map<String, String> stringStringMap = StringToPlanMapUtil.parseTextToMap(response1.getAnswer());
|
|
||||||
System.out.println(stringStringMap.get("Title"));
|
|
||||||
System.out.println(stringStringMap.get("The Passage"));
|
|
||||||
System.out.println(stringStringMap.get("Quiz"));
|
|
||||||
System.out.println(stringStringMap.get("Answer Key & Explanation"));
|
|
||||||
System.out.println(stringStringMap.get("Full Translation"));
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|||||||
@@ -35,68 +35,68 @@ public class TestVocabularyBankInsert {
|
|||||||
private GradeUnitDOMapper gradeUnitDOMapper;
|
private GradeUnitDOMapper gradeUnitDOMapper;
|
||||||
@Test
|
@Test
|
||||||
void test() {
|
void test() {
|
||||||
String file = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\test\\java\\com\\yinlihupo\\enlish\\service\\mapper\\八下.xlsx";
|
// String file = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\test\\java\\com\\yinlihupo\\enlish\\service\\mapper\\八下.xlsx";
|
||||||
HashMap<String, Integer> map = new HashMap<>();
|
// HashMap<String, Integer> map = new HashMap<>();
|
||||||
int gradeId = 8;
|
// int gradeId = 8;
|
||||||
try (FileInputStream fis = new FileInputStream(file); Workbook workbook = new XSSFWorkbook(fis)) {
|
// try (FileInputStream fis = new FileInputStream(file); Workbook workbook = new XSSFWorkbook(fis)) {
|
||||||
|
//
|
||||||
Sheet sheet = workbook.getSheetAt(0);
|
// Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
//
|
||||||
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
// for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
||||||
Row row = sheet.getRow(i);
|
// Row row = sheet.getRow(i);
|
||||||
if (row == null) {
|
// if (row == null) {
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
// String word = row.getCell(0).getStringCellValue();
|
// String word = row.getCell(0).getStringCellValue();
|
||||||
// String pronunciation = row.getCell(1) != null ? row.getCell(1).getStringCellValue() : "";
|
// String pronunciation = row.getCell(1) != null ? row.getCell(1).getStringCellValue() : "";
|
||||||
// String pos = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : "";
|
// String pos = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : "";
|
||||||
// String meaning = row.getCell(3) != null ? row.getCell(3).getStringCellValue() : "";
|
// String meaning = row.getCell(3) != null ? row.getCell(3).getStringCellValue() : "";
|
||||||
// String gradeUnit = row.getCell(4) != null ? row.getCell(4).getStringCellValue() : "";
|
// String gradeUnit = row.getCell(4) != null ? row.getCell(4).getStringCellValue() : "";
|
||||||
|
//
|
||||||
String word = row.getCell(0).getStringCellValue();
|
// String word = row.getCell(0).getStringCellValue();
|
||||||
String meaning = row.getCell(1) != null ? row.getCell(1).getStringCellValue() : "";
|
// String meaning = row.getCell(1) != null ? row.getCell(1).getStringCellValue() : "";
|
||||||
String gradeUnit = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : "";
|
// String gradeUnit = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : "";
|
||||||
String pronunciation = "";
|
// String pronunciation = "";
|
||||||
String pos = "";
|
// String pos = "";
|
||||||
if (word.contains("Unit")) {
|
// if (word.contains("Unit")) {
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
int gradeUnitId;
|
// int gradeUnitId;
|
||||||
if (map.containsKey(gradeUnit)) {
|
// if (map.containsKey(gradeUnit)) {
|
||||||
gradeUnitId = map.get(gradeUnit);
|
// gradeUnitId = map.get(gradeUnit);
|
||||||
} else {
|
// } else {
|
||||||
UnitDO unitDO = unitDOMapper.selectByTitle(gradeUnit);
|
// UnitDO unitDO = unitDOMapper.selectByTitle(gradeUnit);
|
||||||
if (unitDO == null) {
|
// if (unitDO == null) {
|
||||||
unitDO = UnitDO.builder()
|
// unitDO = UnitDO.builder()
|
||||||
.title(gradeUnit)
|
// .title(gradeUnit)
|
||||||
.version("人教版")
|
// .version("人教版")
|
||||||
.createAt(LocalDateTime.now())
|
// .createAt(LocalDateTime.now())
|
||||||
.build();
|
// .build();
|
||||||
unitDOMapper.insert(unitDO);
|
// unitDOMapper.insert(unitDO);
|
||||||
gradeUnitDOMapper.insert(GradeUnitDO.builder().unitId(unitDO.getId()).gradeId(gradeId).build());
|
// gradeUnitDOMapper.insert(GradeUnitDO.builder().unitId(unitDO.getId()).gradeId(gradeId).build());
|
||||||
gradeUnitId = unitDO.getId();
|
// gradeUnitId = unitDO.getId();
|
||||||
} else {
|
// } else {
|
||||||
gradeUnitId = unitDO.getId();
|
// gradeUnitId = unitDO.getId();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
map.put(gradeUnit, gradeUnitId);
|
// map.put(gradeUnit, gradeUnitId);
|
||||||
}
|
// }
|
||||||
VocabularyBankDO vocabularyBankDO = VocabularyBankDO.builder()
|
// VocabularyBankDO vocabularyBankDO = VocabularyBankDO.builder()
|
||||||
.word(word)
|
// .word(word)
|
||||||
.definition(meaning)
|
// .definition(meaning)
|
||||||
.pronunciation(pronunciation)
|
// .pronunciation(pronunciation)
|
||||||
.pos(pos)
|
// .pos(pos)
|
||||||
.unitId(gradeUnitId)
|
// .unitId(gradeUnitId)
|
||||||
.build();
|
// .build();
|
||||||
vocabularyBankMapper.insertSelective(vocabularyBankDO);
|
// vocabularyBankMapper.insertSelective(vocabularyBankDO);
|
||||||
log.info("插入数据 {} ", vocabularyBankDO);
|
// log.info("插入数据 {} ", vocabularyBankDO);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
} catch (IOException e) {
|
// } catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
// throw new RuntimeException(e);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class WordMasteryLogInsertTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void test() {
|
void test() {
|
||||||
List<Integer> integers = vocabularyBankMapper.selectAllIds();
|
// List<Integer> integers = vocabularyBankMapper.selectAllIds();
|
||||||
wordMasteryLogDOMapper.batchInsertInitialization(integers, 1);
|
// wordMasteryLogDOMapper.batchInsertInitialization(integers, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,25 +24,25 @@ public class TestOmr {
|
|||||||
public void testOmr(){
|
public void testOmr(){
|
||||||
OpenCV.loadLocally();
|
OpenCV.loadLocally();
|
||||||
|
|
||||||
List<Integer> knownIds = Arrays.asList(3184, 3185, 3186, 3187, 3188, 3189, 3190, 3191, 3192, 3193,
|
// List<Integer> knownIds = Arrays.asList(3184, 3185, 3186, 3187, 3188, 3189, 3190, 3191, 3192, 3193,
|
||||||
3195, 3196, 3197, 3198, 3199, 3200, 3201, 3202, 3203, 3204,
|
// 3195, 3196, 3197, 3198, 3199, 3200, 3201, 3202, 3203, 3204,
|
||||||
3206, 3208, 3209, 3210, 3211, 3212, 3507, 3508, 3509, 3510,
|
// 3206, 3208, 3209, 3210, 3211, 3212, 3507, 3508, 3509, 3510,
|
||||||
3511, 3512, 3513, 3514, 3515, 3516, 3517, 3519, 3521, 3522,
|
// 3511, 3512, 3513, 3514, 3515, 3516, 3517, 3519, 3521, 3522,
|
||||||
3523, 3524, 3525, 3526, 3527, 3528, 3529, 3530, 3531, 3532,
|
// 3523, 3524, 3525, 3526, 3527, 3528, 3529, 3530, 3531, 3532,
|
||||||
3533, 3535, 3536, 3537, 3538, 3539);
|
// 3533, 3535, 3536, 3537, 3538, 3539);
|
||||||
|
//
|
||||||
List<String> knowsIds = knownIds.stream().map(id -> id + "").toList();
|
// List<String> knowsIds = knownIds.stream().map(id -> id + "").toList();
|
||||||
String path = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\p3.png";
|
// String path = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\p3.png";
|
||||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(path);
|
// List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(path);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInteger(){
|
public void testInteger(){
|
||||||
String filePath = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\p3.png";
|
// String filePath = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\p3.png";
|
||||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(filePath);
|
// List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(filePath);
|
||||||
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(filePath, tessdataPath, coordinatesXIES);
|
// StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(filePath, tessdataPath, coordinatesXIES);
|
||||||
log.info("studentExamId:{}",studentExamId);
|
// log.info("studentExamId:{}",studentExamId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.deepoove.poi.config.Configure;
|
|||||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
||||||
|
import com.yinlihupo.enlish.service.domain.mapper.ExamWordsJudgeResultDOMapper;
|
||||||
import com.yinlihupo.enlish.service.model.bo.Word;
|
import com.yinlihupo.enlish.service.model.bo.Word;
|
||||||
import com.yinlihupo.enlish.service.service.ExamWordsService;
|
import com.yinlihupo.enlish.service.service.ExamWordsService;
|
||||||
import com.yinlihupo.enlish.service.service.VocabularyService;
|
import com.yinlihupo.enlish.service.service.VocabularyService;
|
||||||
@@ -24,38 +25,40 @@ public class ExamTest {
|
|||||||
private ExamWordsService examWordsService;
|
private ExamWordsService examWordsService;
|
||||||
@Resource
|
@Resource
|
||||||
private VocabularyService vocabularyService;
|
private VocabularyService vocabularyService;
|
||||||
|
@Resource
|
||||||
|
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
|
||||||
@Test
|
@Test
|
||||||
public void test() {
|
public void test() {
|
||||||
ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 0);
|
// ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 0);
|
||||||
log.info("{}", examWordsDO);
|
// log.info("{}", examWordsDO);
|
||||||
List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds());
|
// List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds());
|
||||||
List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder()
|
// List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder()
|
||||||
.id(vocabularyBankDO.getId())
|
// .id(vocabularyBankDO.getId())
|
||||||
.title(vocabularyBankDO.getWord())
|
// .title(vocabularyBankDO.getWord())
|
||||||
.definition(vocabularyBankDO.getDefinition())
|
// .definition(vocabularyBankDO.getDefinition())
|
||||||
.build()).toList();
|
// .build()).toList();
|
||||||
|
//
|
||||||
|
//
|
||||||
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||||
Configure config = Configure.builder()
|
// Configure config = Configure.builder()
|
||||||
.bind("words", policy)
|
// .bind("words", policy)
|
||||||
.build();
|
// .build();
|
||||||
|
//
|
||||||
Map<String, Object> data = new HashMap<>();
|
// Map<String, Object> data = new HashMap<>();
|
||||||
data.put("examId", examWordsDO.getId());
|
// data.put("examId", examWordsDO.getId());
|
||||||
data.put("studentId", 1);
|
// data.put("studentId", 1);
|
||||||
data.put("studentStr","小明三班一年级");
|
// data.put("studentStr","小明三班一年级");
|
||||||
data.put("examStr", examWordsDO.getTitle());
|
// data.put("examStr", examWordsDO.getTitle());
|
||||||
data.put("words", assessmentWords);
|
// data.put("words", assessmentWords);
|
||||||
data.put("answer", assessmentWords);
|
// data.put("answer", assessmentWords);
|
||||||
|
//
|
||||||
// 4. 渲染并输出
|
// // 4. 渲染并输出
|
||||||
try (XWPFTemplate template = XWPFTemplate.compile("C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\assessment_v5.docx", config)) {
|
// try (XWPFTemplate template = XWPFTemplate.compile("C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\assessment_v5.docx", config)) {
|
||||||
template.render(data);
|
// template.render(data);
|
||||||
template.write(new FileOutputStream("学生单词测试卷.docx"));
|
// template.write(new FileOutputStream("学生单词测试卷.docx"));
|
||||||
System.out.println("文档生成成功!");
|
// System.out.println("文档生成成功!");
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
e.printStackTrace();
|
// e.printStackTrace();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import com.yinlihupo.enlish.service.model.bo.CoordinatesXY;
|
|||||||
import com.yinlihupo.enlish.service.model.bo.StudentExamId;
|
import com.yinlihupo.enlish.service.model.bo.StudentExamId;
|
||||||
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
||||||
import com.yinlihupo.enlish.service.service.StudentService;
|
import com.yinlihupo.enlish.service.service.StudentService;
|
||||||
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
|
import com.yinlihupo.enlish.service.utils.DifyClient;
|
||||||
import com.yinlihupo.enlish.service.utils.PngUtil;
|
import com.yinlihupo.enlish.service.utils.PngUtil;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -28,7 +28,7 @@ public class ExamWordsJudgeServiceTest {
|
|||||||
@Resource
|
@Resource
|
||||||
private StudentService studentService;
|
private StudentService studentService;
|
||||||
@Resource
|
@Resource
|
||||||
private DifyArticleClient difyArticleClient;
|
private DifyClient difyClient;
|
||||||
@Resource
|
@Resource
|
||||||
private ExamWordsDOMapper examWordsDOMapper;
|
private ExamWordsDOMapper examWordsDOMapper;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -36,40 +36,45 @@ public class ExamWordsJudgeServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void judgeExamWords() {
|
public void judgeExamWords() {
|
||||||
String ansSheetPath = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\3.png";
|
// String ansSheetPath = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\3.png";
|
||||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
// List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
||||||
// 从图片中获取学生 id 和考试 id
|
// // 从图片中获取学生 id 和考试 id
|
||||||
String tessdataPath = "C:\\project\\tess";
|
// String tessdataPath = "C:\\project\\tess";
|
||||||
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
// StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
||||||
Integer examWordsId = 41;
|
// Integer examWordsId = 41;
|
||||||
|
//
|
||||||
|
// ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
||||||
|
//
|
||||||
|
// List<Integer> wordIds = examWordsDO.getWordIds();
|
||||||
|
// List<Integer> unmemorizedWordIds = PngUtil.analyzePngForUnmemorizedWordIds(ansSheetPath, wordIds, coordinatesXIES);
|
||||||
|
// List<Integer> memorizedWordIds = wordIds.stream().filter(wordId -> !unmemorizedWordIds.contains(wordId)).toList();
|
||||||
|
// List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOListByIds(unmemorizedWordIds);
|
||||||
|
// for (VocabularyBankDO vocabularyBankDO : vocabularyBankDOS) {
|
||||||
|
// log.info("未掌握的单词:{}", vocabularyBankDO);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
@Test
|
||||||
|
public void judege() {
|
||||||
List<Integer> wordIds = examWordsDO.getWordIds();
|
examWordsJudgeService.judgeExamWords(5);
|
||||||
List<Integer> unmemorizedWordIds = PngUtil.analyzePngForUnmemorizedWordIds(ansSheetPath, wordIds, coordinatesXIES);
|
|
||||||
List<Integer> memorizedWordIds = wordIds.stream().filter(wordId -> !unmemorizedWordIds.contains(wordId)).toList();
|
|
||||||
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOListByIds(unmemorizedWordIds);
|
|
||||||
for (VocabularyBankDO vocabularyBankDO : vocabularyBankDOS) {
|
|
||||||
log.info("未掌握的单词:{}", vocabularyBankDO);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void selectExamWordsJudgeResult() {
|
public void selectExamWordsJudgeResult() {
|
||||||
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(1, 10);
|
// List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(1, 10);
|
||||||
log.info("examWordsJudgeResult:{}", examWordsJudgeResult);
|
// log.info("examWordsJudgeResult:{}", examWordsJudgeResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
public void selectExamWordsJudgeResult2() {
|
// public void selectExamWordsJudgeResult2() {
|
||||||
String s = studentService.analyzeStudentStudy(1);
|
// String s = studentService.analyzeStudentStudy(1);
|
||||||
try {
|
// try {
|
||||||
DifyArticleClient.DifyResponse difyResponse = difyArticleClient.sendStudentAnalyze(s);
|
// DifyClient.DifyResponse difyResponse = difyClient.sendStudentAnalyze(s);
|
||||||
String answer = difyResponse.getAnswer();
|
// String answer = difyResponse.getAnswer();
|
||||||
log.info("answer:{}", answer);
|
// log.info("answer:{}", answer);
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
// throw new RuntimeException(e);
|
||||||
}
|
// }
|
||||||
log.info("s:{}", s);
|
// log.info("s:{}", s);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<title>enlish-vue</title>
|
<title>enlish-vue</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M5 6C5 4.89543 5.89543 4 7 4H12C13.6569 4 15 5.34315 15 7V17C15 18.6569 13.6569 20 12 20H7C5.89543 20 5 19.1046 5 18V6ZM7 6V10H12C12.5523 10 13 9.55228 13 9C13 8.44772 12.5523 8 12 8H8.5C7.67157 8 7 7.32843 7 6.5V6ZM7 18L7 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H7V17.5C7 17.7761 7.22386 18 7.5 18H12C12.5523 18 13 17.5523 13 17V17C13 16.4477 12.5523 16 12 16H8.5C7.67157 16 7 16.6716 7 17.5V18Z"
|
||||||
|
fill="#0056D2" />
|
||||||
|
<circle cx="17.5" cy="6.5" r="2.5" fill="#FFAB00" />
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 865 B |
@@ -1,16 +1,24 @@
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view></router-view>
|
<el-config-provider :locale="locale">
|
||||||
|
<router-view />
|
||||||
|
<el-drawer v-model="mobileSidebarOpen" class="md:hidden" title="菜单" size="260px">
|
||||||
|
<Sidebar />
|
||||||
|
</el-drawer>
|
||||||
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
const locale = zhCn
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
import { mobileSidebarOpen } from '@/composables/ui.js'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* 自定义顶部加载 Loading 颜色 */
|
/* 自定义顶部加载 Loading 颜色 */
|
||||||
#nprogress .bar {
|
#nprogress .bar {
|
||||||
background: #409eff !important;
|
background: #2563eb !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ export function createUser(data) {
|
|||||||
return axios.post('/admin/user/create', data)
|
return axios.post('/admin/user/create', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createInvitationCode(data) {
|
||||||
|
return axios.post('/admin/user/create/invitation/code', data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ export function uploadExamWordsPng(data) {
|
|||||||
return axios.post('/exam/words/submit', data)
|
return axios.post('/exam/words/submit', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExamWordsResult(page, size) {
|
export function getExamWordsResult(page, size, classId, gradeId, studentName) {
|
||||||
return axios.post('/exam/words/get', {
|
return axios.post('/exam/words/get', {
|
||||||
page: page,
|
page: page,
|
||||||
size: size
|
size: size,
|
||||||
|
classId: classId,
|
||||||
|
gradeId: gradeId,
|
||||||
|
studentName: studentName
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ export function getLessonPlanWords(planId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkIsGenerated(studentId) {
|
||||||
|
return axios.post('plan/check', {
|
||||||
|
studentId: studentId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlanListByStudentId(studentId) {
|
||||||
|
return axios.post('plan/student/list', {
|
||||||
|
studentId: studentId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const resolveBlob = (res, fileName) => {
|
const resolveBlob = (res, fileName) => {
|
||||||
// 创建 Blob 对象,可以指定 type,也可以让浏览器自动推断
|
// 创建 Blob 对象,可以指定 type,也可以让浏览器自动推断
|
||||||
const blob = new Blob([res], { type: 'application/octet-stream' });
|
const blob = new Blob([res], { type: 'application/octet-stream' });
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export function getVerificationCode(data) {
|
|||||||
export function getUserInfo() {
|
export function getUserInfo() {
|
||||||
return axios.post("/user/info")
|
return axios.post("/user/info")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateUserInfo(data) {
|
||||||
|
return axios.post("/user/update-user-info", data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,76 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.panel-shell {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.dark .panel-shell {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(55, 65, 81, 0.4);
|
||||||
|
}
|
||||||
|
.sidebar-fixed {
|
||||||
|
width: 220px;
|
||||||
|
min-width: 220px;
|
||||||
|
flex: 0 0 220px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--el-color-primary: #2563eb;
|
||||||
|
--el-border-radius-base: 10px;
|
||||||
|
--el-border-radius-small: 8px;
|
||||||
|
--el-border-radius-round: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app {
|
||||||
|
min-height: 100%;
|
||||||
|
background: radial-gradient(1200px at 10% 10%, #e0f2fe 0%, transparent 40%),
|
||||||
|
radial-gradient(1200px at 90% 10%, #dbeafe 0%, transparent 40%),
|
||||||
|
linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark html, .dark body, .dark #app {
|
||||||
|
background: radial-gradient(1000px at 10% 10%, rgba(30,58,138,0.35) 0%, transparent 40%),
|
||||||
|
radial-gradient(1000px at 90% 10%, rgba(2,132,199,0.3) 0%, transparent 40%),
|
||||||
|
linear-gradient(180deg, #0f172a 0%, #111827 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-fluid img,
|
||||||
|
.media-fluid video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-target {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-fixed {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.panel-shell {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
12
enlish-vue/src/composables/ui.js
Normal file
12
enlish-vue/src/composables/ui.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const mobileSidebarOpen = ref(false)
|
||||||
|
|
||||||
|
export function openMobileSidebar() {
|
||||||
|
mobileSidebarOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeMobileSidebar() {
|
||||||
|
mobileSidebarOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="新增班级" width="480px" :close-on-click-modal="false">
|
<el-dialog v-model="visible" title="新增班级" width="480px" :fullscreen="isMobile" :close-on-click-modal="false">
|
||||||
<div class="space-y-4" v-loading="loading">
|
<div class="space-y-4" v-loading="loading">
|
||||||
<el-form label-width="80px">
|
<el-form :label-width="isMobile ? 0 : 80" :label-position="isMobile ? 'top' : 'right'">
|
||||||
<el-form-item label="班级名称">
|
<el-form-item label="班级名称">
|
||||||
<el-input v-model="name" placeholder="请输入班级名称,如:二班" clearable />
|
<el-input v-model="name" placeholder="请输入班级名称,如:二班" clearable class="w-full" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="年级">
|
<el-form-item label="年级">
|
||||||
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px">
|
<el-select v-model="gradeId" placeholder="请选择年级" class="w-full sm:w-[260px]">
|
||||||
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { getGradeList } from '@/api/grade'
|
import { getGradeList } from '@/api/grade'
|
||||||
import { addClass } from '@/api/class'
|
import { addClass } from '@/api/class'
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ const loading = ref(false)
|
|||||||
const name = ref('')
|
const name = ref('')
|
||||||
const gradeId = ref(null)
|
const gradeId = ref(null)
|
||||||
const gradeOptions = ref([])
|
const gradeOptions = ref([])
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
const canSubmit = computed(() => name.value.trim().length > 0 && !!gradeId.value)
|
const canSubmit = computed(() => name.value.trim().length > 0 && !!gradeId.value)
|
||||||
|
|
||||||
@@ -71,6 +72,10 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIsMobile() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(v) => {
|
(v) => {
|
||||||
@@ -78,9 +83,22 @@ watch(
|
|||||||
name.value = ''
|
name.value = ''
|
||||||
gradeId.value = props.defaultGradeId ? Number(props.defaultGradeId) : null
|
gradeId.value = props.defaultGradeId ? Number(props.defaultGradeId) : null
|
||||||
fetchGrades()
|
fetchGrades()
|
||||||
|
updateIsMobile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.footer-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="新增年级" width="420px" :close-on-click-modal="false">
|
<el-dialog v-model="visible" title="新增年级" width="420px" :fullscreen="isMobile" :close-on-click-modal="false">
|
||||||
<div class="space-y-4" v-loading="loading">
|
<div class="space-y-4" v-loading="loading">
|
||||||
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable />
|
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { addGrade } from '@/api/grade'
|
import { addGrade } from '@/api/grade'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -29,6 +29,7 @@ const visible = computed({
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const canSubmit = computed(() => name.value.trim().length > 0)
|
const canSubmit = computed(() => name.value.trim().length > 0)
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!canSubmit.value) return
|
if (!canSubmit.value) return
|
||||||
@@ -43,12 +44,29 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIsMobile() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(v) => {
|
(v) => {
|
||||||
if (v) name.value = ''
|
if (v) name.value = ''
|
||||||
|
updateIsMobile()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.footer-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="新增学生" width="560px" :close-on-click-modal="false">
|
<el-dialog v-model="visible" title="新增学生" width="560px" :fullscreen="isMobile" :close-on-click-modal="false" class="responsive-dialog">
|
||||||
<div class="space-y-4" v-loading="loading">
|
<div class="space-y-4" v-loading="loading">
|
||||||
<el-form label-width="90px">
|
<el-form :label-width="isMobile ? 0 : 90" :label-position="isMobile ? 'top' : 'right'">
|
||||||
<el-form-item label="姓名">
|
<el-form-item label="姓名">
|
||||||
<el-input v-model="name" placeholder="请输入学生姓名" clearable />
|
<el-input v-model="name" placeholder="请输入学生姓名" clearable class="w-full" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="年级">
|
<el-form-item label="年级">
|
||||||
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px" @change="handleGradeChange">
|
<el-select v-model="gradeId" placeholder="请选择年级" class="w-full sm:w-[260px]" @change="handleGradeChange">
|
||||||
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="班级">
|
<el-form-item label="班级">
|
||||||
<el-select v-model="classId" placeholder="请选择班级" style="width: 260px">
|
<el-select v-model="classId" placeholder="请选择班级" class="w-full sm:w-[260px]">
|
||||||
<el-option v-for="c in filteredClassOptions" :key="c.id" :label="c.title" :value="c.id" />
|
<el-option v-for="c in filteredClassOptions" :key="c.id" :label="c.title" :value="c.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<div v-if="gradeId && filteredClassOptions.length === 0" class="mt-2 flex items-center gap-2">
|
<div v-if="gradeId && filteredClassOptions.length === 0" class="mt-2 flex items-center gap-2">
|
||||||
@@ -20,15 +20,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="入学时间">
|
<el-form-item label="入学时间">
|
||||||
<el-date-picker v-model="startDate" type="datetime" placeholder="选择日期时间"
|
<el-date-picker v-model="startDate" type="datetime" placeholder="选择日期时间" class="w-full"
|
||||||
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
|
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { getGradeList } from '@/api/grade'
|
import { getGradeList } from '@/api/grade'
|
||||||
import { getClassList } from '@/api/class'
|
import { getClassList } from '@/api/class'
|
||||||
import { addStudent } from '@/api/student'
|
import { addStudent } from '@/api/student'
|
||||||
@@ -61,6 +61,7 @@ const classId = ref(null)
|
|||||||
const startDate = ref('')
|
const startDate = ref('')
|
||||||
const gradeOptions = ref([])
|
const gradeOptions = ref([])
|
||||||
const classOptions = ref([])
|
const classOptions = ref([])
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
const filteredClassOptions = computed(() => {
|
const filteredClassOptions = computed(() => {
|
||||||
if (!gradeId.value) return []
|
if (!gradeId.value) return []
|
||||||
@@ -116,6 +117,10 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIsMobile() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
async (v) => {
|
async (v) => {
|
||||||
@@ -126,9 +131,26 @@ watch(
|
|||||||
startDate.value = ''
|
startDate.value = ''
|
||||||
await fetchBaseOptions()
|
await fetchBaseOptions()
|
||||||
if (gradeId.value) handleGradeChange()
|
if (gradeId.value) handleGradeChange()
|
||||||
|
updateIsMobile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.footer-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.responsive-dialog :deep(.el-dialog__body) {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="词条记录详情" width="820px" :close-on-click-modal="false">
|
<el-dialog v-model="visible" title="词条记录详情" width="820px" :fullscreen="isMobile" :close-on-click-modal="false" class="responsive-dialog">
|
||||||
<div class="space-y-4" v-loading="loading">
|
<div class="space-y-4" v-loading="loading">
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { getExamWordsDetailResult } from '@/api/exam'
|
import { getExamWordsDetailResult } from '@/api/exam'
|
||||||
import { getStudentDetail } from '@/api/student'
|
import { getStudentDetail } from '@/api/student'
|
||||||
import { getWordsListByIds } from '@/api/words'
|
import { getWordsListByIds } from '@/api/words'
|
||||||
@@ -101,6 +101,7 @@ const activeNames = ref(['correct', 'wrong'])
|
|||||||
const correctTitles = ref([])
|
const correctTitles = ref([])
|
||||||
const wrongTitles = ref([])
|
const wrongTitles = ref([])
|
||||||
const studentDetail = ref(null)
|
const studentDetail = ref(null)
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
async function fetchDetail() {
|
async function fetchDetail() {
|
||||||
if (!props.id && props.id !== 0) return
|
if (!props.id && props.id !== 0) return
|
||||||
@@ -149,10 +150,17 @@ async function fetchStudent() {
|
|||||||
studentDetail.value = res?.data?.data ?? null
|
studentDetail.value = res?.data?.data ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIsMobile() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(v) => {
|
(v) => {
|
||||||
if (v) fetchDetail()
|
if (v) {
|
||||||
|
updateIsMobile()
|
||||||
|
fetchDetail()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
watch(
|
watch(
|
||||||
@@ -161,6 +169,19 @@ watch(
|
|||||||
if (visible.value && v !== undefined && v !== null) fetchDetail()
|
if (visible.value && v !== undefined && v !== null) fetchDetail()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.responsive-dialog :deep(.el-dialog__body) {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,91 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<nav class="bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
|
<div class="p-2">
|
||||||
<div class="flex flex-wrap justify-between items-center mx-auto max-w-screen-xl">
|
<div class="panel-shell">
|
||||||
<a href="#" class="flex items-center">
|
<nav class="fluent-nav px-4 lg:px-6 py-2.5">
|
||||||
<img src="https://flowbite.com/docs/images/logo.svg" class="mr-3 h-6 sm:h-9" alt="Flowbite Logo" />
|
<div class="flex flex-wrap justify-between items-center">
|
||||||
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Flowbite</span>
|
<a href="#" class="flex items-center">
|
||||||
</a>
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
|
||||||
<div class="flex items-center lg:order-2">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<template v-if="userName">
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
<div class="relative" ref="menuRef">
|
d="M5 6C5 4.89543 5.89543 4 7 4H12C13.6569 4 15 5.34315 15 7V17C15 18.6569 13.6569 20 12 20H7C5.89543 20 5 19.1046 5 18V6ZM7 6V10H12C12.5523 10 13 9.55228 13 9C13 8.44772 12.5523 8 12 8H8.5C7.67157 8 7 7.32843 7 6.5V6ZM7 18L7 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H7V17.5C7 17.7761 7.22386 18 7.5 18H12C12.5523 18 13 17.5523 13 17V17C13 16.4477 12.5523 16 12 16H8.5C7.67157 16 7 16.6716 7 17.5V18Z"
|
||||||
<button
|
fill="#0056D2" />
|
||||||
@click="menuOpen = !menuOpen"
|
<circle cx="17.5" cy="6.5" r="2.5" fill="#FFAB00" />
|
||||||
class="text-gray-800 dark:text-white font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 flex items-center">
|
</svg>
|
||||||
<span class="mr-2">{{ userName }}</span>
|
<span class="self-center text-xl font-semibold whitespace-nowrap">英语教育</span>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</a>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
<div class="flex items-center lg:order-2">
|
||||||
|
<template v-if="userName">
|
||||||
|
<div class="relative" ref="menuRef">
|
||||||
|
<button @click="menuOpen = !menuOpen"
|
||||||
|
class="fluent-btn font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 flex items-center">
|
||||||
|
<span class="mr-2">{{ userName }}</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div v-if="menuOpen" class="fluent-card absolute right-0 mt-2 z-50">
|
||||||
|
<router-link to="/admid" @click="menuOpen = false"
|
||||||
|
class="block px-4 py-2 fluent-link">
|
||||||
|
后台
|
||||||
|
</router-link>
|
||||||
|
<button @click="handleLogout"
|
||||||
|
class="w-full text-left block px-4 py-2 fluent-link">
|
||||||
|
登出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<button data-collapse-toggle="mobile-menu-2" type="button" @click="openMobileSidebar()"
|
||||||
|
class="inline-flex items-center p-2 ml-1 text-sm rounded-lg lg:hidden fluent-btn"
|
||||||
|
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<svg class="hidden w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div
|
|
||||||
v-if="menuOpen"
|
|
||||||
class="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded shadow z-50">
|
|
||||||
<router-link
|
|
||||||
to="/admid"
|
|
||||||
@click="menuOpen = false"
|
|
||||||
class="block px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
||||||
后台
|
|
||||||
</router-link>
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="w-full text-left block px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
||||||
登出
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1"
|
||||||
<template v-else>
|
id="mobile-menu-2">
|
||||||
<a href="#" @click.prevent="showLogin = true"
|
|
||||||
class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">
|
</div>
|
||||||
Login
|
</div>
|
||||||
</a>
|
</nav>
|
||||||
</template>
|
|
||||||
<button data-collapse-toggle="mobile-menu-2" type="button"
|
|
||||||
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
|
||||||
aria-controls="mobile-menu-2" aria-expanded="false">
|
|
||||||
<span class="sr-only">Open main menu</span>
|
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
<svg class="hidden w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1" id="mobile-menu-2">
|
|
||||||
<ul class="flex flex-col mt-4 font-medium lg:flex-row lg:space-x-8 lg:mt-0">
|
|
||||||
<li>
|
|
||||||
<a href="#"
|
|
||||||
class="block py-2 pr-4 pl-3 text-white rounded bg-primary-700 lg:bg-transparent lg:text-primary-700 lg:p-0 dark:text-white"
|
|
||||||
aria-current="page">Home</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link to="/"
|
|
||||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
|
||||||
班级
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link to="/learningplan"
|
|
||||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
|
||||||
学案
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link to="/uploadpng"
|
|
||||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
|
||||||
上传图片
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
<LoginDialog v-model="showLogin" @success="refreshUser" />
|
<LoginDialog v-model="showLogin" @success="refreshUser" />
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
@@ -97,6 +74,7 @@ import { getUserInfo, logout } from '@/api/user'
|
|||||||
import { removeToken } from '@/composables/auth'
|
import { removeToken } from '@/composables/auth'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showMessage } from '@/composables/util.js'
|
import { showMessage } from '@/composables/util.js'
|
||||||
|
import { openMobileSidebar } from '@/composables/ui.js'
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
const menuOpen = ref(false)
|
const menuOpen = ref(false)
|
||||||
@@ -106,9 +84,14 @@ async function refreshUser() {
|
|||||||
try {
|
try {
|
||||||
const r = await getUserInfo()
|
const r = await getUserInfo()
|
||||||
const d = r?.data
|
const d = r?.data
|
||||||
userName.value = d?.success ? (d?.data?.name || '') : ''
|
console.log("header" + d.success)
|
||||||
|
if (d?.success) {
|
||||||
|
userName.value = d?.data?.name || ''
|
||||||
|
} else {
|
||||||
|
handleLogout()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
userName.value = ''
|
handleLogout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
@@ -119,7 +102,7 @@ async function handleLogout() {
|
|||||||
userName.value = ''
|
userName.value = ''
|
||||||
menuOpen.value = false
|
menuOpen.value = false
|
||||||
showMessage('已退出登录', 'success')
|
showMessage('已退出登录', 'success')
|
||||||
router.push('/')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onDocClick(e) {
|
function onDocClick(e) {
|
||||||
@@ -137,3 +120,83 @@ onBeforeUnmount(() => {
|
|||||||
document.removeEventListener('click', onDocClick)
|
document.removeEventListener('click', onDocClick)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.fluent-nav {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 0;
|
||||||
|
backdrop-filter: none;
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .fluent-nav {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-card {
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .fluent-card {
|
||||||
|
background: rgba(55, 65, 81, 0.4);
|
||||||
|
border-color: rgba(148, 163, 184, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-card:hover {
|
||||||
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-btn {
|
||||||
|
color: #0f172a;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
||||||
|
transition: background 200ms ease, box-shadow 200ms ease, transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .fluent-btn {
|
||||||
|
color: #e5e7eb;
|
||||||
|
background: rgba(55, 65, 81, 0.4);
|
||||||
|
border-color: rgba(148, 163, 184, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-link {
|
||||||
|
color: #2563eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: color 200ms ease, background 200ms ease, box-shadow 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-link:hover {
|
||||||
|
color: #1d4ed8;
|
||||||
|
background: rgba(255, 255, 255, 0.35);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .fluent-link:hover {
|
||||||
|
background: rgba(55, 65, 81, 0.35);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-card {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.el-header) {
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const props = defineProps({
|
|||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
studentId: { type: [Number, String], required: true }
|
studentId: { type: [Number, String], required: true }
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'success'])
|
||||||
|
|
||||||
const visible = computed({
|
const visible = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
@@ -71,6 +71,7 @@ async function handleGenerate() {
|
|||||||
const d = res?.data
|
const d = res?.data
|
||||||
if (d.success) {
|
if (d.success) {
|
||||||
ElMessage.success('生成学案任务已提交,请等待十分钟')
|
ElMessage.success('生成学案任务已提交,请等待十分钟')
|
||||||
|
emit('success', { studentId: Number(props.studentId) })
|
||||||
visible.value = false
|
visible.value = false
|
||||||
} else {
|
} else {
|
||||||
showMessage(d.message || '生成学案失败,请联系管理员', 'error')
|
showMessage(d.message || '生成学案失败,请联系管理员', 'error')
|
||||||
|
|||||||
50
enlish-vue/src/layouts/components/Sidebar.vue
Normal file
50
enlish-vue/src/layouts/components/Sidebar.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full p-2">
|
||||||
|
<div class="h-full panel-shell">
|
||||||
|
<el-menu router :default-active="activePath" class="h-full rounded-xl bg-transparent" :collapse="false">
|
||||||
|
<el-menu-item index="/">
|
||||||
|
<span>班级列表</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/learningplan">
|
||||||
|
<span>学案</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/uploadpng">
|
||||||
|
<span>上传图片</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import router from '@/router'
|
||||||
|
const route = useRoute()
|
||||||
|
const activePath = computed(() => route.path)
|
||||||
|
const allRoutes = router.getRoutes()
|
||||||
|
const menuItems = computed(() => {
|
||||||
|
return allRoutes
|
||||||
|
.filter(r => r.meta && r.meta.title)
|
||||||
|
.filter(r => !r.path.includes(':'))
|
||||||
|
.filter(r => r.path !== '/login')
|
||||||
|
.map(r => ({ path: r.path, title: r.meta.title }))
|
||||||
|
.sort((a, b) => a.title.localeCompare(b.title, 'zh-CN'))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Fluent 2 气质的轻盈质感:柔和玻璃、圆角、细描边 */
|
||||||
|
.el-menu {
|
||||||
|
--el-menu-bg-color: transparent;
|
||||||
|
--el-menu-hover-bg-color: rgba(255, 255, 255, 0.35);
|
||||||
|
--el-menu-active-color: #2563eb;
|
||||||
|
}
|
||||||
|
.el-menu-item {
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.el-menu-item:hover {
|
||||||
|
backdrop-filter: saturate(1.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
96
enlish-vue/src/layouts/components/StudentPlanListDialog.vue
Normal file
96
enlish-vue/src/layouts/components/StudentPlanListDialog.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="学生学案列表" width="680px" :close-on-click-modal="false">
|
||||||
|
<div v-loading="loading">
|
||||||
|
<el-table :data="plans" border>
|
||||||
|
<el-table-column prop="title" label="标题" min-width="360" />
|
||||||
|
<el-table-column label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
||||||
|
{{ row.isFinished === 1 ? '已完成' : '未完成' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="downloadingIds.includes(row.id)"
|
||||||
|
@click="handleDownload(row)"
|
||||||
|
>下载</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<el-button @click="visible = false">关闭</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { getPlanListByStudentId, downloadLessonPlan } from '@/api/plan'
|
||||||
|
import { showMessage } from '../../composables/util'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
studentId: { type: [Number, String], required: true }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const plans = ref([])
|
||||||
|
const downloadingIds = ref([])
|
||||||
|
|
||||||
|
async function fetchPlans() {
|
||||||
|
if (!props.studentId) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getPlanListByStudentId(Number(props.studentId))
|
||||||
|
const d = res?.data
|
||||||
|
if (d?.success !== false) {
|
||||||
|
const items = d?.data?.lessonPlanItems
|
||||||
|
plans.value = Array.isArray(items) ? items : []
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '获取学案失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownload(row) {
|
||||||
|
if (!row?.id) {
|
||||||
|
showMessage('无效的计划ID', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!downloadingIds.value.includes(row.id)) {
|
||||||
|
downloadingIds.value = [...downloadingIds.value, row.id]
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await downloadLessonPlan({ id: row.id })
|
||||||
|
showMessage('开始下载', 'success')
|
||||||
|
} finally {
|
||||||
|
downloadingIds.value = downloadingIds.value.filter(id => id !== row.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(v) => {
|
||||||
|
if (v) {
|
||||||
|
fetchPlans()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -25,7 +25,7 @@ function sortData(arr) {
|
|||||||
|
|
||||||
function toSource(arr) {
|
function toSource(arr) {
|
||||||
return sortData(arr).map(it => ({
|
return sortData(arr).map(it => ({
|
||||||
startTime: it.startTime,
|
startTime: it.startTime.replace('T', ' '),
|
||||||
totalCount: Number(it.totalCount) || 0,
|
totalCount: Number(it.totalCount) || 0,
|
||||||
planId: it.planId ?? null,
|
planId: it.planId ?? null,
|
||||||
id: it.id ?? null
|
id: it.id ?? null
|
||||||
|
|||||||
79
enlish-vue/src/layouts/components/student/StudyAnalysis.vue
Normal file
79
enlish-vue/src/layouts/components/student/StudyAnalysis.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="text-md font-semibold">学习分析</div>
|
||||||
|
<el-button type="primary" size="small" :loading="analyzeLoading" @click="fetchStudyAnalyze">
|
||||||
|
生成学习分析
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<template v-if="analyzeLoading">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<el-progress :percentage="analyzeProgress" :stroke-width="10" />
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">正在生成学习分析,请稍候…</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="analysisHtml">
|
||||||
|
<div class="leading-7 text-gray-700 dark:text-gray-200" v-html="analysisHtml"></div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-empty description="点击右上按钮生成学习分析" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import { getStudentStudyAnalyze } from '@/api/student'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
studentId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const analyzeLoading = ref(false)
|
||||||
|
const analyzeProgress = ref(0)
|
||||||
|
let analyzeTimer = null
|
||||||
|
const analysisText = ref('')
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: false,
|
||||||
|
linkify: true,
|
||||||
|
breaks: true
|
||||||
|
})
|
||||||
|
const analysisHtml = computed(() => {
|
||||||
|
return analysisText.value ? md.render(analysisText.value) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchStudyAnalyze() {
|
||||||
|
const id = props.studentId
|
||||||
|
if (!id) return
|
||||||
|
analyzeLoading.value = true
|
||||||
|
analyzeProgress.value = 0
|
||||||
|
if (analyzeTimer) {
|
||||||
|
clearInterval(analyzeTimer)
|
||||||
|
analyzeTimer = null
|
||||||
|
}
|
||||||
|
analyzeTimer = setInterval(() => {
|
||||||
|
const inc = Math.floor(Math.random() * 8) + 3
|
||||||
|
const next = analyzeProgress.value + inc
|
||||||
|
analyzeProgress.value = next >= 90 ? 90 : next
|
||||||
|
}, 300)
|
||||||
|
try {
|
||||||
|
const res = await getStudentStudyAnalyze({
|
||||||
|
studentId: Number(id)
|
||||||
|
})
|
||||||
|
const d = res.data
|
||||||
|
const raw = typeof d?.data === 'string' ? d.data : ''
|
||||||
|
analysisText.value = raw.replace(/\\n/g, '\n')
|
||||||
|
} finally {
|
||||||
|
analyzeProgress.value = 100
|
||||||
|
if (analyzeTimer) {
|
||||||
|
clearInterval(analyzeTimer)
|
||||||
|
analyzeTimer = null
|
||||||
|
}
|
||||||
|
analyzeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import '@/assets/main.css'
|
import '@/assets/main.css'
|
||||||
import 'nprogress/nprogress.css'
|
import 'nprogress/nprogress.css'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
// 导入路由
|
// 导入路由
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
// 导入全局路由守卫
|
// 导入全局路由守卫
|
||||||
|
|||||||
@@ -1,67 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div class="text-lg font-semibold mb-4">学案查询</div>
|
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
<Sidebar />
|
||||||
<el-input v-model="searchName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
</el-aside>
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
|
||||||
<el-button @click="onReset">重置</el-button>
|
<el-main class="">
|
||||||
</div>
|
<div class="panel-shell p-6">
|
||||||
<el-table ref="tableRef" :data="rows" border class="w-full" v-loading="loading" row-key="id">
|
<div class="text-lg font-semibold mb-4">学案查询</div>
|
||||||
<el-table-column type="expand">
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||||
<template #default="{ row }">
|
<el-input v-model="searchName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
||||||
<div class="p-3">
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
<div class="text-sm font-semibold mb-2">学案</div>
|
<el-button @click="onReset">重置</el-button>
|
||||||
<el-table :data="row.plans || []" size="small" border>
|
</div>
|
||||||
<el-table-column prop="id" label="计划ID" width="100" />
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
<el-table-column prop="title" label="标题" min-width="280" />
|
<el-table ref="tableRef" :data="rows" border class="min-w-[720px]" v-loading="loading" row-key="id">
|
||||||
<el-table-column label="状态" width="120">
|
<el-table-column type="expand">
|
||||||
<template #default="{ row: plan }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
<div class="p-3">
|
||||||
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
<div class="text-sm font-semibold mb-2">学案</div>
|
||||||
</el-tag>
|
<div class="overflow-x-auto">
|
||||||
</template>
|
<el-table :data="row.plans || []" size="small" border class="min-w-[600px]">
|
||||||
</el-table-column>
|
<el-table-column prop="title" label="标题" min-width="280" />
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="状态" width="120">
|
||||||
<template #default="{ row: plan }">
|
<template #default="{ row: plan }">
|
||||||
<el-button
|
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'"
|
||||||
type="primary"
|
effect="plain">
|
||||||
size="small"
|
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||||
:loading="downloadingIds.includes(plan.id)"
|
</el-tag>
|
||||||
@click="onDownload(plan)"
|
</template>
|
||||||
>下载</el-button>
|
</el-table-column>
|
||||||
<el-button
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
class="ml-2"
|
<template #default="{ row: plan }">
|
||||||
type="success"
|
<el-button type="primary" size="small"
|
||||||
size="small"
|
:loading="downloadingIds.includes(plan.id)"
|
||||||
:disabled="plan.isFinished === 1"
|
@click="onDownload(plan)">下载</el-button>
|
||||||
:loading="finishingIds.includes(plan.id)"
|
<el-button class="ml-2" type="primary" size="small"
|
||||||
@click="onFinish(row.id, plan.id, plan)"
|
:disabled="plan.isFinished === 1"
|
||||||
>完成</el-button>
|
:loading="finishingIds.includes(plan.id)"
|
||||||
</template>
|
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
|
||||||
</el-table-column>
|
</template>
|
||||||
</el-table>
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="姓名" min-width="140" />
|
||||||
|
<el-table-column prop="className" label="班级" min-width="140" />
|
||||||
|
<el-table-column prop="gradeName" label="年级" min-width="140" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="sm:hidden space-y-3">
|
||||||
|
<div v-for="row in rows" :key="row.id" class="panel-shell p-4">
|
||||||
|
<div class="text-base font-semibold mb-2">{{ row.name }}</div>
|
||||||
|
<div class="text-sm text-gray-700 mb-1">班级:{{ row.className }}</div>
|
||||||
|
<div class="text-sm text-gray-700 mb-3">年级:{{ row.gradeName }}</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="text-sm font-medium">学案</div>
|
||||||
|
<el-button size="small" @click="toggleMobileExpand(row.id)">
|
||||||
|
{{ mobileExpanded[row.id] ? '收起' : '展开' }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div v-if="mobileExpanded[row.id]" class="mt-3 space-y-2">
|
||||||
</el-table-column>
|
<div v-for="plan in (row.plans || [])" :key="plan.id"
|
||||||
<el-table-column prop="id" label="学生ID" width="100" />
|
class="rounded-lg border border-white/30 bg-white/50 p-3">
|
||||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
<div class="text-sm font-medium mb-2">{{ plan.title }}</div>
|
||||||
<el-table-column prop="className" label="班级" min-width="120" />
|
<div class="mb-2">
|
||||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
||||||
</el-table>
|
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||||
<div class="mt-4 flex justify-end">
|
</el-tag>
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
</div>
|
||||||
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
<div class="flex gap-2">
|
||||||
@size-change="handleSizeChange" />
|
<el-button type="primary" size="small"
|
||||||
|
:loading="downloadingIds.includes(plan.id)"
|
||||||
|
@click="onDownload(plan)">下载</el-button>
|
||||||
|
<el-button type="primary" size="small"
|
||||||
|
:disabled="plan.isFinished === 1"
|
||||||
|
:loading="finishingIds.includes(plan.id)"
|
||||||
|
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
||||||
|
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
||||||
|
@size-change="handleSizeChange" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-main>
|
||||||
</el-main>
|
</el-container>
|
||||||
|
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,6 +110,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { findStudentLessonPlans, finishLessonPlan } from '@/api/studentLessonPlans'
|
import { findStudentLessonPlans, finishLessonPlan } from '@/api/studentLessonPlans'
|
||||||
import { downloadLessonPlan } from '@/api/plan'
|
import { downloadLessonPlan } from '@/api/plan'
|
||||||
import { showMessage } from '@/composables/util'
|
import { showMessage } from '@/composables/util'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -83,6 +121,12 @@ const searchName = ref('')
|
|||||||
const tableRef = ref(null)
|
const tableRef = ref(null)
|
||||||
const downloadingIds = ref([])
|
const downloadingIds = ref([])
|
||||||
const finishingIds = ref([])
|
const finishingIds = ref([])
|
||||||
|
const mobileExpanded = ref({})
|
||||||
|
|
||||||
|
function toggleMobileExpand(id) {
|
||||||
|
const v = mobileExpanded.value[id]
|
||||||
|
mobileExpanded.value[id] = !v
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchLessonPlans() {
|
async function fetchLessonPlans() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|||||||
247
enlish-vue/src/pages/Login.vue
Normal file
247
enlish-vue/src/pages/Login.vue
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen relative flex items-center justify-center bg-scroll md:bg-fixed bg-cover bg-center bg-[url('https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg')] safe-area">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-[rgba(30,20,50,0.4)] to-[rgba(10,10,20,0.6)]"></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative z-10 w-full sm:w-[400px] max-w-[420px] sm:max-w-[92%] bg-white/10 backdrop-blur-2xl border border-white/20 rounded-2xl p-6 sm:p-8 shadow-2xl text-white">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h2 class="text-xl sm:text-2xl font-semibold tracking-wide mb-1">Welcome Back</h2>
|
||||||
|
<p class="text-sm text-white/80">智慧英语 · 让学习更简单</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center mb-6 border-b border-white/10">
|
||||||
|
<button class="px-4 sm:px-5 py-2 text-white/70 hover:text-white transition"
|
||||||
|
:class="mode === 'login' ? 'font-bold text-white border-b-2 border-white' : ''"
|
||||||
|
@click="switchMode('login')">登录</button>
|
||||||
|
<button class="px-4 sm:px-5 py-2 text-white/70 hover:text-white transition"
|
||||||
|
:class="mode === 'register' ? 'font-bold text-white border-b-2 border-white' : ''"
|
||||||
|
@click="switchMode('register')">注册</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" class="mt-2">
|
||||||
|
<el-form-item prop="phone">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
|
<el-input v-model="form.phone" maxlength="11" placeholder="请输入手机号 / 账号" />
|
||||||
|
<i class="fas fa-user absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<transition name="tabfade" mode="out-in">
|
||||||
|
<div :key="mode">
|
||||||
|
<template v-if="mode === 'login'">
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
|
<el-input v-model="form.password" type="password" maxlength="20" placeholder="请输入密码" />
|
||||||
|
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item prop="name">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
|
<el-input v-model="form.name" maxlength="20" placeholder="姓名" />
|
||||||
|
<i class="fas fa-id-card absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
|
<el-input v-model="form.password" type="password" maxlength="20" placeholder="设置密码" />
|
||||||
|
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password_repeat">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
|
<el-input v-model="form.password_repeat" type="password" maxlength="20" placeholder="重复密码" />
|
||||||
|
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="invitationCode">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
|
<el-input v-model="form.invitationCode" maxlength="6" placeholder="邀请码" />
|
||||||
|
<i class="fas fa-ticket absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="code">
|
||||||
|
<div class="relative mb-2 flex items-center gap-2">
|
||||||
|
<div class="input-glass relative flex-1">
|
||||||
|
<el-input v-model="form.code" maxlength="6" placeholder="验证码" />
|
||||||
|
<i class="fas fa-shield absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
<el-button class="rounded-lg bg-gradient-to-r from-blue-600 to-blue-400 text-white px-3 py-2"
|
||||||
|
@click="sendCode" :disabled="codeDisabled || !form.phone">
|
||||||
|
{{ codeBtnText }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full mt-3 px-4 py-3 rounded-xl bg-gradient-to-r from-blue-600 to-blue-400 text-white font-semibold shadow-lg hover:-translate-y-0.5 hover:shadow-xl transition disabled:opacity-70 disabled:cursor-not-allowed"
|
||||||
|
:disabled="loading" @click="userLogin">
|
||||||
|
<span v-if="!loading && mode === 'login'">立即登录</span>
|
||||||
|
<span v-if="loading && mode === 'login'">登录中...</span>
|
||||||
|
<span v-if="!loading && mode === 'register'">立即注册</span>
|
||||||
|
<span v-if="loading && mode === 'register'">处理中...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { login, getVerificationCode } from '@/api/user'
|
||||||
|
import { setToken } from '@/composables/auth'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const mode = ref('login')
|
||||||
|
const codeDisabled = ref(false)
|
||||||
|
const codeBtnText = ref('发送验证码')
|
||||||
|
let timer = null
|
||||||
|
const form = reactive({
|
||||||
|
phone: '',
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
password_repeat: '',
|
||||||
|
code: '',
|
||||||
|
invitationCode: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
phone: [
|
||||||
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入合法的手机号', trigger: ['blur', 'change'] },
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度为6-20位', trigger: ['blur', 'change'] },
|
||||||
|
],
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
validator: (_r, v, cb) => {
|
||||||
|
if (mode.value !== 'register') return cb()
|
||||||
|
if (!v?.trim()) return cb(new Error('请输入姓名'))
|
||||||
|
cb()
|
||||||
|
}, trigger: ['blur', 'change']
|
||||||
|
},
|
||||||
|
],
|
||||||
|
password_repeat: [
|
||||||
|
{
|
||||||
|
validator: (_r, v, cb) => {
|
||||||
|
if (mode.value !== 'register') return cb()
|
||||||
|
if (!v) return cb(new Error('请再次输入密码'))
|
||||||
|
if (v !== form.password) return cb(new Error('两次密码不一致'))
|
||||||
|
cb()
|
||||||
|
}, trigger: ['blur', 'change']
|
||||||
|
},
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{
|
||||||
|
validator: (_r, v, cb) => {
|
||||||
|
if (mode.value !== 'register') return cb()
|
||||||
|
if (!v?.trim()) return cb(new Error('请输入验证码'))
|
||||||
|
cb()
|
||||||
|
}, trigger: ['blur', 'change']
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userLogin() {
|
||||||
|
if (loading.value) return
|
||||||
|
formRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const payload = mode.value === 'login'
|
||||||
|
? {
|
||||||
|
phone: form.phone.trim(),
|
||||||
|
password: form.password.trim(),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
phone: form.phone.trim(),
|
||||||
|
name: form.name.trim(),
|
||||||
|
password: form.password.trim(),
|
||||||
|
code: form.code.trim(),
|
||||||
|
invitationCode: form.invitationCode.trim()
|
||||||
|
}
|
||||||
|
const res = await login(payload)
|
||||||
|
const data = res.data
|
||||||
|
if (data?.success) {
|
||||||
|
if (mode.value === 'login') {
|
||||||
|
try { setToken(data.data) } catch { }
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
router.push('/')
|
||||||
|
} else {
|
||||||
|
ElMessage.success('注册成功')
|
||||||
|
mode.value = 'login'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(data?.message || '登录失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchMode(m) {
|
||||||
|
mode.value = m
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCode() {
|
||||||
|
if (!form.phone || codeDisabled.value) return
|
||||||
|
codeDisabled.value = true
|
||||||
|
try {
|
||||||
|
await getVerificationCode({ phone: form.phone.trim() })
|
||||||
|
ElMessage.success('验证码已发送')
|
||||||
|
let count = 60
|
||||||
|
codeBtnText.value = `${count}s`
|
||||||
|
timer = setInterval(() => {
|
||||||
|
count -= 1
|
||||||
|
if (count <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
codeDisabled.value = false
|
||||||
|
codeBtnText.value = '发送验证码'
|
||||||
|
} else {
|
||||||
|
codeBtnText.value = `${count}s`
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} catch {
|
||||||
|
codeDisabled.value = false
|
||||||
|
codeBtnText.value = '发送验证码'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.input-glass :deep(.el-input__wrapper) {
|
||||||
|
@apply w-full bg-white/10 border border-white/10 rounded-xl pl-10 transition;
|
||||||
|
}
|
||||||
|
.input-glass :deep(.el-input__wrapper:hover),
|
||||||
|
.input-glass :deep(.el-input__wrapper.is-focus) {
|
||||||
|
@apply bg-white/15 border-white/50;
|
||||||
|
}
|
||||||
|
.input-glass :deep(.el-input__inner) {
|
||||||
|
@apply text-white placeholder:text-white/70;
|
||||||
|
}
|
||||||
|
.tabfade-enter-active, .tabfade-leave-active {
|
||||||
|
transition: all .25s ease;
|
||||||
|
}
|
||||||
|
.tabfade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px) scale(.98);
|
||||||
|
}
|
||||||
|
.tabfade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px) scale(.98);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,53 +1,69 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header>
|
|
||||||
<Header></Header>
|
|
||||||
</el-header>
|
<el-container class="pt-4">
|
||||||
<el-main class="p-4">
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<el-main class="p-2">
|
||||||
<div class="text-lg font-semibold mb-4">TTS</div>
|
<div class="panel-shell p-6">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="text-lg font-semibold mb-4">TTS</div>
|
||||||
<el-input v-model="planIdInput" placeholder="planId" style="max-width: 220px" />
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-4">
|
||||||
<el-button type="primary" :loading="loadingWords" @click="onLoadWords">加载词汇</el-button>
|
<!-- <el-input v-model="planIdInput" placeholder="planId" class="w-full sm:w-auto" style="max-width: 220px" /> -->
|
||||||
<el-select v-model="voice" placeholder="选择声线" style="max-width: 160px">
|
<el-button type="primary" :loading="loadingWords" @click="onLoadWords">加载词汇</el-button>
|
||||||
<el-option label="alloy" value="alloy" />
|
<!-- <el-select v-model="voice" placeholder="选择声线" class="w-full sm:w-auto" style="max-width: 160px">
|
||||||
<el-option label="verse" value="verse" />
|
<el-option label="alloy" value="alloy" />
|
||||||
<el-option label="nova" value="nova" />
|
<el-option label="verse" value="verse" />
|
||||||
</el-select>
|
<el-option label="nova" value="nova" />
|
||||||
<el-select v-model="format" placeholder="格式" style="max-width: 120px">
|
</el-select> -->
|
||||||
<el-option label="mp3" value="mp3" />
|
<!-- <el-select v-model="format" placeholder="格式" class="w-full sm:w-auto" style="max-width: 120px">
|
||||||
<el-option label="wav" value="wav" />
|
<el-option label="mp3" value="mp3" />
|
||||||
<el-option label="ogg" value="ogg" />
|
<el-option label="wav" value="wav" />
|
||||||
</el-select>
|
<el-option label="ogg" value="ogg" />
|
||||||
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll"
|
</el-select> -->
|
||||||
@click="onGenerateAll">生成全部音频</el-button>
|
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll" class="!ml-0"
|
||||||
|
@click="onGenerateAll">生成音频</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="sm:hidden">
|
||||||
|
<div v-for="row in tableData" :key="row.word" class="panel-shell p-4 mb-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="font-medium">{{ row.word }}</div>
|
||||||
|
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
||||||
|
{{ row.audioUrl ? '已生成' : '未生成' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<el-button size="small" type="primary" :loading="row.loading" @click="onGenerateOne(row)">生成音频</el-button>
|
||||||
|
<el-button size="small" :disabled="!row.audioUrl" @click="onPlay(row)">播放</el-button>
|
||||||
|
<el-button size="small" :disabled="!row.audioUrl" @click="onDownload(row)">下载</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
|
<el-table :data="tableData" border class="min-w-[640px]" v-loading="loadingWords" size="small">
|
||||||
|
<el-table-column prop="word" label="词汇/短语" min-width="200" />
|
||||||
|
<el-table-column label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
||||||
|
{{ row.audioUrl ? '已生成' : '未生成' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="primary" :loading="row.loading" @click="onGenerateOne(row)">生成音频</el-button>
|
||||||
|
<el-button size="small" class="ml-2" :disabled="!row.audioUrl" @click="onPlay(row)">播放</el-button>
|
||||||
|
<el-button size="small" class="ml-2" :disabled="!row.audioUrl" @click="onDownload(row)">下载</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-sm text-gray-500">
|
||||||
|
共 {{ words.length }} 条
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="tableData" border class="w-full" v-loading="loadingWords">
|
</el-main>
|
||||||
<el-table-column prop="word" label="词汇/短语" min-width="260" />
|
</el-container>
|
||||||
<el-table-column label="状态" width="160">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
|
||||||
{{ row.audioUrl ? '已生成' : '未生成' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="360" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button size="small" type="primary" :loading="row.loading"
|
|
||||||
@click="onGenerateOne(row)">生成音频</el-button>
|
|
||||||
<el-button size="small" class="ml-2" :disabled="!row.audioUrl"
|
|
||||||
@click="onPlay(row)">播放</el-button>
|
|
||||||
<el-button size="small" class="ml-2" :disabled="!row.audioUrl"
|
|
||||||
@click="onDownload(row)">下载</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="mt-3 text-sm text-gray-500">
|
|
||||||
共 {{ words.length }} 条
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-main>
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -59,7 +75,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { getLessonPlanWords } from '@/api/plan'
|
import { getLessonPlanWords } from '@/api/plan'
|
||||||
import { synthesizeOpenAITTS } from '@/api/tts'
|
import { synthesizeOpenAITTS } from '@/api/tts'
|
||||||
import { showMessage } from '@/composables/util'
|
import { showMessage } from '@/composables/util'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const planIdInput = ref(route.query.planId ? String(route.query.planId) : '')
|
const planIdInput = ref(route.query.planId ? String(route.query.planId) : '')
|
||||||
const words = ref([])
|
const words = ref([])
|
||||||
|
|||||||
@@ -5,50 +5,104 @@
|
|||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container>
|
||||||
<el-card>
|
<el-main class="p-2">
|
||||||
<div class="flex items-center mb-4">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<el-input v-model="query.name" placeholder="姓名" clearable style="max-width:220px" />
|
<div class="panel-shell p-6">
|
||||||
<el-button type="primary" class="ml-2" @click="fetchList">查询</el-button>
|
<div class="flex items-center mb-4">
|
||||||
<el-button class="ml-2" @click="resetSearch">重置</el-button>
|
<el-input v-model="query.name" placeholder="姓名" clearable style="max-width:220px" />
|
||||||
<el-button type="success" class="ml-2" @click="openCreate">新增用户</el-button>
|
<el-button type="primary" class="ml-2" @click="fetchList">查询</el-button>
|
||||||
</div>
|
<el-button class="ml-2" @click="resetSearch">重置</el-button>
|
||||||
<el-table :data="list" v-loading="loading" border stripe>
|
<el-button type="primary" class="ml-2" @click="openCreate">新增用户</el-button>
|
||||||
<el-table-column prop="name" label="姓名" />
|
</div>
|
||||||
<el-table-column prop="phone" label="手机号" />
|
<el-table :data="list" v-loading="loading" border stripe>
|
||||||
<el-table-column prop="roleName" label="角色" />
|
<el-table-column prop="name" label="姓名" />
|
||||||
</el-table>
|
<el-table-column prop="phone" label="手机号" />
|
||||||
<div class="mt-4 flex justify-end">
|
<el-table-column prop="roleName" label="角色" />
|
||||||
<el-pagination
|
</el-table>
|
||||||
background
|
<div class="mt-4 flex justify-end">
|
||||||
:current-page="page"
|
<el-pagination background :current-page="page" :page-size="pageSize" :total="totalCount"
|
||||||
:page-size="pageSize"
|
layout="prev, pager, next, sizes, total" @current-change="onPageChange"
|
||||||
:total="totalCount"
|
@size-change="onSizeChange" />
|
||||||
layout="prev, pager, next, sizes, total"
|
</div>
|
||||||
@current-change="onPageChange"
|
</div>
|
||||||
@size-change="onSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
<div class="panel-shell p-6">
|
||||||
<el-form :model="createForm" :rules="rules" ref="createFormRef" label-width="80px">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<el-form-item label="姓名" prop="name">
|
<span class="text-lg font-semibold">生成邀请码</span>
|
||||||
<el-input v-model="createForm.name" />
|
</div>
|
||||||
</el-form-item>
|
<el-form :model="inviteForm" :rules="inviteRules" ref="inviteFormRef" label-width="120px">
|
||||||
<el-form-item label="手机号" prop="phone">
|
<el-form-item label="使用次数限制" prop="limit">
|
||||||
<el-input v-model="createForm.phone" />
|
<el-input-number v-model="inviteForm.limit" :min="1" :max="9999" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="密码" prop="password">
|
<el-form-item label="有效期(天)" prop="expire">
|
||||||
<el-input v-model="createForm.password" type="password" show-password />
|
<el-input-number v-model="inviteForm.expire" :min="1" :max="3650" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
<el-form-item>
|
||||||
<template #footer>
|
<el-button type="primary" :loading="inviteLoading"
|
||||||
<el-button @click="createVisible=false">取消</el-button>
|
@click="submitInvite">生成邀请码</el-button>
|
||||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">提交</el-button>
|
<el-button class="ml-2" @click="resetInvite">重置</el-button>
|
||||||
</template>
|
</el-form-item>
|
||||||
</el-dialog>
|
</el-form>
|
||||||
</el-main>
|
<div v-if="inviteCode" class="mt-2">
|
||||||
|
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-shell p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-lg font-semibold">修改用户信息</span>
|
||||||
|
</div>
|
||||||
|
<el-form :model="pwForm" :rules="pwRules" ref="pwFormRef" label-width="120px">
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="pwForm.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input v-model="pwForm.newPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input v-model="pwForm.confirmPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="pwForm.phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="验证码" prop="code">
|
||||||
|
<el-input v-model="pwForm.code">
|
||||||
|
<template #append>
|
||||||
|
<el-button :disabled="codeCountdown > 0 || codeSending || !pwForm.phone"
|
||||||
|
@click="sendCode">
|
||||||
|
{{ codeCountdown > 0 ? `${codeCountdown}s` : '获取验证码' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="pwLoading" @click="submitPw">修改用户信息</el-button>
|
||||||
|
<el-button class="ml-2" @click="resetPw">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
||||||
|
<el-form :model="createForm" :rules="rules" ref="createFormRef" label-width="80px">
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="createForm.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="createForm.phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input v-model="createForm.password" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="createVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="createLoading" @click="submitCreate">提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,9 +110,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import { getUserList, createUser } from '@/api/admin'
|
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
||||||
|
import { updateUserInfo, getVerificationCode } from '@/api/user'
|
||||||
import { showMessage } from '@/composables/util.js'
|
import { showMessage } from '@/composables/util.js'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
@@ -144,4 +200,147 @@ onMounted(() => {
|
|||||||
fetchList()
|
fetchList()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const inviteForm = reactive({ limit: 10, expire: 3 })
|
||||||
|
const inviteFormRef = ref()
|
||||||
|
const inviteLoading = ref(false)
|
||||||
|
const inviteCode = ref('')
|
||||||
|
const inviteRules = {
|
||||||
|
limit: [{ required: true, message: '请输入使用次数限制', trigger: 'change' }],
|
||||||
|
expire: [{ required: true, message: '请输入有效期', trigger: 'change' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetInvite() {
|
||||||
|
inviteForm.limit = 10
|
||||||
|
inviteForm.expire = 3
|
||||||
|
inviteCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitInvite() {
|
||||||
|
inviteFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
inviteLoading.value = true
|
||||||
|
try {
|
||||||
|
const r = await createInvitationCode({ limit: inviteForm.limit, expire: inviteForm.expire })
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
inviteCode.value = d?.data?.invitationCode || ''
|
||||||
|
if (inviteCode.value) {
|
||||||
|
showMessage('邀请码生成成功', 'success')
|
||||||
|
} else {
|
||||||
|
showMessage('生成成功,但未返回邀请码', 'warning')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '邀请码生成失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
inviteLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwForm = reactive({ name: '', phone: '', newPassword: '', confirmPassword: '', code: '' })
|
||||||
|
const pwFormRef = ref()
|
||||||
|
const pwLoading = ref(false)
|
||||||
|
const pwRules = {
|
||||||
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
||||||
|
newPassword: [],
|
||||||
|
confirmPassword: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (pwForm.newPassword) {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error('请确认密码'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value !== pwForm.newPassword) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPw() {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
codeCountdown.value = 0
|
||||||
|
codeSending.value = false
|
||||||
|
pwForm.name = ''
|
||||||
|
pwForm.phone = ''
|
||||||
|
pwForm.newPassword = ''
|
||||||
|
pwForm.confirmPassword = ''
|
||||||
|
pwForm.code = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeCountdown = ref(0)
|
||||||
|
const codeSending = ref(false)
|
||||||
|
let codeTimer = null
|
||||||
|
|
||||||
|
async function sendCode() {
|
||||||
|
if (!pwForm.phone) {
|
||||||
|
showMessage('请输入手机号', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (codeSending.value || codeCountdown.value > 0) return
|
||||||
|
codeSending.value = true
|
||||||
|
try {
|
||||||
|
const r = await getVerificationCode({ phone: pwForm.phone })
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
showMessage('验证码已发送', 'success')
|
||||||
|
codeCountdown.value = 60
|
||||||
|
codeTimer = setInterval(() => {
|
||||||
|
if (codeCountdown.value > 0) {
|
||||||
|
codeCountdown.value -= 1
|
||||||
|
} else {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '发送验证码失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
codeSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function submitPw() {
|
||||||
|
pwFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
pwLoading.value = true
|
||||||
|
try {
|
||||||
|
const r = await updateUserInfo({
|
||||||
|
newPassword: pwForm.newPassword || '',
|
||||||
|
name: pwForm.name || '',
|
||||||
|
phone: pwForm.phone,
|
||||||
|
code: pwForm.code
|
||||||
|
})
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
showMessage('用户信息修改成功', 'success')
|
||||||
|
resetPw()
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '用户信息修改失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pwLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,155 +1,175 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||||
|
<Sidebar></Sidebar>
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
<div class="lg:col-span-1 flex flex-col gap-6">
|
<el-main class="h-full">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div class="lg:col-span-1 flex flex-col gap-6">
|
||||||
|
<div class="panel-shell p-6">
|
||||||
<div class="text-lg font-semibold mb-4">班级列表</div>
|
<div class="text-lg font-semibold mb-4">班级列表</div>
|
||||||
<el-table ref="classTableRef" :data="classes" border class="w-full" v-loading="loading" highlight-current-row
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
|
<el-table ref="classTableRef" :data="classes" border class="min-w-[520px]" v-loading="loading" highlight-current-row
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
|
||||||
<el-table-column prop="title" label="班级名称" min-width="120" />
|
<el-table-column prop="title" label="班级名称" min-width="160" />
|
||||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="danger" size="small" @click.stop="onDeleteClass(row)">删除</el-button>
|
<el-button type="danger" size="small" @click.stop="onDeleteClass(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt-4 flex justify-end">
|
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
|
||||||
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
|
||||||
@size-change="handleSizeChange" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex justify-end">
|
<div class="sm:hidden space-y-3">
|
||||||
<el-button type="primary" @click="showAddClassDialog = true">新增班级</el-button>
|
<div v-for="row in classes" :key="row.id" class="panel-shell p-4">
|
||||||
|
<div class="text-base font-semibold mb-1">{{ row.title }}</div>
|
||||||
|
<div class="text-sm mb-2">年级:{{ row.gradeName }}</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-button size="small" type="primary" @click="onClassRowClick(row)">选择</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="onDeleteClass(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||||
|
:total="totalCount" :page-size="pageSize" :current-page="pageNo"
|
||||||
|
@current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex justify-end">
|
||||||
|
<el-button type="primary" @click="showAddClassDialog = true">新增班级</el-button>
|
||||||
|
</div>
|
||||||
|
<AddClassDialog v-model="showAddClassDialog" :default-grade-id="selectedGradeId"
|
||||||
|
@success="fetchClasses" />
|
||||||
</div>
|
</div>
|
||||||
<AddClassDialog v-model="showAddClassDialog" :default-grade-id="selectedGradeId"
|
|
||||||
@success="fetchClasses" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 lg:col-span-1 lg:row-span-1">
|
<div class="panel-shell p-6 lg:col-span-1 lg:row-span-2">
|
||||||
<div class="text-lg font-semibold mb-4">学生查询</div>
|
<div class="text-lg font-semibold mb-4">学生查询</div>
|
||||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||||
<el-input v-model="studentName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
<el-input v-model="studentName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
||||||
<el-tag v-if="selectedClassId" effect="plain">班级:{{ selectedClassTitle }} (ID: {{
|
<el-tag v-if="selectedClassId" effect="plain">班级:{{ selectedClassTitle }}</el-tag>
|
||||||
selectedClassId }})</el-tag>
|
<el-tag v-if="selectedGradeId" effect="plain">年级:{{ selectedGradeTitle }}</el-tag>
|
||||||
<el-tag v-if="selectedGradeId" effect="plain">年级:{{ selectedGradeTitle }} (ID: {{
|
<el-button type="primary" @click="fetchStudents">查询</el-button>
|
||||||
selectedGradeId }})</el-tag>
|
<el-button @click="resetStudentFilters">重置</el-button>
|
||||||
<el-button type="primary" @click="fetchStudents">查询</el-button>
|
<el-button type="success" :disabled="selectedStudentIds.length !== 1"
|
||||||
<el-button @click="resetStudentFilters">重置</el-button>
|
@click="showGenerateDialog = true">
|
||||||
<el-button type="success" :disabled="selectedStudentIds.length !== 1"
|
生成试题
|
||||||
@click="showGenerateDialog = true">
|
</el-button>
|
||||||
生成试题
|
<el-button type="warning" :disabled="selectedStudentIds.length !== 1"
|
||||||
</el-button>
|
@click="showLessonPlanDialog = true">
|
||||||
<el-button type="warning" :disabled="selectedStudentIds.length !== 1"
|
生成学案
|
||||||
@click="showLessonPlanDialog = true">
|
</el-button>
|
||||||
生成学案
|
</div>
|
||||||
</el-button>
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
</div>
|
<el-table ref="studentTableRef" :data="students" border class="min-w-[760px]"
|
||||||
<el-table ref="studentTableRef" :data="students" border class="w-full"
|
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
|
||||||
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
|
<el-table-column type="selection" width="48" />
|
||||||
<el-table-column type="selection" width="48" />
|
<el-table-column prop="name" label="姓名" min-width="120" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="className" label="班级" min-width="120" />
|
||||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||||
<el-table-column prop="classId" label="班级ID" width="100" />
|
<el-table-column prop="phone" label="学案" min-width="120">
|
||||||
<el-table-column prop="gradeId" label="年级ID" width="100" />
|
<template #default="{ row }">
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<template v-if="generatingPercents[row.id] !== undefined">
|
||||||
<template #default="{ row }">
|
<div class="flex items-center gap-2">
|
||||||
<el-button type="primary" size="small" @click.stop="onViewStudent(row)">详情</el-button>
|
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
|
||||||
<el-button type="danger" size="small" @click.stop="onDeleteStudent(row)">删除</el-button>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
<template v-else>
|
||||||
</el-table>
|
<div class="flex items-center gap-2">
|
||||||
<div class="mt-4 flex justify-end">
|
<el-button type="primary" size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total"
|
</div>
|
||||||
:total="studentTotalCount" :page-size="studentPageSize" :current-page="studentPageNo"
|
</template>
|
||||||
@current-change="handleStudentPageChange" @size-change="handleStudentSizeChange" />
|
</template>
|
||||||
</div>
|
</el-table-column>
|
||||||
<ExamGenerateDialog v-model="showGenerateDialog" :student-ids="selectedStudentIds"
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
:default-grade-id="selectedGradeId" />
|
<template #default="{ row }">
|
||||||
|
<el-button type="info" size="small" @click.stop="onShowAnalysis(row)">学情分析</el-button>
|
||||||
|
<el-button type="primary" size="small" @click.stop="onViewStudent(row)">详情</el-button>
|
||||||
|
<el-button type="danger" size="small" @click.stop="onDeleteStudent(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="sm:hidden space-y-3">
|
||||||
|
<div v-for="row in students" :key="row.id" class="panel-shell p-4">
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<div class="text-base font-semibold">{{ row.name }}</div>
|
||||||
|
<el-checkbox
|
||||||
|
:model-value="isStudentSelected(row.id)"
|
||||||
|
@change="(val) => setStudentSelection(row.id, val)">
|
||||||
|
选中
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm mb-1">班级:{{ row.className }}</div>
|
||||||
|
<div class="text-sm mb-2">年级:{{ row.gradeName }}</div>
|
||||||
|
<template v-if="generatingPercents[row.id] !== undefined">
|
||||||
|
<div class="mb-2">
|
||||||
|
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<el-button size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
||||||
|
<el-button size="small" type="info" @click="onShowAnalysis(row)">学情分析</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="onViewStudent(row)">详情</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="onDeleteStudent(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||||
|
:total="studentTotalCount" :page-size="studentPageSize"
|
||||||
|
:current-page="studentPageNo" @current-change="handleStudentPageChange"
|
||||||
|
@size-change="handleStudentSizeChange" />
|
||||||
|
</div>
|
||||||
|
<ExamGenerateDialog v-model="showGenerateDialog" :student-ids="selectedStudentIds"
|
||||||
|
:default-grade-id="selectedGradeId" />
|
||||||
<div class="mt-3 flex justify-end">
|
<div class="mt-3 flex justify-end">
|
||||||
<el-button type="primary" @click="showAddStudentDialog = true">新增学生</el-button>
|
<el-button type="primary" @click="showAddStudentDialog = true">新增学生</el-button>
|
||||||
</div>
|
</div>
|
||||||
<AddStudentDialog
|
<AddStudentDialog v-model="showAddStudentDialog" :default-class-id="selectedClassId"
|
||||||
v-model="showAddStudentDialog"
|
:default-grade-id="selectedGradeId" @success="fetchStudents" />
|
||||||
:default-class-id="selectedClassId"
|
<LessonPlanDialog v-model="showLessonPlanDialog" :student-id="selectedStudentIds[0]"
|
||||||
:default-grade-id="selectedGradeId"
|
@success="onLessonPlanGenerateSuccess" />
|
||||||
@success="fetchStudents"
|
<StudentPlanListDialog v-model="showPlanListDialog" :student-id="planStudentId" />
|
||||||
/>
|
<el-dialog v-model="showAnalysisDialog" title="学情分析" width="60%">
|
||||||
<LessonPlanDialog
|
<StudyAnalysis v-if="showAnalysisDialog" :student-id="analysisStudentId" />
|
||||||
v-model="showLessonPlanDialog"
|
</el-dialog>
|
||||||
:student-id="selectedStudentIds[0]"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6" v-loading="gradeLoading">
|
<div class="panel-shell p-6" v-loading="gradeLoading">
|
||||||
<div class="text-lg font-semibold mb-4">年级列表</div>
|
<div class="text-lg font-semibold mb-4">年级列表</div>
|
||||||
<el-table ref="gradeTableRef" :data="grades" border class="w-full" highlight-current-row
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
<el-table ref="gradeTableRef" :data="grades" border class="min-w-[360px]" highlight-current-row
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
||||||
<el-table-column prop="title" label="年级名称" min-width="160" />
|
<el-table-column prop="title" label="年级名称" min-width="160" />
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
</el-table>
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button type="danger" size="small"
|
|
||||||
@click.stop="onDeleteGrade(row)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="mt-4 flex justify-end">
|
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="gradeTotalCount"
|
|
||||||
:page-size="gradePageSize" :current-page="gradePageNo"
|
|
||||||
@current-change="handleGradePageChange" @size-change="handleGradeSizeChange" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex justify-end">
|
<div class="sm:hidden space-y-3">
|
||||||
<el-button type="primary" @click="showAddGradeDialog = true">新增年级</el-button>
|
<div v-for="row in grades" :key="row.id" class="panel-shell p-4">
|
||||||
|
<div class="text-base font-semibold mb-1">{{ row.title }}</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-button size="small" type="primary" @click="onGradeRowClick(row)">选择</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="onDeleteGrade(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||||
|
:total="gradeTotalCount" :page-size="gradePageSize" :current-page="gradePageNo"
|
||||||
|
@current-change="handleGradePageChange" @size-change="handleGradeSizeChange" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AddGradeDialog v-model="showAddGradeDialog" @success="fetchGrades" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6" v-loading="unitLoading">
|
|
||||||
<div class="text-lg font-semibold mb-4">单元列表</div>
|
|
||||||
<el-table ref="unitTableRef" :data="units" border class="w-full">
|
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
|
||||||
<el-table-column prop="title" label="单元名称" min-width="200" />
|
|
||||||
<el-table-column prop="version" label="版本" min-width="120" />
|
|
||||||
<el-table-column prop="createAt" label="创建时间" min-width="160" />
|
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button type="danger" size="small" @click.stop="onDeleteUnit(row)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="mt-4 flex justify-end">
|
|
||||||
<el-pagination
|
|
||||||
background
|
|
||||||
layout="prev, pager, next, sizes, total"
|
|
||||||
:total="unitTotalCount"
|
|
||||||
:page-size="unitPageSize"
|
|
||||||
:current-page="unitPageNo"
|
|
||||||
@current-change="handleUnitPageChange"
|
|
||||||
@size-change="handleUnitSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 flex justify-end">
|
|
||||||
<el-button type="primary" :disabled="!selectedGradeId" @click="showAddUnitDialog = true">新增单元</el-button>
|
|
||||||
</div>
|
|
||||||
<AddUnitDialog
|
|
||||||
v-model="showAddUnitDialog"
|
|
||||||
:default-grade-id="selectedGradeId"
|
|
||||||
@success="fetchUnits"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</el-main>
|
||||||
</div>
|
</el-container>
|
||||||
</el-main>
|
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +177,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
import { ref, onMounted } from 'vue'
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { getClassList, deleteClass } from '@/api/class'
|
import { getClassList, deleteClass } from '@/api/class'
|
||||||
import { getGradeList, deleteGrade } from '@/api/grade'
|
import { getGradeList, deleteGrade } from '@/api/grade'
|
||||||
import { getStudentList, deleteStudent } from '@/api/student'
|
import { getStudentList, deleteStudent } from '@/api/student'
|
||||||
@@ -166,9 +187,13 @@ import AddClassDialog from '@/layouts/components/AddClassDialog.vue'
|
|||||||
import AddGradeDialog from '@/layouts/components/AddGradeDialog.vue'
|
import AddGradeDialog from '@/layouts/components/AddGradeDialog.vue'
|
||||||
import AddStudentDialog from '@/layouts/components/AddStudentDialog.vue'
|
import AddStudentDialog from '@/layouts/components/AddStudentDialog.vue'
|
||||||
import LessonPlanDialog from '@/layouts/components/LessonPlanDialog.vue'
|
import LessonPlanDialog from '@/layouts/components/LessonPlanDialog.vue'
|
||||||
|
import StudentPlanListDialog from '@/layouts/components/StudentPlanListDialog.vue'
|
||||||
|
import StudyAnalysis from '@/layouts/components/student/StudyAnalysis.vue'
|
||||||
import { getUnitList, deleteUnit } from '@/api/unit'
|
import { getUnitList, deleteUnit } from '@/api/unit'
|
||||||
import AddUnitDialog from '@/layouts/components/AddUnitDialog.vue'
|
import AddUnitDialog from '@/layouts/components/AddUnitDialog.vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { checkIsGenerated } from '@/api/plan'
|
||||||
|
|
||||||
const classes = ref([])
|
const classes = ref([])
|
||||||
const pageNo = ref(1)
|
const pageNo = ref(1)
|
||||||
@@ -201,6 +226,24 @@ const selectedStudentIds = ref([])
|
|||||||
const showGenerateDialog = ref(false)
|
const showGenerateDialog = ref(false)
|
||||||
const showAddStudentDialog = ref(false)
|
const showAddStudentDialog = ref(false)
|
||||||
const showLessonPlanDialog = ref(false)
|
const showLessonPlanDialog = ref(false)
|
||||||
|
const generatingPercents = ref({})
|
||||||
|
const pollingTimers = {}
|
||||||
|
const showPlanListDialog = ref(false)
|
||||||
|
const planStudentId = ref(null)
|
||||||
|
const showAnalysisDialog = ref(false)
|
||||||
|
const analysisStudentId = ref(null)
|
||||||
|
|
||||||
|
function isStudentSelected(id) {
|
||||||
|
return selectedStudentIds.value.includes(id)
|
||||||
|
}
|
||||||
|
function setStudentSelection(id, selected) {
|
||||||
|
const exists = selectedStudentIds.value.includes(id)
|
||||||
|
if (selected && !exists) {
|
||||||
|
selectedStudentIds.value = [...selectedStudentIds.value, id]
|
||||||
|
} else if (!selected && exists) {
|
||||||
|
selectedStudentIds.value = selectedStudentIds.value.filter(x => x !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const units = ref([])
|
const units = ref([])
|
||||||
const unitPageNo = ref(1)
|
const unitPageNo = ref(1)
|
||||||
@@ -255,6 +298,7 @@ async function fetchStudents() {
|
|||||||
studentTotalCount.value = d.totalCount || 0
|
studentTotalCount.value = d.totalCount || 0
|
||||||
studentPageNo.value = d.pageNo || studentPageNo.value
|
studentPageNo.value = d.pageNo || studentPageNo.value
|
||||||
studentPageSize.value = d.pageSize || studentPageSize.value
|
studentPageSize.value = d.pageSize || studentPageSize.value
|
||||||
|
ensurePollingForCurrentStudents()
|
||||||
} finally {
|
} finally {
|
||||||
studentLoading.value = false
|
studentLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -295,6 +339,10 @@ function onStudentSelectionChange(rows) {
|
|||||||
function onViewStudent(row) {
|
function onViewStudent(row) {
|
||||||
router.push(`/student/${row.id}`)
|
router.push(`/student/${row.id}`)
|
||||||
}
|
}
|
||||||
|
function onShowAnalysis(row) {
|
||||||
|
analysisStudentId.value = row.id
|
||||||
|
showAnalysisDialog.value = true
|
||||||
|
}
|
||||||
function onClassRowClick(row) {
|
function onClassRowClick(row) {
|
||||||
selectedClassId.value = row.id
|
selectedClassId.value = row.id
|
||||||
selectedClassTitle.value = row.title
|
selectedClassTitle.value = row.title
|
||||||
@@ -309,8 +357,64 @@ function onGradeRowClick(row) {
|
|||||||
studentPageNo.value = 1
|
studentPageNo.value = 1
|
||||||
fetchStudents()
|
fetchStudents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startLessonPlanPolling(studentId) {
|
||||||
|
if (!studentId) return
|
||||||
|
if (pollingTimers[studentId]) return
|
||||||
|
const pollOnce = async () => {
|
||||||
|
try {
|
||||||
|
const res = await checkIsGenerated(studentId)
|
||||||
|
const d = res?.data
|
||||||
|
const ok = d?.success === false || d?.success === false || d === false
|
||||||
|
if (ok) {
|
||||||
|
const p = Number(generatingPercents.value[studentId]) || 1
|
||||||
|
generatingPercents.value[studentId] = Math.min(p + 5, 95)
|
||||||
|
} else {
|
||||||
|
if (generatingPercents.value[studentId] !== undefined) {
|
||||||
|
delete generatingPercents.value[studentId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const p = Number(generatingPercents.value[studentId]) || 1
|
||||||
|
generatingPercents.value[studentId] = Math.min(p + 3, 95)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pollOnce()
|
||||||
|
pollingTimers[studentId] = setInterval(pollOnce, 10000)
|
||||||
|
}
|
||||||
|
function onLessonPlanGenerateSuccess(payload) {
|
||||||
|
const sid = payload?.studentId || selectedStudentIds.value?.[0]
|
||||||
|
startLessonPlanPolling(sid)
|
||||||
|
}
|
||||||
|
function ensurePollingForCurrentStudents() {
|
||||||
|
(students.value || []).forEach(s => startLessonPlanPolling(s.id))
|
||||||
|
}
|
||||||
|
function stopPolling(studentId) {
|
||||||
|
const t = pollingTimers[studentId]
|
||||||
|
if (t) {
|
||||||
|
clearInterval(t)
|
||||||
|
delete pollingTimers[studentId]
|
||||||
|
}
|
||||||
|
if (generatingPercents.value[studentId] !== undefined) {
|
||||||
|
delete generatingPercents.value[studentId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function stopAllPolling() {
|
||||||
|
Object.keys(pollingTimers).forEach(id => stopPolling(id))
|
||||||
|
}
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAllPolling()
|
||||||
|
})
|
||||||
|
onBeforeRouteLeave(() => {
|
||||||
|
stopAllPolling()
|
||||||
|
})
|
||||||
async function onDeleteStudent(row) {
|
async function onDeleteStudent(row) {
|
||||||
try {
|
try {
|
||||||
|
await ElMessageBox.confirm('确认删除该学生?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
await deleteStudent(row.id)
|
await deleteStudent(row.id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
if (selectedStudentIds.value?.length) {
|
if (selectedStudentIds.value?.length) {
|
||||||
@@ -319,11 +423,17 @@ async function onDeleteStudent(row) {
|
|||||||
}
|
}
|
||||||
await fetchStudents()
|
await fetchStudents()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e === 'cancel' || e === 'close') return
|
||||||
ElMessage.error('删除失败')
|
ElMessage.error('删除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function onDeleteClass(row) {
|
async function onDeleteClass(row) {
|
||||||
try {
|
try {
|
||||||
|
await ElMessageBox.confirm('确认删除该班级?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
await deleteClass(row.id)
|
await deleteClass(row.id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
if (selectedClassId.value === row.id) {
|
if (selectedClassId.value === row.id) {
|
||||||
@@ -333,6 +443,7 @@ async function onDeleteClass(row) {
|
|||||||
}
|
}
|
||||||
await fetchClasses()
|
await fetchClasses()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e === 'cancel' || e === 'close') return
|
||||||
ElMessage.error('删除失败')
|
ElMessage.error('删除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
<el-main class="h-full">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
||||||
<div class="text-lg font-semibold mb-4">学生详情</div>
|
<div class="panel-shell p-4 sm:p-6">
|
||||||
<template v-if="detail">
|
<div class="text-lg font-semibold mb-4">学生详情</div>
|
||||||
<el-descriptions :column="1" border>
|
<template v-if="detail">
|
||||||
<el-descriptions-item label="ID">{{ detail.id }}</el-descriptions-item>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
<el-descriptions-item label="ID">{{ detail.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
|
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
|
||||||
</template>
|
</el-descriptions>
|
||||||
<template v-else>
|
</template>
|
||||||
<el-empty description="请从班级页跳转" />
|
<template v-else>
|
||||||
</template>
|
<el-empty description="请从班级页跳转" />
|
||||||
</div>
|
</template>
|
||||||
|
</div>
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="panel-shell p-4 sm:p-6">
|
||||||
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
|
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
|
||||||
<template v-if="wordStat">
|
<template v-if="wordStat">
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount }}</el-descriptions-item>
|
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount }}</el-descriptions-item>
|
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount }}</el-descriptions-item>
|
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-empty description="暂无统计" />
|
<el-empty description="暂无统计" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-shell p-4 sm:p-6">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="text-md font-semibold mb-3">学生考试记录</div>
|
||||||
<div class="text-md font-semibold mb-3">学生考试记录</div>
|
<ExamHistoryChart :data="history" />
|
||||||
<ExamHistoryChart :data="history" />
|
</div>
|
||||||
</div>
|
<div class="panel-shell p-4 sm:p-6">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="text-md font-semibold mb-3">学生学案记录</div>
|
||||||
<div class="text-md font-semibold mb-3">学生学案记录</div>
|
<PlanHistoryChart :student-id="route.params.id" />
|
||||||
<PlanHistoryChart :student-id="route.params.id" />
|
</div>
|
||||||
</div>
|
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
||||||
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
<WordMasteryHeatmap :student-id="route.params.id" :columns="heatmapColumns" />
|
||||||
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
|
</div>
|
||||||
</div>
|
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="text-md font-semibold mb-3">学情分析</div>
|
||||||
<div class="flex items-center justify-between mb-3">
|
<StudyAnalysis :student-id="route.params.id" />
|
||||||
<div class="text-md font-semibold">学习分析</div>
|
|
||||||
<el-button type="primary" size="small" :loading="analyzeLoading" @click="fetchStudyAnalyze">
|
|
||||||
生成学习分析
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<template v-if="analysisHtml">
|
|
||||||
<div class="leading-7 text-gray-700 dark:text-gray-200" v-html="analysisHtml"></div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<el-empty description="点击右上按钮生成学习分析" />
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-main>
|
||||||
</el-main>
|
</el-container>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -74,29 +63,26 @@
|
|||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { getStudentDetail, getStudentStudyAnalyze } from '@/api/student'
|
import { getStudentDetail } from '@/api/student'
|
||||||
import { getStudentExamHistory } from '@/api/exam'
|
import { getStudentExamHistory } from '@/api/exam'
|
||||||
import { getWordStudentDetail } from '@/api/words'
|
import { getWordStudentDetail } from '@/api/words'
|
||||||
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
|
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
|
||||||
import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.vue'
|
import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.vue'
|
||||||
import WordMasteryHeatmap from '@/layouts/components/student/WordMasteryHeatmap.vue'
|
import WordMasteryHeatmap from '@/layouts/components/student/WordMasteryHeatmap.vue'
|
||||||
import MarkdownIt from 'markdown-it'
|
import StudyAnalysis from '@/layouts/components/student/StudyAnalysis.vue'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const detail = ref(null)
|
const detail = ref(null)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const history = ref([])
|
const history = ref([])
|
||||||
const analyzeLoading = ref(false)
|
|
||||||
const analysisText = ref('')
|
|
||||||
const wordStat = ref(null)
|
const wordStat = ref(null)
|
||||||
const md = new MarkdownIt({
|
const isMobile = ref(false)
|
||||||
html: false,
|
const heatmapColumns = computed(() => isMobile.value ? 20 : 50)
|
||||||
linkify: true,
|
|
||||||
breaks: true
|
function updateIsMobile() {
|
||||||
})
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
const analysisHtml = computed(() => {
|
}
|
||||||
return analysisText.value ? md.render(analysisText.value) : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
async function fetchDetail() {
|
async function fetchDetail() {
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
@@ -119,22 +105,10 @@ async function fetchExamHistory() {
|
|||||||
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
|
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
|
||||||
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
||||||
}) : []
|
}) : []
|
||||||
}
|
// 遍历 history 中的 startDate 去掉其中的 T
|
||||||
|
history.value.forEach(item => {
|
||||||
async function fetchStudyAnalyze() {
|
item.startDate = item.startDate.replace('T', ' ')
|
||||||
const id = route.params.id
|
})
|
||||||
if (!id) return
|
|
||||||
analyzeLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getStudentStudyAnalyze({
|
|
||||||
studentId: Number(id)
|
|
||||||
})
|
|
||||||
const d = res.data
|
|
||||||
const raw = typeof d?.data === 'string' ? d.data : ''
|
|
||||||
analysisText.value = raw.replace(/\\n/g, '\n')
|
|
||||||
} finally {
|
|
||||||
analyzeLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchWordStat() {
|
async function fetchWordStat() {
|
||||||
@@ -149,5 +123,7 @@ onMounted(() => {
|
|||||||
fetchDetail()
|
fetchDetail()
|
||||||
fetchExamHistory()
|
fetchExamHistory()
|
||||||
fetchWordStat()
|
fetchWordStat()
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user