完成后台的试题管理

This commit is contained in:
chenqiang 2025-08-31 10:31:57 +08:00
parent 3a05219d2e
commit 7a82c98d00
9 changed files with 1452 additions and 13 deletions

View File

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

View File

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

View File

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

View 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>
// XLSXExcel
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>

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

View File

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

View File

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

View File

@ -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
}
// 可以在这里添加更多子路由
]

File diff suppressed because it is too large Load Diff