完成后台的试题管理
This commit is contained in:
parent
3a05219d2e
commit
7a82c98d00
@ -9,7 +9,6 @@
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
</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 - 填空题数据
|
||||
@ -415,6 +428,7 @@ module.exports = {
|
||||
updateQuestionDescription,
|
||||
addChoiceQuestion,
|
||||
updateChoiceQuestion,
|
||||
deleteChoiceQuestion,
|
||||
addFillBlankQuestion,
|
||||
updateFillBlankQuestion,
|
||||
deleteFillBlankQuestion,
|
||||
|
@ -8,6 +8,7 @@ const {
|
||||
updateQuestionDescription,
|
||||
addChoiceQuestion,
|
||||
updateChoiceQuestion,
|
||||
deleteChoiceQuestion, // 添加这行
|
||||
addFillBlankQuestion,
|
||||
updateFillBlankQuestion,
|
||||
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 - 填空题数据
|
||||
@ -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处理程序
|
||||
ipcMain.handle('question-fetch-fill-blank-by-question-id', async (event, questionId) => {
|
||||
try {
|
||||
@ -398,6 +426,7 @@ module.exports = {
|
||||
removeQuestion,
|
||||
createChoiceQuestion,
|
||||
modifyChoiceQuestion,
|
||||
removeChoiceQuestion, // 添加这行
|
||||
createFillBlankQuestion,
|
||||
modifyFillBlankQuestion,
|
||||
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%;
|
||||
}
|
||||
|
||||
/* 设置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 {
|
||||
margin-right: 10px;
|
||||
|
@ -36,14 +36,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
questionFetchAllWithRelations: () => ipcRenderer.invoke('question-fetch-all-with-relations'),
|
||||
questionUpdateDescription: (questionData) => ipcRenderer.invoke('question-update-description', questionData),
|
||||
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),
|
||||
// 添加questionCreateChoice方法,调用主进程中已注册的'question-create-choice'通道
|
||||
questionCreateChoice: (choiceData) => ipcRenderer.invoke('question-create-choice', choiceData),
|
||||
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),
|
||||
questionGetQuestionWithChoices: (questionId) => ipcRenderer.invoke('question-get-question-with-choices', questionId),
|
||||
questionGetQuestionWithFillBlanks: (questionId) => ipcRenderer.invoke('question-get-question-with-fill-blanks', questionId),
|
||||
questionRemove: (questionId) => ipcRenderer.invoke('question-remove', questionId),
|
||||
// 添加新的questionDelete方法,调用主进程中已注册的'question-delete'通道
|
||||
questionDelete: (questionId) => ipcRenderer.invoke('question-delete', questionId),
|
||||
questionGetStatistics: () => ipcRenderer.invoke('question-get-statistics'),
|
||||
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 AdminLayout from '../components/admin/AdminLayout.vue'
|
||||
import AdminHomeView from '../views/admin/AdminHomeView.vue'
|
||||
import QuestionManagementView from '../views/admin/QuestionManagementView.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
@ -24,6 +25,11 @@ const routes = [
|
||||
path: 'home',
|
||||
name: 'AdminHome',
|
||||
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