feat:更新考试模块

This commit is contained in:
2026-08-02 22:57:58 +08:00
parent 7bb08a7ac4
commit 94afc1519c
10 changed files with 374 additions and 185 deletions

View File

@@ -164,7 +164,7 @@ const generateClick = async () => {
}) })
try { try {
const response = await fetch('/study-api/library/knowledge', { const response = await fetch('/study-api/agent/knowledge', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'

View File

@@ -29,6 +29,7 @@
import { Delete, Edit, Picture, Document, Aim } from '@element-plus/icons-vue' import { Delete, Edit, Picture, Document, Aim } from '@element-plus/icons-vue'
import { useLibraryStore } from '@/stores/library.ts' import { useLibraryStore } from '@/stores/library.ts'
import { usePracticeStore } from '@/stores/practice.ts' import { usePracticeStore } from '@/stores/practice.ts'
import { useQuestionStore } from '@/stores/question.ts'
import type { ILibraryFile } from '@/types/library.ts' import type { ILibraryFile } from '@/types/library.ts'
import LibraryFilter from '@/components/Library/LibraryFilter.vue' import LibraryFilter from '@/components/Library/LibraryFilter.vue'
import { computed } from 'vue' import { computed } from 'vue'
@@ -38,6 +39,7 @@ import { ElLoading, ElMessage } from 'element-plus'
const libraryStore = useLibraryStore() const libraryStore = useLibraryStore()
const practiceStore = usePracticeStore() const practiceStore = usePracticeStore()
const questionStore = useQuestionStore()
const filterLibraryFileList = computed(() => { const filterLibraryFileList = computed(() => {
const subject = libraryStore.selectedSubject const subject = libraryStore.selectedSubject
@@ -62,12 +64,12 @@ const trainClick = async (file: ILibraryFile) => {
await generateQuestion() await generateQuestion()
practiceStore.hasStart = true practiceStore.hasStart = true
practiceStore.hasSubmit = false practiceStore.hasSubmit = false
practiceStore.checkList = [] questionStore.checkList = []
} }
const generateQuestion = async () => { const generateQuestion = async () => {
practiceStore.questionContent = '' questionStore.questionContent = ''
practiceStore.hasGenerateQuestion = false questionStore.hasGenerateQuestion = false
const loading = ElLoading.service({ const loading = ElLoading.service({
lock: true, lock: true,
@@ -76,7 +78,7 @@ const generateQuestion = async () => {
}) })
try { try {
const response = await fetch('/study-api/library/question', { const response = await fetch('/study-api/agent/question', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -96,13 +98,13 @@ const generateQuestion = async () => {
while (true) { while (true) {
const { value, done } = await reader.read() const { value, done } = await reader.read()
if (done) break if (done) break
practiceStore.questionContent += decoder.decode(value, { stream: true }) questionStore.questionContent += decoder.decode(value, { stream: true })
} }
ElMessage.success('生成完成') ElMessage.success('生成完成')
practiceStore.hasGenerateQuestion = true questionStore.hasGenerateQuestion = true
practiceStore.questionList = JSON.parse(practiceStore.questionContent) questionStore.questionList = JSON.parse(questionStore.questionContent)
libraryStore.fileTrainDialogVisible = true libraryStore.fileTrainDialogVisible = true
} catch (error) { } catch (error) {
console.log(error) console.log(error)

View File

@@ -2,177 +2,14 @@
<el-dialog v-model="libraryStore.fileTrainDialogVisible" <el-dialog v-model="libraryStore.fileTrainDialogVisible"
:title="libraryStore.currentFile.name" width="80%" center destroy-on-close :title="libraryStore.currentFile.name" width="80%" center destroy-on-close
class="train-dialog common-dialog"> class="train-dialog common-dialog">
<div class="question-list"> <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">
{{ q.type === 'single' ? '单选' : '多选' }}
</el-tag>
<span class="q-num">{{ idx + 1 }}</span>
</div>
<div class="q-stem">{{ q.question }}</div>
<div class="q-options">
<el-radio-group v-if="q.type === 'single'" v-model="q.select[0]">
<el-radio v-for="(opt, oi) in q.options" :key="oi" :value="optLetter(oi)">
{{ optLetter(oi) }}. {{ opt }}
</el-radio>
</el-radio-group>
<!-- 多选 -->
<el-checkbox-group v-if="q.type === 'multiple'" v-model="q.select">
<el-checkbox v-for="(opt, oi) in q.options" :key="oi" :value="optLetter(oi)">
{{ optLetter(oi) }}. {{ opt }}
</el-checkbox>
</el-checkbox-group>
</div>
<div v-if="practiceStore.checkList.length > 0" class="q-check">
<span style="font-weight: bold;"
:style="{color: practiceStore.checkList[idx].isCorrect ? 'green' : 'red'}">
正确答案 {{ practiceStore.checkList[idx].answer.join('、') }}
</span>
</div>
</div>
</div>
<div v-if="!practiceStore.hasSubmit" class="question-action">
<el-button type="success" @click="submitAnswerClick">提交答案</el-button>
</div>
</el-dialog> </el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { usePracticeStore } from '@/stores/practice.ts' import QuestionList from '@/components/question/QuestionList.vue'
import { useLibraryStore } from '@/stores/library.ts' import { useLibraryStore } from '@/stores/library.ts'
import { useMistakeStore } from '@/stores/mistake.ts'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getMistakeList, isAnswerCorrect, optLetter } from '@/utils/practiceUtil.ts'
import type { IMistake } from '@/types/mistake.ts'
const practiceStore = usePracticeStore()
const libraryStore = useLibraryStore() const libraryStore = useLibraryStore()
const mistakeStore = useMistakeStore()
const submitAnswerClick = async () => {
if (practiceStore.questionList.find((item) => item.select.length === 0)) {
ElMessage.error('还有未完成的题目')
} else {
practiceStore.hasSubmit = true
practiceStore.checkList = practiceStore.questionList.map((q) => ({
id: q.id,
answer: q.answer,
select: q.select,
isCorrect: isAnswerCorrect(q.answer, q.select)
}))
await addPracticeRecord()
await addMistakeList()
const { totalCount, correctCount, wrongCount } = practiceStore.currentRecord
await ElMessageBox.alert(
`${totalCount} 题 · 正确 ${correctCount} · 错误 ${wrongCount}`,
'训练结果',
{
confirmButtonText: '确认'
}
)
}
}
const addPracticeRecord = async () => {
const totalCount = practiceStore.checkList.length
const correctCount = practiceStore.checkList.filter((c) => c.isCorrect).length
practiceStore.currentRecord = {
libraryFileId: libraryStore.currentFile.id,
totalCount,
correctCount,
wrongCount: totalCount - correctCount
}
await practiceStore.addRecord()
}
const addMistakeList = async () => {
const mistakeList = getMistakeList(practiceStore.questionList, practiceStore.checkList)
mistakeStore.currentMistakes = []
mistakeList.forEach((item) => {
const mistake:IMistake = {
answer: item.answer,
module: libraryStore.currentFile.module,
name: libraryStore.currentFile.name,
options: item.options,
question: item.question,
subject: libraryStore.currentFile.subject,
type: item.type
}
mistakeStore.currentMistakes.push(mistake)
})
await mistakeStore.addMistakeList()
}
</script> </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

@@ -0,0 +1,114 @@
<template>
<el-dialog v-model="examStore.dialogVisible" title="考试配置" width="30%" center destroy-on-close>
<el-form ref="examFormRef" :model="examStore.config" :rules="rules" label-width="auto">
<el-form-item label="考试学科" prop="subject">
<el-select
v-model="examStore.config.subject"
placeholder="请选择考试学科"
>
<el-option
v-for="(item, index) in subjectList"
:key="index" :label="item" :value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="题目数量" prop="total">
<el-input-number
v-model="examStore.config.total"
:min="10"
:max="50"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="examStore.dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmClick(examFormRef)">确认</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ElInputNumber, ElLoading, ElMessage, ElOption, ElSelect, type FormInstance } from 'element-plus'
import { useExamStore } from '@/stores/exam.ts'
import { useLibraryStore } from '@/stores/library.ts'
import { useQuestionStore } from '@/stores/question.ts'
import { computed, ref } from 'vue'
const examStore = useExamStore()
const libraryStore = useLibraryStore()
const questionStore = useQuestionStore()
const examFormRef = ref<FormInstance>()
const subjectList = computed(() => {
return [...new Set(libraryStore.fileList.map((item) => item.subject))]
})
const rules = {
subject: [
{ required: true, message: '请选择考试学科', trigger: 'blur' }
],
total: [
{ required: true, message: '请选择题目数量', trigger: 'blur' }
]
}
const confirmClick = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid, fields) => {
if (valid) {
await generateExam()
} else {
ElMessage.error('请先输入信息')
}
})
}
const generateExam = async () => {
questionStore.questionList = []
const loading = ElLoading.service({
lock: true,
text: '题库生成中 请等待',
background: 'rgba(0, 0, 0, 0.7)'
})
try {
const response = await fetch('/study-api/agent/exam', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
subject: examStore.config.subject,
total: examStore.config.total
})
})
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
questionStore.questionContent += decoder.decode(value, { stream: true })
}
ElMessage.success('生成完成')
questionStore.questionList = JSON.parse(questionStore.questionContent)
examStore.dialogVisible = false
} catch (error) {
console.log(error)
ElMessage.error('生成失败 请联系管理员')
} finally {
loading.close()
}
}
</script>

View File

@@ -0,0 +1,177 @@
<template>
<div class="question-container">
<div class="question-list">
<div v-for="(q, idx) in questionStore.questionList" :key="q.id" class="question-card">
<div class="q-header">
<el-tag :type="q.type === 'single' ? 'primary' : 'warning'" effect="dark">
{{ q.type === 'single' ? '单选' : '多选' }}
</el-tag>
<span class="q-num">{{ idx + 1 }}</span>
</div>
<div class="q-stem">{{ q.question }}</div>
<div class="q-options">
<el-radio-group v-if="q.type === 'single'" v-model="q.select[0]">
<el-radio v-for="(opt, oi) in q.options" :key="oi" :value="optLetter(oi)">
{{ optLetter(oi) }}. {{ opt }}
</el-radio>
</el-radio-group>
<!-- 多选 -->
<el-checkbox-group v-if="q.type === 'multiple'" v-model="q.select">
<el-checkbox v-for="(opt, oi) in q.options" :key="oi" :value="optLetter(oi)">
{{ optLetter(oi) }}. {{ opt }}
</el-checkbox>
</el-checkbox-group>
</div>
<div v-if="questionStore.checkList.length > 0" class="q-check">
<span style="font-weight: bold;"
:style="{color: questionStore.checkList[idx].isCorrect ? 'green' : 'red'}">
正确答案 {{ questionStore.checkList[idx].answer.join('、') }}
</span>
</div>
</div>
</div>
<div v-if="!questionStore.hasSubmit" class="question-action">
<el-button type="success" @click="submitAnswerClick">提交答案</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { usePracticeStore } from '@/stores/practice.ts'
import { useLibraryStore } from '@/stores/library.ts'
import { useMistakeStore } from '@/stores/mistake.ts'
import { useQuestionStore } from '@/stores/question.ts'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getMistakeList, isAnswerCorrect, optLetter } from '@/utils/practiceUtil.ts'
import type { IMistake } from '@/types/mistake.ts'
const practiceStore = usePracticeStore()
const libraryStore = useLibraryStore()
const mistakeStore = useMistakeStore()
const questionStore = useQuestionStore()
const submitAnswerClick = async () => {
if (questionStore.questionList.find((item) => item.select.length === 0)) {
ElMessage.error('还有未完成的题目')
} else {
practiceStore.hasSubmit = true
questionStore.checkList = questionStore.questionList.map((q) => ({
id: q.id,
answer: q.answer,
select: q.select,
isCorrect: isAnswerCorrect(q.answer, q.select)
}))
await addPracticeRecord()
await addMistakeList()
const { totalCount, correctCount, wrongCount } = practiceStore.currentRecord
await ElMessageBox.alert(
`${totalCount} 题 · 正确 ${correctCount} · 错误 ${wrongCount}`,
'训练结果',
{
confirmButtonText: '确认'
}
)
}
}
const addPracticeRecord = async () => {
const totalCount = questionStore.checkList.length
const correctCount = questionStore.checkList.filter((c) => c.isCorrect).length
practiceStore.currentRecord = {
libraryFileId: libraryStore.currentFile.id,
totalCount,
correctCount,
wrongCount: totalCount - correctCount
}
await practiceStore.addRecord()
}
const addMistakeList = async () => {
const mistakeList = getMistakeList(questionStore.questionList, questionStore.checkList)
mistakeStore.currentMistakes = []
mistakeList.forEach((item) => {
const mistake:IMistake = {
answer: item.answer,
module: libraryStore.currentFile.module,
name: libraryStore.currentFile.name,
options: item.options,
question: item.question,
subject: libraryStore.currentFile.subject,
type: item.type
}
mistakeStore.currentMistakes.push(mistake)
})
await mistakeStore.addMistakeList()
}
</script>
<style lang="scss">
.question-container {
display: flex;
flex-direction: column;
gap: 16px;
.question-list {
padding: 10px;
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>

19
src/stores/exam.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineStore } from 'pinia'
import type { IExamConfig } from '@/types/exam.ts'
export const useExamStore = defineStore('exam', {
state: () => ({
hasStart: false,
hasSubmit: false,
dialogVisible: false,
config: {} as IExamConfig
}),
actions: {
getEmptyConfig(): IExamConfig {
return {
subject: '',
total: 50
}
}
}
})

View File

@@ -1,18 +1,13 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { addPracticeRecordApi, queryPracticeRecordApi } from '@/apis/practice.ts' import { addPracticeRecordApi, queryPracticeRecordApi } from '@/apis/practice.ts'
import type { IPracticeCheck, IPracticeRecord } from '@/types/practice.ts' import type { IPracticeRecord } from '@/types/practice.ts'
import type { IQuestion } from '@/types/question.ts'
export const usePracticeStore = defineStore('practice', { export const usePracticeStore = defineStore('practice', {
state: () => ({ state: () => ({
hasStart: false, hasStart: false,
hasSubmit: false, hasSubmit: false,
libraryFileId: 0, libraryFileId: 0,
hasGenerateQuestion: false,
questionContent: '',
questionList: [] as IQuestion[],
recordList: [] as IPracticeRecord[], recordList: [] as IPracticeRecord[],
checkList: [] as IPracticeCheck[],
currentRecord: {} as IPracticeRecord currentRecord: {} as IPracticeRecord
}), }),
actions: { actions: {

15
src/stores/question.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineStore } from 'pinia'
import type { IPracticeCheck, IPracticeRecord } from '@/types/practice.ts'
import type { IQuestion } from '@/types/question.ts'
export const useQuestionStore = defineStore('question', {
state: () => ({
hasStart: false,
hasSubmit: false,
hasGenerateQuestion: false,
questionContent: '',
questionList: [] as IQuestion[],
recordList: [] as IPracticeRecord[],
checkList: [] as IPracticeCheck[]
})
})

4
src/types/exam.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface IExamConfig {
subject: string;
total: number;
}

View File

@@ -1,11 +1,37 @@
<template> <template>
<div> <div class="exam-view common-view">
<h2>考试中心</h2> <div class="view-header">
<p style="color:#94a3b8;margin-top:8px;">按学科进行综合检验模拟真实考试环境</p> <div class="view-content">
<el-divider /> <h2 class="view-title">考试中心</h2>
<el-empty description="考试页面 - 待开发" /> <p class="view-subtitle">按学科进行综合检验模拟真实考试环境</p>
</div>
<el-button type="primary" :icon="Aim" @click="examClick">开始考试</el-button>
</div>
<question-list v-if="questionStore.questionList.length > 0"/>
<exam-dialog />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
</script> import QuestionList from '@/components/question/QuestionList.vue'
import ExamDialog from '@/components/exam/ExamDialog.vue'
import { Aim } from '@element-plus/icons-vue'
import { useExamStore } from '@/stores/exam.ts'
import { useQuestionStore } from '@/stores/question.ts'
import { onMounted } from 'vue'
const examStore = useExamStore()
const questionStore = useQuestionStore()
onMounted(() => {
questionStore.questionList = []
})
const examClick = () => {
questionStore.questionList = []
examStore.config = examStore.getEmptyConfig()
examStore.dialogVisible = true
}
</script>