feat:封装axios接口
This commit is contained in:
3
src/apis/index.ts
Normal file
3
src/apis/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import createService from '@/utils/axiosUtil.ts'
|
||||||
|
|
||||||
|
export const cloudService = createService('', 'study')
|
||||||
40
src/apis/library.ts
Normal file
40
src/apis/library.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { cloudService } from './index'
|
||||||
|
import type { ILibraryFile } from '@/types/library.ts'
|
||||||
|
|
||||||
|
export const uploadLibraryFileApi = (formData: FormData): Promise<string> =>
|
||||||
|
cloudService({
|
||||||
|
url: '/study-api/library/upload',
|
||||||
|
method: 'post',
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addLibraryFileApi = (file: ILibraryFile): Promise<boolean> =>
|
||||||
|
cloudService({
|
||||||
|
url: '/study-api/library/files',
|
||||||
|
method: 'post',
|
||||||
|
data: file
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateLibraryFileApi = (id: number, file: ILibraryFile): Promise<boolean> =>
|
||||||
|
cloudService({
|
||||||
|
url: `/study-api/library/files/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: file
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const queryLibraryFileListApi = (): Promise<ILibraryFile[]> =>
|
||||||
|
cloudService({
|
||||||
|
url: '/study-api/library/files',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const queryLibraryFileByIdApi = (id: number): Promise<ILibraryFile> =>
|
||||||
|
cloudService({
|
||||||
|
url: `/study-api/library/files/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog v-model="libraryStore.fileDetailDialogVisible"
|
|
||||||
:title="libraryStore.currentFile.name" center
|
|
||||||
width="90%" destroy-on-close class="file-detail-dialog">
|
|
||||||
<el-splitter>
|
|
||||||
<el-splitter-panel size="50%">
|
|
||||||
<div class="raw-file">
|
|
||||||
<div v-if="libraryStore.currentFile.type === 'pdf'" class="preview">
|
|
||||||
<p>原文预览</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="preview-box">
|
|
||||||
<p>原文预览</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-splitter-panel>
|
|
||||||
<el-splitter-panel size="50%">
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-title">
|
|
||||||
<el-icon><EditPen /></el-icon>
|
|
||||||
生成题目({{ libraryStore.currentFile.questions?.length || 0 }} 道)
|
|
||||||
</div>
|
|
||||||
<div v-for="(q, i) in libraryStore.currentFile.questions" :key="i" class="question-card">
|
|
||||||
<div class="q-header">
|
|
||||||
<span class="q-num">Q{{ i + 1 }}</span>
|
|
||||||
<el-tag size="small" type="primary" effect="dark">{{ q.type }}</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="q-body">{{ q.question }}</div>
|
|
||||||
<ul v-if="q.options" class="q-options">
|
|
||||||
<li v-for="opt in q.options" :key="opt">{{ opt }}</li>
|
|
||||||
</ul>
|
|
||||||
<div class="q-answer">
|
|
||||||
<span class="label">答案:</span>{{ q.answer }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-splitter-panel>
|
|
||||||
</el-splitter>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="libraryStore.fileDetailDialogVisible = false">关闭</el-button>
|
|
||||||
<el-button type="primary" @click="libraryStore.fileDetailDialogVisible = false">开始练习</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { EditPen } from '@element-plus/icons-vue'
|
|
||||||
import { useLibraryStore } from '@/stores/library.ts'
|
|
||||||
|
|
||||||
const libraryStore = useLibraryStore()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.file-detail-dialog {
|
|
||||||
.raw-file {
|
|
||||||
height: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
.preview {
|
|
||||||
height: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 2px dashed #e2e8f0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 知识点 + 题目 */
|
|
||||||
.section {
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-card {
|
|
||||||
background: #f8fafc;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
|
|
||||||
.q-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.q-num {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #14b8a6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-body {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-options {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding: 3px 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-answer {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #16a34a;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: #64748b;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -14,17 +14,21 @@
|
|||||||
:rules="rules" label-width="auto">
|
:rules="rules" label-width="auto">
|
||||||
<el-form-item label="学科" prop="subject">
|
<el-form-item label="学科" prop="subject">
|
||||||
<el-select v-model="libraryStore.currentFile.subject"
|
<el-select v-model="libraryStore.currentFile.subject"
|
||||||
filterable allow-create placeholder="请输入或选择学科名称">
|
filterable allow-create
|
||||||
<el-option v-for="(item, index) in subjectOptions"
|
@change="handleSubjectEvent"
|
||||||
:key="index" :label="item" :value="item"/>
|
placeholder="请输入或选择学科名称">
|
||||||
|
<el-option v-for="(item, index) in libraryStore.subjectOptions"
|
||||||
|
:key="index" :label="item.label" :value="item.value"/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="模块" prop="module">
|
<el-form-item label="模块" prop="module">
|
||||||
<el-select v-model="libraryStore.currentFile.module"
|
<el-select v-model="libraryStore.currentFile.module"
|
||||||
filterable allow-create placeholder="请输入或选择模块名称">
|
filterable allow-create
|
||||||
<el-option v-for="(item, index) in subjectOptions"
|
:disabled="!libraryStore.currentFile.subject"
|
||||||
:key="index" :label="item" :value="item"/>
|
placeholder="请输入或选择模块名称">
|
||||||
|
<el-option v-for="(item, index) in libraryStore.moduleOptions"
|
||||||
|
:key="index" :label="item.label" :value="item.value"/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -50,14 +54,16 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<QuillEditor v-if="activeStep === 2" :content="fileContent" contentType="html" theme="snow"/>
|
<QuillEditor v-if="activeStep === 2"
|
||||||
|
:content="libraryStore.currentFile.content"
|
||||||
|
contentType="html" theme="snow"/>
|
||||||
|
|
||||||
<div v-if="activeStep === 3" class="generate-question">
|
<div v-if="activeStep === 3" class="generate-question">
|
||||||
<div>
|
<div>
|
||||||
<el-button type="primary" @click="generate">生成题库</el-button>
|
<el-button type="primary" @click="generateQuestion">生成题库</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasGenerate" class="question-list">
|
<div v-if="libraryStore.hasGenerateQuestion" class="question-list">
|
||||||
<el-card v-for="(q, index) in libraryStore.currentFile.questions"
|
<el-card v-for="(q, index) in libraryStore.currentFile.questions"
|
||||||
:key="q.id" class="question-card">
|
:key="q.id" class="question-card">
|
||||||
<div class="question-title">
|
<div class="question-title">
|
||||||
@@ -102,19 +108,15 @@ import { ref } from 'vue'
|
|||||||
import { useLibraryStore } from '@/stores/library.ts'
|
import { useLibraryStore } from '@/stores/library.ts'
|
||||||
import { ElLoading, ElMessage, type FormInstance, type UploadRequestOptions } from 'element-plus'
|
import { ElLoading, ElMessage, type FormInstance, type UploadRequestOptions } from 'element-plus'
|
||||||
import { Document, MagicStick, Upload, UploadFilled } from '@element-plus/icons-vue'
|
import { Document, MagicStick, Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||||
import axios from 'axios'
|
|
||||||
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 { getModuleOptions } from '@/utils/library.ts'
|
||||||
|
|
||||||
const libraryStore = useLibraryStore()
|
const libraryStore = useLibraryStore()
|
||||||
|
|
||||||
const libraryFileFormRef = ref<FormInstance>()
|
const libraryFileFormRef = ref<FormInstance>()
|
||||||
const activeStep = ref(1)
|
const activeStep = ref(1)
|
||||||
const hasGenerate = ref(false)
|
|
||||||
|
|
||||||
const subjectOptions = ['中医学', '公务员']
|
|
||||||
|
|
||||||
const fileContent = ref<string>()
|
|
||||||
const questionContent = ref<string>()
|
const questionContent = ref<string>()
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -150,6 +152,10 @@ const nextClick = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubjectEvent = (value: string) => {
|
||||||
|
libraryStore.moduleOptions = getModuleOptions(libraryStore.subjectTree, value)
|
||||||
|
}
|
||||||
|
|
||||||
const upload = async (options: UploadRequestOptions) => {
|
const upload = async (options: UploadRequestOptions) => {
|
||||||
const { file } = options
|
const { file } = options
|
||||||
libraryStore.currentFile.size = file.size
|
libraryStore.currentFile.size = file.size
|
||||||
@@ -165,13 +171,8 @@ const upload = async (options: UploadRequestOptions) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post('http://127.0.0.1:8000/upload', formData, {
|
libraryStore.currentFile.content = await uploadLibraryFileApi(formData)
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ElMessage.success('上传成功')
|
ElMessage.success('上传成功')
|
||||||
fileContent.value = res.data.text
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
ElMessage.error('上传失败 请联系管理员')
|
ElMessage.error('上传失败 请联系管理员')
|
||||||
@@ -180,14 +181,14 @@ const upload = async (options: UploadRequestOptions) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generate = async () => {
|
const generateQuestion = async () => {
|
||||||
if (!fileContent.value) {
|
if (!libraryStore.currentFile.content) {
|
||||||
ElMessage.error('请先上传资料')
|
ElMessage.error('请先上传资料')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
questionContent.value = ''
|
questionContent.value = ''
|
||||||
hasGenerate.value = false
|
libraryStore.hasGenerateQuestion = false
|
||||||
|
|
||||||
const loading = ElLoading.service({
|
const loading = ElLoading.service({
|
||||||
lock: true,
|
lock: true,
|
||||||
@@ -196,13 +197,13 @@ const generate = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:8000/generate', {
|
const response = await fetch('/study-api/library/question', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: fileContent.value
|
message: libraryStore.currentFile.content
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -220,7 +221,7 @@ const generate = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success('生成完成')
|
ElMessage.success('生成完成')
|
||||||
hasGenerate.value = true
|
libraryStore.hasGenerateQuestion = true
|
||||||
|
|
||||||
libraryStore.currentFile.questions = JSON.parse(questionContent.value)
|
libraryStore.currentFile.questions = JSON.parse(questionContent.value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -233,11 +234,7 @@ const generate = async () => {
|
|||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
if (libraryStore.currentFile.questions.length > 0) {
|
if (libraryStore.currentFile.questions.length > 0) {
|
||||||
const res = await axios.post('http://127.0.0.1:8000/files', libraryStore.currentFile, {
|
await libraryStore.handleFileApi(libraryStore.currentFile)
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('请先生成题库')
|
ElMessage.error('请先生成题库')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="file-list">
|
<div class="file-list">
|
||||||
<div v-for="file in libraryStore.fileList" :key="file.id" class="file-card card-item">
|
<library-filter v-if="libraryStore.fileList.length"/>
|
||||||
<div class="file-info" @click="viewDetailClick(file)">
|
|
||||||
|
|
||||||
<div class="file-icon" :class="file.iconClass">
|
<div v-for="file in libraryStore.fileList" :key="file.id" class="file-card card-item">
|
||||||
|
<div class="file-info">
|
||||||
|
|
||||||
|
<div class="file-icon" :class="file.type === 'image' ? 'icon-image' : 'icon-pdf'">
|
||||||
<el-icon v-if="file.type === 'image'" ><Picture /></el-icon>
|
<el-icon v-if="file.type === 'image'" ><Picture /></el-icon>
|
||||||
<el-icon v-else><Document /></el-icon>
|
<el-icon v-else><Document /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,12 +16,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<el-button type="primary" :icon="Edit">编辑</el-button>
|
<el-button type="primary" :icon="Edit" @click="editClick(file)">编辑</el-button>
|
||||||
<el-button type="danger" :icon="Delete">删除</el-button>
|
<el-button type="danger" :icon="Delete">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-empty v-if="!libraryStore.fileList.length" description="该模块暂无文件" :image-size="80" />
|
<el-empty v-if="!libraryStore.fileList.length" description="暂无文件" :image-size="80" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -27,11 +29,14 @@
|
|||||||
import { Delete, Edit, Picture, Document } from '@element-plus/icons-vue'
|
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'
|
||||||
|
|
||||||
const libraryStore = useLibraryStore()
|
const libraryStore = useLibraryStore()
|
||||||
|
|
||||||
const viewDetailClick = (file: ILibraryFile) => {
|
const editClick = async (file: ILibraryFile) => {
|
||||||
libraryStore.currentFile = file
|
await libraryStore.queryFileByList(file.id)
|
||||||
libraryStore.fileDetailDialogVisible = true
|
libraryStore.fileApiType = 'UPDATE'
|
||||||
|
libraryStore.hasGenerateQuestion = true
|
||||||
|
libraryStore.fileDialogVisible = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,30 +2,24 @@
|
|||||||
<div class="filter-bar card-item">
|
<div class="filter-bar card-item">
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<span class="filter-label">学科</span>
|
<span class="filter-label">学科</span>
|
||||||
<el-segmented v-model="libraryStore.selectedSubject" :options="subjectOptions"/>
|
<el-segmented v-model="libraryStore.selectedSubject"
|
||||||
|
:options="libraryStore.subjectOptions"
|
||||||
|
@change="handleSubjectEvent"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<span class="filter-label">模块</span>
|
<span class="filter-label">模块</span>
|
||||||
<el-segmented v-model="libraryStore.selectedModule" :options="moduleOptions"/>
|
<el-segmented v-model="libraryStore.selectedModule" :options="libraryStore.moduleOptions"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useLibraryStore } from '@/stores/library.ts'
|
import { useLibraryStore } from '@/stores/library.ts'
|
||||||
|
import { getModuleOptions } from '@/utils/library.ts'
|
||||||
|
|
||||||
const libraryStore = useLibraryStore()
|
const libraryStore = useLibraryStore()
|
||||||
|
|
||||||
const subjectOptions = computed(() => {
|
const handleSubjectEvent = (value: string): void => {
|
||||||
return libraryStore.subjectTree.map((s) => ({ label: s.name, value: s.id }))
|
libraryStore.moduleOptions = getModuleOptions(libraryStore.subjectTree, value)
|
||||||
})
|
}
|
||||||
|
|
||||||
const moduleOptions = computed(() => {
|
|
||||||
const node = libraryStore.subjectTree.find((s) => s.id === libraryStore.selectedSubject)
|
|
||||||
return (node?.children || []).map((m) => ({
|
|
||||||
label: m.name,
|
|
||||||
value: m.id
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,82 +1,60 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { ILibraryFile, ITreeNode } from '@/types/library.ts'
|
import type { ILibraryFile } from '@/types/library.ts'
|
||||||
|
import {
|
||||||
|
addLibraryFileApi,
|
||||||
|
queryLibraryFileByIdApi,
|
||||||
|
queryLibraryFileListApi,
|
||||||
|
updateLibraryFileApi
|
||||||
|
} from '@/apis/library.ts'
|
||||||
|
import type { IHandleApi, IOptions, ITreeNode } from '@/types'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getModuleOptions, getSubjectOptions, getSubjectTree } from '@/utils/library.ts'
|
||||||
|
|
||||||
export const useLibraryStore = defineStore('library', {
|
export const useLibraryStore = defineStore('library', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
selectedSubject: 1,
|
selectedSubject: '',
|
||||||
selectedModule: 101,
|
selectedModule: '',
|
||||||
subjectTree: [
|
subjectTree: [] as ITreeNode[],
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: '中医学',
|
|
||||||
children: [
|
|
||||||
{ id: 101, name: '中医基础理论' },
|
|
||||||
{ id: 102, name: '中医诊断学' },
|
|
||||||
{ id: 103, name: '中药学' },
|
|
||||||
{ id: 104, name: '方剂学' },
|
|
||||||
{ id: 105, name: '中医内科学' },
|
|
||||||
{ id: 106, name: '中医外科学' },
|
|
||||||
{ id: 107, name: '中医妇科学' },
|
|
||||||
{ id: 108, name: '中医儿科学' },
|
|
||||||
{ id: 109, name: '针灸学' },
|
|
||||||
{ id: 110, name: '推拿学' },
|
|
||||||
{ id: 111, name: '中医骨伤科学' },
|
|
||||||
{ id: 112, name: '黄帝内经' },
|
|
||||||
{ id: 113, name: '伤寒论' },
|
|
||||||
{ id: 114, name: '金匮要略' },
|
|
||||||
{ id: 115, name: '温病学' },
|
|
||||||
{ id: 116, name: '中医养生学' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '公务员',
|
|
||||||
children: [
|
|
||||||
{ id: 201, name: '言语理解与表达' },
|
|
||||||
{ id: 202, name: '数量关系' },
|
|
||||||
{ id: 203, name: '判断推理' },
|
|
||||||
{ id: 204, name: '资料分析' },
|
|
||||||
{ id: 205, name: '常识判断' },
|
|
||||||
{ id: 206, name: '申论写作' },
|
|
||||||
{ id: 207, name: '公文写作' },
|
|
||||||
{ id: 208, name: '公共基础知识' },
|
|
||||||
{ id: 209, name: '面试技巧' },
|
|
||||||
{ id: 210, name: '时政热点' },
|
|
||||||
{ id: 211, name: '法律基础' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
] as ITreeNode[],
|
|
||||||
fileList: [] as ILibraryFile[],
|
fileList: [] as ILibraryFile[],
|
||||||
currentFile: {} as ILibraryFile,
|
currentFile: {} as ILibraryFile,
|
||||||
fileDetailDialogVisible: false,
|
fileDetailDialogVisible: false,
|
||||||
fileDialogVisible: false
|
fileDialogVisible: false,
|
||||||
|
hasGenerateQuestion: false,
|
||||||
|
fileApiType: 'ADD' as IHandleApi,
|
||||||
|
subjectOptions: [] as IOptions[],
|
||||||
|
moduleOptions: [] as IOptions[]
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
queryFileList() {
|
async queryFileList() {
|
||||||
this.fileList = [
|
this.fileList = []
|
||||||
{
|
this.fileList = await queryLibraryFileListApi()
|
||||||
id: 1,
|
this.subjectTree = getSubjectTree(this.fileList)
|
||||||
subject: '中医学',
|
this.subjectOptions = getSubjectOptions(this.subjectTree)
|
||||||
module: '中医基础理疗',
|
this.selectedSubject = this.subjectOptions[0].label
|
||||||
name: '向量代数.pdf',
|
this.moduleOptions = getModuleOptions(this.subjectTree, this.selectedSubject)
|
||||||
size: 2.4,
|
this.selectedModule = this.moduleOptions[0].label
|
||||||
uploadTime: '2分钟前',
|
},
|
||||||
type: 'pdf',
|
async queryFileByList(id: number) {
|
||||||
questions: [
|
this.currentFile = await queryLibraryFileByIdApi(id)
|
||||||
{ id: 1, type: 'single', question: '设 a=(1,2,3), b=(2,-1,1),求 a·b = ?', options: ['A. 3', 'B. 5', 'C. 7', 'D. 9'], answer: ['A'] }
|
},
|
||||||
]
|
async handleFileApi(file: ILibraryFile) {
|
||||||
},
|
switch (this.fileApiType) {
|
||||||
{
|
case 'ADD':
|
||||||
id: 2,
|
await addLibraryFileApi(file)
|
||||||
subject: '中医学',
|
ElMessage.success('新增资料成功')
|
||||||
module: '中医基础理疗',
|
break
|
||||||
name: '向量代数精讲.png',
|
case 'UPDATE':
|
||||||
size: 156,
|
await updateLibraryFileApi(file.id as number, file)
|
||||||
uploadTime: '3小时前',
|
ElMessage.success('更新资料成功')
|
||||||
type: 'image',
|
break
|
||||||
questions: []
|
case 'DELETE':
|
||||||
}
|
ElMessage.success('删除资料成功')
|
||||||
]
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.fileDialogVisible = false
|
||||||
|
await this.queryFileList()
|
||||||
},
|
},
|
||||||
getEmptyFile(): ILibraryFile {
|
getEmptyFile(): ILibraryFile {
|
||||||
return {
|
return {
|
||||||
@@ -87,6 +65,7 @@ export const useLibraryStore = defineStore('library', {
|
|||||||
subject: '',
|
subject: '',
|
||||||
uploadTime: '',
|
uploadTime: '',
|
||||||
type: '',
|
type: '',
|
||||||
|
content: '',
|
||||||
questions: []
|
questions: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/types/index.ts
Normal file
12
src/types/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export type IHandleApi = 'ADD' | 'UPDATE' | 'DELETE';
|
||||||
|
|
||||||
|
export interface ITreeNode {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
children?: ITreeNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOptions {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
}
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
export interface ITreeNode {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
children?: ITreeNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILibraryFileQuestion {
|
export interface ILibraryFileQuestion {
|
||||||
id: number;
|
id: number;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -20,5 +14,6 @@ export interface ILibraryFile {
|
|||||||
size: number;
|
size: number;
|
||||||
uploadTime: string;
|
uploadTime: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
content: string;
|
||||||
questions: ILibraryFileQuestion[];
|
questions: ILibraryFileQuestion[];
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/utils/axiosUtil.ts
Normal file
91
src/utils/axiosUtil.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import axios, { AxiosError } from 'axios'
|
||||||
|
import type { InternalAxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const createService = (baseURL = '', prefix = '', timeout = 5000) => {
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL,
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config: InternalAxiosRequestConfig) => {
|
||||||
|
const token = localStorage.getItem(prefix ? `${prefix}-token` : 'token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.satoken = prefix ? `${prefix} ${token}` : token
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
|
||||||
|
(error: AxiosError) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
(error: AxiosError) => {
|
||||||
|
const { code, message } = error
|
||||||
|
if (error.response) {
|
||||||
|
// 发送请求成功 并收到服务器的响应
|
||||||
|
handleServiceError(error.response, message)
|
||||||
|
return Promise.reject(error)
|
||||||
|
} else if (error.request) {
|
||||||
|
// 发送请求成功 未收到服务器的响应
|
||||||
|
handleHttpError(code, message)
|
||||||
|
return Promise.reject(error)
|
||||||
|
} else {
|
||||||
|
// 其他错误情况 如发送请求失败等
|
||||||
|
ElMessage.error(`前端错误 请联系管理人员:${message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理服务器响应错误
|
||||||
|
* @param response 服务器响应
|
||||||
|
* @param message 错误消息
|
||||||
|
*/
|
||||||
|
const handleServiceError = (response: AxiosResponse, message: string) => {
|
||||||
|
const { status, data, statusText } = response
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
ElMessage.error('账号已过期 请重新登录')
|
||||||
|
window.location.href = '/'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
ElMessage.error(`${data.message || statusText}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
ElMessage.error(`服务器错误 请联系管理人员:${status}:${message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理Http错误
|
||||||
|
* @param code code码
|
||||||
|
* @param message 消息
|
||||||
|
*/
|
||||||
|
const handleHttpError = (code: string | undefined, message: string) => {
|
||||||
|
switch (code) {
|
||||||
|
case 'ECONNABORTED':
|
||||||
|
case 'ETIMEDOUT':
|
||||||
|
ElMessage.error('网络连接错误 请联系管理人员')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
ElMessage.error(`网络错误 请联系管理人员:${code}:${message}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createService
|
||||||
51
src/utils/library.ts
Normal file
51
src/utils/library.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { ILibraryFile } from '@/types/library.ts'
|
||||||
|
import type { IOptions, ITreeNode } from '@/types'
|
||||||
|
|
||||||
|
export const getSubjectTree = (files: ILibraryFile[]): ITreeNode[] => {
|
||||||
|
const subjectMap = new Map<number, ITreeNode>()
|
||||||
|
let subjectIdCounter = 1
|
||||||
|
let moduleIdCounter = 1000
|
||||||
|
|
||||||
|
files.forEach((f) => {
|
||||||
|
// 查找是否已存在该学科节点
|
||||||
|
let subjectNode = Array.from(subjectMap.values()).find(
|
||||||
|
(node) => node.name === f.subject
|
||||||
|
)
|
||||||
|
|
||||||
|
// 不存在则创建学科节点
|
||||||
|
if (!subjectNode) {
|
||||||
|
subjectNode = {
|
||||||
|
id: subjectIdCounter++,
|
||||||
|
name: f.subject,
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
subjectMap.set(subjectNode.id, subjectNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 避免重复模块
|
||||||
|
const exists = subjectNode.children?.some(
|
||||||
|
(c) => c.name === f.module
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
subjectNode.children!.push({
|
||||||
|
id: moduleIdCounter++,
|
||||||
|
name: f.module
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(subjectMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSubjectOptions = (subjectTree: ITreeNode[]): IOptions[] => {
|
||||||
|
return subjectTree.map((s) => ({ label: s.name, value: s.name }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getModuleOptions = (subjectTree: ITreeNode[], subject: string): IOptions[] => {
|
||||||
|
const node = subjectTree.find((s) => s.name === subject)
|
||||||
|
return (node?.children || []).map((m) => ({
|
||||||
|
label: m.name,
|
||||||
|
value: m.name
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="library-view">
|
<div class="library-view">
|
||||||
<!-- 标题行 -->
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="page-title">知识库</h2>
|
<h2 class="page-title">知识库</h2>
|
||||||
@@ -9,33 +8,29 @@
|
|||||||
<el-button type="primary" :icon="Plus" @click="addFileClick">新增资料</el-button>
|
<el-button type="primary" :icon="Plus" @click="addFileClick">新增资料</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<library-filter />
|
|
||||||
|
|
||||||
<library-file-list />
|
<library-file-list />
|
||||||
|
|
||||||
<library-file-dialog />
|
<library-file-dialog />
|
||||||
|
|
||||||
<library-file-detail-dialog />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LibraryFilter from '@/components/Library/LibraryFilter.vue'
|
|
||||||
import LibraryFileList from '@/components/Library/LibraryFileList.vue'
|
import LibraryFileList from '@/components/Library/LibraryFileList.vue'
|
||||||
import LibraryFileDialog from '@/components/Library/LibraryFileDialog.vue'
|
import LibraryFileDialog from '@/components/Library/LibraryFileDialog.vue'
|
||||||
import LibraryFileDetailDialog from '@/components/Library/LibraryFileDetailDialog.vue'
|
|
||||||
import { useLibraryStore } from '@/stores/library.ts'
|
import { useLibraryStore } from '@/stores/library.ts'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
const libraryStore = useLibraryStore()
|
const libraryStore = useLibraryStore()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
libraryStore.queryFileList()
|
await libraryStore.queryFileList()
|
||||||
})
|
})
|
||||||
|
|
||||||
const addFileClick = () => {
|
const addFileClick = () => {
|
||||||
libraryStore.currentFile = libraryStore.getEmptyFile()
|
libraryStore.currentFile = libraryStore.getEmptyFile()
|
||||||
|
libraryStore.fileApiType = 'ADD'
|
||||||
|
libraryStore.hasGenerateQuestion = false
|
||||||
libraryStore.fileDialogVisible = true
|
libraryStore.fileDialogVisible = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5174,
|
|
||||||
// 服务器代理 服务器没有部署Nginx的情况下使用
|
|
||||||
proxy: {
|
proxy: {
|
||||||
|
'/study-api': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (item) => item.replace(/^\/study-api/, '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user