增加考题管理模块
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user