Compare commits

...

2 Commits

Author SHA1 Message Date
lbw
fb29acc145 feat(ui): 提升界面响应式支持和移动端适配体验
- 新增移动端全屏对话框支持及标签宽度和位置动态调整,优化新增班级、年级和学生弹窗布局
- 所有对话框增加屏幕宽度监听,实现自动切换移动端和桌面端样式
- 表格组件增加移动端列表视图,隐藏侧边栏并改进分页和按钮自适应,提升小屏幕浏览体验
- Dialog及详情弹窗添加最大高度限制并启用滚动,防止移动端显示区域拥挤
- 登录页增加安全区域内边距,保证iOS等设备显示完整性
- 新增移动端菜单抽屉组件,支持手机端侧边栏交互显示
- 学生详情页调整词汇热力图列数,实现移动端更合理布局
- 表格和按钮统一增设触控友好大尺寸区域,提升移动端操作便利性
- 修正后端空词汇ID查询问题,避免空列表导致查询异常
- 统一隐藏小屏幕时的固定侧边栏,避免界面混乱和重复显示
- 搜索页和上传页表格添加移动端适配样式和展开收起逻辑,提升列表浏览灵活性
2026-01-05 18:47:50 +08:00
lbw
7182371c92 fix(enlish-service): 优化单词数据处理及模板配置更新
- 修改开发和生产环境配置中的数据库连接及模板文件路径
- ExamWordsController新增单词列表拆分为两部分返回
- ExamWordsServiceImpl增加单词数量不足时补充逻辑,确保单词数量满足要求
- LessonPlansServiceImpl优化教案数据组装,增加班级信息及单词列表拆分功能
- PngUtil调整图像二值化阈值,完善轮廓检测及未背熟单词识别逻辑,移除冗余代码
- SaTokenConfigure更新路由权限配置,添加对tts接口的不拦截支持
- 删除StudentExamWordsDOMapper中is_completed条件,调整查询方式
- UserController修正接口日志注释,准确描述修改用户信息功能
- VocabularyBankDOMapper新增根据年级与排除ID查询单词接口及SQL映射
- WordExportUtil更新导出配置,支持拆分单词列表绑定两个集合以适应新结构
2026-01-05 18:09:36 +08:00
32 changed files with 489 additions and 207 deletions

View File

@@ -25,11 +25,13 @@ public class SaTokenConfigure implements WebMvcConfigurer {
SaRouter.match("/**")
.notMatch("/login/**")
.notMatch("plan/word/voice")
.notMatch("/plan/word/voice")
.notMatch("/plan/word/voice/tts")
.check(r -> StpUtil.checkLogin());
SaRouter.match("/admin/**")
.notMatch("plan/word/voice")
.notMatch("/plan/word/voice")
.notMatch("/plan/word/voice/tts")
.check(r -> StpUtil.checkRole("root"));
}))

View File

@@ -72,6 +72,12 @@ public class ExamWordsController {
data.put("examStr", examWordsDO.getTitle());
data.put("words", assessmentWords);
data.put("answer", assessmentWords);
List<Word> words1 = assessmentWords.subList(0, assessmentWords.size() / 2);
List<Word> words2 = assessmentWords.subList(assessmentWords.size() / 2, assessmentWords.size());
data.put("words1", words1);
data.put("words2", words2);
return data;
}).toList();

View File

@@ -35,7 +35,7 @@ public class UserController {
}
@PostMapping("update-user-info")
@ApiOperationLog(description = "修改密码")
@ApiOperationLog(description = "修改用户信息")
public Response<String> updatePassword(@RequestBody UpdateUserInfoReqVO updateUserInfoReqVO) {
try {
String code = updateUserInfoReqVO.getCode();

View File

@@ -26,4 +26,6 @@ public interface VocabularyBankDOMapper {
Integer selectWordTotal();
List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
List<VocabularyBankDO> selectByGradeIdAndNotMatchIds(@Param("gradeId") Integer gradeId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
}

View File

@@ -47,7 +47,6 @@ public class ExamWordsServiceImpl implements ExamWordsService {
@Transactional(rollbackFor = RuntimeException.class)
public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
ExamWordsDO examWordsDO;
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
@@ -61,6 +60,16 @@ public class ExamWordsServiceImpl implements ExamWordsService {
examWordsDO = generateFinalExamWords(studentId);
}
List<Integer> wordIds = new ArrayList<>(examWordsDO.getWordIds());
if (wordIds.size() < wordCount) {
log.info("单词数量不足,补充单词");
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectByGradeIdAndNotMatchIds(studentDO.getGradeId(), wordIds, wordCount - wordIds.size());
List<Integer> list = new ArrayList<>(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList());
wordIds.addAll(list);
examWordsDO.setWordIds(wordIds);
}
return examWordsDO;
}

View File

@@ -50,7 +50,8 @@ public class LessonPlansServiceImpl implements LessonPlansService {
private PlanExamDOMapper planExamDOMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private ClassDOMapper classDOMapper;
@Override
public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) {
@@ -227,11 +228,16 @@ public class LessonPlansServiceImpl implements LessonPlansService {
examWordsDOMapper.insert(examWordsDO);
studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
ClassDO classDO = classDOMapper.selectClassDOById(studentDOMapper.selectStudentById(studentId).getClassId());
data.put("examId", examWordsDO.getId());
data.put("studentId", studentId);
data.put("studentStr", studentDO.getName());
data.put("studentStr", gradeDO.getTitle() + " " + classDO.getTitle() + " " + studentDO.getName());
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();
// Configure config = Configure.builder()
// .bind("checkList", policy)
@@ -250,6 +256,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
List<VocabularyBankDO> checkList,
int day,
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "" + day + "";
Map<String, Object> data = new HashMap<>();
data.put("title", title);
@@ -258,7 +265,6 @@ public class LessonPlansServiceImpl implements LessonPlansService {
data.put("reviewVocabList", reviewVocabList);
data.put("checkList", checkList);
data.put("checkListAns", checkList);
// 中译英
List<Word> list = syncVocabList.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList();
list.forEach(word -> word.setTitle(" "));
@@ -344,7 +350,12 @@ public class LessonPlansServiceImpl implements LessonPlansService {
data.put("studentId", studentId);
data.put("studentStr", studentDO.getName());
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("生成教案小测成功");
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();

View File

@@ -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());
wordIds.addAll(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getWrongWordIds).flatMap(List::stream).toList());
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordIds);
List<VocabularyBankDO> vocabularyBankDOS = new ArrayList<>();
if (!wordIds.isEmpty()) {
vocabularyBankDOS = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordIds);
}
Map<Integer, VocabularyBankDO> id2Word = vocabularyBankDOS.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
List<ExamWordsJudgeResultDetail> examWordsJudgeResultDetails = new ArrayList<>();

View File

@@ -48,14 +48,14 @@ public class PngUtil {
// 反转后,黑色块变成白色(255),背景变成黑色(0),方便 findContours 查找。
Mat binary = new Mat();
// 阈值设为 50 左右即可,因为块是纯黑的
Imgproc.threshold(gray, binary, 50, 255, Imgproc.THRESH_BINARY_INV);
Imgproc.threshold(gray, binary, 80, 255, Imgproc.THRESH_BINARY_INV);
// 4. 查找轮廓
List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat();
// RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
//Imgcodecs.imwrite("output_red___v1.png", binary);
System.out.println("检测到的轮廓总数: " + contours.size());
System.out.println("------------------------------------------------");
@@ -89,32 +89,18 @@ public class PngUtil {
System.out.println("------------------------------------------------");
list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build());
// 可选:在原图上画出框,用于调试验证
// Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
// Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
// Imgcodecs.imwrite("output_red.png", src);
Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
//Imgcodecs.imwrite("output_red.png", src);
}
}
System.out.println("找到 " + blockCount + " 个黑色块。");
// 获取每一列的宽度
list.sort(Comparator.comparingInt(CoordinatesXY::getHeight));
int height = list.get(list.size() - 1).getHeight() / ExamWordsConstant.PGN_COL;
// 删除两列答题卡区块
list.sort(Comparator.comparingInt(CoordinatesXY::getWidth));
list.remove(list.size() - 1);
list.remove(list.size() - 1);
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
// 计算起始坐标
List<CoordinatesXY> ans = getCoordinatesXIES(list, height);
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
src.release();
binary.release();
hierarchy.release();
binary.release();
return ans;
return list;
}
// 获取(未背熟)单词的 id
@@ -134,22 +120,18 @@ public class PngUtil {
// 建议:如果光照不均匀,考虑使用 THRESH_OTSU 自动阈值,或者自适应阈值
Imgproc.threshold(gray, binary, 150, 255, Imgproc.THRESH_BINARY_INV);
// 调试时打印
// Imgcodecs.imwrite("output_binary.png", binary);
//Imgcodecs.imwrite("output_binary.png", binary);
List<Integer> answer = new ArrayList<>();
int words_index = 0;
for (int i = 0; i < coordinatesXYList.size(); i++) {
CoordinatesXY coordinatesXY = coordinatesXYList.get(i);
for (CoordinatesXY coordinatesXY : coordinatesXYList) {
int width = coordinatesXY.getWidth();
int height = coordinatesXY.getHeight();
int currentX = coordinatesXY.getX();
int currentY = coordinatesXY.getY();
int count = i == 0 ? ExamWordsConstant.PGN_COL - 1 : ExamWordsConstant.PGN_COL;
int currentY = coordinatesXY.getY() + height;
// 内层循环:遍历这一列的每一行
for (int j = 0; j < count; j++) {
for (int j = 0; j < 50; j++) {
// 安全检查:防止单词列表比格子少导致越界
if (words_index >= wordIds.size()) {
log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
@@ -169,11 +151,14 @@ public class PngUtil {
Mat region = binary.submat(rect);
int countNonZero = Core.countNonZero(region);
if (countNonZero > 500) {
if (countNonZero > 370) {
Integer id = wordIds.get(words_index);
answer.add(id);
log.info("检测到标记未背熟ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
}
if (countNonZero == 0) {
break;
}
region.release();
words_index++;
@@ -217,7 +202,7 @@ public class PngUtil {
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
// 可选:保存预处理后的图片查看效果
// Imgcodecs.imwrite("debug_roi.jpg", binary);
//Imgcodecs.imwrite("debug_roi.jpg", binary);
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
BufferedImage processedImage = matToBufferedImage(binary);
@@ -276,21 +261,4 @@ public class PngUtil {
return image;
}
private static @NonNull List<CoordinatesXY> getCoordinatesXIES(List<CoordinatesXY> list, int height) {
List<CoordinatesXY> ans = new ArrayList<>();
CoordinatesXY left = new CoordinatesXY();
left.setX(list.get(1).getX());
left.setWidth(list.get(1).getWidth());
left.setHeight(height);
left.setY(list.get(0).getY() + left.getHeight());
ans.add(left);
CoordinatesXY right = new CoordinatesXY();
right.setX(list.get(2).getX());
right.setY(list.get(0).getY());
right.setWidth(list.get(1).getWidth());
right.setHeight(height);
ans.add(right);
return ans;
}
}

View File

@@ -36,6 +36,8 @@ public class WordExportUtil {
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
config = Configure.builder()
.bind("words", policy)
.bind("words1", policy)
.bind("words2", policy)
.bind("answer", policy)
.build();
@@ -52,12 +54,14 @@ public class WordExportUtil {
.bind("checkListAns", policyLessonPlanWeekday)
.bind("sentences", policyLessonPlanWeekday)
.bind("sentencesAns", policyLessonPlanWeekday)
.bind("words", policyLessonPlanWeekday)
.bind("words1", policy)
.bind("words2", policy)
.build();
LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy();
configLessonPlanWeekend = Configure.builder()
.bind("checkList", policyLessonPlan)
.bind("words1", policy)
.bind("words2", policy)
.build();
}

View File

@@ -2,7 +2,7 @@ spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # 指定数据库驱动类
# 数据库连接信息
url: jdbc:mysql://124.220.58.5:3306/enlish?allowMultiQueries=true
url: jdbc:mysql://124.220.58.5:3306/dev_english?allowMultiQueries=true
username: root # 数据库用户名
password: YLHP@admin123 # 数据库密码
data:
@@ -31,12 +31,12 @@ spring:
templates:
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v5.docx
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v9.docx
count: 100
data: C:\project\tess
plan:
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v6.docx
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v2.docx
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v7.docx
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v3.docx
plan_day: 7
tmp:
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\

View File

@@ -31,11 +31,11 @@ spring:
templates:
word: assessment_v5.docx
word: assessment_v7.docx
count: 100
data: eng.traineddata
plan:
weekday: tem_study_plan_v5.docx
weekday: tem_study_plan_v6.docx
weekend: study_plan_review_v2.docx
plan_day: 7
tmp:

View File

@@ -21,7 +21,6 @@
from student_exam_words
where student_id = #{studentId}
and exam_words_id = #{examWordsId}
and is_completed = 0
</select>
<update id="updateStudentExamWordsFinished">

View File

@@ -145,5 +145,21 @@
order by rand()
limit 100
</select>
<select id="selectByGradeIdAndNotMatchIds"
resultType="com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO">
select *
from vocabulary_bank
where unit_id in (
select unit_id
from grade_unit
where grade_id = #{gradeId}
)
and id not in
<foreach item="id" collection="ids" separator="," open="(" close=")">
#{id}
</foreach>
order by rand()
limit #{wordCount}
</select>
</mapper>

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>enlish-vue</title>
</head>
<body>

View File

@@ -3,12 +3,17 @@
<template>
<el-config-provider :locale="locale">
<router-view />
<el-drawer v-model="mobileSidebarOpen" class="md:hidden" title="菜单" size="260px">
<Sidebar />
</el-drawer>
</el-config-provider>
</template>
<script setup>
import zhCn from 'element-plus/es/locale/lang/zh-cn'
const locale = zhCn
import Sidebar from '@/layouts/components/Sidebar.vue'
import { mobileSidebarOpen } from '@/composables/ui.js'
</script>
<style>

View File

@@ -43,3 +43,34 @@ html, body, #app {
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;
}
}

View 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
}

View File

@@ -1,28 +1,28 @@
<template>
<el-dialog v-model="visible" title="新增班级" width="480px" :close-on-click-modal="false">
<el-dialog v-model="visible" title="新增班级" width="480px" :fullscreen="isMobile" :close-on-click-modal="false">
<div class="space-y-4" v-loading="loading">
<el-form label-width="80px">
<el-form :label-width="isMobile ? 0 : 80" :label-position="isMobile ? 'top' : 'right'">
<el-form-item label="班级名称">
<el-input v-model="name" placeholder="请输入班级名称,如:二班" clearable />
<el-input v-model="name" placeholder="请输入班级名称,如:二班" clearable class="w-full" />
</el-form-item>
<el-form-item label="年级">
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px">
<el-select v-model="gradeId" placeholder="请选择年级" class="w-full sm:w-[260px]">
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
</el-select>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { getGradeList } from '@/api/grade'
import { addClass } from '@/api/class'
@@ -41,6 +41,7 @@ const loading = ref(false)
const name = ref('')
const gradeId = ref(null)
const gradeOptions = ref([])
const isMobile = ref(false)
const canSubmit = computed(() => name.value.trim().length > 0 && !!gradeId.value)
@@ -71,6 +72,10 @@ async function handleSubmit() {
}
}
function updateIsMobile() {
isMobile.value = window.matchMedia('(max-width: 768px)').matches
}
watch(
() => props.modelValue,
(v) => {
@@ -78,9 +83,22 @@ watch(
name.value = ''
gradeId.value = props.defaultGradeId ? Number(props.defaultGradeId) : null
fetchGrades()
updateIsMobile()
}
}
)
onMounted(() => {
updateIsMobile()
window.addEventListener('resize', updateIsMobile)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateIsMobile)
})
</script>
<style scoped></style>
<style scoped>
.footer-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
</style>

View File

@@ -1,19 +1,19 @@
<template>
<el-dialog v-model="visible" title="新增年级" width="420px" :close-on-click-modal="false">
<el-dialog v-model="visible" title="新增年级" width="420px" :fullscreen="isMobile" :close-on-click-modal="false">
<div class="space-y-4" v-loading="loading">
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable />
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable class="w-full" />
</div>
<template #footer>
<div class="flex justify-end gap-2">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { addGrade } from '@/api/grade'
const props = defineProps({
@@ -29,6 +29,7 @@ const visible = computed({
const loading = ref(false)
const name = ref('')
const canSubmit = computed(() => name.value.trim().length > 0)
const isMobile = ref(false)
async function handleSubmit() {
if (!canSubmit.value) return
@@ -43,12 +44,29 @@ async function handleSubmit() {
}
}
function updateIsMobile() {
isMobile.value = window.matchMedia('(max-width: 768px)').matches
}
watch(
() => props.modelValue,
(v) => {
if (v) name.value = ''
updateIsMobile()
}
)
onMounted(() => {
updateIsMobile()
window.addEventListener('resize', updateIsMobile)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateIsMobile)
})
</script>
<style scoped></style>
<style scoped>
.footer-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
</style>

View File

@@ -1,17 +1,17 @@
<template>
<el-dialog v-model="visible" title="新增学生" width="560px" :close-on-click-modal="false">
<el-dialog v-model="visible" title="新增学生" width="560px" :fullscreen="isMobile" :close-on-click-modal="false" class="responsive-dialog">
<div class="space-y-4" v-loading="loading">
<el-form label-width="90px">
<el-form :label-width="isMobile ? 0 : 90" :label-position="isMobile ? 'top' : 'right'">
<el-form-item label="姓名">
<el-input v-model="name" placeholder="请输入学生姓名" clearable />
<el-input v-model="name" placeholder="请输入学生姓名" clearable class="w-full" />
</el-form-item>
<el-form-item label="年级">
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px" @change="handleGradeChange">
<el-select v-model="gradeId" placeholder="请选择年级" class="w-full sm:w-[260px]" @change="handleGradeChange">
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
</el-select>
</el-form-item>
<el-form-item label="班级">
<el-select v-model="classId" placeholder="请选择班级" style="width: 260px">
<el-select v-model="classId" placeholder="请选择班级" class="w-full sm:w-[260px]">
<el-option v-for="c in filteredClassOptions" :key="c.id" :label="c.title" :value="c.id" />
</el-select>
<div v-if="gradeId && filteredClassOptions.length === 0" class="mt-2 flex items-center gap-2">
@@ -20,15 +20,15 @@
</div>
</el-form-item>
<el-form-item label="入学时间">
<el-date-picker v-model="startDate" type="datetime" placeholder="选择日期时间"
<el-date-picker v-model="startDate" type="datetime" placeholder="选择日期时间" class="w-full"
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
</div>
</template>
</el-dialog>
@@ -36,7 +36,7 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { getGradeList } from '@/api/grade'
import { getClassList } from '@/api/class'
import { addStudent } from '@/api/student'
@@ -61,6 +61,7 @@ const classId = ref(null)
const startDate = ref('')
const gradeOptions = ref([])
const classOptions = ref([])
const isMobile = ref(false)
const filteredClassOptions = computed(() => {
if (!gradeId.value) return []
@@ -116,6 +117,10 @@ async function handleSubmit() {
}
}
function updateIsMobile() {
isMobile.value = window.matchMedia('(max-width: 768px)').matches
}
watch(
() => props.modelValue,
async (v) => {
@@ -126,9 +131,26 @@ watch(
startDate.value = ''
await fetchBaseOptions()
if (gradeId.value) handleGradeChange()
updateIsMobile()
}
}
)
onMounted(() => {
updateIsMobile()
window.addEventListener('resize', updateIsMobile)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateIsMobile)
})
</script>
<style scoped></style>
<style scoped>
.footer-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
.responsive-dialog :deep(.el-dialog__body) {
max-height: 70vh;
overflow-y: auto;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="visible" title="词条记录详情" width="820px" :close-on-click-modal="false">
<el-dialog v-model="visible" title="词条记录详情" width="820px" :fullscreen="isMobile" :close-on-click-modal="false" class="responsive-dialog">
<div class="space-y-4" v-loading="loading">
<el-card shadow="hover">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -79,7 +79,7 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { getExamWordsDetailResult } from '@/api/exam'
import { getStudentDetail } from '@/api/student'
import { getWordsListByIds } from '@/api/words'
@@ -101,6 +101,7 @@ const activeNames = ref(['correct', 'wrong'])
const correctTitles = ref([])
const wrongTitles = ref([])
const studentDetail = ref(null)
const isMobile = ref(false)
async function fetchDetail() {
if (!props.id && props.id !== 0) return
@@ -149,10 +150,17 @@ async function fetchStudent() {
studentDetail.value = res?.data?.data ?? null
}
function updateIsMobile() {
isMobile.value = window.matchMedia('(max-width: 768px)').matches
}
watch(
() => props.modelValue,
(v) => {
if (v) fetchDetail()
if (v) {
updateIsMobile()
fetchDetail()
}
}
)
watch(
@@ -161,6 +169,19 @@ watch(
if (visible.value && v !== undefined && v !== null) fetchDetail()
}
)
onMounted(() => {
updateIsMobile()
window.addEventListener('resize', updateIsMobile)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateIsMobile)
})
</script>
<style scoped></style>
<style scoped>
.responsive-dialog :deep(.el-dialog__body) {
max-height: 70vh;
overflow-y: auto;
}
</style>

View File

@@ -37,7 +37,7 @@
</div>
</div>
</template>
<button data-collapse-toggle="mobile-menu-2" type="button"
<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>
@@ -74,6 +74,7 @@ import { getUserInfo, logout } from '@/api/user'
import { removeToken } from '@/composables/auth'
import { useRouter } from 'vue-router'
import { showMessage } from '@/composables/util.js'
import { openMobileSidebar } from '@/composables/ui.js'
const showLogin = ref(false)
const userName = ref('')
const menuOpen = ref(false)

View File

@@ -7,7 +7,7 @@
<el-container class="pt-4">
<el-aside width="200px" class="">
<el-aside width="200px" class="hidden md:block sidebar-fixed">
<Sidebar />
</el-aside>
@@ -19,40 +19,77 @@
<el-button type="primary" @click="onSearch">查询</el-button>
<el-button @click="onReset">重置</el-button>
</div>
<el-table ref="tableRef" :data="rows" border class="w-full" v-loading="loading" row-key="id">
<el-table-column type="expand">
<template #default="{ row }">
<div class="p-3">
<div class="text-sm font-semibold mb-2">学案</div>
<el-table :data="row.plans || []" size="small" border>
<el-table-column prop="title" label="标题" min-width="280" />
<el-table-column label="状态" width="120">
<template #default="{ row: plan }">
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'"
effect="plain">
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row: plan }">
<el-button type="primary" size="small"
:loading="downloadingIds.includes(plan.id)"
@click="onDownload(plan)">下载</el-button>
<el-button class="ml-2" type="primary" size="small"
:disabled="plan.isFinished === 1"
:loading="finishingIds.includes(plan.id)"
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
</template>
</el-table-column>
</el-table>
<div class="hidden sm:block overflow-x-auto">
<el-table ref="tableRef" :data="rows" border class="min-w-[720px]" v-loading="loading" row-key="id">
<el-table-column type="expand">
<template #default="{ row }">
<div class="p-3">
<div class="text-sm font-semibold mb-2">学案</div>
<div class="overflow-x-auto">
<el-table :data="row.plans || []" size="small" border class="min-w-[600px]">
<el-table-column prop="title" label="标题" min-width="280" />
<el-table-column label="状态" width="120">
<template #default="{ row: plan }">
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'"
effect="plain">
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row: plan }">
<el-button type="primary" size="small"
:loading="downloadingIds.includes(plan.id)"
@click="onDownload(plan)">下载</el-button>
<el-button class="ml-2" type="primary" size="small"
:disabled="plan.isFinished === 1"
:loading="finishingIds.includes(plan.id)"
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
</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 v-if="mobileExpanded[row.id]" class="mt-3 space-y-2">
<div v-for="plan in (row.plans || [])" :key="plan.id"
class="rounded-lg border border-white/30 bg-white/50 p-3">
<div class="text-sm font-medium mb-2">{{ plan.title }}</div>
<div class="mb-2">
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
</el-tag>
</div>
<div class="flex gap-2">
<el-button type="primary" size="small"
:loading="downloadingIds.includes(plan.id)"
@click="onDownload(plan)">下载</el-button>
<el-button type="primary" size="small"
:disabled="plan.isFinished === 1"
:loading="finishingIds.includes(plan.id)"
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="className" label="班级" min-width="120" />
<el-table-column prop="gradeName" label="年级" min-width="120" />
</el-table>
</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"
@@ -84,6 +121,12 @@ const searchName = ref('')
const tableRef = ref(null)
const downloadingIds = ref([])
const finishingIds = ref([])
const mobileExpanded = ref({})
function toggleMobileExpand(id) {
const v = mobileExpanded.value[id]
mobileExpanded.value[id] = !v
}
async function fetchLessonPlans() {
loading.value = true

View File

@@ -1,20 +1,20 @@
<template>
<div
class="min-h-screen relative flex items-center justify-center bg-fixed bg-cover bg-center bg-[url('https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg')]">
class="min-h-screen relative flex items-center justify-center bg-scroll md:bg-fixed bg-cover bg-center bg-[url('https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg')] safe-area">
<div class="absolute inset-0 bg-gradient-to-br from-[rgba(30,20,50,0.4)] to-[rgba(10,10,20,0.6)]"></div>
<div
class="relative z-10 w-[400px] max-w-[92%] bg-white/10 backdrop-blur-2xl border border-white/20 rounded-2xl p-8 shadow-2xl text-white">
class="relative z-10 w-full sm:w-[400px] max-w-[420px] sm:max-w-[92%] bg-white/10 backdrop-blur-2xl border border-white/20 rounded-2xl p-6 sm:p-8 shadow-2xl text-white">
<div class="text-center mb-6">
<h2 class="text-2xl font-semibold tracking-wide mb-1">Welcome Back</h2>
<h2 class="text-xl sm:text-2xl font-semibold tracking-wide mb-1">Welcome Back</h2>
<p class="text-sm text-white/80">智慧英语 · 让学习更简单</p>
</div>
<div class="flex justify-center mb-6 border-b border-white/10">
<button class="px-5 py-2 text-white/70 hover:text-white transition"
<button class="px-4 sm:px-5 py-2 text-white/70 hover:text-white transition"
:class="mode === 'login' ? 'font-bold text-white border-b-2 border-white' : ''"
@click="switchMode('login')">登录</button>
<button class="px-5 py-2 text-white/70 hover:text-white transition"
<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>

View File

@@ -6,7 +6,7 @@
</el-header>
<el-container class="pt-4">
<el-aside width="200px">
<el-aside width="200px" class="hidden md:block sidebar-fixed">
<Sidebar></Sidebar>
</el-aside>
@@ -15,17 +15,28 @@
<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>
<el-table ref="classTableRef" :data="classes" border class="w-full" v-loading="loading" highlight-current-row
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
<el-table-column prop="title" label="班级名称" min-width="120" />
<el-table-column prop="gradeName" label="年级" min-width="120" />
<div class="hidden sm:block overflow-x-auto">
<el-table ref="classTableRef" :data="classes" border class="min-w-[520px]" v-loading="loading" highlight-current-row
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
<el-table-column prop="title" label="班级名称" min-width="160" />
<el-table-column prop="gradeName" label="年级" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="danger" size="small"
@click.stop="onDeleteClass(row)">删除</el-button>
<el-button type="danger" size="small" @click.stop="onDeleteClass(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="sm:hidden space-y-3">
<div v-for="row in classes" :key="row.id" class="panel-shell p-4">
<div class="text-base font-semibold mb-1">{{ row.title }}</div>
<div class="text-sm mb-2">年级{{ row.gradeName }}</div>
<div class="flex gap-2">
<el-button size="small" type="primary" @click="onClassRowClick(row)">选择</el-button>
<el-button size="small" type="danger" @click="onDeleteClass(row)">删除</el-button>
</div>
</div>
</div>
<div class="mt-4 flex justify-end">
<el-pagination background layout="prev, pager, next, sizes, total"
:total="totalCount" :page-size="pageSize" :current-page="pageNo"
@@ -56,40 +67,61 @@
生成学案
</el-button>
</div>
<el-table ref="studentTableRef" :data="students" border class="w-full"
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
<el-table-column type="selection" width="48" />
<el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="className" label="班级" min-width="120" />
<el-table-column prop="gradeName" label="级" min-width="120" />
<el-table-column prop="phone" label="学案" min-width="120">
<template #default="{ row }">
<template v-if="generatingPercents[row.id] !== undefined">
<div class="flex items-center gap-2">
<el-progress :percentage="generatingPercents[row.id]"
:stroke-width="8" />
</div>
<div class="hidden sm:block overflow-x-auto">
<el-table ref="studentTableRef" :data="students" border class="min-w-[760px]"
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
<el-table-column type="selection" width="48" />
<el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="className" label="级" min-width="120" />
<el-table-column prop="gradeName" label="年级" min-width="120" />
<el-table-column prop="phone" label="学案" min-width="120">
<template #default="{ row }">
<template v-if="generatingPercents[row.id] !== undefined">
<div class="flex items-center gap-2">
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
</div>
</template>
<template v-else>
<div class="flex items-center gap-2">
<el-button type="primary" size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
</div>
</template>
</template>
<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>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button type="info" size="small" @click.stop="onShowAnalysis(row)">学情分析</el-button>
<el-button type="primary" size="small" @click.stop="onViewStudent(row)">详情</el-button>
<el-button type="danger" size="small" @click.stop="onDeleteStudent(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="sm:hidden space-y-3">
<div v-for="row in students" :key="row.id" class="panel-shell p-4">
<div class="flex items-center justify-between mb-1">
<div class="text-base font-semibold">{{ row.name }}</div>
<el-checkbox
:model-value="isStudentSelected(row.id)"
@change="(val) => setStudentSelection(row.id, val)">
选中
</el-checkbox>
</div>
<div class="text-sm mb-1">班级{{ row.className }}</div>
<div class="text-sm mb-2">年级{{ row.gradeName }}</div>
<template v-if="generatingPercents[row.id] !== undefined">
<div class="mb-2">
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
</div>
</template>
</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 class="flex flex-wrap gap-2">
<el-button size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
<el-button size="small" type="info" @click="onShowAnalysis(row)">学情分析</el-button>
<el-button size="small" type="primary" @click="onViewStudent(row)">详情</el-button>
<el-button size="small" type="danger" @click="onDeleteStudent(row)">删除</el-button>
</div>
</div>
</div>
<div class="mt-4 flex justify-end">
<el-pagination background layout="prev, pager, next, sizes, total"
:total="studentTotalCount" :page-size="studentPageSize"
@@ -113,10 +145,21 @@
<div class="panel-shell p-6" v-loading="gradeLoading">
<div class="text-lg font-semibold mb-4">年级列表</div>
<el-table ref="gradeTableRef" :data="grades" border class="w-full" highlight-current-row
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
<el-table-column prop="title" label="年级名称" min-width="160" />
</el-table>
<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"
@@ -190,6 +233,18 @@ const planStudentId = ref(null)
const showAnalysisDialog = ref(false)
const analysisStudentId = ref(null)
function isStudentSelected(id) {
return selectedStudentIds.value.includes(id)
}
function setStudentSelection(id, selected) {
const exists = selectedStudentIds.value.includes(id)
if (selected && !exists) {
selectedStudentIds.value = [...selectedStudentIds.value, id]
} else if (!selected && exists) {
selectedStudentIds.value = selectedStudentIds.value.filter(x => x !== id)
}
}
const units = ref([])
const unitPageNo = ref(1)
const unitPageSize = ref(10)

View File

@@ -8,7 +8,7 @@
<el-container class="pt-4">
<el-main class="h-full">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
<div class="panel-shell p-6">
<div class="panel-shell p-4 sm:p-6">
<div class="text-lg font-semibold mb-4">学生详情</div>
<template v-if="detail">
<el-descriptions :column="1" border>
@@ -23,7 +23,7 @@
<el-empty description="请从班级页跳转" />
</template>
</div>
<div class="panel-shell p-6">
<div class="panel-shell p-4 sm:p-6">
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
<template v-if="wordStat">
<el-descriptions :column="1" border>
@@ -36,19 +36,19 @@
<el-empty description="暂无统计" />
</template>
</div>
<div class="panel-shell p-6">
<div class="panel-shell p-4 sm:p-6">
<div class="text-md font-semibold mb-3">学生考试记录</div>
<ExamHistoryChart :data="history" />
</div>
<div class="panel-shell p-6">
<div class="panel-shell p-4 sm:p-6">
<div class="text-md font-semibold mb-3">学生学案记录</div>
<PlanHistoryChart :student-id="route.params.id" />
</div>
<div class="panel-shell p-6 lg:col-span-2">
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
<WordMasteryHeatmap :student-id="route.params.id" :columns="heatmapColumns" />
</div>
<div class="panel-shell p-6 lg:col-span-2">
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
<div class="text-md font-semibold mb-3">学情分析</div>
<StudyAnalysis :student-id="route.params.id" />
</div>
@@ -77,6 +77,12 @@ const detail = ref(null)
const route = useRoute()
const history = ref([])
const wordStat = ref(null)
const isMobile = ref(false)
const heatmapColumns = computed(() => isMobile.value ? 20 : 50)
function updateIsMobile() {
isMobile.value = window.matchMedia('(max-width: 768px)').matches
}
async function fetchDetail() {
const id = route.params.id
@@ -117,5 +123,7 @@ onMounted(() => {
fetchDetail()
fetchExamHistory()
fetchWordStat()
updateIsMobile()
window.addEventListener('resize', updateIsMobile)
})
</script>

View File

@@ -6,7 +6,7 @@
</el-header>
<el-container class="pt-4">
<el-aside width="200px" class="">
<el-aside width="200px" class="hidden md:block sidebar-fixed">
<Sidebar />
</el-aside>
<el-main class="">
@@ -14,11 +14,12 @@
<div class="panel-shell p-6">
<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>
</div>
<div class="panel-shell p-6">
<div class="text-lg font-semibold mb-4">结果集</div>
<div class="hidden sm:block">
<el-form :inline="true" class="mb-4">
<el-form-item label="班级">
<el-select v-model="classId" placeholder="选择班级" clearable filterable
@@ -43,26 +44,53 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border class="w-full" v-loading="loading"
@row-click="handleRowClick">
<el-table-column prop="studentName" label="学生姓名" min-width="70" />
<el-table-column prop="examWordsTitle" label="试题名称" min-width="100" />
<el-table-column prop="correctWordCount" label="正确词数" width="110" />
<el-table-column prop="wrongWordCount" label="错误词数" width="110" />
<el-table-column label="完成状态" width="110">
<template #default="{ row }">
</div>
<div class="hidden sm:block overflow-x-auto">
<el-table :data="list" border class="min-w-[800px]" v-loading="loading"
@row-click="handleRowClick">
<el-table-column prop="studentName" label="学生姓名" min-width="120" />
<el-table-column prop="examWordsTitle" label="试题名称" min-width="160" />
<el-table-column prop="correctWordCount" label="正确词数" width="120" />
<el-table-column prop="wrongWordCount" label="错误词数" width="120" />
<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>
</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="160" />
</el-table>
<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"