Compare commits
4 Commits
e468be74b7
...
0802f6fe70
| Author | SHA1 | Date | |
|---|---|---|---|
| 0802f6fe70 | |||
| 679241588f | |||
| fe7128dd4e | |||
| 1184ea7895 |
@@ -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 分钟");
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -55,7 +56,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
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);
|
||||||
@@ -249,7 +250,7 @@ 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);
|
||||||
@@ -259,13 +260,16 @@ public class LessonPlansServiceImpl implements LessonPlansService {
|
|||||||
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);
|
||||||
|
|
||||||
@@ -274,8 +278,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 中文开始");
|
||||||
|
|||||||
Binary file not shown.
@@ -14,6 +14,6 @@ const locale = zhCn
|
|||||||
<style>
|
<style>
|
||||||
/* 自定义顶部加载 Loading 颜色 */
|
/* 自定义顶部加载 Loading 颜色 */
|
||||||
#nprogress .bar {
|
#nprogress .bar {
|
||||||
background: #409eff !important;
|
background: #2563eb !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,45 @@
|
|||||||
@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%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,41 +1,36 @@
|
|||||||
<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">
|
||||||
|
<nav class="fluent-nav px-4 lg:px-6 py-2.5">
|
||||||
|
<div class="flex flex-wrap justify-between items-center">
|
||||||
<a href="#" class="flex items-center">
|
<a href="#" class="flex items-center">
|
||||||
<img src="https://flowbite.com/docs/images/logo.svg" class="mr-3 h-6 sm:h-9" alt="Flowbite Logo" />
|
<img src="https://flowbite.com/docs/images/logo.svg" class="mr-3 h-6 sm:h-9" alt="Flowbite Logo" width="24" height="24" />
|
||||||
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Flowbite</span>
|
<span class="self-center text-xl font-semibold whitespace-nowrap">Flowbite</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center lg:order-2">
|
<div class="flex items-center lg:order-2">
|
||||||
<template v-if="userName">
|
<template v-if="userName">
|
||||||
<div class="relative" ref="menuRef">
|
<div class="relative" ref="menuRef">
|
||||||
<button
|
<button @click="menuOpen = !menuOpen"
|
||||||
@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">
|
||||||
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">
|
|
||||||
<span class="mr-2">{{ userName }}</span>
|
<span class="mr-2">{{ userName }}</span>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div v-if="menuOpen" class="fluent-card absolute right-0 mt-2 z-50">
|
||||||
v-if="menuOpen"
|
<router-link to="/admid" @click="menuOpen = false" class="block px-4 py-2 fluent-link">
|
||||||
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>
|
</router-link>
|
||||||
<button
|
<button @click="handleLogout" class="w-full text-left block px-4 py-2 fluent-link">
|
||||||
@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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<button data-collapse-toggle="mobile-menu-2" type="button"
|
<button data-collapse-toggle="mobile-menu-2" type="button"
|
||||||
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
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">
|
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||||
<span class="sr-only">Open main menu</span>
|
<span class="sr-only">Open main menu</span>
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -52,34 +47,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1" id="mobile-menu-2">
|
<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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<LoginDialog v-model="showLogin" @success="refreshUser" />
|
<LoginDialog v-model="showLogin" @success="refreshUser" />
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
@@ -131,3 +104,71 @@ 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>
|
||||||
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>
|
||||||
@@ -5,8 +5,14 @@
|
|||||||
<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">
|
|
||||||
|
<el-aside width="200px" class="">
|
||||||
|
<Sidebar />
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
|
<el-main class="">
|
||||||
|
<div class="panel-shell p-6">
|
||||||
<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="searchName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
<el-input v-model="searchName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
||||||
@@ -22,27 +28,21 @@
|
|||||||
<el-table-column prop="title" label="标题" min-width="280" />
|
<el-table-column prop="title" label="标题" min-width="280" />
|
||||||
<el-table-column label="状态" width="120">
|
<el-table-column label="状态" width="120">
|
||||||
<template #default="{ row: plan }">
|
<template #default="{ row: plan }">
|
||||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'"
|
||||||
|
effect="plain">
|
||||||
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{ row: plan }">
|
<template #default="{ row: plan }">
|
||||||
<el-button
|
<el-button type="primary" size="small"
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
:loading="downloadingIds.includes(plan.id)"
|
:loading="downloadingIds.includes(plan.id)"
|
||||||
@click="onDownload(plan)"
|
@click="onDownload(plan)">下载</el-button>
|
||||||
>下载</el-button>
|
<el-button class="ml-2" type="primary" size="small"
|
||||||
<el-button
|
|
||||||
class="ml-2"
|
|
||||||
type="success"
|
|
||||||
size="small"
|
|
||||||
:disabled="plan.isFinished === 1"
|
:disabled="plan.isFinished === 1"
|
||||||
:loading="finishingIds.includes(plan.id)"
|
:loading="finishingIds.includes(plan.id)"
|
||||||
@click="onFinish(row.id, plan.id, plan)"
|
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
|
||||||
>完成</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -60,6 +60,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +73,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)
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
|
<el-container>
|
||||||
|
<el-aside width="200px" class="pt-4">
|
||||||
|
<Sidebar />
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-main class="p-4">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
<div class="text-lg font-semibold mb-4">TTS</div>
|
<div class="text-lg font-semibold mb-4">TTS</div>
|
||||||
@@ -49,6 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -59,7 +66,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,13 +5,15 @@
|
|||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container>
|
||||||
<el-card>
|
<el-main class="p-2">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div class="panel-shell p-6">
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<el-input v-model="query.name" placeholder="姓名" clearable style="max-width:220px" />
|
<el-input v-model="query.name" placeholder="姓名" clearable style="max-width:220px" />
|
||||||
<el-button type="primary" class="ml-2" @click="fetchList">查询</el-button>
|
<el-button type="primary" class="ml-2" @click="fetchList">查询</el-button>
|
||||||
<el-button class="ml-2" @click="resetSearch">重置</el-button>
|
<el-button class="ml-2" @click="resetSearch">重置</el-button>
|
||||||
<el-button type="success" class="ml-2" @click="openCreate">新增用户</el-button>
|
<el-button type="primary" class="ml-2" @click="openCreate">新增用户</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="list" v-loading="loading" border stripe>
|
<el-table :data="list" v-loading="loading" border stripe>
|
||||||
<el-table-column prop="name" label="姓名" />
|
<el-table-column prop="name" label="姓名" />
|
||||||
@@ -19,24 +21,16 @@
|
|||||||
<el-table-column prop="roleName" label="角色" />
|
<el-table-column prop="roleName" label="角色" />
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-4 flex justify-end">
|
||||||
<el-pagination
|
<el-pagination background :current-page="page" :page-size="pageSize" :total="totalCount"
|
||||||
background
|
layout="prev, pager, next, sizes, total" @current-change="onPageChange"
|
||||||
:current-page="page"
|
@size-change="onSizeChange" />
|
||||||
:page-size="pageSize"
|
</div>
|
||||||
:total="totalCount"
|
|
||||||
layout="prev, pager, next, sizes, total"
|
|
||||||
@current-change="onPageChange"
|
|
||||||
@size-change="onSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="mt-4">
|
<div class="panel-shell p-6">
|
||||||
<template #header>
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center justify-between">
|
<span class="text-lg font-semibold">生成邀请码</span>
|
||||||
<span>生成邀请码</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<el-form :model="inviteForm" :rules="inviteRules" ref="inviteFormRef" label-width="120px">
|
<el-form :model="inviteForm" :rules="inviteRules" ref="inviteFormRef" label-width="120px">
|
||||||
<el-form-item label="使用次数限制" prop="limit">
|
<el-form-item label="使用次数限制" prop="limit">
|
||||||
<el-input-number v-model="inviteForm.limit" :min="1" :max="9999" />
|
<el-input-number v-model="inviteForm.limit" :min="1" :max="9999" />
|
||||||
@@ -45,14 +39,16 @@
|
|||||||
<el-input-number v-model="inviteForm.expire" :min="1" :max="3650" />
|
<el-input-number v-model="inviteForm.expire" :min="1" :max="3650" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" :loading="inviteLoading" @click="submitInvite">生成邀请码</el-button>
|
<el-button type="primary" :loading="inviteLoading"
|
||||||
|
@click="submitInvite">生成邀请码</el-button>
|
||||||
<el-button class="ml-2" @click="resetInvite">重置</el-button>
|
<el-button class="ml-2" @click="resetInvite">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div v-if="inviteCode" class="mt-2">
|
<div v-if="inviteCode" class="mt-2">
|
||||||
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
||||||
<el-form :model="createForm" :rules="rules" ref="createFormRef" label-width="80px">
|
<el-form :model="createForm" :rules="rules" ref="createFormRef" label-width="80px">
|
||||||
@@ -67,11 +63,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="createVisible=false">取消</el-button>
|
<el-button @click="createVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">提交</el-button>
|
<el-button type="primary" :loading="createLoading" @click="submitCreate">提交</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</el-main>
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,6 +79,7 @@ import Header from '@/layouts/components/Header.vue'
|
|||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
||||||
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([])
|
||||||
|
|||||||
@@ -5,11 +5,15 @@
|
|||||||
<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">
|
||||||
|
<Sidebar></Sidebar>
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
|
<el-main class="">
|
||||||
|
<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="lg:col-span-1 flex flex-col gap-6">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="panel-shell p-6">
|
||||||
<div class="text-lg font-semibold mb-4">班级列表</div>
|
<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
|
<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">
|
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
|
||||||
@@ -17,14 +21,15 @@
|
|||||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="danger" size="small" @click.stop="onDeleteClass(row)">删除</el-button>
|
<el-button type="danger" size="small"
|
||||||
|
@click.stop="onDeleteClass(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-4 flex justify-end">
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||||
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
:total="totalCount" :page-size="pageSize" :current-page="pageNo"
|
||||||
@size-change="handleSizeChange" />
|
@current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex justify-end">
|
<div class="mt-3 flex justify-end">
|
||||||
<el-button type="primary" @click="showAddClassDialog = true">新增班级</el-button>
|
<el-button type="primary" @click="showAddClassDialog = true">新增班级</el-button>
|
||||||
@@ -34,7 +39,7 @@
|
|||||||
</div>
|
</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" />
|
||||||
@@ -63,65 +68,67 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="generatingPercents[row.id] !== undefined">
|
<template v-if="generatingPercents[row.id] !== undefined">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-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>
|
<template v-else>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<el-button type="primary" size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
<el-button type="primary" size="small"
|
||||||
|
@click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" size="small" @click.stop="onViewStudent(row)">详情</el-button>
|
<!-- 学情分析 -->
|
||||||
<el-button type="danger" size="small" @click.stop="onDeleteStudent(row)">删除</el-button>
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-4 flex justify-end">
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total"
|
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||||
:total="studentTotalCount" :page-size="studentPageSize" :current-page="studentPageNo"
|
:total="studentTotalCount" :page-size="studentPageSize"
|
||||||
@current-change="handleStudentPageChange" @size-change="handleStudentSizeChange" />
|
:current-page="studentPageNo" @current-change="handleStudentPageChange"
|
||||||
|
@size-change="handleStudentSizeChange" />
|
||||||
</div>
|
</div>
|
||||||
<ExamGenerateDialog v-model="showGenerateDialog" :student-ids="selectedStudentIds"
|
<ExamGenerateDialog v-model="showGenerateDialog" :student-ids="selectedStudentIds"
|
||||||
:default-grade-id="selectedGradeId" />
|
: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>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6" v-loading="gradeLoading">
|
<div class="panel-shell p-6" v-loading="gradeLoading">
|
||||||
<div class="text-lg font-semibold mb-4">年级列表</div>
|
<div class="text-lg font-semibold mb-4">年级列表</div>
|
||||||
<el-table ref="gradeTableRef" :data="grades" border class="w-full" highlight-current-row
|
<el-table ref="gradeTableRef" :data="grades" border class="w-full" highlight-current-row
|
||||||
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
||||||
<el-table-column prop="title" label="年级名称" min-width="160" />
|
<el-table-column prop="title" label="年级名称" min-width="160" />
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-4 flex justify-end">
|
||||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="gradeTotalCount"
|
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||||
:page-size="gradePageSize" :current-page="gradePageNo"
|
:total="gradeTotalCount" :page-size="gradePageSize" :current-page="gradePageNo"
|
||||||
@current-change="handleGradePageChange" @size-change="handleGradeSizeChange" />
|
@current-change="handleGradePageChange" @size-change="handleGradeSizeChange" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,6 +136,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 +147,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 +189,8 @@ 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)
|
||||||
|
|
||||||
const units = ref([])
|
const units = ref([])
|
||||||
const unitPageNo = ref(1)
|
const unitPageNo = ref(1)
|
||||||
@@ -275,6 +286,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
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
|
<el-container>
|
||||||
|
<el-aside width="200px" class="pt-4">
|
||||||
|
<Sidebar />
|
||||||
|
</el-aside>
|
||||||
<el-main class="p-4">
|
<el-main class="p-4">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
@@ -15,7 +19,8 @@
|
|||||||
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
|
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade
|
||||||
|
}}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -27,9 +32,12 @@
|
|||||||
<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 label="未掌握">{{ wordStat.unmasteredWordCount }}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount }}</el-descriptions-item>
|
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount
|
||||||
|
}}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount
|
||||||
|
}}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -49,29 +57,10 @@
|
|||||||
<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="50" />
|
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<StudyAnalysis :student-id="route.params.id" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
|
</el-container>
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -80,31 +69,20 @@
|
|||||||
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({
|
|
||||||
html: false,
|
|
||||||
linkify: true,
|
|
||||||
breaks: true
|
|
||||||
})
|
|
||||||
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
|
||||||
@@ -129,37 +107,6 @@ async function fetchExamHistory() {
|
|||||||
}) : []
|
}) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchStudyAnalyze() {
|
|
||||||
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() {
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|||||||
@@ -5,39 +5,34 @@
|
|||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
|
<el-aside width="200px" class="">
|
||||||
|
<Sidebar />
|
||||||
|
</el-aside>
|
||||||
|
<el-main class="">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="panel-shell p-6">
|
||||||
<div class="text-lg font-semibold mb-4">上传图片</div>
|
<div class="text-lg font-semibold mb-4">上传图片</div>
|
||||||
<el-upload
|
<el-upload :show-file-list="false" :http-request="doUpload" accept="image/*">
|
||||||
:show-file-list="false"
|
|
||||||
:http-request="doUpload"
|
|
||||||
accept="image/*"
|
|
||||||
>
|
|
||||||
<el-button type="primary">选择图片并上传</el-button>
|
<el-button type="primary">选择图片并上传</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-form :inline="true" class="mb-4">
|
<el-form :inline="true" class="mb-4">
|
||||||
<el-form-item label="班级">
|
<el-form-item label="班级">
|
||||||
<el-select v-model="classId" placeholder="选择班级" clearable filterable @change="onClassChange" style="min-width: 220px">
|
<el-select v-model="classId" placeholder="选择班级" clearable filterable
|
||||||
<el-option
|
@change="onClassChange" style="min-width: 220px">
|
||||||
v-for="item in classOptions"
|
<el-option v-for="item in classOptions" :key="item.id"
|
||||||
:key="item.id"
|
|
||||||
:label="`${item.title}${item.gradeName ? '(' + item.gradeName + ')' : ''}`"
|
:label="`${item.title}${item.gradeName ? '(' + item.gradeName + ')' : ''}`"
|
||||||
:value="item.id"
|
:value="item.id" />
|
||||||
/>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="年级">
|
<el-form-item label="年级">
|
||||||
<el-select v-model="gradeId" placeholder="选择年级" clearable filterable style="min-width: 220px">
|
<el-select v-model="gradeId" placeholder="选择年级" clearable filterable
|
||||||
<el-option
|
style="min-width: 220px">
|
||||||
v-for="g in gradeOptions"
|
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title"
|
||||||
:key="g.id"
|
:value="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="学生姓名">
|
||||||
@@ -48,13 +43,8 @@
|
|||||||
<el-button @click="handleReset">重置</el-button>
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-table
|
<el-table :data="list" border class="w-full" v-loading="loading"
|
||||||
:data="list"
|
@row-click="handleRowClick">
|
||||||
border
|
|
||||||
class="w-full"
|
|
||||||
v-loading="loading"
|
|
||||||
@row-click="handleRowClick"
|
|
||||||
>
|
|
||||||
<el-table-column prop="studentName" label="学生姓名" min-width="70" />
|
<el-table-column prop="studentName" label="学生姓名" min-width="70" />
|
||||||
<el-table-column prop="examWordsTitle" label="试题名称" min-width="100" />
|
<el-table-column prop="examWordsTitle" label="试题名称" min-width="100" />
|
||||||
<el-table-column prop="correctWordCount" label="正确词数" width="110" />
|
<el-table-column prop="correctWordCount" label="正确词数" width="110" />
|
||||||
@@ -74,21 +64,15 @@
|
|||||||
<el-table-column prop="msg" label="判卷结算" min-width="160" />
|
<el-table-column prop="msg" label="判卷结算" min-width="160" />
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-4 flex justify-end">
|
||||||
<el-pagination
|
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
||||||
background
|
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
||||||
layout="prev, pager, next, sizes, total"
|
@size-change="handleSizeChange" />
|
||||||
: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>
|
||||||
@@ -101,6 +85,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { uploadExamWordsPng, getExamWordsResult } from '@/api/exam'
|
import { uploadExamWordsPng, getExamWordsResult } from '@/api/exam'
|
||||||
import { getClassList } from '@/api/class'
|
import { getClassList } from '@/api/class'
|
||||||
import { getGradeList } from '@/api/grade'
|
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)
|
||||||
@@ -196,5 +181,4 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user