feat:增加训练场和错题集模块
This commit is contained in:
@@ -11,7 +11,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueup/vue-quill": "^1.5.5",
|
"@vueup/vue-quill": "^1.5.5",
|
||||||
"axios": "^1.18.1",
|
"axios": "^1.18.1",
|
||||||
|
"dayjs": "^1.11.21",
|
||||||
"element-plus": "^2.14.3",
|
"element-plus": "^2.14.3",
|
||||||
|
"filesize": "^11.0.22",
|
||||||
"pinia": "^4.0.2",
|
"pinia": "^4.0.2",
|
||||||
"vue": "^3.5.39",
|
"vue": "^3.5.39",
|
||||||
"vue-router": "^5.2.0",
|
"vue-router": "^5.2.0",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -14,9 +14,15 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.18.1
|
specifier: ^1.18.1
|
||||||
version: 1.18.1
|
version: 1.18.1
|
||||||
|
dayjs:
|
||||||
|
specifier: ^1.11.21
|
||||||
|
version: 1.11.21
|
||||||
element-plus:
|
element-plus:
|
||||||
specifier: ^2.14.3
|
specifier: ^2.14.3
|
||||||
version: 2.14.3(vue@3.5.40(typescript@6.0.3))
|
version: 2.14.3(vue@3.5.40(typescript@6.0.3))
|
||||||
|
filesize:
|
||||||
|
specifier: ^11.0.22
|
||||||
|
version: 11.0.22
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^4.0.2
|
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))
|
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==}
|
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
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:
|
fill-range@7.1.1:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -3219,6 +3229,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 3.2.0
|
flat-cache: 3.2.0
|
||||||
|
|
||||||
|
filesize@11.0.22: {}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
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%"
|
title="新增资料" width="80%"
|
||||||
center destroy-on-close
|
center destroy-on-close
|
||||||
class="file-dialog common-dialog">
|
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="Upload" />
|
||||||
<el-step title="确认资料" :icon="Document " />
|
<el-step title="确认资料" :icon="Document " />
|
||||||
<el-step title="生成题库" :icon="MagicStick" />
|
<el-step title="生成题库" :icon="MagicStick" />
|
||||||
</el-steps>
|
</el-steps>
|
||||||
|
|
||||||
<el-form ref="libraryFileFormRef" v-if="activeStep === 1"
|
<el-form ref="libraryFileFormRef" v-if="libraryStore.activeStep === 1"
|
||||||
:model="libraryStore.currentFile"
|
:model="libraryStore.currentFile"
|
||||||
:rules="rules" label-width="auto">
|
:rules="rules" label-width="auto">
|
||||||
<el-form-item label="学科" prop="subject">
|
<el-form-item label="学科" prop="subject">
|
||||||
@@ -54,13 +54,19 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<QuillEditor v-if="activeStep === 2"
|
<QuillEditor v-if="libraryStore.activeStep === 2"
|
||||||
:content="libraryStore.currentFile.content"
|
:content="libraryStore.currentFile.content"
|
||||||
contentType="html" theme="snow"/>
|
contentType="html" theme="snow"/>
|
||||||
|
|
||||||
<div v-if="activeStep === 3" class="generate-question">
|
<div v-if="libraryStore.activeStep === 3" class="generate-question">
|
||||||
<div>
|
<div class="generate-action">
|
||||||
<el-button type="primary" @click="generateQuestion">生成题库</el-button>
|
<el-button type="primary" @click="generateQuestion">
|
||||||
|
{{ libraryStore.hasGenerateQuestion ? '重新生成' : '生成题库' }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<span v-if="libraryStore.hasGenerateQuestion">
|
||||||
|
共 {{ libraryStore.currentFile.questions.length }} 道题目
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="libraryStore.hasGenerateQuestion" class="question-list">
|
<div v-if="libraryStore.hasGenerateQuestion" class="question-list">
|
||||||
@@ -91,14 +97,17 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else v-html="questionContent" />
|
<div v-else v-html="libraryStore.questionContent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="libraryStore.fileDialogVisible = false">取消</el-button>
|
<el-button @click="libraryStore.fileDialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="previousClick" :disabled="activeStep === 1">上一步</el-button>
|
<el-button type="primary" @click="previousClick"
|
||||||
<el-button type="primary" @click="nextClick" :disabled="activeStep === 3">下一步</el-button>
|
:disabled="libraryStore.activeStep === 1">上一步</el-button>
|
||||||
<el-button type="primary" @click="submitForm()" :disabled="activeStep !== 3">确认</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>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -111,13 +120,10 @@ import { Document, MagicStick, Upload, UploadFilled } from '@element-plus/icons-
|
|||||||
import { QuillEditor } from '@vueup/vue-quill'
|
import { QuillEditor } from '@vueup/vue-quill'
|
||||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||||
import { uploadLibraryFileApi } from '@/apis/library.ts'
|
import { uploadLibraryFileApi } from '@/apis/library.ts'
|
||||||
import { getModuleOptions } from '@/utils/library.ts'
|
import { getModuleOptions } from '@/utils/libraryUtil.ts'
|
||||||
|
|
||||||
const libraryStore = useLibraryStore()
|
const libraryStore = useLibraryStore()
|
||||||
|
|
||||||
const libraryFileFormRef = ref<FormInstance>()
|
const libraryFileFormRef = ref<FormInstance>()
|
||||||
const activeStep = ref(1)
|
|
||||||
const questionContent = ref<string>()
|
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
subject: [
|
subject: [
|
||||||
@@ -132,13 +138,13 @@ const rules = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousClick = () => {
|
const previousClick = () => {
|
||||||
if (activeStep.value > 1) {
|
if (libraryStore.activeStep > 1) {
|
||||||
activeStep.value--
|
libraryStore.activeStep--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextClick = async () => {
|
const nextClick = async () => {
|
||||||
if (activeStep.value === 1) {
|
if (libraryStore.activeStep === 1) {
|
||||||
try {
|
try {
|
||||||
await libraryFileFormRef.value?.validate()
|
await libraryFileFormRef.value?.validate()
|
||||||
} catch {
|
} catch {
|
||||||
@@ -147,8 +153,8 @@ const nextClick = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeStep.value < 3) {
|
if (libraryStore.activeStep < 3) {
|
||||||
activeStep.value++
|
libraryStore.activeStep++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +193,7 @@ const generateQuestion = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
questionContent.value = ''
|
libraryStore.questionContent = ''
|
||||||
libraryStore.hasGenerateQuestion = false
|
libraryStore.hasGenerateQuestion = false
|
||||||
|
|
||||||
const loading = ElLoading.service({
|
const loading = ElLoading.service({
|
||||||
@@ -217,13 +223,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
|
||||||
questionContent.value += decoder.decode(value, { stream: true })
|
libraryStore.questionContent += decoder.decode(value, { stream: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success('生成完成')
|
ElMessage.success('生成完成')
|
||||||
libraryStore.hasGenerateQuestion = true
|
libraryStore.hasGenerateQuestion = true
|
||||||
|
|
||||||
libraryStore.currentFile.questions = JSON.parse(questionContent.value)
|
libraryStore.currentFile.questions = JSON.parse(libraryStore.questionContent)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
ElMessage.error('生成失败 请联系管理员')
|
ElMessage.error('生成失败 请联系管理员')
|
||||||
@@ -256,6 +262,12 @@ const submitForm = async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
|
.generate-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-list {
|
.question-list {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="file-list">
|
<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-info">
|
||||||
|
|
||||||
<div class="file-icon" :class="file.type === 'image' ? 'icon-image' : 'icon-pdf'">
|
<div class="file-icon" :class="file.type === 'image' ? 'icon-image' : 'icon-pdf'">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="file-text">
|
<div class="file-text">
|
||||||
<div class="file-name">{{ file.name }}</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-empty v-if="!libraryStore.fileList.length" description="暂无文件" :image-size="80" />
|
<el-empty v-if="!filterLibraryFileList.length" description="暂无文件" :image-size="80" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -30,12 +30,22 @@ import { Delete, Edit, Picture, Document } from '@element-plus/icons-vue'
|
|||||||
import { useLibraryStore } from '@/stores/library.ts'
|
import { useLibraryStore } from '@/stores/library.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 { fromNow } from '@/utils/dayUtil.ts'
|
||||||
|
import { filesize } from 'filesize'
|
||||||
|
|
||||||
const libraryStore = useLibraryStore()
|
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) => {
|
const editClick = async (file: ILibraryFile) => {
|
||||||
await libraryStore.queryFileByList(file.id)
|
await libraryStore.queryFileById(file.id)
|
||||||
libraryStore.fileApiType = 'UPDATE'
|
libraryStore.fileApiType = 'UPDATE'
|
||||||
|
libraryStore.activeStep = 1
|
||||||
libraryStore.hasGenerateQuestion = true
|
libraryStore.hasGenerateQuestion = true
|
||||||
libraryStore.fileDialogVisible = true
|
libraryStore.fileDialogVisible = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,26 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLibraryStore } from '@/stores/library.ts'
|
import { useLibraryStore } from '@/stores/library.ts'
|
||||||
import { getModuleOptions } from '@/utils/library.ts'
|
import { getModuleOptions } from '@/utils/libraryUtil.ts'
|
||||||
|
|
||||||
const libraryStore = useLibraryStore()
|
const libraryStore = useLibraryStore()
|
||||||
|
|
||||||
const handleSubjectEvent = (value: string): void => {
|
const handleSubjectEvent = (value: string): void => {
|
||||||
libraryStore.moduleOptions = getModuleOptions(libraryStore.subjectTree, value)
|
libraryStore.moduleOptions = getModuleOptions(libraryStore.subjectTree, value)
|
||||||
|
libraryStore.selectedModule = libraryStore.moduleOptions[0].value
|
||||||
}
|
}
|
||||||
</script>
|
</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'
|
} from '@/apis/library.ts'
|
||||||
import type { IHandleApi, IOptions, ITreeNode } from '@/types'
|
import type { IHandleApi, IOptions, ITreeNode } from '@/types'
|
||||||
import { ElMessage } from 'element-plus'
|
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', {
|
export const useLibraryStore = defineStore('library', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
selectedSubject: '',
|
selectedSubject: '' as number | string,
|
||||||
selectedModule: '',
|
selectedModule: '' as number | string,
|
||||||
subjectTree: [] as ITreeNode[],
|
subjectTree: [] as ITreeNode[],
|
||||||
fileList: [] as ILibraryFile[],
|
fileList: [] as ILibraryFile[],
|
||||||
currentFile: {} as ILibraryFile,
|
currentFile: {
|
||||||
|
id: 0,
|
||||||
|
subject: '',
|
||||||
|
module: '',
|
||||||
|
name: '',
|
||||||
|
size: 0,
|
||||||
|
uploadTime: '',
|
||||||
|
type: '',
|
||||||
|
content: '',
|
||||||
|
questions: []
|
||||||
|
} as ILibraryFile,
|
||||||
fileDetailDialogVisible: false,
|
fileDetailDialogVisible: false,
|
||||||
fileDialogVisible: false,
|
fileDialogVisible: false,
|
||||||
hasGenerateQuestion: false,
|
hasGenerateQuestion: false,
|
||||||
fileApiType: 'ADD' as IHandleApi,
|
fileApiType: 'ADD' as IHandleApi,
|
||||||
subjectOptions: [] as IOptions[],
|
subjectOptions: [] as IOptions[],
|
||||||
moduleOptions: [] as IOptions[]
|
moduleOptions: [] as IOptions[],
|
||||||
|
activeStep: 1,
|
||||||
|
questionContent: ''
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async queryFileList() {
|
async queryFileList() {
|
||||||
@@ -30,11 +42,11 @@ export const useLibraryStore = defineStore('library', {
|
|||||||
this.fileList = await queryLibraryFileListApi()
|
this.fileList = await queryLibraryFileListApi()
|
||||||
this.subjectTree = getSubjectTree(this.fileList)
|
this.subjectTree = getSubjectTree(this.fileList)
|
||||||
this.subjectOptions = getSubjectOptions(this.subjectTree)
|
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.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)
|
this.currentFile = await queryLibraryFileByIdApi(id)
|
||||||
},
|
},
|
||||||
async handleFileApi(file: ILibraryFile) {
|
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 {
|
.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 {
|
.common-dialog {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface ILibraryFileQuestion {
|
|||||||
question: string;
|
question: string;
|
||||||
options: string[];
|
options: string[];
|
||||||
answer: string[];
|
answer: string[];
|
||||||
|
select: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILibraryFile {
|
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 }))
|
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)
|
const node = subjectTree.find((s) => s.name === subject)
|
||||||
return (node?.children || []).map((m) => ({
|
return (node?.children || []).map((m) => ({
|
||||||
label: m.name,
|
label: m.name,
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="library-view">
|
<div class="library-view common-view">
|
||||||
<div class="page-header">
|
<div class="view-header">
|
||||||
<div>
|
<div class="view-content">
|
||||||
<h2 class="page-title">知识库</h2>
|
<h2 class="view-title">知识库</h2>
|
||||||
<p class="page-subtitle">管理学习资料,查看 AI 解析结果</p>
|
<p class="view-subtitle">管理学习资料,查看 AI 解析结果</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" :icon="Plus" @click="addFileClick">新增资料</el-button>
|
<el-button type="primary" :icon="Plus" @click="addFileClick">新增资料</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,49 +30,15 @@ onMounted(async () => {
|
|||||||
const addFileClick = () => {
|
const addFileClick = () => {
|
||||||
libraryStore.currentFile = libraryStore.getEmptyFile()
|
libraryStore.currentFile = libraryStore.getEmptyFile()
|
||||||
libraryStore.fileApiType = 'ADD'
|
libraryStore.fileApiType = 'ADD'
|
||||||
|
libraryStore.activeStep = 1
|
||||||
libraryStore.hasGenerateQuestion = false
|
libraryStore.hasGenerateQuestion = false
|
||||||
|
libraryStore.questionContent = ''
|
||||||
libraryStore.fileDialogVisible = true
|
libraryStore.fileDialogVisible = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.library-view {
|
.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 {
|
.file-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,11 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="mistake-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>
|
||||||
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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>
|
</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>
|
<template>
|
||||||
<div>
|
<div class="practice-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>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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>
|
</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