【修改】修改试卷页面,其它细节

This commit is contained in:
YOHO\20373
2025-05-29 20:13:12 +08:00
parent 457178f093
commit 93891d575e
29 changed files with 5340 additions and 937 deletions

View File

@@ -5,10 +5,16 @@ export interface QuestionQueryVO {
subjectName: string
}
// 查询用户管理列表
// 查询试题列表
export const getQuestionList = (params: QuestionQueryVO) => {
return request.get({ url: '/exam/question/list', params })
}
// 查询试题列表(带答案)
export const getQuestionlistAnswer = (params: QuestionQueryVO) => {
return request.get({ url: '/exam/question/listAnswer', params })
}
export const getQuestionSendList = (params: QuestionQueryVO) => {
return request.get({ url: '/exam/question/sendList', params })
@@ -25,6 +31,12 @@ export async function auditQuestion(data) {
export async function rabbitmqConnect() {
return await request.post({ url: '/rabbitmq/connect' })
}
export async function changePaperQu(data) {
return await request.post({ url: '/exam/qu/changePaperQu',data })
}
export async function changePaperQuRandom(data) {
return await request.post({ url: '/exam/qu/changePaperQuRandom',data })
}
// 获取试题详情
export const getQuestion = (id: number) => {
@@ -63,7 +75,13 @@ export function editQuestionNoAudit(data: any) {
}
// 更改状态
export async function updateQuStatus(quId: string, status: number) {
return await request.put({
url: '/exam/question/updateQuStatus',
data: { quId, status } // 使用data
})
}
export const getQuestionExamineList = (params: QuestionQueryVO) => {
return request.get({ url: '/exam/question/auditList', params })

View File

@@ -72,7 +72,9 @@ export async function getPaperDetailByTaskId(paperId) {
return await request.get({ url: '/exam/qu/getPaper' , params:{paperId} })
}
export async function getPaperDetailCenterByTaskId(paperId) {
return await request.get({ url: '/exam/qu/getPaperCenter' , params:{paperId} })
}
export async function getPaperList(taskId) {
return await request.get({ url: '/exam/paper/getPaperByTaskId' , params:{taskId} })

View File

@@ -0,0 +1,971 @@
<template>
<div class="edit-dialog">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh" class="custom-dialog">
<el-scrollbar>
<div class="main">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-row>
<el-col :span="12">
<el-form-item label="专业" prop="specialtyName">
<el-input v-model="formData.specialtyName" placeholder="请输入专业" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="知识点" prop="pointNames">
<el-input v-model="formData.pointNames" placeholder="请输入知识点" disabled/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="课程" prop="courseName">
<el-input v-model="formData.courseName" placeholder="请输入课程" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- <el-form-item label="题型难度" prop="quLevel">
<el-input v-model="formData.quLevel" placeholder="请输入题型难度" />
</el-form-item> -->
<el-form-item label="题型难度" prop="quLevel">
<el-select
v-model="formData.quLevel"
placeholder="请选择题型难度"
clearable
>
<el-option label="简单" :value="'0'" />
<el-option label="一般" :value="'1'" />
<el-option label="困难" :value="'2'" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="题型" prop="subjectName">
<el-input v-model="formData.subjectName" placeholder="请输入题型" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="章节名称" prop="chapteridDictText">
<el-input v-model="formData.chapteridDictText" placeholder="请输入章节名称" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="'0'">启用</el-radio>
<el-radio :label="'1'">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="edit-bottom">
<div class="edit-left bottom-common">
<el-tabs v-model="leftActiveName" class="demo-tabs">
<el-tab-pane label="试题描述" name="desc">
<div class="block">
<Editor v-model="formData.content" height="250px" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<div class="edit-right bottom-common">
<el-tabs v-model="rightActiveName" class="demo-tabs" @tab-click="rightHandleClick">
<el-tab-pane label="试题解析" name="analysis">
<div class="block">
<Editor v-model="formData.analysis" height="250px" />
</div>
</el-tab-pane>
<el-tab-pane name="keyword">
<template #label>
<div class="custom-tabs-label">
<p>关键字</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="editKeyword('create')">新建</el-dropdown-item>
<el-dropdown-item @click="editKeyword('update')">编辑</el-dropdown-item>
<el-dropdown-item @click="editKeyword('delete')">删除</el-dropdown-item>
<el-dropdown-item @click="editKeyword('deleteall')"
>删除全部</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block">
<el-table
:data="keywordList"
style="width: 100%"
@selection-change="handleKeywordSelectionChange"
>
<el-table-column type="index" width="50" />
<el-table-column type="selection" width="55" />
<el-table-column prop="keyword" label="关键字" />
</el-table>
<el-dialog
v-model="keyVisible"
title="编辑关键字"
width="50%"
:before-close="keyDialogClose"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="main" style="width: 100%; height: 100%">
<el-input v-model="keyWord" placeholder="请输入关键字" size="large" />
<div class="dialog-footer" style="margin-top: 20px;">
<el-button @click="keyDialogClose">取消</el-button>
<el-button type="primary" @click="confirmKeyDialogVisible">
确定
</el-button>
</div>
</div>
</el-dialog>
</div>
</el-tab-pane>
<!-- <el-tab-pane name="medium">
<template #label>
<div class="custom-tabs-label">
<p>媒体文件</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-upload
v-model:file-list="fileList"
class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
>
<el-button>导入</el-button>
</el-upload>
</el-dropdown-item>
<el-dropdown-item>导出</el-dropdown-item>
<el-dropdown-item>播放</el-dropdown-item>
<el-dropdown-item>编辑</el-dropdown-item>
<el-dropdown-item>删除选中</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block">
<el-table
:data="mediumList"
style="width: 100%"
@selection-change="handleMediumSelectionChange"
>
<el-table-column type="index" width="50" />
<el-table-column type="selection" width="55" />
<el-table-column prop="type" label="媒体类型" width="80" />
<el-table-column prop="title" label="标题" />
<el-table-column prop="displayIndex" label="显示序号" />
<el-table-column prop="size" label="大小" />
</el-table>
</div>
</el-tab-pane> -->
<el-tab-pane v-if="formType === 'update'" name="point">
<template #label>
<div class="custom-tabs-label">
<p>考点设置</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="setKao()">考点设置</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block" style="height: 300px;width: 100% ;">
<el-table
:data="kaodianList"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="scoreRate" label="权值" width="90" />
<el-table-column prop="content" label="文件名(不带后缀)" width="260" />
<el-table-column prop="contentIn" label="考察类型" width="300" />
</el-table>
</div>
<!-- 弹框 -->
<el-dialog
title="考点设置"
v-model="kaoDialogVisible"
width="60%"
:close-on-click-modal="false"
class="custom-dialog"
>
<!-- 可滚动容器 -->
<div style="height: 400px; overflow-y: auto;">
<el-table
:data="kaodianList"
style="width: 100%;"
row-key="answerId"
:default-expand-all="false"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column label="文件名(不带后缀)" width="260">
<template #default="scope">
<el-input
v-model="scope.row.content"
size="small"
type="input"
/>
</template>
</el-table-column>
<el-table-column label="权值" width="100">
<template #default="scope">
<el-input
v-model="scope.row.scoreRate"
size="small"
type="number"
/>
</template>
</el-table-column>
<el-table-column label="考察类型" width="300">
<template #default="scope">
<el-select
v-model="scope.row.contentIn"
placeholder="请选择类型"
size="small"
style="width: 100%"
>
<el-option label="添加到收藏夹" value="添加到收藏夹" />
<el-option label="添加到文件夹" value="添加到文件夹" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="removeKaodian(scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 增加按钮 -->
<div style="margin-top: 12px; text-align: center;">
<el-button type="primary" @click="addKaodianRow">
添加考点
</el-button>
</div>
</div>
<template #footer>
<el-button @click="kaoDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmKao">确定</el-button>
</template>
</el-dialog>
</el-tab-pane>
<el-tab-pane name="annex">
<template #label>
<div class="custom-tabs-label">
<p>试题附件</p>
<!-- <el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>导入单文件</el-dropdown-item>
<el-dropdown-item>导入多文件</el-dropdown-item>
<el-dropdown-item>导出</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
<el-dropdown-item>帮助</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> -->
</div>
</template>
<!-- 提示 -->
<el-alert type="warning" show-icon :closable="false">
<template #default>
<span>提示文件名称可默认不设置</span>
</template>
</el-alert>
<div class="block">
<el-table :data="formData.fileUploads" style="width: 100%">
<el-table-column type="index" label="#" width="50" />
<el-table-column label="文件名称" width="250">
<template #default="scope">
<el-input
v-model="scope.row.fileName"
size="small"
placeholder="请输入文件名称"
/>
</template>
</el-table-column>
<el-table-column
prop="fileType"
label="类型"
:formatter="fileTypeFormatter"
/>
<el-table-column prop="url" label="地址" width="200">
<template #default="{ row }">
<div
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"
:title="row.url"
>
{{ row.url }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="320">
<template #default="scope">
<el-button
type="primary"
plain
@click="openForm(scope.row.fileType)"
size="small"
>
<Icon icon="ep:upload" class="mr-5px" /> 上传
</el-button>
<el-button
type="success"
plain
@click="downloadFile(scope.row.url)"
size="small"
>
<Icon icon="ep:download" class="mr-5px" /> 下载
</el-button>
<el-button
type="danger"
plain
@click="deleteUrl(scope.$index)"
size="small"
><Icon icon="ep:download" class="mr-5px" />删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</el-scrollbar>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</div>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="FileRef" @success="handleUploadSuccess"/>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { FormRules } from 'element-plus'
import * as QuestionApi from '@/api/paper/question'
import FileForm from './components/FileForm.vue';
defineOptions({ name: 'ChoiceForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
//右键菜单状态
const kaoDialogVisible = ref(false)
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
content: '',
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '',
pointNames: '',
audit: '',
subjectName: '',
status: ' ',
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
})
const kaodianList = ref<any[]>([])
function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '1') return '考试文件'
return '未知类型'
}
//考点
// formData.value.quId 中包含 quId假设它已有值
const kaodianData = ref({
quId: '', // 你打开编辑弹窗时会赋值
})
function setKao() {
kaoDialogVisible.value = true
}
function confirmKao() {
if (!kaodianList.value.length) {
ElMessage.warning("没有考点数据");
return;
}
// 构建 questionAnswerList
const questionAnswerList = kaodianList.value.map(item => ({
answerId: item.answerId,
content: item.content,
quId:kaodianData.value.quId,
contentIn:item.contentIn,
scoreRate:item.scoreRate
}));
const payload = {
quId:kaodianData.value.quId,
questionAnswerList
};
console.log('确认后的结果:', payload)
QuestionApi.setBrowserPoint(payload)
kaoDialogVisible.value= false;
}
// 删除行
function removeKaodian(index: number) {
kaodianList.value.splice(index, 1)
}
// 增加行
function addKaodianRow() {
kaodianList.value.push({
answerId: '', // 用你项目里生成 ID 的方法
content: '',
quId:kaodianData.value.quId,
scoreRate: 1,
contentIn: ''
})
}
const formRules = reactive<FormRules>({
// specialtyName: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
// 左侧试题描述
const leftActiveName = ref('desc')
// 右侧tab
const rightActiveName = ref('analysis')
const rightHandleClick = (tab, e) => {
rightActiveName.value = tab.paneName.value
if (tab.paneName === 'point' ) {
// 发起考点请求
QuestionApi.getListByQuId(kaodianData.value.quId).then(res => {
// res 是接口返回的数据数组
kaodianList.value = res;
});
}
}
const radio = ref('A')
// 保留选项的值
const optionsContent = reactive({})
// 关键字
const keywordList = ref([] as any)
const multipleKeywordSelection = ref([] as any)
const handleKeywordSelectionChange = (val: any) => {
multipleKeywordSelection.value = val
}
const keyVisible = ref(false)
const keyEditType = ref('')
const keyWord = ref('')
const editKeyword = (key: string) => {
keyEditType.value = key
if (key === 'create') {
keyWord.value = ''
keyVisible.value = true
} else if (key === 'update') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择一个要编辑的关键字')
return
}
keyWord.value = multipleKeywordSelection.value[0].keyword
keyVisible.value = true
} else if (key === 'delete') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择要删除的关键字')
return
}
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
ElMessage.success('已删除选中项')
} else if (key === 'deleteall') {
keywordList.value = []
ElMessage.success('已清空关键字列表')
}
updateKeywordsToForm()
}
const updateKeywordsToForm = () => {
const keywordStr = keywordList.value
.map(item => item.keyword)
.filter(k => k && k.trim() !== '')
.join(',')
formData.value.keywords = keywordStr
console.log(formData.value.keywords+"formData.value.keywords")
}
const keyDialogClose = () => {
keyVisible.value = false
}
const confirmKeyDialogVisible = () => {
if (keyEditType.value === 'create') {
keywordList.value.push({
keyword: keyWord.value
})
} else if (keyEditType.value === 'update') {
multipleKeywordSelection.value.forEach(item => {
item.keyword = keyWord.value
})
} else if (keyEditType.value === 'delete') {
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
} else if (keyEditType.value === 'deleteall') {
keywordList.value = []
}
updateKeywordsToForm()
keyVisible.value = false
}
/** 添加/修改操作 */
const FileRef = ref()
const openForm = (type: string) => {
FileRef.value.open(type)
}
// 媒体文件
const mediumList = ref([] as any)
const multipleMediumSelection = ref([] as any)
const handleMediumSelectionChange = (val: any) => {
multipleMediumSelection.value = val
}
const fileList = []
const multipleDocumentSelection = ref([] as any)
const handleDocumentSelectionChange = (val: any) => {
multipleDocumentSelection.value = val
}
//文件
const handleUploadSuccess = ({ url, fileType }) => {
const index = formData.value.fileUploads.findIndex(item => item.fileType === fileType)
if (index !== -1) {
formData.value.fileUploads[index].url = url
}
}
const downloadFile = async (url: string) => {
if (!url) {
ElMessage.warning('暂无可下载的文件地址')
return
}
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error('下载失败')
}
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
// 提取文件名
const filename = url.substring(url.lastIndexOf('/') + 1).split('?')[0]
// 创建 a 标签并下载
const a = document.createElement('a')
a.href = blobUrl
a.download = filename
a.style.display = 'none'
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(blobUrl)
} catch (err: any) {
ElMessage.error(`下载失败:${err.message}`)
}
}
const deleteUrl = (index: number) => {
formData.value.fileUploads[index].url = ''
formData.value.fileUploads[index].fileName = ''
}
/** 打开弹窗 */
const open = async (queryParams: any ,type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
rightActiveName.value = 'analysis'
// 修改时,设置数据
if (id) {
kaodianData.value.quId=id;
formLoading.value = true
try {
const res = await QuestionApi.getQuestionnotId(id);
// 默认两个类型
const fileTypes = ['1'];
// 后端返回的上传文件列表(可能为空)
const documentList = res.fileUploads ?? [];
// 遍历两种类型,找到对应的上传文件,如果没有就用默认值
const fileUploads= fileTypes.map(type => {
const match = documentList.find(file => file.fileType === type);
return {
quId: match?.quId ?? res.quId ?? '',
url: match?.url ?? '',
fileType: type,
fileName: match?.fileName ?? ''
};
});
formData.value = {
...res,
fileUploads,
};
keywordList.value = res.keywords
? res.keywords.split(',').filter(item => item.trim() !== '').map(item => ({ keyword: item.trim() }))
: []
} catch (error) {
console.error("获取问题失败:", error);
} finally {
formLoading.value = false
}
} else {
resetForm()
formData.value.specialtyName = queryParams.specialtyName
formData.value.courseName = queryParams.courseName
formData.value.subjectName = queryParams.subjectName
formData.value.pointNames=queryParams.pointNames
formData.value.chapteridDictText=queryParams.chapteridDictText
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
// 转换函数:将大写字母 A-Z 映射为 0-25
const mappedNumber = computed(() => {
const char = radio.value.toUpperCase();
const code = char.charCodeAt(0);
if (code >= 65 && code <= 90) {
return code - 65;
} else {
return '请输入 A-Z 的字母';
}
});
// 选择答案数据
const answerData = ref([]);
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return;
const valid = await formRef.value.validate();
if (!valid) return;
formLoading.value = true;
try {
// 深拷贝一份 formData
const data = JSON.parse(JSON.stringify(formData.value));
// 过滤掉 url 为空的文件项
data.fileUploads = data.fileUploads?.filter(file => file.url && file.url.trim() !== '');
await QuestionApi.editQuestionNoAudit(data);
message.success(t('common.updateSuccess'));
dialogVisible.value = false;
emit('success');
} finally {
formLoading.value = false;
}
};
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: `---------------------------------------------------------------------
请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。
---------------------------------------------------------------------
`,
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '0',
pointNames: '',
audit: '',
subjectName: '',
status: '0',
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
}
keywordList.value=[],
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.edit-dialog {
:deep(.el-dialog) {
display: flex;
flex-direction: column;
.el-dialog__header {
border-bottom: 1px solid #ededed;
}
.el-dialog__footer {
border-top: 1px solid #ededed;
}
.el-dialog__body {
flex: 1;
overflow: hidden;
.main {
.el-form {
padding: 10px;
.el-row {
justify-content: space-between;
margin-bottom: 10px;
}
.el-form-item {
width: 100%;
margin-bottom: 0;
align-items: center;
}
}
.edit-bottom {
display: flex;
justify-content: space-between;
padding-bottom: 10px;
.bottom-common {
width: calc(50% - 10px);
.el-tabs {
height: 100%;
.el-tabs__content {
flex: 1;
overflow-y: auto;
.block {
width: 100%;
height: 80%;
overflow: auto;
}
.answer {
.tip {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
padding: 10px 20px;
display: flex;
align-items: center;
> p {
flex-shrink: 0;
}
}
.el-radio-group {
width: 100%;
display: inline-flex;
align-items: flex-start;
font-size: 0;
flex-direction: column;
.options {
width: 100%;
margin-top: 15px;
.content {
display: flex;
.text {
width: 100%;
height: 70px;
border: 1px solid #ededed;
.el-textarea__inner {
resize: none;
}
}
}
.more-btn {
margin-top: 8px;
background-color: #ffffff;
border-color: #007bff;
color: #007bff;
width: auto;
height: auto;
padding: 10px 10px;
}
}
}
}
}
.custom-tabs-label {
display: flex;
align-items: center;
.setting_icon {
width: 16px;
height: 16px;
background: url('@/assets/icon/setting_blue.png') no-repeat center;
background-size: 100%;
margin-left: 3px;
display: none;
}
}
.is-active {
.custom-tabs-label {
.setting_icon {
display: block;
}
}
}
:deep(.ele-pro-table) {
flex: 1;
margin-top: 10px;
.el-table--fit {
height: 100%;
}
}
}
}
}
}
}
}
}
:deep(.tox-tinymce) {
.tox-statusbar {
display: none;
}
}
:deep(.el-table) {
.el-table__header-wrapper {
.el-table__header {
thead {
tr {
th {
background: #ebebeb;
}
}
}
}
}
}
</style>

View File

@@ -349,9 +349,12 @@ const open = async (queryParams: any ,type: string, id?: number) => {
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
console.log(id+"idid")
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await QuestionApi.getQuestionnotId(id)
@@ -419,13 +422,10 @@ const submitForm = async () => {
formLoading.value = true
try {
const data = formData.value as unknown
if (formType.value === 'create') {
await QuestionApi.addQuestion(data)
message.success(t('common.createSuccess'))
} else {
await QuestionApi.editQuestionNoAudit(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
@@ -449,8 +449,14 @@ const resetForm = () => {
audit: '',
subjectName: '',
status: '',
resourceValue: ''
resourceValue: '',
}
radio.value = 'A',
// 清空 optionsContent 的属性
Object.keys(optionsContent).forEach(key => {
delete optionsContent[key]
})
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,993 @@
<template>
<div class="edit-dialog">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh" class="custom-dialog">
<el-scrollbar>
<div class="main">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-row>
<el-col :span="12">
<el-form-item label="专业" prop="specialtyName">
<el-input v-model="formData.specialtyName" placeholder="请输入专业" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="知识点" prop="pointNames">
<el-input v-model="formData.pointNames" placeholder="请输入知识点" disabled/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="课程" prop="courseName">
<el-input v-model="formData.courseName" placeholder="请输入课程" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- <el-form-item label="题型难度" prop="quLevel">
<el-input v-model="formData.quLevel" placeholder="请输入题型难度" />
</el-form-item> -->
<el-form-item label="题型难度" prop="quLevel">
<el-select
v-model="formData.quLevel"
placeholder="请选择题型难度"
clearable
>
<el-option label="简单" :value="'0'" />
<el-option label="一般" :value="'1'" />
<el-option label="困难" :value="'2'" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="题型" prop="subjectName">
<el-input v-model="formData.subjectName" placeholder="请输入题型" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="章节名称" prop="chapteridDictText">
<el-input v-model="formData.chapteridDictText" placeholder="请输入章节名称" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="'0'">启用</el-radio>
<el-radio :label="'1'">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="edit-bottom">
<div class="edit-left bottom-common">
<el-tabs v-model="leftActiveName" class="demo-tabs">
<el-tab-pane label="试题描述" name="desc">
<div class="block">
<Editor v-model="formData.content" height="250px" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<div class="edit-right bottom-common">
<el-tabs v-model="rightActiveName" class="demo-tabs" @tab-click="rightHandleClick">
<el-tab-pane label="试题解析" name="analysis">
<div class="block">
<Editor v-model="formData.analysis" height="250px" />
</div>
</el-tab-pane>
<el-tab-pane name="keyword">
<template #label>
<div class="custom-tabs-label">
<p>关键字</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="editKeyword('create')">新建</el-dropdown-item>
<el-dropdown-item @click="editKeyword('update')">编辑</el-dropdown-item>
<el-dropdown-item @click="editKeyword('delete')">删除</el-dropdown-item>
<el-dropdown-item @click="editKeyword('deleteall')"
>删除全部</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block">
<el-table
:data="keywordList"
style="width: 100%"
@selection-change="handleKeywordSelectionChange"
>
<el-table-column type="index" width="50" />
<el-table-column type="selection" width="55" />
<el-table-column prop="keyword" label="关键字" />
</el-table>
<el-dialog
v-model="keyVisible"
title="编辑关键字"
width="50%"
:before-close="keyDialogClose"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="main" style="width: 100%; height: 100%">
<el-input v-model="keyWord" placeholder="请输入关键字" size="large" />
<div class="dialog-footer" style="margin-top: 20px;">
<el-button @click="keyDialogClose">取消</el-button>
<el-button type="primary" @click="confirmKeyDialogVisible">
确定
</el-button>
</div>
</div>
</el-dialog>
</div>
</el-tab-pane>
<!-- <el-tab-pane name="medium">
<template #label>
<div class="custom-tabs-label">
<p>媒体文件</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-upload
v-model:file-list="fileList"
class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
>
<el-button>导入</el-button>
</el-upload>
</el-dropdown-item>
<el-dropdown-item>导出</el-dropdown-item>
<el-dropdown-item>播放</el-dropdown-item>
<el-dropdown-item>编辑</el-dropdown-item>
<el-dropdown-item>删除选中</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block">
<el-table
:data="mediumList"
style="width: 100%"
@selection-change="handleMediumSelectionChange"
>
<el-table-column type="index" width="50" />
<el-table-column type="selection" width="55" />
<el-table-column prop="type" label="媒体类型" width="80" />
<el-table-column prop="title" label="标题" />
<el-table-column prop="displayIndex" label="显示序号" />
<el-table-column prop="size" label="大小" />
</el-table>
</div>
</el-tab-pane> -->
<el-tab-pane v-if="formType === 'update'" name="point">
<el-alert type="warning" show-icon :closable="false">
<template #default>
<span>提示考点导入需上传考试和结果文件</span>
</template>
</el-alert>
<template #label>
<div class="custom-tabs-label">
<p>考点设置</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="setKao()">考点设置</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block" style="height: 300px;width: 100% ;">
<el-table
:data="kaodianList"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="scoreRate" label="权值" width="90" />
<el-table-column prop="content" label="文件/文件夹名(带后缀)" width="260" />
<el-table-column prop="contentIn" label="考察类型" width="300" />
</el-table>
</div>
<!-- 弹框 -->
<el-dialog
title="考点设置"
v-model="kaoDialogVisible"
width="60%"
:close-on-click-modal="false"
class="custom-dialog"
>
<!-- 可滚动容器 -->
<div style="height: 400px; overflow-y: auto;">
<el-table
:data="kaodianList"
style="width: 100%;"
row-key="answerId"
:default-expand-all="false"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column label="文件/文件夹名(带后缀)" width="260">
<template #default="scope">
<el-input
v-model="scope.row.content"
size="small"
type="input"
/>
</template>
</el-table-column>
<el-table-column label="权值" width="100">
<template #default="scope">
<el-input
v-model="scope.row.scoreRate"
size="small"
type="number"
/>
</template>
</el-table-column>
<el-table-column label="考察类型" width="300">
<template #default="scope">
<el-select
v-model="scope.row.contentIn"
placeholder="请选择类型"
size="small"
style="width: 100%"
>
<el-option label="考察名称" value="考察名称" />
<el-option label="考察删除" value="考察删除" />
<el-option label="考察属性" value="考察属性" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="removeKaodian(scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 增加按钮 -->
<div style="margin-top: 12px; text-align: center;">
<el-button type="primary" @click="setKaodianRow">
导入考点
</el-button>
<el-button type="primary" @click="addKaodianRow">
添加考点
</el-button>
</div>
</div>
<template #footer>
<el-button @click="kaoDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmKao">确定</el-button>
</template>
</el-dialog>
</el-tab-pane>
<el-tab-pane name="annex">
<template #label>
<div class="custom-tabs-label">
<p>试题附件</p>
<!-- <el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>导入单文件</el-dropdown-item>
<el-dropdown-item>导入多文件</el-dropdown-item>
<el-dropdown-item>导出</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
<el-dropdown-item>帮助</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> -->
</div>
</template>
<!-- 提示 -->
<el-alert type="warning" show-icon :closable="false">
<template #default>
<span>提示文件名称可默认不设置</span>
</template>
</el-alert>
<div class="block">
<el-table :data="formData.fileUploads" style="width: 100%">
<el-table-column type="index" label="#" width="50" />
<el-table-column label="文件名称" width="250">
<template #default="scope">
<el-input
v-model="scope.row.fileName"
size="small"
placeholder="请输入文件名称"
/>
</template>
</el-table-column>
<el-table-column
prop="fileType"
label="类型"
:formatter="fileTypeFormatter"
/>
<el-table-column prop="url" label="地址" width="200">
<template #default="{ row }">
<div
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"
:title="row.url"
>
{{ row.url }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="320">
<template #default="scope">
<el-button
type="primary"
plain
@click="openForm(scope.row.fileType)"
size="small"
>
<Icon icon="ep:upload" class="mr-5px" /> 上传
</el-button>
<el-button
type="success"
plain
@click="downloadFile(scope.row.url)"
size="small"
>
<Icon icon="ep:download" class="mr-5px" /> 下载
</el-button>
<el-button
type="danger"
plain
@click="deleteUrl(scope.$index)"
size="small"
><Icon icon="ep:download" class="mr-5px" />删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</el-scrollbar>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</div>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="FileRef" @success="handleUploadSuccess"/>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { FormRules } from 'element-plus'
import * as QuestionApi from '@/api/paper/question'
import FileForm from './components/FileForm.vue';
defineOptions({ name: 'ChoiceForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
content: '',
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '',
pointNames: '',
audit: '',
subjectName: '',
status: ' ',
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
})
function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '1') return '考试文件'
if (cellValue === '2') return '结果文件'
return '未知类型'
}
//考点
const kaoDialogVisible = ref(false)
const kaodianData = ref({
quId: '', // 你打开编辑弹窗时会赋值
})
function setKao() {
kaoDialogVisible.value = true
}
const kaodianList = ref<any[]>([])
function confirmKao() {
if (!kaodianList.value.length) {
ElMessage.warning("没有考点数据");
return;
}
// 构建 questionAnswerList
const questionAnswerList = kaodianList.value.map(item => ({
answerId: item.answerId,
content: item.content,
quId:kaodianData.value.quId,
contentIn:item.contentIn,
scoreRate:item.scoreRate
}));
const payload = {
quId:kaodianData.value.quId,
questionAnswerList
};
console.log('确认后的结果:', payload)
QuestionApi.setBrowserPoint(payload)
kaoDialogVisible.value= false;
}
// 增加行
function addKaodianRow() {
kaodianList.value.push({
answerId: '', // 用你项目里生成 ID 的方法
content: '',
quId:kaodianData.value.quId,
scoreRate: 1,
contentIn: ''
})
}
// 删除行
function removeKaodian(index: number) {
kaodianList.value.splice(index, 1)
}
const setKaodianRow =async () => {
const uploads = formData.value.fileUploads;
if (!uploads[0].url || !uploads[1].url) {
ElMessage.error('请先上传两个文件再导入考点');
return;
}
const fileUrl1 = uploads[0].url;
const fileUrl2 = uploads[1].url;
console.log('导入考点的文件地址为:', fileUrl1, fileUrl2);
const params = {
answerPath: fileUrl1,
shucaiPath: fileUrl2 // 如果不传可以是空字符串,也可以删除这个字段(根据后端是否必填)
};
const res = await QuestionApi.getFilePoint(params);
kaodianList.value=res;
// 示例:调用后端接口进行导入
// importKaodianFromFiles(fileUrl1, fileUrl2);
};
const formRules = reactive<FormRules>({
// specialtyName: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
// 左侧试题描述
const leftActiveName = ref('desc')
// 右侧tab
const rightActiveName = ref('analysis')
const rightHandleClick = (tab, e) => {
rightActiveName.value = tab.paneName.value
if (tab.paneName === 'point' ) {
// 发起考点请求
QuestionApi.getListByQuId(kaodianData.value.quId).then(res => {
// res 是接口返回的数据数组
kaodianList.value = res;
});
}
}
const radio = ref('A')
// 保留选项的值
const optionsContent = reactive({})
// 关键字
const keywordList = ref([] as any)
const multipleKeywordSelection = ref([] as any)
const handleKeywordSelectionChange = (val: any) => {
multipleKeywordSelection.value = val
}
const keyVisible = ref(false)
const keyEditType = ref('')
const keyWord = ref('')
const editKeyword = (key: string) => {
keyEditType.value = key
if (key === 'create') {
keyWord.value = ''
keyVisible.value = true
} else if (key === 'update') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择一个要编辑的关键字')
return
}
keyWord.value = multipleKeywordSelection.value[0].keyword
keyVisible.value = true
} else if (key === 'delete') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择要删除的关键字')
return
}
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
ElMessage.success('已删除选中项')
} else if (key === 'deleteall') {
keywordList.value = []
ElMessage.success('已清空关键字列表')
}
updateKeywordsToForm()
}
const updateKeywordsToForm = () => {
const keywordStr = keywordList.value
.map(item => item.keyword)
.filter(k => k && k.trim() !== '')
.join(',')
formData.value.keywords = keywordStr
console.log(formData.value.keywords+"formData.value.keywords")
}
const keyDialogClose = () => {
keyVisible.value = false
}
const confirmKeyDialogVisible = () => {
if (keyEditType.value === 'create') {
keywordList.value.push({
keyword: keyWord.value
})
} else if (keyEditType.value === 'update') {
multipleKeywordSelection.value.forEach(item => {
item.keyword = keyWord.value
})
} else if (keyEditType.value === 'delete') {
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
} else if (keyEditType.value === 'deleteall') {
keywordList.value = []
}
updateKeywordsToForm()
keyVisible.value = false
}
/** 添加/修改操作 */
const FileRef = ref()
const openForm = (type: string) => {
FileRef.value.open(type)
}
// 媒体文件
const mediumList = ref([] as any)
const multipleMediumSelection = ref([] as any)
const handleMediumSelectionChange = (val: any) => {
multipleMediumSelection.value = val
}
const fileList = []
const multipleDocumentSelection = ref([] as any)
const handleDocumentSelectionChange = (val: any) => {
multipleDocumentSelection.value = val
}
//文件
const handleUploadSuccess = ({ url, fileType }) => {
const index = formData.value.fileUploads.findIndex(item => item.fileType === fileType)
if (index !== -1) {
formData.value.fileUploads[index].url = url
}
}
const downloadFile = async (url: string) => {
if (!url) {
ElMessage.warning('暂无可下载的文件地址')
return
}
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error('下载失败')
}
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
// 提取文件名
const filename = url.substring(url.lastIndexOf('/') + 1).split('?')[0]
// 创建 a 标签并下载
const a = document.createElement('a')
a.href = blobUrl
a.download = filename
a.style.display = 'none'
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(blobUrl)
} catch (err: any) {
ElMessage.error(`下载失败:${err.message}`)
}
}
const deleteUrl = (index: number) => {
formData.value.fileUploads[index].url = ''
formData.value.fileUploads[index].fileName = ''
}
/** 打开弹窗 */
const open = async (queryParams: any ,type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
rightActiveName.value = 'analysis'
// 修改时,设置数据
if (id) {
kaodianData.value.quId=id;
formLoading.value = true
try {
const res = await QuestionApi.getQuestionnotId(id);
// 默认两个类型
const fileTypes = ['1', '2'];
// 后端返回的上传文件列表(可能为空)
const documentList = res.fileUploads ?? [];
// 遍历两种类型,找到对应的上传文件,如果没有就用默认值
const fileUploads = fileTypes.map(type => {
const match = documentList.find(file => file.fileType === type);
return {
quId: match?.quId ?? res.quId ?? '',
url: match?.url ?? '',
fileType: type,
fileName: match?.fileName ?? ''
};
});
formData.value = {
...res,
fileUploads,
};
keywordList.value = res.keywords
? res.keywords.split(',').filter(item => item.trim() !== '').map(item => ({ keyword: item.trim() }))
: []
} finally {
formLoading.value = false
}
} else {
resetForm()
formData.value.specialtyName = queryParams.specialtyName
formData.value.courseName = queryParams.courseName
formData.value.subjectName = queryParams.subjectName
formData.value.pointNames=queryParams.pointNames
formData.value.chapteridDictText=queryParams.chapteridDictText
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
// 转换函数:将大写字母 A-Z 映射为 0-25
const mappedNumber = computed(() => {
const char = radio.value.toUpperCase();
const code = char.charCodeAt(0);
if (code >= 65 && code <= 90) {
return code - 65;
} else {
return '请输入 A-Z 的字母';
}
});
// 选择答案数据
const answerData = ref([]);
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return;
const valid = await formRef.value.validate();
if (!valid) return;
formLoading.value = true;
try {
// 深拷贝一份 formData
const data = JSON.parse(JSON.stringify(formData.value));
// 过滤掉 url 为空的文件项
data.fileUploads = data.fileUploads?.filter(file => file.url && file.url.trim() !== '');
await QuestionApi.editQuestionNoAudit(data)
message.success(t('common.updateSuccess'));
dialogVisible.value = false;
emit('success');
} finally {
formLoading.value = false;
}
};
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: `---------------------------------------------------------------------
请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。
---------------------------------------------------------------------
`,
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '0',
pointNames: '',
audit: '',
subjectName: '',
status: '0',
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
}
keywordList.value=[],
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.edit-dialog {
:deep(.el-dialog) {
display: flex;
flex-direction: column;
.el-dialog__header {
border-bottom: 1px solid #ededed;
}
.el-dialog__footer {
border-top: 1px solid #ededed;
}
.el-dialog__body {
flex: 1;
overflow: hidden;
.main {
.el-form {
padding: 10px;
.el-row {
justify-content: space-between;
margin-bottom: 10px;
}
.el-form-item {
width: 100%;
margin-bottom: 0;
align-items: center;
}
}
.edit-bottom {
display: flex;
justify-content: space-between;
padding-bottom: 10px;
.bottom-common {
width: calc(50% - 10px);
.el-tabs {
height: 100%;
.el-tabs__content {
flex: 1;
overflow-y: auto;
.block {
width: 100%;
height: 80%;
overflow: auto;
}
.answer {
.tip {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
padding: 10px 20px;
display: flex;
align-items: center;
> p {
flex-shrink: 0;
}
}
.el-radio-group {
width: 100%;
display: inline-flex;
align-items: flex-start;
font-size: 0;
flex-direction: column;
.options {
width: 100%;
margin-top: 15px;
.content {
display: flex;
.text {
width: 100%;
height: 70px;
border: 1px solid #ededed;
.el-textarea__inner {
resize: none;
}
}
}
.more-btn {
margin-top: 8px;
background-color: #ffffff;
border-color: #007bff;
color: #007bff;
width: auto;
height: auto;
padding: 10px 10px;
}
}
}
}
}
.custom-tabs-label {
display: flex;
align-items: center;
.setting_icon {
width: 16px;
height: 16px;
background: url('@/assets/icon/setting_blue.png') no-repeat center;
background-size: 100%;
margin-left: 3px;
display: none;
}
}
.is-active {
.custom-tabs-label {
.setting_icon {
display: block;
}
}
}
:deep(.ele-pro-table) {
flex: 1;
margin-top: 10px;
.el-table--fit {
height: 100%;
}
}
}
}
}
}
}
}
}
:deep(.tox-tinymce) {
.tox-statusbar {
display: none;
}
}
:deep(.el-table) {
.el-table__header-wrapper {
.el-table__header {
thead {
tr {
th {
background: #ebebeb;
}
}
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
<template>
<Dialog v-model="dialogVisible" title="上传文件">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="uploadUrl"
:auto-upload="false"
:data="data"
:disabled="formLoading"
:limit="1"
:on-change="handleFileChange"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
:http-request="httpRequest"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text"> 将文件拖到此处 <em>点击上传</em></div>
<template #tip>
<!-- <div class="el-upload__tip" style="color: red">
提示仅允许导入 jpgpnggif 格式文件
</div> -->
</template>
</el-upload>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitFileForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'InfraFileForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const fileList = ref([]) // 文件列表
const data = ref({ path: '' })
const uploadRef = ref()
const { uploadUrl, httpRequest } = useUpload()
const currentType = ref(null)
/** 打开弹窗 */
const open = async (type) => {
currentType.value = type // 记录类型
console.log(currentType.value+"123123")
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 处理上传的文件发生变化 */
const handleFileChange = (file) => {
data.value.path = file.name
}
/** 提交表单 */
const submitFileForm = () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
unref(uploadRef)?.submit()
}
/** 文件上传成功处理 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitFormSuccess = (response: any, file: any) => {
// response 是后端接口返回的数据
// file 是当前上传的文件对象
// 假设后端返回格式:{ url: '文件地址', fileName: '文件名' }
const url = response.data
console.log(currentType.value+"v")
dialogVisible.value = false
formLoading.value = false
uploadRef.value?.clearFiles()
message.success(t('common.createSuccess'))
// 触发成功事件,携带文件信息传递给父组件
emit('success', { url, fileType: currentType.value })
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
// 重置上传状态和文件
formLoading.value = false
uploadRef.value?.clearFiles()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>

View File

@@ -295,6 +295,12 @@
<UserImportForm ref="importFormRef" @success="getList" />
<!-- 分配角色 -->
<UserAssignRoleForm ref="assignRoleFormRef" @success="getList" />
<MdesignForm ref="mformRef" @success="getList" />
<BdesignForm ref="bformRef" @success="getList" />
<FdesignForm ref="fformRef" @success="getList" />
</template>
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
@@ -305,7 +311,9 @@ import * as QuestionApi from '@/api/paper/question'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import ChoiceForm from './ChoiceForm.vue'
import CdesignForm from './CdesignForm.vue'
import MdesignForm from './MysqlForm.vue'
import BdesignForm from './BrowerForm.vue'
import FdesignForm from './FileForm.vue'
import UserImportForm from './UserImportForm.vue'
import UserAssignRoleForm from './UserAssignRoleForm.vue'
import {handleTree} from "@/utils/tree";
@@ -624,17 +632,30 @@ const findNamePathFromTreeList = (treeList: Tree[], targetId: number): string[]
/** 添加/修改操作 */
const formRef = ref()
const cformRef = ref()
const mformRef = ref()
const bformRef = ref()
const fformRef = ref()
const wformRef = ref()
const openForm = (type: string, row: any) => {
console.log(queryParams)
// if (queryParams.subjectName == "") {
// return message.confirm('请选择题型!');
// }
// if (chooseQuestionType.value.includes("选择题")){
// cformRef.value.open(type, id)
// } else {
// formRef.value.open(queryParams, type, id)
// }
formRef.value.open(queryParams, type, row.quId)
console.log(row.subjectName+"subjectName");
if (row.subjectName == "") {
return message.confirm('请选择题型!');
}
if (row.subjectName.includes("选择题")){
console.log(row.quId+"row.quId")
formRef.value.open(queryParams,type, row.quId)
} else if(row.subjectName.includes("编程题")) {
cformRef.value.open(queryParams,type, row.quId)
}else if(row.subjectName.includes("程序设计")) {
mformRef.value.open(queryParams,type, row.quId)
}else if(row.subjectName.includes("网络题")) {
bformRef.value.open(queryParams,type, row.quId)
}else if(row.subjectName.includes("文件处理")) {
fformRef.value.open(queryParams,type, row.quId)
} else if (row.subjectName.includes("文字")){
wformRef.value.open(queryParams,type, row.quId)
}
}
/** 用户导入 */

View File

@@ -447,7 +447,6 @@ const formData = ref({
analysis: '',
quLevel: '',
pointNames: '',
audit: '',
subjectName: '',
status: ' ',
keywords: '',
@@ -725,7 +724,9 @@ formData.value = {
...res,
fileUploads,
};
keywordList.value = res.keywords
? res.keywords.split(',').filter(item => item.trim() !== '').map(item => ({ keyword: item.trim() }))
: []
} catch (error) {
console.error("获取问题失败:", error);
} finally {
@@ -793,10 +794,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: `---------------------------------------------------------------------
请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。
---------------------------------------------------------------------
`,
content: `<p>---------------------------------------------------------------------</p><p> 请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。</p><p>---------------------------------------------------------------------</p><p>`,
specialtyName: '',
courseName: '',
quBankName: '',
@@ -805,7 +803,6 @@ const resetForm = () => {
analysis: '',
quLevel: '0',
pointNames: '',
audit: '',
subjectName: '',
status: '0',
keywords: '',

View File

@@ -65,18 +65,13 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="'0'">启用</el-radio>
<el-radio :label="'1'">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">

View File

@@ -436,7 +436,7 @@ const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
content: '',
content: '<p>---------------------------------------------------------------------</p><p> 请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。</p><p>---------------------------------------------------------------------</p><p>',
specialtyName: '',
courseName: '',
quBankName: '',
@@ -445,7 +445,6 @@ const formData = ref({
analysis: '',
quLevel: '',
pointNames: '',
audit: '',
subjectName: '',
status: ' ',
keywords: '',
@@ -749,7 +748,9 @@ formData.value = {
};
keywordList.value = res.keywords.split(',').map(item => ({ keyword: item.trim() }));
keywordList.value = res.keywords
? res.keywords.split(',').filter(item => item.trim() !== '').map(item => ({ keyword: item.trim() }))
: []
} finally {
formLoading.value = false
@@ -816,9 +817,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: `---------------------------------------------------------------------
请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。
---------------------------------------------------------------------
content: `<p>---------------------------------------------------------------------</p><p> 请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。</p><p>---------------------------------------------------------------------</p><p>
`,
specialtyName: '',
courseName: '',
@@ -828,7 +827,6 @@ const resetForm = () => {
analysis: '',
quLevel: '0',
pointNames: '',
audit: '',
subjectName: '',
status: '0',
keywords: '',

View File

@@ -443,7 +443,7 @@ const formData = ref({
analysis: '',
quLevel: '',
pointNames: '',
audit: '',
subjectName: '',
status: ' ',
keywords: '',
@@ -868,7 +868,9 @@ formData.value = {
...res,
fileUploads,
};
keywordList.value = res.keywords
? res.keywords.split(',').filter(item => item.trim() !== '').map(item => ({ keyword: item.trim() }))
: []
} finally {
formLoading.value = false
@@ -936,14 +938,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: `---------------------------------------------------------------------------
注意事项:
请利用图形化管理界面或者MySQL命令行工具在指定的试题数据库中进行答题。
MySQL连接地址为localhost
MySQL登录用户名为root
MySQL端口号为6033或3306
MySQL密码为空
---------------------------------------------------------------------------`,
content: `<p style="text-align: left;"><span style="font-family: 宋体;">--------------------------------------------------------------------------- </span></p><p style="text-align: left;"><span style="font-family: 宋体;">注意事项:</span></p><p style="text-align: left;"><span style="font-family: 宋体;">请利用图形化管理界面或者MySQL命令行工具在指定的试题数据库中进行答题。 </span></p><p style="text-align: left;"><span style="font-family: 宋体;">MySQL连接地址为localhost</span></p><p style="text-align: left;"><span style="font-family: 宋体;">MySQL登录用户名为root</span></p><p style="text-align: left;"><span style="font-family: 宋体;">MySQL端口号为3306或者6033</span></p><p style="text-align: left;"><span style="font-family: 宋体;">MySQL密码为空</span></p><p style="text-align: left;"><span style="font-family: 宋体;">---------------------------------------------------------------------------</span>`,
specialtyName: '',
courseName: '',
quBankName: '',
@@ -952,7 +947,6 @@ MySQL密码为空
analysis: '',
quLevel: '0',
pointNames: '',
audit: '',
subjectName: '',
status: '0',
keywords: '',

View File

@@ -1,181 +1,634 @@
<template>
<Dialog v-model="visible" :title="'编辑试卷'" width="80%" height="10%" @open="handleOpen" center >
<ContentWrap>
<el-button @click="handleSelect"> <Icon icon="ep:refresh" class="mr-5px" />刷新</el-button>
<el-button @click="handleChange"><Icon icon="ep:sort" class="mr-5px" /> 换题</el-button>
<div style="display: flex; gap: 20px; height: 500px; width: 100%;">
<!-- 左侧题目表格 -->
<div style="flex: 3; overflow: auto;">
<el-table
v-loading="loading"
:data="questionList"
@row-click="handleQuestionRowClick"
highlight-current-row
ref="questionTableRef"
>
<el-table-column label="序号" prop="index" align="center" width="60" />
<el-table-column label="编号" prop="questionIndex" align="center" width="80" />
<el-table-column label="题型" prop="subjectName" align="center" />
<el-table-column label="难度" prop="quLevel" align="center">
<template #default="scope">
<dict-tag :type="DICT_TYPE.EXAM_QUE_DIFF" :value="scope.row.quLevel" />
</template>
</el-table-column>
<el-table-column label="知识点" prop="pointNames" align="left" />
</el-table>
</div>
<!-- 右侧题目详情 -->
<div style=" flex: 4; border: 1px solid #ebeef5; padding: 16px; border-radius: 4px; overflow: auto; height: 100%;">
<template v-if="selectedQuestion">
<div style="font-weight: bold; margin-bottom: 8px;">题目描述</div>
<div v-html="selectedQuestion.content " style="margin-bottom: 16px;"></div>
<template v-if="isChoiceQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">选项及答案</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div v-for="(answer, index) in selectedQuestion.answerList" :key="answer.answerId" style="margin-bottom: 4px;">
{{ optionLabel(index) }}. {{ answer.content }}
<span v-if="answer.isRight === '0'" style="color: green; font-weight: bold">正确</span>
</div>
</div>
<div v-else style="color: #999;">暂无答案</div>
</template>
<Dialog v-model="visible" :title="'修改试卷'" width="460" @open="handleOpen" center>
<!-- <div style="margin-bottom: 16px">
<strong>试卷ID</strong>{{ paperId }}
</div> -->
<!-- 滚动区域 -->
<div style="max-height: 400px; overflow-y: auto;">
<!-- 分组展示题目 -->
<div
v-for="(items, subject) in sortedGroupedQuestions"
:key="subject"
>
<h4 class="mb-2 text-base text-gray-700 font-medium">
{{ subject }}
<span v-if="schemeMap[subject]" class="text-sm text-gray-500 font-normal">
每小题 {{ parseFloat(schemeMap[subject].quScores).toFixed(1) }} {{ parseFloat(schemeMap[subject].subtotalScore).toFixed(1) }}
</span>
</h4>
<el-card
v-for="(item, index) in items"
:key="item.quId"
class="mb-3"
shadow="never"
>
<div class="text-gray-500 mb-2">题目 {{ index + 1 }}</div>
<div v-html="item.content" class="mb-2"></div>
<!-- 🎯 判断是否为编程题 -->
<div v-if="item.subjectName === '编程题'">
<!-- 编程题解析 -->
<div class="mt-2">
<div class="font-bold text-blue-600">解析</div>
<pre
style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;"
>{{ item.analysis }}</pre>
<!-- 程序设计题 -->
<template v-else-if="isProgrammingQuestion(selectedQuestion.subjectName)">
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div style="margin-bottom: 8px; white-space: pre-wrap;">
<strong>考点语句</strong><br />
{{ answer.content }}
</div>
<div
v-for="(answer, aIndex) in item.answerList"
:key="answer.answerId"
class="mb-1"
>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}%
<div v-if="answer.examMysqlKeywordList && answer.examMysqlKeywordList.length">
<div style="margin-top: 10px;"><strong>关键词列表</strong></div>
<ul style="padding-left: 20px;">
<li
v-for="keyword in answer.examMysqlKeywordList"
:key="keyword.keywordId"
style="line-height: 1.6;"
>
- 关键词{{ keyword.keyword }}权值{{ keyword.scoreRate }}
</li>
</ul>
</div>
</div>
<!-- 📝 普通题目的选项显示 -->
<div v-else-if="item.answerList && item.answerList.length">
<div
v-for="(answer, aIndex) in item.answerList.slice().sort((a, b) => a.sort - b.sort)"
:key="answer.answerId"
:style="{
color: answer.isRight === '0' ? 'green' : 'inherit',
fontWeight: answer.isRight === '0' ? 'bold' : 'normal'
}"
class="mb-1"
>
{{ String.fromCharCode(65 + aIndex) }}. {{ answer.content }}
</div>
</div>
</el-card>
</div>
<div v-else style="color: #999;">暂无程序设计答案</div>
</template>
<!-- 编程题 -->
<template v-else-if="isCodingTestQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">答案</div>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;">
{{ selectedQuestion.analysis }}
</pre>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
</div>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<div style="font-weight: bold; margin-bottom: 8px;">判分测试用例</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>输入</strong>{{ answer.contentIn }}</div>
<div><strong>输出</strong>{{ answer.content }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无测试用例信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分关键字</div>
<div v-if="selectedQuestion.questionKeywords && selectedQuestion.questionKeywords.length">
<div
v-for="answer in selectedQuestion.questionKeywords"
:key="answer.keywordId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>关键字</strong>{{ answer.keyword }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无关键字信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分标准</div>
<div v-if="selectedQuestion.questionScores">
<div
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>检查程序编译</strong>{{ selectedQuestion.questionScores.isPass === '0' ? '是' : '否' }} , <strong>检查程序编译百分比</strong>{{ selectedQuestion.questionScores.isPassScore }}%</div>
<div><strong>检查程序结果</strong>{{ selectedQuestion.questionScores.isResult === '0' ? '是' : '否' }} ,<strong>检查程序结果百分比</strong>{{ selectedQuestion.questionScores.isResultScore }}%</div>
<div><strong>检查关键字</strong>{{ selectedQuestion.questionScores.isKeyword === '0' ? '是' : '否' }} , <strong>检查关键字百分比</strong>{{ selectedQuestion.questionScores.isKeywordScore }}%</div>
<div><strong>使用测试用例</strong>{{ selectedQuestion.questionScores.isCompile === '0' ? '是' : '否' }} , <strong>使用测试用例百分比</strong>{{ selectedQuestion.questionScores.isCompileScore }}%</div>
<div><strong>关键字得分临界值</strong>{{ selectedQuestion.questionScores.keywordCutoff }}%</div>
<div><strong>测试用例得分临界值小于等于测试用例个数</strong>{{ selectedQuestion.questionScores.compileCutoff }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无输入输出信息</div>
</template>
<template v-else>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div v-for="answer in selectedQuestion.answerList" :key="answer.answerId" style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace">
<div><strong>考察点</strong>{{ answer.content }}</div>
<div><strong>考察类型</strong>{{ answer.contentIn }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无考点信息</div>
</template>
</template>
</Dialog>
<template v-else>
<div style="color: #999;">请点击左侧试题查看详细内容</div>
</template>
</div>
</div>
</ContentWrap>
<template #footer>
<el-button @click="handleCancel"> </el-button>
</template>
</Dialog>
<Dialog v-model="visibleChange" :title="'换题列表'" width="80%" center @close="handleCancelQue" >
<ContentWrap>
<div style="display: flex; gap: 20px;height: 500px; width: 100%;">
<!-- 左侧题目表格 -->
<div style="flex: 3;">
<!-- 搜索条件区域 -->
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<el-input
v-model="queryParams.pointNames"
placeholder="试题知识点"
clearable
style="flex: 1; min-width: 180px;"
/>
<el-select
v-model="queryParams.quLevel"
placeholder="难度"
clearable
style="flex: 1; min-width: 180px;"
>
<el-option label="简单" value="0" />
<el-option label="一般" value="1" />
<el-option label="困难" value="2" />
</el-select>
<el-input
v-model="queryParams.quId"
placeholder="题号"
clearable
style="flex: 1; min-width: 180px;"
/>
<el-button type="primary" @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
<el-button @click="resetSearch"> <Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
<el-button @click="resetRandom" > 随机换题</el-button>
</div>
<el-table
v-loading="loading"
:data="list"
ref="changeTableRef"
highlight-current-row
@row-click="handleRowClick"
>
<el-table-column label="试题编号" align="center" key="id" prop="quId" :show-overflow-tooltip="true"/>
<el-table-column label="专业" align="center" prop="specialtyName" width="120" />
<el-table-column label="课程" align="center" prop="courseName" :show-overflow-tooltip="true" />
<el-table-column label="章节名称" align="center" prop="chapteridDictText" :show-overflow-tooltip="true" />
<el-table-column label="难度" align="center" prop="quLevel" :show-overflow-tooltip="true">
<template #default="scope">
<dict-tag :type="DICT_TYPE.EXAM_QUE_DIFF" :value="scope.row.quLevel" />
</template>
</el-table-column>
<el-table-column label="知识点" align="center" prop="pointNames" :show-overflow-tooltip="true" />
<!-- <el-table-column label="审核状态" align="center" prop="audit" :show-overflow-tooltip="true">
<template #default="scope">
<dict-tag :type="DICT_TYPE.QUESTION_AUDIT" :value="scope.row.audit" />
</template>
</el-table-column>
<el-table-column label="状态" key="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYS_STATUS" :value="scope.row.status" />
</template>
</el-table-column> -->
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
<!-- 右侧题目详情 -->
<div style="flex: 4; border: 1px solid #ebeef5; padding: 16px; border-radius: 4px; overflow-y: auto;">
<template v-if="selectedSwapQuestion">
<div style="font-weight: bold; margin-bottom: 8px;">题目描述</div>
<div v-html="selectedSwapQuestion.content" style="margin-bottom: 16px;"></div>
<template v-if="isChoiceQuestion(selectedSwapQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">选项及答案</div>
<div v-if="selectedSwapQuestion.answerList && selectedSwapQuestion.answerList.length">
<div
v-for="(answer, index) in selectedSwapQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 4px;"
>
{{ optionLabel(index) }}. {{ answer.content }}
<span v-if="answer.isRight === '0'" style="color: green; font-weight: bold">正确</span>
</div>
</div>
<div v-else style="color: #999;">暂无答案</div>
</template>
<!-- 编程题 -->
<template v-else-if="isCodingTestQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">答案</div>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;">
{{ selectedQuestion.analysis }}
</pre>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分测试用例</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>输入</strong>{{ answer.contentIn }}</div>
<div><strong>输出</strong>{{ answer.content }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无测试用例信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分关键字</div>
<div v-if="selectedQuestion.questionKeywords && selectedQuestion.questionKeywords.length">
<div
v-for="answer in selectedQuestion.questionKeywords"
:key="answer.keywordId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>关键字</strong>{{ answer.keyword }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无关键字信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分标准</div>
<div v-if="selectedQuestion.questionScores">
<div
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>检查程序编译</strong>{{ selectedQuestion.questionScores.isPass === '0' ? '是' : '否' }} , <strong>检查程序编译百分比</strong>{{ selectedQuestion.questionScores.isPassScore }}%</div>
<div><strong>检查程序结果</strong>{{ selectedQuestion.questionScores.isResult === '0' ? '是' : '否' }} ,<strong>检查程序结果百分比</strong>{{ selectedQuestion.questionScores.isResultScore }}%</div>
<div><strong>检查关键字</strong>{{ selectedQuestion.questionScores.isKeyword === '0' ? '是' : '否' }} , <strong>检查关键字百分比</strong>{{ selectedQuestion.questionScores.isKeywordScore }}%</div>
<div><strong>使用测试用例</strong>{{ selectedQuestion.questionScores.isCompile === '0' ? '是' : '否' }} , <strong>使用测试用例百分比</strong>{{ selectedQuestion.questionScores.isCompileScore }}%</div>
<div><strong>关键字得分临界值</strong>{{ selectedQuestion.questionScores.keywordCutoff }}%</div>
<div><strong>测试用例得分临界值小于等于测试用例个数</strong>{{ selectedQuestion.questionScores.compileCutoff }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无输入输出信息</div>
</template>
<template v-else-if="isProgrammingQuestion(selectedQuestion.subjectName)">
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div style="margin-bottom: 8px; white-space: pre-wrap;">
<strong>考点语句</strong><br />
{{ answer.content }}
</div>
<div v-if="answer.examMysqlKeywordList && answer.examMysqlKeywordList.length">
<div style="margin-top: 10px;"><strong>关键词列表</strong></div>
<ul style="padding-left: 20px;">
<li
v-for="keyword in answer.examMysqlKeywordList"
:key="keyword.keywordId"
style="line-height: 1.6;"
>
- 关键词{{ keyword.keyword }}权值{{ keyword.scoreRate }}
</li>
</ul>
</div>
</div>
</div>
<div v-else style="color: #999;">暂无程序设计答案</div>
</template>
<template v-else>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedSwapQuestion.answerList && selectedSwapQuestion.answerList.length">
<div
v-for="answer in selectedSwapQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace"
>
<div><strong>考察点</strong>{{ answer.content }}</div>
<div><strong>考察类型</strong>{{ answer.contentIn }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无考点信息</div>
</template>
</template>
<template v-else>
<div style="color: #999;">请点击左侧题目查看详情</div>
</template>
</div>
</div>
</ContentWrap>
<template #footer>
<el-button @click="handleCancelQue"> </el-button>
<el-button type="primary" @click="handleConfirmChange"> </el-button>
</template>
<script setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { getPaperDetailByTaskId } from '@/api/system/paper';
const message = useMessage() // 消息弹窗
const props = defineProps({
paperId: String
});
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean });
const loading = ref(false);
const formRef = ref(null);
const questionList = ref([]); // 用于存放试题列表
const schemeMap = ref({});
const [form, resetFields, assignFields] = useFormData({
paperId: void 0,
taskId: '',
counts: '',
rollUp: '',
isAb: '',
status: ''
});
const rules = reactive({});
const handleCancel = () => {
visible.value = false;
</Dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { getPaperDetailCenterByTaskId } from '@/api/system/paper';
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const message = useMessage(); // 消息弹窗
import * as QuestionApi from '@/api/paper/question'
const props = defineProps({
paperId: String // 接收父组件传来的 paperId
});
const queryParams = reactive({
quLevel: "",
pointNames: "",
subjectName: "",
quId:"",
chapteridDictText:"",
pageNo: 1,
pageSize: 10,
audit:"0",
status:"0",
specialtyName:"",
courseName:"",
})
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean }); // 对应 v-model
const visibleChange = defineModel('visibleChange', { type: Boolean }); // 对应 v-model:visibleChange
const selectedChange = ref<any[]>([]);
const selectedSwapQuestion = ref<any>(null); // 换题弹窗中点击的题目
const questionTableRef = ref();
const selectedQuestion = ref<any>(null); // 第一个弹窗选中的试题
const handleQuestionRowClick = (row: any) => {
selectedQuestion.value = row;
};
const questionList = ref<any[]>([]);
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数
const handleCancel = () => {
visible.value = false;
};
const handleChange =async () => {
if (!selectedQuestion.value) {
message.warning('请先选中一个原题');
return;
}
visibleChange.value = true;
await getList();
};
const handleSelect =async () => {
await handleOpen();
};
const handleCancelQue = () => {
visibleChange.value = false;
list.value=[];
selectedSwapQuestion.value = null; // 🔁 清空右侧题目内容
selectedChange.value = []; // (可选)清空选中项
list.value = [];
};
const resetSearch = () => {
queryParams.pointNames = '';
queryParams.quLevel = '';
queryParams.quId = '';
queryParams.pageNo = 1;
getList();
};
const resetRandom = () => {
const originalId = selectedQuestion.value.quId || selectedQuestion.value.id;
const sub=selectedQuestion.value.subjectName;
console.log('原题 ID:', originalId);
console.log('试卷ID:',props.paperId)
console.log('原题题型:',sub)
// 构造参数
const payload = {
paperId: props.paperId,
oldQuId: originalId,
subjectName:sub,
newQuId: null,
};
const groupedQuestions = ref({});
const subjectPriority = {
'选择题': 1,
'多选题': 2,
'判断题': 3,
'编程题': 4,
'其他': 5
};
const groupBySubjectName = (list) => {
const group = {};
list.forEach((item) => {
const subject = item.subjectName || '其他'; // 默认为'其他'
if (!group[subject]) {
group[subject] = [];
}
group[subject].push(item);
try {
QuestionApi.changePaperQuRandom( payload); // 根据实际路径修改
message.success('换题成功');
} catch (error) {
console.error('换题请求出错:', error);
message.error('请求失败,请稍后重试');
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const changeTableRef = ref();
const handleRowClick = (row: any) => {
selectedChange.value = [row]; // 只选中一个换题
selectedSwapQuestion.value = row;
};
/** 查询列表 */
const getList = async () => {
loading.value = true
queryParams.subjectName=selectedQuestion.value.subjectName
try {
const data = await QuestionApi.getQuestionlistAnswer(queryParams);
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
// 打开弹窗时加载题目数据
const handleOpen = async () => {
loading.value = true;
try {
const res = await getPaperDetailCenterByTaskId(props.paperId);
const rawList = res.examQuestionList || [];
const schemeList = res.educationPaperSchemeList || [];
// 构建题型 => sort 数字映射,例如 { '选择题': 1, '文件处理': 2, ... }
const sortMap = {};
schemeList.forEach((scheme) => {
sortMap[scheme.spName] = Number(scheme.sort);
});
groupedQuestions.value = group;
// 先排序:根据题型的 sort 排序
const sortedRawList = [...rawList].sort((a, b) => {
return (sortMap[a.subjectName] || 999) - (sortMap[b.subjectName] || 999);
});
// 每种题型的编号计数器
const typeCounter = {};
let globalIndex = 1;
questionList.value = sortedRawList.map((q) => {
const groupIndex = sortMap[q.subjectName] || 0;
typeCounter[groupIndex] = (typeCounter[groupIndex] || 0) + 1;
const questionIndex = `${groupIndex}.${typeCounter[groupIndex]}`;
return {
...q,
index: globalIndex++, // 全局序号 1、2、3…
questionIndex, // 按题型分组的编号 1.1、1.2、2.1…
};
});
} finally {
loading.value = false;
}
};
const handleConfirmChange = () => {
if (selectedChange.value.length !== 1) {
message.warning('请选择一个换题');
return;
}
const originalId = selectedQuestion.value.quId || selectedQuestion.value.id;
const newId = selectedChange.value[0].quId || selectedChange.value[0].id;
console.log('原题 ID:', originalId);
console.log('换题 ID:', newId);
console.log('试卷ID:',props.paperId)
// 构造参数
const payload = {
paperId: props.paperId,
oldQuId: originalId,
newQuId: newId,
};
const sortedGroupedQuestions = computed(() => {
return Object.keys(groupedQuestions.value)
.sort((a, b) => (subjectPriority[a] || subjectPriority['其他']) - (subjectPriority[b] || subjectPriority['其他']))
.reduce((acc, subject) => {
acc[subject] = groupedQuestions.value[subject];
return acc;
}, {});
});
const handleOpen = () => {
resetFields();
questionList.value = [];
if (props.paperId) {
loading.value = true;
getPaperDetailByTaskId(props.paperId)
.then((res) => {
const { educationPaperSchemeList, examQuestionList } = res.data || {};
if (Array.isArray(examQuestionList)) {
questionList.value = examQuestionList;
groupBySubjectName(examQuestionList); // 分类
}
if (Array.isArray(educationPaperSchemeList)) {
// 构建以题型名为 key 的映射表spName 与 scheme 对应)
schemeMap.value = educationPaperSchemeList.reduce((acc, item) => {
acc[item.spName] = item;
return acc;
}, {});
}
assignFields(res);
})
.catch((e) => {
message.error(`获取试卷详情失败: ${e.message}`);
})
.finally(() => {
loading.value = false;
nextTick(() => {
formRef.value?.clearValidate?.();
});
});
}
};
</script>
try {
QuestionApi.changePaperQu( payload); // 根据实际路径修改
message.success('换题成功');
} catch (error) {
console.error('换题请求出错:', error);
message.error('请求失败,请稍后重试');
}
// 关闭弹窗
visibleChange.value = false;
};
const choiceSubjects = ["选择题", "单选题", "多选题"];
const programmingSubjects = ["程序设计"];
function isChoiceQuestion(subjectName) {
return choiceSubjects.some(item => subjectName.includes(item));
}
function isProgrammingQuestion(subjectName) {
return programmingSubjects.some(item => subjectName.includes(item));
}
const isCodingTestQuestion = (subjectName: string): boolean => {
return subjectName === '编程题';
};
function optionLabel(index) {
return String.fromCharCode(65 + index); // A, B, C, D ...
}
</script>

View File

@@ -44,13 +44,13 @@
style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;"
>{{ item.analysis }}</pre>
</div>
<div
<!-- <div
v-for="(answer, aIndex) in item.answerList"
:key="answer.answerId"
class="mb-1"
>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}%
</div>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}
</div> -->
</div>
<!-- 📝 普通题目的选项显示 -->

View File

@@ -162,6 +162,7 @@
v-model="showEdit"
:data="current"
ref="taskEditRef"
:paper-Id="currentPaperId"
@done="reload"
/>
<paper-look
@@ -319,11 +320,15 @@ const openDown = async () => {
}
};
// 打开编辑弹窗
const openEdit = (type: string, row?: object) => {
const openEdit = (type: string, row?: any)=> {
if (row?.counts > 0) {
ElMessage.warning('当前试卷使用次数大于0不允许更换试题');
return;
}
showEdit.value = true;
current.value = row;
currentPaperId.value = row.paperId ?? null;
nextTick(() => {
taskEditRef.value?.open(type, row);
taskEditRef.value?.open(type, row.paperId);
});
};
const currentPaperId = ref<number | null>(null);

View File

@@ -128,26 +128,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="12">
<el-form-item label="测评时长" >
<el-time-picker
v-model="form.examTime"
@@ -158,11 +139,30 @@
/>
</el-form-item>
</el-col>
<!-- <el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col> -->
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="定时检查与学生端联通性,每">
<!-- <el-form-item label="定时检查与学生端联通性,每">
<el-input-number v-model="form.isConnect" label="分钟" @change="handleFormChange" />
<span>分钟传一次,断联直接交卷</span>
</el-form-item>
</el-form-item> -->
</el-col>
</el-row>

View File

@@ -147,12 +147,12 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- <el-col :span="12">
<el-form-item label="定时检查与学生端联通性,每">
<el-input-number v-model="form.isConnect" label="分钟" @change="handleFormChange" />
<span>分钟传一次,断联直接交卷</span>
</el-form-item>
</el-col>
</el-col> -->
</el-row>
</el-form>
</el-tab-pane>

View File

@@ -129,25 +129,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="12">
<el-form-item label="测评时长" >
<el-time-picker
v-model="form.examTime"
@@ -158,12 +140,30 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- <el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col> -->
</el-row>
<el-row :gutter="20">
<!-- <el-col :span="12">
<el-form-item label="定时检查与学生端联通性,每">
<el-input-number v-model="form.isConnect" label="分钟" @change="handleFormChange" />
<span>分钟传一次,断联直接交卷</span>
</el-form-item>
</el-col>
</el-col> -->
</el-row>

View File

@@ -130,24 +130,7 @@
<el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="12">
<el-form-item label="测评时长" >
<el-time-picker
v-model="form.examTime"
@@ -158,12 +141,29 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- <el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col> -->
</el-row>
<el-row :gutter="20">
<!-- <el-col :span="12">
<el-form-item label="定时检查与学生端联通性,每">
<el-input-number v-model="form.isConnect" label="分钟" @change="handleFormChange" />
<span>分钟传一次,断联直接交卷</span>
</el-form-item>
</el-col>
</el-col> -->
</el-row>

View File

@@ -130,23 +130,6 @@
<el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="测评时长" >
<el-time-picker
@@ -157,13 +140,30 @@
@change="handleFormChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
</el-col>
<!-- <el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col> -->
</el-row>
<el-row :gutter="20">
<!-- <el-col :span="12">
<el-form-item label="定时检查与学生端联通性,每">
<el-input-number v-model="form.isConnect" label="分钟" @change="handleFormChange" />
<span>分钟传一次,断联直接交卷</span>
</el-form-item>
</el-col>
</el-col> -->
</el-row>

View File

@@ -1,181 +1,634 @@
<template>
<Dialog v-model="visible" :title="'编辑试卷'" width="80%" height="10%" @open="handleOpen" center >
<ContentWrap>
<el-button @click="handleSelect"> <Icon icon="ep:refresh" class="mr-5px" />刷新</el-button>
<el-button @click="handleChange"><Icon icon="ep:sort" class="mr-5px" /> 换题</el-button>
<div style="display: flex; gap: 20px; height: 500px; width: 100%;">
<!-- 左侧题目表格 -->
<div style="flex: 3; overflow: auto;">
<el-table
v-loading="loading"
:data="questionList"
@row-click="handleQuestionRowClick"
highlight-current-row
ref="questionTableRef"
>
<el-table-column label="序号" prop="index" align="center" width="60" />
<el-table-column label="编号" prop="questionIndex" align="center" width="80" />
<el-table-column label="题型" prop="subjectName" align="center" />
<el-table-column label="难度" prop="quLevel" align="center">
<template #default="scope">
<dict-tag :type="DICT_TYPE.EXAM_QUE_DIFF" :value="scope.row.quLevel" />
</template>
</el-table-column>
<el-table-column label="知识点" prop="pointNames" align="left" />
</el-table>
</div>
<!-- 右侧题目详情 -->
<div style=" flex: 4; border: 1px solid #ebeef5; padding: 16px; border-radius: 4px; overflow: auto; height: 100%;">
<template v-if="selectedQuestion">
<div style="font-weight: bold; margin-bottom: 8px;">题目描述</div>
<div v-html="selectedQuestion.content " style="margin-bottom: 16px;"></div>
<template v-if="isChoiceQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">选项及答案</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div v-for="(answer, index) in selectedQuestion.answerList" :key="answer.answerId" style="margin-bottom: 4px;">
{{ optionLabel(index) }}. {{ answer.content }}
<span v-if="answer.isRight === '0'" style="color: green; font-weight: bold">正确</span>
</div>
</div>
<div v-else style="color: #999;">暂无答案</div>
</template>
<Dialog v-model="visible" :title="'修改试卷'" width="460" @open="handleOpen" center>
<!-- <div style="margin-bottom: 16px">
<strong>试卷ID</strong>{{ paperId }}
</div> -->
<!-- 滚动区域 -->
<div style="max-height: 400px; overflow-y: auto;">
<!-- 分组展示题目 -->
<div
v-for="(items, subject) in sortedGroupedQuestions"
:key="subject"
>
<h4 class="mb-2 text-base text-gray-700 font-medium">
{{ subject }}
<span v-if="schemeMap[subject]" class="text-sm text-gray-500 font-normal">
每小题 {{ parseFloat(schemeMap[subject].quScores).toFixed(1) }} {{ parseFloat(schemeMap[subject].subtotalScore).toFixed(1) }}
</span>
</h4>
<el-card
v-for="(item, index) in items"
:key="item.quId"
class="mb-3"
shadow="never"
>
<div class="text-gray-500 mb-2">题目 {{ index + 1 }}</div>
<div v-html="item.content" class="mb-2"></div>
<!-- 🎯 判断是否为编程题 -->
<div v-if="item.subjectName === '编程题'">
<!-- 编程题解析 -->
<div class="mt-2">
<div class="font-bold text-blue-600">解析</div>
<pre
style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;"
>{{ item.analysis }}</pre>
<!-- 程序设计题 -->
<template v-else-if="isProgrammingQuestion(selectedQuestion.subjectName)">
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div style="margin-bottom: 8px; white-space: pre-wrap;">
<strong>考点语句</strong><br />
{{ answer.content }}
</div>
<div
v-for="(answer, aIndex) in item.answerList"
:key="answer.answerId"
class="mb-1"
>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}%
<div v-if="answer.examMysqlKeywordList && answer.examMysqlKeywordList.length">
<div style="margin-top: 10px;"><strong>关键词列表</strong></div>
<ul style="padding-left: 20px;">
<li
v-for="keyword in answer.examMysqlKeywordList"
:key="keyword.keywordId"
style="line-height: 1.6;"
>
- 关键词{{ keyword.keyword }}权值{{ keyword.scoreRate }}
</li>
</ul>
</div>
</div>
<!-- 📝 普通题目的选项显示 -->
<div v-else-if="item.answerList && item.answerList.length">
<div
v-for="(answer, aIndex) in item.answerList.slice().sort((a, b) => a.sort - b.sort)"
:key="answer.answerId"
:style="{
color: answer.isRight === '0' ? 'green' : 'inherit',
fontWeight: answer.isRight === '0' ? 'bold' : 'normal'
}"
class="mb-1"
>
{{ String.fromCharCode(65 + aIndex) }}. {{ answer.content }}
</div>
</div>
</el-card>
</div>
<div v-else style="color: #999;">暂无程序设计答案</div>
</template>
<!-- 编程题 -->
<template v-else-if="isCodingTestQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">答案</div>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;">
{{ selectedQuestion.analysis }}
</pre>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
</div>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<div style="font-weight: bold; margin-bottom: 8px;">判分测试用例</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>输入</strong>{{ answer.contentIn }}</div>
<div><strong>输出</strong>{{ answer.content }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无测试用例信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分关键字</div>
<div v-if="selectedQuestion.questionKeywords && selectedQuestion.questionKeywords.length">
<div
v-for="answer in selectedQuestion.questionKeywords"
:key="answer.keywordId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>关键字</strong>{{ answer.keyword }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无关键字信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分标准</div>
<div v-if="selectedQuestion.questionScores">
<div
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>检查程序编译</strong>{{ selectedQuestion.questionScores.isPass === '0' ? '是' : '否' }} , <strong>检查程序编译百分比</strong>{{ selectedQuestion.questionScores.isPassScore }}%</div>
<div><strong>检查程序结果</strong>{{ selectedQuestion.questionScores.isResult === '0' ? '是' : '否' }} ,<strong>检查程序结果百分比</strong>{{ selectedQuestion.questionScores.isResultScore }}%</div>
<div><strong>检查关键字</strong>{{ selectedQuestion.questionScores.isKeyword === '0' ? '是' : '否' }} , <strong>检查关键字百分比</strong>{{ selectedQuestion.questionScores.isKeywordScore }}%</div>
<div><strong>使用测试用例</strong>{{ selectedQuestion.questionScores.isCompile === '0' ? '是' : '否' }} , <strong>使用测试用例百分比</strong>{{ selectedQuestion.questionScores.isCompileScore }}%</div>
<div><strong>关键字得分临界值</strong>{{ selectedQuestion.questionScores.keywordCutoff }}%</div>
<div><strong>测试用例得分临界值小于等于测试用例个数</strong>{{ selectedQuestion.questionScores.compileCutoff }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无输入输出信息</div>
</template>
<template v-else>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div v-for="answer in selectedQuestion.answerList" :key="answer.answerId" style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace">
<div><strong>考察点</strong>{{ answer.content }}</div>
<div><strong>考察类型</strong>{{ answer.contentIn }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无考点信息</div>
</template>
</template>
</Dialog>
<template v-else>
<div style="color: #999;">请点击左侧试题查看详细内容</div>
</template>
</div>
</div>
</ContentWrap>
<template #footer>
<el-button @click="handleCancel"> </el-button>
</template>
</Dialog>
<Dialog v-model="visibleChange" :title="'换题列表'" width="80%" center @close="handleCancelQue" >
<ContentWrap>
<div style="display: flex; gap: 20px;height: 500px; width: 100%;">
<!-- 左侧题目表格 -->
<div style="flex: 3;">
<!-- 搜索条件区域 -->
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<el-input
v-model="queryParams.pointNames"
placeholder="试题知识点"
clearable
style="flex: 1; min-width: 180px;"
/>
<el-select
v-model="queryParams.quLevel"
placeholder="难度"
clearable
style="flex: 1; min-width: 180px;"
>
<el-option label="简单" value="0" />
<el-option label="一般" value="1" />
<el-option label="困难" value="2" />
</el-select>
<el-input
v-model="queryParams.quId"
placeholder="题号"
clearable
style="flex: 1; min-width: 180px;"
/>
<el-button type="primary" @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
<el-button @click="resetSearch"> <Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
<el-button @click="resetRandom" > 随机换题</el-button>
</div>
<el-table
v-loading="loading"
:data="list"
ref="changeTableRef"
highlight-current-row
@row-click="handleRowClick"
>
<el-table-column label="试题编号" align="center" key="id" prop="quId" :show-overflow-tooltip="true"/>
<el-table-column label="专业" align="center" prop="specialtyName" width="120" />
<el-table-column label="课程" align="center" prop="courseName" :show-overflow-tooltip="true" />
<el-table-column label="章节名称" align="center" prop="chapteridDictText" :show-overflow-tooltip="true" />
<el-table-column label="难度" align="center" prop="quLevel" :show-overflow-tooltip="true">
<template #default="scope">
<dict-tag :type="DICT_TYPE.EXAM_QUE_DIFF" :value="scope.row.quLevel" />
</template>
</el-table-column>
<el-table-column label="知识点" align="center" prop="pointNames" :show-overflow-tooltip="true" />
<!-- <el-table-column label="审核状态" align="center" prop="audit" :show-overflow-tooltip="true">
<template #default="scope">
<dict-tag :type="DICT_TYPE.QUESTION_AUDIT" :value="scope.row.audit" />
</template>
</el-table-column>
<el-table-column label="状态" key="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYS_STATUS" :value="scope.row.status" />
</template>
</el-table-column> -->
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
<!-- 右侧题目详情 -->
<div style="flex: 4; border: 1px solid #ebeef5; padding: 16px; border-radius: 4px; overflow-y: auto;">
<template v-if="selectedSwapQuestion">
<div style="font-weight: bold; margin-bottom: 8px;">题目描述</div>
<div v-html="selectedSwapQuestion.content" style="margin-bottom: 16px;"></div>
<template v-if="isChoiceQuestion(selectedSwapQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">选项及答案</div>
<div v-if="selectedSwapQuestion.answerList && selectedSwapQuestion.answerList.length">
<div
v-for="(answer, index) in selectedSwapQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 4px;"
>
{{ optionLabel(index) }}. {{ answer.content }}
<span v-if="answer.isRight === '0'" style="color: green; font-weight: bold">正确</span>
</div>
</div>
<div v-else style="color: #999;">暂无答案</div>
</template>
<!-- 编程题 -->
<template v-else-if="isCodingTestQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">答案</div>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;">
{{ selectedQuestion.analysis }}
</pre>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分测试用例</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>输入</strong>{{ answer.contentIn }}</div>
<div><strong>输出</strong>{{ answer.content }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无测试用例信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分关键字</div>
<div v-if="selectedQuestion.questionKeywords && selectedQuestion.questionKeywords.length">
<div
v-for="answer in selectedQuestion.questionKeywords"
:key="answer.keywordId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>关键字</strong>{{ answer.keyword }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无关键字信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分标准</div>
<div v-if="selectedQuestion.questionScores">
<div
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>检查程序编译</strong>{{ selectedQuestion.questionScores.isPass === '0' ? '是' : '否' }} , <strong>检查程序编译百分比</strong>{{ selectedQuestion.questionScores.isPassScore }}%</div>
<div><strong>检查程序结果</strong>{{ selectedQuestion.questionScores.isResult === '0' ? '是' : '否' }} ,<strong>检查程序结果百分比</strong>{{ selectedQuestion.questionScores.isResultScore }}%</div>
<div><strong>检查关键字</strong>{{ selectedQuestion.questionScores.isKeyword === '0' ? '是' : '否' }} , <strong>检查关键字百分比</strong>{{ selectedQuestion.questionScores.isKeywordScore }}%</div>
<div><strong>使用测试用例</strong>{{ selectedQuestion.questionScores.isCompile === '0' ? '是' : '否' }} , <strong>使用测试用例百分比</strong>{{ selectedQuestion.questionScores.isCompileScore }}%</div>
<div><strong>关键字得分临界值</strong>{{ selectedQuestion.questionScores.keywordCutoff }}%</div>
<div><strong>测试用例得分临界值小于等于测试用例个数</strong>{{ selectedQuestion.questionScores.compileCutoff }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无输入输出信息</div>
</template>
<template v-else-if="isProgrammingQuestion(selectedQuestion.subjectName)">
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div style="margin-bottom: 8px; white-space: pre-wrap;">
<strong>考点语句</strong><br />
{{ answer.content }}
</div>
<div v-if="answer.examMysqlKeywordList && answer.examMysqlKeywordList.length">
<div style="margin-top: 10px;"><strong>关键词列表</strong></div>
<ul style="padding-left: 20px;">
<li
v-for="keyword in answer.examMysqlKeywordList"
:key="keyword.keywordId"
style="line-height: 1.6;"
>
- 关键词{{ keyword.keyword }}权值{{ keyword.scoreRate }}
</li>
</ul>
</div>
</div>
</div>
<div v-else style="color: #999;">暂无程序设计答案</div>
</template>
<template v-else>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedSwapQuestion.answerList && selectedSwapQuestion.answerList.length">
<div
v-for="answer in selectedSwapQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace"
>
<div><strong>考察点</strong>{{ answer.content }}</div>
<div><strong>考察类型</strong>{{ answer.contentIn }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无考点信息</div>
</template>
</template>
<template v-else>
<div style="color: #999;">请点击左侧题目查看详情</div>
</template>
</div>
</div>
</ContentWrap>
<template #footer>
<el-button @click="handleCancelQue"> </el-button>
<el-button type="primary" @click="handleConfirmChange"> </el-button>
</template>
<script setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { getPaperDetailByTaskId } from '@/api/system/paper';
const message = useMessage() // 消息弹窗
const props = defineProps({
paperId: String
});
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean });
const loading = ref(false);
const formRef = ref(null);
const questionList = ref([]); // 用于存放试题列表
const schemeMap = ref({});
const [form, resetFields, assignFields] = useFormData({
paperId: void 0,
taskId: '',
counts: '',
rollUp: '',
isAb: '',
status: ''
});
const rules = reactive({});
const handleCancel = () => {
visible.value = false;
</Dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { getPaperDetailCenterByTaskId } from '@/api/system/paper';
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const message = useMessage(); // 消息弹窗
import * as QuestionApi from '@/api/paper/question'
const props = defineProps({
paperId: String // 接收父组件传来的 paperId
});
const queryParams = reactive({
quLevel: "",
pointNames: "",
subjectName: "",
quId:"",
chapteridDictText:"",
pageNo: 1,
pageSize: 10,
audit:"0",
status:"0",
specialtyName:"",
courseName:"",
})
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean }); // 对应 v-model
const visibleChange = defineModel('visibleChange', { type: Boolean }); // 对应 v-model:visibleChange
const selectedChange = ref<any[]>([]);
const selectedSwapQuestion = ref<any>(null); // 换题弹窗中点击的题目
const questionTableRef = ref();
const selectedQuestion = ref<any>(null); // 第一个弹窗选中的试题
const handleQuestionRowClick = (row: any) => {
selectedQuestion.value = row;
};
const questionList = ref<any[]>([]);
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数
const handleCancel = () => {
visible.value = false;
};
const handleChange =async () => {
if (!selectedQuestion.value) {
message.warning('请先选中一个原题');
return;
}
visibleChange.value = true;
await getList();
};
const handleSelect =async () => {
await handleOpen();
};
const handleCancelQue = () => {
visibleChange.value = false;
list.value=[];
selectedSwapQuestion.value = null; // 🔁 清空右侧题目内容
selectedChange.value = []; // (可选)清空选中项
list.value = [];
};
const resetSearch = () => {
queryParams.pointNames = '';
queryParams.quLevel = '';
queryParams.quId = '';
queryParams.pageNo = 1;
getList();
};
const resetRandom = () => {
const originalId = selectedQuestion.value.quId || selectedQuestion.value.id;
const sub=selectedQuestion.value.subjectName;
console.log('原题 ID:', originalId);
console.log('试卷ID:',props.paperId)
console.log('原题题型:',sub)
// 构造参数
const payload = {
paperId: props.paperId,
oldQuId: originalId,
subjectName:sub,
newQuId: null,
};
const groupedQuestions = ref({});
const subjectPriority = {
'选择题': 1,
'多选题': 2,
'判断题': 3,
'编程题': 4,
'其他': 5
};
const groupBySubjectName = (list) => {
const group = {};
list.forEach((item) => {
const subject = item.subjectName || '其他'; // 默认为'其他'
if (!group[subject]) {
group[subject] = [];
}
group[subject].push(item);
try {
QuestionApi.changePaperQuRandom( payload); // 根据实际路径修改
message.success('换题成功');
} catch (error) {
console.error('换题请求出错:', error);
message.error('请求失败,请稍后重试');
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const changeTableRef = ref();
const handleRowClick = (row: any) => {
selectedChange.value = [row]; // 只选中一个换题
selectedSwapQuestion.value = row;
};
/** 查询列表 */
const getList = async () => {
loading.value = true
queryParams.subjectName=selectedQuestion.value.subjectName
try {
const data = await QuestionApi.getQuestionlistAnswer(queryParams);
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
// 打开弹窗时加载题目数据
const handleOpen = async () => {
loading.value = true;
try {
const res = await getPaperDetailCenterByTaskId(props.paperId);
const rawList = res.examQuestionList || [];
const schemeList = res.educationPaperSchemeList || [];
// 构建题型 => sort 数字映射,例如 { '选择题': 1, '文件处理': 2, ... }
const sortMap = {};
schemeList.forEach((scheme) => {
sortMap[scheme.spName] = Number(scheme.sort);
});
groupedQuestions.value = group;
// 先排序:根据题型的 sort 排序
const sortedRawList = [...rawList].sort((a, b) => {
return (sortMap[a.subjectName] || 999) - (sortMap[b.subjectName] || 999);
});
// 每种题型的编号计数器
const typeCounter = {};
let globalIndex = 1;
questionList.value = sortedRawList.map((q) => {
const groupIndex = sortMap[q.subjectName] || 0;
typeCounter[groupIndex] = (typeCounter[groupIndex] || 0) + 1;
const questionIndex = `${groupIndex}.${typeCounter[groupIndex]}`;
return {
...q,
index: globalIndex++, // 全局序号 1、2、3…
questionIndex, // 按题型分组的编号 1.1、1.2、2.1…
};
});
} finally {
loading.value = false;
}
};
const handleConfirmChange = () => {
if (selectedChange.value.length !== 1) {
message.warning('请选择一个换题');
return;
}
const originalId = selectedQuestion.value.quId || selectedQuestion.value.id;
const newId = selectedChange.value[0].quId || selectedChange.value[0].id;
console.log('原题 ID:', originalId);
console.log('换题 ID:', newId);
console.log('试卷ID:',props.paperId)
// 构造参数
const payload = {
paperId: props.paperId,
oldQuId: originalId,
newQuId: newId,
};
const sortedGroupedQuestions = computed(() => {
return Object.keys(groupedQuestions.value)
.sort((a, b) => (subjectPriority[a] || subjectPriority['其他']) - (subjectPriority[b] || subjectPriority['其他']))
.reduce((acc, subject) => {
acc[subject] = groupedQuestions.value[subject];
return acc;
}, {});
});
const handleOpen = () => {
resetFields();
questionList.value = [];
if (props.paperId) {
loading.value = true;
getPaperDetailByTaskId(props.paperId)
.then((res) => {
const { educationPaperSchemeList, examQuestionList } = res.data || {};
if (Array.isArray(examQuestionList)) {
questionList.value = examQuestionList;
groupBySubjectName(examQuestionList); // 分类
}
if (Array.isArray(educationPaperSchemeList)) {
// 构建以题型名为 key 的映射表spName 与 scheme 对应)
schemeMap.value = educationPaperSchemeList.reduce((acc, item) => {
acc[item.spName] = item;
return acc;
}, {});
}
assignFields(res);
})
.catch((e) => {
message.error(`获取试卷详情失败: ${e.message}`);
})
.finally(() => {
loading.value = false;
nextTick(() => {
formRef.value?.clearValidate?.();
});
});
}
};
</script>
try {
QuestionApi.changePaperQu( payload); // 根据实际路径修改
message.success('换题成功');
} catch (error) {
console.error('换题请求出错:', error);
message.error('请求失败,请稍后重试');
}
// 关闭弹窗
visibleChange.value = false;
};
const choiceSubjects = ["选择题", "单选题", "多选题"];
const programmingSubjects = ["程序设计"];
function isChoiceQuestion(subjectName) {
return choiceSubjects.some(item => subjectName.includes(item));
}
function isProgrammingQuestion(subjectName) {
return programmingSubjects.some(item => subjectName.includes(item));
}
const isCodingTestQuestion = (subjectName: string): boolean => {
return subjectName === '编程题';
};
function optionLabel(index) {
return String.fromCharCode(65 + index); // A, B, C, D ...
}
</script>

View File

@@ -44,13 +44,13 @@
style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;"
>{{ item.analysis }}</pre>
</div>
<div
<!-- <div
v-for="(answer, aIndex) in item.answerList"
:key="answer.answerId"
class="mb-1"
>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}%
</div>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}
</div> -->
</div>
<!-- 📝 普通题目的选项显示 -->

View File

@@ -1,211 +0,0 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:sms-channel:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增</el-button
>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="试卷编号" align="center" prop="paperId" />
<el-table-column label="使用次数" align="center" prop="counts" />
<el-table-column label="抽卷方式" align="center" prop="rollUp">
<template #default="scope">
<span v-if="scope.row.quLevel === '0'">固定</span>
<span v-else-if="scope.row.quLevel === '1'">AB卷</span>
<span v-else-if="scope.row.quLevel === '2'">随机</span>
<span v-else-if="scope.row.quLevel === '3'">自选</span>
<span v-else>未知</span>
</template>
</el-table-column>
<el-table-column label="AB卷" align="center" prop="isAb">
<template #default="scope">
<span v-if="scope.row.quLevel === '0'">A卷</span>
<span v-else-if="scope.row.quLevel === '1'">B卷</span>
<span v-else>未知</span>
</template>
</el-table-column>
<el-table-column label="是否启用" align="center" prop="status" >
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYS_YES_NO" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openEdit('update', scope.row)"
v-hasPermi="['system:sms-channel:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:sms-channel:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<paper-add v-model="showAdd" :task-Id="taskId" :task-specialty="taskSpecialty" />
<paper-edit v-model="showEdit" :data="current" />
<!-- <paper-look v-model="showLook" :paper-id="paperId" /> -->
<paper-set v-model="showSet" :task-Id="taskId" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as SmsChannelApi from '@/api/system/paper';
import PaperEdit from './components/step-edit.vue';
import PaperAdd from './components/step-add.vue';
import PaperLook from './components/step-look.vue';
import PaperSet from './components/step-set.vue';
import PaperSearch from './components/step-search.vue';
import { pagePapers, removePapers, exportPapers } from '@/api/system/paper';
defineOptions({ name: 'SystemPaper' });
const props = defineProps({
taskSpecialty: {
type: String,
default: ''
},
taskId: {
type: String,
default: ''
}
})
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
/** 当前编辑数据 */
const current = ref<object>();
/** 是否显示编辑弹窗 */
const showEdit = ref(false);
const showLook = ref(false);
const showAdd = ref(false);
const showSet = ref(false);
const smsChannelFormRef = ref()
const taskEditRef = ref()
const taskAddRef = ref()
const taskTempRef = ref()
const loading = ref(false) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
signature: undefined,
status: undefined,
createTime: []
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const res = await SmsChannelApi.pagePapers(queryParams)
console.log(res)
list.value = res
total.value = res.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref();
const openForm = (type: string, id?: number) => {
showAdd.value = true
taskAddRef.value?.open(type, id)
}
const openEdit = (type: string, row?: object) => {
showEdit.value = true
current.value = row
console.log( current.value )
taskEditRef.value?.open(type, row)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SmsChannelApi.removePaper(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -162,6 +162,7 @@
v-model="showEdit"
:data="current"
ref="taskEditRef"
:paper-Id="currentPaperId"
@done="reload"
/>
<paper-look
@@ -262,7 +263,6 @@ const reload = () => {
getList();
};
const handleSelectionChange = (rows) => {
selections.value = rows;
}
@@ -320,11 +320,15 @@ const openDown = async () => {
}
};
// 打开编辑弹窗
const openEdit = (type: string, row?: object) => {
const openEdit = (type: string, row?: any)=> {
if (row?.counts > 0) {
ElMessage.warning('当前试卷使用次数大于0不允许更换试题');
return;
}
showEdit.value = true;
current.value = row;
currentPaperId.value = row.paperId ?? null;
nextTick(() => {
taskEditRef.value?.open(type, row);
taskEditRef.value?.open(type, row.paperId);
});
};
const currentPaperId = ref<number | null>(null);

View File

@@ -127,26 +127,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="12">
<el-form-item label="测评时长" >
<el-time-picker
v-model="form.examTime"
@@ -156,13 +137,31 @@
@change="handleFormChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
</el-col>
<!-- <el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col> -->
</el-row>
<el-row :gutter="20">
<!-- <el-col :span="12">
<el-form-item label="定时检查与学生端联通性,每">
<el-input-number v-model="form.isConnect" label="分钟" @change="handleFormChange" />
<span>分钟传一次,断联直接交卷</span>
</el-form-item>
</el-col>
</el-col> -->
</el-row>

View File

@@ -1,181 +1,634 @@
<template>
<Dialog v-model="visible" :title="'编辑试卷'" width="80%" height="10%" @open="handleOpen" center >
<ContentWrap>
<el-button @click="handleSelect"> <Icon icon="ep:refresh" class="mr-5px" />刷新</el-button>
<el-button @click="handleChange"><Icon icon="ep:sort" class="mr-5px" /> 换题</el-button>
<div style="display: flex; gap: 20px; height: 500px; width: 100%;">
<!-- 左侧题目表格 -->
<div style="flex: 3; overflow: auto;">
<el-table
v-loading="loading"
:data="questionList"
@row-click="handleQuestionRowClick"
highlight-current-row
ref="questionTableRef"
>
<el-table-column label="序号" prop="index" align="center" width="60" />
<el-table-column label="编号" prop="questionIndex" align="center" width="80" />
<el-table-column label="题型" prop="subjectName" align="center" />
<el-table-column label="难度" prop="quLevel" align="center">
<template #default="scope">
<dict-tag :type="DICT_TYPE.EXAM_QUE_DIFF" :value="scope.row.quLevel" />
</template>
</el-table-column>
<el-table-column label="知识点" prop="pointNames" align="left" />
</el-table>
</div>
<!-- 右侧题目详情 -->
<div style=" flex: 4; border: 1px solid #ebeef5; padding: 16px; border-radius: 4px; overflow: auto; height: 100%;">
<template v-if="selectedQuestion">
<div style="font-weight: bold; margin-bottom: 8px;">题目描述</div>
<div v-html="selectedQuestion.content " style="margin-bottom: 16px;"></div>
<template v-if="isChoiceQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">选项及答案</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div v-for="(answer, index) in selectedQuestion.answerList" :key="answer.answerId" style="margin-bottom: 4px;">
{{ optionLabel(index) }}. {{ answer.content }}
<span v-if="answer.isRight === '0'" style="color: green; font-weight: bold">正确</span>
</div>
</div>
<div v-else style="color: #999;">暂无答案</div>
</template>
<Dialog v-model="visible" :title="'修改试卷'" width="460" @open="handleOpen" center>
<!-- <div style="margin-bottom: 16px">
<strong>试卷ID</strong>{{ paperId }}
</div> -->
<!-- 滚动区域 -->
<div style="max-height: 400px; overflow-y: auto;">
<!-- 分组展示题目 -->
<div
v-for="(items, subject) in sortedGroupedQuestions"
:key="subject"
>
<h4 class="mb-2 text-base text-gray-700 font-medium">
{{ subject }}
<span v-if="schemeMap[subject]" class="text-sm text-gray-500 font-normal">
每小题 {{ parseFloat(schemeMap[subject].quScores).toFixed(1) }} {{ parseFloat(schemeMap[subject].subtotalScore).toFixed(1) }}
</span>
</h4>
<el-card
v-for="(item, index) in items"
:key="item.quId"
class="mb-3"
shadow="never"
>
<div class="text-gray-500 mb-2">题目 {{ index + 1 }}</div>
<div v-html="item.content" class="mb-2"></div>
<!-- 🎯 判断是否为编程题 -->
<div v-if="item.subjectName === '编程题'">
<!-- 编程题解析 -->
<div class="mt-2">
<div class="font-bold text-blue-600">解析</div>
<pre
style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;"
>{{ item.analysis }}</pre>
<!-- 程序设计题 -->
<template v-else-if="isProgrammingQuestion(selectedQuestion.subjectName)">
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div style="margin-bottom: 8px; white-space: pre-wrap;">
<strong>考点语句</strong><br />
{{ answer.content }}
</div>
<div
v-for="(answer, aIndex) in item.answerList"
:key="answer.answerId"
class="mb-1"
>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}%
<div v-if="answer.examMysqlKeywordList && answer.examMysqlKeywordList.length">
<div style="margin-top: 10px;"><strong>关键词列表</strong></div>
<ul style="padding-left: 20px;">
<li
v-for="keyword in answer.examMysqlKeywordList"
:key="keyword.keywordId"
style="line-height: 1.6;"
>
- 关键词{{ keyword.keyword }}权值{{ keyword.scoreRate }}
</li>
</ul>
</div>
</div>
<!-- 📝 普通题目的选项显示 -->
<div v-else-if="item.answerList && item.answerList.length">
<div
v-for="(answer, aIndex) in item.answerList.slice().sort((a, b) => a.sort - b.sort)"
:key="answer.answerId"
:style="{
color: answer.isRight === '0' ? 'green' : 'inherit',
fontWeight: answer.isRight === '0' ? 'bold' : 'normal'
}"
class="mb-1"
>
{{ String.fromCharCode(65 + aIndex) }}. {{ answer.content }}
</div>
</div>
</el-card>
</div>
<div v-else style="color: #999;">暂无程序设计答案</div>
</template>
<!-- 编程题 -->
<template v-else-if="isCodingTestQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">答案</div>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;">
{{ selectedQuestion.analysis }}
</pre>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
</div>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<div style="font-weight: bold; margin-bottom: 8px;">判分测试用例</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>输入</strong>{{ answer.contentIn }}</div>
<div><strong>输出</strong>{{ answer.content }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无测试用例信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分关键字</div>
<div v-if="selectedQuestion.questionKeywords && selectedQuestion.questionKeywords.length">
<div
v-for="answer in selectedQuestion.questionKeywords"
:key="answer.keywordId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>关键字</strong>{{ answer.keyword }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无关键字信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分标准</div>
<div v-if="selectedQuestion.questionScores">
<div
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>检查程序编译</strong>{{ selectedQuestion.questionScores.isPass === '0' ? '是' : '否' }} , <strong>检查程序编译百分比</strong>{{ selectedQuestion.questionScores.isPassScore }}%</div>
<div><strong>检查程序结果</strong>{{ selectedQuestion.questionScores.isResult === '0' ? '是' : '否' }} ,<strong>检查程序结果百分比</strong>{{ selectedQuestion.questionScores.isResultScore }}%</div>
<div><strong>检查关键字</strong>{{ selectedQuestion.questionScores.isKeyword === '0' ? '是' : '否' }} , <strong>检查关键字百分比</strong>{{ selectedQuestion.questionScores.isKeywordScore }}%</div>
<div><strong>使用测试用例</strong>{{ selectedQuestion.questionScores.isCompile === '0' ? '是' : '否' }} , <strong>使用测试用例百分比</strong>{{ selectedQuestion.questionScores.isCompileScore }}%</div>
<div><strong>关键字得分临界值</strong>{{ selectedQuestion.questionScores.keywordCutoff }}%</div>
<div><strong>测试用例得分临界值小于等于测试用例个数</strong>{{ selectedQuestion.questionScores.compileCutoff }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无输入输出信息</div>
</template>
<template v-else>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div v-for="answer in selectedQuestion.answerList" :key="answer.answerId" style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace">
<div><strong>考察点</strong>{{ answer.content }}</div>
<div><strong>考察类型</strong>{{ answer.contentIn }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无考点信息</div>
</template>
</template>
</Dialog>
<template v-else>
<div style="color: #999;">请点击左侧试题查看详细内容</div>
</template>
</div>
</div>
</ContentWrap>
<template #footer>
<el-button @click="handleCancel"> </el-button>
</template>
</Dialog>
<Dialog v-model="visibleChange" :title="'换题列表'" width="80%" center @close="handleCancelQue" >
<ContentWrap>
<div style="display: flex; gap: 20px;height: 500px; width: 100%;">
<!-- 左侧题目表格 -->
<div style="flex: 3;">
<!-- 搜索条件区域 -->
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<el-input
v-model="queryParams.pointNames"
placeholder="试题知识点"
clearable
style="flex: 1; min-width: 180px;"
/>
<el-select
v-model="queryParams.quLevel"
placeholder="难度"
clearable
style="flex: 1; min-width: 180px;"
>
<el-option label="简单" value="0" />
<el-option label="一般" value="1" />
<el-option label="困难" value="2" />
</el-select>
<el-input
v-model="queryParams.quId"
placeholder="题号"
clearable
style="flex: 1; min-width: 180px;"
/>
<el-button type="primary" @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
<el-button @click="resetSearch"> <Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
<el-button @click="resetRandom" > 随机换题</el-button>
</div>
<el-table
v-loading="loading"
:data="list"
ref="changeTableRef"
highlight-current-row
@row-click="handleRowClick"
>
<el-table-column label="试题编号" align="center" key="id" prop="quId" :show-overflow-tooltip="true"/>
<el-table-column label="专业" align="center" prop="specialtyName" width="120" />
<el-table-column label="课程" align="center" prop="courseName" :show-overflow-tooltip="true" />
<el-table-column label="章节名称" align="center" prop="chapteridDictText" :show-overflow-tooltip="true" />
<el-table-column label="难度" align="center" prop="quLevel" :show-overflow-tooltip="true">
<template #default="scope">
<dict-tag :type="DICT_TYPE.EXAM_QUE_DIFF" :value="scope.row.quLevel" />
</template>
</el-table-column>
<el-table-column label="知识点" align="center" prop="pointNames" :show-overflow-tooltip="true" />
<!-- <el-table-column label="审核状态" align="center" prop="audit" :show-overflow-tooltip="true">
<template #default="scope">
<dict-tag :type="DICT_TYPE.QUESTION_AUDIT" :value="scope.row.audit" />
</template>
</el-table-column>
<el-table-column label="状态" key="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYS_STATUS" :value="scope.row.status" />
</template>
</el-table-column> -->
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
<!-- 右侧题目详情 -->
<div style="flex: 4; border: 1px solid #ebeef5; padding: 16px; border-radius: 4px; overflow-y: auto;">
<template v-if="selectedSwapQuestion">
<div style="font-weight: bold; margin-bottom: 8px;">题目描述</div>
<div v-html="selectedSwapQuestion.content" style="margin-bottom: 16px;"></div>
<template v-if="isChoiceQuestion(selectedSwapQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">选项及答案</div>
<div v-if="selectedSwapQuestion.answerList && selectedSwapQuestion.answerList.length">
<div
v-for="(answer, index) in selectedSwapQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 4px;"
>
{{ optionLabel(index) }}. {{ answer.content }}
<span v-if="answer.isRight === '0'" style="color: green; font-weight: bold">正确</span>
</div>
</div>
<div v-else style="color: #999;">暂无答案</div>
</template>
<!-- 编程题 -->
<template v-else-if="isCodingTestQuestion(selectedQuestion.subjectName)">
<div style="font-weight: bold; margin-bottom: 8px;">答案</div>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;">
{{ selectedQuestion.analysis }}
</pre>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分测试用例</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>输入</strong>{{ answer.contentIn }}</div>
<div><strong>输出</strong>{{ answer.content }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无测试用例信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分关键字</div>
<div v-if="selectedQuestion.questionKeywords && selectedQuestion.questionKeywords.length">
<div
v-for="answer in selectedQuestion.questionKeywords"
:key="answer.keywordId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>关键字</strong>{{ answer.keyword }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无关键字信息</div>
<div style="font-weight: bold; margin-bottom: 8px;">判分标准</div>
<div v-if="selectedQuestion.questionScores">
<div
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div><strong>检查程序编译</strong>{{ selectedQuestion.questionScores.isPass === '0' ? '是' : '否' }} , <strong>检查程序编译百分比</strong>{{ selectedQuestion.questionScores.isPassScore }}%</div>
<div><strong>检查程序结果</strong>{{ selectedQuestion.questionScores.isResult === '0' ? '是' : '否' }} ,<strong>检查程序结果百分比</strong>{{ selectedQuestion.questionScores.isResultScore }}%</div>
<div><strong>检查关键字</strong>{{ selectedQuestion.questionScores.isKeyword === '0' ? '是' : '否' }} , <strong>检查关键字百分比</strong>{{ selectedQuestion.questionScores.isKeywordScore }}%</div>
<div><strong>使用测试用例</strong>{{ selectedQuestion.questionScores.isCompile === '0' ? '是' : '否' }} , <strong>使用测试用例百分比</strong>{{ selectedQuestion.questionScores.isCompileScore }}%</div>
<div><strong>关键字得分临界值</strong>{{ selectedQuestion.questionScores.keywordCutoff }}%</div>
<div><strong>测试用例得分临界值小于等于测试用例个数</strong>{{ selectedQuestion.questionScores.compileCutoff }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无输入输出信息</div>
</template>
<template v-else-if="isProgrammingQuestion(selectedQuestion.subjectName)">
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedQuestion.answerList && selectedQuestion.answerList.length">
<div
v-for="answer in selectedQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace;"
>
<div style="margin-bottom: 8px; white-space: pre-wrap;">
<strong>考点语句</strong><br />
{{ answer.content }}
</div>
<div v-if="answer.examMysqlKeywordList && answer.examMysqlKeywordList.length">
<div style="margin-top: 10px;"><strong>关键词列表</strong></div>
<ul style="padding-left: 20px;">
<li
v-for="keyword in answer.examMysqlKeywordList"
:key="keyword.keywordId"
style="line-height: 1.6;"
>
- 关键词{{ keyword.keyword }}权值{{ keyword.scoreRate }}
</li>
</ul>
</div>
</div>
</div>
<div v-else style="color: #999;">暂无程序设计答案</div>
</template>
<template v-else>
<div style="display: flex; align-items: center; font-weight: bold; margin-bottom: 8px;">
<span>考点信息</span>
<div style="flex: 1; height: 1px; background: #ccc; margin-left: 10px;"></div>
</div>
<div v-if="selectedSwapQuestion.answerList && selectedSwapQuestion.answerList.length">
<div
v-for="answer in selectedSwapQuestion.answerList"
:key="answer.answerId"
style="margin-bottom: 12px; background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace"
>
<div><strong>考察点</strong>{{ answer.content }}</div>
<div><strong>考察类型</strong>{{ answer.contentIn }}</div>
<div><strong>权值</strong>{{ answer.scoreRate }}</div>
</div>
</div>
<div v-else style="color: #999;">暂无考点信息</div>
</template>
</template>
<template v-else>
<div style="color: #999;">请点击左侧题目查看详情</div>
</template>
</div>
</div>
</ContentWrap>
<template #footer>
<el-button @click="handleCancelQue"> </el-button>
<el-button type="primary" @click="handleConfirmChange"> </el-button>
</template>
<script setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { getPaperDetailByTaskId } from '@/api/system/paper';
const message = useMessage() // 消息弹窗
const props = defineProps({
paperId: String
});
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean });
const loading = ref(false);
const formRef = ref(null);
const questionList = ref([]); // 用于存放试题列表
const schemeMap = ref({});
const [form, resetFields, assignFields] = useFormData({
paperId: void 0,
taskId: '',
counts: '',
rollUp: '',
isAb: '',
status: ''
});
const rules = reactive({});
const handleCancel = () => {
visible.value = false;
</Dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { getPaperDetailCenterByTaskId } from '@/api/system/paper';
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const message = useMessage(); // 消息弹窗
import * as QuestionApi from '@/api/paper/question'
const props = defineProps({
paperId: String // 接收父组件传来的 paperId
});
const queryParams = reactive({
quLevel: "",
pointNames: "",
subjectName: "",
quId:"",
chapteridDictText:"",
pageNo: 1,
pageSize: 10,
audit:"0",
status:"0",
specialtyName:"",
courseName:"",
})
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean }); // 对应 v-model
const visibleChange = defineModel('visibleChange', { type: Boolean }); // 对应 v-model:visibleChange
const selectedChange = ref<any[]>([]);
const selectedSwapQuestion = ref<any>(null); // 换题弹窗中点击的题目
const questionTableRef = ref();
const selectedQuestion = ref<any>(null); // 第一个弹窗选中的试题
const handleQuestionRowClick = (row: any) => {
selectedQuestion.value = row;
};
const questionList = ref<any[]>([]);
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数
const handleCancel = () => {
visible.value = false;
};
const handleChange =async () => {
if (!selectedQuestion.value) {
message.warning('请先选中一个原题');
return;
}
visibleChange.value = true;
await getList();
};
const handleSelect =async () => {
await handleOpen();
};
const handleCancelQue = () => {
visibleChange.value = false;
list.value=[];
selectedSwapQuestion.value = null; // 🔁 清空右侧题目内容
selectedChange.value = []; // (可选)清空选中项
list.value = [];
};
const resetSearch = () => {
queryParams.pointNames = '';
queryParams.quLevel = '';
queryParams.quId = '';
queryParams.pageNo = 1;
getList();
};
const resetRandom = () => {
const originalId = selectedQuestion.value.quId || selectedQuestion.value.id;
const sub=selectedQuestion.value.subjectName;
console.log('原题 ID:', originalId);
console.log('试卷ID:',props.paperId)
console.log('原题题型:',sub)
// 构造参数
const payload = {
paperId: props.paperId,
oldQuId: originalId,
subjectName:sub,
newQuId: null,
};
const groupedQuestions = ref({});
const subjectPriority = {
'选择题': 1,
'多选题': 2,
'判断题': 3,
'编程题': 4,
'其他': 5
};
const groupBySubjectName = (list) => {
const group = {};
list.forEach((item) => {
const subject = item.subjectName || '其他'; // 默认为'其他'
if (!group[subject]) {
group[subject] = [];
}
group[subject].push(item);
try {
QuestionApi.changePaperQuRandom( payload); // 根据实际路径修改
message.success('换题成功');
} catch (error) {
console.error('换题请求出错:', error);
message.error('请求失败,请稍后重试');
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const changeTableRef = ref();
const handleRowClick = (row: any) => {
selectedChange.value = [row]; // 只选中一个换题
selectedSwapQuestion.value = row;
};
/** 查询列表 */
const getList = async () => {
loading.value = true
queryParams.subjectName=selectedQuestion.value.subjectName
try {
const data = await QuestionApi.getQuestionlistAnswer(queryParams);
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
// 打开弹窗时加载题目数据
const handleOpen = async () => {
loading.value = true;
try {
const res = await getPaperDetailCenterByTaskId(props.paperId);
const rawList = res.examQuestionList || [];
const schemeList = res.educationPaperSchemeList || [];
// 构建题型 => sort 数字映射,例如 { '选择题': 1, '文件处理': 2, ... }
const sortMap = {};
schemeList.forEach((scheme) => {
sortMap[scheme.spName] = Number(scheme.sort);
});
groupedQuestions.value = group;
// 先排序:根据题型的 sort 排序
const sortedRawList = [...rawList].sort((a, b) => {
return (sortMap[a.subjectName] || 999) - (sortMap[b.subjectName] || 999);
});
// 每种题型的编号计数器
const typeCounter = {};
let globalIndex = 1;
questionList.value = sortedRawList.map((q) => {
const groupIndex = sortMap[q.subjectName] || 0;
typeCounter[groupIndex] = (typeCounter[groupIndex] || 0) + 1;
const questionIndex = `${groupIndex}.${typeCounter[groupIndex]}`;
return {
...q,
index: globalIndex++, // 全局序号 1、2、3…
questionIndex, // 按题型分组的编号 1.1、1.2、2.1…
};
});
} finally {
loading.value = false;
}
};
const handleConfirmChange = () => {
if (selectedChange.value.length !== 1) {
message.warning('请选择一个换题');
return;
}
const originalId = selectedQuestion.value.quId || selectedQuestion.value.id;
const newId = selectedChange.value[0].quId || selectedChange.value[0].id;
console.log('原题 ID:', originalId);
console.log('换题 ID:', newId);
console.log('试卷ID:',props.paperId)
// 构造参数
const payload = {
paperId: props.paperId,
oldQuId: originalId,
newQuId: newId,
};
const sortedGroupedQuestions = computed(() => {
return Object.keys(groupedQuestions.value)
.sort((a, b) => (subjectPriority[a] || subjectPriority['其他']) - (subjectPriority[b] || subjectPriority['其他']))
.reduce((acc, subject) => {
acc[subject] = groupedQuestions.value[subject];
return acc;
}, {});
});
const handleOpen = () => {
resetFields();
questionList.value = [];
if (props.paperId) {
loading.value = true;
getPaperDetailByTaskId(props.paperId)
.then((res) => {
const { educationPaperSchemeList, examQuestionList } = res.data || {};
if (Array.isArray(examQuestionList)) {
questionList.value = examQuestionList;
groupBySubjectName(examQuestionList); // 分类
}
if (Array.isArray(educationPaperSchemeList)) {
// 构建以题型名为 key 的映射表spName 与 scheme 对应)
schemeMap.value = educationPaperSchemeList.reduce((acc, item) => {
acc[item.spName] = item;
return acc;
}, {});
}
assignFields(res);
})
.catch((e) => {
message.error(`获取试卷详情失败: ${e.message}`);
})
.finally(() => {
loading.value = false;
nextTick(() => {
formRef.value?.clearValidate?.();
});
});
}
};
</script>
try {
QuestionApi.changePaperQu( payload); // 根据实际路径修改
message.success('换题成功');
} catch (error) {
console.error('换题请求出错:', error);
message.error('请求失败,请稍后重试');
}
// 关闭弹窗
visibleChange.value = false;
};
const choiceSubjects = ["选择题", "单选题", "多选题"];
const programmingSubjects = ["程序设计"];
function isChoiceQuestion(subjectName) {
return choiceSubjects.some(item => subjectName.includes(item));
}
function isProgrammingQuestion(subjectName) {
return programmingSubjects.some(item => subjectName.includes(item));
}
const isCodingTestQuestion = (subjectName: string): boolean => {
return subjectName === '编程题';
};
function optionLabel(index) {
return String.fromCharCode(65 + index); // A, B, C, D ...
}
</script>

View File

@@ -44,13 +44,13 @@
style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;"
>{{ item.analysis }}</pre>
</div>
<div
<!-- <div
v-for="(answer, aIndex) in item.answerList"
:key="answer.answerId"
class="mb-1"
>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}%
</div>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}
</div> -->
</div>
<!-- 📝 普通题目的选项显示 -->

View File

@@ -66,7 +66,6 @@
<Icon icon="ep:files" class="mr-5px" />
生成笔试试卷
</el-button>
<!-- 批量删除按钮 -->
<!-- <el-button
type="danger"
@@ -81,7 +80,7 @@
<!-- 列表展示 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :row-key="row => row.id" ref="tableRef" @selection-change="handleSelectionChange">
<el-table v-loading="loading" :data="list" :row-key="row => row.id" ref="tableRef" @selection-change="handleSelectionChange">
<!-- 多选列 -->
<el-table-column type="selection" width="55" />
@@ -163,6 +162,7 @@
v-model="showEdit"
:data="current"
ref="taskEditRef"
:paper-Id="currentPaperId"
@done="reload"
/>
<paper-look
@@ -263,7 +263,9 @@ const reload = () => {
getList();
};
const handleSelectionChange = (rows) => {
selections.value = rows;
}
// 搜索操作
const handleQuery = () => {
queryParams.pageNo = 1;
@@ -290,7 +292,6 @@ const openSet = () => {
taskSetRef.value?.open();
});
};
/** 表格选中数据 */
const selections = ref([]);
const downloadLoading = ref(false)
@@ -318,21 +319,16 @@ const openDown = async () => {
downloadLoading.value = false;
}
};
const handleSelectionChange = (rows) => {
selections.value = rows;
}
// 打开编辑弹窗
const openEdit = (type: string, row?: object) => {
const openEdit = (type: string, row?: any)=> {
if (row?.counts > 0) {
ElMessage.warning('当前试卷使用次数大于0不允许更换试题');
return;
}
showEdit.value = true;
current.value = row;
currentPaperId.value = row.paperId ?? null;
nextTick(() => {
taskEditRef.value?.open(type, row);
taskEditRef.value?.open(type, row.paperId);
});
};
const currentPaperId = ref<number | null>(null);

View File

@@ -127,25 +127,6 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="测评时长" >
<el-time-picker
@@ -156,13 +137,32 @@
@change="handleFormChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
</el-col>
<!-- <el-col :span="12">
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
</el-col> -->
</el-row>
<el-row :gutter="20">
<!-- <el-col :span="12">
<el-form-item label="定时检查与学生端联通性,每">
<el-input-number v-model="form.isConnect" label="分钟" @change="handleFormChange" />
<span>分钟传一次,断联直接交卷</span>
</el-form-item>
</el-col>
</el-col> -->
</el-row>