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();
|
||||
|
||||
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("/plan/word/voice")
|
||||
.notMatch("/plan/word/voice/tts")
|
||||
.check(r -> StpUtil.checkLogin());
|
||||
|
||||
SaRouter.match("/admin/**")
|
||||
.notMatch("/plan/word/voice")
|
||||
.notMatch("/plan/word/voice/tts")
|
||||
.check(r -> StpUtil.checkRole("root"));
|
||||
|
||||
}))
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns("/error");
|
||||
|
||||
@@ -28,7 +28,11 @@ public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
@Override
|
||||
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) {
|
||||
|
||||
@@ -31,9 +31,15 @@ public class ExamWordsConstant {
|
||||
public static final int ZONE_F_SIZE = 7;
|
||||
|
||||
|
||||
// 摸底
|
||||
public static final int EXAM_TYPE_BASELINE = 1;
|
||||
// 中期
|
||||
public static final int EXAM_TYPE_MIDTERM = 2;
|
||||
// 期末
|
||||
public static final int EXAM_TYPE_FINAL = 3;
|
||||
// 小测
|
||||
public static final int EXAM_TYPE_TEST = 4;
|
||||
|
||||
|
||||
public static int getZoneA(int gradeId) {
|
||||
return switch (gradeId) {
|
||||
@@ -131,4 +137,17 @@ public class ExamWordsConstant {
|
||||
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;
|
||||
|
||||
public interface LessonPlanConstant {
|
||||
public class LessonPlanConstant {
|
||||
|
||||
String TITLE = "Title";
|
||||
String PASSAGE = "The Passage";
|
||||
String QUIZ = "Quiz";
|
||||
String ANSWER_KEY_EXPLANATION = "Answer Key & Explanation";
|
||||
String FULL_TRANSLATION = "Full Translation";
|
||||
public static final String TITLE = "Title";
|
||||
public static final String PASSAGE = "ThePassage";
|
||||
public static final String QUIZ = "Quiz";
|
||||
public static final String ANSWER_KEY_EXPLANATION = "AnswerKey&Explanation";
|
||||
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_INVITATION_CODE = "user:invitation:code:";
|
||||
|
||||
public static String buildUserLoginCode(String 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;
|
||||
|
||||
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
|
||||
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.FindUserListRepVO;
|
||||
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.Response;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RequestMapping("/admin/")
|
||||
@RestController
|
||||
@@ -30,6 +36,8 @@ public class AdminController {
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@PostMapping("user/list")
|
||||
@ApiOperationLog(description = "查询用户列表")
|
||||
@@ -66,4 +74,18 @@ public class AdminController {
|
||||
|
||||
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
|
||||
examWordsDO.setWordIds(assessmentWords.stream().map(Word::getId).toList());
|
||||
examWordsService.updateExamWordsWordIdsOrder(examWordsDO);
|
||||
|
||||
log.info("生成试卷成功 {}", examWordsDO);
|
||||
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
|
||||
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
@@ -72,6 +72,12 @@ public class ExamWordsController {
|
||||
data.put("examStr", examWordsDO.getTitle());
|
||||
data.put("words", 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;
|
||||
}).toList();
|
||||
|
||||
@@ -107,12 +113,17 @@ public class ExamWordsController {
|
||||
PageResponse<ExamWordsResultRspVO> getExamWordsResult(@RequestBody ExamWordsResultReqVO examWordsResultReqVO) {
|
||||
Integer page = examWordsResultReqVO.getPage();
|
||||
Integer size = examWordsResultReqVO.getSize();
|
||||
Integer classId = examWordsResultReqVO.getClassId();
|
||||
Integer gradeId = examWordsResultReqVO.getGradeId();
|
||||
String studentName = examWordsResultReqVO.getStudentName();
|
||||
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
|
||||
.builder()
|
||||
.id(examWordsJudgeResultDO.getId())
|
||||
.studentId(examWordsJudgeResultDO.getStudentId())
|
||||
.studentName(examWordsJudgeResultDO.getStudentId() != null ? studentService.getStudentById(examWordsJudgeResultDO.getStudentId()).getName() : "")
|
||||
.examWordsTitle(examWordsJudgeResultDO.getExamWordsId() != null ? examWordsService.getExamWordsDOById(examWordsJudgeResultDO.getExamWordsId()).getTitle() : "")
|
||||
.examWordsId(examWordsJudgeResultDO.getExamWordsId())
|
||||
.startDate(examWordsJudgeResultDO.getStartDate())
|
||||
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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.VocabularyBankDO;
|
||||
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.WordExportUtil;
|
||||
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.util.JsonUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -30,6 +33,9 @@ public class LessonPlanController {
|
||||
@Resource
|
||||
private LessonPlansService lessonPlanService;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Resource(name = "taskExecutor")
|
||||
private Executor taskExecutor;
|
||||
@Resource
|
||||
@@ -47,6 +53,9 @@ public class LessonPlanController {
|
||||
Integer unitId = addLessonPlanReqVO.getUnitId();
|
||||
Integer wordSize = addLessonPlanReqVO.getWordSize();
|
||||
try {
|
||||
if (redisTemplate.opsForValue().get(LessonPlanConstant.buildGeneratePlanContent(studentId)) != null) {
|
||||
throw new RuntimeException("学案正在生成,请耐心等待");
|
||||
}
|
||||
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId, wordSize));
|
||||
return Response.success("生成学案成功,请等待 10 分钟");
|
||||
} catch (Exception e) {
|
||||
@@ -96,4 +105,31 @@ public class LessonPlanController {
|
||||
public void findPlanWordVoiceTTS(@RequestBody FindWordTTSVoiceReqVO findWordVoiceReqVO, HttpServletResponse 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 = "登录")
|
||||
public Response<String> login(@RequestBody LoginReqVO loginReqVO) {
|
||||
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());
|
||||
} catch (Exception e) {
|
||||
log.error("注册或登录失败 {}", e.getMessage());
|
||||
|
||||
@@ -48,6 +48,8 @@ public class StudentController {
|
||||
.id(studentDO.getId())
|
||||
.name(studentDO.getName())
|
||||
.classId(studentDO.getClassId())
|
||||
.className(classService.findClassById(studentDO.getClassId()).getTitle())
|
||||
.gradeName(gradeService.findByClassId(studentDO.getGradeId()).getTitle())
|
||||
.gradeId(studentDO.getGradeId())
|
||||
.build()).toList();
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
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.model.vo.user.FindUserInfoRspVO;
|
||||
import com.yinlihupo.enlish.service.model.vo.user.UpdateUserInfoReqVO;
|
||||
import com.yinlihupo.enlish.service.service.UserService;
|
||||
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||
import com.yinlihupo.framework.common.response.Response;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user/")
|
||||
@Slf4j
|
||||
public class UserController {
|
||||
|
||||
@Resource
|
||||
@@ -27,4 +33,23 @@ public class UserController {
|
||||
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 selectUnfinishedCount();
|
||||
|
||||
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
|
||||
|
||||
List<ExamWordsJudgeResultDO> selectByStudentId(@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);
|
||||
|
||||
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 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 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);
|
||||
|
||||
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
||||
|
||||
@@ -26,4 +26,6 @@ public interface VocabularyBankDOMapper {
|
||||
Integer selectWordTotal();
|
||||
|
||||
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 size;
|
||||
|
||||
private Integer classId;
|
||||
private Integer gradeId;
|
||||
private String studentName;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Builder
|
||||
public class ExamWordsResultRspVO {
|
||||
|
||||
private Integer id;
|
||||
|
||||
private String ansSheetPath;
|
||||
@@ -20,6 +21,10 @@ public class ExamWordsResultRspVO {
|
||||
|
||||
private Integer examWordsId;
|
||||
|
||||
private String studentName;
|
||||
|
||||
private String examWordsTitle;
|
||||
|
||||
private Integer correctWordCount;
|
||||
|
||||
private Integer wrongWordCount;
|
||||
|
||||
@@ -15,4 +15,5 @@ public class LoginReqVO {
|
||||
private String name;
|
||||
private String password;
|
||||
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;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -16,5 +17,6 @@ public class AddStudentReqVO {
|
||||
private String name;
|
||||
private Integer classId;
|
||||
private Integer gradeId;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
|
||||
@@ -15,4 +15,6 @@ public class StudentItemRspVO {
|
||||
private String name;
|
||||
private Integer classId;
|
||||
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);
|
||||
|
||||
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize);
|
||||
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize, Integer classId, Integer gradeId, String studentName);
|
||||
|
||||
Integer getExamWordsJudgeResultCount();
|
||||
|
||||
Integer getExamUnfinishedCount();
|
||||
|
||||
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
|
||||
|
||||
List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId);
|
||||
|
||||
@@ -13,4 +13,6 @@ public interface ExamWordsService {
|
||||
int saveExamWordsPngToDbAndLocal(MultipartFile file);
|
||||
|
||||
void updateExamWordsWordIdsOrder(ExamWordsDO examWordsDO);
|
||||
|
||||
ExamWordsDO getExamWordsDOById(Integer id);
|
||||
}
|
||||
|
||||
@@ -10,4 +10,6 @@ public interface LessonPlansService {
|
||||
List<LessonPlansDO> findLessonPlans(List<Integer> ids);
|
||||
|
||||
LessonPlansDO findLessonPlanById(Integer id);
|
||||
|
||||
List<LessonPlansDO> findLessonPlansByStudentId(Integer studentId);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.yinlihupo.enlish.service.service;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ public interface UserService {
|
||||
Integer findUserTotal();
|
||||
|
||||
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.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
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.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -47,7 +48,6 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
||||
@Transactional(rollbackFor = RuntimeException.class)
|
||||
public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
|
||||
|
||||
|
||||
ExamWordsDO examWordsDO;
|
||||
|
||||
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
|
||||
@@ -61,6 +61,16 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -109,7 +119,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
||||
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||
Integer gradeId = studentDO.getGradeId();
|
||||
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());
|
||||
return getExamWordsDO(studentId, examWordsDO);
|
||||
|
||||
@@ -119,13 +129,13 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
||||
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||
Integer gradeId = studentDO.getGradeId();
|
||||
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());
|
||||
return getExamWordsDO(studentId, examWordsDO);
|
||||
}
|
||||
|
||||
@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()) {
|
||||
throw new RuntimeException("没有找到对应的单元");
|
||||
}
|
||||
@@ -134,7 +144,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
||||
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
||||
.gradeId(gradeId)
|
||||
.level(1)
|
||||
.type(ExamWordsConstant.EXAM_TYPE_BASELINE)
|
||||
.type(type)
|
||||
.title(studentDO.getName())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
|
||||
@@ -161,29 +171,55 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = RuntimeException.class)
|
||||
public int saveExamWordsPngToDbAndLocal(MultipartFile file) {
|
||||
|
||||
File dir = new File(tmpPng);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
// 1. 基础校验:判空
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new RuntimeException("上传文件不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 安全校验:检查后缀名白名单
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String suffix = "";
|
||||
if (originalFilename != null && originalFilename.contains(".")) {
|
||||
suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
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);
|
||||
}
|
||||
String newFileName = UUID.randomUUID() + suffix;
|
||||
String path = tmpPng + newFileName;
|
||||
|
||||
File dest = new File(path);
|
||||
file.transferTo(dest);
|
||||
// 3. 准备目录 (使用 NIO)
|
||||
// 假设 tmpPng 是配置好的基础路径字符串
|
||||
Path directoryPath = Paths.get(tmpPng);
|
||||
try {
|
||||
if (!Files.exists(directoryPath)) {
|
||||
Files.createDirectories(directoryPath);
|
||||
}
|
||||
|
||||
// 4. 生成文件名 (防止文件名冲突)
|
||||
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) {
|
||||
throw new RuntimeException("上传失败", e);
|
||||
log.error("文件上传失败: {}", originalFilename, e);
|
||||
throw new RuntimeException("上传失败,请稍后重试", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,5 +227,10 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
||||
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.ZoneStats;
|
||||
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
||||
import com.yinlihupo.enlish.service.service.StudentLessonPlansService;
|
||||
import com.yinlihupo.enlish.service.utils.PngUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -40,23 +41,29 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
||||
private GradeUnitDOMapper gradeUnitDOMapper;
|
||||
@Resource
|
||||
private StudentDOMapper studentDOMapper;
|
||||
@Resource
|
||||
private PlanExamDOMapper planExamDOMapper;
|
||||
@Resource
|
||||
private StudentLessonPlansService studentLessonPlansService;
|
||||
|
||||
@Value("${templates.data}")
|
||||
private String tessdataPath;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void judgeExamWords(int count) {
|
||||
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
|
||||
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
|
||||
String ansSheetPath = null;
|
||||
try {
|
||||
String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
|
||||
ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
|
||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
||||
|
||||
// 从图片中获取学生 id 和考试 id
|
||||
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
||||
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
|
||||
if (studentExamId == null) {
|
||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生 id 和考试 id");
|
||||
log.info("未找到学生 id 和考试 id");
|
||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生和考试");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -74,7 +81,7 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
||||
}
|
||||
|
||||
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
||||
if(examWordsDO == null) {
|
||||
if (examWordsDO == null) {
|
||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
|
||||
continue;
|
||||
}
|
||||
@@ -93,9 +100,13 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
||||
.wrongWordCount(unmemorizedWordIds.size())
|
||||
.isFinished(1)
|
||||
.build();
|
||||
|
||||
if (examWordsDO.getType().equals(ExamWordsConstant.EXAM_TYPE_BASELINE)) {
|
||||
// 判断考试等级
|
||||
judgeExamActualGrade(wordsJudgeResultDO, examWordsDO);
|
||||
|
||||
} else {
|
||||
wordsJudgeResultDO.setMsg("此次考试" + examWordsDO.getTitle() + "答对单词数为" + memorizedWordIds.size());
|
||||
}
|
||||
int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO);
|
||||
if (updated != 1) {
|
||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新考试记录失败");
|
||||
@@ -103,6 +114,12 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
||||
}
|
||||
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()
|
||||
.wordId(wordId)
|
||||
.studentId(studentId)
|
||||
@@ -131,9 +148,14 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
||||
boolean delete = new File(ansSheetPath).delete();
|
||||
if (delete) {
|
||||
log.info("删除文件成功:{}", ansSheetPath);
|
||||
} else {
|
||||
log.error("删除文件失败:{}", ansSheetPath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("识别考试失败 {}", e.getMessage());
|
||||
if (ansSheetPath != null) {
|
||||
new File(ansSheetPath).delete();
|
||||
}
|
||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -310,8 +332,24 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize) {
|
||||
public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize, Integer classId, Integer gradeId, String studentName) {
|
||||
|
||||
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
|
||||
@@ -319,6 +357,11 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
||||
return examWordsJudgeResultDOMapper.selectCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getExamUnfinishedCount() {
|
||||
return examWordsJudgeResultDOMapper.selectUnfinishedCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
|
||||
return examWordsJudgeResultDOMapper.selectDetailById(id);
|
||||
|
||||
@@ -13,11 +13,15 @@ import com.yinlihupo.enlish.service.service.LoginService;
|
||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -28,20 +32,32 @@ public class LoginServiceImpl implements LoginService {
|
||||
@Resource
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Resource
|
||||
private Client client;
|
||||
|
||||
@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);
|
||||
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 (code == null || !code.equals(reqCode)) {
|
||||
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()
|
||||
.phone(phone)
|
||||
.name(name)
|
||||
@@ -68,7 +84,9 @@ public class LoginServiceImpl implements LoginService {
|
||||
@Override
|
||||
public void sendVerificationCode(String phone) {
|
||||
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 templateCode = "SMS_154950909";
|
||||
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
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.domain.dataobject.*;
|
||||
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.utils.DifyArticleClient;
|
||||
import com.yinlihupo.enlish.service.utils.DifyClient;
|
||||
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
|
||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -35,17 +39,34 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
@Resource
|
||||
private GradeDOMapper gradeDOMapper;
|
||||
@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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
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);
|
||||
UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId);
|
||||
GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId);
|
||||
GradeDO gradeDO = gradeDOMapper.selectById(gradeUnitDO.getGradeId());
|
||||
|
||||
List<VocabularyBankDO> totalWords = new ArrayList<>();
|
||||
|
||||
// 补差词汇所用词汇的
|
||||
List<VocabularyBankDO> vocabularyBankListStudentNotMaster = getVocabListRandom(vocabularyBankDOMapper
|
||||
.selectVocabularyBankListStudentNotMaster(gradeUnitDO.getGradeId(), studentId), 50);
|
||||
@@ -57,18 +78,21 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
int checkTotal = 50;
|
||||
List<List<VocabularyBankDO>> weeksSync = new ArrayList<>();
|
||||
List<List<VocabularyBankDO>> weeksGap = new ArrayList<>();
|
||||
int j = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
List<VocabularyBankDO> syncVocabList;
|
||||
if ((i + 1) * wordSize < syncSize) {
|
||||
syncVocabList = vocabularyBankDOS.subList(i * wordSize, (i + 1) * wordSize);
|
||||
} else if (i == 4) {
|
||||
syncVocabList = vocabularyBankDOS.subList(i * wordSize, syncSize);
|
||||
} 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));
|
||||
weeksSync.add(syncVocabList);
|
||||
weeksGap.add(gapVocabList);
|
||||
|
||||
totalWords.addAll(syncVocabList);
|
||||
totalWords.addAll(gapVocabList);
|
||||
|
||||
List<VocabularyBankDO> reviewVocabList = new ArrayList<>();
|
||||
List<VocabularyBankDO> checkList = new ArrayList<>();
|
||||
// 艾宾浩斯遗忘曲线
|
||||
@@ -118,21 +142,29 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
.build();
|
||||
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) {
|
||||
throw new RuntimeException(e);
|
||||
log.info("生成第{}天计划失败,失败原因 {}", i + 1, e.getMessage());
|
||||
}
|
||||
log.info("生成第{}天计划成功", i + 1);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
int syncWeekender = syncSize / 2;
|
||||
log.info("开始生成周末计划");
|
||||
int syncWeekendSize = totalWords.size() / 2;
|
||||
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);
|
||||
|
||||
LessonPlansDO lessonPlansDO = LessonPlansDO.builder()
|
||||
.title(map.get("title").toString())
|
||||
.title(map.get("examStr").toString() + "复习")
|
||||
.gradeId(gradeDO.getId().toString())
|
||||
.unitId(unitDO.getId())
|
||||
.createdAt(LocalDateTime.now())
|
||||
@@ -140,6 +172,13 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
.build();
|
||||
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()
|
||||
.studentId(studentId)
|
||||
.planId(lessonPlansDO.getId())
|
||||
@@ -162,14 +201,43 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
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,
|
||||
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws IOException {
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("title", "第" + day + "天" + "复习" + gradeDO.getTitle() + unitDO.getTitle() + studentId);
|
||||
data.put("checkList", checkList);
|
||||
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());
|
||||
|
||||
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();
|
||||
// Configure config = Configure.builder()
|
||||
// .bind("checkList", policy)
|
||||
@@ -188,7 +256,8 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
List<VocabularyBankDO> checkList,
|
||||
int day,
|
||||
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<>();
|
||||
data.put("title", title);
|
||||
data.put("syncVocabList", syncVocabList);
|
||||
@@ -196,15 +265,17 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
data.put("reviewVocabList", reviewVocabList);
|
||||
data.put("checkList", 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);
|
||||
data.put("drillRound1", drillRound1);
|
||||
List<VocabularyBankDO> drillRound2 = new ArrayList<>(syncVocabList);
|
||||
List<Word> drillRound2 = new ArrayList<>(list);
|
||||
Collections.shuffle(drillRound2);
|
||||
data.put("drillRound2", drillRound2);
|
||||
List<VocabularyBankDO> drillRound3 = new ArrayList<>(syncVocabList);
|
||||
List<Word> drillRound3 = new ArrayList<>(list);
|
||||
Collections.shuffle(drillRound3);
|
||||
data.put("drillRound3", drillRound3);
|
||||
|
||||
@@ -213,8 +284,10 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
mixedDrill.addAll(syncVocabList);
|
||||
mixedDrill.addAll(gapVocabList);
|
||||
mixedDrill.addAll(reviewVocabList);
|
||||
Collections.shuffle(mixedDrill);
|
||||
data.put("mixedDrill", mixedDrill);
|
||||
List<Word> mixedList = new ArrayList<>(mixedDrill.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList());
|
||||
mixedList.forEach(word -> word.setDefinition(" "));
|
||||
Collections.shuffle(mixedList);
|
||||
data.put("mixedDrill", mixedList);
|
||||
|
||||
// 文章 A
|
||||
log.info("生成文章 A 中文开始");
|
||||
@@ -241,6 +314,50 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
data.put("articleBans", mapB.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION));
|
||||
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();
|
||||
// Configure config = Configure.builder()
|
||||
// .bind("syncVocabList", policy)
|
||||
@@ -271,7 +388,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
||||
int i = 0;
|
||||
do {
|
||||
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);
|
||||
} while (map.get(LessonPlanConstant.TITLE) == 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<String> user2RoleKeys = roleDOs.stream().map(RoleDO::getRoleKey).toList();
|
||||
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.vo.student.AddStudentReqVO;
|
||||
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 jakarta.annotation.Resource;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -37,7 +36,7 @@ public class StudentServiceImpl implements StudentService {
|
||||
@Resource
|
||||
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
|
||||
@Resource
|
||||
private DifyArticleClient difyArticleClient;
|
||||
private DifyClient difyClient;
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@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());
|
||||
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));
|
||||
|
||||
List<ExamWordsJudgeResultDetail> examWordsJudgeResultDetails = new ArrayList<>();
|
||||
@@ -159,7 +161,7 @@ public class StudentServiceImpl implements StudentService {
|
||||
studentStudyInfo.put("单词掌握情况", wordMasteryDetails);
|
||||
|
||||
try {
|
||||
String analyze = difyArticleClient.sendStudentAnalyze(JsonUtils.toJsonString(studentStudyInfo)).getAnswer();
|
||||
String analyze = difyClient.sendStudentAnalyze(JsonUtils.toJsonString(studentStudyInfo)).getAnswer();
|
||||
// 设置过期时间 3 天
|
||||
redisTemplate.opsForValue().set(key, analyze);
|
||||
redisTemplate.expire(key, 3, TimeUnit.DAYS);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package com.yinlihupo.enlish.service.service.user;
|
||||
|
||||
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.mapper.UserDOMapper;
|
||||
import com.yinlihupo.enlish.service.service.UserService;
|
||||
import jakarta.annotation.Resource;
|
||||
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 java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -17,8 +21,10 @@ public class UserServiceImpl implements UserService {
|
||||
|
||||
@Resource
|
||||
private UserDOMapper userDOMapper;
|
||||
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Resource
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public UserDO findUser() {
|
||||
@@ -43,4 +49,21 @@ public class UserServiceImpl implements UserService {
|
||||
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 jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AutoJudgeExamWordsTask {
|
||||
|
||||
@Resource
|
||||
@@ -15,7 +15,9 @@ public class AutoJudgeExamWordsTask {
|
||||
|
||||
@Scheduled(fixedRate = 5000)
|
||||
public void autoJudgeExamWords() {
|
||||
System.out.println("【固定频率】开始自动判卷,时间:" + LocalDateTime.now());
|
||||
if (examWordsJudgeService.getExamUnfinishedCount() != 0) {
|
||||
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.JsonProperty;
|
||||
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.ToString;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -14,21 +17,25 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class DifyArticleClient {
|
||||
public class DifyClient {
|
||||
|
||||
@Value("${ai.key}")
|
||||
private String apiKey;
|
||||
private String anaKey = "app-hrUFcopdcpnflsvpHWRuBfCp";
|
||||
@Value("${ai.analyzeKey}")
|
||||
private String analyzeKey;
|
||||
@Value("${ai.sentenceKey}")
|
||||
private String sentenceKey;
|
||||
@Value("${ai.url}")
|
||||
private String baseUrl;
|
||||
private final HttpClient httpClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// 构造函数
|
||||
public DifyArticleClient() {
|
||||
public DifyClient() {
|
||||
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10)) // 连接超时
|
||||
@@ -36,6 +43,48 @@ public class DifyArticleClient {
|
||||
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 {
|
||||
String endpoint = this.baseUrl;
|
||||
|
||||
@@ -52,7 +101,7 @@ public class DifyArticleClient {
|
||||
// 3. 构建 HTTP 请求
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(endpoint))
|
||||
.header("Authorization", "Bearer " + anaKey)
|
||||
.header("Authorization", "Bearer " + analyzeKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||
.timeout(Duration.ofSeconds(30)) // 读取超时
|
||||
@@ -15,6 +15,7 @@ import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -31,29 +32,11 @@ public class PngUtil {
|
||||
// 获取起始坐标
|
||||
public static List<CoordinatesXY> analysisXY(String imagePath) {
|
||||
|
||||
Mat binary = image2BinaryMath(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. 查找轮廓
|
||||
List<MatOfPoint> contours = new ArrayList<>();
|
||||
Mat hierarchy = new Mat();
|
||||
// RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点
|
||||
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
||||
|
||||
System.out.println("检测到的轮廓总数: " + contours.size());
|
||||
@@ -89,67 +72,44 @@ public class PngUtil {
|
||||
System.out.println("------------------------------------------------");
|
||||
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.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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
Imgcodecs.imwrite("output_red.png", src);
|
||||
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<CoordinatesXY> ans = getCoordinatesXIES(list, height);
|
||||
list.forEach(coordinatesXY -> coordinatesXY.setHeight(coordinatesXY.getHeight() / 51));
|
||||
list.forEach(coordinatesXY -> coordinatesXY.setWidth(coordinatesXY.getWidth() / 3));
|
||||
list.forEach(coordinatesXY -> coordinatesXY.setX(coordinatesXY.getX() + coordinatesXY.getWidth() * 2));
|
||||
|
||||
src.release();
|
||||
binary.release();
|
||||
hierarchy.release();
|
||||
binary.release();
|
||||
log.info("起始坐标: {}", list);
|
||||
|
||||
return ans;
|
||||
return list;
|
||||
}
|
||||
|
||||
// 获取(未背熟)单词的 id
|
||||
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 = new Mat();
|
||||
Mat binary = image2BinaryMath(filePath);
|
||||
|
||||
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<>();
|
||||
int words_index = 0;
|
||||
|
||||
for (int i = 0; i < coordinatesXYList.size(); i++) {
|
||||
CoordinatesXY coordinatesXY = coordinatesXYList.get(i);
|
||||
|
||||
for (CoordinatesXY coordinatesXY : coordinatesXYList) {
|
||||
int width = coordinatesXY.getWidth();
|
||||
int height = coordinatesXY.getHeight();
|
||||
int currentX = coordinatesXY.getX();
|
||||
int currentY = coordinatesXY.getY();
|
||||
|
||||
int count = i == 0 ? ExamWordsConstant.PGN_COL - 1 : ExamWordsConstant.PGN_COL;
|
||||
int currentY = coordinatesXY.getY() + height;
|
||||
|
||||
// 内层循环:遍历这一列的每一行
|
||||
for (int j = 0; j < count; j++) {
|
||||
for (int j = 0; j < 50; j++) {
|
||||
// 安全检查:防止单词列表比格子少导致越界
|
||||
if (words_index >= wordIds.size()) {
|
||||
log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
|
||||
@@ -168,12 +128,15 @@ public class PngUtil {
|
||||
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
|
||||
Mat region = binary.submat(rect);
|
||||
int countNonZero = Core.countNonZero(region);
|
||||
|
||||
if (countNonZero > 500) {
|
||||
log.info("当前位置为 words_index={},坐标为 x={} y={} 当前区域非零像素数: {}", words_index, currentX, currentY, countNonZero);
|
||||
if (countNonZero > 1000) {
|
||||
Integer id = wordIds.get(words_index);
|
||||
answer.add(id);
|
||||
log.info("检测到标记(未背熟):ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
|
||||
}
|
||||
if (countNonZero == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
region.release();
|
||||
words_index++;
|
||||
@@ -185,8 +148,6 @@ public class PngUtil {
|
||||
|
||||
} finally {
|
||||
|
||||
src.release();
|
||||
gray.release();
|
||||
binary.release();
|
||||
}
|
||||
}
|
||||
@@ -206,21 +167,21 @@ public class PngUtil {
|
||||
Rect roiRect = new Rect(0, 0, left.getX(), left.getY());
|
||||
Mat roi = new Mat(src, roiRect);
|
||||
|
||||
// 3. 图像预处理 (提高 OCR 准确率)
|
||||
// 3.1 转为灰度图
|
||||
Mat gray = new Mat();
|
||||
Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
|
||||
|
||||
// 3.2 二值化 (Thresholding)
|
||||
// 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
|
||||
Mat binary = new Mat();
|
||||
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
|
||||
// // 3. 图像预处理 (提高 OCR 准确率)
|
||||
// // 3.1 转为灰度图
|
||||
// Mat gray = new Mat();
|
||||
// Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
|
||||
//
|
||||
// // 3.2 二值化 (Thresholding)
|
||||
// // 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
|
||||
// Mat binary = new Mat();
|
||||
// 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 使用)
|
||||
BufferedImage processedImage = matToBufferedImage(binary);
|
||||
BufferedImage processedImage = matToBufferedImage(src);
|
||||
|
||||
// 5. 使用 Tesseract 进行 OCR 识别
|
||||
ITesseract instance = new Tesseract();
|
||||
@@ -245,6 +206,50 @@ public class PngUtil {
|
||||
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) {
|
||||
Matcher matcher = pattern.matcher(result);
|
||||
StudentExamId studentExamId = new StudentExamId(0, 0);
|
||||
@@ -276,21 +281,4 @@ public class PngUtil {
|
||||
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();
|
||||
config = Configure.builder()
|
||||
.bind("words", policy)
|
||||
.bind("words1", policy)
|
||||
.bind("words2", policy)
|
||||
.bind("answer", policy)
|
||||
.build();
|
||||
|
||||
@@ -50,11 +52,16 @@ public class WordExportUtil {
|
||||
.bind("mixedDrill", policyLessonPlanWeekday)
|
||||
.bind("checkList", policyLessonPlanWeekday)
|
||||
.bind("checkListAns", policyLessonPlanWeekday)
|
||||
.bind("sentences", policyLessonPlanWeekday)
|
||||
.bind("sentencesAns", policyLessonPlanWeekday)
|
||||
.bind("words1", policy)
|
||||
.bind("words2", policy)
|
||||
.build();
|
||||
|
||||
LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy();
|
||||
configLessonPlanWeekend = Configure.builder()
|
||||
.bind("checkList", policyLessonPlan)
|
||||
.bind("words1", policy)
|
||||
.bind("words2", policy)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -94,7 +101,7 @@ public class WordExportUtil {
|
||||
} else {
|
||||
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());
|
||||
OutputStream out = response.getOutputStream();
|
||||
template.render(map);
|
||||
|
||||
@@ -2,7 +2,7 @@ spring:
|
||||
datasource:
|
||||
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 # 数据库用户名
|
||||
password: YLHP@admin123 # 数据库密码
|
||||
data:
|
||||
@@ -31,18 +31,20 @@ spring:
|
||||
|
||||
|
||||
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
|
||||
data: C:\project\tess
|
||||
plan:
|
||||
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v3.docx
|
||||
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v1.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_v3.docx
|
||||
plan_day: 7
|
||||
tmp:
|
||||
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\
|
||||
png:
|
||||
|
||||
ai:
|
||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||
analyzeKey: app-hrUFcopdcpnflsvpHWRuBfCp
|
||||
sentenceKey: app-Emk5YQBaD2YruRXuE5sK1vEU
|
||||
url: https://chat.cosonggle.com/v1/chat-messages
|
||||
|
||||
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:
|
||||
profiles:
|
||||
active: dev # 默认激活 dev 本地开发环境
|
||||
|
||||
active: pro # 默认激活 dev 本地开发环境
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 30MB
|
||||
max-request-size: 30MB
|
||||
mybatis:
|
||||
# MyBatis xml 配置文件路径
|
||||
mapper-locations: classpath:/mapper/**/*.xml
|
||||
|
||||
|
||||
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
|
||||
sa-token:
|
||||
# token 名称(同时也是 cookie 名称)
|
||||
@@ -27,3 +31,6 @@ sa-token:
|
||||
is-share: true
|
||||
# 是否输出操作日志
|
||||
is-log: true
|
||||
#logging:
|
||||
# level:
|
||||
# com.yinlihupo.enlish.service.domain.mapper: debug
|
||||
@@ -45,7 +45,7 @@
|
||||
targetProject="src/main/java"/>
|
||||
|
||||
<!-- 需要生成的表-实体类 -->
|
||||
<table tableName="student_stage_learning_remark" domainObjectName="StudentStageLearningRemarkDO"
|
||||
<table tableName="plan_exam" domainObjectName="PlanExamDO"
|
||||
enableCountByExample="false"
|
||||
enableUpdateByExample="false"
|
||||
enableDeleteByExample="false"
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
</resultMap>
|
||||
|
||||
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into exam_words (grade_id, level, title, word_ids, created_at)
|
||||
VALUES (#{gradeId}, #{level}, #{title}, #{wordIds, typeHandler=com.yinlihupo.enlish.service.config.ListWordIdTypeHandler}, #{createdAt})
|
||||
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}, #{type}, #{createdAt})
|
||||
</insert>
|
||||
|
||||
<update id="updateWordIdsOrder">
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
select count(1)
|
||||
from exam_words_judge_result
|
||||
</select>
|
||||
|
||||
<select id="selectDetailById" resultMap="ResultMapWithBLOBs">
|
||||
select *
|
||||
from exam_words_judge_result
|
||||
@@ -81,4 +82,22 @@
|
||||
and start_date between date_sub(now(), interval 7 day) and now()
|
||||
</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>
|
||||
@@ -39,4 +39,15 @@
|
||||
where id = #{lessonId}
|
||||
</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>
|
||||
@@ -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}
|
||||
and is_deleted = 0
|
||||
</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>
|
||||
@@ -21,7 +21,6 @@
|
||||
from student_exam_words
|
||||
where student_id = #{studentId}
|
||||
and exam_words_id = #{examWordsId}
|
||||
and is_completed = 0
|
||||
</select>
|
||||
|
||||
<update id="updateStudentExamWordsFinished">
|
||||
|
||||
@@ -20,6 +20,28 @@
|
||||
values (#{phone}, #{name}, #{password})
|
||||
</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 *
|
||||
from user
|
||||
|
||||
@@ -145,5 +145,21 @@
|
||||
order by rand()
|
||||
limit 100
|
||||
</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>
|
||||
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;
|
||||
|
||||
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.framework.common.util.JsonUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@SpringBootTest
|
||||
public class AITest {
|
||||
|
||||
@Resource
|
||||
private DifyArticleClient client;
|
||||
private DifyClient client;
|
||||
@Resource
|
||||
private VocabularyBankDOMapper vocabularyBankDOMapper;
|
||||
|
||||
@Test
|
||||
public void test1() throws IOException {
|
||||
|
||||
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -35,68 +35,68 @@ public class TestVocabularyBankInsert {
|
||||
private GradeUnitDOMapper gradeUnitDOMapper;
|
||||
@Test
|
||||
void test() {
|
||||
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<>();
|
||||
int gradeId = 8;
|
||||
try (FileInputStream fis = new FileInputStream(file); Workbook workbook = new XSSFWorkbook(fis)) {
|
||||
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
|
||||
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
||||
Row row = sheet.getRow(i);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 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<>();
|
||||
// int gradeId = 8;
|
||||
// try (FileInputStream fis = new FileInputStream(file); Workbook workbook = new XSSFWorkbook(fis)) {
|
||||
//
|
||||
// Sheet sheet = workbook.getSheetAt(0);
|
||||
//
|
||||
// for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
||||
// Row row = sheet.getRow(i);
|
||||
// if (row == null) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// String word = row.getCell(0).getStringCellValue();
|
||||
// String pronunciation = row.getCell(1) != null ? row.getCell(1).getStringCellValue() : "";
|
||||
// String pos = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : "";
|
||||
// String meaning = row.getCell(3) != null ? row.getCell(3).getStringCellValue() : "";
|
||||
// String gradeUnit = row.getCell(4) != null ? row.getCell(4).getStringCellValue() : "";
|
||||
|
||||
String word = row.getCell(0).getStringCellValue();
|
||||
String meaning = row.getCell(1) != null ? row.getCell(1).getStringCellValue() : "";
|
||||
String gradeUnit = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : "";
|
||||
String pronunciation = "";
|
||||
String pos = "";
|
||||
if (word.contains("Unit")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int gradeUnitId;
|
||||
if (map.containsKey(gradeUnit)) {
|
||||
gradeUnitId = map.get(gradeUnit);
|
||||
} else {
|
||||
UnitDO unitDO = unitDOMapper.selectByTitle(gradeUnit);
|
||||
if (unitDO == null) {
|
||||
unitDO = UnitDO.builder()
|
||||
.title(gradeUnit)
|
||||
.version("人教版")
|
||||
.createAt(LocalDateTime.now())
|
||||
.build();
|
||||
unitDOMapper.insert(unitDO);
|
||||
gradeUnitDOMapper.insert(GradeUnitDO.builder().unitId(unitDO.getId()).gradeId(gradeId).build());
|
||||
gradeUnitId = unitDO.getId();
|
||||
} else {
|
||||
gradeUnitId = unitDO.getId();
|
||||
}
|
||||
|
||||
map.put(gradeUnit, gradeUnitId);
|
||||
}
|
||||
VocabularyBankDO vocabularyBankDO = VocabularyBankDO.builder()
|
||||
.word(word)
|
||||
.definition(meaning)
|
||||
.pronunciation(pronunciation)
|
||||
.pos(pos)
|
||||
.unitId(gradeUnitId)
|
||||
.build();
|
||||
vocabularyBankMapper.insertSelective(vocabularyBankDO);
|
||||
log.info("插入数据 {} ", vocabularyBankDO);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
//
|
||||
// String word = row.getCell(0).getStringCellValue();
|
||||
// String meaning = row.getCell(1) != null ? row.getCell(1).getStringCellValue() : "";
|
||||
// String gradeUnit = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : "";
|
||||
// String pronunciation = "";
|
||||
// String pos = "";
|
||||
// if (word.contains("Unit")) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// int gradeUnitId;
|
||||
// if (map.containsKey(gradeUnit)) {
|
||||
// gradeUnitId = map.get(gradeUnit);
|
||||
// } else {
|
||||
// UnitDO unitDO = unitDOMapper.selectByTitle(gradeUnit);
|
||||
// if (unitDO == null) {
|
||||
// unitDO = UnitDO.builder()
|
||||
// .title(gradeUnit)
|
||||
// .version("人教版")
|
||||
// .createAt(LocalDateTime.now())
|
||||
// .build();
|
||||
// unitDOMapper.insert(unitDO);
|
||||
// gradeUnitDOMapper.insert(GradeUnitDO.builder().unitId(unitDO.getId()).gradeId(gradeId).build());
|
||||
// gradeUnitId = unitDO.getId();
|
||||
// } else {
|
||||
// gradeUnitId = unitDO.getId();
|
||||
// }
|
||||
//
|
||||
// map.put(gradeUnit, gradeUnitId);
|
||||
// }
|
||||
// VocabularyBankDO vocabularyBankDO = VocabularyBankDO.builder()
|
||||
// .word(word)
|
||||
// .definition(meaning)
|
||||
// .pronunciation(pronunciation)
|
||||
// .pos(pos)
|
||||
// .unitId(gradeUnitId)
|
||||
// .build();
|
||||
// vocabularyBankMapper.insertSelective(vocabularyBankDO);
|
||||
// log.info("插入数据 {} ", vocabularyBankDO);
|
||||
// }
|
||||
//
|
||||
// } catch (IOException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class WordMasteryLogInsertTest {
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
List<Integer> integers = vocabularyBankMapper.selectAllIds();
|
||||
wordMasteryLogDOMapper.batchInsertInitialization(integers, 1);
|
||||
// List<Integer> integers = vocabularyBankMapper.selectAllIds();
|
||||
// wordMasteryLogDOMapper.batchInsertInitialization(integers, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,25 +24,25 @@ public class TestOmr {
|
||||
public void testOmr(){
|
||||
OpenCV.loadLocally();
|
||||
|
||||
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,
|
||||
3206, 3208, 3209, 3210, 3211, 3212, 3507, 3508, 3509, 3510,
|
||||
3511, 3512, 3513, 3514, 3515, 3516, 3517, 3519, 3521, 3522,
|
||||
3523, 3524, 3525, 3526, 3527, 3528, 3529, 3530, 3531, 3532,
|
||||
3533, 3535, 3536, 3537, 3538, 3539);
|
||||
|
||||
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";
|
||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(path);
|
||||
// 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,
|
||||
// 3206, 3208, 3209, 3210, 3211, 3212, 3507, 3508, 3509, 3510,
|
||||
// 3511, 3512, 3513, 3514, 3515, 3516, 3517, 3519, 3521, 3522,
|
||||
// 3523, 3524, 3525, 3526, 3527, 3528, 3529, 3530, 3531, 3532,
|
||||
// 3533, 3535, 3536, 3537, 3538, 3539);
|
||||
//
|
||||
// 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";
|
||||
// List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(path);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteger(){
|
||||
String filePath = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\p3.png";
|
||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(filePath);
|
||||
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(filePath, tessdataPath, coordinatesXIES);
|
||||
log.info("studentExamId:{}",studentExamId);
|
||||
// String filePath = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\p3.png";
|
||||
// List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(filePath);
|
||||
// StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(filePath, tessdataPath, coordinatesXIES);
|
||||
// log.info("studentExamId:{}",studentExamId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.deepoove.poi.config.Configure;
|
||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO;
|
||||
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.service.ExamWordsService;
|
||||
import com.yinlihupo.enlish.service.service.VocabularyService;
|
||||
@@ -24,38 +25,40 @@ public class ExamTest {
|
||||
private ExamWordsService examWordsService;
|
||||
@Resource
|
||||
private VocabularyService vocabularyService;
|
||||
@Resource
|
||||
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
|
||||
@Test
|
||||
public void test() {
|
||||
ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 0);
|
||||
log.info("{}", examWordsDO);
|
||||
List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds());
|
||||
List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder()
|
||||
.id(vocabularyBankDO.getId())
|
||||
.title(vocabularyBankDO.getWord())
|
||||
.definition(vocabularyBankDO.getDefinition())
|
||||
.build()).toList();
|
||||
|
||||
|
||||
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||
Configure config = Configure.builder()
|
||||
.bind("words", policy)
|
||||
.build();
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("examId", examWordsDO.getId());
|
||||
data.put("studentId", 1);
|
||||
data.put("studentStr","小明三班一年级");
|
||||
data.put("examStr", examWordsDO.getTitle());
|
||||
data.put("words", assessmentWords);
|
||||
data.put("answer", assessmentWords);
|
||||
|
||||
// 4. 渲染并输出
|
||||
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.write(new FileOutputStream("学生单词测试卷.docx"));
|
||||
System.out.println("文档生成成功!");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 0);
|
||||
// log.info("{}", examWordsDO);
|
||||
// List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds());
|
||||
// List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder()
|
||||
// .id(vocabularyBankDO.getId())
|
||||
// .title(vocabularyBankDO.getWord())
|
||||
// .definition(vocabularyBankDO.getDefinition())
|
||||
// .build()).toList();
|
||||
//
|
||||
//
|
||||
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||
// Configure config = Configure.builder()
|
||||
// .bind("words", policy)
|
||||
// .build();
|
||||
//
|
||||
// Map<String, Object> data = new HashMap<>();
|
||||
// data.put("examId", examWordsDO.getId());
|
||||
// data.put("studentId", 1);
|
||||
// data.put("studentStr","小明三班一年级");
|
||||
// data.put("examStr", examWordsDO.getTitle());
|
||||
// data.put("words", assessmentWords);
|
||||
// data.put("answer", assessmentWords);
|
||||
//
|
||||
// // 4. 渲染并输出
|
||||
// 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.write(new FileOutputStream("学生单词测试卷.docx"));
|
||||
// System.out.println("文档生成成功!");
|
||||
// } catch (Exception e) {
|
||||
// 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.service.ExamWordsJudgeService;
|
||||
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 jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -28,7 +28,7 @@ public class ExamWordsJudgeServiceTest {
|
||||
@Resource
|
||||
private StudentService studentService;
|
||||
@Resource
|
||||
private DifyArticleClient difyArticleClient;
|
||||
private DifyClient difyClient;
|
||||
@Resource
|
||||
private ExamWordsDOMapper examWordsDOMapper;
|
||||
@Resource
|
||||
@@ -36,40 +36,45 @@ public class ExamWordsJudgeServiceTest {
|
||||
|
||||
@Test
|
||||
public void judgeExamWords() {
|
||||
String ansSheetPath = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\3.png";
|
||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
||||
// 从图片中获取学生 id 和考试 id
|
||||
String tessdataPath = "C:\\project\\tess";
|
||||
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
||||
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);
|
||||
// String ansSheetPath = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\3.png";
|
||||
// List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
||||
// // 从图片中获取学生 id 和考试 id
|
||||
// String tessdataPath = "C:\\project\\tess";
|
||||
// StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
||||
// 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);
|
||||
// }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void judege() {
|
||||
examWordsJudgeService.judgeExamWords(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectExamWordsJudgeResult() {
|
||||
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(1, 10);
|
||||
log.info("examWordsJudgeResult:{}", examWordsJudgeResult);
|
||||
// List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(1, 10);
|
||||
// log.info("examWordsJudgeResult:{}", examWordsJudgeResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectExamWordsJudgeResult2() {
|
||||
String s = studentService.analyzeStudentStudy(1);
|
||||
try {
|
||||
DifyArticleClient.DifyResponse difyResponse = difyArticleClient.sendStudentAnalyze(s);
|
||||
String answer = difyResponse.getAnswer();
|
||||
log.info("answer:{}", answer);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
log.info("s:{}", s);
|
||||
}
|
||||
// @Test
|
||||
// public void selectExamWordsJudgeResult2() {
|
||||
// String s = studentService.analyzeStudentStudy(1);
|
||||
// try {
|
||||
// DifyClient.DifyResponse difyResponse = difyClient.sendStudentAnalyze(s);
|
||||
// String answer = difyResponse.getAnswer();
|
||||
// log.info("answer:{}", answer);
|
||||
// } catch (Exception e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// log.info("s:{}", s);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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>
|
||||
</head>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<style>
|
||||
/* 自定义顶部加载 Loading 颜色 */
|
||||
#nprogress .bar {
|
||||
background: #409eff !important;
|
||||
background: #2563eb !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,3 +8,6 @@ export function createUser(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)
|
||||
}
|
||||
|
||||
export function getExamWordsResult(page, size) {
|
||||
export function getExamWordsResult(page, size, classId, gradeId, studentName) {
|
||||
return axios.post('/exam/words/get', {
|
||||
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) => {
|
||||
// 创建 Blob 对象,可以指定 type,也可以让浏览器自动推断
|
||||
const blob = new Blob([res], { type: 'application/octet-stream' });
|
||||
|
||||
@@ -15,3 +15,7 @@ export function getVerificationCode(data) {
|
||||
export function getUserInfo() {
|
||||
return axios.post("/user/info")
|
||||
}
|
||||
|
||||
export function updateUserInfo(data) {
|
||||
return axios.post("/user/update-user-info", data)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@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>
|
||||
<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">
|
||||
<el-form label-width="80px">
|
||||
<el-form :label-width="isMobile ? 0 : 80" :label-position="isMobile ? 'top' : 'right'">
|
||||
<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 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-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getGradeList } from '@/api/grade'
|
||||
import { addClass } from '@/api/class'
|
||||
|
||||
@@ -41,6 +41,7 @@ const loading = ref(false)
|
||||
const name = ref('')
|
||||
const gradeId = ref(null)
|
||||
const gradeOptions = ref([])
|
||||
const isMobile = ref(false)
|
||||
|
||||
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(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
@@ -78,9 +83,22 @@ watch(
|
||||
name.value = ''
|
||||
gradeId.value = props.defaultGradeId ? Number(props.defaultGradeId) : null
|
||||
fetchGrades()
|
||||
updateIsMobile()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.footer-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<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">
|
||||
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable />
|
||||
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable class="w-full" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { addGrade } from '@/api/grade'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -29,6 +29,7 @@ const visible = computed({
|
||||
const loading = ref(false)
|
||||
const name = ref('')
|
||||
const canSubmit = computed(() => name.value.trim().length > 0)
|
||||
const isMobile = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!canSubmit.value) return
|
||||
@@ -43,12 +44,29 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
if (v) name.value = ''
|
||||
updateIsMobile()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.footer-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<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">
|
||||
<el-form label-width="90px">
|
||||
<el-form :label-width="isMobile ? 0 : 90" :label-position="isMobile ? 'top' : 'right'">
|
||||
<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 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-select>
|
||||
</el-form-item>
|
||||
<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-select>
|
||||
<div v-if="gradeId && filteredClassOptions.length === 0" class="mt-2 flex items-center gap-2">
|
||||
@@ -20,15 +20,15 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<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" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getGradeList } from '@/api/grade'
|
||||
import { getClassList } from '@/api/class'
|
||||
import { addStudent } from '@/api/student'
|
||||
@@ -61,6 +61,7 @@ const classId = ref(null)
|
||||
const startDate = ref('')
|
||||
const gradeOptions = ref([])
|
||||
const classOptions = ref([])
|
||||
const isMobile = ref(false)
|
||||
|
||||
const filteredClassOptions = computed(() => {
|
||||
if (!gradeId.value) return []
|
||||
@@ -116,6 +117,10 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (v) => {
|
||||
@@ -126,9 +131,26 @@ watch(
|
||||
startDate.value = ''
|
||||
await fetchBaseOptions()
|
||||
if (gradeId.value) handleGradeChange()
|
||||
updateIsMobile()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</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>
|
||||
<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">
|
||||
<el-card shadow="hover">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -79,7 +79,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getExamWordsDetailResult } from '@/api/exam'
|
||||
import { getStudentDetail } from '@/api/student'
|
||||
import { getWordsListByIds } from '@/api/words'
|
||||
@@ -101,6 +101,7 @@ const activeNames = ref(['correct', 'wrong'])
|
||||
const correctTitles = ref([])
|
||||
const wrongTitles = ref([])
|
||||
const studentDetail = ref(null)
|
||||
const isMobile = ref(false)
|
||||
|
||||
async function fetchDetail() {
|
||||
if (!props.id && props.id !== 0) return
|
||||
@@ -149,10 +150,17 @@ async function fetchStudent() {
|
||||
studentDetail.value = res?.data?.data ?? null
|
||||
}
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
if (v) fetchDetail()
|
||||
if (v) {
|
||||
updateIsMobile()
|
||||
fetchDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(
|
||||
@@ -161,6 +169,19 @@ watch(
|
||||
if (visible.value && v !== undefined && v !== null) fetchDetail()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.responsive-dialog :deep(.el-dialog__body) {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
<template>
|
||||
<header>
|
||||
<nav class="bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
|
||||
<div class="flex flex-wrap justify-between items-center mx-auto max-w-screen-xl">
|
||||
<div class="p-2">
|
||||
<div class="panel-shell">
|
||||
<nav class="fluent-nav px-4 lg:px-6 py-2.5">
|
||||
<div class="flex flex-wrap justify-between items-center">
|
||||
<a href="#" class="flex items-center">
|
||||
<img src="https://flowbite.com/docs/images/logo.svg" class="mr-3 h-6 sm:h-9" alt="Flowbite Logo" />
|
||||
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Flowbite</span>
|
||||
<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>
|
||||
<span class="self-center text-xl font-semibold whitespace-nowrap">英语教育</span>
|
||||
</a>
|
||||
<div class="flex items-center lg:order-2">
|
||||
<template v-if="userName">
|
||||
<div class="relative" ref="menuRef">
|
||||
<button
|
||||
@click="menuOpen = !menuOpen"
|
||||
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">
|
||||
<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"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</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">
|
||||
<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 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600">
|
||||
<button @click="handleLogout"
|
||||
class="w-full text-left block px-4 py-2 fluent-link">
|
||||
登出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<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">
|
||||
Login
|
||||
</a>
|
||||
</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"
|
||||
<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">
|
||||
<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>
|
||||
@@ -57,35 +55,14 @@
|
||||
</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 class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1"
|
||||
id="mobile-menu-2">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<LoginDialog v-model="showLogin" @success="refreshUser" />
|
||||
</header>
|
||||
</template>
|
||||
@@ -97,6 +74,7 @@ import { getUserInfo, logout } from '@/api/user'
|
||||
import { removeToken } from '@/composables/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showMessage } from '@/composables/util.js'
|
||||
import { openMobileSidebar } from '@/composables/ui.js'
|
||||
const showLogin = ref(false)
|
||||
const userName = ref('')
|
||||
const menuOpen = ref(false)
|
||||
@@ -106,9 +84,14 @@ async function refreshUser() {
|
||||
try {
|
||||
const r = await getUserInfo()
|
||||
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 {
|
||||
userName.value = ''
|
||||
handleLogout()
|
||||
}
|
||||
}
|
||||
async function handleLogout() {
|
||||
@@ -119,7 +102,7 @@ async function handleLogout() {
|
||||
userName.value = ''
|
||||
menuOpen.value = false
|
||||
showMessage('已退出登录', 'success')
|
||||
router.push('/')
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
function onDocClick(e) {
|
||||
@@ -137,3 +120,83 @@ onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocClick)
|
||||
})
|
||||
</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 },
|
||||
studentId: { type: [Number, String], required: true }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -71,6 +71,7 @@ async function handleGenerate() {
|
||||
const d = res?.data
|
||||
if (d.success) {
|
||||
ElMessage.success('生成学案任务已提交,请等待十分钟')
|
||||
emit('success', { studentId: Number(props.studentId) })
|
||||
visible.value = false
|
||||
} else {
|
||||
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) {
|
||||
return sortData(arr).map(it => ({
|
||||
startTime: it.startTime,
|
||||
startTime: it.startTime.replace('T', ' '),
|
||||
totalCount: Number(it.totalCount) || 0,
|
||||
planId: it.planId ?? 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 'nprogress/nprogress.css'
|
||||
import 'element-plus/dist/index.css'
|
||||
// 导入路由
|
||||
import router from '@/router'
|
||||
// 导入全局路由守卫
|
||||
|
||||
@@ -1,60 +1,95 @@
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-container class="min-h-screen">
|
||||
<el-header>
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
|
||||
<el-main class="p-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<el-container class="pt-4">
|
||||
|
||||
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||
<Sidebar />
|
||||
</el-aside>
|
||||
|
||||
<el-main class="">
|
||||
<div class="panel-shell p-6">
|
||||
<div class="text-lg font-semibold mb-4">学案查询</div>
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||
<el-input v-model="searchName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||
<el-button @click="onReset">重置</el-button>
|
||||
</div>
|
||||
<el-table ref="tableRef" :data="rows" border class="w-full" v-loading="loading" row-key="id">
|
||||
<div class="hidden sm:block overflow-x-auto">
|
||||
<el-table ref="tableRef" :data="rows" border class="min-w-[720px]" v-loading="loading" row-key="id">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="p-3">
|
||||
<div class="text-sm font-semibold mb-2">学案</div>
|
||||
<el-table :data="row.plans || []" size="small" border>
|
||||
<el-table-column prop="id" label="计划ID" width="100" />
|
||||
<div class="overflow-x-auto">
|
||||
<el-table :data="row.plans || []" size="small" border class="min-w-[600px]">
|
||||
<el-table-column prop="title" label="标题" min-width="280" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{ row: plan }">
|
||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'"
|
||||
effect="plain">
|
||||
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row: plan }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
<el-button type="primary" size="small"
|
||||
:loading="downloadingIds.includes(plan.id)"
|
||||
@click="onDownload(plan)"
|
||||
>下载</el-button>
|
||||
<el-button
|
||||
class="ml-2"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="onDownload(plan)">下载</el-button>
|
||||
<el-button class="ml-2" type="primary" size="small"
|
||||
:disabled="plan.isFinished === 1"
|
||||
:loading="finishingIds.includes(plan.id)"
|
||||
@click="onFinish(row.id, plan.id, plan)"
|
||||
>完成</el-button>
|
||||
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="id" label="学生ID" width="100" />
|
||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
||||
<el-table-column prop="className" label="班级" min-width="120" />
|
||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||
<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 v-if="mobileExpanded[row.id]" class="mt-3 space-y-2">
|
||||
<div v-for="plan in (row.plans || [])" :key="plan.id"
|
||||
class="rounded-lg border border-white/30 bg-white/50 p-3">
|
||||
<div class="text-sm font-medium mb-2">{{ plan.title }}</div>
|
||||
<div class="mb-2">
|
||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
||||
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<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"
|
||||
@@ -62,6 +97,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
|
||||
</el-container>
|
||||
</div>
|
||||
@@ -73,6 +110,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { findStudentLessonPlans, finishLessonPlan } from '@/api/studentLessonPlans'
|
||||
import { downloadLessonPlan } from '@/api/plan'
|
||||
import { showMessage } from '@/composables/util'
|
||||
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
@@ -83,6 +121,12 @@ const searchName = ref('')
|
||||
const tableRef = ref(null)
|
||||
const downloadingIds = ref([])
|
||||
const finishingIds = ref([])
|
||||
const mobileExpanded = ref({})
|
||||
|
||||
function toggleMobileExpand(id) {
|
||||
const v = mobileExpanded.value[id]
|
||||
mobileExpanded.value[id] = !v
|
||||
}
|
||||
|
||||
async function fetchLessonPlans() {
|
||||
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,54 +1,70 @@
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-header>
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
<el-main class="p-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
|
||||
|
||||
<el-container class="pt-4">
|
||||
|
||||
<el-main class="p-2">
|
||||
<div class="panel-shell p-6">
|
||||
<div class="text-lg font-semibold mb-4">TTS</div>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<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-input v-model="planIdInput" placeholder="planId" class="w-full sm:w-auto" style="max-width: 220px" /> -->
|
||||
<el-button type="primary" :loading="loadingWords" @click="onLoadWords">加载词汇</el-button>
|
||||
<el-select v-model="voice" placeholder="选择声线" style="max-width: 160px">
|
||||
<!-- <el-select v-model="voice" placeholder="选择声线" class="w-full sm:w-auto" style="max-width: 160px">
|
||||
<el-option label="alloy" value="alloy" />
|
||||
<el-option label="verse" value="verse" />
|
||||
<el-option label="nova" value="nova" />
|
||||
</el-select>
|
||||
<el-select v-model="format" placeholder="格式" style="max-width: 120px">
|
||||
</el-select> -->
|
||||
<!-- <el-select v-model="format" placeholder="格式" class="w-full sm:w-auto" style="max-width: 120px">
|
||||
<el-option label="mp3" value="mp3" />
|
||||
<el-option label="wav" value="wav" />
|
||||
<el-option label="ogg" value="ogg" />
|
||||
</el-select>
|
||||
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll"
|
||||
@click="onGenerateAll">生成全部音频</el-button>
|
||||
</el-select> -->
|
||||
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll" class="!ml-0"
|
||||
@click="onGenerateAll">生成音频</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" border class="w-full" v-loading="loadingWords">
|
||||
<el-table-column prop="word" label="词汇/短语" min-width="260" />
|
||||
<el-table-column label="状态" width="160">
|
||||
<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="360" fixed="right">
|
||||
<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>
|
||||
<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>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -59,7 +75,7 @@ import { useRoute } from 'vue-router'
|
||||
import { getLessonPlanWords } from '@/api/plan'
|
||||
import { synthesizeOpenAITTS } from '@/api/tts'
|
||||
import { showMessage } from '@/composables/util'
|
||||
|
||||
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||
const route = useRoute()
|
||||
const planIdInput = ref(route.query.planId ? String(route.query.planId) : '')
|
||||
const words = ref([])
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
|
||||
<el-main class="p-4">
|
||||
<el-card>
|
||||
<el-container>
|
||||
<el-main class="p-2">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="panel-shell p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<el-input v-model="query.name" placeholder="姓名" clearable style="max-width:220px" />
|
||||
<el-button type="primary" class="ml-2" @click="fetchList">查询</el-button>
|
||||
<el-button class="ml-2" @click="resetSearch">重置</el-button>
|
||||
<el-button type="success" class="ml-2" @click="openCreate">新增用户</el-button>
|
||||
<el-button type="primary" class="ml-2" @click="openCreate">新增用户</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="name" label="姓名" />
|
||||
@@ -19,17 +21,68 @@
|
||||
<el-table-column prop="roleName" label="角色" />
|
||||
</el-table>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="totalCount"
|
||||
layout="prev, pager, next, sizes, total"
|
||||
@current-change="onPageChange"
|
||||
@size-change="onSizeChange"
|
||||
/>
|
||||
<el-pagination background :current-page="page" :page-size="pageSize" :total="totalCount"
|
||||
layout="prev, pager, next, sizes, total" @current-change="onPageChange"
|
||||
@size-change="onSizeChange" />
|
||||
</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="inviteForm" :rules="inviteRules" ref="inviteFormRef" label-width="120px">
|
||||
<el-form-item label="使用次数限制" prop="limit">
|
||||
<el-input-number v-model="inviteForm.limit" :min="1" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="有效期(天)" prop="expire">
|
||||
<el-input-number v-model="inviteForm.expire" :min="1" :max="3650" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="inviteLoading"
|
||||
@click="submitInvite">生成邀请码</el-button>
|
||||
<el-button class="ml-2" @click="resetInvite">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<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-card>
|
||||
|
||||
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
||||
<el-form :model="createForm" :rules="rules" ref="createFormRef" label-width="80px">
|
||||
@@ -44,11 +97,12 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible=false">取消</el-button>
|
||||
<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>
|
||||
</div>
|
||||
@@ -56,9 +110,11 @@
|
||||
|
||||
<script setup>
|
||||
import Header from '@/layouts/components/Header.vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getUserList, createUser } from '@/api/admin'
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
||||
import { updateUserInfo, getVerificationCode } from '@/api/user'
|
||||
import { showMessage } from '@/composables/util.js'
|
||||
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
@@ -144,4 +200,147 @@ onMounted(() => {
|
||||
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>
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-container class="min-h-screen">
|
||||
<el-header>
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
|
||||
<el-main class="p-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<el-container class="pt-4">
|
||||
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||
<Sidebar></Sidebar>
|
||||
</el-aside>
|
||||
|
||||
<el-main class="h-full">
|
||||
<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="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="panel-shell p-6">
|
||||
<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">
|
||||
<el-table ref="classTableRef" :data="classes" border class="min-w-[520px]" v-loading="loading" highlight-current-row
|
||||
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<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 label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
@@ -22,10 +26,21 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="sm:hidden space-y-3">
|
||||
<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" />
|
||||
<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>
|
||||
@@ -35,14 +50,12 @@
|
||||
</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="flex flex-wrap items-center gap-3 mb-4">
|
||||
<el-input v-model="studentName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
||||
<el-tag v-if="selectedClassId" effect="plain">班级:{{ selectedClassTitle }} (ID: {{
|
||||
selectedClassId }})</el-tag>
|
||||
<el-tag v-if="selectedGradeId" effect="plain">年级:{{ selectedGradeTitle }} (ID: {{
|
||||
selectedGradeId }})</el-tag>
|
||||
<el-tag v-if="selectedClassId" effect="plain">班级:{{ selectedClassTitle }}</el-tag>
|
||||
<el-tag v-if="selectedGradeId" effect="plain">年级:{{ selectedGradeTitle }}</el-tag>
|
||||
<el-button type="primary" @click="fetchStudents">查询</el-button>
|
||||
<el-button @click="resetStudentFilters">重置</el-button>
|
||||
<el-button type="success" :disabled="selectedStudentIds.length !== 1"
|
||||
@@ -54,102 +67,109 @@
|
||||
生成学案
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table ref="studentTableRef" :data="students" border class="w-full"
|
||||
<div class="hidden sm:block overflow-x-auto">
|
||||
<el-table ref="studentTableRef" :data="students" border class="min-w-[760px]"
|
||||
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
||||
<el-table-column prop="classId" label="班级ID" width="100" />
|
||||
<el-table-column prop="gradeId" label="年级ID" width="100" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<el-table-column prop="className" label="班级" min-width="120" />
|
||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||
<el-table-column prop="phone" label="学案" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<template v-if="generatingPercents[row.id] !== undefined">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button type="primary" size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<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" />
|
||||
: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">
|
||||
<el-button type="primary" @click="showAddStudentDialog = true">新增学生</el-button>
|
||||
</div>
|
||||
<AddStudentDialog
|
||||
v-model="showAddStudentDialog"
|
||||
:default-class-id="selectedClassId"
|
||||
:default-grade-id="selectedGradeId"
|
||||
@success="fetchStudents"
|
||||
/>
|
||||
<LessonPlanDialog
|
||||
v-model="showLessonPlanDialog"
|
||||
:student-id="selectedStudentIds[0]"
|
||||
/>
|
||||
<AddStudentDialog v-model="showAddStudentDialog" :default-class-id="selectedClassId"
|
||||
:default-grade-id="selectedGradeId" @success="fetchStudents" />
|
||||
<LessonPlanDialog v-model="showLessonPlanDialog" :student-id="selectedStudentIds[0]"
|
||||
@success="onLessonPlanGenerateSuccess" />
|
||||
<StudentPlanListDialog v-model="showPlanListDialog" :student-id="planStudentId" />
|
||||
<el-dialog v-model="showAnalysisDialog" title="学情分析" width="60%">
|
||||
<StudyAnalysis v-if="showAnalysisDialog" :student-id="analysisStudentId" />
|
||||
</el-dialog>
|
||||
</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>
|
||||
<el-table ref="gradeTableRef" :data="grades" border class="w-full" highlight-current-row
|
||||
<div class="hidden sm:block overflow-x-auto">
|
||||
<el-table ref="gradeTableRef" :data="grades" border class="min-w-[360px]" highlight-current-row
|
||||
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="年级名称" min-width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small"
|
||||
@click.stop="onDeleteGrade(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="sm:hidden space-y-3">
|
||||
<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"
|
||||
<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 class="mt-3 flex justify-end">
|
||||
<el-button type="primary" @click="showAddGradeDialog = true">新增年级</el-button>
|
||||
</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>
|
||||
</el-container>
|
||||
|
||||
</el-container>
|
||||
</div>
|
||||
@@ -157,7 +177,8 @@
|
||||
|
||||
<script setup>
|
||||
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 { getGradeList, deleteGrade } from '@/api/grade'
|
||||
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 AddStudentDialog from '@/layouts/components/AddStudentDialog.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 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 pageNo = ref(1)
|
||||
@@ -201,6 +226,24 @@ const selectedStudentIds = ref([])
|
||||
const showGenerateDialog = ref(false)
|
||||
const showAddStudentDialog = 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 unitPageNo = ref(1)
|
||||
@@ -255,6 +298,7 @@ async function fetchStudents() {
|
||||
studentTotalCount.value = d.totalCount || 0
|
||||
studentPageNo.value = d.pageNo || studentPageNo.value
|
||||
studentPageSize.value = d.pageSize || studentPageSize.value
|
||||
ensurePollingForCurrentStudents()
|
||||
} finally {
|
||||
studentLoading.value = false
|
||||
}
|
||||
@@ -295,6 +339,10 @@ function onStudentSelectionChange(rows) {
|
||||
function onViewStudent(row) {
|
||||
router.push(`/student/${row.id}`)
|
||||
}
|
||||
function onShowAnalysis(row) {
|
||||
analysisStudentId.value = row.id
|
||||
showAnalysisDialog.value = true
|
||||
}
|
||||
function onClassRowClick(row) {
|
||||
selectedClassId.value = row.id
|
||||
selectedClassTitle.value = row.title
|
||||
@@ -309,8 +357,64 @@ function onGradeRowClick(row) {
|
||||
studentPageNo.value = 1
|
||||
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) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该学生?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteStudent(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
if (selectedStudentIds.value?.length) {
|
||||
@@ -319,11 +423,17 @@ async function onDeleteStudent(row) {
|
||||
}
|
||||
await fetchStudents()
|
||||
} catch (e) {
|
||||
if (e === 'cancel' || e === 'close') return
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
async function onDeleteClass(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该班级?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteClass(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
if (selectedClassId.value === row.id) {
|
||||
@@ -333,6 +443,7 @@ async function onDeleteClass(row) {
|
||||
}
|
||||
await fetchClasses()
|
||||
} catch (e) {
|
||||
if (e === 'cancel' || e === 'close') return
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-container class="min-h-screen">
|
||||
<el-header>
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
|
||||
<el-main class="p-4">
|
||||
<el-container class="pt-4">
|
||||
<el-main class="h-full">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
||||
<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>
|
||||
<template v-if="detail">
|
||||
<el-descriptions :column="1" border>
|
||||
@@ -22,8 +23,7 @@
|
||||
<el-empty description="请从班级页跳转" />
|
||||
</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>
|
||||
<template v-if="wordStat">
|
||||
<el-descriptions :column="1" border>
|
||||
@@ -36,36 +36,25 @@
|
||||
<el-empty description="暂无统计" />
|
||||
</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-md font-semibold mb-3">学生考试记录</div>
|
||||
<ExamHistoryChart :data="history" />
|
||||
</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-md font-semibold mb-3">学生学案记录</div>
|
||||
<PlanHistoryChart :student-id="route.params.id" />
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
|
||||
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
||||
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
|
||||
<WordMasteryHeatmap :student-id="route.params.id" :columns="heatmapColumns" />
|
||||
</div>
|
||||
<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="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 class="panel-shell p-4 sm:p-6 lg:col-span-2">
|
||||
<div class="text-md font-semibold mb-3">学情分析</div>
|
||||
<StudyAnalysis :student-id="route.params.id" />
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
@@ -74,29 +63,26 @@
|
||||
import Header from '@/layouts/components/Header.vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getStudentDetail, getStudentStudyAnalyze } from '@/api/student'
|
||||
import { getStudentDetail } from '@/api/student'
|
||||
import { getStudentExamHistory } from '@/api/exam'
|
||||
import { getWordStudentDetail } from '@/api/words'
|
||||
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
|
||||
import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.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 detail = ref(null)
|
||||
const route = useRoute()
|
||||
const history = ref([])
|
||||
const analyzeLoading = ref(false)
|
||||
const analysisText = ref('')
|
||||
const wordStat = ref(null)
|
||||
const md = new MarkdownIt({
|
||||
html: false,
|
||||
linkify: true,
|
||||
breaks: true
|
||||
})
|
||||
const analysisHtml = computed(() => {
|
||||
return analysisText.value ? md.render(analysisText.value) : ''
|
||||
})
|
||||
const isMobile = ref(false)
|
||||
const heatmapColumns = computed(() => isMobile.value ? 20 : 50)
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
async function fetchDetail() {
|
||||
const id = route.params.id
|
||||
@@ -119,22 +105,10 @@ async function fetchExamHistory() {
|
||||
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
|
||||
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
||||
}) : []
|
||||
}
|
||||
|
||||
async function fetchStudyAnalyze() {
|
||||
const id = route.params.id
|
||||
if (!id) return
|
||||
analyzeLoading.value = true
|
||||
try {
|
||||
const res = await getStudentStudyAnalyze({
|
||||
studentId: Number(id)
|
||||
// 遍历 history 中的 startDate 去掉其中的 T
|
||||
history.value.forEach(item => {
|
||||
item.startDate = item.startDate.replace('T', ' ')
|
||||
})
|
||||
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() {
|
||||
@@ -149,5 +123,7 @@ onMounted(() => {
|
||||
fetchDetail()
|
||||
fetchExamHistory()
|
||||
fetchWordStat()
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user