1304 lines
48 KiB
Vue
1304 lines
48 KiB
Vue
<template>
|
||
<div class="program-edit">
|
||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="70%" top="10vh">
|
||
<el-scrollbar height="600px">
|
||
<div class="main">
|
||
<div class="tabsTip">
|
||
<el-icon><InfoFilled /></el-icon>C语言基础环境采用gcc编译器
|
||
</div>
|
||
<el-tabs v-model="activeName" class="demo-tabs">
|
||
<el-tab-pane label="基本内容" name="common">
|
||
<div class="line">
|
||
<div class="title-text">试题描述</div>
|
||
<ElInput v-model="formData.contentText" style="width: 100%" />
|
||
</div>
|
||
<div class="line">
|
||
<div class="title-text">试题题目</div>
|
||
<div class="buttons">
|
||
<ElButton type="primary" plain>
|
||
<el-icon><Document /></el-icon>
|
||
更换题目模板
|
||
</ElButton>
|
||
<ElButton type="primary" plain>
|
||
<el-icon><Search /></el-icon>
|
||
预览
|
||
</ElButton>
|
||
</div>
|
||
<div class="block">
|
||
<Editor v-model="formData.content" height="200px" />
|
||
</div>
|
||
<div class="buttons">
|
||
<ElButton type="primary" plain>
|
||
<el-icon><Paperclip /></el-icon>
|
||
添加附件
|
||
</ElButton>
|
||
</div>
|
||
</div>
|
||
<div class="line">
|
||
<div class="title-text">答题程序模板</div>
|
||
<div class="tips">
|
||
学生端答题的初始程序内容,学生可在此基础上进行程序的编写,插入作答区标记后关键字评分只在作答区内评分,否则关键字评分在整段代码评分。
|
||
注意: 为确保系统能正确捕获程序运行的输出结果,要求 scanf() 函数不能加参数,
|
||
不能采用类似 scanf("请输入: %s") 的形式
|
||
插入作答区标记后请在试题题目中明确告知学生不要自行更改作答区标记,以免无法正常评分。
|
||
</div>
|
||
<CodeEditor v-model="formData.answerEmplate" :rows="8" />
|
||
<div class="tips"> 更改答题程序模板无效,若需要更改请上传程序文件。 </div>
|
||
<div class="buttons">
|
||
<ElButton type="primary" plain @click="openForm('1')">
|
||
<el-icon><Upload /></el-icon>
|
||
上传程序文件
|
||
</ElButton>
|
||
<ElButton type="success" plain @click="openForm('2')">
|
||
<el-icon><Upload /></el-icon>
|
||
上传结果文件
|
||
</ElButton>
|
||
<el-tooltip
|
||
v-for="tag in formData.fileUploads.map((i) => i.url).filter((i) => i !== '')"
|
||
:key="tag"
|
||
content="点击下载"
|
||
placement="bottom"
|
||
>
|
||
<el-tag
|
||
:type="tagsType(formData.fileUploads.find((i) => i.url === tag)!.fileType)"
|
||
closable
|
||
:disable-transitions="false"
|
||
@close="
|
||
handleCloseTag(
|
||
tag,
|
||
formData.fileUploads.find((i) => i.url === tag)!.fileType
|
||
)
|
||
"
|
||
@click="downloadFile(formData.fileUploads.find((i) => i.url === tag)!.url)"
|
||
style="margin-left: 10px; cursor: pointer"
|
||
>
|
||
{{ formData.fileUploads.find((i) => i.url === tag)!.fileName }}
|
||
</el-tag>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
<div class="line">
|
||
<el-form
|
||
ref="formRef"
|
||
:model="formData"
|
||
label-width="80px"
|
||
:inline="true"
|
||
label-position="right"
|
||
class="formList"
|
||
>
|
||
<el-form-item label="专业" prop="specialtyName">
|
||
<el-input v-model="formData.specialtyName" placeholder="请输入专业" disabled />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="章节名称" prop="chapteridDictText">
|
||
<el-input
|
||
v-model="formData.chapteridDictTextVo"
|
||
placeholder="请输入章节名称"
|
||
disabled
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="课程" prop="courseName">
|
||
<el-input v-model="formData.courseName" placeholder="请输入课程" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="题型难度" prop="quLevel">
|
||
<el-select v-model="formData.quLevel" placeholder="请选择题型难度" clearable>
|
||
<el-option
|
||
v-for="dict in getIntDictOptions(DICT_TYPE.EXAM_QUE_DIFF)"
|
||
:key="dict.value"
|
||
:label="dict.label"
|
||
:value="dict.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="题型" prop="subjectName">
|
||
<el-input v-model="formData.subjectName" placeholder="请输入题型" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="知识点" prop="pointNames">
|
||
<el-input
|
||
v-model="formData.pointNamesVo"
|
||
placeholder="请选择知识点"
|
||
readonly
|
||
@click="openPoints()"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-dialog v-model="dialogVisiblePoints" title="选择知识点" width="30%">
|
||
<el-tree
|
||
ref="treeRef"
|
||
:data="deptList"
|
||
node-key="id"
|
||
:props="{ label: 'name', children: 'children' }"
|
||
highlight-current
|
||
default-expand-all
|
||
@node-click="handleNodeClick"
|
||
/>
|
||
</el-dialog>
|
||
<el-form-item label="启用状态" prop="status">
|
||
<el-radio-group v-model="formData.status">
|
||
<el-radio :value="'0'">启用</el-radio>
|
||
<el-radio :value="'1'">禁用</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="判分设置" name="score">
|
||
<div class="line">
|
||
<div class="title-text">测试程序(参考答案)</div>
|
||
<div class="block">
|
||
<div class="tip">
|
||
<p
|
||
>此处的程序可以用来验证测试用例的有效性,非自动判分的试题在人工批阅时可作为批阅的参考答案。
|
||
<br />
|
||
注意: 为确保系统能正确捕获程序运行的输出结果,要求 scanf() 函数不能加参数,
|
||
不能采用类似 scanf("请输入: %s") 的形式</p
|
||
>
|
||
</div>
|
||
<CodeEditor ref="textareaRef" v-model="referenceAnswer" :rows="10" />
|
||
<div class="btn-line">
|
||
<el-button type="primary" plain @click="addKeywordFromSelection"
|
||
>添加为关键字</el-button
|
||
>
|
||
<el-button type="primary" plain @click="openForm('4')">上传程序文件</el-button>
|
||
<el-button type="primary" plain @click="openRunTestDialog"
|
||
>运行并测试</el-button
|
||
>
|
||
</div>
|
||
<el-checkbox
|
||
v-model="formData.questionScores.checkAutoScore"
|
||
label="自动判分方式"
|
||
size="large"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<template v-if="formData.questionScores.checkAutoScore">
|
||
<div class="line">
|
||
<div class="title-text">测试用例</div>
|
||
<div class="block">
|
||
<div class="flex">
|
||
<el-checkbox
|
||
v-model="formData.questionScores.isPass"
|
||
label="编译通过得分"
|
||
size="large"
|
||
true-value="0"
|
||
false-value="1"
|
||
/>
|
||
<template v-if="formData.questionScores.isPass === '0'">
|
||
<div class="flex" style="margin-left: 20px">
|
||
编译得分比例:
|
||
<el-input
|
||
v-model="formData.questionScores.isPassScore"
|
||
type="number"
|
||
placeholder="Please input"
|
||
>
|
||
<template #append>%</template>
|
||
</el-input>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div>
|
||
<el-checkbox
|
||
v-model="formData.questionScores.isCompile"
|
||
label="使用测试用例"
|
||
size="large"
|
||
true-value="0"
|
||
false-value="1"
|
||
/>
|
||
<div v-if="formData.questionScores.isCompile === '0'">
|
||
<div class="flex" style="margin-left: 20px">
|
||
测试用例得分比例:
|
||
<el-input
|
||
v-model="formData.questionScores.isCompileScore"
|
||
type="number"
|
||
placeholder="Please input"
|
||
>
|
||
<template #append>%</template>
|
||
</el-input>
|
||
</div>
|
||
<div class="btn-line" style="margin-bottom: 10px">
|
||
<el-button type="primary" plain @click="addTestCase">添加用例</el-button>
|
||
<el-button type="danger" plain @click="deleteAllTestCases"
|
||
>删除全部</el-button
|
||
>
|
||
<el-button type="primary" plain>测试所有用例</el-button>
|
||
<el-button type="primary" plain @click="averageTestCasesWeight"
|
||
>均分权重</el-button
|
||
>
|
||
</div>
|
||
<el-table
|
||
:data="formData.answerList"
|
||
style="width: 100%"
|
||
@selection-change="handleExamSelectionChange"
|
||
>
|
||
<el-table-column type="index" width="50" />
|
||
<el-table-column type="selection" width="55" />
|
||
<el-table-column label="输入" width="180">
|
||
<template #default="scope">
|
||
<el-input v-model="scope.row.contentIn" type="textarea" :rows="1" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="输出" width="180">
|
||
<template #default="scope">
|
||
<el-input v-model="scope.row.content" type="textarea" :rows="1" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="权重(%)">
|
||
<template #default="scope">
|
||
<el-input v-model="scope.row.scoreRate" type="number" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="隐藏用例" width="90" align="center">
|
||
<template #default="scope">
|
||
<el-switch v-model="scope.row.hideCase" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="仅隐藏输出" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-switch v-model="scope.row.hideOutput" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="90" align="center">
|
||
<template #default="scope">
|
||
<el-button type="danger" link @click="deleteTestCase(scope.$index)"
|
||
>删除</el-button
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="line">
|
||
<div class="title-text">代码关键字</div>
|
||
<div class="block">
|
||
<div class="flex">
|
||
<el-checkbox
|
||
v-model="formData.questionScores.isKeyword"
|
||
label="检查代码关键字"
|
||
size="large"
|
||
true-value="0"
|
||
false-value="1"
|
||
/>
|
||
<template v-if="formData.questionScores.isKeyword === '0'">
|
||
<div class="flex" style="margin-left: 20px">
|
||
关键字得分比例:
|
||
<el-input
|
||
v-model="formData.questionScores.isKeywordScore"
|
||
type="number"
|
||
placeholder="Please input"
|
||
>
|
||
<template #append>%</template>
|
||
</el-input>
|
||
</div>
|
||
<div class="flex">
|
||
关键字得分临界值:
|
||
<el-input
|
||
v-model="formData.questionScores.keywordCutoff"
|
||
type="number"
|
||
placeholder="Please input"
|
||
>
|
||
<template #append>%</template>
|
||
</el-input>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<template v-if="formData.questionScores.isKeyword === '0'">
|
||
<div class="tip" style="margin-top: 10px">
|
||
<p
|
||
>提示:关键字支持正则表达式匹配方式,在新建或编辑中设置。<br />
|
||
判分时,关键字正确比例小于关键字得分临界值时,忽略验证编译及测试用例得分比例,仅获得关键字正确比例分数。<br />
|
||
判分时,关键字正确比例大于等于关键字得分临界值,编译通过且测试用例结果全对时,忽略关键字得分,直接获得试题满分。<br />
|
||
判分时,关键字正确比例大于等于关键字得分临界值,编译不一定通过且测试用例不一定全对时,获得分数
|
||
= 关键字得分 + 编译得分 + 测试用例结果得分。</p
|
||
>
|
||
</div>
|
||
<div class="btn-line" style="margin-bottom: 10px">
|
||
<el-button type="primary" plain @click="addScoreKeyword">新建</el-button>
|
||
<el-button type="danger" plain @click="deleteAllScoreKeywords"
|
||
>删除全部</el-button
|
||
>
|
||
<el-button type="primary" plain>校验关键字</el-button>
|
||
</div>
|
||
<el-table
|
||
:data="formData.questionKeywords"
|
||
style="width: 100%"
|
||
@selection-change="handleScoreSelectionChange"
|
||
>
|
||
<el-table-column type="index" width="50" />
|
||
<el-table-column type="selection" width="55" />
|
||
<el-table-column
|
||
prop="keyword"
|
||
label="关键字"
|
||
width="180"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column label="权重(%)">
|
||
<template #default="scope">
|
||
<el-input v-model="scope.row.scoreRate" type="number" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="180" align="center">
|
||
<template #default="scope">
|
||
<el-button type="primary" link @click="editScore(scope.row)"
|
||
>编辑</el-button
|
||
>
|
||
<el-button type="danger" link @click="deleteScoreKeyword(scope.$index)"
|
||
>删除</el-button
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="line">
|
||
<div class="title-text">自动判分选项</div>
|
||
<div class="block">
|
||
<el-form label-width="120px" label-position="right">
|
||
<el-form-item label="输出读取方式:">
|
||
<el-radio-group v-model="formData.questionScores.outputReadMethod">
|
||
<el-radio value="1">自动捕获程序输出</el-radio>
|
||
<el-radio value="2">从文件中读取输出</el-radio>
|
||
</el-radio-group>
|
||
<div
|
||
v-if="formData.questionScores.outputReadMethod === '2'"
|
||
class="flex"
|
||
style="margin-left: 10px"
|
||
>
|
||
<el-input
|
||
v-model="formData.questionScores.outputFileName"
|
||
placeholder="请输入文件名"
|
||
style="width: 200px"
|
||
maxlength="20"
|
||
/>
|
||
<span style="margin-left: 10px; color: #999"
|
||
>注:文件名最长20个字符。</span
|
||
>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="输出匹配规则:">
|
||
<el-radio-group v-model="formData.questionScores.outputMatchRule">
|
||
<el-radio value="1">完全匹配</el-radio>
|
||
<el-radio value="2">包含匹配</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="匹配选项:">
|
||
<el-checkbox
|
||
v-model="formData.questionScores.ignoreCase"
|
||
label="忽略输出大小写"
|
||
/>
|
||
<el-checkbox v-model="formData.questionScores.ignoreSymbols"
|
||
>忽略输出中英文符号(认为半角符号和全角中文符号是一样的)</el-checkbox
|
||
>
|
||
<el-checkbox
|
||
v-model="formData.questionScores.ignoreSpaces"
|
||
label="忽略输出中每行内容中的全部空格"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
<div class="line">
|
||
<div class="title-text">其它选项</div>
|
||
<div>
|
||
<div class="flex" style="margin-bottom: 10px">
|
||
<div class="text" style="width: 100px; display: flex; justify-content: right"
|
||
>时间限制:</div
|
||
>
|
||
<el-input
|
||
v-model="formData.questionScores.timeLimit"
|
||
type="number"
|
||
placeholder=""
|
||
/>
|
||
(ms,毫秒,1秒 等于 1000毫秒)
|
||
</div>
|
||
<div class="flex" style="margin-bottom: 10px">
|
||
<div class="text" style="width: 100px; display: flex; justify-content: right"
|
||
>内存限制:</div
|
||
>
|
||
<el-input
|
||
v-model="formData.questionScores.memoryLimit"
|
||
type="number"
|
||
placeholder=""
|
||
/>
|
||
(MB)
|
||
</div>
|
||
<div class="flex">
|
||
<div class="text" style="width: 100px; display: flex; justify-content: right"
|
||
>代码长度限制:</div
|
||
>
|
||
<el-input
|
||
v-model="formData.questionScores.codeLengthLimit"
|
||
type="number"
|
||
placeholder=""
|
||
/>
|
||
(KB)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="试题解析" name="analyse">
|
||
<div class="block">
|
||
<CodeEditor v-model="formData.analysis" :rows="10" />
|
||
</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('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="formData.keywords"
|
||
style="width: 100%"
|
||
height="410"
|
||
@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%"
|
||
style="height: auto"
|
||
: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="keyWordInput" 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="media">
|
||
<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-item>删除全部</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</template>
|
||
<div class="block">
|
||
<el-table
|
||
:data="mediumList"
|
||
style="width: 100%"
|
||
height="410"
|
||
@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-tabs>
|
||
</div>
|
||
</el-scrollbar>
|
||
<template #footer>
|
||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||
</template>
|
||
</Dialog>
|
||
|
||
<!-- 编辑关键字弹窗 -->
|
||
<el-dialog v-model="scoreDialogVisible" title="编辑关键字" width="600px">
|
||
<el-form :model="currentScoreItem" label-width="100px">
|
||
<el-form-item label="关键字">
|
||
<el-input v-model="currentScoreItem.keyword" placeholder="请输入关键字或正则表达式" />
|
||
</el-form-item>
|
||
<el-form-item label="插入正则">
|
||
<el-button-group>
|
||
<el-button @click="insertRegexMark('\\d')">数字</el-button>
|
||
<el-button @click="insertRegexMark('\\s')">空白</el-button>
|
||
<el-button @click="insertRegexMark('\\w')">单词字符</el-button>
|
||
<el-button @click="insertRegexMark('.')">任意字符</el-button>
|
||
</el-button-group>
|
||
</el-form-item>
|
||
<el-form-item label="测试正则">
|
||
<el-input
|
||
v-model="regexTestInput"
|
||
placeholder="输入测试字符串"
|
||
style="width: 200px; margin-right: 10px"
|
||
/>
|
||
<el-button @click="testRegex">测试</el-button>
|
||
<span style="margin-left: 10px">{{ regexTestResult }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="等价关键字">
|
||
<el-input
|
||
v-model="equivalentKeywordInput"
|
||
placeholder="输入后按回车或点击添加"
|
||
style="width: 200px; margin-right: 10px"
|
||
@keyup.enter="addEquivalentKeyword"
|
||
/>
|
||
<el-button @click="addEquivalentKeyword">添加</el-button>
|
||
</el-form-item>
|
||
<el-form-item label="">
|
||
<el-tag
|
||
v-for="tag in currentScoreItem.equivalentKeywords"
|
||
:key="tag"
|
||
closable
|
||
:disable-transitions="false"
|
||
@close="removeEquivalentKeyword(tag)"
|
||
style="margin-right: 5px"
|
||
>
|
||
{{ tag }}
|
||
</el-tag>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="scoreDialogVisible = false">取 消</el-button>
|
||
<el-button type="primary" @click="confirmScoreEdit">确 定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
<!-- 运行并测试弹窗 -->
|
||
<el-dialog v-model="runTestDialogVisible" title="运行与测试" width="700px">
|
||
<div class="run-test-dialog">
|
||
<div class="button-group">
|
||
<el-button type="primary" @click="runCode" :loading="isCodeRunning">调试运行</el-button>
|
||
<el-button type="success" @click="openReportDialog">评分报告</el-button>
|
||
</div>
|
||
<div class="result-panel">
|
||
<div class="result-title">运行结果:</div>
|
||
<pre class="result-content">{{ runResult }}</pre>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="runTestDialogVisible = false">关 闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 评分报告弹窗 -->
|
||
<el-dialog v-model="reportDialogVisible" title="评分报告" width="800px" append-to-body>
|
||
<!-- 在这里填充评分报告的具体内容 -->
|
||
<div v-if="scoringReport">
|
||
<p><strong>总得分:</strong> {{ scoringReport.totalScore }}</p>
|
||
<p><strong>编译结果:</strong> {{ scoringReport.compileResult }}</p>
|
||
<!-- ... 其他报告项 -->
|
||
</div>
|
||
<div v-else>
|
||
<p>暂无评分报告。</p>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="reportDialogVisible = false">关 闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
<!-- 表单弹窗:添加/修改 -->
|
||
<FileForm ref="FileRef" @success="handleUploadSuccess" />
|
||
</template>
|
||
<script lang="ts" setup>
|
||
import { ref, nextTick, reactive } from 'vue'
|
||
import * as QuestionApi from '@/api/paper/question'
|
||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||
import { CommonStatusEnum } from '@/utils/constants'
|
||
import CodeEditor from './components/CodeEditor.vue'
|
||
import { defaultProps, handleTree } from '@/utils/tree'
|
||
import * as SpecialtyApi from '@/api/points'
|
||
import * as DeptApi from '@/api/system/dept'
|
||
import * as UserApi from '@/api/system/user'
|
||
import { FormRules, ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Document, Search, InfoFilled, Paperclip, Upload } from '@element-plus/icons-vue'
|
||
import FileForm from './components/FileForm.vue'
|
||
import type { InputInstance } from 'element-plus'
|
||
import { useI18n } from '@/hooks/web/useI18n'
|
||
import { useMessage } from '@/hooks/web/useMessage'
|
||
import { Editor } from '@/components/Editor'
|
||
|
||
defineOptions({ name: 'CdesignForm' })
|
||
|
||
const { t } = useI18n() // 国际化
|
||
const message = useMessage() // 消息弹窗
|
||
|
||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||
const dialogTitle = ref('') // 弹窗的标题
|
||
const formLoading = ref(false) // 表单的加载中
|
||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||
|
||
const getDefaultFormData = () => ({
|
||
id: undefined,
|
||
pointNamesVo: '',
|
||
chapteridDictTextVo: '',
|
||
content: '',
|
||
contentText: '',
|
||
specialtyName: '',
|
||
courseName: '',
|
||
quBankName: '',
|
||
required: '',
|
||
chapteridDictText: '',
|
||
analysis: '',
|
||
quLevel: 0,
|
||
questionScores: {
|
||
scoreId: '',
|
||
quId: '',
|
||
isPass: '1',
|
||
isResult: '1',
|
||
isKeyword: '1',
|
||
isCompile: '',
|
||
isPassScore: '',
|
||
isResultScore: '',
|
||
isKeywordScore: '',
|
||
isCompileScore: '',
|
||
keywordCutoff: '',
|
||
compileCutoff: '',
|
||
checkAutoScore: false,
|
||
outputReadMethod: '1',
|
||
outputFileName: '',
|
||
outputMatchRule: '1',
|
||
ignoreCase: false,
|
||
ignoreSymbols: false,
|
||
ignoreSpaces: false,
|
||
timeLimit: 1000,
|
||
memoryLimit: 128,
|
||
codeLengthLimit: 64
|
||
},
|
||
pointNames: '',
|
||
subjectName: '',
|
||
status: '0',
|
||
resourceValue: '',
|
||
answer: '',
|
||
answerList: [],
|
||
questionKeywords: [],
|
||
keywords: [], // 用于关键字tab的表格
|
||
fileUploads: [
|
||
{ quId: '', url: '', fileType: '0', fileName: '' },
|
||
{ quId: '', url: '', fileType: '1', fileName: '' },
|
||
{ quId: '', url: '', fileType: '2', fileName: '' }
|
||
]
|
||
})
|
||
|
||
const formData = ref<any>(getDefaultFormData())
|
||
const formRef = ref() // 表单 Ref
|
||
const textareaRef = ref<any>() // 参考答案编辑器
|
||
|
||
/** 页面内容相关 */
|
||
const activeName = ref('common')
|
||
const referenceAnswer = ref('') // 参考答案(独立于formData,用于编辑器绑定)
|
||
|
||
const tagsType = (type: string) => {
|
||
if (type === '1') return 'primary'
|
||
if (type === '2') return 'success'
|
||
return 'info'
|
||
}
|
||
|
||
const downloadFile = async (url: string) => {
|
||
if (!url) {
|
||
message.warning('暂无可下载的文件地址')
|
||
return
|
||
}
|
||
try {
|
||
window.open(url, '_blank')
|
||
} catch (err: any) {
|
||
message.error(`下载失败:${err.message}`)
|
||
}
|
||
}
|
||
|
||
const handleCloseTag = (url: string, type: string) => {
|
||
const index = formData.value.fileUploads.findIndex((item) => item.url === url)
|
||
if (index !== -1) {
|
||
formData.value.fileUploads[index].url = ''
|
||
if (type === '1') {
|
||
formData.value.answer = ''
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 文件上传 */
|
||
const FileRef = ref()
|
||
const openForm = (type: string) => {
|
||
FileRef.value.open(type)
|
||
}
|
||
|
||
const handleUploadSuccess = async ({ url, fileType, file }) => {
|
||
const res = await QuestionApi.previewQuestion({ file })
|
||
if (res.code !== 0) return
|
||
|
||
const index = formData.value.fileUploads.findIndex((item) => item.fileType === fileType)
|
||
if (index !== -1) {
|
||
formData.value.fileUploads[index].url = url
|
||
switch (fileType) {
|
||
case '1':
|
||
formData.value.fileUploads[index].fileName = 'C语言编程题'
|
||
formData.value.answer = res.data
|
||
break
|
||
case '2':
|
||
formData.value.fileUploads[index].fileName = '结果'
|
||
break
|
||
case '3':
|
||
formData.value.fileUploads[index].fileName = '素材'
|
||
break
|
||
}
|
||
}
|
||
if (fileType === '4') {
|
||
referenceAnswer.value = res.data
|
||
}
|
||
}
|
||
|
||
/** 打开弹窗 */
|
||
const open = async (queryParams: any, type: string, id?: number) => {
|
||
dialogVisible.value = true
|
||
dialogTitle.value = t('action.' + type)
|
||
formType.value = type
|
||
resetForm()
|
||
|
||
if (id) {
|
||
formLoading.value = true
|
||
try {
|
||
const res = await QuestionApi.getQuestion(id)
|
||
res.keywords = Array.isArray(res.keywords) ? res.keywords : []
|
||
res.questionKeywords = Array.isArray(res.questionKeywords) ? res.questionKeywords : []
|
||
res.answerList = Array.isArray(res.answerList) ? res.answerList : []
|
||
res.fileUploads = Array.isArray(res.fileUploads)
|
||
? res.fileUploads
|
||
: getDefaultFormData().fileUploads
|
||
res.content = res.content || ''
|
||
res.answer = res.answer || ''
|
||
res.analysis = res.analysis || ''
|
||
|
||
formData.value = { ...formData.value, ...res }
|
||
referenceAnswer.value = res.analysis || '' // 假设参考答案在 analysis 字段
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
} else {
|
||
formData.value.specialtyName = queryParams.specialtyName
|
||
formData.value.courseName = queryParams.courseName
|
||
formData.value.subjectName = queryParams.subjectName
|
||
formData.value.pointNames = queryParams.pointNamesVo
|
||
formData.value.pointNamesVo = queryParams.pointNames
|
||
formData.value.chapteridDictText = queryParams.chapteridDictTextVo
|
||
formData.value.chapteridDictTextVo = queryParams.chapteridDictText
|
||
}
|
||
}
|
||
defineExpose({ open })
|
||
|
||
/** 测试用例相关 */
|
||
const multipleSelection = ref<any[]>([])
|
||
const handleExamSelectionChange = (val: any) => {
|
||
multipleSelection.value = val
|
||
}
|
||
|
||
const addTestCase = () => {
|
||
formData.value.answerList.push({
|
||
contentIn: '',
|
||
content: '',
|
||
scoreRate: 0,
|
||
hideCase: false,
|
||
hideOutput: false
|
||
})
|
||
}
|
||
|
||
const deleteTestCase = (index: number) => {
|
||
formData.value.answerList.splice(index, 1)
|
||
}
|
||
|
||
const deleteAllTestCases = async () => {
|
||
if (formData.value.answerList.length === 0) {
|
||
message.warning('当前没有测试用例可删除')
|
||
return
|
||
}
|
||
await message.confirm('确定要删除所有测试用例吗?')
|
||
formData.value.answerList = []
|
||
}
|
||
|
||
const averageTestCasesWeight = () => {
|
||
const cases = formData.value.answerList
|
||
if (cases.length > 0) {
|
||
const averageWeight = Math.floor(100 / cases.length)
|
||
const remainder = 100 % cases.length
|
||
const firstPartCount = cases.length - remainder // 计算不加余数的部分有多少个
|
||
|
||
cases.forEach((item, index) => {
|
||
// 将余数分配给末尾的用例
|
||
if (index < firstPartCount) {
|
||
item.scoreRate = averageWeight
|
||
} else {
|
||
item.scoreRate = averageWeight + 1
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
/** 代码关键字(计分)相关 */
|
||
const multipleScoreSelection = ref<any[]>([])
|
||
const handleScoreSelectionChange = (val: any) => {
|
||
multipleScoreSelection.value = val
|
||
}
|
||
|
||
const scoreDialogVisible = ref(false)
|
||
const currentScoreItem = ref<any>({})
|
||
const equivalentKeywordInput = ref('')
|
||
const regexTestInput = ref('')
|
||
const regexTestResult = ref('')
|
||
const isEditingScoreKeyword = ref(false)
|
||
|
||
const addScoreKeyword = () => {
|
||
isEditingScoreKeyword.value = false
|
||
currentScoreItem.value = reactive({
|
||
keyword: '',
|
||
scoreRate: 0,
|
||
equivalentKeywords: []
|
||
})
|
||
scoreDialogVisible.value = true
|
||
}
|
||
|
||
const editScore = (item: any) => {
|
||
isEditingScoreKeyword.value = true
|
||
currentScoreItem.value = reactive(JSON.parse(JSON.stringify(item)))
|
||
if (!currentScoreItem.value.equivalentKeywords) {
|
||
currentScoreItem.value.equivalentKeywords = []
|
||
}
|
||
scoreDialogVisible.value = true
|
||
}
|
||
|
||
const confirmScoreEdit = () => {
|
||
if (!isEditingScoreKeyword.value) {
|
||
formData.value.questionKeywords.push(currentScoreItem.value)
|
||
} else {
|
||
const index = formData.value.questionKeywords.findIndex(
|
||
(item) => item.keywordId === currentScoreItem.value.keywordId
|
||
)
|
||
if (index !== -1) {
|
||
formData.value.questionKeywords[index] = currentScoreItem.value
|
||
}
|
||
}
|
||
scoreDialogVisible.value = false
|
||
}
|
||
|
||
const deleteScoreKeyword = (index: number) => {
|
||
formData.value.questionKeywords.splice(index, 1)
|
||
}
|
||
|
||
const deleteAllScoreKeywords = async () => {
|
||
if (formData.value.questionKeywords.length === 0) {
|
||
message.warning('当前没有关键字可删除')
|
||
return
|
||
}
|
||
await message.confirm('确定要删除所有代码关键字吗?')
|
||
formData.value.questionKeywords = []
|
||
}
|
||
|
||
const addEquivalentKeyword = () => {
|
||
if (
|
||
equivalentKeywordInput.value &&
|
||
!currentScoreItem.value.equivalentKeywords.includes(equivalentKeywordInput.value)
|
||
) {
|
||
currentScoreItem.value.equivalentKeywords.push(equivalentKeywordInput.value)
|
||
equivalentKeywordInput.value = ''
|
||
}
|
||
}
|
||
|
||
const removeEquivalentKeyword = (keyword: string) => {
|
||
const index = currentScoreItem.value.equivalentKeywords.indexOf(keyword)
|
||
if (index > -1) {
|
||
currentScoreItem.value.equivalentKeywords.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
const insertRegexMark = (mark: string) => {
|
||
currentScoreItem.value.keyword += mark
|
||
}
|
||
|
||
const testRegex = () => {
|
||
if (!currentScoreItem.value.keyword) {
|
||
regexTestResult.value = '请输入正则表达式'
|
||
return
|
||
}
|
||
try {
|
||
const regex = new RegExp(currentScoreItem.value.keyword)
|
||
regexTestResult.value = regex.test(regexTestInput.value) ? '匹配成功' : '匹配失败'
|
||
} catch (e: any) {
|
||
regexTestResult.value = `正则无效: ${e.message}`
|
||
}
|
||
}
|
||
|
||
const addKeywordFromSelection = () => {
|
||
if (!textareaRef.value) return
|
||
const selectedText = textareaRef.value.getSelectedText().trim()
|
||
if (selectedText) {
|
||
if (formData.value.questionKeywords.some((item) => item.keyword === selectedText)) {
|
||
message.warning('关键字已存在')
|
||
return
|
||
}
|
||
formData.value.questionKeywords.push({
|
||
keyword: selectedText,
|
||
scoreRate: 0
|
||
})
|
||
} else {
|
||
message.warning('请先在测试程序中选择要添加的关键字')
|
||
}
|
||
}
|
||
|
||
/** 关键字 Tab 相关 */
|
||
const multipleKeywordSelection = ref<any[]>([])
|
||
const handleKeywordSelectionChange = (val: any) => {
|
||
multipleKeywordSelection.value = val
|
||
}
|
||
const keyVisible = ref(false)
|
||
const keyEditType = ref('')
|
||
const keyWordInput = ref('')
|
||
|
||
const editKeyword = (key: string) => {
|
||
keyEditType.value = key
|
||
if (key === 'create') {
|
||
keyWordInput.value = ''
|
||
keyVisible.value = true
|
||
} else if (key === 'delete') {
|
||
if (multipleKeywordSelection.value.length === 0) {
|
||
message.warning('请选择要删除的关键字')
|
||
return
|
||
}
|
||
ElMessageBox.confirm('确定要删除选中的关键字吗?').then(() => {
|
||
const selectedKeywords = multipleKeywordSelection.value.map((item) => item.keyword)
|
||
formData.value.keywords = formData.value.keywords.filter(
|
||
(item) => !selectedKeywords.includes(item.keyword)
|
||
)
|
||
})
|
||
} else if (key === 'deleteall') {
|
||
ElMessageBox.confirm('确定要删除所有关键字吗?').then(() => {
|
||
formData.value.keywords = []
|
||
})
|
||
}
|
||
}
|
||
|
||
const keyDialogClose = () => {
|
||
keyVisible.value = false
|
||
}
|
||
|
||
const confirmKeyDialogVisible = () => {
|
||
if (keyEditType.value === 'create' && keyWordInput.value) {
|
||
if (formData.value.keywords.some((item) => item.keyword === keyWordInput.value)) {
|
||
message.warning('关键字已存在')
|
||
return
|
||
}
|
||
formData.value.keywords.push({ keyword: keyWordInput.value })
|
||
}
|
||
keyVisible.value = false
|
||
}
|
||
|
||
/** 媒体文件 Tab 相关 */
|
||
const mediumList = ref<any[]>([])
|
||
const multipleMediumSelection = ref<any[]>([])
|
||
const handleMediumSelectionChange = (val: any) => {
|
||
multipleMediumSelection.value = val
|
||
}
|
||
const fileList = ref([])
|
||
|
||
/** 运行与测试相关 */
|
||
const runTestDialogVisible = ref(false)
|
||
const reportDialogVisible = ref(false)
|
||
const isCodeRunning = ref(false)
|
||
const runResult = ref('')
|
||
const scoringReport = ref<any>(null)
|
||
|
||
const openRunTestDialog = () => {
|
||
runResult.value = '点击“调试运行”按钮以执行代码。'
|
||
runTestDialogVisible.value = true
|
||
}
|
||
|
||
const runCode = async () => {
|
||
if (!referenceAnswer.value) {
|
||
message.warning('参考答案代码为空,无法运行。')
|
||
return
|
||
}
|
||
isCodeRunning.value = true
|
||
runResult.value = '代码正在运行中,请稍候...'
|
||
try {
|
||
// const res = await QuestionApi.runCode({ code: referenceAnswer.value })
|
||
// runResult.value = res.data.output || '代码运行完成,无输出。'
|
||
setTimeout(() => {
|
||
runResult.value = '模拟运行结果:\nHello World!'
|
||
isCodeRunning.value = false
|
||
}, 1500)
|
||
} catch (error) {
|
||
runResult.value = '代码运行出错,请检查代码或联系管理员。'
|
||
console.error('Run code error:', error)
|
||
isCodeRunning.value = false
|
||
}
|
||
}
|
||
|
||
const openReportDialog = async () => {
|
||
scoringReport.value = {
|
||
totalScore: 85,
|
||
compileResult: '编译成功'
|
||
}
|
||
reportDialogVisible.value = true
|
||
}
|
||
|
||
/** 提交表单 */
|
||
const emit = defineEmits(['success'])
|
||
const submitForm = async () => {
|
||
if (!formRef.value) return
|
||
const valid = await formRef.value.validate()
|
||
if (!valid) return
|
||
|
||
formLoading.value = true
|
||
try {
|
||
const dataToSubmit = JSON.parse(JSON.stringify(formData.value))
|
||
// 如果参考答案是独立管理的,提交时需要赋值给 analysis
|
||
dataToSubmit.analysis = referenceAnswer.value
|
||
|
||
if (formType.value === 'create') {
|
||
await QuestionApi.addQuestion(dataToSubmit)
|
||
message.success(t('common.createSuccess'))
|
||
} else {
|
||
await QuestionApi.editQuestion(dataToSubmit)
|
||
message.success(t('common.updateSuccess'))
|
||
}
|
||
dialogVisible.value = false
|
||
emit('success')
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 重置表单 */
|
||
const resetForm = () => {
|
||
formData.value = getDefaultFormData()
|
||
referenceAnswer.value = ''
|
||
mediumList.value = []
|
||
formRef.value?.resetFields()
|
||
}
|
||
|
||
/** 知识点/章节选择 */
|
||
const dialogVisiblePoints = ref(false)
|
||
const treeRef = ref()
|
||
const deptList = ref<any[]>([])
|
||
|
||
const handleTreeWithLevel = (list, level = 1) => {
|
||
return list.map((item) => {
|
||
const node = { ...item, level }
|
||
if (item.children && item.children.length > 0) {
|
||
node.children = handleTreeWithLevel(item.children, level + 1)
|
||
}
|
||
return node
|
||
})
|
||
}
|
||
|
||
const handleNodeClick = (data) => {
|
||
if (data.level === 3) {
|
||
formData.value.pointNames = data.id
|
||
formData.value.pointNamesVo = data.name
|
||
const currentNode = treeRef.value.getNode(data)
|
||
const parentNode = currentNode.parent
|
||
if (parentNode && parentNode.data) {
|
||
formData.value.chapteridDictTextVo = parentNode.data.name
|
||
formData.value.chapteridDictText = parentNode.data.id
|
||
}
|
||
dialogVisiblePoints.value = false
|
||
}
|
||
}
|
||
|
||
const getTree = async () => {
|
||
const res = await SpecialtyApi.listPoints()
|
||
const tree = handleTree(res)
|
||
deptList.value = handleTreeWithLevel(tree)
|
||
}
|
||
|
||
const openPoints = async () => {
|
||
await getTree()
|
||
dialogVisiblePoints.value = true
|
||
}
|
||
</script>
|
||
<style lang="scss" scoped>
|
||
.program-edit {
|
||
:deep(.el-dialog) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 85vh;
|
||
.el-dialog__header {
|
||
border-bottom: 1px solid #ededed;
|
||
}
|
||
.el-dialog__footer {
|
||
border-top: 1px solid #ededed;
|
||
}
|
||
.el-dialog__body {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
.main {
|
||
height: 100%;
|
||
position: relative;
|
||
padding-bottom: 100px;
|
||
padding-left: 10px;
|
||
padding-right: 10px;
|
||
.tabsTip {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 20px;
|
||
color: #4f9dfd;
|
||
line-height: 100%;
|
||
text-align: center;
|
||
align-items: center;
|
||
display: flex;
|
||
}
|
||
.el-tabs {
|
||
height: 100%;
|
||
.el-tabs__content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
.line {
|
||
margin-bottom: 10px;
|
||
.tips {
|
||
background-color: #fbf8e4;
|
||
color: #886d3e;
|
||
padding: 10px;
|
||
margin-bottom: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
.title-text {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
.buttons {
|
||
margin-top: 10px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.block {
|
||
.tip {
|
||
color: #8a6d3b;
|
||
background-color: #fcf8e3;
|
||
border: 1px solid transparent;
|
||
border-color: #faebcc;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
margin-bottom: 10px;
|
||
> p {
|
||
margin: 0;
|
||
}
|
||
}
|
||
.btn-line {
|
||
display: flex;
|
||
margin-top: 10px;
|
||
}
|
||
.flex {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
}
|
||
.formList {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
margin-top: 60px;
|
||
width: 100%;
|
||
.el-form-item {
|
||
margin-bottom: 20px;
|
||
width: 100%;
|
||
label {
|
||
font-weight: 600;
|
||
}
|
||
.el-select {
|
||
width: 100%;
|
||
}
|
||
.el-input {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.el-input {
|
||
width: 45%;
|
||
}
|
||
}
|
||
|
||
.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>
|