diff --git a/package.json b/package.json
index 1e703ee..bd4a637 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4",
+ "ace-builds": "^1.43.2",
"animate.css": "^4.1.1",
"axios": "^1.6.8",
"benz-amr-recorder": "^1.1.5",
@@ -76,6 +77,7 @@
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",
+ "vue3-ace-editor": "^2.2.4",
"vue3-signature": "^0.2.4",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d3e047e..694e8a7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
'@zxcvbn-ts/core':
specifier: ^3.0.4
version: 3.0.4
+ ace-builds:
+ specifier: ^1.43.2
+ version: 1.43.2
animate.css:
specifier: ^4.1.1
version: 4.1.1
@@ -161,6 +164,9 @@ importers:
vue-types:
specifier: ^5.1.1
version: 5.1.3(vue@3.5.12(typescript@5.3.3))
+ vue3-ace-editor:
+ specifier: ^2.2.4
+ version: 2.2.4(ace-builds@1.43.2)(vue@3.5.12(typescript@5.3.3))
vue3-signature:
specifier: ^0.2.4
version: 0.2.4(vue@3.5.12(typescript@5.3.3))
@@ -2340,8 +2346,8 @@ packages:
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==, tarball: https://registry.npmmirror.com/JSONStream/-/JSONStream-1.3.5.tgz}
hasBin: true
- ace-builds@1.39.1:
- resolution: {integrity: sha512-HcJbBzx8qY66t9gZo/sQu7pi0wO/CFLdYn1LxQO1WQTfIkMfyc7LRnBpsp/oNCSSU/LL83jXHN1fqyOTuIhUjg==}
+ ace-builds@1.43.2:
+ resolution: {integrity: sha512-3wzJUJX0RpMc03jo0V8Q3bSb/cKPnS7Nqqw8fVHsCCHweKMiTIxT3fP46EhjmVy6MCuxwP801ere+RW245phGw==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, tarball: https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz}
@@ -5079,6 +5085,12 @@ packages:
vue:
optional: true
+ vue3-ace-editor@2.2.4:
+ resolution: {integrity: sha512-FZkEyfpbH068BwjhMyNROxfEI8135Sc+x8ouxkMdCNkuj/Tuw83VP/gStFQqZHqljyX9/VfMTCdTqtOnJZGN8g==}
+ peerDependencies:
+ ace-builds: '*'
+ vue: ^3
+
vue3-signature@0.2.4:
resolution: {integrity: sha512-XFwwFVK9OG3F085pKIq2SlNVqx32WdFH+TXbGEWc5FfEKpx8oMmZuAwZZ50K/pH2FgmJSE8IRwU9DDhrLpd6iA==}
peerDependencies:
@@ -7659,7 +7671,7 @@ snapshots:
jsonparse: 1.3.1
through: 2.3.8
- ace-builds@1.39.1: {}
+ ace-builds@1.43.2: {}
acorn-jsx@5.3.2(acorn@8.14.0):
dependencies:
@@ -9124,7 +9136,7 @@ snapshots:
jsoneditor@9.10.5:
dependencies:
- ace-builds: 1.39.1
+ ace-builds: 1.43.2
ajv: 6.12.6
javascript-natural-sort: 0.7.1
jmespath: 0.16.0
@@ -10546,6 +10558,12 @@ snapshots:
optionalDependencies:
vue: 3.5.12(typescript@5.3.3)
+ vue3-ace-editor@2.2.4(ace-builds@1.43.2)(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ ace-builds: 1.43.2
+ resize-observer-polyfill: 1.5.1
+ vue: 3.5.12(typescript@5.3.3)
+
vue3-signature@0.2.4(vue@3.5.12(typescript@5.3.3)):
dependencies:
default-passive-events: 2.0.0
diff --git a/src/views/paper/question/CdesignForm.vue b/src/views/paper/question/CdesignForm.vue
index cade925..8bbc060 100644
--- a/src/views/paper/question/CdesignForm.vue
+++ b/src/views/paper/question/CdesignForm.vue
@@ -42,7 +42,7 @@
不能采用类似 scanf("请输入: %s") 的形式
插入作答区标记后请在试题题目中明确告知学生不要自行更改作答区标记,以免无法正常评分。
-
+
更改答题程序模板无效,若需要更改请上传程序文件。
@@ -154,11 +154,15 @@
不能采用类似 scanf("请输入: %s") 的形式
-
+
- 添加为关键字
- 上传程序文件
- 运行并测试
+ 添加为关键字
+ 上传程序文件
+ 运行并测试
-
-
测试用例
-
-
-
-
- 编译得分比例:
-
- %
-
+
+
+
测试用例
+
+
+
+
+
+ 编译得分比例:
+
+ %
+
+
+
+
+
+
+
+
+
+
+
+
判分时,测试用例得分比例 = 100% - 关键字得分比例 -
+ 编译得分比例;默认最小 10% 得分比例;
+ 设置“测试用例全对时直接得满分”,测试用例结果全对时,忽略关键字得分,直接获得试题满分;否则按上述条件计算得分。
+
+
+ 测试用例部分对时得分
+ 测试用例全对时得分
+
+
+ 添加用例
+ 导入用例
+ 导出模板
+ 删除全部
+ 测试所有用例
+ 均分权重
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
-
-
-
-
-
-
-
+
+
+
代码关键字
+
+
+
+
+
+ 关键字得分比例:
+
+ %
+
+
+
+ 关键字得分临界值:
+
+ %
+
+
+
+
+
+
判分时,测试用例得分比例 = 100% - 关键字得分比例 - 编译得分比例;默认最小
- 10% 得分比例;
- 设置“测试用例全对时直接得满分”,测试用例结果全对时,忽略关键字得分,直接获得试题满分;否则按上述条件计算得分。
提示:关键字支持正则表达式匹配方式,在新建或编辑中设置。
+ 判分时,关键字正确比例小于关键字得分临界值时,忽略验证编译及测试用例得分比例,仅获得关键字正确比例分数。
+ 判分时,关键字正确比例大于等于关键字得分临界值,编译通过且测试用例结果全对时,忽略关键字得分,直接获得试题满分。
+ 判分时,关键字正确比例大于等于关键字得分临界值,编译不一定通过且测试用例不一定全对时,获得分数
+ = 关键字得分 + 编译得分 + 测试用例结果得分。
-
- 测试用例部分对时得分
- 测试用例全对时得分
-
- 添加用例
- 导入用例
- 导出模板
+ 新建
+ 导入
删除全部
- 测试所有用例
- 均分权重
+ 校验关键字
-
-
-
- 删除
+ 编辑
+ 删除
-
+
-
-
-
代码关键字
-
-
-
-
- 关键字得分比例:
+
+
自动判分选项
+
+
+
+
+ 自动捕获程序输出
+ 从文件中读取输出
+
+
+
+ 注:文件名最长20个字符。
+
+
+
+
+ 完全匹配
+ 包含匹配
+
+
+
+
+ 忽略输出中英文符号(认为半角符号和全角中文符号是一样的)
+
+
+
+
+
+
+
其它选项
+
+
+ 时间限制:
- %
-
+ />
+ (ms,毫秒,1秒 等于 1000毫秒)
- 关键字得分临界值:
+ 内存限制:
- %
-
+ />
+ (MB)
+
+
+ 代码长度限制:
+
+ (KB)
-
-
提示:关键字支持正则表达式匹配方式,在新建或编辑中设置。
- 判分时,关键字正确比例小于关键字得分临界值时,忽略验证编译及测试用例得分比例,仅获得关键字正确比例分数。
- 判分时,关键字正确比例大于等于关键字得分临界值,编译通过且测试用例结果全对时,忽略关键字得分,直接获得试题满分。
- 判分时,关键字正确比例大于等于关键字得分临界值,编译不一定通过且测试用例不一定全对时,获得分数
- = 关键字得分 + 编译得分 + 测试用例结果得分。
-
-
- 新建
- 导入
- 删除全部
- 校验关键字
-
-
-
-
-
-
-
-
- 删除
-
-
-
-
-
-
其它选项
-
-
- 时间限制:
-
- (ms,毫秒,1秒 等于 1000毫秒)
-
-
- 内存限制:
-
- (MB)
-
-
- 代码长度限制:
-
- (KB)
-
-
-
+
-
+
@@ -456,6 +513,89 @@
取 消
+
+
+
+
+
+
+
+
+
+ 数字
+ 空白
+ 单词字符
+ 任意字符
+
+
+
+
+ 测试
+ {{ regexTestResult }}
+
+
+
+ 添加
+
+
+
+ {{ tag }}
+
+
+
+
+ 取 消
+ 确 定
+
+
+
+
+
+
+ 调试运行
+ 评分报告
+
+
+
运行结果:
+
{{ runResult }}
+
+
+
+ 关 闭
+
+
+
+
+
+
+
+
总得分: {{ scoringReport.totalScore }}
+
编译结果: {{ scoringReport.compileResult }}
+
+
+
+
+ 关 闭
+
+
@@ -464,7 +604,7 @@
import * as QuestionApi from '@/api/paper/question'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
-import ElTextarea from './components/el-textarea.vue'
+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'
@@ -529,6 +669,50 @@ const handleCloseTag = (url: string, type: string) => {
}
}
+// 运行与测试相关
+const runTestDialogVisible = ref(false)
+const reportDialogVisible = ref(false)
+const isCodeRunning = ref(false)
+const runResult = ref('')
+const scoringReport = ref
(null) // 存储评分报告
+
+const openRunTestDialog = () => {
+ runResult.value = '点击“调试运行”按钮以执行代码。'
+ runTestDialogVisible.value = true
+}
+
+const runCode = async () => {
+ if (!textarea.value) {
+ message.warning('参考答案代码为空,无法运行。')
+ return
+ }
+ isCodeRunning.value = true
+ runResult.value = '代码正在运行中,请稍候...'
+ try {
+ // 假设有一个API用于运行代码
+ // const res = await QuestionApi.runCode({ code: textarea.value })
+ // runResult.value = res.data.output || '代码运行完成,无输出。'
+ } catch (error) {
+ runResult.value = '代码运行出错,请检查代码或联系管理员。'
+ console.error('Run code error:', error)
+ } finally {
+ isCodeRunning.value = false
+ }
+}
+
+const openReportDialog = async () => {
+ // 假设有一个API用于获取评分报告
+ // const report = await QuestionApi.getScoringReport({ questionId: formData.value.id });
+ // scoringReport.value = report.data;
+
+ // 模拟数据
+ scoringReport.value = {
+ totalScore: 85,
+ compileResult: '编译成功'
+ }
+ reportDialogVisible.value = true
+}
+
const showInput = () => {
inputVisible.value = true
nextTick(() => {
@@ -589,7 +773,7 @@ const formData = ref({
]
})
const formRef = ref() // 表单 Ref
-
+const textareaRef = ref()
/** 添加/修改操作 */
const FileRef = ref()
const openForm = (type: string) => {
@@ -630,10 +814,12 @@ const handleUploadSuccess = async ({ url, fileType, file }) => {
break
}
}
+ // 处理参考答案的上传
+ if (fileType === '4') {
+ textarea.value = res.data
+ }
}
-/** 页面内容结束 */
-
/** 打开弹窗 */
const open = async (queryParams: any, type: string, id?: number) => {
dialogVisible.value = true
@@ -672,11 +858,26 @@ const open = async (queryParams: any, type: string, id?: number) => {
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
const programData = ref({
- title: '',
- checkAutoScore: false,
- checkKeyword: false,
- percent: 0,
- radio1: ''
+ title: '', // 测试程序标题
+ checkAutoScore: false, // 控制“自动判分方式”复选框
+ checkCompile: false, // 控制“编译通过得分”复选框
+ compilePercent: 0, // 编译得分比例
+ useTestCases: false, // 控制“使用测试用例”复选框
+ fullScoreOnAllTestCases: false, // 控制“全对得满分”复选框
+ checkKeyword: false, // 控制“检查代码关键字”复选框
+ percent: 0, // 关键字得分比例
+ radio1: '', // 控制测试用例部分对或全对的单选框
+ // 自动判分选项
+ outputReadMethod: '1',
+ outputFileName: '',
+ outputMatchRule: '1',
+ ignoreCase: false,
+ ignoreSymbols: false,
+ ignoreSpaces: false,
+ // 其它选项
+ timeLimit: 1000,
+ memoryLimit: 128,
+ codeLengthLimit: 64
})
const examList = ref([] as any)
@@ -693,8 +894,100 @@ const multipleScoreSelection = ref([] as any)
const handleScoreSelectionChange = (val: any) => {
multipleScoreSelection.value = val
}
-const delScore = (val: any) => {
- console.log(val)
+
+// 编辑关键字弹窗相关
+const scoreDialogVisible = ref(false)
+const currentScoreItem = ref({})
+const equivalentKeywordInput = ref('')
+const regexTestInput = ref('')
+const regexTestResult = ref('')
+
+const editScore = (scope: any) => {
+ // 创建一个深拷贝,避免直接修改原始数据
+ currentScoreItem.value = JSON.parse(JSON.stringify(scope.row))
+ // 确保等价关键字列表存在
+ if (!currentScoreItem.value.equivalentKeywords) {
+ currentScoreItem.value.equivalentKeywords = []
+ }
+ regexTestInput.value = ''
+ regexTestResult.value = ''
+ scoreDialogVisible.value = true
+}
+
+const confirmScoreEdit = () => {
+ const index = scoreKeyList.value.findIndex((item) => item.title === currentScoreItem.value.title) // 假设title是唯一标识
+ if (index !== -1) {
+ scoreKeyList.value[index] = currentScoreItem.value
+ }
+ scoreDialogVisible.value = false
+}
+
+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.title += mark
+}
+
+const testRegex = () => {
+ if (!currentScoreItem.value.title) {
+ regexTestResult.value = '请输入正则表达式'
+ return
+ }
+ try {
+ const regex = new RegExp(currentScoreItem.value.title)
+ if (regex.test(regexTestInput.value)) {
+ regexTestResult.value = '匹配成功'
+ } else {
+ regexTestResult.value = '匹配失败'
+ }
+ } catch (e: any) {
+ regexTestResult.value = `正则表达式错误: ${e.message}`
+ }
+}
+
+const delScore = async (scope: any) => {
+ try {
+ // 删除的二次确认
+ await message.confirm('是否确认删除该关键字?')
+ // 从列表中移除
+ scoreKeyList.value.splice(scope.$index, 1)
+ message.success('删除成功')
+ } catch {}
+}
+
+const addKeywordFromSelection = () => {
+ if (!textareaRef.value) return
+
+ const selectedText = textareaRef.value.getSelectedText().trim()
+
+ if (selectedText) {
+ // 检查关键字是否已存在
+ if (scoreKeyList.value.some((item) => item.title === selectedText)) {
+ message.warning('关键字已存在')
+ return
+ }
+ scoreKeyList.value.push({
+ title: selectedText,
+ displayIndex: 0 // 默认权重为0,可以根据需要修改
+ })
+ } else {
+ message.warning('请先在测试程序中选择要添加的关键字')
+ }
}
// 关键字
@@ -791,6 +1084,28 @@ const resetForm = () => {
{ quId: '', url: '', fileType: '2', fileName: '' }
]
}
+
+ // 重置 programData
+ programData.value = {
+ title: '',
+ checkAutoScore: false,
+ checkCompile: false,
+ compilePercent: 0,
+ useTestCases: false,
+ fullScoreOnAllTestCases: false,
+ checkKeyword: false,
+ percent: 0,
+ radio1: '',
+ outputReadMethod: '1',
+ outputFileName: '',
+ outputMatchRule: '1',
+ ignoreCase: false,
+ ignoreSymbols: false,
+ ignoreSpaces: false,
+ timeLimit: 1000,
+ memoryLimit: 128,
+ codeLengthLimit: 64
+ }
content.value = ''
textarea.value = ''
inputText.value = ''
diff --git a/src/views/paper/question/components/CodeEditor.vue b/src/views/paper/question/components/CodeEditor.vue
new file mode 100644
index 0000000..4ff0835
--- /dev/null
+++ b/src/views/paper/question/components/CodeEditor.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/src/views/paper/question/components/FileForm.vue b/src/views/paper/question/components/FileForm.vue
index 3bad2e3..0eb87cb 100644
--- a/src/views/paper/question/components/FileForm.vue
+++ b/src/views/paper/question/components/FileForm.vue
@@ -80,7 +80,7 @@ const submitFormSuccess = (response: any, file: any) => {
dialogVisible.value = false
formLoading.value = false
uploadRef.value?.clearFiles()
- message.success(t('common.createSuccess'))
+ // message.success(t('common.createSuccess'))
// 触发成功事件,携带文件信息传递给父组件
emit('success', { url, fileType: currentType.value, file: file.raw })