feat:更新资料库模块

This commit is contained in:
2026-08-02 21:01:42 +08:00
parent fefd329ea8
commit 7bb08a7ac4
9 changed files with 136 additions and 283 deletions

View File

@@ -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'
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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',

View File

@@ -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[],

View File

@@ -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>

View File

@@ -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>