feat:增加训练场和错题集模块
This commit is contained in:
@@ -11,7 +11,9 @@
|
||||
"dependencies": {
|
||||
"@vueup/vue-quill": "^1.5.5",
|
||||
"axios": "^1.18.1",
|
||||
"dayjs": "^1.11.21",
|
||||
"element-plus": "^2.14.3",
|
||||
"filesize": "^11.0.22",
|
||||
"pinia": "^4.0.2",
|
||||
"vue": "^3.5.39",
|
||||
"vue-router": "^5.2.0",
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -14,9 +14,15 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.18.1
|
||||
version: 1.18.1
|
||||
dayjs:
|
||||
specifier: ^1.11.21
|
||||
version: 1.11.21
|
||||
element-plus:
|
||||
specifier: ^2.14.3
|
||||
version: 2.14.3(vue@3.5.40(typescript@6.0.3))
|
||||
filesize:
|
||||
specifier: ^11.0.22
|
||||
version: 11.0.22
|
||||
pinia:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
|
||||
@@ -1038,6 +1044,10 @@ packages:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
|
||||
filesize@11.0.22:
|
||||
resolution: {integrity: sha512-RlCVs9CY+oSsRnNZn95J9vDXjNjOwddKyTFjOYtA4yxYVIxBnwiVVGJX+TFhsmu3uUf81JDGyijtYL9xgawlTw==}
|
||||
engines: {node: '>= 10.8.0'}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3219,6 +3229,8 @@ snapshots:
|
||||
dependencies:
|
||||
flat-cache: 3.2.0
|
||||
|
||||
filesize@11.0.22: {}
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
8
src/apis/mistake.ts
Normal file
8
src/apis/mistake.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { cloudService } from './index'
|
||||
import type { IMistake } from '@/types/mistake.ts'
|
||||
|
||||
export const queryMistakeListApi = (): Promise<IMistake[]> =>
|
||||
cloudService({
|
||||
url: '/study-api/mistake',
|
||||
method: 'get'
|
||||
})
|
||||
26
src/apis/practice.ts
Normal file
26
src/apis/practice.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { cloudService } from './index'
|
||||
import type { ILibraryFileQuestion } from '@/types/library.ts'
|
||||
import type { IPracticeCheck, IPracticeRecord } from '@/types/practice.ts'
|
||||
|
||||
|
||||
export const queryPracticeQuestionByIdApi = (id: number): Promise<ILibraryFileQuestion[]> =>
|
||||
cloudService({
|
||||
url: `/study-api/practice/files/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
|
||||
export const queryPracticeRecordApi = (id: number): Promise<IPracticeRecord[]> =>
|
||||
cloudService({
|
||||
url: `/study-api/practice/record/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const checkPracticeApi = (id: number,
|
||||
data: ILibraryFileQuestion[]): Promise<IPracticeCheck[]> =>
|
||||
cloudService({
|
||||
url: `/study-api/practice/check/${id}`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
title="新增资料" width="80%"
|
||||
center destroy-on-close
|
||||
class="file-dialog common-dialog">
|
||||
<el-steps :active="activeStep" align-center>
|
||||
<el-steps :active="libraryStore.activeStep" align-center>
|
||||
<el-step title="上传资料" :icon="Upload" />
|
||||
<el-step title="确认资料" :icon="Document " />
|
||||
<el-step title="生成题库" :icon="MagicStick" />
|
||||
</el-steps>
|
||||
|
||||
<el-form ref="libraryFileFormRef" v-if="activeStep === 1"
|
||||
<el-form ref="libraryFileFormRef" v-if="libraryStore.activeStep === 1"
|
||||
:model="libraryStore.currentFile"
|
||||
:rules="rules" label-width="auto">
|
||||
<el-form-item label="学科" prop="subject">
|
||||
@@ -54,13 +54,19 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<QuillEditor v-if="activeStep === 2"
|
||||
<QuillEditor v-if="libraryStore.activeStep === 2"
|
||||
:content="libraryStore.currentFile.content"
|
||||
contentType="html" theme="snow"/>
|
||||
|
||||
<div v-if="activeStep === 3" class="generate-question">
|
||||
<div>
|
||||
<el-button type="primary" @click="generateQuestion">生成题库</el-button>
|
||||
<div v-if="libraryStore.activeStep === 3" class="generate-question">
|
||||
<div class="generate-action">
|
||||
<el-button type="primary" @click="generateQuestion">
|
||||
{{ libraryStore.hasGenerateQuestion ? '重新生成' : '生成题库' }}
|
||||
</el-button>
|
||||
|
||||
<span v-if="libraryStore.hasGenerateQuestion">
|
||||
共 {{ libraryStore.currentFile.questions.length }} 道题目
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="libraryStore.hasGenerateQuestion" class="question-list">
|
||||
@@ -91,14 +97,17 @@
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div v-else v-html="questionContent" />
|
||||
<div v-else v-html="libraryStore.questionContent" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="libraryStore.fileDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="previousClick" :disabled="activeStep === 1">上一步</el-button>
|
||||
<el-button type="primary" @click="nextClick" :disabled="activeStep === 3">下一步</el-button>
|
||||
<el-button type="primary" @click="submitForm()" :disabled="activeStep !== 3">确认</el-button>
|
||||
<el-button type="primary" @click="previousClick"
|
||||
:disabled="libraryStore.activeStep === 1">上一步</el-button>
|
||||
<el-button type="primary" @click="nextClick"
|
||||
:disabled="libraryStore.activeStep === 3">下一步</el-button>
|
||||
<el-button type="primary" @click="submitForm()"
|
||||
:disabled="libraryStore.activeStep !== 3">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -111,13 +120,10 @@ import { Document, MagicStick, Upload, UploadFilled } from '@element-plus/icons-
|
||||
import { QuillEditor } from '@vueup/vue-quill'
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||
import { uploadLibraryFileApi } from '@/apis/library.ts'
|
||||
import { getModuleOptions } from '@/utils/library.ts'
|
||||
import { getModuleOptions } from '@/utils/libraryUtil.ts'
|
||||
|
||||
const libraryStore = useLibraryStore()
|
||||
|
||||
const libraryFileFormRef = ref<FormInstance>()
|
||||
const activeStep = ref(1)
|
||||
const questionContent = ref<string>()
|
||||
|
||||
const rules = {
|
||||
subject: [
|
||||
@@ -132,13 +138,13 @@ const rules = {
|
||||
}
|
||||
|
||||
const previousClick = () => {
|
||||
if (activeStep.value > 1) {
|
||||
activeStep.value--
|
||||
if (libraryStore.activeStep > 1) {
|
||||
libraryStore.activeStep--
|
||||
}
|
||||
}
|
||||
|
||||
const nextClick = async () => {
|
||||
if (activeStep.value === 1) {
|
||||
if (libraryStore.activeStep === 1) {
|
||||
try {
|
||||
await libraryFileFormRef.value?.validate()
|
||||
} catch {
|
||||
@@ -147,8 +153,8 @@ const nextClick = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (activeStep.value < 3) {
|
||||
activeStep.value++
|
||||
if (libraryStore.activeStep < 3) {
|
||||
libraryStore.activeStep++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +193,7 @@ const generateQuestion = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
questionContent.value = ''
|
||||
libraryStore.questionContent = ''
|
||||
libraryStore.hasGenerateQuestion = false
|
||||
|
||||
const loading = ElLoading.service({
|
||||
@@ -217,13 +223,13 @@ const generateQuestion = async () => {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
questionContent.value += decoder.decode(value, { stream: true })
|
||||
libraryStore.questionContent += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
ElMessage.success('生成完成')
|
||||
libraryStore.hasGenerateQuestion = true
|
||||
|
||||
libraryStore.currentFile.questions = JSON.parse(questionContent.value)
|
||||
libraryStore.currentFile.questions = JSON.parse(libraryStore.questionContent)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('生成失败 请联系管理员')
|
||||
@@ -256,6 +262,12 @@ const submitForm = async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.generate-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.question-list {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="file-list">
|
||||
<library-filter v-if="libraryStore.fileList.length"/>
|
||||
<library-filter v-if="filterLibraryFileList.length"/>
|
||||
|
||||
<div v-for="file in libraryStore.fileList" :key="file.id" class="file-card card-item">
|
||||
<div v-for="file in filterLibraryFileList" :key="file.id" class="file-card card-item">
|
||||
<div class="file-info">
|
||||
|
||||
<div class="file-icon" :class="file.type === 'image' ? 'icon-image' : 'icon-pdf'">
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<div class="file-text">
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-meta">{{ file.size }} · {{ file.uploadTime }}</div>
|
||||
<div class="file-meta">{{ filesize(file.size) }} · {{ fromNow(file.uploadTime)}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!libraryStore.fileList.length" description="暂无文件" :image-size="80" />
|
||||
<el-empty v-if="!filterLibraryFileList.length" description="暂无文件" :image-size="80" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,12 +30,22 @@ import { Delete, Edit, Picture, Document } from '@element-plus/icons-vue'
|
||||
import { useLibraryStore } from '@/stores/library.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'
|
||||
|
||||
const libraryStore = useLibraryStore()
|
||||
|
||||
const filterLibraryFileList = computed(() => {
|
||||
const subject = libraryStore.selectedSubject
|
||||
const module = libraryStore.selectedModule
|
||||
return libraryStore.fileList.filter((item) => item.subject === subject && item.module === module)
|
||||
})
|
||||
|
||||
const editClick = async (file: ILibraryFile) => {
|
||||
await libraryStore.queryFileByList(file.id)
|
||||
await libraryStore.queryFileById(file.id)
|
||||
libraryStore.fileApiType = 'UPDATE'
|
||||
libraryStore.activeStep = 1
|
||||
libraryStore.hasGenerateQuestion = true
|
||||
libraryStore.fileDialogVisible = true
|
||||
}
|
||||
|
||||
@@ -15,11 +15,26 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLibraryStore } from '@/stores/library.ts'
|
||||
import { getModuleOptions } from '@/utils/library.ts'
|
||||
import { getModuleOptions } from '@/utils/libraryUtil.ts'
|
||||
|
||||
const libraryStore = useLibraryStore()
|
||||
|
||||
const handleSubjectEvent = (value: string): void => {
|
||||
libraryStore.moduleOptions = getModuleOptions(libraryStore.subjectTree, value)
|
||||
libraryStore.selectedModule = libraryStore.moduleOptions[0].value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
71
src/components/Practice/PracticeQuestion.vue
Normal file
71
src/components/Practice/PracticeQuestion.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="practice-question">
|
||||
<div v-if="practiceStore.hasStart" 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">
|
||||
{{ 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.hasStart" class="question-action">
|
||||
<el-button type="success" @click="submitAnswerClick">
|
||||
{{ practiceStore.hasSubmit ? '重新练习' : '提交答案' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePracticeStore } from '@/stores/practice.ts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const practiceStore = usePracticeStore()
|
||||
|
||||
const optLetter = (index: number): string => {
|
||||
return String.fromCharCode(65 + index)
|
||||
}
|
||||
|
||||
const submitAnswerClick = async () => {
|
||||
if (practiceStore.hasSubmit) {
|
||||
practiceStore.hasSubmit = false
|
||||
practiceStore.checkList = []
|
||||
await practiceStore.queryQuestionById()
|
||||
} else {
|
||||
practiceStore.hasSubmit = true
|
||||
if (practiceStore.questionList.find((item) => item.select.length === 0)) {
|
||||
ElMessage.error('还有未完成的题目')
|
||||
return
|
||||
}
|
||||
|
||||
await practiceStore.checkQuestion()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
34
src/components/Practice/PracticeRecord.vue
Normal file
34
src/components/Practice/PracticeRecord.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<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>
|
||||
@@ -8,21 +8,33 @@ import {
|
||||
} from '@/apis/library.ts'
|
||||
import type { IHandleApi, IOptions, ITreeNode } from '@/types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getModuleOptions, getSubjectOptions, getSubjectTree } from '@/utils/library.ts'
|
||||
import { getModuleOptions, getSubjectOptions, getSubjectTree } from '@/utils/libraryUtil.ts'
|
||||
|
||||
export const useLibraryStore = defineStore('library', {
|
||||
state: () => ({
|
||||
selectedSubject: '',
|
||||
selectedModule: '',
|
||||
selectedSubject: '' as number | string,
|
||||
selectedModule: '' as number | string,
|
||||
subjectTree: [] as ITreeNode[],
|
||||
fileList: [] as ILibraryFile[],
|
||||
currentFile: {} as ILibraryFile,
|
||||
currentFile: {
|
||||
id: 0,
|
||||
subject: '',
|
||||
module: '',
|
||||
name: '',
|
||||
size: 0,
|
||||
uploadTime: '',
|
||||
type: '',
|
||||
content: '',
|
||||
questions: []
|
||||
} as ILibraryFile,
|
||||
fileDetailDialogVisible: false,
|
||||
fileDialogVisible: false,
|
||||
hasGenerateQuestion: false,
|
||||
fileApiType: 'ADD' as IHandleApi,
|
||||
subjectOptions: [] as IOptions[],
|
||||
moduleOptions: [] as IOptions[]
|
||||
moduleOptions: [] as IOptions[],
|
||||
activeStep: 1,
|
||||
questionContent: ''
|
||||
}),
|
||||
actions: {
|
||||
async queryFileList() {
|
||||
@@ -30,11 +42,11 @@ export const useLibraryStore = defineStore('library', {
|
||||
this.fileList = await queryLibraryFileListApi()
|
||||
this.subjectTree = getSubjectTree(this.fileList)
|
||||
this.subjectOptions = getSubjectOptions(this.subjectTree)
|
||||
this.selectedSubject = this.subjectOptions[0].label
|
||||
this.selectedSubject = this.subjectOptions[0].value
|
||||
this.moduleOptions = getModuleOptions(this.subjectTree, this.selectedSubject)
|
||||
this.selectedModule = this.moduleOptions[0].label
|
||||
this.selectedModule = this.moduleOptions[0].value
|
||||
},
|
||||
async queryFileByList(id: number) {
|
||||
async queryFileById(id: number) {
|
||||
this.currentFile = await queryLibraryFileByIdApi(id)
|
||||
},
|
||||
async handleFileApi(file: ILibraryFile) {
|
||||
|
||||
14
src/stores/mistake.ts
Normal file
14
src/stores/mistake.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { IMistake } from '@/types/mistake.ts'
|
||||
import { queryMistakeListApi } from '@/apis/mistake.ts'
|
||||
|
||||
export const useMistakeStore = defineStore('mistake', {
|
||||
state: () => ({
|
||||
mistakeList: [] as IMistake[]
|
||||
}),
|
||||
actions: {
|
||||
async queryMistakeList() {
|
||||
this.mistakeList = await queryMistakeListApi()
|
||||
}
|
||||
}
|
||||
})
|
||||
26
src/stores/practice.ts
Normal file
26
src/stores/practice.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { ILibraryFileQuestion } from '@/types/library.ts'
|
||||
import { checkPracticeApi, queryPracticeQuestionByIdApi, queryPracticeRecordApi } from '@/apis/practice.ts'
|
||||
import type { IPracticeCheck, IPracticeRecord } from '@/types/practice.ts'
|
||||
|
||||
export const usePracticeStore = defineStore('practice', {
|
||||
state: () => ({
|
||||
hasStart: false,
|
||||
hasSubmit: false,
|
||||
libraryFileId: 0,
|
||||
questionList: [] as ILibraryFileQuestion[],
|
||||
recordList: [] as IPracticeRecord[],
|
||||
checkList: [] as IPracticeCheck[]
|
||||
}),
|
||||
actions: {
|
||||
async queryQuestionById() {
|
||||
this.questionList = await queryPracticeQuestionByIdApi(this.libraryFileId)
|
||||
},
|
||||
async queryRecord() {
|
||||
this.recordList = await queryPracticeRecordApi(this.libraryFileId)
|
||||
},
|
||||
async checkQuestion() {
|
||||
this.checkList = await checkPracticeApi(this.libraryFileId, this.questionList)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,27 @@
|
||||
.common-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.view-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.view-content {
|
||||
.view-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.view-subtitle {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.common-dialog {
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ILibraryFileQuestion {
|
||||
question: string;
|
||||
options: string[];
|
||||
answer: string[];
|
||||
select: string[];
|
||||
}
|
||||
|
||||
export interface ILibraryFile {
|
||||
|
||||
11
src/types/mistake.ts
Normal file
11
src/types/mistake.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface IMistake {
|
||||
id: number;
|
||||
subject: string;
|
||||
module: string;
|
||||
name: string;
|
||||
type: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
answer: string[];
|
||||
wrongCount: number;
|
||||
}
|
||||
15
src/types/practice.ts
Normal file
15
src/types/practice.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface IPracticeCheck {
|
||||
id: number;
|
||||
answer: string[];
|
||||
select: string[];
|
||||
isCorrect: boolean;
|
||||
}
|
||||
|
||||
export interface IPracticeRecord {
|
||||
id: number;
|
||||
libraryFileId:number;
|
||||
totalCount: number;
|
||||
correctCount: number;
|
||||
wrongCount: number;
|
||||
createTime: string;
|
||||
}
|
||||
8
src/utils/dayUtil.ts
Normal file
8
src/utils/dayUtil.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
export const fromNow = (time: Date | string | number) => dayjs(time).fromNow()
|
||||
@@ -42,7 +42,7 @@ export const getSubjectOptions = (subjectTree: ITreeNode[]): IOptions[] => {
|
||||
return subjectTree.map((s) => ({ label: s.name, value: s.name }))
|
||||
}
|
||||
|
||||
export const getModuleOptions = (subjectTree: ITreeNode[], subject: string): IOptions[] => {
|
||||
export const getModuleOptions = (subjectTree: ITreeNode[], subject: string | number): IOptions[] => {
|
||||
const node = subjectTree.find((s) => s.name === subject)
|
||||
return (node?.children || []).map((m) => ({
|
||||
label: m.name,
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="library-view">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">知识库</h2>
|
||||
<p class="page-subtitle">管理学习资料,查看 AI 解析结果</p>
|
||||
<div class="library-view common-view">
|
||||
<div class="view-header">
|
||||
<div class="view-content">
|
||||
<h2 class="view-title">知识库</h2>
|
||||
<p class="view-subtitle">管理学习资料,查看 AI 解析结果</p>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="addFileClick">新增资料</el-button>
|
||||
</div>
|
||||
@@ -30,49 +30,15 @@ onMounted(async () => {
|
||||
const addFileClick = () => {
|
||||
libraryStore.currentFile = libraryStore.getEmptyFile()
|
||||
libraryStore.fileApiType = 'ADD'
|
||||
libraryStore.activeStep = 1
|
||||
libraryStore.hasGenerateQuestion = false
|
||||
libraryStore.questionContent = ''
|
||||
libraryStore.fileDialogVisible = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.library-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
/* 标题行 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 文件列表 */
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,11 +1,105 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>错题本</h2>
|
||||
<p style="color:#94a3b8;margin-top:8px;">回顾错题,查漏补缺</p>
|
||||
<el-divider />
|
||||
<el-empty description="错题本页面 - 待开发" />
|
||||
<div class="mistake-view common-view">
|
||||
<div class="view-header">
|
||||
<div class="view-content">
|
||||
<h2 class="view-title">错题本</h2>
|
||||
<p class="view-subtitle">回顾错题,查漏补缺</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mistake-record">
|
||||
<div class="mistake-list">
|
||||
<el-card
|
||||
v-for="item in mistakeStore.mistakeList"
|
||||
:key="item.id"
|
||||
class="mistake-card"
|
||||
shadow="never"
|
||||
>
|
||||
<div class="library">
|
||||
<el-tag type="primary" effect="dark">{{ item.subject }} · {{ item.module }}</el-tag>
|
||||
<el-tag type="danger" effect="dark">错 {{ item.wrongCount }} 次</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="question">{{ item.question }}</div>
|
||||
|
||||
<div class="option-list">
|
||||
<div v-for="(opt, index) in item.options" :key="index" class="option"
|
||||
:class="optionClass(item, index)">
|
||||
{{ letter(index) }}.{{ opt }}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useMistakeStore } from '@/stores/mistake.ts'
|
||||
|
||||
const mistakeStore = useMistakeStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await mistakeStore.queryMistakeList()
|
||||
})
|
||||
|
||||
const letter = (i: number) => String.fromCharCode(65 + i)
|
||||
|
||||
const optionClass = (item: any, index: number) => {
|
||||
const letters = ['A', 'B', 'C', 'D']
|
||||
const isCorrect = item.answer.includes(letters[index])
|
||||
return {
|
||||
correct: isCorrect
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.mistake-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.mistake-card {
|
||||
.el-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.library {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.question {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.option-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.option {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e6eb;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.option.correct {
|
||||
border-color: #52c41a;
|
||||
background: #f6ffed;
|
||||
color: #389e0d;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,182 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>训练场</h2>
|
||||
<p style="color:#94a3b8;margin-top:8px;">专项练习,巩固知识点</p>
|
||||
<el-divider />
|
||||
<el-empty description="训练场页面 - 待开发" />
|
||||
<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 v-if="!practiceStore.hasStart" type="primary" :icon="Aim"
|
||||
@click="startPracticeClick">开始练习</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<library-filter />
|
||||
|
||||
<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'
|
||||
|
||||
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()
|
||||
await practiceStore.queryQuestionById()
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const startPracticeClick = () => {
|
||||
practiceStore.hasStart = true
|
||||
practiceStore.checkList = []
|
||||
}
|
||||
</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