feat:更新资料库模块
This commit is contained in:
@@ -6,6 +6,7 @@ export const uploadLibraryFileApi = (formData: FormData): Promise<string> =>
|
||||
url: '/study-api/library/upload',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
timeout: 60000,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<div class="card-actions">
|
||||
<el-button type="primary" :icon="Edit" @click="editClick(file)">编辑</el-button>
|
||||
<el-button type="primary" :icon="Aim" @click="trainClick(file)">训练</el-button>
|
||||
<el-button type="danger" :icon="Delete">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,15 +26,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Delete, Edit, Picture, Document } from '@element-plus/icons-vue'
|
||||
import { Delete, Edit, Picture, Document, Aim } from '@element-plus/icons-vue'
|
||||
import { useLibraryStore } from '@/stores/library.ts'
|
||||
import { usePracticeStore } from '@/stores/practice.ts'
|
||||
import type { ILibraryFile } from '@/types/library.ts'
|
||||
import LibraryFilter from '@/components/Library/LibraryFilter.vue'
|
||||
import { computed } from 'vue'
|
||||
import { fromNow } from '@/utils/dayUtil.ts'
|
||||
import { filesize } from 'filesize'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
|
||||
const libraryStore = useLibraryStore()
|
||||
const practiceStore = usePracticeStore()
|
||||
|
||||
const filterLibraryFileList = computed(() => {
|
||||
const subject = libraryStore.selectedSubject
|
||||
@@ -52,4 +56,59 @@ const editClick = async (file: ILibraryFile) => {
|
||||
libraryStore.activeStep = 1
|
||||
libraryStore.fileDialogVisible = true
|
||||
}
|
||||
|
||||
const trainClick = async (file: ILibraryFile) => {
|
||||
await libraryStore.queryFileById(file.id)
|
||||
await generateQuestion()
|
||||
practiceStore.hasStart = true
|
||||
practiceStore.hasSubmit = false
|
||||
practiceStore.checkList = []
|
||||
}
|
||||
|
||||
const generateQuestion = async () => {
|
||||
practiceStore.questionContent = ''
|
||||
practiceStore.hasGenerateQuestion = false
|
||||
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '题库生成中 请等待',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch('/study-api/library/question', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: libraryStore.currentFile.content
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('未获取到响应流')
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
practiceStore.questionContent += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
ElMessage.success('生成完成')
|
||||
practiceStore.hasGenerateQuestion = true
|
||||
|
||||
practiceStore.questionList = JSON.parse(practiceStore.questionContent)
|
||||
libraryStore.fileTrainDialogVisible = true
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('生成失败 请联系管理员')
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="practice-question">
|
||||
<div v-if="practiceStore.hasStart" class="question-list">
|
||||
<el-dialog v-model="libraryStore.fileTrainDialogVisible"
|
||||
:title="libraryStore.currentFile.name" width="80%" center destroy-on-close
|
||||
class="train-dialog common-dialog">
|
||||
<div class="question-list">
|
||||
<div v-for="(q, idx) in practiceStore.questionList" :key="q.id" class="question-card">
|
||||
<div class="q-header">
|
||||
<el-tag :type="q.type === 'single' ? 'primary' : 'warning'" effect="dark">
|
||||
@@ -38,7 +40,7 @@
|
||||
<div v-if="!practiceStore.hasSubmit" class="question-action">
|
||||
<el-button type="success" @click="submitAnswerClick">提交答案</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -109,3 +111,68 @@ const addMistakeList = async () => {
|
||||
await mistakeStore.addMistakeList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.train-dialog {
|
||||
.el-dialog__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.question-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.question-card {
|
||||
padding: 10px 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
border-bottom: 1px solid lightgray;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.q-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.q-num {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.q-stem {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.q-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.q-check {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.question-action {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<div class="practice-record">
|
||||
<div v-if="practiceStore.recordList.length > 0" class="record-list">
|
||||
<div v-for="r in practiceStore.recordList" :key="r.id" class="record-card">
|
||||
<div class="record-main">
|
||||
<span class="record-status" :style="{color: getCorrectRate(r) < 50 ? 'red' : 'green'}">
|
||||
正确率 {{ r.totalCount ? getCorrectRate(r) : 0 }}%
|
||||
</span>
|
||||
<span class="record-time">{{ fromNow(r.createTime) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="record-stats">
|
||||
<span>共 {{ r.totalCount }}</span>
|
||||
<span class="text-success">对 {{ r.correctCount }}</span>
|
||||
<span class="text-error">错 {{ r.wrongCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="暂无数据"></el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePracticeStore } from '@/stores/practice.ts'
|
||||
import { fromNow } from '@/utils/dayUtil.ts'
|
||||
import type { IPracticeRecord } from '@/types/practice.ts'
|
||||
|
||||
const practiceStore = usePracticeStore()
|
||||
|
||||
const getCorrectRate = (record: IPracticeRecord) => {
|
||||
return Math.round((record.correctCount / record.totalCount) * 100)
|
||||
}
|
||||
</script>
|
||||
@@ -20,10 +20,6 @@
|
||||
<el-icon><Reading /></el-icon>
|
||||
<template #title>知识库</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/practice">
|
||||
<el-icon><Aim /></el-icon>
|
||||
<template #title>训练场</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/exam">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
<template #title>考试中心</template>
|
||||
@@ -91,7 +87,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { Reading, Aim, EditPen, Notebook, DataLine, Fold, Expand, Bell } from '@element-plus/icons-vue'
|
||||
import { Reading, EditPen, Notebook, DataLine, Fold, Expand, Bell } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const isCollapse = ref(false)
|
||||
|
||||
@@ -13,12 +13,6 @@ const routes = [
|
||||
component: () => import('@/views/Library.vue'),
|
||||
meta: { title: '知识库' }
|
||||
},
|
||||
{
|
||||
path: 'practice',
|
||||
name: 'Practice',
|
||||
component: () => import('@/views/Practice.vue'),
|
||||
meta: { title: '训练场' }
|
||||
},
|
||||
{
|
||||
path: 'exam',
|
||||
name: 'Exam',
|
||||
|
||||
@@ -30,6 +30,7 @@ export const useLibraryStore = defineStore('library', {
|
||||
fileDetailDialogVisible: false,
|
||||
fileDialogVisible: false,
|
||||
fileContentDialogVisible: false,
|
||||
fileTrainDialogVisible: false,
|
||||
fileApiType: 'ADD' as IHandleApi,
|
||||
subjectOptions: [] as IOptions[],
|
||||
moduleOptions: [] as IOptions[],
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
<library-file-dialog />
|
||||
|
||||
<library-file-content-dialog />
|
||||
|
||||
<library-file-train-dialog />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LibraryFileList from '@/components/Library/LibraryFileList.vue'
|
||||
import LibraryFileDialog from '@/components/Library/LibraryFileDialog.vue'
|
||||
import LibraryFileTrainDialog from '@/components/Library/LibraryFileTrainDialog.vue'
|
||||
import LibraryFileContentDialog from '@/components/Library/LibraryFileContentDialog.vue'
|
||||
import { useLibraryStore } from '@/stores/library.ts'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
@@ -34,8 +37,6 @@ const addFileClick = () => {
|
||||
libraryStore.currentFile = libraryStore.getEmptyFile()
|
||||
libraryStore.fileApiType = 'ADD'
|
||||
libraryStore.activeStep = 1
|
||||
libraryStore.hasGenerateQuestion = false
|
||||
libraryStore.questionContent = ''
|
||||
libraryStore.fileDialogVisible = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
<template>
|
||||
<div class="practice-view common-view">
|
||||
<div class="view-header">
|
||||
<div class="view-content">
|
||||
<h2 class="view-title">训练场</h2>
|
||||
<p class="view-subtitle">专项练习,巩固知识点</p>
|
||||
</div>
|
||||
|
||||
<div class="view-action">
|
||||
<el-button type="primary" :icon="Aim" @click="startPracticeClick">
|
||||
{{ practiceStore.hasStart ? '重新练习' : '开始练习' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<library-filter v-if="libraryStore.fileList.length" />
|
||||
|
||||
<practice-question v-if="practiceStore.hasStart"/>
|
||||
|
||||
<practice-record v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LibraryFilter from '@/components/Library/LibraryFilter.vue'
|
||||
import PracticeQuestion from '@/components/Practice/PracticeQuestion.vue'
|
||||
import PracticeRecord from '@/components/Practice/PracticeRecord.vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useLibraryStore } from '@/stores/library'
|
||||
import { usePracticeStore } from '@/stores/practice.ts'
|
||||
import { Aim } from '@element-plus/icons-vue'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
|
||||
const libraryStore = useLibraryStore()
|
||||
const practiceStore = usePracticeStore()
|
||||
|
||||
onMounted(async () => {
|
||||
practiceStore.hasStart = false
|
||||
await libraryStore.queryFileList()
|
||||
})
|
||||
|
||||
watch(() => libraryStore.selectedModule, async () => {
|
||||
practiceStore.hasStart = false
|
||||
const subject = libraryStore.selectedSubject
|
||||
const module = libraryStore.selectedModule
|
||||
const fileList = libraryStore.fileList.filter((item) => {
|
||||
return item.subject === subject && item.module === module
|
||||
})
|
||||
|
||||
if (fileList.length > 0) {
|
||||
const { id } = fileList[0]
|
||||
practiceStore.libraryFileId = id
|
||||
await practiceStore.queryRecord()
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const startPracticeClick = async () => {
|
||||
await libraryStore.queryFileById(practiceStore.libraryFileId)
|
||||
await generateQuestion()
|
||||
practiceStore.hasStart = true
|
||||
practiceStore.hasSubmit = false
|
||||
practiceStore.checkList = []
|
||||
}
|
||||
|
||||
const generateQuestion = async () => {
|
||||
practiceStore.questionContent = ''
|
||||
practiceStore.hasGenerateQuestion = false
|
||||
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '题库生成中 请等待',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch('/study-api/library/question', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: libraryStore.currentFile.content
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('未获取到响应流')
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
practiceStore.questionContent += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
ElMessage.success('生成完成')
|
||||
practiceStore.hasGenerateQuestion = true
|
||||
|
||||
practiceStore.questionList = JSON.parse(practiceStore.questionContent)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('生成失败 请联系管理员')
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.practice-view {
|
||||
.practice-question {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.question-action {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.question-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.question-card {
|
||||
padding: 16px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
border-bottom: 1px solid lightgray;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.q-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.q-num {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.q-stem {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.q-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.q-check {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.practice-record {
|
||||
.record-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.record-card {
|
||||
padding: 16px;
|
||||
|
||||
border-bottom: 1px solid lightgray;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.record-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.record-time {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.record-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user