feat:封装axios接口

This commit is contained in:
2026-07-28 22:57:45 +08:00
parent efa27f126d
commit 1f88341019
13 changed files with 306 additions and 277 deletions

3
src/apis/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import createService from '@/utils/axiosUtil.ts'
export const cloudService = createService('', 'study')

40
src/apis/library.ts Normal file
View 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'
})

View File

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

View File

@@ -14,17 +14,21 @@
:rules="rules" label-width="auto">
<el-form-item label="学科" prop="subject">
<el-select v-model="libraryStore.currentFile.subject"
filterable allow-create placeholder="请输入或选择学科名称">
<el-option v-for="(item, index) in subjectOptions"
:key="index" :label="item" :value="item"/>
filterable allow-create
@change="handleSubjectEvent"
placeholder="请输入或选择学科名称">
<el-option v-for="(item, index) in libraryStore.subjectOptions"
:key="index" :label="item.label" :value="item.value"/>
</el-select>
</el-form-item>
<el-form-item label="模块" prop="module">
<el-select v-model="libraryStore.currentFile.module"
filterable allow-create placeholder="请输入或选择模块名称">
<el-option v-for="(item, index) in subjectOptions"
:key="index" :label="item" :value="item"/>
filterable allow-create
:disabled="!libraryStore.currentFile.subject"
placeholder="请输入或选择模块名称">
<el-option v-for="(item, index) in libraryStore.moduleOptions"
:key="index" :label="item.label" :value="item.value"/>
</el-select>
</el-form-item>
@@ -50,14 +54,16 @@
</el-form-item>
</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>
<el-button type="primary" @click="generate">生成题库</el-button>
<el-button type="primary" @click="generateQuestion">生成题库</el-button>
</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"
:key="q.id" class="question-card">
<div class="question-title">
@@ -102,19 +108,15 @@ import { ref } from 'vue'
import { useLibraryStore } from '@/stores/library.ts'
import { ElLoading, ElMessage, type FormInstance, type UploadRequestOptions } from 'element-plus'
import { Document, MagicStick, Upload, UploadFilled } from '@element-plus/icons-vue'
import axios from 'axios'
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'
const libraryStore = useLibraryStore()
const libraryFileFormRef = ref<FormInstance>()
const activeStep = ref(1)
const hasGenerate = ref(false)
const subjectOptions = ['中医学', '公务员']
const fileContent = ref<string>()
const questionContent = ref<string>()
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 { file } = options
libraryStore.currentFile.size = file.size
@@ -165,13 +171,8 @@ const upload = async (options: UploadRequestOptions) => {
})
try {
const res = await axios.post('http://127.0.0.1:8000/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
libraryStore.currentFile.content = await uploadLibraryFileApi(formData)
ElMessage.success('上传成功')
fileContent.value = res.data.text
} catch (error) {
console.log(error)
ElMessage.error('上传失败 请联系管理员')
@@ -180,14 +181,14 @@ const upload = async (options: UploadRequestOptions) => {
}
}
const generate = async () => {
if (!fileContent.value) {
const generateQuestion = async () => {
if (!libraryStore.currentFile.content) {
ElMessage.error('请先上传资料')
return
}
questionContent.value = ''
hasGenerate.value = false
libraryStore.hasGenerateQuestion = false
const loading = ElLoading.service({
lock: true,
@@ -196,13 +197,13 @@ const generate = async () => {
})
try {
const response = await fetch('http://127.0.0.1:8000/generate', {
const response = await fetch('/study-api/library/question', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: fileContent.value
message: libraryStore.currentFile.content
})
})
@@ -220,7 +221,7 @@ const generate = async () => {
}
ElMessage.success('生成完成')
hasGenerate.value = true
libraryStore.hasGenerateQuestion = true
libraryStore.currentFile.questions = JSON.parse(questionContent.value)
} catch (error) {
@@ -233,11 +234,7 @@ const generate = async () => {
const submitForm = async () => {
if (libraryStore.currentFile.questions.length > 0) {
const res = await axios.post('http://127.0.0.1:8000/files', libraryStore.currentFile, {
headers: {
'Content-Type': 'application/json'
}
})
await libraryStore.handleFileApi(libraryStore.currentFile)
} else {
ElMessage.error('请先生成题库')
}

View File

@@ -1,9 +1,11 @@
<template>
<div class="file-list">
<div v-for="file in libraryStore.fileList" :key="file.id" class="file-card card-item">
<div class="file-info" @click="viewDetailClick(file)">
<library-filter v-if="libraryStore.fileList.length"/>
<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-else><Document /></el-icon>
</div>
@@ -14,12 +16,12 @@
</div>
<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>
</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>
</template>
@@ -27,11 +29,14 @@
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'
const libraryStore = useLibraryStore()
const viewDetailClick = (file: ILibraryFile) => {
libraryStore.currentFile = file
libraryStore.fileDetailDialogVisible = true
const editClick = async (file: ILibraryFile) => {
await libraryStore.queryFileByList(file.id)
libraryStore.fileApiType = 'UPDATE'
libraryStore.hasGenerateQuestion = true
libraryStore.fileDialogVisible = true
}
</script>

View File

@@ -2,30 +2,24 @@
<div class="filter-bar card-item">
<div class="filter-row">
<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 class="filter-row">
<span class="filter-label">模块</span>
<el-segmented v-model="libraryStore.selectedModule" :options="moduleOptions"/>
<el-segmented v-model="libraryStore.selectedModule" :options="libraryStore.moduleOptions"/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useLibraryStore } from '@/stores/library.ts'
import { getModuleOptions } from '@/utils/library.ts'
const libraryStore = useLibraryStore()
const subjectOptions = computed(() => {
return libraryStore.subjectTree.map((s) => ({ label: s.name, value: s.id }))
})
const moduleOptions = computed(() => {
const node = libraryStore.subjectTree.find((s) => s.id === libraryStore.selectedSubject)
return (node?.children || []).map((m) => ({
label: m.name,
value: m.id
}))
})
const handleSubjectEvent = (value: string): void => {
libraryStore.moduleOptions = getModuleOptions(libraryStore.subjectTree, value)
}
</script>

View File

@@ -1,82 +1,60 @@
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', {
state: () => ({
selectedSubject: 1,
selectedModule: 101,
subjectTree: [
{
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[],
selectedSubject: '',
selectedModule: '',
subjectTree: [] as ITreeNode[],
fileList: [] as ILibraryFile[],
currentFile: {} as ILibraryFile,
fileDetailDialogVisible: false,
fileDialogVisible: false
fileDialogVisible: false,
hasGenerateQuestion: false,
fileApiType: 'ADD' as IHandleApi,
subjectOptions: [] as IOptions[],
moduleOptions: [] as IOptions[]
}),
actions: {
queryFileList() {
this.fileList = [
{
id: 1,
subject: '中医学',
module: '中医基础理疗',
name: '向量代数.pdf',
size: 2.4,
uploadTime: '2分钟前',
type: 'pdf',
questions: [
{ 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'] }
]
},
{
id: 2,
subject: '中医学',
module: '中医基础理疗',
name: '向量代数精讲.png',
size: 156,
uploadTime: '3小时前',
type: 'image',
questions: []
}
]
async queryFileList() {
this.fileList = []
this.fileList = await queryLibraryFileListApi()
this.subjectTree = getSubjectTree(this.fileList)
this.subjectOptions = getSubjectOptions(this.subjectTree)
this.selectedSubject = this.subjectOptions[0].label
this.moduleOptions = getModuleOptions(this.subjectTree, this.selectedSubject)
this.selectedModule = this.moduleOptions[0].label
},
async queryFileByList(id: number) {
this.currentFile = await queryLibraryFileByIdApi(id)
},
async handleFileApi(file: ILibraryFile) {
switch (this.fileApiType) {
case 'ADD':
await addLibraryFileApi(file)
ElMessage.success('新增资料成功')
break
case 'UPDATE':
await updateLibraryFileApi(file.id as number, file)
ElMessage.success('更新资料成功')
break
case 'DELETE':
ElMessage.success('删除资料成功')
break
default:
break
}
this.fileDialogVisible = false
await this.queryFileList()
},
getEmptyFile(): ILibraryFile {
return {
@@ -87,6 +65,7 @@ export const useLibraryStore = defineStore('library', {
subject: '',
uploadTime: '',
type: '',
content: '',
questions: []
}
}

12
src/types/index.ts Normal file
View 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;
}

View File

@@ -1,9 +1,3 @@
export interface ITreeNode {
id: number;
name: string;
children?: ITreeNode[];
}
export interface ILibraryFileQuestion {
id: number;
type: string;
@@ -20,5 +14,6 @@ export interface ILibraryFile {
size: number;
uploadTime: string;
type: string;
content: string;
questions: ILibraryFileQuestion[];
}

91
src/utils/axiosUtil.ts Normal file
View 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
View 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
}))
}

View File

@@ -1,6 +1,5 @@
<template>
<div class="library-view">
<!-- 标题行 -->
<div class="page-header">
<div>
<h2 class="page-title">知识库</h2>
@@ -9,33 +8,29 @@
<el-button type="primary" :icon="Plus" @click="addFileClick">新增资料</el-button>
</div>
<library-filter />
<library-file-list />
<library-file-dialog />
<library-file-detail-dialog />
</div>
</template>
<script setup lang="ts">
import LibraryFilter from '@/components/Library/LibraryFilter.vue'
import LibraryFileList from '@/components/Library/LibraryFileList.vue'
import LibraryFileDialog from '@/components/Library/LibraryFileDialog.vue'
import LibraryFileDetailDialog from '@/components/Library/LibraryFileDetailDialog.vue'
import { useLibraryStore } from '@/stores/library.ts'
import { Plus } from '@element-plus/icons-vue'
import { onMounted } from 'vue'
const libraryStore = useLibraryStore()
onMounted(() => {
libraryStore.queryFileList()
onMounted(async () => {
await libraryStore.queryFileList()
})
const addFileClick = () => {
libraryStore.currentFile = libraryStore.getEmptyFile()
libraryStore.fileApiType = 'ADD'
libraryStore.hasGenerateQuestion = false
libraryStore.fileDialogVisible = true
}
</script>

View File

@@ -21,10 +21,12 @@ export default defineConfig({
}
},
server: {
port: 5174,
// 服务器代理 服务器没有部署Nginx的情况下使用
proxy: {
'/study-api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
rewrite: (item) => item.replace(/^\/study-api/, '')
}
}
}
})