Compare commits
14 Commits
7f2fda16ec
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fcf381b8f1 | |||
| fb29acc145 | |||
| 7182371c92 | |||
| 09b326c07a | |||
| 49963bb49c | |||
| bf2a80917c | |||
| deabd5f7f5 | |||
| 0802f6fe70 | |||
| 679241588f | |||
| fe7128dd4e | |||
| 1184ea7895 | |||
| e468be74b7 | |||
| 57166af2f4 | |||
| fcb8ac9c22 |
@@ -25,9 +25,13 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
|
|
||||||
SaRouter.match("/**")
|
SaRouter.match("/**")
|
||||||
.notMatch("/login/**")
|
.notMatch("/login/**")
|
||||||
|
.notMatch("/plan/word/voice")
|
||||||
|
.notMatch("/plan/word/voice/tts")
|
||||||
.check(r -> StpUtil.checkLogin());
|
.check(r -> StpUtil.checkLogin());
|
||||||
|
|
||||||
SaRouter.match("/admin/**")
|
SaRouter.match("/admin/**")
|
||||||
|
.notMatch("/plan/word/voice")
|
||||||
|
.notMatch("/plan/word/voice/tts")
|
||||||
.check(r -> StpUtil.checkRole("root"));
|
.check(r -> StpUtil.checkRole("root"));
|
||||||
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class ExamWordsController {
|
|||||||
// bug: 获取单词后,单词的id会乱序、 需要重新更新考试记录中的 id
|
// bug: 获取单词后,单词的id会乱序、 需要重新更新考试记录中的 id
|
||||||
examWordsDO.setWordIds(assessmentWords.stream().map(Word::getId).toList());
|
examWordsDO.setWordIds(assessmentWords.stream().map(Word::getId).toList());
|
||||||
examWordsService.updateExamWordsWordIdsOrder(examWordsDO);
|
examWordsService.updateExamWordsWordIdsOrder(examWordsDO);
|
||||||
|
log.info("生成试卷成功 {}", examWordsDO);
|
||||||
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
|
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
|
||||||
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
|
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
@@ -72,6 +72,12 @@ public class ExamWordsController {
|
|||||||
data.put("examStr", examWordsDO.getTitle());
|
data.put("examStr", examWordsDO.getTitle());
|
||||||
data.put("words", assessmentWords);
|
data.put("words", assessmentWords);
|
||||||
data.put("answer", assessmentWords);
|
data.put("answer", assessmentWords);
|
||||||
|
|
||||||
|
List<Word> words1 = assessmentWords.subList(0, assessmentWords.size() / 2);
|
||||||
|
List<Word> words2 = assessmentWords.subList(assessmentWords.size() / 2, assessmentWords.size());
|
||||||
|
data.put("words1", words1);
|
||||||
|
data.put("words2", words2);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@@ -107,8 +113,11 @@ public class ExamWordsController {
|
|||||||
PageResponse<ExamWordsResultRspVO> getExamWordsResult(@RequestBody ExamWordsResultReqVO examWordsResultReqVO) {
|
PageResponse<ExamWordsResultRspVO> getExamWordsResult(@RequestBody ExamWordsResultReqVO examWordsResultReqVO) {
|
||||||
Integer page = examWordsResultReqVO.getPage();
|
Integer page = examWordsResultReqVO.getPage();
|
||||||
Integer size = examWordsResultReqVO.getSize();
|
Integer size = examWordsResultReqVO.getSize();
|
||||||
|
Integer classId = examWordsResultReqVO.getClassId();
|
||||||
|
Integer gradeId = examWordsResultReqVO.getGradeId();
|
||||||
|
String studentName = examWordsResultReqVO.getStudentName();
|
||||||
Integer total = examWordsJudgeService.getExamWordsJudgeResultCount();
|
Integer total = examWordsJudgeService.getExamWordsJudgeResultCount();
|
||||||
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(page, size);
|
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(page, size, classId, gradeId, studentName);
|
||||||
List<ExamWordsResultRspVO> list = examWordsJudgeResult.stream().map(examWordsJudgeResultDO -> ExamWordsResultRspVO
|
List<ExamWordsResultRspVO> list = examWordsJudgeResult.stream().map(examWordsJudgeResultDO -> ExamWordsResultRspVO
|
||||||
.builder()
|
.builder()
|
||||||
.id(examWordsJudgeResultDO.getId())
|
.id(examWordsJudgeResultDO.getId())
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public class LessonPlanController {
|
|||||||
Integer wordSize = addLessonPlanReqVO.getWordSize();
|
Integer wordSize = addLessonPlanReqVO.getWordSize();
|
||||||
try {
|
try {
|
||||||
if (redisTemplate.opsForValue().get(LessonPlanConstant.buildGeneratePlanContent(studentId)) != null) {
|
if (redisTemplate.opsForValue().get(LessonPlanConstant.buildGeneratePlanContent(studentId)) != null) {
|
||||||
throw new RuntimeException("学案正常生成中");
|
throw new RuntimeException("学案正在生成,请耐心等待");
|
||||||
}
|
}
|
||||||
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId, wordSize));
|
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId, wordSize));
|
||||||
return Response.success("生成学案成功,请等待 10 分钟");
|
return Response.success("生成学案成功,请等待 10 分钟");
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
package com.yinlihupo.enlish.service.controller;
|
package com.yinlihupo.enlish.service.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
|
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
|
||||||
|
import com.yinlihupo.enlish.service.model.vo.user.UpdateUserInfoReqVO;
|
||||||
import com.yinlihupo.enlish.service.service.UserService;
|
import com.yinlihupo.enlish.service.service.UserService;
|
||||||
|
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||||
import com.yinlihupo.framework.common.response.Response;
|
import com.yinlihupo.framework.common.response.Response;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/user/")
|
@RequestMapping("/user/")
|
||||||
|
@Slf4j
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -27,4 +33,23 @@ public class UserController {
|
|||||||
return Response.success(findUserInfoRspVO);
|
return Response.success(findUserInfoRspVO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("update-user-info")
|
||||||
|
@ApiOperationLog(description = "修改用户信息")
|
||||||
|
public Response<String> updatePassword(@RequestBody UpdateUserInfoReqVO updateUserInfoReqVO) {
|
||||||
|
try {
|
||||||
|
String code = updateUserInfoReqVO.getCode();
|
||||||
|
String newPassword = updateUserInfoReqVO.getNewPassword();
|
||||||
|
String phone = updateUserInfoReqVO.getPhone();
|
||||||
|
String name = updateUserInfoReqVO.getName();
|
||||||
|
userService.updateUserInfo(newPassword, code, phone, name);
|
||||||
|
|
||||||
|
StpUtil.logout();
|
||||||
|
|
||||||
|
return Response.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("修改密码失败 {}", e.getMessage());
|
||||||
|
return Response.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,13 @@ public interface ExamWordsJudgeResultDOMapper {
|
|||||||
|
|
||||||
Integer selectCount();
|
Integer selectCount();
|
||||||
|
|
||||||
|
Integer selectUnfinishedCount();
|
||||||
|
|
||||||
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
|
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDO> selectByStudentId(@Param("studentId") Integer studentId);
|
List<ExamWordsJudgeResultDO> selectByStudentId(@Param("studentId") Integer studentId);
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
|
List<ExamWordsJudgeResultDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
|
||||||
|
|
||||||
|
List<ExamWordsJudgeResultDO> selectByPageAndStudentIds(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize, @Param("studentIds") List<Integer> studentIds);
|
||||||
}
|
}
|
||||||
@@ -23,4 +23,10 @@ public interface StudentDOMapper {
|
|||||||
int selectStudentCountByClassId(@Param("classId") Integer classId);
|
int selectStudentCountByClassId(@Param("classId") Integer classId);
|
||||||
|
|
||||||
int updateStudentActualGradeId(@Param("studentId") Integer studentId, @Param("gradeId") Integer gradeId);
|
int updateStudentActualGradeId(@Param("studentId") Integer studentId, @Param("gradeId") Integer gradeId);
|
||||||
|
|
||||||
|
List<StudentDO> selectStudentDOListByClassId(@Param("classId") Integer classId);
|
||||||
|
|
||||||
|
List<StudentDO> selectStudentDOListByGradeId(@Param("gradeId") Integer gradeId);
|
||||||
|
|
||||||
|
List<StudentDO> selectStudentDOListByName(@Param("name") String name);
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,10 @@ public interface UserDOMapper {
|
|||||||
|
|
||||||
void insert(UserDO userDO);
|
void insert(UserDO userDO);
|
||||||
|
|
||||||
|
void updatePassword(@Param("id") Long id, @Param("password") String password);
|
||||||
|
|
||||||
|
void updateUserInfo(@Param("id") Long id, @Param("name") String name, @Param("password") String password, @Param("phone") String phone);
|
||||||
|
|
||||||
UserDO selectById(Long id);
|
UserDO selectById(Long id);
|
||||||
|
|
||||||
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ public interface VocabularyBankDOMapper {
|
|||||||
Integer selectWordTotal();
|
Integer selectWordTotal();
|
||||||
|
|
||||||
List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
|
List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
|
||||||
|
|
||||||
|
List<VocabularyBankDO> selectByGradeIdAndNotMatchIds(@Param("gradeId") Integer gradeId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
|
||||||
}
|
}
|
||||||
@@ -13,4 +13,8 @@ public class ExamWordsResultReqVO {
|
|||||||
|
|
||||||
private Integer page;
|
private Integer page;
|
||||||
private Integer size;
|
private Integer size;
|
||||||
|
|
||||||
|
private Integer classId;
|
||||||
|
private Integer gradeId;
|
||||||
|
private String studentName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.yinlihupo.enlish.service.model.vo.student;
|
package com.yinlihupo.enlish.service.model.vo.student;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -16,5 +17,6 @@ public class AddStudentReqVO {
|
|||||||
private String name;
|
private String name;
|
||||||
private Integer classId;
|
private Integer classId;
|
||||||
private Integer gradeId;
|
private Integer gradeId;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yinlihupo.enlish.service.model.vo.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Data
|
||||||
|
public class UpdateUserInfoReqVO {
|
||||||
|
|
||||||
|
private String newPassword;
|
||||||
|
private String name;
|
||||||
|
private String phone;
|
||||||
|
private String code;
|
||||||
|
}
|
||||||
@@ -9,10 +9,12 @@ public interface ExamWordsJudgeService {
|
|||||||
|
|
||||||
void judgeExamWords(int count);
|
void judgeExamWords(int count);
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize);
|
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize, Integer classId, Integer gradeId, String studentName);
|
||||||
|
|
||||||
Integer getExamWordsJudgeResultCount();
|
Integer getExamWordsJudgeResultCount();
|
||||||
|
|
||||||
|
Integer getExamUnfinishedCount();
|
||||||
|
|
||||||
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
|
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId);
|
List<ExamWordsJudgeResultDO> getStudentExamWordsResultList(Integer studentId);
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ public interface UserService {
|
|||||||
Integer findUserTotal();
|
Integer findUserTotal();
|
||||||
|
|
||||||
void createUser(UserDO userDO);
|
void createUser(UserDO userDO);
|
||||||
|
|
||||||
|
void updateUserInfo(String password, String reqCode, String phone, String name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,17 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -47,7 +48,6 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
@Transactional(rollbackFor = RuntimeException.class)
|
@Transactional(rollbackFor = RuntimeException.class)
|
||||||
public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
|
public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
|
||||||
|
|
||||||
|
|
||||||
ExamWordsDO examWordsDO;
|
ExamWordsDO examWordsDO;
|
||||||
|
|
||||||
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
|
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
|
||||||
@@ -61,6 +61,16 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
examWordsDO = generateFinalExamWords(studentId);
|
examWordsDO = generateFinalExamWords(studentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Integer> wordIds = new ArrayList<>(examWordsDO.getWordIds());
|
||||||
|
if (wordIds.size() < wordCount) {
|
||||||
|
log.info("单词数量不足,补充单词");
|
||||||
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
|
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectByGradeIdAndNotMatchIds(studentDO.getGradeId(), wordIds, wordCount - wordIds.size());
|
||||||
|
List<Integer> list = new ArrayList<>(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList());
|
||||||
|
wordIds.addAll(list);
|
||||||
|
examWordsDO.setWordIds(wordIds);
|
||||||
|
}
|
||||||
|
|
||||||
return examWordsDO;
|
return examWordsDO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +119,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
Integer gradeId = studentDO.getGradeId();
|
Integer gradeId = studentDO.getGradeId();
|
||||||
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId) + "上");
|
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId) + "上");
|
||||||
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS);
|
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS, ExamWordsConstant.EXAM_TYPE_MIDTERM);
|
||||||
examWordsDO.setTitle("期中测试" + studentDO.getName());
|
examWordsDO.setTitle("期中测试" + studentDO.getName());
|
||||||
return getExamWordsDO(studentId, examWordsDO);
|
return getExamWordsDO(studentId, examWordsDO);
|
||||||
|
|
||||||
@@ -119,13 +129,13 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
Integer gradeId = studentDO.getGradeId();
|
Integer gradeId = studentDO.getGradeId();
|
||||||
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId));
|
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId));
|
||||||
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS);
|
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS, ExamWordsConstant.EXAM_TYPE_FINAL);
|
||||||
examWordsDO.setTitle("期末测试" + studentDO.getName());
|
examWordsDO.setTitle("期末测试" + studentDO.getName());
|
||||||
return getExamWordsDO(studentId, examWordsDO);
|
return getExamWordsDO(studentId, examWordsDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private ExamWordsDO getExamWordsDO(Integer studentId, StudentDO studentDO, Integer gradeId, List<UnitDO> unitDOS) {
|
private ExamWordsDO getExamWordsDO(Integer studentId, StudentDO studentDO, Integer gradeId, List<UnitDO> unitDOS, Integer type) {
|
||||||
if (unitDOS.isEmpty()) {
|
if (unitDOS.isEmpty()) {
|
||||||
throw new RuntimeException("没有找到对应的单元");
|
throw new RuntimeException("没有找到对应的单元");
|
||||||
}
|
}
|
||||||
@@ -134,7 +144,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
||||||
.gradeId(gradeId)
|
.gradeId(gradeId)
|
||||||
.level(1)
|
.level(1)
|
||||||
.type(ExamWordsConstant.EXAM_TYPE_BASELINE)
|
.type(type)
|
||||||
.title(studentDO.getName())
|
.title(studentDO.getName())
|
||||||
.createdAt(LocalDateTime.now())
|
.createdAt(LocalDateTime.now())
|
||||||
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
|
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
|
||||||
@@ -161,29 +171,55 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = RuntimeException.class)
|
@Transactional(rollbackFor = RuntimeException.class)
|
||||||
public int saveExamWordsPngToDbAndLocal(MultipartFile file) {
|
public int saveExamWordsPngToDbAndLocal(MultipartFile file) {
|
||||||
|
// 1. 基础校验:判空
|
||||||
File dir = new File(tmpPng);
|
if (file == null || file.isEmpty()) {
|
||||||
if (!dir.exists()) {
|
throw new RuntimeException("上传文件不能为空");
|
||||||
dir.mkdirs();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 安全校验:检查后缀名白名单
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String extension = StringUtils.getFilenameExtension(originalFilename); // Spring工具类
|
||||||
|
List<String> allowedExtensions = Arrays.asList("png", "jpg", "jpeg");
|
||||||
|
|
||||||
|
if (extension == null || !allowedExtensions.contains(extension.toLowerCase())) {
|
||||||
|
throw new RuntimeException("不支持的文件格式,仅支持: " + allowedExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 准备目录 (使用 NIO)
|
||||||
|
// 假设 tmpPng 是配置好的基础路径字符串
|
||||||
|
Path directoryPath = Paths.get(tmpPng);
|
||||||
try {
|
try {
|
||||||
String originalFilename = file.getOriginalFilename();
|
if (!Files.exists(directoryPath)) {
|
||||||
String suffix = "";
|
Files.createDirectories(directoryPath);
|
||||||
if (originalFilename != null && originalFilename.contains(".")) {
|
|
||||||
suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
|
|
||||||
}
|
}
|
||||||
String newFileName = UUID.randomUUID() + suffix;
|
|
||||||
String path = tmpPng + newFileName;
|
|
||||||
|
|
||||||
File dest = new File(path);
|
// 4. 生成文件名 (防止文件名冲突)
|
||||||
file.transferTo(dest);
|
String newFileName = UUID.randomUUID().toString() + "." + extension;
|
||||||
|
|
||||||
|
// 5. 组合最终路径 (自动处理分隔符)
|
||||||
|
Path targetPath = directoryPath.resolve(newFileName);
|
||||||
|
|
||||||
|
// 6. 保存文件
|
||||||
|
file.transferTo(targetPath.toAbsolutePath().toFile());
|
||||||
|
String string = targetPath.toAbsolutePath().toFile().toString();
|
||||||
|
log.info("文件上传成功路径为 {}", string);
|
||||||
|
if (!targetPath.toFile().exists()) {
|
||||||
|
log.error("文件上传失败: {}", newFileName);
|
||||||
|
throw new RuntimeException("文件上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 入库
|
||||||
|
// 建议:存相对路径或文件名,不要存 targetPath.toString() 的绝对路径
|
||||||
|
// 这里为了演示,假设 insert 依然接受字符串,建议存 newFileName
|
||||||
|
int insert = examWordsJudgeResultDOMapper.insert(targetPath.toString());
|
||||||
|
|
||||||
|
log.info("上传文件成功: {}", newFileName);
|
||||||
|
return insert;
|
||||||
|
|
||||||
return examWordsJudgeResultDOMapper.insert(path);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("上传失败", e);
|
log.error("文件上传失败: {}", originalFilename, e);
|
||||||
|
throw new RuntimeException("上传失败,请稍后重试", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -50,19 +50,20 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
private String tessdataPath;
|
private String tessdataPath;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void judgeExamWords(int count) {
|
public void judgeExamWords(int count) {
|
||||||
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
|
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
|
||||||
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
|
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
|
||||||
|
String ansSheetPath = null;
|
||||||
try {
|
try {
|
||||||
String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
|
ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
|
||||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
||||||
|
|
||||||
// 从图片中获取学生 id 和考试 id
|
// 从图片中获取学生 id 和考试 id
|
||||||
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
||||||
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
|
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
|
||||||
if (studentExamId == null) {
|
if (studentExamId == null) {
|
||||||
log.info("未找到学生 id 和考试 id");
|
log.info("未找到学生 id 和考试 id");
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生 id 和考试 id");
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生和考试");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
||||||
if(examWordsDO == null) {
|
if (examWordsDO == null) {
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -147,9 +148,14 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
boolean delete = new File(ansSheetPath).delete();
|
boolean delete = new File(ansSheetPath).delete();
|
||||||
if (delete) {
|
if (delete) {
|
||||||
log.info("删除文件成功:{}", ansSheetPath);
|
log.info("删除文件成功:{}", ansSheetPath);
|
||||||
|
} else {
|
||||||
|
log.error("删除文件失败:{}", ansSheetPath);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("识别考试失败 {}", e.getMessage());
|
log.error("识别考试失败 {}", e.getMessage());
|
||||||
|
if (ansSheetPath != null) {
|
||||||
|
new File(ansSheetPath).delete();
|
||||||
|
}
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage());
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,8 +332,24 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize) {
|
public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize, Integer classId, Integer gradeId, String studentName) {
|
||||||
return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize);
|
|
||||||
|
List<Integer> studentIds = new ArrayList<>();
|
||||||
|
if (classId != null) {
|
||||||
|
studentIds.addAll(studentDOMapper.selectStudentDOListByClassId(classId).stream().map(StudentDO::getId).toList());
|
||||||
|
}
|
||||||
|
if (gradeId != null) {
|
||||||
|
studentIds.addAll(studentDOMapper.selectStudentDOListByGradeId(gradeId).stream().map(StudentDO::getId).toList());
|
||||||
|
}
|
||||||
|
if (!studentName.isEmpty()) {
|
||||||
|
studentIds.addAll(studentDOMapper.selectStudentDOListByName(studentName).stream().map(StudentDO::getId).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (studentIds.isEmpty()) {
|
||||||
|
return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize);
|
||||||
|
} else {
|
||||||
|
return examWordsJudgeResultDOMapper.selectByPageAndStudentIds((page - 1) * pageSize, page * pageSize, studentIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -335,6 +357,11 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
return examWordsJudgeResultDOMapper.selectCount();
|
return examWordsJudgeResultDOMapper.selectCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getExamUnfinishedCount() {
|
||||||
|
return examWordsJudgeResultDOMapper.selectUnfinishedCount();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
|
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
|
||||||
return examWordsJudgeResultDOMapper.selectDetailById(id);
|
return examWordsJudgeResultDOMapper.selectDetailById(id);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -83,7 +84,9 @@ public class LoginServiceImpl implements LoginService {
|
|||||||
@Override
|
@Override
|
||||||
public void sendVerificationCode(String phone) {
|
public void sendVerificationCode(String phone) {
|
||||||
String code = RandomUtil.randomNumbers(6);
|
String code = RandomUtil.randomNumbers(6);
|
||||||
redisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
|
String key = UserRedisConstants.buildUserLoginCode(phone);
|
||||||
|
redisTemplate.opsForValue().set(key, code);
|
||||||
|
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
|
||||||
String signName = "短信测试";
|
String signName = "短信测试";
|
||||||
String templateCode = "SMS_154950909";
|
String templateCode = "SMS_154950909";
|
||||||
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.yinlihupo.enlish.service.constant.LessonPlanConstant;
|
|||||||
import com.yinlihupo.enlish.service.domain.dataobject.*;
|
import com.yinlihupo.enlish.service.domain.dataobject.*;
|
||||||
import com.yinlihupo.enlish.service.domain.mapper.*;
|
import com.yinlihupo.enlish.service.domain.mapper.*;
|
||||||
import com.yinlihupo.enlish.service.model.bo.Sentence;
|
import com.yinlihupo.enlish.service.model.bo.Sentence;
|
||||||
|
import com.yinlihupo.enlish.service.model.bo.Word;
|
||||||
import com.yinlihupo.enlish.service.service.LessonPlansService;
|
import com.yinlihupo.enlish.service.service.LessonPlansService;
|
||||||
import com.yinlihupo.enlish.service.utils.DifyClient;
|
import com.yinlihupo.enlish.service.utils.DifyClient;
|
||||||
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
|
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
|
||||||
@@ -49,13 +50,14 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
private PlanExamDOMapper planExamDOMapper;
|
private PlanExamDOMapper planExamDOMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private RedisTemplate<String, Object> redisTemplate;
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
@Resource
|
||||||
|
private ClassDOMapper classDOMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) {
|
public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) {
|
||||||
String key = LessonPlanConstant.buildGeneratePlanContent(studentId);
|
String key = LessonPlanConstant.buildGeneratePlanContent(studentId);
|
||||||
redisTemplate.opsForValue().set(key, studentId);
|
redisTemplate.opsForValue().set(key, studentId);
|
||||||
redisTemplate.expire( key, 12, TimeUnit.MINUTES);
|
redisTemplate.expire(key, 7, TimeUnit.MINUTES);
|
||||||
|
|
||||||
log.info("开始生成计划");
|
log.info("开始生成计划");
|
||||||
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId);
|
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId);
|
||||||
@@ -155,6 +157,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
log.info("开始生成周末计划");
|
||||||
int syncWeekendSize = totalWords.size() / 2;
|
int syncWeekendSize = totalWords.size() / 2;
|
||||||
for (int i = 0; i < 2; i++) {
|
for (int i = 0; i < 2; i++) {
|
||||||
List<VocabularyBankDO> checkList = totalWords.subList(i * syncWeekendSize, Math.min((i + 1) * syncWeekendSize, syncWeekendSize));
|
List<VocabularyBankDO> checkList = totalWords.subList(i * syncWeekendSize, Math.min((i + 1) * syncWeekendSize, syncWeekendSize));
|
||||||
@@ -225,11 +228,16 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
examWordsDOMapper.insert(examWordsDO);
|
examWordsDOMapper.insert(examWordsDO);
|
||||||
studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
|
studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
|
||||||
|
|
||||||
|
ClassDO classDO = classDOMapper.selectClassDOById(studentDOMapper.selectStudentById(studentId).getClassId());
|
||||||
data.put("examId", examWordsDO.getId());
|
data.put("examId", examWordsDO.getId());
|
||||||
data.put("studentId", studentId);
|
data.put("studentId", studentId);
|
||||||
data.put("studentStr", studentDO.getName());
|
data.put("studentStr", gradeDO.getTitle() + " " + classDO.getTitle() + " " + studentDO.getName());
|
||||||
data.put("examStr", ExamTitle);
|
data.put("examStr", ExamTitle);
|
||||||
data.put("checkList", words);
|
|
||||||
|
List<VocabularyBankDO> words1 = words.subList(0, words.size() / 2);
|
||||||
|
List<VocabularyBankDO> words2 = words.subList(words.size() / 2, words.size());
|
||||||
|
data.put("words1", words1);
|
||||||
|
data.put("words2", words2);
|
||||||
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||||
// Configure config = Configure.builder()
|
// Configure config = Configure.builder()
|
||||||
// .bind("checkList", policy)
|
// .bind("checkList", policy)
|
||||||
@@ -248,7 +256,8 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
List<VocabularyBankDO> checkList,
|
List<VocabularyBankDO> checkList,
|
||||||
int day,
|
int day,
|
||||||
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
|
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
|
||||||
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "第" + day + "天" + studentId;
|
|
||||||
|
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "第" + day + "天";
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("title", title);
|
data.put("title", title);
|
||||||
data.put("syncVocabList", syncVocabList);
|
data.put("syncVocabList", syncVocabList);
|
||||||
@@ -256,15 +265,17 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
data.put("reviewVocabList", reviewVocabList);
|
data.put("reviewVocabList", reviewVocabList);
|
||||||
data.put("checkList", checkList);
|
data.put("checkList", checkList);
|
||||||
data.put("checkListAns", checkList);
|
data.put("checkListAns", checkList);
|
||||||
|
|
||||||
// 中译英
|
// 中译英
|
||||||
List<VocabularyBankDO> drillRound1 = new ArrayList<>(syncVocabList);
|
List<Word> list = syncVocabList.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList();
|
||||||
|
list.forEach(word -> word.setTitle(" "));
|
||||||
|
|
||||||
|
List<Word> drillRound1 = new ArrayList<>(list);
|
||||||
Collections.shuffle(drillRound1);
|
Collections.shuffle(drillRound1);
|
||||||
data.put("drillRound1", drillRound1);
|
data.put("drillRound1", drillRound1);
|
||||||
List<VocabularyBankDO> drillRound2 = new ArrayList<>(syncVocabList);
|
List<Word> drillRound2 = new ArrayList<>(list);
|
||||||
Collections.shuffle(drillRound2);
|
Collections.shuffle(drillRound2);
|
||||||
data.put("drillRound2", drillRound2);
|
data.put("drillRound2", drillRound2);
|
||||||
List<VocabularyBankDO> drillRound3 = new ArrayList<>(syncVocabList);
|
List<Word> drillRound3 = new ArrayList<>(list);
|
||||||
Collections.shuffle(drillRound3);
|
Collections.shuffle(drillRound3);
|
||||||
data.put("drillRound3", drillRound3);
|
data.put("drillRound3", drillRound3);
|
||||||
|
|
||||||
@@ -273,8 +284,10 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
mixedDrill.addAll(syncVocabList);
|
mixedDrill.addAll(syncVocabList);
|
||||||
mixedDrill.addAll(gapVocabList);
|
mixedDrill.addAll(gapVocabList);
|
||||||
mixedDrill.addAll(reviewVocabList);
|
mixedDrill.addAll(reviewVocabList);
|
||||||
Collections.shuffle(mixedDrill);
|
List<Word> mixedList = new ArrayList<>(mixedDrill.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList());
|
||||||
data.put("mixedDrill", mixedDrill);
|
mixedList.forEach(word -> word.setDefinition(" "));
|
||||||
|
Collections.shuffle(mixedList);
|
||||||
|
data.put("mixedDrill", mixedList);
|
||||||
|
|
||||||
// 文章 A
|
// 文章 A
|
||||||
log.info("生成文章 A 中文开始");
|
log.info("生成文章 A 中文开始");
|
||||||
@@ -337,7 +350,12 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
data.put("studentId", studentId);
|
data.put("studentId", studentId);
|
||||||
data.put("studentStr", studentDO.getName());
|
data.put("studentStr", studentDO.getName());
|
||||||
data.put("examStr", ExamTitle);
|
data.put("examStr", ExamTitle);
|
||||||
data.put("words", words);
|
|
||||||
|
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("生成教案小测成功");
|
log.info("生成教案小测成功");
|
||||||
|
|
||||||
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||||
|
|||||||
@@ -120,7 +120,10 @@ public class StudentServiceImpl implements StudentService {
|
|||||||
|
|
||||||
List<Integer> wordIds = new java.util.ArrayList<>(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getCorrectWordIds).flatMap(List::stream).toList());
|
List<Integer> wordIds = new java.util.ArrayList<>(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getCorrectWordIds).flatMap(List::stream).toList());
|
||||||
wordIds.addAll(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getWrongWordIds).flatMap(List::stream).toList());
|
wordIds.addAll(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getWrongWordIds).flatMap(List::stream).toList());
|
||||||
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordIds);
|
List<VocabularyBankDO> vocabularyBankDOS = new ArrayList<>();
|
||||||
|
if (!wordIds.isEmpty()) {
|
||||||
|
vocabularyBankDOS = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordIds);
|
||||||
|
}
|
||||||
Map<Integer, VocabularyBankDO> id2Word = vocabularyBankDOS.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
|
Map<Integer, VocabularyBankDO> id2Word = vocabularyBankDOS.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
|
||||||
|
|
||||||
List<ExamWordsJudgeResultDetail> examWordsJudgeResultDetails = new ArrayList<>();
|
List<ExamWordsJudgeResultDetail> examWordsJudgeResultDetails = new ArrayList<>();
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.yinlihupo.enlish.service.service.user;
|
package com.yinlihupo.enlish.service.service.user;
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
||||||
import com.yinlihupo.enlish.service.service.UserService;
|
import com.yinlihupo.enlish.service.service.UserService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -17,8 +21,10 @@ public class UserServiceImpl implements UserService {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private UserDOMapper userDOMapper;
|
private UserDOMapper userDOMapper;
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
@Resource
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDO findUser() {
|
public UserDO findUser() {
|
||||||
@@ -43,4 +49,21 @@ public class UserServiceImpl implements UserService {
|
|||||||
userDOMapper.insert(userDO);
|
userDOMapper.insert(userDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateUserInfo(String password, String reqCode, String phone, String name) {
|
||||||
|
long id = Integer.parseInt(String.valueOf(StpUtil.getLoginId()));
|
||||||
|
UserDO userDO = userDOMapper.selectById(id);
|
||||||
|
|
||||||
|
String key = UserRedisConstants.buildUserLoginCode(userDO.getPhone());
|
||||||
|
String code = Objects.requireNonNull(redisTemplate.opsForValue().get(key)).toString();
|
||||||
|
if (code == null || !code.equals(reqCode)) {
|
||||||
|
throw new RuntimeException("验证码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password != null) {
|
||||||
|
password = passwordEncoder.encode(password);
|
||||||
|
}
|
||||||
|
userDOMapper.updateUserInfo(id, name, password, phone);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class AutoJudgeExamWordsTask {
|
|||||||
|
|
||||||
@Scheduled(fixedRate = 5000)
|
@Scheduled(fixedRate = 5000)
|
||||||
public void autoJudgeExamWords() {
|
public void autoJudgeExamWords() {
|
||||||
if (examWordsJudgeService.getExamWordsJudgeResultCount() != 0) {
|
if (examWordsJudgeService.getExamUnfinishedCount() != 0) {
|
||||||
log.info("有试卷待检测,开始检测");
|
log.info("有试卷待检测,开始检测");
|
||||||
examWordsJudgeService.judgeExamWords(5);
|
examWordsJudgeService.judgeExamWords(5);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.opencv.imgproc.Imgproc;
|
|||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferByte;
|
import java.awt.image.DataBufferByte;
|
||||||
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -31,29 +32,11 @@ public class PngUtil {
|
|||||||
// 获取起始坐标
|
// 获取起始坐标
|
||||||
public static List<CoordinatesXY> analysisXY(String imagePath) {
|
public static List<CoordinatesXY> analysisXY(String imagePath) {
|
||||||
|
|
||||||
|
Mat binary = image2BinaryMath(imagePath);
|
||||||
Mat src = Imgcodecs.imread(imagePath);
|
Mat src = Imgcodecs.imread(imagePath);
|
||||||
|
|
||||||
if (src.empty()) {
|
|
||||||
System.out.println("无法读取图片,请检查路径。");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 预处理
|
|
||||||
// 3.1 转换为灰度图
|
|
||||||
Mat gray = new Mat();
|
|
||||||
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
|
||||||
|
|
||||||
// 3.2 二值化处理 (Thresholding)
|
|
||||||
// 使用 THRESH_BINARY_INV (反转二值化),因为我们需要找的是白色背景上的黑色块。
|
|
||||||
// 反转后,黑色块变成白色(255),背景变成黑色(0),方便 findContours 查找。
|
|
||||||
Mat binary = new Mat();
|
|
||||||
// 阈值设为 50 左右即可,因为块是纯黑的
|
|
||||||
Imgproc.threshold(gray, binary, 50, 255, Imgproc.THRESH_BINARY_INV);
|
|
||||||
|
|
||||||
// 4. 查找轮廓
|
// 4. 查找轮廓
|
||||||
List<MatOfPoint> contours = new ArrayList<>();
|
List<MatOfPoint> contours = new ArrayList<>();
|
||||||
Mat hierarchy = new Mat();
|
Mat hierarchy = new Mat();
|
||||||
// RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点
|
|
||||||
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
||||||
|
|
||||||
System.out.println("检测到的轮廓总数: " + contours.size());
|
System.out.println("检测到的轮廓总数: " + contours.size());
|
||||||
@@ -89,67 +72,44 @@ public class PngUtil {
|
|||||||
System.out.println("------------------------------------------------");
|
System.out.println("------------------------------------------------");
|
||||||
list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build());
|
list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build());
|
||||||
// 可选:在原图上画出框,用于调试验证
|
// 可选:在原图上画出框,用于调试验证
|
||||||
// Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
|
Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
|
||||||
// Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
|
Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
|
||||||
// Imgcodecs.imwrite("output_red.png", src);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Imgcodecs.imwrite("output_red.png", src);
|
||||||
System.out.println("找到 " + blockCount + " 个黑色块。");
|
System.out.println("找到 " + blockCount + " 个黑色块。");
|
||||||
|
|
||||||
// 获取每一列的宽度
|
// 计算起始坐标
|
||||||
list.sort(Comparator.comparingInt(CoordinatesXY::getHeight));
|
|
||||||
int height = list.get(list.size() - 1).getHeight() / ExamWordsConstant.PGN_COL;
|
|
||||||
|
|
||||||
// 删除两列答题卡区块
|
|
||||||
list.sort(Comparator.comparingInt(CoordinatesXY::getWidth));
|
|
||||||
list.remove(list.size() - 1);
|
|
||||||
list.remove(list.size() - 1);
|
|
||||||
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
|
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
|
||||||
|
|
||||||
// 计算起始坐标
|
list.forEach(coordinatesXY -> coordinatesXY.setHeight(coordinatesXY.getHeight() / 51));
|
||||||
List<CoordinatesXY> ans = getCoordinatesXIES(list, height);
|
list.forEach(coordinatesXY -> coordinatesXY.setWidth(coordinatesXY.getWidth() / 3));
|
||||||
|
list.forEach(coordinatesXY -> coordinatesXY.setX(coordinatesXY.getX() + coordinatesXY.getWidth() * 2));
|
||||||
|
|
||||||
src.release();
|
log.info("起始坐标: {}", list);
|
||||||
binary.release();
|
|
||||||
hierarchy.release();
|
|
||||||
binary.release();
|
|
||||||
|
|
||||||
return ans;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取(未背熟)单词的 id
|
// 获取(未背熟)单词的 id
|
||||||
public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Integer> wordIds, List<CoordinatesXY> coordinatesXYList) {
|
public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Integer> wordIds, List<CoordinatesXY> coordinatesXYList) {
|
||||||
|
|
||||||
Mat src = Imgcodecs.imread(filePath);
|
|
||||||
if (src.empty()) {
|
|
||||||
log.error("无法读取图片,请检查路径: {}", filePath);
|
|
||||||
throw new RuntimeException("无法读取图片");
|
|
||||||
}
|
|
||||||
|
|
||||||
Mat gray = new Mat();
|
Mat binary = image2BinaryMath(filePath);
|
||||||
Mat binary = new Mat();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
|
||||||
// 建议:如果光照不均匀,考虑使用 THRESH_OTSU 自动阈值,或者自适应阈值
|
|
||||||
Imgproc.threshold(gray, binary, 150, 255, Imgproc.THRESH_BINARY_INV);
|
|
||||||
// 调试时打印
|
|
||||||
// Imgcodecs.imwrite("output_binary.png", binary);
|
|
||||||
List<Integer> answer = new ArrayList<>();
|
List<Integer> answer = new ArrayList<>();
|
||||||
int words_index = 0;
|
int words_index = 0;
|
||||||
|
|
||||||
for (int i = 0; i < coordinatesXYList.size(); i++) {
|
for (CoordinatesXY coordinatesXY : coordinatesXYList) {
|
||||||
CoordinatesXY coordinatesXY = coordinatesXYList.get(i);
|
|
||||||
|
|
||||||
int width = coordinatesXY.getWidth();
|
int width = coordinatesXY.getWidth();
|
||||||
int height = coordinatesXY.getHeight();
|
int height = coordinatesXY.getHeight();
|
||||||
int currentX = coordinatesXY.getX();
|
int currentX = coordinatesXY.getX();
|
||||||
int currentY = coordinatesXY.getY();
|
int currentY = coordinatesXY.getY() + height;
|
||||||
|
|
||||||
int count = i == 0 ? ExamWordsConstant.PGN_COL - 1 : ExamWordsConstant.PGN_COL;
|
|
||||||
|
|
||||||
// 内层循环:遍历这一列的每一行
|
// 内层循环:遍历这一列的每一行
|
||||||
for (int j = 0; j < count; j++) {
|
for (int j = 0; j < 50; j++) {
|
||||||
// 安全检查:防止单词列表比格子少导致越界
|
// 安全检查:防止单词列表比格子少导致越界
|
||||||
if (words_index >= wordIds.size()) {
|
if (words_index >= wordIds.size()) {
|
||||||
log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
|
log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
|
||||||
@@ -168,12 +128,15 @@ public class PngUtil {
|
|||||||
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
|
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
|
||||||
Mat region = binary.submat(rect);
|
Mat region = binary.submat(rect);
|
||||||
int countNonZero = Core.countNonZero(region);
|
int countNonZero = Core.countNonZero(region);
|
||||||
|
log.info("当前位置为 words_index={},坐标为 x={} y={} 当前区域非零像素数: {}", words_index, currentX, currentY, countNonZero);
|
||||||
if (countNonZero > 500) {
|
if (countNonZero > 1000) {
|
||||||
Integer id = wordIds.get(words_index);
|
Integer id = wordIds.get(words_index);
|
||||||
answer.add(id);
|
answer.add(id);
|
||||||
log.info("检测到标记(未背熟):ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
|
log.info("检测到标记(未背熟):ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
|
||||||
}
|
}
|
||||||
|
if (countNonZero == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
region.release();
|
region.release();
|
||||||
words_index++;
|
words_index++;
|
||||||
@@ -185,8 +148,6 @@ public class PngUtil {
|
|||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
src.release();
|
|
||||||
gray.release();
|
|
||||||
binary.release();
|
binary.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,21 +167,21 @@ public class PngUtil {
|
|||||||
Rect roiRect = new Rect(0, 0, left.getX(), left.getY());
|
Rect roiRect = new Rect(0, 0, left.getX(), left.getY());
|
||||||
Mat roi = new Mat(src, roiRect);
|
Mat roi = new Mat(src, roiRect);
|
||||||
|
|
||||||
// 3. 图像预处理 (提高 OCR 准确率)
|
// // 3. 图像预处理 (提高 OCR 准确率)
|
||||||
// 3.1 转为灰度图
|
// // 3.1 转为灰度图
|
||||||
Mat gray = new Mat();
|
// Mat gray = new Mat();
|
||||||
Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
|
// Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
|
||||||
|
//
|
||||||
// 3.2 二值化 (Thresholding)
|
// // 3.2 二值化 (Thresholding)
|
||||||
// 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
|
// // 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
|
||||||
Mat binary = new Mat();
|
// Mat binary = new Mat();
|
||||||
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
|
// Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
|
||||||
|
|
||||||
// 可选:保存预处理后的图片查看效果
|
// 可选:保存预处理后的图片查看效果
|
||||||
// Imgcodecs.imwrite("debug_roi.jpg", binary);
|
Imgcodecs.imwrite("debug_roi.jpg", src);
|
||||||
|
|
||||||
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
|
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
|
||||||
BufferedImage processedImage = matToBufferedImage(binary);
|
BufferedImage processedImage = matToBufferedImage(src);
|
||||||
|
|
||||||
// 5. 使用 Tesseract 进行 OCR 识别
|
// 5. 使用 Tesseract 进行 OCR 识别
|
||||||
ITesseract instance = new Tesseract();
|
ITesseract instance = new Tesseract();
|
||||||
@@ -245,6 +206,50 @@ public class PngUtil {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Mat image2BinaryMath(String imagePath) {
|
||||||
|
|
||||||
|
if (!new File(imagePath).exists()) {
|
||||||
|
log.error("图片不存在,请检查路径: {}", imagePath);
|
||||||
|
throw new RuntimeException("图片不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Mat src = Imgcodecs.imread(imagePath);
|
||||||
|
|
||||||
|
if (src.empty()) {
|
||||||
|
log.info("无法读取图片,请检查路径: {}", imagePath);
|
||||||
|
throw new RuntimeException("无法读取图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
Mat gray = new Mat();
|
||||||
|
//转换为灰度图
|
||||||
|
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
||||||
|
|
||||||
|
Imgproc.GaussianBlur(gray, gray, new Size(5, 5), 0);
|
||||||
|
|
||||||
|
Mat binary = new Mat();
|
||||||
|
Imgproc.adaptiveThreshold(gray, binary, 255,
|
||||||
|
Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
Imgproc.THRESH_BINARY_INV, 25, 10);
|
||||||
|
|
||||||
|
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
|
||||||
|
|
||||||
|
//开运算 (Open):先腐蚀后膨胀,用于去除背景中的微小噪点
|
||||||
|
Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_OPEN, kernel);
|
||||||
|
|
||||||
|
// 闭运算 (Close):先膨胀后腐蚀,用于连接断裂的区域并填充块内部的空洞
|
||||||
|
// 如果块比较大且内部反光严重,可以将 Size(3,3) 改为 Size(5,5) 或更大
|
||||||
|
Mat closeKernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(7, 7));
|
||||||
|
Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_CLOSE, closeKernel);
|
||||||
|
|
||||||
|
// 保存二值化过程图用于调试 (生产环境可注释)
|
||||||
|
Imgcodecs.imwrite("debug_binary_natural.png", binary);
|
||||||
|
|
||||||
|
src.release();
|
||||||
|
gray.release();
|
||||||
|
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
private static @NonNull StudentExamId getStudentExamId(Pattern pattern, String result) {
|
private static @NonNull StudentExamId getStudentExamId(Pattern pattern, String result) {
|
||||||
Matcher matcher = pattern.matcher(result);
|
Matcher matcher = pattern.matcher(result);
|
||||||
StudentExamId studentExamId = new StudentExamId(0, 0);
|
StudentExamId studentExamId = new StudentExamId(0, 0);
|
||||||
@@ -276,21 +281,4 @@ public class PngUtil {
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull List<CoordinatesXY> getCoordinatesXIES(List<CoordinatesXY> list, int height) {
|
|
||||||
List<CoordinatesXY> ans = new ArrayList<>();
|
|
||||||
CoordinatesXY left = new CoordinatesXY();
|
|
||||||
left.setX(list.get(1).getX());
|
|
||||||
left.setWidth(list.get(1).getWidth());
|
|
||||||
left.setHeight(height);
|
|
||||||
left.setY(list.get(0).getY() + left.getHeight());
|
|
||||||
ans.add(left);
|
|
||||||
|
|
||||||
CoordinatesXY right = new CoordinatesXY();
|
|
||||||
right.setX(list.get(2).getX());
|
|
||||||
right.setY(list.get(0).getY());
|
|
||||||
right.setWidth(list.get(1).getWidth());
|
|
||||||
right.setHeight(height);
|
|
||||||
ans.add(right);
|
|
||||||
return ans;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ public class WordExportUtil {
|
|||||||
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||||
config = Configure.builder()
|
config = Configure.builder()
|
||||||
.bind("words", policy)
|
.bind("words", policy)
|
||||||
|
.bind("words1", policy)
|
||||||
|
.bind("words2", policy)
|
||||||
.bind("answer", policy)
|
.bind("answer", policy)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -52,12 +54,14 @@ public class WordExportUtil {
|
|||||||
.bind("checkListAns", policyLessonPlanWeekday)
|
.bind("checkListAns", policyLessonPlanWeekday)
|
||||||
.bind("sentences", policyLessonPlanWeekday)
|
.bind("sentences", policyLessonPlanWeekday)
|
||||||
.bind("sentencesAns", policyLessonPlanWeekday)
|
.bind("sentencesAns", policyLessonPlanWeekday)
|
||||||
.bind("words", policyLessonPlanWeekday)
|
.bind("words1", policy)
|
||||||
|
.bind("words2", policy)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy();
|
LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy();
|
||||||
configLessonPlanWeekend = Configure.builder()
|
configLessonPlanWeekend = Configure.builder()
|
||||||
.bind("checkList", policyLessonPlan)
|
.bind("words1", policy)
|
||||||
|
.bind("words2", policy)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ spring:
|
|||||||
datasource:
|
datasource:
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver # 指定数据库驱动类
|
driver-class-name: com.mysql.cj.jdbc.Driver # 指定数据库驱动类
|
||||||
# 数据库连接信息
|
# 数据库连接信息
|
||||||
url: jdbc:mysql://124.220.58.5:3306/enlish?allowMultiQueries=true
|
url: jdbc:mysql://124.220.58.5:3306/dev_english?allowMultiQueries=true
|
||||||
username: root # 数据库用户名
|
username: root # 数据库用户名
|
||||||
password: YLHP@admin123 # 数据库密码
|
password: YLHP@admin123 # 数据库密码
|
||||||
data:
|
data:
|
||||||
@@ -31,15 +31,15 @@ spring:
|
|||||||
|
|
||||||
|
|
||||||
templates:
|
templates:
|
||||||
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v5.docx
|
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v9.docx
|
||||||
count: 100
|
count: 100
|
||||||
data: C:\project\tess
|
data: C:\project\tess
|
||||||
plan:
|
plan:
|
||||||
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v5.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_v2.docx
|
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v3.docx
|
||||||
plan_day: 7
|
plan_day: 7
|
||||||
tmp:
|
tmp:
|
||||||
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\
|
png:
|
||||||
|
|
||||||
ai:
|
ai:
|
||||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||||
|
|||||||
@@ -31,15 +31,15 @@ spring:
|
|||||||
|
|
||||||
|
|
||||||
templates:
|
templates:
|
||||||
word: assessment_v5.docx
|
word: assessment_v9.docx
|
||||||
count: 100
|
count: 100
|
||||||
data: eng.traineddata
|
data:
|
||||||
plan:
|
plan:
|
||||||
weekday: tem_study_plan_v5.docx
|
weekday: tem_study_plan_v7.docx
|
||||||
weekend: study_plan_review_v2.docx
|
weekend: study_plan_review_v3.docx
|
||||||
plan_day: 7
|
plan_day: 7
|
||||||
tmp:
|
tmp:
|
||||||
png: tmp\png\
|
png:
|
||||||
|
|
||||||
ai:
|
ai:
|
||||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ server:
|
|||||||
|
|
||||||
spring:
|
spring:
|
||||||
profiles:
|
profiles:
|
||||||
active: dev # 默认激活 dev 本地开发环境
|
active: pro # 默认激活 dev 本地开发环境
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 30MB
|
||||||
|
max-request-size: 30MB
|
||||||
mybatis:
|
mybatis:
|
||||||
# MyBatis xml 配置文件路径
|
# MyBatis xml 配置文件路径
|
||||||
mapper-locations: classpath:/mapper/**/*.xml
|
mapper-locations: classpath:/mapper/**/*.xml
|
||||||
|
|
||||||
|
|
||||||
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
|
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
|
||||||
sa-token:
|
sa-token:
|
||||||
# token 名称(同时也是 cookie 名称)
|
# token 名称(同时也是 cookie 名称)
|
||||||
@@ -26,4 +30,7 @@ sa-token:
|
|||||||
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
||||||
is-share: true
|
is-share: true
|
||||||
# 是否输出操作日志
|
# 是否输出操作日志
|
||||||
is-log: true
|
is-log: true
|
||||||
|
#logging:
|
||||||
|
# level:
|
||||||
|
# com.yinlihupo.enlish.service.domain.mapper: debug
|
||||||
@@ -61,7 +61,6 @@
|
|||||||
<select id="selectCount" resultType="java.lang.Integer">
|
<select id="selectCount" resultType="java.lang.Integer">
|
||||||
select count(1)
|
select count(1)
|
||||||
from exam_words_judge_result
|
from exam_words_judge_result
|
||||||
where is_finished = 0
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectDetailById" resultMap="ResultMapWithBLOBs">
|
<select id="selectDetailById" resultMap="ResultMapWithBLOBs">
|
||||||
@@ -83,4 +82,22 @@
|
|||||||
and start_date between date_sub(now(), interval 7 day) and now()
|
and start_date between date_sub(now(), interval 7 day) and now()
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByPageAndStudentIds" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from exam_words_judge_result
|
||||||
|
where student_id in
|
||||||
|
<foreach item="item" index="index" collection="studentIds"
|
||||||
|
open="(" separator="," close=")">
|
||||||
|
#{item}
|
||||||
|
</foreach>
|
||||||
|
order by start_date
|
||||||
|
limit #{startIndex}, #{pageSize}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectUnfinishedCount" resultType="java.lang.Integer">
|
||||||
|
select count(*)
|
||||||
|
from exam_words_judge_result
|
||||||
|
where is_finished = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -75,4 +75,24 @@
|
|||||||
where class_id = #{classId}
|
where class_id = #{classId}
|
||||||
and is_deleted = 0
|
and is_deleted = 0
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectStudentDOListByClassId" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from student
|
||||||
|
where class_id = #{classId}
|
||||||
|
and is_deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectStudentDOListByGradeId" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from student
|
||||||
|
where grade_id = #{gradeId}
|
||||||
|
and is_deleted = 0
|
||||||
|
</select>
|
||||||
|
<select id="selectStudentDOListByName" resultMap="BaseResultMap">
|
||||||
|
select *
|
||||||
|
from student
|
||||||
|
where name like concat('%', #{name}, '%')
|
||||||
|
and is_deleted = 0
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
from student_exam_words
|
from student_exam_words
|
||||||
where student_id = #{studentId}
|
where student_id = #{studentId}
|
||||||
and exam_words_id = #{examWordsId}
|
and exam_words_id = #{examWordsId}
|
||||||
and is_completed = 0
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<update id="updateStudentExamWordsFinished">
|
<update id="updateStudentExamWordsFinished">
|
||||||
|
|||||||
@@ -20,6 +20,28 @@
|
|||||||
values (#{phone}, #{name}, #{password})
|
values (#{phone}, #{name}, #{password})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<update id="updatePassword">
|
||||||
|
update user
|
||||||
|
set password = #{password}
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateUserInfo">
|
||||||
|
update user
|
||||||
|
<set>
|
||||||
|
<if test="password != null">
|
||||||
|
password = #{password},
|
||||||
|
</if>
|
||||||
|
<if test="name != null">
|
||||||
|
`name` = #{name},
|
||||||
|
</if>
|
||||||
|
<if test="phone != null">
|
||||||
|
phone = #{phone},
|
||||||
|
</if>
|
||||||
|
</set>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
<select id="selectByPhone" resultMap="BaseResultMap">
|
<select id="selectByPhone" resultMap="BaseResultMap">
|
||||||
select *
|
select *
|
||||||
from user
|
from user
|
||||||
|
|||||||
@@ -145,5 +145,21 @@
|
|||||||
order by rand()
|
order by rand()
|
||||||
limit 100
|
limit 100
|
||||||
</select>
|
</select>
|
||||||
|
<select id="selectByGradeIdAndNotMatchIds"
|
||||||
|
resultType="com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO">
|
||||||
|
select *
|
||||||
|
from vocabulary_bank
|
||||||
|
where unit_id in (
|
||||||
|
select unit_id
|
||||||
|
from grade_unit
|
||||||
|
where grade_id = #{gradeId}
|
||||||
|
)
|
||||||
|
and id not in
|
||||||
|
<foreach item="id" collection="ids" separator="," open="(" close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
order by rand()
|
||||||
|
limit #{wordCount}
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
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.
@@ -5,6 +5,7 @@ import com.deepoove.poi.config.Configure;
|
|||||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
||||||
|
import com.yinlihupo.enlish.service.domain.mapper.ExamWordsJudgeResultDOMapper;
|
||||||
import com.yinlihupo.enlish.service.model.bo.Word;
|
import com.yinlihupo.enlish.service.model.bo.Word;
|
||||||
import com.yinlihupo.enlish.service.service.ExamWordsService;
|
import com.yinlihupo.enlish.service.service.ExamWordsService;
|
||||||
import com.yinlihupo.enlish.service.service.VocabularyService;
|
import com.yinlihupo.enlish.service.service.VocabularyService;
|
||||||
@@ -24,6 +25,8 @@ public class ExamTest {
|
|||||||
private ExamWordsService examWordsService;
|
private ExamWordsService examWordsService;
|
||||||
@Resource
|
@Resource
|
||||||
private VocabularyService vocabularyService;
|
private VocabularyService vocabularyService;
|
||||||
|
@Resource
|
||||||
|
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
|
||||||
@Test
|
@Test
|
||||||
public void test() {
|
public void test() {
|
||||||
// ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 0);
|
// ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 0);
|
||||||
|
|||||||
@@ -54,10 +54,15 @@ public class ExamWordsJudgeServiceTest {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void judege() {
|
||||||
|
examWordsJudgeService.judgeExamWords(5);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void selectExamWordsJudgeResult() {
|
public void selectExamWordsJudgeResult() {
|
||||||
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(1, 10);
|
// List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(1, 10);
|
||||||
log.info("examWordsJudgeResult:{}", examWordsJudgeResult);
|
// log.info("examWordsJudgeResult:{}", examWordsJudgeResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<title>enlish-vue</title>
|
<title>enlish-vue</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M5 6C5 4.89543 5.89543 4 7 4H12C13.6569 4 15 5.34315 15 7V17C15 18.6569 13.6569 20 12 20H7C5.89543 20 5 19.1046 5 18V6ZM7 6V10H12C12.5523 10 13 9.55228 13 9C13 8.44772 12.5523 8 12 8H8.5C7.67157 8 7 7.32843 7 6.5V6ZM7 18L7 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H7V17.5C7 17.7761 7.22386 18 7.5 18H12C12.5523 18 13 17.5523 13 17V17C13 16.4477 12.5523 16 12 16H8.5C7.67157 16 7 16.6716 7 17.5V18Z"
|
||||||
|
fill="#0056D2" />
|
||||||
|
<circle cx="17.5" cy="6.5" r="2.5" fill="#FFAB00" />
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 865 B |
@@ -1,16 +1,24 @@
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view></router-view>
|
<el-config-provider :locale="locale">
|
||||||
|
<router-view />
|
||||||
|
<el-drawer v-model="mobileSidebarOpen" class="md:hidden" title="菜单" size="260px">
|
||||||
|
<Sidebar />
|
||||||
|
</el-drawer>
|
||||||
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
const locale = zhCn
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
import { mobileSidebarOpen } from '@/composables/ui.js'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* 自定义顶部加载 Loading 颜色 */
|
/* 自定义顶部加载 Loading 颜色 */
|
||||||
#nprogress .bar {
|
#nprogress .bar {
|
||||||
background: #409eff !important;
|
background: #2563eb !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ export function uploadExamWordsPng(data) {
|
|||||||
return axios.post('/exam/words/submit', data)
|
return axios.post('/exam/words/submit', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExamWordsResult(page, size) {
|
export function getExamWordsResult(page, size, classId, gradeId, studentName) {
|
||||||
return axios.post('/exam/words/get', {
|
return axios.post('/exam/words/get', {
|
||||||
page: page,
|
page: page,
|
||||||
size: size
|
size: size,
|
||||||
|
classId: classId,
|
||||||
|
gradeId: gradeId,
|
||||||
|
studentName: studentName
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export function getVerificationCode(data) {
|
|||||||
export function getUserInfo() {
|
export function getUserInfo() {
|
||||||
return axios.post("/user/info")
|
return axios.post("/user/info")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateUserInfo(data) {
|
||||||
|
return axios.post("/user/update-user-info", data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,76 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.panel-shell {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.dark .panel-shell {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(55, 65, 81, 0.4);
|
||||||
|
}
|
||||||
|
.sidebar-fixed {
|
||||||
|
width: 220px;
|
||||||
|
min-width: 220px;
|
||||||
|
flex: 0 0 220px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--el-color-primary: #2563eb;
|
||||||
|
--el-border-radius-base: 10px;
|
||||||
|
--el-border-radius-small: 8px;
|
||||||
|
--el-border-radius-round: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app {
|
||||||
|
min-height: 100%;
|
||||||
|
background: radial-gradient(1200px at 10% 10%, #e0f2fe 0%, transparent 40%),
|
||||||
|
radial-gradient(1200px at 90% 10%, #dbeafe 0%, transparent 40%),
|
||||||
|
linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark html, .dark body, .dark #app {
|
||||||
|
background: radial-gradient(1000px at 10% 10%, rgba(30,58,138,0.35) 0%, transparent 40%),
|
||||||
|
radial-gradient(1000px at 90% 10%, rgba(2,132,199,0.3) 0%, transparent 40%),
|
||||||
|
linear-gradient(180deg, #0f172a 0%, #111827 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-fluid img,
|
||||||
|
.media-fluid video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-target {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-fixed {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.panel-shell {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
12
enlish-vue/src/composables/ui.js
Normal file
12
enlish-vue/src/composables/ui.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const mobileSidebarOpen = ref(false)
|
||||||
|
|
||||||
|
export function openMobileSidebar() {
|
||||||
|
mobileSidebarOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeMobileSidebar() {
|
||||||
|
mobileSidebarOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="新增班级" width="480px" :close-on-click-modal="false">
|
<el-dialog v-model="visible" title="新增班级" width="480px" :fullscreen="isMobile" :close-on-click-modal="false">
|
||||||
<div class="space-y-4" v-loading="loading">
|
<div class="space-y-4" v-loading="loading">
|
||||||
<el-form label-width="80px">
|
<el-form :label-width="isMobile ? 0 : 80" :label-position="isMobile ? 'top' : 'right'">
|
||||||
<el-form-item label="班级名称">
|
<el-form-item label="班级名称">
|
||||||
<el-input v-model="name" placeholder="请输入班级名称,如:二班" clearable />
|
<el-input v-model="name" placeholder="请输入班级名称,如:二班" clearable class="w-full" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="年级">
|
<el-form-item label="年级">
|
||||||
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px">
|
<el-select v-model="gradeId" placeholder="请选择年级" class="w-full sm:w-[260px]">
|
||||||
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { getGradeList } from '@/api/grade'
|
import { getGradeList } from '@/api/grade'
|
||||||
import { addClass } from '@/api/class'
|
import { addClass } from '@/api/class'
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ const loading = ref(false)
|
|||||||
const name = ref('')
|
const name = ref('')
|
||||||
const gradeId = ref(null)
|
const gradeId = ref(null)
|
||||||
const gradeOptions = ref([])
|
const gradeOptions = ref([])
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
const canSubmit = computed(() => name.value.trim().length > 0 && !!gradeId.value)
|
const canSubmit = computed(() => name.value.trim().length > 0 && !!gradeId.value)
|
||||||
|
|
||||||
@@ -71,6 +72,10 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIsMobile() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(v) => {
|
(v) => {
|
||||||
@@ -78,9 +83,22 @@ watch(
|
|||||||
name.value = ''
|
name.value = ''
|
||||||
gradeId.value = props.defaultGradeId ? Number(props.defaultGradeId) : null
|
gradeId.value = props.defaultGradeId ? Number(props.defaultGradeId) : null
|
||||||
fetchGrades()
|
fetchGrades()
|
||||||
|
updateIsMobile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.footer-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="新增年级" width="420px" :close-on-click-modal="false">
|
<el-dialog v-model="visible" title="新增年级" width="420px" :fullscreen="isMobile" :close-on-click-modal="false">
|
||||||
<div class="space-y-4" v-loading="loading">
|
<div class="space-y-4" v-loading="loading">
|
||||||
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable />
|
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { addGrade } from '@/api/grade'
|
import { addGrade } from '@/api/grade'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -29,6 +29,7 @@ const visible = computed({
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const canSubmit = computed(() => name.value.trim().length > 0)
|
const canSubmit = computed(() => name.value.trim().length > 0)
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!canSubmit.value) return
|
if (!canSubmit.value) return
|
||||||
@@ -43,12 +44,29 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIsMobile() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(v) => {
|
(v) => {
|
||||||
if (v) name.value = ''
|
if (v) name.value = ''
|
||||||
|
updateIsMobile()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.footer-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="新增学生" width="560px" :close-on-click-modal="false">
|
<el-dialog v-model="visible" title="新增学生" width="560px" :fullscreen="isMobile" :close-on-click-modal="false" class="responsive-dialog">
|
||||||
<div class="space-y-4" v-loading="loading">
|
<div class="space-y-4" v-loading="loading">
|
||||||
<el-form label-width="90px">
|
<el-form :label-width="isMobile ? 0 : 90" :label-position="isMobile ? 'top' : 'right'">
|
||||||
<el-form-item label="姓名">
|
<el-form-item label="姓名">
|
||||||
<el-input v-model="name" placeholder="请输入学生姓名" clearable />
|
<el-input v-model="name" placeholder="请输入学生姓名" clearable class="w-full" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="年级">
|
<el-form-item label="年级">
|
||||||
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px" @change="handleGradeChange">
|
<el-select v-model="gradeId" placeholder="请选择年级" class="w-full sm:w-[260px]" @change="handleGradeChange">
|
||||||
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="班级">
|
<el-form-item label="班级">
|
||||||
<el-select v-model="classId" placeholder="请选择班级" style="width: 260px">
|
<el-select v-model="classId" placeholder="请选择班级" class="w-full sm:w-[260px]">
|
||||||
<el-option v-for="c in filteredClassOptions" :key="c.id" :label="c.title" :value="c.id" />
|
<el-option v-for="c in filteredClassOptions" :key="c.id" :label="c.title" :value="c.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<div v-if="gradeId && filteredClassOptions.length === 0" class="mt-2 flex items-center gap-2">
|
<div v-if="gradeId && filteredClassOptions.length === 0" class="mt-2 flex items-center gap-2">
|
||||||
@@ -20,15 +20,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="入学时间">
|
<el-form-item label="入学时间">
|
||||||
<el-date-picker v-model="startDate" type="datetime" placeholder="选择日期时间"
|
<el-date-picker v-model="startDate" type="datetime" placeholder="选择日期时间" class="w-full"
|
||||||
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
|
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { getGradeList } from '@/api/grade'
|
import { getGradeList } from '@/api/grade'
|
||||||
import { getClassList } from '@/api/class'
|
import { getClassList } from '@/api/class'
|
||||||
import { addStudent } from '@/api/student'
|
import { addStudent } from '@/api/student'
|
||||||
@@ -61,6 +61,7 @@ const classId = ref(null)
|
|||||||
const startDate = ref('')
|
const startDate = ref('')
|
||||||
const gradeOptions = ref([])
|
const gradeOptions = ref([])
|
||||||
const classOptions = ref([])
|
const classOptions = ref([])
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
const filteredClassOptions = computed(() => {
|
const filteredClassOptions = computed(() => {
|
||||||
if (!gradeId.value) return []
|
if (!gradeId.value) return []
|
||||||
@@ -116,6 +117,10 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIsMobile() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
async (v) => {
|
async (v) => {
|
||||||
@@ -126,9 +131,26 @@ watch(
|
|||||||
startDate.value = ''
|
startDate.value = ''
|
||||||
await fetchBaseOptions()
|
await fetchBaseOptions()
|
||||||
if (gradeId.value) handleGradeChange()
|
if (gradeId.value) handleGradeChange()
|
||||||
|
updateIsMobile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.footer-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.responsive-dialog :deep(.el-dialog__body) {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="词条记录详情" width="820px" :close-on-click-modal="false">
|
<el-dialog v-model="visible" title="词条记录详情" width="820px" :fullscreen="isMobile" :close-on-click-modal="false" class="responsive-dialog">
|
||||||
<div class="space-y-4" v-loading="loading">
|
<div class="space-y-4" v-loading="loading">
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { getExamWordsDetailResult } from '@/api/exam'
|
import { getExamWordsDetailResult } from '@/api/exam'
|
||||||
import { getStudentDetail } from '@/api/student'
|
import { getStudentDetail } from '@/api/student'
|
||||||
import { getWordsListByIds } from '@/api/words'
|
import { getWordsListByIds } from '@/api/words'
|
||||||
@@ -101,6 +101,7 @@ const activeNames = ref(['correct', 'wrong'])
|
|||||||
const correctTitles = ref([])
|
const correctTitles = ref([])
|
||||||
const wrongTitles = ref([])
|
const wrongTitles = ref([])
|
||||||
const studentDetail = ref(null)
|
const studentDetail = ref(null)
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
async function fetchDetail() {
|
async function fetchDetail() {
|
||||||
if (!props.id && props.id !== 0) return
|
if (!props.id && props.id !== 0) return
|
||||||
@@ -149,10 +150,17 @@ async function fetchStudent() {
|
|||||||
studentDetail.value = res?.data?.data ?? null
|
studentDetail.value = res?.data?.data ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateIsMobile() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(v) => {
|
(v) => {
|
||||||
if (v) fetchDetail()
|
if (v) {
|
||||||
|
updateIsMobile()
|
||||||
|
fetchDetail()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
watch(
|
watch(
|
||||||
@@ -161,6 +169,19 @@ watch(
|
|||||||
if (visible.value && v !== undefined && v !== null) fetchDetail()
|
if (visible.value && v !== undefined && v !== null) fetchDetail()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.responsive-dialog :deep(.el-dialog__body) {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,85 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<nav class="bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
|
<div class="p-2">
|
||||||
<div class="flex flex-wrap justify-between items-center mx-auto max-w-screen-xl">
|
<div class="panel-shell">
|
||||||
<a href="#" class="flex items-center">
|
<nav class="fluent-nav px-4 lg:px-6 py-2.5">
|
||||||
<img src="https://flowbite.com/docs/images/logo.svg" class="mr-3 h-6 sm:h-9" alt="Flowbite Logo" />
|
<div class="flex flex-wrap justify-between items-center">
|
||||||
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Flowbite</span>
|
<a href="#" class="flex items-center">
|
||||||
</a>
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
|
||||||
<div class="flex items-center lg:order-2">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<template v-if="userName">
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
<div class="relative" ref="menuRef">
|
d="M5 6C5 4.89543 5.89543 4 7 4H12C13.6569 4 15 5.34315 15 7V17C15 18.6569 13.6569 20 12 20H7C5.89543 20 5 19.1046 5 18V6ZM7 6V10H12C12.5523 10 13 9.55228 13 9C13 8.44772 12.5523 8 12 8H8.5C7.67157 8 7 7.32843 7 6.5V6ZM7 18L7 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H7V17.5C7 17.7761 7.22386 18 7.5 18H12C12.5523 18 13 17.5523 13 17V17C13 16.4477 12.5523 16 12 16H8.5C7.67157 16 7 16.6716 7 17.5V18Z"
|
||||||
<button
|
fill="#0056D2" />
|
||||||
@click="menuOpen = !menuOpen"
|
<circle cx="17.5" cy="6.5" r="2.5" fill="#FFAB00" />
|
||||||
class="text-gray-800 dark:text-white font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 flex items-center">
|
</svg>
|
||||||
<span class="mr-2">{{ userName }}</span>
|
<span class="self-center text-xl font-semibold whitespace-nowrap">英语教育</span>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</a>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
<div class="flex items-center lg:order-2">
|
||||||
|
<template v-if="userName">
|
||||||
|
<div class="relative" ref="menuRef">
|
||||||
|
<button @click="menuOpen = !menuOpen"
|
||||||
|
class="fluent-btn font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 flex items-center">
|
||||||
|
<span class="mr-2">{{ userName }}</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div v-if="menuOpen" class="fluent-card absolute right-0 mt-2 z-50">
|
||||||
|
<router-link to="/admid" @click="menuOpen = false"
|
||||||
|
class="block px-4 py-2 fluent-link">
|
||||||
|
后台
|
||||||
|
</router-link>
|
||||||
|
<button @click="handleLogout"
|
||||||
|
class="w-full text-left block px-4 py-2 fluent-link">
|
||||||
|
登出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<button data-collapse-toggle="mobile-menu-2" type="button" @click="openMobileSidebar()"
|
||||||
|
class="inline-flex items-center p-2 ml-1 text-sm rounded-lg lg:hidden fluent-btn"
|
||||||
|
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<svg class="hidden w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div
|
|
||||||
v-if="menuOpen"
|
|
||||||
class="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded shadow z-50">
|
|
||||||
<router-link
|
|
||||||
to="/admid"
|
|
||||||
@click="menuOpen = false"
|
|
||||||
class="block px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
||||||
后台
|
|
||||||
</router-link>
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="w-full text-left block px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
||||||
登出
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1"
|
||||||
<button data-collapse-toggle="mobile-menu-2" type="button"
|
id="mobile-menu-2">
|
||||||
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
|
||||||
aria-controls="mobile-menu-2" aria-expanded="false">
|
</div>
|
||||||
<span class="sr-only">Open main menu</span>
|
</div>
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
</nav>
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
<svg class="hidden w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1" id="mobile-menu-2">
|
|
||||||
<ul class="flex flex-col mt-4 font-medium lg:flex-row lg:space-x-8 lg:mt-0">
|
|
||||||
<li>
|
|
||||||
<a href="#"
|
|
||||||
class="block py-2 pr-4 pl-3 text-white rounded bg-primary-700 lg:bg-transparent lg:text-primary-700 lg:p-0 dark:text-white"
|
|
||||||
aria-current="page">Home</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link to="/"
|
|
||||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
|
||||||
班级
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link to="/learningplan"
|
|
||||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
|
||||||
学案
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link to="/uploadpng"
|
|
||||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
|
||||||
上传图片
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
<LoginDialog v-model="showLogin" @success="refreshUser" />
|
<LoginDialog v-model="showLogin" @success="refreshUser" />
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
@@ -91,6 +74,7 @@ import { getUserInfo, logout } from '@/api/user'
|
|||||||
import { removeToken } from '@/composables/auth'
|
import { removeToken } from '@/composables/auth'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showMessage } from '@/composables/util.js'
|
import { showMessage } from '@/composables/util.js'
|
||||||
|
import { openMobileSidebar } from '@/composables/ui.js'
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
const menuOpen = ref(false)
|
const menuOpen = ref(false)
|
||||||
@@ -100,9 +84,14 @@ async function refreshUser() {
|
|||||||
try {
|
try {
|
||||||
const r = await getUserInfo()
|
const r = await getUserInfo()
|
||||||
const d = r?.data
|
const d = r?.data
|
||||||
userName.value = d?.success ? (d?.data?.name || '') : ''
|
console.log("header" + d.success)
|
||||||
|
if (d?.success) {
|
||||||
|
userName.value = d?.data?.name || ''
|
||||||
|
} else {
|
||||||
|
handleLogout()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
userName.value = ''
|
handleLogout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
@@ -113,7 +102,7 @@ async function handleLogout() {
|
|||||||
userName.value = ''
|
userName.value = ''
|
||||||
menuOpen.value = false
|
menuOpen.value = false
|
||||||
showMessage('已退出登录', 'success')
|
showMessage('已退出登录', 'success')
|
||||||
router.push('/')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onDocClick(e) {
|
function onDocClick(e) {
|
||||||
@@ -131,3 +120,83 @@ onBeforeUnmount(() => {
|
|||||||
document.removeEventListener('click', onDocClick)
|
document.removeEventListener('click', onDocClick)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.fluent-nav {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 0;
|
||||||
|
backdrop-filter: none;
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .fluent-nav {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-card {
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .fluent-card {
|
||||||
|
background: rgba(55, 65, 81, 0.4);
|
||||||
|
border-color: rgba(148, 163, 184, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-card:hover {
|
||||||
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-btn {
|
||||||
|
color: #0f172a;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
||||||
|
transition: background 200ms ease, box-shadow 200ms ease, transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .fluent-btn {
|
||||||
|
color: #e5e7eb;
|
||||||
|
background: rgba(55, 65, 81, 0.4);
|
||||||
|
border-color: rgba(148, 163, 184, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-link {
|
||||||
|
color: #2563eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: color 200ms ease, background 200ms ease, box-shadow 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-link:hover {
|
||||||
|
color: #1d4ed8;
|
||||||
|
background: rgba(255, 255, 255, 0.35);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .fluent-link:hover {
|
||||||
|
background: rgba(55, 65, 81, 0.35);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluent-card {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.el-header) {
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
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>
|
||||||
@@ -25,7 +25,7 @@ function sortData(arr) {
|
|||||||
|
|
||||||
function toSource(arr) {
|
function toSource(arr) {
|
||||||
return sortData(arr).map(it => ({
|
return sortData(arr).map(it => ({
|
||||||
startTime: it.startTime,
|
startTime: it.startTime.replace('T', ' '),
|
||||||
totalCount: Number(it.totalCount) || 0,
|
totalCount: Number(it.totalCount) || 0,
|
||||||
planId: it.planId ?? null,
|
planId: it.planId ?? null,
|
||||||
id: it.id ?? null
|
id: it.id ?? null
|
||||||
|
|||||||
79
enlish-vue/src/layouts/components/student/StudyAnalysis.vue
Normal file
79
enlish-vue/src/layouts/components/student/StudyAnalysis.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="text-md font-semibold">学习分析</div>
|
||||||
|
<el-button type="primary" size="small" :loading="analyzeLoading" @click="fetchStudyAnalyze">
|
||||||
|
生成学习分析
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<template v-if="analyzeLoading">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<el-progress :percentage="analyzeProgress" :stroke-width="10" />
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">正在生成学习分析,请稍候…</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="analysisHtml">
|
||||||
|
<div class="leading-7 text-gray-700 dark:text-gray-200" v-html="analysisHtml"></div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-empty description="点击右上按钮生成学习分析" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import { getStudentStudyAnalyze } from '@/api/student'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
studentId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const analyzeLoading = ref(false)
|
||||||
|
const analyzeProgress = ref(0)
|
||||||
|
let analyzeTimer = null
|
||||||
|
const analysisText = ref('')
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: false,
|
||||||
|
linkify: true,
|
||||||
|
breaks: true
|
||||||
|
})
|
||||||
|
const analysisHtml = computed(() => {
|
||||||
|
return analysisText.value ? md.render(analysisText.value) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchStudyAnalyze() {
|
||||||
|
const id = props.studentId
|
||||||
|
if (!id) return
|
||||||
|
analyzeLoading.value = true
|
||||||
|
analyzeProgress.value = 0
|
||||||
|
if (analyzeTimer) {
|
||||||
|
clearInterval(analyzeTimer)
|
||||||
|
analyzeTimer = null
|
||||||
|
}
|
||||||
|
analyzeTimer = setInterval(() => {
|
||||||
|
const inc = Math.floor(Math.random() * 8) + 3
|
||||||
|
const next = analyzeProgress.value + inc
|
||||||
|
analyzeProgress.value = next >= 90 ? 90 : next
|
||||||
|
}, 300)
|
||||||
|
try {
|
||||||
|
const res = await getStudentStudyAnalyze({
|
||||||
|
studentId: Number(id)
|
||||||
|
})
|
||||||
|
const d = res.data
|
||||||
|
const raw = typeof d?.data === 'string' ? d.data : ''
|
||||||
|
analysisText.value = raw.replace(/\\n/g, '\n')
|
||||||
|
} finally {
|
||||||
|
analyzeProgress.value = 100
|
||||||
|
if (analyzeTimer) {
|
||||||
|
clearInterval(analyzeTimer)
|
||||||
|
analyzeTimer = null
|
||||||
|
}
|
||||||
|
analyzeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,65 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div class="text-lg font-semibold mb-4">学案查询</div>
|
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
<Sidebar />
|
||||||
<el-input v-model="searchName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
</el-aside>
|
||||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
|
||||||
<el-button @click="onReset">重置</el-button>
|
<el-main class="">
|
||||||
</div>
|
<div class="panel-shell p-6">
|
||||||
<el-table ref="tableRef" :data="rows" border class="w-full" v-loading="loading" row-key="id">
|
<div class="text-lg font-semibold mb-4">学案查询</div>
|
||||||
<el-table-column type="expand">
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||||
<template #default="{ row }">
|
<el-input v-model="searchName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
||||||
<div class="p-3">
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
<div class="text-sm font-semibold mb-2">学案</div>
|
<el-button @click="onReset">重置</el-button>
|
||||||
<el-table :data="row.plans || []" size="small" border>
|
</div>
|
||||||
<el-table-column prop="title" label="标题" min-width="280" />
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
<el-table-column label="状态" width="120">
|
<el-table ref="tableRef" :data="rows" border class="min-w-[720px]" v-loading="loading" row-key="id">
|
||||||
<template #default="{ row: plan }">
|
<el-table-column type="expand">
|
||||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
<template #default="{ row }">
|
||||||
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
<div class="p-3">
|
||||||
</el-tag>
|
<div class="text-sm font-semibold mb-2">学案</div>
|
||||||
</template>
|
<div class="overflow-x-auto">
|
||||||
</el-table-column>
|
<el-table :data="row.plans || []" size="small" border class="min-w-[600px]">
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column prop="title" label="标题" min-width="280" />
|
||||||
<template #default="{ row: plan }">
|
<el-table-column label="状态" width="120">
|
||||||
<el-button
|
<template #default="{ row: plan }">
|
||||||
type="primary"
|
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'"
|
||||||
size="small"
|
effect="plain">
|
||||||
:loading="downloadingIds.includes(plan.id)"
|
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||||
@click="onDownload(plan)"
|
</el-tag>
|
||||||
>下载</el-button>
|
</template>
|
||||||
<el-button
|
</el-table-column>
|
||||||
class="ml-2"
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
type="success"
|
<template #default="{ row: plan }">
|
||||||
size="small"
|
<el-button type="primary" size="small"
|
||||||
:disabled="plan.isFinished === 1"
|
:loading="downloadingIds.includes(plan.id)"
|
||||||
:loading="finishingIds.includes(plan.id)"
|
@click="onDownload(plan)">下载</el-button>
|
||||||
@click="onFinish(row.id, plan.id, plan)"
|
<el-button class="ml-2" type="primary" size="small"
|
||||||
>完成</el-button>
|
:disabled="plan.isFinished === 1"
|
||||||
</template>
|
:loading="finishingIds.includes(plan.id)"
|
||||||
</el-table-column>
|
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
|
||||||
</el-table>
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="姓名" min-width="140" />
|
||||||
|
<el-table-column prop="className" label="班级" min-width="140" />
|
||||||
|
<el-table-column prop="gradeName" label="年级" min-width="140" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="sm:hidden space-y-3">
|
||||||
|
<div v-for="row in rows" :key="row.id" class="panel-shell p-4">
|
||||||
|
<div class="text-base font-semibold mb-2">{{ row.name }}</div>
|
||||||
|
<div class="text-sm text-gray-700 mb-1">班级:{{ row.className }}</div>
|
||||||
|
<div class="text-sm text-gray-700 mb-3">年级:{{ row.gradeName }}</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="text-sm font-medium">学案</div>
|
||||||
|
<el-button size="small" @click="toggleMobileExpand(row.id)">
|
||||||
|
{{ mobileExpanded[row.id] ? '收起' : '展开' }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div v-if="mobileExpanded[row.id]" class="mt-3 space-y-2">
|
||||||
</el-table-column>
|
<div v-for="plan in (row.plans || [])" :key="plan.id"
|
||||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
class="rounded-lg border border-white/30 bg-white/50 p-3">
|
||||||
<el-table-column prop="className" label="班级" min-width="120" />
|
<div class="text-sm font-medium mb-2">{{ plan.title }}</div>
|
||||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
<div class="mb-2">
|
||||||
</el-table>
|
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
||||||
<div class="mt-4 flex justify-end">
|
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
</el-tag>
|
||||||
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
</div>
|
||||||
@size-change="handleSizeChange" />
|
<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"
|
||||||
|
@size-change="handleSizeChange" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-main>
|
||||||
</el-main>
|
</el-container>
|
||||||
|
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +110,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { findStudentLessonPlans, finishLessonPlan } from '@/api/studentLessonPlans'
|
import { findStudentLessonPlans, finishLessonPlan } from '@/api/studentLessonPlans'
|
||||||
import { downloadLessonPlan } from '@/api/plan'
|
import { downloadLessonPlan } from '@/api/plan'
|
||||||
import { showMessage } from '@/composables/util'
|
import { showMessage } from '@/composables/util'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -81,6 +121,12 @@ const searchName = ref('')
|
|||||||
const tableRef = ref(null)
|
const tableRef = ref(null)
|
||||||
const downloadingIds = ref([])
|
const downloadingIds = ref([])
|
||||||
const finishingIds = ref([])
|
const finishingIds = ref([])
|
||||||
|
const mobileExpanded = ref({})
|
||||||
|
|
||||||
|
function toggleMobileExpand(id) {
|
||||||
|
const v = mobileExpanded.value[id]
|
||||||
|
mobileExpanded.value[id] = !v
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchLessonPlans() {
|
async function fetchLessonPlans() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|||||||
@@ -1,72 +1,94 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-screen h-screen overflow-hidden flex">
|
<div
|
||||||
<div class="flex-1 bg-black">
|
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">
|
||||||
<el-image fit="cover" src="https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg"
|
<div class="absolute inset-0 bg-gradient-to-br from-[rgba(30,20,50,0.4)] to-[rgba(10,10,20,0.6)]"></div>
|
||||||
style="width: 100%; height: 100%" />
|
|
||||||
</div>
|
<div
|
||||||
<div class="w-[500px] z-10 bg-white dark:bg-gray-800 p-8">
|
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="mb-4 grid grid-cols-2 gap-2 justify-center pt-64">
|
<div class="text-center mb-6">
|
||||||
<button type="button" @click="switchMode('login')" class="w-full px-3 py-1.5 rounded text-sm"
|
<h2 class="text-xl sm:text-2xl font-semibold tracking-wide mb-1">Welcome Back</h2>
|
||||||
:class="mode === 'login' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
<p class="text-sm text-white/80">智慧英语 · 让学习更简单</p>
|
||||||
登录
|
|
||||||
</button>
|
|
||||||
<button type="button" @click="switchMode('register')" class="w-full px-3 py-1.5 rounded text-sm"
|
|
||||||
:class="mode === 'register' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
|
||||||
注册
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
|
||||||
<h1 class="text-2xl font-bold" v-if="mode === 'login'">登录</h1>
|
<div class="flex justify-center mb-6 border-b border-white/10">
|
||||||
<h1 class="text-2xl font-bold" v-else>注册新用户</h1>
|
<button class="px-4 sm:px-5 py-2 text-white/70 hover:text-white transition"
|
||||||
<p class="text-sm text-gray-500" v-if="mode === 'login'">请输入手机号和密码进行登录</p>
|
:class="mode === 'login' ? 'font-bold text-white border-b-2 border-white' : ''"
|
||||||
<p class="text-sm text-gray-500" v-else>请填写以下信息进行注册</p>
|
@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>
|
</div>
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" class="space-y-4">
|
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" class="mt-2">
|
||||||
<el-form-item prop="phone">
|
<el-form-item prop="phone">
|
||||||
<el-input v-model="form.phone" maxlength="11" placeholder="手机号" />
|
<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>
|
</el-form-item>
|
||||||
<template v-if="mode === 'login'">
|
|
||||||
<el-form-item prop="password">
|
<transition name="tabfade" mode="out-in">
|
||||||
<el-input v-model="form.password" type="password" maxlength="20" placeholder="密码" />
|
<div :key="mode">
|
||||||
</el-form-item>
|
<template v-if="mode === 'login'">
|
||||||
</template>
|
<el-form-item prop="password">
|
||||||
<template v-else>
|
<div class="input-glass relative mb-4 w-full">
|
||||||
<el-form-item prop="name">
|
<el-input v-model="form.password" type="password" maxlength="20" placeholder="请输入密码" />
|
||||||
<el-input v-model="form.name" 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>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item prop="password">
|
</el-form-item>
|
||||||
<el-input v-model="form.password" type="password" maxlength="20" placeholder="设置密码" />
|
</template>
|
||||||
</el-form-item>
|
<template v-else>
|
||||||
<el-form-item prop="password_repeat">
|
<el-form-item prop="name">
|
||||||
<el-input v-model="form.password_repeat" type="password" maxlength="20" placeholder="重复密码" />
|
<div class="input-glass relative mb-4 w-full">
|
||||||
</el-form-item>
|
<el-input v-model="form.name" maxlength="20" placeholder="姓名" />
|
||||||
<el-form-item prop="invitationCode">
|
<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>
|
||||||
<el-input v-model="form.invitationCode" maxlength="6" placeholder="邀请码" />
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="code">
|
<el-form-item prop="password">
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="input-glass relative mb-4 w-full">
|
||||||
<el-input v-model="form.code" maxlength="6" placeholder="验证码" />
|
<el-input v-model="form.password" type="password" maxlength="20" placeholder="设置密码" />
|
||||||
<el-button type="primary" @click="sendCode" :disabled="codeDisabled || !form.phone">
|
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
{{ codeBtnText }}
|
</div>
|
||||||
</el-button>
|
</el-form-item>
|
||||||
</div>
|
<el-form-item prop="password_repeat">
|
||||||
</el-form-item>
|
<div class="input-glass relative mb-4 w-full">
|
||||||
</template>
|
<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>
|
</el-form>
|
||||||
<div class="mt-6">
|
|
||||||
<el-button :type="mode === 'login' ? 'success' : 'warning'" plain class="w-full" :disabled="loading"
|
<button
|
||||||
@click="userLogin">
|
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"
|
||||||
<span v-if="!loading && mode === 'login'">立即登录</span>
|
: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 === 'login'">登录中...</span>
|
||||||
<span v-if="loading && mode === 'register'">处理中...</span>
|
<span v-if="!loading && mode === 'register'">立即注册</span>
|
||||||
</el-button>
|
<span v-if="loading && mode === 'register'">处理中...</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="absolute bottom-2 left-6 text-white drop-shadow">
|
|
||||||
<h2 class="text-xl">欢迎来到智慧英语</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -200,4 +222,26 @@ async function sendCode() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.input-glass :deep(.el-input__wrapper) {
|
||||||
|
@apply w-full bg-white/10 border border-white/10 rounded-xl pl-10 transition;
|
||||||
|
}
|
||||||
|
.input-glass :deep(.el-input__wrapper:hover),
|
||||||
|
.input-glass :deep(.el-input__wrapper.is-focus) {
|
||||||
|
@apply bg-white/15 border-white/50;
|
||||||
|
}
|
||||||
|
.input-glass :deep(.el-input__inner) {
|
||||||
|
@apply text-white placeholder:text-white/70;
|
||||||
|
}
|
||||||
|
.tabfade-enter-active, .tabfade-leave-active {
|
||||||
|
transition: all .25s ease;
|
||||||
|
}
|
||||||
|
.tabfade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px) scale(.98);
|
||||||
|
}
|
||||||
|
.tabfade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px) scale(.98);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,53 +1,69 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header>
|
|
||||||
<Header></Header>
|
|
||||||
</el-header>
|
<el-container class="pt-4">
|
||||||
<el-main class="p-4">
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<el-main class="p-2">
|
||||||
<div class="text-lg font-semibold mb-4">TTS</div>
|
<div class="panel-shell p-6">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="text-lg font-semibold mb-4">TTS</div>
|
||||||
<el-input v-model="planIdInput" placeholder="planId" style="max-width: 220px" />
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-4">
|
||||||
<el-button type="primary" :loading="loadingWords" @click="onLoadWords">加载词汇</el-button>
|
<!-- <el-input v-model="planIdInput" placeholder="planId" class="w-full sm:w-auto" style="max-width: 220px" /> -->
|
||||||
<el-select v-model="voice" placeholder="选择声线" style="max-width: 160px">
|
<el-button type="primary" :loading="loadingWords" @click="onLoadWords">加载词汇</el-button>
|
||||||
<el-option label="alloy" value="alloy" />
|
<!-- <el-select v-model="voice" placeholder="选择声线" class="w-full sm:w-auto" style="max-width: 160px">
|
||||||
<el-option label="verse" value="verse" />
|
<el-option label="alloy" value="alloy" />
|
||||||
<el-option label="nova" value="nova" />
|
<el-option label="verse" value="verse" />
|
||||||
</el-select>
|
<el-option label="nova" value="nova" />
|
||||||
<el-select v-model="format" placeholder="格式" style="max-width: 120px">
|
</el-select> -->
|
||||||
<el-option label="mp3" value="mp3" />
|
<!-- <el-select v-model="format" placeholder="格式" class="w-full sm:w-auto" style="max-width: 120px">
|
||||||
<el-option label="wav" value="wav" />
|
<el-option label="mp3" value="mp3" />
|
||||||
<el-option label="ogg" value="ogg" />
|
<el-option label="wav" value="wav" />
|
||||||
</el-select>
|
<el-option label="ogg" value="ogg" />
|
||||||
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll"
|
</el-select> -->
|
||||||
@click="onGenerateAll">生成全部音频</el-button>
|
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll" class="!ml-0"
|
||||||
|
@click="onGenerateAll">生成音频</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="sm:hidden">
|
||||||
|
<div v-for="row in tableData" :key="row.word" class="panel-shell p-4 mb-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="font-medium">{{ row.word }}</div>
|
||||||
|
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
||||||
|
{{ row.audioUrl ? '已生成' : '未生成' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<el-button size="small" type="primary" :loading="row.loading" @click="onGenerateOne(row)">生成音频</el-button>
|
||||||
|
<el-button size="small" :disabled="!row.audioUrl" @click="onPlay(row)">播放</el-button>
|
||||||
|
<el-button size="small" :disabled="!row.audioUrl" @click="onDownload(row)">下载</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
|
<el-table :data="tableData" border class="min-w-[640px]" v-loading="loadingWords" size="small">
|
||||||
|
<el-table-column prop="word" label="词汇/短语" min-width="200" />
|
||||||
|
<el-table-column label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
||||||
|
{{ row.audioUrl ? '已生成' : '未生成' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="primary" :loading="row.loading" @click="onGenerateOne(row)">生成音频</el-button>
|
||||||
|
<el-button size="small" class="ml-2" :disabled="!row.audioUrl" @click="onPlay(row)">播放</el-button>
|
||||||
|
<el-button size="small" class="ml-2" :disabled="!row.audioUrl" @click="onDownload(row)">下载</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-sm text-gray-500">
|
||||||
|
共 {{ words.length }} 条
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="tableData" border class="w-full" v-loading="loadingWords">
|
</el-main>
|
||||||
<el-table-column prop="word" label="词汇/短语" min-width="260" />
|
</el-container>
|
||||||
<el-table-column label="状态" width="160">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
|
||||||
{{ row.audioUrl ? '已生成' : '未生成' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="360" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button size="small" type="primary" :loading="row.loading"
|
|
||||||
@click="onGenerateOne(row)">生成音频</el-button>
|
|
||||||
<el-button size="small" class="ml-2" :disabled="!row.audioUrl"
|
|
||||||
@click="onPlay(row)">播放</el-button>
|
|
||||||
<el-button size="small" class="ml-2" :disabled="!row.audioUrl"
|
|
||||||
@click="onDownload(row)">下载</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="mt-3 text-sm text-gray-500">
|
|
||||||
共 {{ words.length }} 条
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-main>
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -59,7 +75,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { getLessonPlanWords } from '@/api/plan'
|
import { getLessonPlanWords } from '@/api/plan'
|
||||||
import { synthesizeOpenAITTS } from '@/api/tts'
|
import { synthesizeOpenAITTS } from '@/api/tts'
|
||||||
import { showMessage } from '@/composables/util'
|
import { showMessage } from '@/composables/util'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const planIdInput = ref(route.query.planId ? String(route.query.planId) : '')
|
const planIdInput = ref(route.query.planId ? String(route.query.planId) : '')
|
||||||
const words = ref([])
|
const words = ref([])
|
||||||
|
|||||||
@@ -5,73 +5,104 @@
|
|||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container>
|
||||||
<el-card>
|
<el-main class="p-2">
|
||||||
<div class="flex items-center mb-4">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<el-input v-model="query.name" placeholder="姓名" clearable style="max-width:220px" />
|
<div class="panel-shell p-6">
|
||||||
<el-button type="primary" class="ml-2" @click="fetchList">查询</el-button>
|
<div class="flex items-center mb-4">
|
||||||
<el-button class="ml-2" @click="resetSearch">重置</el-button>
|
<el-input v-model="query.name" placeholder="姓名" clearable style="max-width:220px" />
|
||||||
<el-button type="success" class="ml-2" @click="openCreate">新增用户</el-button>
|
<el-button type="primary" class="ml-2" @click="fetchList">查询</el-button>
|
||||||
</div>
|
<el-button class="ml-2" @click="resetSearch">重置</el-button>
|
||||||
<el-table :data="list" v-loading="loading" border stripe>
|
<el-button type="primary" class="ml-2" @click="openCreate">新增用户</el-button>
|
||||||
<el-table-column prop="name" label="姓名" />
|
</div>
|
||||||
<el-table-column prop="phone" label="手机号" />
|
<el-table :data="list" v-loading="loading" border stripe>
|
||||||
<el-table-column prop="roleName" label="角色" />
|
<el-table-column prop="name" label="姓名" />
|
||||||
</el-table>
|
<el-table-column prop="phone" label="手机号" />
|
||||||
<div class="mt-4 flex justify-end">
|
<el-table-column prop="roleName" label="角色" />
|
||||||
<el-pagination
|
</el-table>
|
||||||
background
|
<div class="mt-4 flex justify-end">
|
||||||
:current-page="page"
|
<el-pagination background :current-page="page" :page-size="pageSize" :total="totalCount"
|
||||||
:page-size="pageSize"
|
layout="prev, pager, next, sizes, total" @current-change="onPageChange"
|
||||||
:total="totalCount"
|
@size-change="onSizeChange" />
|
||||||
layout="prev, pager, next, sizes, total"
|
</div>
|
||||||
@current-change="onPageChange"
|
|
||||||
@size-change="onSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="mt-4">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span>生成邀请码</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<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>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
<div class="panel-shell p-6">
|
||||||
<el-form :model="createForm" :rules="rules" ref="createFormRef" label-width="80px">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<el-form-item label="姓名" prop="name">
|
<span class="text-lg font-semibold">生成邀请码</span>
|
||||||
<el-input v-model="createForm.name" />
|
</div>
|
||||||
</el-form-item>
|
<el-form :model="inviteForm" :rules="inviteRules" ref="inviteFormRef" label-width="120px">
|
||||||
<el-form-item label="手机号" prop="phone">
|
<el-form-item label="使用次数限制" prop="limit">
|
||||||
<el-input v-model="createForm.phone" />
|
<el-input-number v-model="inviteForm.limit" :min="1" :max="9999" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="密码" prop="password">
|
<el-form-item label="有效期(天)" prop="expire">
|
||||||
<el-input v-model="createForm.password" type="password" show-password />
|
<el-input-number v-model="inviteForm.expire" :min="1" :max="3650" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
<el-form-item>
|
||||||
<template #footer>
|
<el-button type="primary" :loading="inviteLoading"
|
||||||
<el-button @click="createVisible=false">取消</el-button>
|
@click="submitInvite">生成邀请码</el-button>
|
||||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">提交</el-button>
|
<el-button class="ml-2" @click="resetInvite">重置</el-button>
|
||||||
</template>
|
</el-form-item>
|
||||||
</el-dialog>
|
</el-form>
|
||||||
</el-main>
|
<div v-if="inviteCode" class="mt-2">
|
||||||
|
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-shell p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-lg font-semibold">修改用户信息</span>
|
||||||
|
</div>
|
||||||
|
<el-form :model="pwForm" :rules="pwRules" ref="pwFormRef" label-width="120px">
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="pwForm.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input v-model="pwForm.newPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input v-model="pwForm.confirmPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="pwForm.phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="验证码" prop="code">
|
||||||
|
<el-input v-model="pwForm.code">
|
||||||
|
<template #append>
|
||||||
|
<el-button :disabled="codeCountdown > 0 || codeSending || !pwForm.phone"
|
||||||
|
@click="sendCode">
|
||||||
|
{{ codeCountdown > 0 ? `${codeCountdown}s` : '获取验证码' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="pwLoading" @click="submitPw">修改用户信息</el-button>
|
||||||
|
<el-button class="ml-2" @click="resetPw">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
||||||
|
<el-form :model="createForm" :rules="rules" ref="createFormRef" label-width="80px">
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="createForm.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="createForm.phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input v-model="createForm.password" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="createVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="createLoading" @click="submitCreate">提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,9 +110,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
||||||
|
import { updateUserInfo, getVerificationCode } from '@/api/user'
|
||||||
import { showMessage } from '@/composables/util.js'
|
import { showMessage } from '@/composables/util.js'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
@@ -205,4 +238,109 @@ function submitInvite() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pwForm = reactive({ name: '', phone: '', newPassword: '', confirmPassword: '', code: '' })
|
||||||
|
const pwFormRef = ref()
|
||||||
|
const pwLoading = ref(false)
|
||||||
|
const pwRules = {
|
||||||
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
||||||
|
newPassword: [],
|
||||||
|
confirmPassword: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (pwForm.newPassword) {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error('请确认密码'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value !== pwForm.newPassword) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPw() {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
codeCountdown.value = 0
|
||||||
|
codeSending.value = false
|
||||||
|
pwForm.name = ''
|
||||||
|
pwForm.phone = ''
|
||||||
|
pwForm.newPassword = ''
|
||||||
|
pwForm.confirmPassword = ''
|
||||||
|
pwForm.code = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeCountdown = ref(0)
|
||||||
|
const codeSending = ref(false)
|
||||||
|
let codeTimer = null
|
||||||
|
|
||||||
|
async function sendCode() {
|
||||||
|
if (!pwForm.phone) {
|
||||||
|
showMessage('请输入手机号', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (codeSending.value || codeCountdown.value > 0) return
|
||||||
|
codeSending.value = true
|
||||||
|
try {
|
||||||
|
const r = await getVerificationCode({ phone: pwForm.phone })
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
showMessage('验证码已发送', 'success')
|
||||||
|
codeCountdown.value = 60
|
||||||
|
codeTimer = setInterval(() => {
|
||||||
|
if (codeCountdown.value > 0) {
|
||||||
|
codeCountdown.value -= 1
|
||||||
|
} else {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '发送验证码失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
codeSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function submitPw() {
|
||||||
|
pwFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
pwLoading.value = true
|
||||||
|
try {
|
||||||
|
const r = await updateUserInfo({
|
||||||
|
newPassword: pwForm.newPassword || '',
|
||||||
|
name: pwForm.name || '',
|
||||||
|
phone: pwForm.phone,
|
||||||
|
code: pwForm.code
|
||||||
|
})
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
showMessage('用户信息修改成功', 'success')
|
||||||
|
resetPw()
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '用户信息修改失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pwLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,127 +1,175 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||||
|
<Sidebar></Sidebar>
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
<div class="lg:col-span-1 flex flex-col gap-6">
|
<el-main class="h-full">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div class="lg:col-span-1 flex flex-col gap-6">
|
||||||
|
<div class="panel-shell p-6">
|
||||||
<div class="text-lg font-semibold mb-4">班级列表</div>
|
<div class="text-lg font-semibold mb-4">班级列表</div>
|
||||||
<el-table ref="classTableRef" :data="classes" border class="w-full" v-loading="loading" highlight-current-row
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
|
<el-table ref="classTableRef" :data="classes" border class="min-w-[520px]" v-loading="loading" highlight-current-row
|
||||||
<el-table-column prop="title" label="班级名称" min-width="120" />
|
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
|
||||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
<el-table-column prop="title" label="班级名称" min-width="160" />
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||||
<template #default="{ row }">
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
<el-button type="danger" size="small" @click.stop="onDeleteClass(row)">删除</el-button>
|
<template #default="{ row }">
|
||||||
</template>
|
<el-button type="danger" size="small" @click.stop="onDeleteClass(row)">删除</el-button>
|
||||||
</el-table-column>
|
</template>
|
||||||
</el-table>
|
</el-table-column>
|
||||||
<div class="mt-4 flex justify-end">
|
</el-table>
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
|
||||||
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
|
||||||
@size-change="handleSizeChange" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex justify-end">
|
<div class="sm:hidden space-y-3">
|
||||||
<el-button type="primary" @click="showAddClassDialog = true">新增班级</el-button>
|
<div v-for="row in classes" :key="row.id" class="panel-shell p-4">
|
||||||
|
<div class="text-base font-semibold mb-1">{{ row.title }}</div>
|
||||||
|
<div class="text-sm mb-2">年级:{{ row.gradeName }}</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-button size="small" type="primary" @click="onClassRowClick(row)">选择</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="onDeleteClass(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||||
|
:total="totalCount" :page-size="pageSize" :current-page="pageNo"
|
||||||
|
@current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex justify-end">
|
||||||
|
<el-button type="primary" @click="showAddClassDialog = true">新增班级</el-button>
|
||||||
|
</div>
|
||||||
|
<AddClassDialog v-model="showAddClassDialog" :default-grade-id="selectedGradeId"
|
||||||
|
@success="fetchClasses" />
|
||||||
</div>
|
</div>
|
||||||
<AddClassDialog v-model="showAddClassDialog" :default-grade-id="selectedGradeId"
|
|
||||||
@success="fetchClasses" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 lg:col-span-1 lg:row-span-2">
|
<div class="panel-shell p-6 lg:col-span-1 lg:row-span-2">
|
||||||
<div class="text-lg font-semibold mb-4">学生查询</div>
|
<div class="text-lg font-semibold mb-4">学生查询</div>
|
||||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||||
<el-input v-model="studentName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
<el-input v-model="studentName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
||||||
<el-tag v-if="selectedClassId" effect="plain">班级:{{ selectedClassTitle }} (ID: {{
|
<el-tag v-if="selectedClassId" effect="plain">班级:{{ selectedClassTitle }}</el-tag>
|
||||||
selectedClassId }})</el-tag>
|
<el-tag v-if="selectedGradeId" effect="plain">年级:{{ selectedGradeTitle }}</el-tag>
|
||||||
<el-tag v-if="selectedGradeId" effect="plain">年级:{{ selectedGradeTitle }} (ID: {{
|
<el-button type="primary" @click="fetchStudents">查询</el-button>
|
||||||
selectedGradeId }})</el-tag>
|
<el-button @click="resetStudentFilters">重置</el-button>
|
||||||
<el-button type="primary" @click="fetchStudents">查询</el-button>
|
<el-button type="success" :disabled="selectedStudentIds.length !== 1"
|
||||||
<el-button @click="resetStudentFilters">重置</el-button>
|
@click="showGenerateDialog = true">
|
||||||
<el-button type="success" :disabled="selectedStudentIds.length !== 1"
|
生成试题
|
||||||
@click="showGenerateDialog = true">
|
</el-button>
|
||||||
生成试题
|
<el-button type="warning" :disabled="selectedStudentIds.length !== 1"
|
||||||
</el-button>
|
@click="showLessonPlanDialog = true">
|
||||||
<el-button type="warning" :disabled="selectedStudentIds.length !== 1"
|
生成学案
|
||||||
@click="showLessonPlanDialog = true">
|
</el-button>
|
||||||
生成学案
|
</div>
|
||||||
</el-button>
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
</div>
|
<el-table ref="studentTableRef" :data="students" border class="min-w-[760px]"
|
||||||
<el-table ref="studentTableRef" :data="students" border class="w-full"
|
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
|
||||||
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
|
<el-table-column type="selection" width="48" />
|
||||||
<el-table-column type="selection" width="48" />
|
<el-table-column prop="name" label="姓名" min-width="120" />
|
||||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
<el-table-column prop="className" 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="gradeName" label="年级" min-width="120" />
|
<el-table-column prop="phone" label="学案" min-width="120">
|
||||||
<el-table-column prop="phone" label="学案" min-width="120">
|
<template #default="{ row }">
|
||||||
<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">
|
<template v-if="generatingPercents[row.id] !== undefined">
|
||||||
<div class="flex items-center gap-2">
|
<div class="mb-2">
|
||||||
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
|
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<el-button size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
||||||
<el-button type="primary" size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
<el-button size="small" type="info" @click="onShowAnalysis(row)">学情分析</el-button>
|
||||||
</div>
|
<el-button size="small" type="primary" @click="onViewStudent(row)">详情</el-button>
|
||||||
</template>
|
<el-button size="small" type="danger" @click="onDeleteStudent(row)">删除</el-button>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
</div>
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
</div>
|
||||||
<template #default="{ row }">
|
<div class="mt-4 flex justify-end">
|
||||||
<el-button type="primary" size="small" @click.stop="onViewStudent(row)">详情</el-button>
|
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||||
<el-button type="danger" size="small" @click.stop="onDeleteStudent(row)">删除</el-button>
|
:total="studentTotalCount" :page-size="studentPageSize"
|
||||||
</template>
|
:current-page="studentPageNo" @current-change="handleStudentPageChange"
|
||||||
</el-table-column>
|
@size-change="handleStudentSizeChange" />
|
||||||
</el-table>
|
</div>
|
||||||
<div class="mt-4 flex justify-end">
|
<ExamGenerateDialog v-model="showGenerateDialog" :student-ids="selectedStudentIds"
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total"
|
:default-grade-id="selectedGradeId" />
|
||||||
:total="studentTotalCount" :page-size="studentPageSize" :current-page="studentPageNo"
|
|
||||||
@current-change="handleStudentPageChange" @size-change="handleStudentSizeChange" />
|
|
||||||
</div>
|
|
||||||
<ExamGenerateDialog v-model="showGenerateDialog" :student-ids="selectedStudentIds"
|
|
||||||
:default-grade-id="selectedGradeId" />
|
|
||||||
<div class="mt-3 flex justify-end">
|
<div class="mt-3 flex justify-end">
|
||||||
<el-button type="primary" @click="showAddStudentDialog = true">新增学生</el-button>
|
<el-button type="primary" @click="showAddStudentDialog = true">新增学生</el-button>
|
||||||
</div>
|
</div>
|
||||||
<AddStudentDialog
|
<AddStudentDialog v-model="showAddStudentDialog" :default-class-id="selectedClassId"
|
||||||
v-model="showAddStudentDialog"
|
:default-grade-id="selectedGradeId" @success="fetchStudents" />
|
||||||
:default-class-id="selectedClassId"
|
<LessonPlanDialog v-model="showLessonPlanDialog" :student-id="selectedStudentIds[0]"
|
||||||
:default-grade-id="selectedGradeId"
|
@success="onLessonPlanGenerateSuccess" />
|
||||||
@success="fetchStudents"
|
<StudentPlanListDialog v-model="showPlanListDialog" :student-id="planStudentId" />
|
||||||
/>
|
<el-dialog v-model="showAnalysisDialog" title="学情分析" width="60%">
|
||||||
<LessonPlanDialog
|
<StudyAnalysis v-if="showAnalysisDialog" :student-id="analysisStudentId" />
|
||||||
v-model="showLessonPlanDialog"
|
</el-dialog>
|
||||||
:student-id="selectedStudentIds[0]"
|
|
||||||
@success="onLessonPlanGenerateSuccess"
|
|
||||||
/>
|
|
||||||
<StudentPlanListDialog
|
|
||||||
v-model="showPlanListDialog"
|
|
||||||
:student-id="planStudentId"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow 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
|
|
||||||
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
|
||||||
<el-table-column prop="title" label="年级名称" min-width="160" />
|
|
||||||
</el-table>
|
|
||||||
<div class="mt-4 flex justify-end">
|
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="gradeTotalCount"
|
|
||||||
:page-size="gradePageSize" :current-page="gradePageNo"
|
|
||||||
@current-change="handleGradePageChange" @size-change="handleGradeSizeChange" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<div class="panel-shell p-6" v-loading="gradeLoading">
|
||||||
</el-main>
|
<div class="text-lg font-semibold mb-4">年级列表</div>
|
||||||
|
<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="title" label="年级名称" min-width="160" />
|
||||||
|
</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"
|
||||||
|
@current-change="handleGradePageChange" @size-change="handleGradeSizeChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,6 +177,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { getClassList, deleteClass } from '@/api/class'
|
import { getClassList, deleteClass } from '@/api/class'
|
||||||
import { getGradeList, deleteGrade } from '@/api/grade'
|
import { getGradeList, deleteGrade } from '@/api/grade'
|
||||||
@@ -139,6 +188,7 @@ import AddGradeDialog from '@/layouts/components/AddGradeDialog.vue'
|
|||||||
import AddStudentDialog from '@/layouts/components/AddStudentDialog.vue'
|
import AddStudentDialog from '@/layouts/components/AddStudentDialog.vue'
|
||||||
import LessonPlanDialog from '@/layouts/components/LessonPlanDialog.vue'
|
import LessonPlanDialog from '@/layouts/components/LessonPlanDialog.vue'
|
||||||
import StudentPlanListDialog from '@/layouts/components/StudentPlanListDialog.vue'
|
import StudentPlanListDialog from '@/layouts/components/StudentPlanListDialog.vue'
|
||||||
|
import StudyAnalysis from '@/layouts/components/student/StudyAnalysis.vue'
|
||||||
import { getUnitList, deleteUnit } from '@/api/unit'
|
import { getUnitList, deleteUnit } from '@/api/unit'
|
||||||
import AddUnitDialog from '@/layouts/components/AddUnitDialog.vue'
|
import AddUnitDialog from '@/layouts/components/AddUnitDialog.vue'
|
||||||
import { useRouter, onBeforeRouteLeave } from 'vue-router'
|
import { useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||||
@@ -180,6 +230,20 @@ const generatingPercents = ref({})
|
|||||||
const pollingTimers = {}
|
const pollingTimers = {}
|
||||||
const showPlanListDialog = ref(false)
|
const showPlanListDialog = ref(false)
|
||||||
const planStudentId = ref(null)
|
const planStudentId = ref(null)
|
||||||
|
const showAnalysisDialog = ref(false)
|
||||||
|
const analysisStudentId = ref(null)
|
||||||
|
|
||||||
|
function isStudentSelected(id) {
|
||||||
|
return selectedStudentIds.value.includes(id)
|
||||||
|
}
|
||||||
|
function setStudentSelection(id, selected) {
|
||||||
|
const exists = selectedStudentIds.value.includes(id)
|
||||||
|
if (selected && !exists) {
|
||||||
|
selectedStudentIds.value = [...selectedStudentIds.value, id]
|
||||||
|
} else if (!selected && exists) {
|
||||||
|
selectedStudentIds.value = selectedStudentIds.value.filter(x => x !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const units = ref([])
|
const units = ref([])
|
||||||
const unitPageNo = ref(1)
|
const unitPageNo = ref(1)
|
||||||
@@ -275,6 +339,10 @@ function onStudentSelectionChange(rows) {
|
|||||||
function onViewStudent(row) {
|
function onViewStudent(row) {
|
||||||
router.push(`/student/${row.id}`)
|
router.push(`/student/${row.id}`)
|
||||||
}
|
}
|
||||||
|
function onShowAnalysis(row) {
|
||||||
|
analysisStudentId.value = row.id
|
||||||
|
showAnalysisDialog.value = true
|
||||||
|
}
|
||||||
function onClassRowClick(row) {
|
function onClassRowClick(row) {
|
||||||
selectedClassId.value = row.id
|
selectedClassId.value = row.id
|
||||||
selectedClassTitle.value = row.title
|
selectedClassTitle.value = row.title
|
||||||
|
|||||||
@@ -1,77 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
<el-main class="h-full">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
||||||
<div class="text-lg font-semibold mb-4">学生详情</div>
|
<div class="panel-shell p-4 sm:p-6">
|
||||||
<template v-if="detail">
|
<div class="text-lg font-semibold mb-4">学生详情</div>
|
||||||
<el-descriptions :column="1" border>
|
<template v-if="detail">
|
||||||
<el-descriptions-item label="ID">{{ detail.id }}</el-descriptions-item>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
<el-descriptions-item label="ID">{{ detail.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
|
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
|
||||||
</template>
|
</el-descriptions>
|
||||||
<template v-else>
|
</template>
|
||||||
<el-empty description="请从班级页跳转" />
|
<template v-else>
|
||||||
</template>
|
<el-empty description="请从班级页跳转" />
|
||||||
</div>
|
</template>
|
||||||
|
</div>
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="panel-shell p-4 sm:p-6">
|
||||||
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
|
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
|
||||||
<template v-if="wordStat">
|
<template v-if="wordStat">
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount }}</el-descriptions-item>
|
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount }}</el-descriptions-item>
|
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount }}</el-descriptions-item>
|
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-empty description="暂无统计" />
|
<el-empty description="暂无统计" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-shell p-4 sm:p-6">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="text-md font-semibold mb-3">学生考试记录</div>
|
||||||
<div class="text-md font-semibold mb-3">学生考试记录</div>
|
<ExamHistoryChart :data="history" />
|
||||||
<ExamHistoryChart :data="history" />
|
</div>
|
||||||
</div>
|
<div class="panel-shell p-4 sm:p-6">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="text-md font-semibold mb-3">学生学案记录</div>
|
||||||
<div class="text-md font-semibold mb-3">学生学案记录</div>
|
<PlanHistoryChart :student-id="route.params.id" />
|
||||||
<PlanHistoryChart :student-id="route.params.id" />
|
</div>
|
||||||
</div>
|
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
||||||
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
<WordMasteryHeatmap :student-id="route.params.id" :columns="heatmapColumns" />
|
||||||
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
|
</div>
|
||||||
</div>
|
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="text-md font-semibold mb-3">学情分析</div>
|
||||||
<div class="flex items-center justify-between mb-3">
|
<StudyAnalysis :student-id="route.params.id" />
|
||||||
<div class="text-md font-semibold">学习分析</div>
|
|
||||||
<el-button type="primary" size="small" :loading="analyzeLoading" @click="fetchStudyAnalyze">
|
|
||||||
生成学习分析
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<template v-if="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>
|
</div>
|
||||||
</div>
|
</el-main>
|
||||||
</el-main>
|
</el-container>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -80,31 +63,26 @@
|
|||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { getStudentDetail, getStudentStudyAnalyze } from '@/api/student'
|
import { getStudentDetail } from '@/api/student'
|
||||||
import { getStudentExamHistory } from '@/api/exam'
|
import { getStudentExamHistory } from '@/api/exam'
|
||||||
import { getWordStudentDetail } from '@/api/words'
|
import { getWordStudentDetail } from '@/api/words'
|
||||||
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
|
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
|
||||||
import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.vue'
|
import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.vue'
|
||||||
import WordMasteryHeatmap from '@/layouts/components/student/WordMasteryHeatmap.vue'
|
import WordMasteryHeatmap from '@/layouts/components/student/WordMasteryHeatmap.vue'
|
||||||
import MarkdownIt from 'markdown-it'
|
import StudyAnalysis from '@/layouts/components/student/StudyAnalysis.vue'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const detail = ref(null)
|
const detail = ref(null)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const history = ref([])
|
const history = ref([])
|
||||||
const analyzeLoading = ref(false)
|
|
||||||
const analyzeProgress = ref(0)
|
|
||||||
let analyzeTimer = null
|
|
||||||
const analysisText = ref('')
|
|
||||||
const wordStat = ref(null)
|
const wordStat = ref(null)
|
||||||
const md = new MarkdownIt({
|
const isMobile = ref(false)
|
||||||
html: false,
|
const heatmapColumns = computed(() => isMobile.value ? 20 : 50)
|
||||||
linkify: true,
|
|
||||||
breaks: true
|
function updateIsMobile() {
|
||||||
})
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
const analysisHtml = computed(() => {
|
}
|
||||||
return analysisText.value ? md.render(analysisText.value) : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
async function fetchDetail() {
|
async function fetchDetail() {
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
@@ -127,37 +105,10 @@ async function fetchExamHistory() {
|
|||||||
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
|
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
|
||||||
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
||||||
}) : []
|
}) : []
|
||||||
}
|
// 遍历 history 中的 startDate 去掉其中的 T
|
||||||
|
history.value.forEach(item => {
|
||||||
async function fetchStudyAnalyze() {
|
item.startDate = item.startDate.replace('T', ' ')
|
||||||
const id = route.params.id
|
})
|
||||||
if (!id) return
|
|
||||||
analyzeLoading.value = true
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchWordStat() {
|
async function fetchWordStat() {
|
||||||
@@ -172,5 +123,7 @@ onMounted(() => {
|
|||||||
fetchDetail()
|
fetchDetail()
|
||||||
fetchExamHistory()
|
fetchExamHistory()
|
||||||
fetchWordStat()
|
fetchWordStat()
|
||||||
|
updateIsMobile()
|
||||||
|
window.addEventListener('resize', updateIsMobile)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,65 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<Sidebar />
|
||||||
<div class="text-lg font-semibold mb-4">上传图片</div>
|
</el-aside>
|
||||||
<el-upload
|
<el-main class="">
|
||||||
:show-file-list="false"
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
:http-request="doUpload"
|
<div class="panel-shell p-6">
|
||||||
accept="image/*"
|
<div class="text-lg font-semibold mb-4">上传图片</div>
|
||||||
>
|
<el-upload :show-file-list="false" :http-request="doUpload" accept="image/*">
|
||||||
<el-button type="primary">选择图片并上传</el-button>
|
<el-button type="primary" class="w-full sm:w-auto touch-target">选择图片并上传</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<div class="text-lg font-semibold mb-4">结果集</div>
|
||||||
<el-table
|
<div class="hidden sm:block">
|
||||||
:data="list"
|
<el-form :inline="true" class="mb-4">
|
||||||
border
|
<el-form-item label="班级">
|
||||||
class="w-full"
|
<el-select v-model="classId" placeholder="选择班级" clearable filterable
|
||||||
v-loading="loading"
|
@change="onClassChange" style="min-width: 220px">
|
||||||
@row-click="handleRowClick"
|
<el-option v-for="item in classOptions" :key="item.id"
|
||||||
>
|
:label="`${item.title}${item.gradeName ? '(' + item.gradeName + ')' : ''}`"
|
||||||
<el-table-column prop="studentName" label="学生姓名" min-width="70" />
|
:value="item.id" />
|
||||||
<el-table-column prop="examWordsTitle" label="试题名称" min-width="100" />
|
</el-select>
|
||||||
<el-table-column prop="correctWordCount" label="正确词数" width="110" />
|
</el-form-item>
|
||||||
<el-table-column prop="wrongWordCount" label="错误词数" width="110" />
|
<el-form-item label="年级">
|
||||||
<el-table-column label="完成状态" width="110">
|
<el-select v-model="gradeId" placeholder="选择年级" clearable filterable
|
||||||
<template #default="{ row }">
|
style="min-width: 220px">
|
||||||
<el-tag :type="row.isFinished === 1 ? 'success' : 'info'">
|
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title"
|
||||||
{{ row.isFinished === 1 ? '已完成' : '未完成' }}
|
:value="g.id" />
|
||||||
</el-tag>
|
</el-select>
|
||||||
</template>
|
</el-form-item>
|
||||||
</el-table-column>
|
<el-form-item label="学生姓名">
|
||||||
<el-table-column prop="startDate" label="开始时间" min-width="160">
|
<el-input v-model="studentName" placeholder="学生姓名" clearable />
|
||||||
<template #default="{ row }">
|
</el-form-item>
|
||||||
{{ row.startDate.replace('T', ' ') }}
|
<el-form-item>
|
||||||
</template>
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
</el-table-column>
|
<el-button @click="handleReset">重置</el-button>
|
||||||
<el-table-column prop="msg" label="判卷结算" min-width="160" />
|
</el-form-item>
|
||||||
</el-table>
|
</el-form>
|
||||||
<div class="mt-4 flex justify-end">
|
</div>
|
||||||
<el-pagination
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
background
|
<el-table :data="list" border class="min-w-[800px]" v-loading="loading"
|
||||||
layout="prev, pager, next, sizes, total"
|
@row-click="handleRowClick">
|
||||||
:total="totalCount"
|
<el-table-column prop="studentName" label="学生姓名" min-width="120" />
|
||||||
:page-size="pageSize"
|
<el-table-column prop="examWordsTitle" label="试题名称" min-width="160" />
|
||||||
:current-page="pageNo"
|
<el-table-column prop="correctWordCount" label="正确词数" width="120" />
|
||||||
@current-change="handlePageChange"
|
<el-table-column prop="wrongWordCount" label="错误词数" width="120" />
|
||||||
@size-change="handleSizeChange"
|
<el-table-column label="完成状态" width="120">
|
||||||
/>
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.isFinished === 1 ? 'success' : 'info'">
|
||||||
|
{{ row.isFinished === 1 ? '已完成' : '未完成' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="startDate" label="开始时间" min-width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.startDate.replace('T', ' ') }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="msg" label="判卷结算" min-width="180" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="sm:hidden space-y-3">
|
||||||
|
<div class="mb-3 grid grid-cols-1 gap-3">
|
||||||
|
<el-select v-model="classId" placeholder="选择班级" clearable filterable @change="onClassChange" />
|
||||||
|
<el-select v-model="gradeId" placeholder="选择年级" clearable filterable />
|
||||||
|
<el-input v-model="studentName" placeholder="学生姓名" clearable />
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-button type="primary" class="flex-1" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button class="flex-1" @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="row in list" :key="row.id" class="panel-shell p-4">
|
||||||
|
<div class="text-base font-semibold mb-1">{{ row.studentName }}</div>
|
||||||
|
<div class="text-sm mb-1">试题:{{ row.examWordsTitle }}</div>
|
||||||
|
<div class="text-sm mb-1">正确:{{ row.correctWordCount }},错误:{{ row.wrongWordCount }}</div>
|
||||||
|
<div class="text-sm mb-1">开始:{{ row.startDate.replace('T', ' ') }}</div>
|
||||||
|
<div class="text-sm mb-2">结算:{{ row.msg }}</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<el-tag :type="row.isFinished === 1 ? 'success' : 'info'">
|
||||||
|
{{ row.isFinished === 1 ? '已完成' : '未完成' }}
|
||||||
|
</el-tag>
|
||||||
|
<el-button size="small" type="primary" @click="handleRowClick(row)">查看详情</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
||||||
|
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
||||||
|
@size-change="handleSizeChange" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<ExamWordsDetailCard v-model="showDetail" :id="selectedId" />
|
||||||
<ExamWordsDetailCard v-model="showDetail" :id="selectedId" />
|
</el-main>
|
||||||
</el-main>
|
</el-container>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -70,6 +111,9 @@ import Header from '@/layouts/components/Header.vue'
|
|||||||
import ExamWordsDetailCard from '@/layouts/components/ExamWordsDetailCard.vue'
|
import ExamWordsDetailCard from '@/layouts/components/ExamWordsDetailCard.vue'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { uploadExamWordsPng, getExamWordsResult } from '@/api/exam'
|
import { uploadExamWordsPng, getExamWordsResult } from '@/api/exam'
|
||||||
|
import { getClassList } from '@/api/class'
|
||||||
|
import { getGradeList } from '@/api/grade'
|
||||||
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
const pageNo = ref(1)
|
const pageNo = ref(1)
|
||||||
@@ -78,11 +122,22 @@ const totalCount = ref(0)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const showDetail = ref(false)
|
const showDetail = ref(false)
|
||||||
const selectedId = ref(null)
|
const selectedId = ref(null)
|
||||||
|
const classId = ref(null)
|
||||||
|
const gradeId = ref(null)
|
||||||
|
const studentName = ref('')
|
||||||
|
const classOptions = ref([])
|
||||||
|
const gradeOptions = ref([])
|
||||||
|
|
||||||
async function fetchList() {
|
async function fetchList() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getExamWordsResult(pageNo.value, pageSize.value)
|
const res = await getExamWordsResult(
|
||||||
|
pageNo.value,
|
||||||
|
pageSize.value,
|
||||||
|
classId.value,
|
||||||
|
gradeId.value,
|
||||||
|
studentName.value
|
||||||
|
)
|
||||||
const d = res.data
|
const d = res.data
|
||||||
list.value = Array.isArray(d.data) ? d.data : []
|
list.value = Array.isArray(d.data) ? d.data : []
|
||||||
totalCount.value = d.totalCount || 0
|
totalCount.value = d.totalCount || 0
|
||||||
@@ -93,6 +148,18 @@ async function fetchList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchClassOptions() {
|
||||||
|
const res = await getClassList(1, 200)
|
||||||
|
const d = res.data
|
||||||
|
classOptions.value = Array.isArray(d.data) ? d.data : []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGradeOptions() {
|
||||||
|
const res = await getGradeList(1, 200)
|
||||||
|
const d = res.data
|
||||||
|
gradeOptions.value = Array.isArray(d.data) ? d.data : []
|
||||||
|
}
|
||||||
|
|
||||||
function handlePageChange(p) {
|
function handlePageChange(p) {
|
||||||
pageNo.value = p
|
pageNo.value = p
|
||||||
fetchList()
|
fetchList()
|
||||||
@@ -117,10 +184,29 @@ function handleRowClick(row) {
|
|||||||
showDetail.value = true
|
showDetail.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
pageNo.value = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
classId.value = null
|
||||||
|
gradeId.value = null
|
||||||
|
studentName.value = ''
|
||||||
|
pageNo.value = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClassChange(val) {
|
||||||
|
const item = classOptions.value.find(i => i.id === val)
|
||||||
|
gradeId.value = item ? item.gradeId : null
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchList()
|
fetchList()
|
||||||
|
fetchClassOptions()
|
||||||
|
fetchGradeOptions()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user