完成后台的试题管理
This commit is contained in:
parent
3a05219d2e
commit
7a82c98d00
@ -9,7 +9,6 @@
|
|||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -293,6 +293,19 @@ async function updateChoiceQuestion(id, choiceData) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除选择题问题
|
||||||
|
* @param {number} id - 选择题ID
|
||||||
|
* @returns {Promise<boolean>} 是否删除成功
|
||||||
|
*/
|
||||||
|
async function deleteChoiceQuestion(id) {
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const result = await db.runAsync('DELETE FROM question_choices WHERE id = ?', [id]);
|
||||||
|
return result.changes > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加填空题问题
|
* 添加填空题问题
|
||||||
* @param {Object} fillBlankData - 填空题数据
|
* @param {Object} fillBlankData - 填空题数据
|
||||||
@ -415,6 +428,7 @@ module.exports = {
|
|||||||
updateQuestionDescription,
|
updateQuestionDescription,
|
||||||
addChoiceQuestion,
|
addChoiceQuestion,
|
||||||
updateChoiceQuestion,
|
updateChoiceQuestion,
|
||||||
|
deleteChoiceQuestion,
|
||||||
addFillBlankQuestion,
|
addFillBlankQuestion,
|
||||||
updateFillBlankQuestion,
|
updateFillBlankQuestion,
|
||||||
deleteFillBlankQuestion,
|
deleteFillBlankQuestion,
|
||||||
|
@ -8,6 +8,7 @@ const {
|
|||||||
updateQuestionDescription,
|
updateQuestionDescription,
|
||||||
addChoiceQuestion,
|
addChoiceQuestion,
|
||||||
updateChoiceQuestion,
|
updateChoiceQuestion,
|
||||||
|
deleteChoiceQuestion, // 添加这行
|
||||||
addFillBlankQuestion,
|
addFillBlankQuestion,
|
||||||
updateFillBlankQuestion,
|
updateFillBlankQuestion,
|
||||||
deleteFillBlankQuestion,
|
deleteFillBlankQuestion,
|
||||||
@ -165,6 +166,23 @@ async function modifyChoiceQuestion(id, choiceData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:删除选择题问题
|
||||||
|
* @param {number} id - 选择题ID
|
||||||
|
* @returns {Promise<boolean>} 是否删除成功
|
||||||
|
*/
|
||||||
|
async function removeChoiceQuestion(id) {
|
||||||
|
try {
|
||||||
|
const result = await deleteChoiceQuestion(id);
|
||||||
|
// 调用增加题库版本号的方法
|
||||||
|
await increaseQuestionBankVersion();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 删除选择题失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务层:添加填空题问题
|
* 服务层:添加填空题问题
|
||||||
* @param {Object} fillBlankData - 填空题数据
|
* @param {Object} fillBlankData - 填空题数据
|
||||||
@ -366,6 +384,16 @@ function initQuestionIpc(ipcMain) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加删除选择题问题的IPC处理程序
|
||||||
|
ipcMain.handle('question-delete-choice', async (event, id) => {
|
||||||
|
try {
|
||||||
|
return await removeChoiceQuestion(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete choice question ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 根据题干ID查询填空题问题的IPC处理程序
|
// 根据题干ID查询填空题问题的IPC处理程序
|
||||||
ipcMain.handle('question-fetch-fill-blank-by-question-id', async (event, questionId) => {
|
ipcMain.handle('question-fetch-fill-blank-by-question-id', async (event, questionId) => {
|
||||||
try {
|
try {
|
||||||
@ -398,6 +426,7 @@ module.exports = {
|
|||||||
removeQuestion,
|
removeQuestion,
|
||||||
createChoiceQuestion,
|
createChoiceQuestion,
|
||||||
modifyChoiceQuestion,
|
modifyChoiceQuestion,
|
||||||
|
removeChoiceQuestion, // 添加这行
|
||||||
createFillBlankQuestion,
|
createFillBlankQuestion,
|
||||||
modifyFillBlankQuestion,
|
modifyFillBlankQuestion,
|
||||||
removeFillBlankQuestion,
|
removeFillBlankQuestion,
|
||||||
|
190
src/components/admin/QuestionAddForm.vue
Normal file
190
src/components/admin/QuestionAddForm.vue
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<div class="question-add-form-container">
|
||||||
|
<el-form ref="questionFormRef" :model="formData" label-width="120px">
|
||||||
|
<el-form-item label="题型" prop="question_type" :rules="[{ required: true, message: '请选择题型', trigger: 'change' }]">
|
||||||
|
<el-select v-model="formData.question_type" placeholder="请选择题型">
|
||||||
|
<el-option v-for="type in questionTypes" :key="type.item_code" :label="type.item_name" :value="type.item_code" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="题干描述" prop="question_description">
|
||||||
|
<el-input ref="descriptionInputRef" v-model="formData.question_description" type="textarea"
|
||||||
|
placeholder="请输入题干描述" :rows="4" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="题干配图">
|
||||||
|
<div class="upload-container">
|
||||||
|
<el-upload ref="imageUploadRef" class="upload-demo" action="" :before-upload="handleBeforeUploadImage"
|
||||||
|
:on-change="handleImageChange" list-type="picture-card" :auto-upload="false" accept="image/*" multiple>
|
||||||
|
<i class="el-icon-plus"></i>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="题干数据集">
|
||||||
|
<div class="upload-container">
|
||||||
|
<el-upload ref="datasetUploadRef" class="upload-demo dataset-upload" action="" :before-upload="handleBeforeUploadDataset"
|
||||||
|
:on-change="handleDatasetChange" accept=".xlsx,.xls" :auto-upload="false" multiple>
|
||||||
|
<el-button type="primary">上传Excel文件</el-button>
|
||||||
|
<div class="el-upload__text">支持.xlsx和.xls格式</div>
|
||||||
|
</el-upload>
|
||||||
|
<div v-if="formData.datasets && formData.datasets.length > 0" class="upload-tip">
|
||||||
|
已上传 {{ formData.datasets.length }} 个数据集
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<!-- 添加取消和保存按钮 -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<el-button @click="handleCancel">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 引入XLSX库处理Excel文件
|
||||||
|
import * as XLSX from 'xlsx'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'QuestionAddForm',
|
||||||
|
props: {
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
questionTypes: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update-form-data', 'on-cancel', 'on-save'],
|
||||||
|
methods: {
|
||||||
|
// 处理图片上传前
|
||||||
|
handleBeforeUploadImage(file) {
|
||||||
|
// 阻止默认上传
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理图片上传变化
|
||||||
|
handleImageChange(file, fileList) {
|
||||||
|
// 读取图片文件并转换为base64
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const imageBase64 = e.target.result
|
||||||
|
// 更新父组件的formData
|
||||||
|
const newImages = [...this.formData.images, {
|
||||||
|
file_name: file.name,
|
||||||
|
image_base64: imageBase64
|
||||||
|
}]
|
||||||
|
this.$emit('update-form-data', { ...this.formData, images: newImages })
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file.raw)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理数据集上传前
|
||||||
|
handleBeforeUploadDataset(file) {
|
||||||
|
// 阻止默认上传
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理数据集上传变化
|
||||||
|
handleDatasetChange(file, fileList) {
|
||||||
|
// 读取Excel文件
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = new Uint8Array(e.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const firstSheetName = workbook.SheetNames[0]
|
||||||
|
const worksheet = workbook.Sheets[firstSheetName]
|
||||||
|
// 转换为二维数组
|
||||||
|
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
|
||||||
|
|
||||||
|
// 更新父组件的formData
|
||||||
|
const newDatasets = [...this.formData.datasets, {
|
||||||
|
file_name: file.name,
|
||||||
|
content: jsonData
|
||||||
|
}]
|
||||||
|
this.$emit('update-form-data', { ...this.formData, datasets: newDatasets })
|
||||||
|
this.$message.success('数据集上传成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse Excel file:', error)
|
||||||
|
this.$message.error('数据集解析失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file.raw)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 验证表单并提交
|
||||||
|
validateAndSubmit() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.$refs.questionFormRef.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
resolve(this.formData)
|
||||||
|
} else {
|
||||||
|
reject(new Error('表单验证失败'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消按钮方法
|
||||||
|
handleCancel() {
|
||||||
|
this.$emit('on-cancel')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存按钮方法
|
||||||
|
handleSave() {
|
||||||
|
this.validateAndSubmit().then(() => {
|
||||||
|
this.$emit('on-save')
|
||||||
|
}).catch(error => {
|
||||||
|
this.$message.error(error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.question-add-form-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-demo .el-upload-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-upload {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-upload .el-upload__text {
|
||||||
|
margin-left: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-upload >>> .el-upload {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-upload >>> .el-upload__tip {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
75
src/components/admin/QuestionDescriptionEdit.vue
Normal file
75
src/components/admin/QuestionDescriptionEdit.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="question-description-edit-container">
|
||||||
|
<el-form ref="editFormRef" :model="formData" label-width="120px">
|
||||||
|
<el-form-item label="题干描述" prop="question_description">
|
||||||
|
<el-input v-model="formData.question_description" type="textarea"
|
||||||
|
placeholder="请输入题干描述" :rows="6" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="form-actions">
|
||||||
|
<el-button @click="handleCancel">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'QuestionDescriptionEditForm',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['submit', 'on-cancel'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
question_id: '',
|
||||||
|
question_description: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.formData = { ...newValue }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 提交表单
|
||||||
|
handleSubmit() {
|
||||||
|
this.$refs.editFormRef.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
this.$emit('submit', this.formData)
|
||||||
|
} else {
|
||||||
|
this.$message.error('请输入题干描述')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消操作
|
||||||
|
handleCancel() {
|
||||||
|
this.$emit('on-cancel')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.question-description-edit-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -140,16 +140,6 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 设置el-menu-item的padding-left为0 */
|
|
||||||
.el-menu-item {
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对于折叠状态的菜单也应用相同的padding */
|
|
||||||
.el-menu--collapse .el-menu-item {
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保菜单中的图标和文字正确显示 */
|
/* 确保菜单中的图标和文字正确显示 */
|
||||||
.el-menu-item i {
|
.el-menu-item i {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
@ -36,14 +36,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
questionFetchAllWithRelations: () => ipcRenderer.invoke('question-fetch-all-with-relations'),
|
questionFetchAllWithRelations: () => ipcRenderer.invoke('question-fetch-all-with-relations'),
|
||||||
questionUpdateDescription: (questionData) => ipcRenderer.invoke('question-update-description', questionData),
|
questionUpdateDescription: (questionData) => ipcRenderer.invoke('question-update-description', questionData),
|
||||||
questionAddChoice: (choiceData) => ipcRenderer.invoke('question-add-choice', choiceData),
|
questionAddChoice: (choiceData) => ipcRenderer.invoke('question-add-choice', choiceData),
|
||||||
questionUpdateChoice: (choiceData) => ipcRenderer.invoke('question-update-choice', choiceData),
|
// 修改questionUpdateChoice方法,使其接收两个参数并正确传递
|
||||||
|
questionUpdateChoice: (id, choiceData) => ipcRenderer.invoke('question-update-choice', id, choiceData),
|
||||||
questionDeleteChoice: (id) => ipcRenderer.invoke('question-delete-choice', id),
|
questionDeleteChoice: (id) => ipcRenderer.invoke('question-delete-choice', id),
|
||||||
|
// 添加questionCreateChoice方法,调用主进程中已注册的'question-create-choice'通道
|
||||||
|
questionCreateChoice: (choiceData) => ipcRenderer.invoke('question-create-choice', choiceData),
|
||||||
questionAddFillBlank: (fillBlankData) => ipcRenderer.invoke('question-add-fill-blank', fillBlankData),
|
questionAddFillBlank: (fillBlankData) => ipcRenderer.invoke('question-add-fill-blank', fillBlankData),
|
||||||
questionUpdateFillBlank: (fillBlankData) => ipcRenderer.invoke('question-update-fill-blank', fillBlankData),
|
// 添加新的questionCreateFillBlank方法,调用主进程中已注册的'question-create-fill-blank'通道
|
||||||
|
questionCreateFillBlank: (fillBlankData) => ipcRenderer.invoke('question-create-fill-blank', fillBlankData),
|
||||||
|
// 修改questionUpdateFillBlank方法,使其接收两个参数并正确传递
|
||||||
|
questionUpdateFillBlank: (id, fillBlankData) => ipcRenderer.invoke('question-update-fill-blank', id, fillBlankData),
|
||||||
questionDeleteFillBlank: (id) => ipcRenderer.invoke('question-delete-fill-blank', id),
|
questionDeleteFillBlank: (id) => ipcRenderer.invoke('question-delete-fill-blank', id),
|
||||||
questionGetQuestionWithChoices: (questionId) => ipcRenderer.invoke('question-get-question-with-choices', questionId),
|
questionGetQuestionWithChoices: (questionId) => ipcRenderer.invoke('question-get-question-with-choices', questionId),
|
||||||
questionGetQuestionWithFillBlanks: (questionId) => ipcRenderer.invoke('question-get-question-with-fill-blanks', questionId),
|
questionGetQuestionWithFillBlanks: (questionId) => ipcRenderer.invoke('question-get-question-with-fill-blanks', questionId),
|
||||||
questionRemove: (questionId) => ipcRenderer.invoke('question-remove', questionId),
|
questionRemove: (questionId) => ipcRenderer.invoke('question-remove', questionId),
|
||||||
|
// 添加新的questionDelete方法,调用主进程中已注册的'question-delete'通道
|
||||||
|
questionDelete: (questionId) => ipcRenderer.invoke('question-delete', questionId),
|
||||||
questionGetStatistics: () => ipcRenderer.invoke('question-get-statistics'),
|
questionGetStatistics: () => ipcRenderer.invoke('question-get-statistics'),
|
||||||
questionGetQuestionById: (questionId) => ipcRenderer.invoke('question-get-question-by-id', questionId),
|
questionGetQuestionById: (questionId) => ipcRenderer.invoke('question-get-question-by-id', questionId),
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import VueRouter from 'vue-router'
|
|||||||
import WelcomeView from '../views/WelcomeView.vue'
|
import WelcomeView from '../views/WelcomeView.vue'
|
||||||
import AdminLayout from '../components/admin/AdminLayout.vue'
|
import AdminLayout from '../components/admin/AdminLayout.vue'
|
||||||
import AdminHomeView from '../views/admin/AdminHomeView.vue'
|
import AdminHomeView from '../views/admin/AdminHomeView.vue'
|
||||||
|
import QuestionManagementView from '../views/admin/QuestionManagementView.vue'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
@ -24,6 +25,11 @@ const routes = [
|
|||||||
path: 'home',
|
path: 'home',
|
||||||
name: 'AdminHome',
|
name: 'AdminHome',
|
||||||
component: AdminHomeView
|
component: AdminHomeView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'question',
|
||||||
|
name: 'QuestionManagement',
|
||||||
|
component: QuestionManagementView
|
||||||
}
|
}
|
||||||
// 可以在这里添加更多子路由
|
// 可以在这里添加更多子路由
|
||||||
]
|
]
|
||||||
|
1128
src/views/admin/QuestionManagementView.vue
Normal file
1128
src/views/admin/QuestionManagementView.vue
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user