优化题库代码

This commit is contained in:
李志强 2025-11-18 17:11:18 +08:00
parent ff9a181c99
commit 5fddba8a30
13 changed files with 138 additions and 116 deletions

1
.gitignore vendored
View File

@ -24,6 +24,7 @@ dist
dist-ssr
*.local
server/uploads
server/conf/
# Editor directories and files
.vscode/*

View File

@ -0,0 +1,3 @@
<template>
<div class="p-4">Certificate</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div class="p-4">Create Exam Placeholder</div>
</template>

View File

@ -124,7 +124,8 @@ import '@wangeditor/editor/dist/css/style.css'
const props = defineProps({
visible: { type: Boolean, default: false },
mode: { type: String, default: 'create' },
model: { type: Object, default: null }
model: { type: Object, default: null },
bankId: { type: [Number, String], default: null }
})
const emit = defineEmits(['close', 'saved'])
@ -380,6 +381,7 @@ function handleSubmit() {
options: showOptions.value ? form.options.map(o => ({ key: o.key, label: o.label, content: o.content })) : [],
answer: isMultipleType.value ? [...multiAnswer.value] : form.answer
}
if (props.bankId) payload.bank_id = props.bankId
emit('saved', payload)
} catch (e) {
ElMessage.error('保存失败')

View File

@ -4,7 +4,7 @@
<!-- 顶部工具栏返回 + 标题 -->
<div class="topbar">
<el-button :icon="Back" @click="emit('back')">返回</el-button>
<div class="title">题目列表</div>
<div class="title">{{ bankName }}</div>
</div>
<el-divider />
@ -16,36 +16,38 @@
<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-divider />
<div class="tools">
<el-form-item>
<el-button type="primary" :icon="Plus" @click="handleCreate">新建题目</el-button>
<el-button type="primary" :icon="Plus" @click="handleSmartCreate">智能新建</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
<el-button :icon="Upload" @click="handleBatchImport">批量导入</el-button>
</el-form-item>
</div>
</el-form>
</div>
</div>
<div class="header">
<!-- 操作栏 -->
<!-- <div class="header">
<div class="toolbar">
<el-button type="primary" :icon="Plus" @click="handleCreate">新建题目</el-button>
<el-button type="primary" :icon="Plus" @click="handleSmartCreate">智能新建</el-button>
<div style="flex:1" />
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
<el-button :icon="Upload" @click="handleBatchImport">批量导入</el-button>
</div>
</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_title" label="题目" min-width="260" show-overflow-tooltip>
<template #default="{ row }">
<div v-html="row.question_title"></div>
</template>
</el-table-column>
<el-table-column prop="question_type" label="题型" width="120" align="center">
<template #default="{ row }">
<el-tag>{{ getTypeLabel(row.question_type) }}</el-tag>
@ -72,7 +74,7 @@
</div>
<!-- 弹窗仅用于创建/编辑题目与查看详情 -->
<EditDialog :visible="editVisible" :mode="editMode" :model="editModel" @close="editVisible = false"
<EditDialog :visible="editVisible" :mode="editMode" :model="editModel" :bank-id="props.bankId" @close="editVisible = false"
@saved="handleSaved" />
<DetailDialog :visible="viewVisible" :model="viewModel" @close="viewVisible = false" />
@ -86,15 +88,11 @@
</div>
<el-divider content-position="left">上传 Excel 文件</el-divider>
<el-upload
class="upload-block"
:show-file-list="false"
accept=".xls,.xlsx"
:auto-upload="false"
:on-change="handleImportFileChange"
>
<el-upload class="upload-block" :show-file-list="false" accept=".xls,.xlsx" :auto-upload="false"
:on-change="handleImportFileChange">
<el-button type="primary" :loading="importing">选择 Excel 文件</el-button>
<span style="margin-left: 12px; color:#888; font-size:12px;">表头需包含question_title, question_type, score, question_analysis, option_a, option_b, ... , answer</span>
<span style="margin-left: 12px; color:#888; font-size:12px;">表头需包含question_title, question_type, score,
question_analysis, option_a, option_b, ... , answer</span>
</el-upload>
<template #footer>
@ -109,12 +107,8 @@
<el-form label-width="80px">
<el-form-item label="题型">
<el-select v-model="smartType" placeholder="请选择题型" style="width: 260px;">
<el-option
v-for="opt in typeOptions"
:key="opt.dict_value"
:label="opt.dict_label"
:value="String(opt.dict_value)"
/>
<el-option v-for="opt in typeOptions" :key="opt.dict_value" :label="opt.dict_label"
:value="String(opt.dict_value)" />
</el-select>
</el-form-item>
</el-form>
@ -122,13 +116,8 @@
<p style="margin: 4px 0 8px; font-size: 13px; color: #666;">
请粘贴完整的题目文本包含题干选项答案和解析例如
</p>
<el-input
v-model="smartRawText"
type="textarea"
:rows="10"
placeholder="在这里粘贴题目文本..."
style="margin-top: 12px;"
/>
<el-input v-model="smartRawText" type="textarea" :rows="10" placeholder="在这里粘贴题目文本..."
style="margin-top: 12px;" />
<template #footer>
<div class="dlg-actions">
@ -152,9 +141,6 @@ import { getExamQuestions, getExamQuestionDetail, createExamQuestion, updateExam
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()
@ -173,6 +159,11 @@ const typeDictMap = computed(() => {
return m
})
const props = defineProps({
bankId: { type: [Number, String], default: null },
bankName: { type: String, default: '' },
})
//
function handleDownloadTemplate() {
// 使
@ -451,7 +442,7 @@ function handleImportFileChange(file) {
return
}
importing.value = true
const res = await batchImportExamQuestions({ items })
const res = await batchImportExamQuestions({ bank_id: props.bankId || undefined, items })
if (res && res.code === 0) {
const data = res.data || {}
ElMessage.success(`导入完成:成功 ${data.success ?? 0} 条,失败 ${data.failed ?? 0}`)
@ -668,11 +659,16 @@ onMounted(async () => {
.topbar .title {
font-weight: 600;
font-size: 16px;
font-size: 20px;
}
.search-bar {
/* margin-bottom: 12px; */
.tools {
display: flex;
justify-content: space-between;
}
}
.toolbar {

View File

@ -1,75 +1,50 @@
<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 class="search-bar">
<el-form :inline="true" :model="filters">
<div class="form-actions">
<div class="left">
<el-button type="primary" :icon="Plus" @click="handleCreate">新建题库</el-button>
</div>
<div class="right">
<el-button @click="handleBatchImport" :icon="Upload">批量导入</el-button>
<el-button @click="handleRefresh" :icon="Refresh">刷新</el-button>
</div>
</div>
</el-form>
</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>
<!-- 题库列表 -->
<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 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" />
<!-- 分页 -->
<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" />
<ExamList :bank-id="examListBankId" :bank-name="examListBankName" @back="examListVisible = false" />
</template>
</div>
</template>
@ -111,6 +86,7 @@ const bankModel = ref(null);
const examListVisible = ref(false);
const examListBankId = ref(null);
const examListBankName = ref('');
const auth = useAuthStore();
@ -156,7 +132,7 @@ const handleBatchImport = () => {
ElMessage.info('批量导入功能开发中');
};
const handleView = async (row) => {};
const handleView = async (row) => { };
const handleRefresh = () => {
fetchBankList();
@ -171,6 +147,7 @@ const handleBankSaved = async (payload) => {
}
ElMessage.success('保存成功');
bankVisible.value = false;
await fetchBankList();
} catch (e) {
ElMessage.error('保存失败');
}
@ -184,6 +161,7 @@ const handleEdit = (row) => {
const handleAdd = (row) => {
examListBankId.value = row?.id || null;
examListBankName.value = row?.bank_name || '';
examListVisible.value = true;
};
@ -214,7 +192,7 @@ const handleDelete = async (row) => {
ElMessage.success('删除成功');
fetchBankList();
} catch (error) {
}
};
@ -301,6 +279,7 @@ onMounted(() => {
.form-actions {
display: flex;
gap: 12px;
justify-content: space-between;
}
}

View File

@ -0,0 +1,3 @@
<template>
<div class="p-4">Examinee</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div class="p-4">Practice</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div class="p-4">Training</div>
</template>

View File

@ -6,7 +6,7 @@ runmode = dev
# MySQL - 远程连接配置
mysqluser = gotest
mysqlpass = 2nZhRdMPCNZrdzsd
mysqlurls = 192.168.31.10:3306
mysqlurls = 212.64.112.158:3388
mysqldb = gotest
# ORM配置

View File

@ -24,6 +24,8 @@ func (c *ExamQuestionController) GetList() {
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
keyword := c.GetString("keyword")
typeStr := c.GetString("type")
bankId, _ := c.GetInt64("bank_id", 0)
var qtype *int8
if typeStr != "" {
if iv, err := strconv.Atoi(typeStr); err == nil {
@ -31,12 +33,15 @@ func (c *ExamQuestionController) GetList() {
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,
BankId: bankId,
Page: page,
PageSize: pageSize,
})
@ -45,7 +50,7 @@ func (c *ExamQuestionController) GetList() {
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 {
@ -59,6 +64,7 @@ func (c *ExamQuestionController) GetList() {
"score": q.Score,
})
}
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "ok",
@ -101,6 +107,7 @@ func (c *ExamQuestionController) Create() {
QuestionAnalysis string `json:"question_analysis"`
Options []map[string]string `json:"options"`
Answer interface{} `json:"answer"`
BankId int64 `json:"bank_id"`
}
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil}
@ -115,6 +122,9 @@ func (c *ExamQuestionController) Create() {
Score: payload.Score,
Status: 1,
}
if payload.BankId > 0 {
q.BankId = payload.BankId
}
var opts []models.ExamQuestionOption
for _, o := range payload.Options {
opts = append(opts, models.ExamQuestionOption{OptionLabel: o["label"], OptionContent: o["content"]})
@ -149,6 +159,7 @@ func (c *ExamQuestionController) Create() {
func (c *ExamQuestionController) BatchCreate() {
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
var payload struct {
BankId int64 `json:"bank_id"`
Items []struct {
QuestionTitle string `json:"question_title"`
QuestionType int8 `json:"question_type"`
@ -182,6 +193,9 @@ func (c *ExamQuestionController) BatchCreate() {
Score: item.Score,
Status: 1,
}
if payload.BankId > 0 {
q.BankId = payload.BankId
}
var opts []models.ExamQuestionOption
for _, o := range item.Options {
opts = append(opts, models.ExamQuestionOption{OptionLabel: o["label"], OptionContent: o["content"]})
@ -251,6 +265,7 @@ func (c *ExamQuestionController) Update() {
QuestionAnalysis string `json:"question_analysis"`
Options []map[string]string `json:"options"`
Answer interface{} `json:"answer"`
BankId int64 `json:"bank_id"`
}
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil}
@ -258,6 +273,9 @@ func (c *ExamQuestionController) Update() {
return
}
q := &models.ExamQuestion{QuestionType: payload.QuestionType, QuestionTitle: payload.QuestionTitle, QuestionAnalysis: payload.QuestionAnalysis, Score: payload.Score}
if payload.BankId > 0 {
q.BankId = payload.BankId
}
var opts []models.ExamQuestionOption
for _, o := range payload.Options {
opts = append(opts, models.ExamQuestionOption{OptionLabel: o["label"], OptionContent: o["content"]})

View File

@ -10,6 +10,7 @@ import (
type ExamQuestion struct {
Id int64 `orm:"column(id);auto" json:"id"`
TenantId int `orm:"column(tenant_id)" json:"tenant_id"`
BankId int64 `orm:"column(bank_id);null" json:"bank_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"`

View File

@ -13,6 +13,7 @@ type QuestionListParams struct {
TenantId int
Keyword string
QuestionType *int8
BankId int64
Page int
PageSize int
}
@ -42,6 +43,9 @@ func GetExamQuestions(params QuestionListParams) ([]*models.ExamQuestion, int64,
if params.QuestionType != nil {
qs = qs.Filter("question_type", *params.QuestionType)
}
if params.BankId > 0 {
qs = qs.Filter("bank_id", params.BankId)
}
total, err := qs.Count()
if err != nil {
return nil, 0, err
@ -121,7 +125,13 @@ func UpdateExamQuestion(tenantId int, id int64, q *models.ExamQuestion, options
existing.QuestionTitle = q.QuestionTitle
existing.QuestionAnalysis = q.QuestionAnalysis
existing.Score = q.Score
if _, err := o.Update(&existing, "QuestionType", "QuestionTitle", "QuestionAnalysis", "Score"); err != nil {
// 可选更新题库归属
fields := []string{"QuestionType", "QuestionTitle", "QuestionAnalysis", "Score"}
if q.BankId > 0 {
existing.BankId = q.BankId
fields = append(fields, "BankId")
}
if _, err := o.Update(&existing, fields...); err != nil {
return err
}
// soft delete old options and answer