增加考题管理模块
This commit is contained in:
parent
e98b30edd3
commit
0fff276e59
140
pc/src/api/exam.js
Normal file
140
pc/src/api/exam.js
Normal file
@ -0,0 +1,140 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取考试列表
|
||||
export function getExams(params) {
|
||||
return request({
|
||||
url: '/api/exams',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 根据ID获取考试
|
||||
export function getExamById(id) {
|
||||
return request({
|
||||
url: `/api/exams/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加考试
|
||||
export function addExam(data) {
|
||||
return request({
|
||||
url: '/api/exams',
|
||||
method: 'post',
|
||||
data: {
|
||||
...data,
|
||||
is_global: data.is_global !== undefined ? data.is_global : 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更新考试
|
||||
export function updateExam(id, data) {
|
||||
return request({
|
||||
url: `/api/exams/${id}`,
|
||||
method: 'put',
|
||||
data: {
|
||||
...data,
|
||||
is_global: data.is_global !== undefined ? data.is_global : 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除考试
|
||||
export function deleteExam(id) {
|
||||
return request({
|
||||
url: `/api/exams/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** 题库相关接口 */
|
||||
//题库列表
|
||||
export function getExamQuestionBanks(params) {
|
||||
return request({
|
||||
url: '/api/exam-question-banks',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
//题库详情
|
||||
export function getExamQuestionBankDetail(id) {
|
||||
return request({
|
||||
url: `/api/exam-question-banks/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
//新增题库
|
||||
export function createExamQuestionBank(data) {
|
||||
return request({
|
||||
url: '/api/exam-question-banks',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//更新题库
|
||||
export function updateExamQuestionBank(id, data) {
|
||||
return request({
|
||||
url: `/api/exam-question-banks/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//删除题库
|
||||
export function deleteExamQuestionBank(id) {
|
||||
return request({
|
||||
url: `/api/exam-question-banks/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** 题目相关接口 */
|
||||
// 题目列表
|
||||
export function getExamQuestions(params) {
|
||||
return request({
|
||||
url: '/api/exam-questions',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 题目详情
|
||||
export function getExamQuestionDetail(id) {
|
||||
return request({
|
||||
url: `/api/exam-questions/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增题目
|
||||
export function createExamQuestion(data) {
|
||||
return request({
|
||||
url: '/api/exam-questions',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新题目
|
||||
export function updateExamQuestion(id, data) {
|
||||
return request({
|
||||
url: `/api/exam-questions/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除题目
|
||||
export function deleteExamQuestion(id) {
|
||||
return request({
|
||||
url: `/api/exam-questions/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@ -1,40 +1,46 @@
|
||||
<template>
|
||||
<div class="exam-container">
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<div class="form-fields">
|
||||
<el-form-item label="考试名称">
|
||||
<el-input v-model="filters.keyword" placeholder="请输入考试名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="全部" clearable>
|
||||
<el-option label="进行中" :value="1" />
|
||||
<el-option label="已结束" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围" class="col-span-2">
|
||||
<el-date-picker v-model="filters.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
|
||||
end-placeholder="结束日期" value-format="YYYY-MM-DD" unlink-panels clearable :teleported="false"
|
||||
placement="bottom-start" :popper-options="{
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{ name: 'flip', options: { fallbackPlacements: [] } },
|
||||
{ name: 'offset', options: { offset: [0, 8] } },
|
||||
{ name: 'preventOverflow', options: { boundary: 'viewport' } }
|
||||
]
|
||||
}" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreateExam">
|
||||
创建考试
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="toolright">
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="考试名称">
|
||||
<el-input v-model="filters.keyword" placeholder="请输入考试名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 160px">
|
||||
<el-option label="进行中" :value="1" />
|
||||
<el-option label="已结束" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
unlink-panels
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 考试列表 -->
|
||||
@ -63,14 +69,8 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper" v-if="examList.length > 0">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next, total"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
<el-pagination background layout="prev, pager, next, total" :total="total" :page-size="pageSize"
|
||||
:current-page="currentPage" @current-change="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -154,8 +154,11 @@ onMounted(() => {
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
@ -165,6 +168,29 @@ onMounted(() => {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
.form-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
column-gap: 16px;
|
||||
row-gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
margin: 12px 0 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.exam-list {
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<el-dialog :model-value="visible" title="题目详情" width="720px" @close="handleClose">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="题目">{{ q?.question_title || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="题型">{{ displayType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分值">{{ q?.score ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="答案">
|
||||
<span v-if="Array.isArray(answerDisplay)">{{ (answerDisplay || []).join(', ') }}</span>
|
||||
<span v-else>{{ answerDisplay || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="解析">{{ q?.question_analysis || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatTime(q?.create_time || q?.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatTime(q?.update_time || q?.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="Array.isArray(optionsDisplay) && optionsDisplay.length" style="margin-top: 16px;">
|
||||
<el-table :data="optionsDisplay" size="small" style="width: 100%">
|
||||
<el-table-column prop="label" label="选项" width="80" />
|
||||
<el-table-column prop="content" label="内容" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
model: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const typeOptions = ref([])
|
||||
|
||||
const q = computed(() => (props.model && (props.model.Question || props.model)) || null)
|
||||
const displayType = computed(() => {
|
||||
const typeValue = String(q.value?.question_type ?? '')
|
||||
const found = typeOptions.value.find(opt => String(opt.dict_value) === typeValue)
|
||||
return found ? found.dict_label : typeValue
|
||||
})
|
||||
const optionsDisplay = computed(() => {
|
||||
if (Array.isArray(props.model?.Options)) {
|
||||
return props.model.Options.map(o => ({
|
||||
label: o.option_label || o.label,
|
||||
content: o.option_content || o.content
|
||||
}))
|
||||
}
|
||||
if (Array.isArray(q.value?.options)) return q.value.options
|
||||
return []
|
||||
})
|
||||
const answerDisplay = computed(() => {
|
||||
const a = (props.model && props.model.Answer && props.model.Answer.answer_content) ?? q.value?.answer
|
||||
if (Array.isArray(a)) return a
|
||||
if (typeof a === 'string' && a.indexOf(',') !== -1) return a.split(',').map(s => s.trim()).filter(Boolean)
|
||||
return a || ''
|
||||
})
|
||||
|
||||
watch(() => props.visible, async (v) => {
|
||||
if (v && typeOptions.value.length === 0) {
|
||||
try {
|
||||
const res = await getDictItemsByCode('exam_question_type')
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
typeOptions.value = res.data
|
||||
} else if (Array.isArray(res)) {
|
||||
typeOptions.value = res
|
||||
}
|
||||
} catch (e) {
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function formatTime(t) {
|
||||
if (!t) return ''
|
||||
return dayjs(t).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
276
pc/src/views/apps/exams/examinationQuestions/components/edit.vue
Normal file
276
pc/src/views/apps/exams/examinationQuestions/components/edit.vue
Normal file
@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<el-dialog :title="dialogTitle" :model-value="visible" width="720px" @close="handleClose">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<div class="form-flex">
|
||||
<el-form-item label="题型" prop="question_type">
|
||||
<el-select v-model="form.question_type" placeholder="请选择题型" style="width: 100%">
|
||||
<el-option v-for="opt in typeOptions" :key="opt.dict_value" :label="opt.dict_label" :value="opt.dict_value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分值" prop="score">
|
||||
<el-input-number v-model="form.score" :step="1" :min="0" :max="100" :precision="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="题目" prop="question_title">
|
||||
<el-input v-model="form.question_title" placeholder="请输入题目" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<template v-if="showOptions">
|
||||
<el-divider />
|
||||
<div class="options-header">
|
||||
<span>选项</span>
|
||||
<div class="op">
|
||||
<el-button v-if="canCustomizeOptions" size="small" @click="addOption">新增选项</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-list">
|
||||
<div v-for="(opt, idx) in form.options" :key="opt.key" class="option-item">
|
||||
<div class="label">{{ opt.label }}</div>
|
||||
<el-input v-model="opt.content" placeholder="请输入选项内容" />
|
||||
<el-button v-if="canCustomizeOptions" link type="danger" @click="removeOption(idx)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="showAnswer">
|
||||
<el-divider />
|
||||
<el-form-item label="正确答案" prop="answer">
|
||||
<template v-if="isSingleType">
|
||||
<el-select v-model="form.answer" placeholder="请选择">
|
||||
<el-option v-for="opt in form.options" :key="opt.key" :label="opt.label" :value="opt.label" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else-if="isMultipleType">
|
||||
<el-select v-model="multiAnswer" multiple placeholder="请选择">
|
||||
<el-option v-for="opt in form.options" :key="opt.key" :label="opt.label" :value="opt.label" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input v-model="form.answer" type="textarea" :rows="2" placeholder="请输入答案" />
|
||||
</template>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="解析">
|
||||
<el-input v-model="form.question_analysis" type="textarea" :rows="3" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dlg-actions">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmit">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
mode: { type: String, default: 'create' },
|
||||
model: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'saved'])
|
||||
|
||||
const formRef = ref()
|
||||
const saving = ref(false)
|
||||
const typeOptions = ref([])
|
||||
const dialogTitle = computed(() => (props.mode === 'edit' ? '编辑题目' : '新建题目'))
|
||||
|
||||
const defaultOptions = () => ([
|
||||
{ key: 'A', label: 'A', content: '' },
|
||||
{ key: 'B', label: 'B', content: '' },
|
||||
{ key: 'C', label: 'C', content: '' },
|
||||
{ key: 'D', label: 'D', content: '' }
|
||||
])
|
||||
|
||||
const form = reactive({
|
||||
question_title: '',
|
||||
question_type: '',
|
||||
score: 5,
|
||||
question_analysis: '',
|
||||
options: defaultOptions(),
|
||||
answer: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
question_title: [{ required: true, message: '请输入题目', trigger: 'blur' }],
|
||||
question_type: [{ required: true, message: '请选择题型', trigger: 'change' }],
|
||||
score: [{ required: true, message: '请输入分值', trigger: 'change' }],
|
||||
answer: [{ validator: validateAnswer, trigger: 'change' }]
|
||||
}
|
||||
|
||||
function validateAnswer(rule, value, callback) {
|
||||
if (!showAnswer.value) return callback()
|
||||
if (isMultipleType.value) {
|
||||
if (!multiAnswer.value || multiAnswer.value.length === 0) return callback(new Error('请选择答案'))
|
||||
return callback()
|
||||
}
|
||||
if (!form.answer) return callback(new Error('请输入/选择答案'))
|
||||
callback()
|
||||
}
|
||||
|
||||
const isSingleType = computed(() => ['single', 'judge', '1', '3'].includes(String(form.question_type)))
|
||||
const isMultipleType = computed(() => ['multiple', '2'].includes(String(form.question_type)))
|
||||
const isObjective = computed(() => isSingleType.value || isMultipleType.value)
|
||||
const showOptions = computed(() => isObjective.value)
|
||||
const showAnswer = computed(() => true)
|
||||
const canCustomizeOptions = computed(() => ['single', 'multiple', '1', '2'].includes(String(form.question_type)))
|
||||
const multiAnswer = ref([])
|
||||
|
||||
watch(() => props.visible, async (v) => {
|
||||
if (v) {
|
||||
await ensureTypeOptions()
|
||||
initForm()
|
||||
await nextTick()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.model, (/* newVal */) => {
|
||||
if (props.visible) {
|
||||
initForm()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => form.question_type, (t) => {
|
||||
const s = String(t)
|
||||
if (['judge', '3'].includes(s)) {
|
||||
form.options = [
|
||||
{ key: 'T', label: '对', content: '对' },
|
||||
{ key: 'F', label: '错', content: '错' }
|
||||
]
|
||||
form.answer = ''
|
||||
multiAnswer.value = []
|
||||
} else if (['single', 'multiple', '1', '2'].includes(s)) {
|
||||
if (!form.options || form.options.length === 0 || form.options[0].label === '对') {
|
||||
form.options = defaultOptions()
|
||||
}
|
||||
form.answer = ''
|
||||
multiAnswer.value = []
|
||||
} else {
|
||||
form.options = []
|
||||
}
|
||||
})
|
||||
|
||||
function initForm() {
|
||||
if (props.model) {
|
||||
form.question_title = props.model.question_title || ''
|
||||
form.question_type = props.model.question_type || ''
|
||||
form.score = props.model.score ?? 5
|
||||
form.question_analysis = props.model.question_analysis || ''
|
||||
form.options = Array.isArray(props.model.options) && props.model.options.length > 0 ? props.model.options.map((o, i) => ({ key: o.key || String.fromCharCode(65 + i), label: o.label || String.fromCharCode(65 + i), content: o.content || '' })) : (isObjective.value ? defaultOptions() : [])
|
||||
if (Array.isArray(props.model.answer)) {
|
||||
multiAnswer.value = props.model.answer
|
||||
form.answer = ''
|
||||
} else {
|
||||
form.answer = props.model.answer || ''
|
||||
multiAnswer.value = []
|
||||
}
|
||||
} else {
|
||||
form.question_title = ''
|
||||
form.question_type = ''
|
||||
form.score = 5
|
||||
form.question_analysis = ''
|
||||
form.options = defaultOptions()
|
||||
form.answer = ''
|
||||
multiAnswer.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTypeOptions() {
|
||||
if (typeOptions.value && typeOptions.value.length > 0) return
|
||||
try {
|
||||
const res = await getDictItemsByCode('exam_question_type')
|
||||
if (res && res.success && Array.isArray(res.data)) typeOptions.value = res.data
|
||||
else if (Array.isArray(res)) typeOptions.value = res
|
||||
else typeOptions.value = []
|
||||
} catch {
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function addOption() {
|
||||
const nextIndex = form.options.length
|
||||
const label = String.fromCharCode(65 + nextIndex)
|
||||
form.options.push({ key: label, label, content: '' })
|
||||
}
|
||||
|
||||
function removeOption(idx) {
|
||||
form.options.splice(idx, 1)
|
||||
form.options.forEach((o, i) => {
|
||||
o.key = String.fromCharCode(65 + i)
|
||||
o.label = o.key
|
||||
})
|
||||
if (isMultipleType.value) {
|
||||
multiAnswer.value = multiAnswer.value.filter(l => form.options.some(o => o.label === l))
|
||||
} else if (isSingleType.value) {
|
||||
if (!form.options.some(o => o.label === form.answer)) form.answer = ''
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value.validate(async (ok) => {
|
||||
if (!ok) return
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
question_title: form.question_title,
|
||||
question_type: form.question_type,
|
||||
score: form.score,
|
||||
question_analysis: form.question_analysis,
|
||||
options: showOptions.value ? form.options.map(o => ({ key: o.key, label: o.label, content: o.content })) : [],
|
||||
answer: isMultipleType.value ? [...multiAnswer.value] : form.answer
|
||||
}
|
||||
emit('saved', payload)
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap:15px;
|
||||
}
|
||||
.options-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.option-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.option-item {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.label {
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
.dlg-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<el-dialog :title="dialogTitle" :model-value="visible" width="560px" @close="handleClose">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="题库名称" prop="bank_name">
|
||||
<el-input v-model="form.bank_name" placeholder="请输入题库名称" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="题库描述" prop="bank_desc">
|
||||
<el-input v-model="form.bank_desc" type="textarea" :rows="4" placeholder="可选" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div style="display:flex;justify-content:flex-end;gap:12px;">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmit">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
mode: { type: String, default: 'create' },
|
||||
model: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'saved'])
|
||||
|
||||
const formRef = ref()
|
||||
const saving = ref(false)
|
||||
const dialogTitle = computed(() => (props.mode === 'edit' ? '编辑题库' : '新建题库'))
|
||||
|
||||
const form = reactive({
|
||||
bank_name: '',
|
||||
bank_desc: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
bank_name: [{ required: true, message: '请输入题库名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function initForm() {
|
||||
if (props.model) {
|
||||
form.bank_name = props.model.bank_name || ''
|
||||
form.bank_desc = props.model.bank_desc || ''
|
||||
} else {
|
||||
form.bank_name = ''
|
||||
form.bank_desc = ''
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.visible, async (v) => {
|
||||
if (v) {
|
||||
initForm()
|
||||
await nextTick()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.model, () => {
|
||||
if (props.visible) initForm()
|
||||
})
|
||||
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value.validate(async (ok) => {
|
||||
if (!ok) return
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = { bank_name: form.bank_name, bank_desc: form.bank_desc }
|
||||
emit('saved', payload)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div class="exam-list-page">
|
||||
<div class="header">
|
||||
<!-- 顶部工具栏:返回 + 标题 -->
|
||||
<div class="topbar">
|
||||
<el-button :icon="Back" @click="emit('back')">返回</el-button>
|
||||
<div class="title">题目列表</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="filters.keyword" placeholder="请输入题干关键词" clearable
|
||||
@keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="题型">
|
||||
<el-select v-model="filters.type" placeholder="请选择题型" clearable style="width: 200px;">
|
||||
<el-option v-for="opt in typeOptions" :key="opt.dict_value" :label="opt.dict_label"
|
||||
:value="opt.dict_value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<!-- 操作栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">新建题目</el-button>
|
||||
<div style="flex:1" />
|
||||
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table v-loading="loading" :data="list" stripe border>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="question_title" label="题目" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column prop="question_type" label="题型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ getTypeLabel(row.question_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="score" label="分值" width="100" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">{{ formatTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleView(row)">查看</el-button>
|
||||
<el-button link type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="total"
|
||||
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
|
||||
<!-- 弹窗:仅用于创建/编辑题目与查看详情 -->
|
||||
<EditDialog :visible="editVisible" :mode="editMode" :model="editModel" @close="editVisible = false"
|
||||
@saved="handleSaved" />
|
||||
<DetailDialog :visible="viewVisible" :model="viewModel" @close="viewVisible = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Refresh, Back } from '@element-plus/icons-vue'
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getExamQuestions, getExamQuestionDetail, createExamQuestion, updateExamQuestion, deleteExamQuestion } from '@/api/exam'
|
||||
import EditDialog from './edit.vue'
|
||||
import DetailDialog from './detail.vue'
|
||||
|
||||
const props = defineProps({
|
||||
bankId: { type: [Number, String], default: null },
|
||||
})
|
||||
const emit = defineEmits(['back', 'saved'])
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
const filters = ref({ keyword: '', type: null })
|
||||
const typeOptions = ref([])
|
||||
const typeDictMap = computed(() => {
|
||||
const m = {}
|
||||
for (const it of typeOptions.value || []) m[String(it.dict_value)] = it.dict_label
|
||||
return m
|
||||
})
|
||||
const getTypeLabel = (type) => typeDictMap.value[String(type)] || String(type || '')
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editMode = ref('create')
|
||||
const editModel = ref(null)
|
||||
const viewVisible = ref(false)
|
||||
const viewModel = ref(null)
|
||||
|
||||
function normalizeType(val) {
|
||||
if (val === null || val === undefined || val === '') return undefined
|
||||
const n = Number(val)
|
||||
if (!Number.isNaN(n)) return n
|
||||
// fallback by dict value is usually numeric already
|
||||
return val
|
||||
}
|
||||
|
||||
function formatTime(t) {
|
||||
if (!t) return ''
|
||||
return dayjs(t).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: filters.value.keyword || undefined,
|
||||
type: normalizeType(filters.value.type),
|
||||
bank_id: props.bankId || undefined,
|
||||
tenant_id: auth.user?.tenant_id || undefined,
|
||||
}
|
||||
const res = await getExamQuestions(params)
|
||||
let rows = [], sum = 0
|
||||
if (res && res.code === 0 && res.data) {
|
||||
rows = res.data.list || []
|
||||
sum = res.data.total || 0
|
||||
} else if (res && res.success && Array.isArray(res.data)) {
|
||||
rows = res.data
|
||||
sum = res.total || 0
|
||||
}
|
||||
list.value = rows
|
||||
total.value = sum
|
||||
} catch (e) {
|
||||
console.error('获取题目列表失败', e)
|
||||
ElMessage.error('获取题目列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => { currentPage.value = 1; fetchList() }
|
||||
const handleReset = () => { filters.value = { keyword: '', type: null }; currentPage.value = 1; fetchList() }
|
||||
const handlePageChange = (p) => { currentPage.value = p; fetchList() }
|
||||
const handleSizeChange = (s) => { pageSize.value = s; currentPage.value = 1; fetchList() }
|
||||
|
||||
// 改为顶部返回按钮 emit('back')
|
||||
|
||||
function handleCreate() {
|
||||
editMode.value = 'create'
|
||||
editModel.value = null
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
async function handleView(row) {
|
||||
try {
|
||||
const res = await getExamQuestionDetail(row.id)
|
||||
if (res && res.code === 0 && res.data) {
|
||||
viewModel.value = res.data
|
||||
} else if (res && res.success && res.data) {
|
||||
viewModel.value = res.data
|
||||
} else {
|
||||
viewModel.value = row
|
||||
}
|
||||
} catch (e) {
|
||||
viewModel.value = row
|
||||
}
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
editMode.value = 'edit'
|
||||
editModel.value = {
|
||||
id: row.id,
|
||||
question_title: row.question_title,
|
||||
question_type: row.question_type,
|
||||
score: row.score,
|
||||
question_analysis: row.question_analysis,
|
||||
options: row.options || [],
|
||||
answer: row.answer,
|
||||
}
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该题目吗?', '提示', { type: 'warning' })
|
||||
await deleteExamQuestion(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
// cancel
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaved(payload) {
|
||||
try {
|
||||
const submit = { ...payload, question_type: normalizeType(payload.question_type) }
|
||||
if (props.bankId) submit.bank_id = props.bankId
|
||||
if (editMode.value === 'edit' && editModel.value && editModel.value.id) {
|
||||
await updateExamQuestion(editModel.value.id, submit)
|
||||
} else {
|
||||
await createExamQuestion(submit)
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
editVisible.value = false
|
||||
fetchList()
|
||||
emit('saved')
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 父组件切换到本页时请自行触发 fetchList,或在此自动加载一次
|
||||
onMounted(() => { fetchList() })
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await getDictItemsByCode('exam_question_type')
|
||||
if (res && res.success && Array.isArray(res.data)) typeOptions.value = res.data
|
||||
else if (Array.isArray(res)) typeOptions.value = res
|
||||
else typeOptions.value = []
|
||||
} catch (e) {
|
||||
typeOptions.value = []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.exam-list-page {
|
||||
padding: 0;
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.topbar .title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
/* margin-bottom: 12px; */
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* margin-bottom: 12px; */
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div class="examination-questions">
|
||||
<template v-if="!examListVisible">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<div class="form-fields">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="filters.keyword" placeholder="请输入题目关键词" clearable
|
||||
@keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="题型">
|
||||
<el-select v-model="filters.type" placeholder="请选择题型" clearable>
|
||||
<el-option v-for="opt in dictTypeOptions" :key="opt.dict_value" :label="opt.dict_label"
|
||||
:value="opt.dict_value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">新建题库</el-button>
|
||||
<div class="toolright">
|
||||
<el-button @click="handleBatchImport" :icon="Upload">批量导入</el-button>
|
||||
<el-button @click="handleRefresh" :icon="Refresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 题库列表 -->
|
||||
<el-table v-loading="loading" :data="bankList" stripe border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="bank_name" label="题库名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="bank_desc" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<!-- <el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="String(row.status)==='0' ? 'success' : 'info'">{{ String(row.status)==='0' ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<!-- <el-table-column prop="create_time" label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="handleAdd(row)">题目管理</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="total"
|
||||
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
<EditDialog :visible="editVisible" :mode="editMode" :model="editModel" @close="editVisible = false"
|
||||
@saved="handleSaved" />
|
||||
<EditBanks :visible="bankVisible" :mode="bankMode" :model="bankModel" @close="bankVisible = false" @saved="handleBankSaved" />
|
||||
<DetailDialog :visible="viewVisible" :model="viewModel" @close="viewVisible = false" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ExamList :bank-id="examListBankId" @back="examListVisible = false" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Search, Plus, Refresh, Upload } from '@element-plus/icons-vue';
|
||||
import { getDictItemsByCode } from '@/api/dict';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { getExamQuestionBanks, createExamQuestionBank, updateExamQuestionBank, deleteExamQuestionBank } from '@/api/exam';
|
||||
import EditDialog from './components/edit.vue';
|
||||
import DetailDialog from './components/detail.vue';
|
||||
import EditBanks from './components/editBanks.vue';
|
||||
import ExamList from './components/examlist.vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const bankList = ref([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const dictTypeOptions = ref([]);
|
||||
|
||||
const editVisible = ref(false);
|
||||
const editMode = ref('create');
|
||||
const editModel = ref(null);
|
||||
|
||||
const viewVisible = ref(false);
|
||||
const viewModel = ref(null);
|
||||
|
||||
const bankVisible = ref(false);
|
||||
const bankMode = ref('create');
|
||||
const bankModel = ref(null);
|
||||
|
||||
const examListVisible = ref(false);
|
||||
const examListBankId = ref(null);
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const filters = ref({
|
||||
keyword: '',
|
||||
type: null,
|
||||
difficulty: null
|
||||
});
|
||||
|
||||
const normalizeType = (val) => undefined;
|
||||
|
||||
const typeDictMap = computed(() => ({}));
|
||||
|
||||
const getTypeLabel = (type) => '';
|
||||
|
||||
const getTypeTagType = (type) => '';
|
||||
|
||||
// 难度字段后端未实现,先移除对应逻辑
|
||||
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1;
|
||||
fetchBankList();
|
||||
};
|
||||
|
||||
//格式化时间
|
||||
const formatTime = (time) => {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
filters.value = { keyword: '', type: null, difficulty: null };
|
||||
currentPage.value = 1;
|
||||
fetchBankList();
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
bankMode.value = 'create';
|
||||
bankModel.value = null;
|
||||
bankVisible.value = true;
|
||||
};
|
||||
|
||||
const handleBatchImport = () => {
|
||||
ElMessage.info('批量导入功能开发中');
|
||||
};
|
||||
|
||||
const handleView = async (row) => {};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchBankList();
|
||||
};
|
||||
|
||||
const handleBankSaved = async (payload) => {
|
||||
try {
|
||||
if (bankMode.value === 'edit' && bankModel.value && bankModel.value.id) {
|
||||
await updateExamQuestionBank(bankModel.value.id, payload);
|
||||
} else {
|
||||
await createExamQuestionBank(payload);
|
||||
}
|
||||
ElMessage.success('保存成功');
|
||||
bankVisible.value = false;
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (row) => {
|
||||
bankMode.value = 'edit';
|
||||
bankModel.value = { id: row.id, bank_name: row.bank_name, bank_desc: row.bank_desc };
|
||||
bankVisible.value = true;
|
||||
};
|
||||
|
||||
const handleAdd = (row) => {
|
||||
examListBankId.value = row?.id || null;
|
||||
examListVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSaved = async (payload) => {
|
||||
try {
|
||||
const submit = { ...payload, question_type: normalizeType(payload.question_type) };
|
||||
if (editMode.value === 'edit' && editModel.value && editModel.value.id) {
|
||||
await updateExamQuestion(editModel.value.id, submit);
|
||||
} else {
|
||||
await createExamQuestion(submit);
|
||||
}
|
||||
ElMessage.success('保存成功');
|
||||
editVisible.value = false;
|
||||
await fetchBankList();
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该题库吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
await deleteExamQuestionBank(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
fetchBankList();
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page;
|
||||
fetchBankList();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size;
|
||||
currentPage.value = 1;
|
||||
fetchBankList();
|
||||
};
|
||||
|
||||
const fetchBankList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: filters.value.keyword || undefined,
|
||||
tenant_id: auth.user?.tenant_id || undefined,
|
||||
};
|
||||
const res = await getExamQuestionBanks(params);
|
||||
let list = [], sum = 0;
|
||||
if (res && res.code === 0 && res.data) {
|
||||
list = res.data.list || [];
|
||||
sum = res.data.total || 0;
|
||||
} else if (res && res.success && Array.isArray(res.data)) {
|
||||
list = res.data;
|
||||
sum = res.total || 0;
|
||||
}
|
||||
bankList.value = list;
|
||||
total.value = sum;
|
||||
} catch (error) {
|
||||
console.error('获取题库列表失败', error);
|
||||
ElMessage.error('获取题库列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await getDictItemsByCode('exam_question_type');
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
dictTypeOptions.value = res.data;
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypeOptions.value = res;
|
||||
} else {
|
||||
dictTypeOptions.value = [];
|
||||
}
|
||||
} catch (e) {
|
||||
dictTypeOptions.value = [];
|
||||
}
|
||||
})();
|
||||
fetchBankList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.examination-questions {
|
||||
padding: 20px;
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
|
||||
.form-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
column-gap: 16px;
|
||||
row-gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
margin: 12px 0 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -4,4 +4,8 @@
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
</style>
|
||||
340
server/controllers/exam.go
Normal file
340
server/controllers/exam.go
Normal file
@ -0,0 +1,340 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type ExamQuestionController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type ExamQuestionBankController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// Get list
|
||||
// @router /exam-questions [get]
|
||||
func (c *ExamQuestionController) GetList() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
keyword := c.GetString("keyword")
|
||||
typeStr := c.GetString("type")
|
||||
var qtype *int8
|
||||
if typeStr != "" {
|
||||
if iv, err := strconv.Atoi(typeStr); err == nil {
|
||||
v := int8(iv)
|
||||
qtype = &v
|
||||
}
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
list, total, err := services.GetExamQuestions(services.QuestionListParams{
|
||||
TenantId: tenantId,
|
||||
Keyword: keyword,
|
||||
QuestionType: qtype,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "获取试题列表失败: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
// build minimal DTO to limit fields in response
|
||||
items := make([]map[string]interface{}, 0, len(list))
|
||||
for _, q := range list {
|
||||
if q == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": q.Id,
|
||||
"tenant_id": q.TenantId,
|
||||
"question_type": q.QuestionType,
|
||||
"question_title": q.QuestionTitle,
|
||||
"score": q.Score,
|
||||
})
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"list": items,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Get detail
|
||||
// @router /exam-questions/:id [get]
|
||||
func (c *ExamQuestionController) GetDetail() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
id, err := c.GetInt64(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
detail, err := services.GetExamQuestionDetail(tenantId, id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "试题不存在", "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": detail}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Create
|
||||
// @router /exam-questions [post]
|
||||
func (c *ExamQuestionController) Create() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
var payload struct {
|
||||
QuestionTitle string `json:"question_title"`
|
||||
QuestionType int8 `json:"question_type"`
|
||||
Score float64 `json:"score"`
|
||||
QuestionAnalysis string `json:"question_analysis"`
|
||||
Options []map[string]string `json:"options"`
|
||||
Answer interface{} `json:"answer"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
q := &models.ExamQuestion{
|
||||
TenantId: tenantId,
|
||||
QuestionType: payload.QuestionType,
|
||||
QuestionTitle: payload.QuestionTitle,
|
||||
QuestionAnalysis: payload.QuestionAnalysis,
|
||||
Score: payload.Score,
|
||||
Status: 1,
|
||||
}
|
||||
var opts []models.ExamQuestionOption
|
||||
for _, o := range payload.Options {
|
||||
opts = append(opts, models.ExamQuestionOption{OptionLabel: o["label"], OptionContent: o["content"]})
|
||||
}
|
||||
answerContent := ""
|
||||
switch v := payload.Answer.(type) {
|
||||
case string:
|
||||
answerContent = v
|
||||
case []interface{}:
|
||||
for i, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
if i == 0 {
|
||||
answerContent = s
|
||||
} else {
|
||||
answerContent += "," + s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
id, err := services.CreateExamQuestion(tenantId, q, opts, answerContent)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "创建失败: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "创建成功", "data": map[string]interface{}{"id": id}}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update
|
||||
// @router /exam-questions/:id [put]
|
||||
func (c *ExamQuestionController) Update() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
id, err := c.GetInt64(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
var payload struct {
|
||||
QuestionTitle string `json:"question_title"`
|
||||
QuestionType int8 `json:"question_type"`
|
||||
Score float64 `json:"score"`
|
||||
QuestionAnalysis string `json:"question_analysis"`
|
||||
Options []map[string]string `json:"options"`
|
||||
Answer interface{} `json:"answer"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
q := &models.ExamQuestion{QuestionType: payload.QuestionType, QuestionTitle: payload.QuestionTitle, QuestionAnalysis: payload.QuestionAnalysis, Score: payload.Score}
|
||||
var opts []models.ExamQuestionOption
|
||||
for _, o := range payload.Options {
|
||||
opts = append(opts, models.ExamQuestionOption{OptionLabel: o["label"], OptionContent: o["content"]})
|
||||
}
|
||||
answerContent := ""
|
||||
switch v := payload.Answer.(type) {
|
||||
case string:
|
||||
answerContent = v
|
||||
case []interface{}:
|
||||
for i, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
if i == 0 {
|
||||
answerContent = s
|
||||
} else {
|
||||
answerContent += "," + s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := services.UpdateExamQuestion(tenantId, id, q, opts, answerContent); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "更新失败: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "更新成功", "data": nil}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete
|
||||
// @router /exam-questions/:id [delete]
|
||||
func (c *ExamQuestionController) Delete() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
id, err := c.GetInt64(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.DeleteExamQuestion(tenantId, id); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "删除失败: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "删除成功", "data": nil}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetBankList
|
||||
// @router /exam-question-banks [get]
|
||||
func (c *ExamQuestionBankController) GetBankList() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
keyword := c.GetString("keyword")
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
list, total, err := services.GetExamQuestionBanks(services.BankListParams{
|
||||
TenantId: tenantId,
|
||||
Keyword: keyword,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "获取试题库列表失败: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": map[string]interface{}{"list": list, "total": total}}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetBankDetail
|
||||
// @router /exam-question-banks/:id [get]
|
||||
func (c *ExamQuestionBankController) GetBankDetail() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
id, err := c.GetInt64(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
bank, err := services.GetExamQuestionBankDetail(tenantId, id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "试题库不存在", "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": bank}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// CreateBank
|
||||
// @router /exam-question-banks [post]
|
||||
func (c *ExamQuestionBankController) CreateBank() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
var payload struct {
|
||||
BankName string `json:"bank_name"`
|
||||
BankDesc string `json:"bank_desc"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
bank := &models.ExamQuestionBank{
|
||||
TenantId: tenantId,
|
||||
BankName: payload.BankName,
|
||||
BankDesc: payload.BankDesc,
|
||||
Status: 1,
|
||||
}
|
||||
id, err := services.CreateExamQuestionBank(tenantId, bank)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "创建失败: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "创建成功", "data": map[string]interface{}{"id": id}}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateBank
|
||||
// @router /exam-question-banks/:id [put]
|
||||
func (c *ExamQuestionBankController) UpdateBank() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
id, err := c.GetInt64(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
var payload struct {
|
||||
BankName string `json:"bank_name"`
|
||||
BankDesc string `json:"bank_desc"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
bank := &models.ExamQuestionBank{
|
||||
Id: id,
|
||||
TenantId: tenantId,
|
||||
BankName: payload.BankName,
|
||||
BankDesc: payload.BankDesc,
|
||||
}
|
||||
if err := services.UpdateExamQuestionBank(tenantId, id, bank); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "更新失败: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "更新成功", "data": nil}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteBank
|
||||
// @router /exam-question-banks/:id [delete]
|
||||
func (c *ExamQuestionBankController) DeleteBank() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
id, err := c.GetInt64(":id")
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.DeleteExamQuestionBank(tenantId, id); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "删除失败: " + err.Error(), "data": nil}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "删除成功", "data": nil}
|
||||
c.ServeJSON()
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
-- MySQL dump 10.13 Distrib 8.0.41, for Win64 (x86_64)
|
||||
-- MySQL dump 10.13 Distrib 8.0.41, for Win64 (x86_64)
|
||||
--
|
||||
-- Host: 212.64.112.158 Database: gotest
|
||||
-- ------------------------------------------------------
|
||||
@ -228,6 +228,30 @@ CREATE TABLE `yz_exam_category` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试分类表(含多租户隔离)';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `yz_exam_question_bank`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `yz_exam_question_bank`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `yz_exam_question_bank` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '题库ID',
|
||||
`tenant_id` int(11) NOT NULL COMMENT '租户ID(多租户隔离)',
|
||||
`bank_name` varchar(100) NOT NULL COMMENT '题库名称',
|
||||
`bank_desc` varchar(500) DEFAULT '' COMMENT '题库描述',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态(1-启用,0-禁用)',
|
||||
`sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序序号',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_status` (`tenant_id`,`status`),
|
||||
KEY `idx_tenant_name` (`tenant_id`,`bank_name`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试题库表(支持租户隔离、软删除)';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `yz_exam_users`
|
||||
--
|
||||
|
||||
79
server/models/exam.go
Normal file
79
server/models/exam.go
Normal file
@ -0,0 +1,79 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// ExamQuestion 对应表 yz_exam_question
|
||||
type ExamQuestion struct {
|
||||
Id int64 `orm:"column(id);auto" json:"id"`
|
||||
TenantId int `orm:"column(tenant_id)" json:"tenant_id"`
|
||||
QuestionType int8 `orm:"column(question_type)" json:"question_type"`
|
||||
QuestionTitle string `orm:"column(question_title);size(1000)" json:"question_title"`
|
||||
QuestionAnalysis string `orm:"column(question_analysis);size(2000);null" json:"question_analysis"`
|
||||
Score float64 `orm:"column(score);digits(5);decimals(2)" json:"score"`
|
||||
SortOrder int8 `orm:"column(sort_order);null" json:"sort_order"`
|
||||
Status int8 `orm:"column(status)" json:"status"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"`
|
||||
}
|
||||
|
||||
func (q *ExamQuestion) TableName() string {
|
||||
return "yz_exam_question"
|
||||
}
|
||||
|
||||
// ExamQuestionOption 对应表 yz_exam_question_option
|
||||
type ExamQuestionOption struct {
|
||||
Id int64 `orm:"column(id);auto" json:"id"`
|
||||
TenantId int `orm:"column(tenant_id)" json:"tenant_id"`
|
||||
QuestionId int64 `orm:"column(question_id)" json:"question_id"`
|
||||
OptionLabel string `orm:"column(option_label);size(10)" json:"option_label"`
|
||||
OptionContent string `orm:"column(option_content);size(500)" json:"option_content"`
|
||||
SortOrder int8 `orm:"column(sort_order);null" json:"sort_order"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"`
|
||||
}
|
||||
|
||||
func (o *ExamQuestionOption) TableName() string {
|
||||
return "yz_exam_question_option"
|
||||
}
|
||||
|
||||
// ExamQuestionAnswer 对应表 yz_exam_question_answer
|
||||
type ExamQuestionAnswer struct {
|
||||
Id int64 `orm:"column(id);auto" json:"id"`
|
||||
TenantId int `orm:"column(tenant_id)" json:"tenant_id"`
|
||||
QuestionId int64 `orm:"column(question_id)" json:"question_id"`
|
||||
AnswerContent string `orm:"column(answer_content);size(1000)" json:"answer_content"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"`
|
||||
}
|
||||
|
||||
func (a *ExamQuestionAnswer) TableName() string {
|
||||
return "yz_exam_question_answer"
|
||||
}
|
||||
|
||||
// ExamQuestionBank 对应表 yz_exam_question_bank
|
||||
type ExamQuestionBank struct {
|
||||
Id int64 `orm:"column(id);auto" json:"id"`
|
||||
TenantId int `orm:"column(tenant_id)" json:"tenant_id"`
|
||||
BankName string `orm:"column(bank_name);size(100)" json:"bank_name"`
|
||||
BankDesc string `orm:"column(bank_desc);size(500);null" json:"bank_desc"`
|
||||
Status int8 `orm:"column(status)" json:"status"`
|
||||
SortOrder int8 `orm:"column(sort_order);null" json:"sort_order"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"`
|
||||
}
|
||||
|
||||
func (b *ExamQuestionBank) TableName() string {
|
||||
return "yz_exam_question_bank"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(ExamQuestion), new(ExamQuestionOption), new(ExamQuestionAnswer), new(ExamQuestionBank))
|
||||
}
|
||||
@ -283,12 +283,20 @@ func init() {
|
||||
beego.Router("/api/files/search", &controllers.FileController{}, "get:SearchFiles")
|
||||
beego.Router("/api/files/statistics", &controllers.FileController{}, "get:GetFileStatistics")
|
||||
|
||||
// 考试题目路由
|
||||
beego.Router("/api/exam-questions", &controllers.ExamQuestionController{}, "get:GetList;post:Create")
|
||||
beego.Router("/api/exam-questions/:id", &controllers.ExamQuestionController{}, "get:GetDetail;put:Update;delete:Delete")
|
||||
|
||||
// 考试题库路由
|
||||
beego.Router("/api/exam-question-banks", &controllers.ExamQuestionBankController{}, "get:GetBankList;post:CreateBank")
|
||||
beego.Router("/api/exam-question-banks/:id", &controllers.ExamQuestionBankController{}, "get:GetBankDetail;put:UpdateBank;delete:DeleteBank")
|
||||
|
||||
// 知识库路由
|
||||
beego.Router("/api/knowledge/list", &controllers.KnowledgeController{}, "get:List")
|
||||
beego.Router("/api/knowledge/count", &controllers.KnowledgeController{}, "get:GetCount")
|
||||
beego.Router("/api/knowledge/detail", &controllers.KnowledgeController{}, "get:Detail")
|
||||
beego.Router("/api/knowledge/create", &controllers.KnowledgeController{}, "post:Create")
|
||||
beego.Router("/api/knowledge/update", &controllers.KnowledgeController{}, "post:Update")
|
||||
// ...
|
||||
beego.Router("/api/knowledge/delete", &controllers.KnowledgeController{}, "post:Delete")
|
||||
beego.Router("/api/knowledge/categories", &controllers.KnowledgeController{}, "get:GetCategories")
|
||||
beego.Router("/api/knowledge/tags", &controllers.KnowledgeController{}, "get:GetTags")
|
||||
@ -314,10 +322,10 @@ func init() {
|
||||
// OA基础数据合并接口(一次性获取部门、职位、角色)
|
||||
beego.Router("/api/oa/base-data/:tenantId", &controllers.OAController{}, "get:GetOABaseData")
|
||||
|
||||
// OA任务管理路由
|
||||
beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask")
|
||||
beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask")
|
||||
beego.Router("/api/oa/tasks/todo", &controllers.TaskController{}, "get:GetTodoTasks")
|
||||
// OA任务管理路由
|
||||
beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask")
|
||||
beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask")
|
||||
beego.Router("/api/oa/tasks/todo", &controllers.TaskController{}, "get:GetTodoTasks")
|
||||
|
||||
// CRM 客户路由
|
||||
beego.Router("/api/crm/customer/list", &controllers.CustomerController{}, "get:List")
|
||||
|
||||
224
server/services/exam.go
Normal file
224
server/services/exam.go
Normal file
@ -0,0 +1,224 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
type QuestionListParams struct {
|
||||
TenantId int
|
||||
Keyword string
|
||||
QuestionType *int8
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type QuestionWithMeta struct {
|
||||
Question *models.ExamQuestion
|
||||
Options []*models.ExamQuestionOption
|
||||
Answer *models.ExamQuestionAnswer
|
||||
}
|
||||
|
||||
type BankListParams struct {
|
||||
TenantId int
|
||||
Keyword string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func GetExamQuestions(params QuestionListParams) ([]*models.ExamQuestion, int64, error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable(new(models.ExamQuestion)).Filter("delete_time__isnull", true).Filter("status", 1)
|
||||
if params.TenantId > 0 {
|
||||
qs = qs.Filter("tenant_id", params.TenantId)
|
||||
}
|
||||
if params.Keyword != "" {
|
||||
qs = qs.Filter("question_title__icontains", params.Keyword)
|
||||
}
|
||||
if params.QuestionType != nil {
|
||||
qs = qs.Filter("question_type", *params.QuestionType)
|
||||
}
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if params.Page <= 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.PageSize <= 0 {
|
||||
params.PageSize = 10
|
||||
}
|
||||
offset := (params.Page - 1) * params.PageSize
|
||||
var list []*models.ExamQuestion
|
||||
_, err = qs.OrderBy("-id").Limit(params.PageSize, offset).All(&list, "Id", "TenantId", "QuestionType", "QuestionTitle", "Score")
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
func GetExamQuestionDetail(tenantId int, id int64) (*QuestionWithMeta, error) {
|
||||
o := orm.NewOrm()
|
||||
q := new(models.ExamQuestion)
|
||||
if err := o.QueryTable(q).Filter("id", id).Filter("delete_time__isnull", true).One(q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantId > 0 && q.TenantId != tenantId {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
var opts []*models.ExamQuestionOption
|
||||
_, _ = o.QueryTable(new(models.ExamQuestionOption)).Filter("question_id", q.Id).Filter("delete_time__isnull", true).All(&opts)
|
||||
a := new(models.ExamQuestionAnswer)
|
||||
_ = o.QueryTable(a).Filter("question_id", q.Id).Filter("delete_time__isnull", true).One(a)
|
||||
return &QuestionWithMeta{Question: q, Options: opts, Answer: a}, nil
|
||||
}
|
||||
|
||||
func CreateExamQuestion(tenantId int, q *models.ExamQuestion, options []models.ExamQuestionOption, answerContent string) (int64, error) {
|
||||
o := orm.NewOrm()
|
||||
if tenantId > 0 {
|
||||
q.TenantId = tenantId
|
||||
}
|
||||
q.Status = 1
|
||||
if _, err := o.Insert(q); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for i := range options {
|
||||
options[i].TenantId = q.TenantId
|
||||
options[i].QuestionId = q.Id
|
||||
_, _ = o.Insert(&options[i])
|
||||
}
|
||||
ans := &models.ExamQuestionAnswer{TenantId: q.TenantId, QuestionId: q.Id, AnswerContent: answerContent}
|
||||
_, _ = o.Insert(ans)
|
||||
return q.Id, nil
|
||||
}
|
||||
|
||||
func UpdateExamQuestion(tenantId int, id int64, q *models.ExamQuestion, options []models.ExamQuestionOption, answerContent string) error {
|
||||
o := orm.NewOrm()
|
||||
existing := models.ExamQuestion{Id: id}
|
||||
if err := o.Read(&existing); err != nil {
|
||||
return err
|
||||
}
|
||||
if tenantId > 0 && existing.TenantId != tenantId {
|
||||
return errors.New("not found")
|
||||
}
|
||||
existing.QuestionType = q.QuestionType
|
||||
existing.QuestionTitle = q.QuestionTitle
|
||||
existing.QuestionAnalysis = q.QuestionAnalysis
|
||||
existing.Score = q.Score
|
||||
if _, err := o.Update(&existing, "QuestionType", "QuestionTitle", "QuestionAnalysis", "Score"); err != nil {
|
||||
return err
|
||||
}
|
||||
// soft delete old options and answer
|
||||
now := time.Now()
|
||||
o.QueryTable(new(models.ExamQuestionOption)).Filter("question_id", id).Filter("delete_time__isnull", true).Update(orm.Params{"delete_time": now})
|
||||
o.QueryTable(new(models.ExamQuestionAnswer)).Filter("question_id", id).Filter("delete_time__isnull", true).Update(orm.Params{"delete_time": now})
|
||||
for i := range options {
|
||||
options[i].TenantId = existing.TenantId
|
||||
options[i].QuestionId = id
|
||||
_, _ = o.Insert(&options[i])
|
||||
}
|
||||
ans := &models.ExamQuestionAnswer{TenantId: existing.TenantId, QuestionId: id, AnswerContent: answerContent}
|
||||
_, _ = o.Insert(ans)
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteExamQuestion(tenantId int, id int64) error {
|
||||
o := orm.NewOrm()
|
||||
existing := models.ExamQuestion{Id: id}
|
||||
if err := o.Read(&existing); err != nil {
|
||||
return err
|
||||
}
|
||||
if tenantId > 0 && existing.TenantId != tenantId {
|
||||
return errors.New("not found")
|
||||
}
|
||||
now := time.Now()
|
||||
if _, err := o.QueryTable(new(models.ExamQuestion)).Filter("id", id).Update(orm.Params{"delete_time": now}); err != nil {
|
||||
return err
|
||||
}
|
||||
o.QueryTable(new(models.ExamQuestionOption)).Filter("question_id", id).Filter("delete_time__isnull", true).Update(orm.Params{"delete_time": now})
|
||||
o.QueryTable(new(models.ExamQuestionAnswer)).Filter("question_id", id).Filter("delete_time__isnull", true).Update(orm.Params{"delete_time": now})
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetExamQuestionBanks(params BankListParams) ([]*models.ExamQuestionBank, int64, error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable(new(models.ExamQuestionBank)).Filter("delete_time__isnull", true).Filter("status", 1)
|
||||
if params.TenantId > 0 {
|
||||
qs = qs.Filter("tenant_id", params.TenantId)
|
||||
}
|
||||
if params.Keyword != "" {
|
||||
qs = qs.Filter("bank_name__icontains", params.Keyword)
|
||||
}
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if params.Page <= 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.PageSize <= 0 {
|
||||
params.PageSize = 10
|
||||
}
|
||||
offset := (params.Page - 1) * params.PageSize
|
||||
var list []*models.ExamQuestionBank
|
||||
_, err = qs.OrderBy("-id").Limit(params.PageSize, offset).All(&list, "Id", "TenantId", "BankName", "BankDesc")
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
func GetExamQuestionBankDetail(tenantId int, id int64) (*models.ExamQuestionBank, error) {
|
||||
o := orm.NewOrm()
|
||||
bank := new(models.ExamQuestionBank)
|
||||
if err := o.QueryTable(bank).Filter("id", id).Filter("delete_time__isnull", true).One(bank); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantId > 0 && bank.TenantId != tenantId {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
return bank, nil
|
||||
}
|
||||
|
||||
func CreateExamQuestionBank(tenantId int, bank *models.ExamQuestionBank) (int64, error) {
|
||||
o := orm.NewOrm()
|
||||
if tenantId > 0 {
|
||||
bank.TenantId = tenantId
|
||||
}
|
||||
bank.Status = 1
|
||||
if _, err := o.Insert(bank); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return bank.Id, nil
|
||||
}
|
||||
|
||||
func UpdateExamQuestionBank(tenantId int, id int64, bank *models.ExamQuestionBank) error {
|
||||
o := orm.NewOrm()
|
||||
existing := models.ExamQuestionBank{Id: id}
|
||||
if err := o.Read(&existing); err != nil {
|
||||
return err
|
||||
}
|
||||
if tenantId > 0 && existing.TenantId != tenantId {
|
||||
return errors.New("not found")
|
||||
}
|
||||
existing.BankName = bank.BankName
|
||||
existing.BankDesc = bank.BankDesc
|
||||
if _, err := o.Update(&existing, "BankName", "BankDesc"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteExamQuestionBank(tenantId int, id int64) error {
|
||||
o := orm.NewOrm()
|
||||
existing := models.ExamQuestionBank{Id: id}
|
||||
if err := o.Read(&existing); err != nil {
|
||||
return err
|
||||
}
|
||||
if tenantId > 0 && existing.TenantId != tenantId {
|
||||
return errors.New("not found")
|
||||
}
|
||||
now := time.Now()
|
||||
if _, err := o.QueryTable(new(models.ExamQuestionBank)).Filter("id", id).Update(orm.Params{"delete_time": now}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user