feat(course): 添加自定义考试功能并优化试卷组件

- 新增自定义考试试卷类型支持
- 实现试卷预览和编辑功能
- 添加试题管理组件,支持单选、多选、判断题
- 集成雪花ID生成器用于试题唯一标识
- 优化课程创建流程,支持考试内容配置
- 扩展SCSS样式库,增加flex布局和间距工具类
- 新增课程API模块,完善考试相关接口
- 实现试卷内容动态加载和保存逻辑
This commit is contained in:
陈昱达
2025-11-25 14:45:44 +08:00
parent f07582d5c1
commit 6c87968ab4
8 changed files with 1240 additions and 14 deletions

View File

@@ -35,6 +35,7 @@
"quill-blot-formatter": "^1.0.5",
"quill-image-drop-module": "^1.0.3",
"quill-image-resize-module": "^3.0.0",
"snowflake-id-js": "^1.0.1",
"sortablejs": "^1.15.0",
"vue": "^3.2.13",
"vue-cookies": "^1.8.2",

503
src/api/modules/course.js Normal file
View File

@@ -0,0 +1,503 @@
/**
* 课程的操作,课程的添加,修改,列表查询,课程的审核发布等操作。
* 针对于管理员,教师的功能
*
**/
import ajax from "./xajax.js";
/**
* 保存课程基本信息,新增和更新都是此方式
* @param {Object} data
*{
course:{
课程的基本信息,具体字段内容另外提供
name课程名称
type课程类型10微课21在线课(直播)20:在线课( 录播)30:面授课40:混合式,
summary摘人
overview 课程简介
coverImg封面图
sysType系统分类只存储最后一级,
resOwner1:资源归属一级的id
resOwner2:资源归属二级的id
resOwner3:资源归属三级的id
forUsers: 目标人群
forScene应用场景
openObject 开放权限
value:课程价值
tags标签多个使用-分隔
keywords:关键字
device: 1PC端可见2移动端可见3多端可见
status: 1:未提交(草稿);2:已提交;
publishedtrue/false 是否发布
enabledtrue/false 启用、停用
isTop: true/false 是否置顶
source整数课程来源1:内部2;外部
},
teachers:[
{
teacherId:教师的id,
teacherName:教师的名称
}
]
}
*/
const saveBase = function (data) {
return ajax.postJson("/systemapi/xboe/m/course/manage/save", data);
};
/**
* 仅仅是保存课程信息,不包括教师信息
* @param {Object} data
*/
const saveOnlyCourse = function (data) {
return ajax.postJson(
"/systemapi/xboe/m/course/manage/save-only-course",
data
);
};
/**提交课程*/
const submitCourse = function (data) {
return ajax.postJson("/systemapi/xboe/m/course/manage/submit", data);
};
/**撤销已提交审核的课程*/
const revokeSubmit = function (id) {
return ajax.post("/systemapi/xboe/m/course/manage/revoke", { id });
};
/**
* 复制课程
* @param {Object}
*/
const copyCourse = function (data) {
return ajax.post("/systemapi/xboe/m/course/manage/copy", data);
};
/*
查询课程是否有重复名称
*/
const isRedoName = function () {
return ajax.get("/systemapi/xboe/m/course/manage/isRedoName");
};
/*
查询当前添加课程是否已有
courseName 要添加的课程姓名
*/
const isCourseName = function (courseName, courseId) {
return ajax.get(
`/systemapi/xboe/m/course/manage/isCourseName?courseName=${courseName}&courseId=${courseId}`
);
};
/**
* 查询修改日志,列表,不分页
* @param {Object} params
* {
num:数量可以不传默认是10条最新的10条
courseId:课程的id
name: 修改人
}
*/
const findUpdateLogs = function (params) {
return ajax.post("/systemapi/xboe/m/course/manage/upldate-logs", params);
};
/**
* 根据id获取修改的详细信息
* @param {Object} id
*/
const getUpdateLog = function (id) {
return ajax.get(
"/systemapi/xboe/m/course/manage/upldate-log-detail?id=" + id
);
};
/**
* 保存课程的一条学习内容信息,新增和更新都是此方式
* @param {Object} data
* {
content内容 {
courseId,csectionId章节id(微课为空),sortIndex排序顺序微课为空,contentType 内容类型图文41连接52作业60考试61评估62
contentName,contentRefId无关联内容时为空此字段内容后台会控制,content具体的内容
}
homework作业信息 单个对象{
courseIdcontentId上面对象的id后台会控制,name,content,file,deadTime,
submitMode:1表提交附件2直接填写3表两者都可以
}
exam考试信息 单个对象
{
courseIdcontentId上面对象的id后台会控制,testName,testDuration考试时长,
paperType试卷类型 1自定义2使用已有试卷
paperId试题的id,使用已有试卷时保存选择试卷的id
showAnalysis是否显示解析showAnswer否显示答案times尝试次数
arrange试题排序 0表不乱序,1试题乱序2选项乱序3全部乱序
scoringType评分方式 1最高一次2最后一次
passLine及格线整数
randomMode是否随机模式true/false
paperContent试卷的内容json存储的对象{items:[]}
}
assess评估信息,list 多条记录.
[
{ courseIdcontentId,assessId评估id一期为空,question问题qType问题类型}
]
}
*/
const saveContent = function (data) {
return ajax.postJson("/systemapi/xboe/m/course/content/save", data);
};
/**
* 更新课程内容的顺序
* @param {String} cid //课程的id
* @param {Array} items
* [
* {
* id:章的id
* index:整数,顺序值
* items:[
* {id:内容的id,index:顺序值}
* ]
* }
* ]
* @returns
*/
const updateContentOrders = function (cid, items) {
return ajax.postJson(
"/systemapi/xboe/m/course/content/update-orders/" + cid,
items
);
};
/**
* 课程的详细信息
* @param {String} id
*/
const detail = function (id) {
return ajax.get("/systemapi/xboe/m/course/manage/detail?id=" + id);
};
const getDictIds = function (pid, type) {
return ajax.get(
`/systemapi/xboe/m/course/manage/getDictIds?pid=${pid}&type=${type}`
);
};
/**
* 更新内容的名称
* @param {Object} data
* {
id:'',
name:''
}
*/
const updateContentName = function (data) {
return ajax.post("/systemapi/xboe/m/course/content/update-name", data);
};
/**
* 删除一条学习内容
* @param {Object} data
* {
id: 内容的id,
ctype:对应内容的类型contentType
erasable:是否物理删除,此值是课程信息中系统带过来的字段,直接使用它就可以了
}
*/
const delContent = function (data) {
return ajax.post("/systemapi/xboe/m/course/content/delete", data);
};
/**
* 保存课程的章信息,新增和修改保存都是一个
* @param {Object} data
* courseId课程的id
* name章节名称
description章节说明
parentId 上级id。如果没有可以填“-1”字符串
orderIndex显示顺序顺序索引整数
*/
const saveSection = function (data) {
return ajax.post("/systemapi/xboe/m/course/content/save-section", data);
};
/**
* 删除章节目录,注意只有目录下没有学习内容时才允许删除
* @param {Object} data
*/
const delSection = function (id) {
return ajax.post("/systemapi/xboe/m/course/content/delete-section?id=" + id);
};
/**
* 根据课程学习内容的id。获取作业信息只有学习内容是作业时才会有信息
* @param {Object} ccid
*/
const getHomework = function (ccid) {
return ajax.post("/systemapi/xboe/m/course/content/homework?ccid=" + ccid);
};
/**
* 根据课程学习内容的id。获取考试信息只有学习内容是考试时才会有信息
* @param {Object} ccid
*/
const getExam = function (ccid) {
return ajax.post("/systemapi/xboe/m/course/content/exam?ccid=" + ccid);
};
/**
* 根据课程学习内容的id。获取评估信息评估内容可以获取
* @param {Object} ccid
*/
const getAssess = function (ccid) {
return ajax.post("/systemapi/xboe/m/course/content/assess?ccid=" + ccid);
};
/**
* 管理列表查询
* @param {Object} query
* pageIndex:第几页
* pageSize:每页多少条
* resOwner1:资源归属一级的id
resOwner2:资源归属二级的id
resOwner3:资源归属三级的id
types授课方式多个使用 - 分隔
scenes应用场景多个使用 - 分隔
publishtrue/false 是否发布,空值或不传就是全部
aid创建人 aid
sysCreateUser: 创建人姓名
keyword查询关键词
sysTypes系统的分类多级使用 - 分隔,注一期功能是分类的最后一级值,不支持多个的查询
orderField排序字段 id s
orderAsctrue/false 是否是正序,从小到大
status状态,多个使用 - 分隔 1代表待审核 5代表已审核 1 未提交 2 已提交 5 审核完成
type课程类型10微课21在线课(直播)20:在线课( 录播)30:面授课40:混合式,
name 课程名称
*/
const pageList = function (query) {
return ajax.post("/systemapi/xboe/m/course/manage/pagelist", query);
};
/**计算待审核课程*/
const countWaitAudit = function () {
return ajax.get("/systemapi/xboe/m/course/manage/wait-audit-num");
};
/**
* [已用courseAudit中的hrbpAuditList替换]
* 当前用户需要审核的课程列表
* @param {Object} query 同pageList
*/
const auditList = function (query) {
return ajax.post("/systemapi/xboe/m/course/manage/audit-pagelist", query);
};
/**
* 【已移到courseAudit中】
* 教师需要审核的课程列表
*/
const teacherAuditList = function (query) {
return ajax.post("/systemapi/xboe/m/course/audit/teacher-course", query);
};
/**
* 指定审核人,转审核人
* 点击“转审” 弹出教师查询窗口,查询教师,填写备注,提交,调用此接口
* @param {Object} data
* {courseId:课程id,teacherId:指定的审核人教师的id,teacherName:教师名称,remark:备注}
*/
const auditAppoint = function (data) {
return ajax.post("/systemapi/xboe/m/course/audit/appoint", data);
};
/**
* 获取审核信息,上面教师点击审核课程时,用于查询,上面“转审”时,用户填写的备注信息
* @param {courseId:'课程id',teacherId:'可以不填写,系统会查询当前人'} data
*/
const getAuditInfo = function (data) {
return ajax.post("/systemapi/xboe/m/course/audit/infos", data);
};
/**
* 管理员的课程审核处理
* @param {Object} query {id:课程id,title:课程的名称, Boolean pass 是否通过,remark 备注}
*/
const audit = function (data) {
return ajax.post("/systemapi/xboe/m/course/manage/audit", data);
};
/**
* 审核记录列表,分页查询
*/
const auditPageRecords = function (data) {
return ajax.post("/systemapi/xboe/m/course/audit/page-records", data);
};
/**
* 审核记录列表要卖课程id查询出审核列记录信息
* { courseId:必须}
*/
const auditCourseRecords = function (data) {
return ajax.post("/systemapi/xboe/m/course/audit/course-records", data);
};
/**
* 管理员的课程发布,当前已经不再使用了
* @param {Object} query {ids:课程id,多个使用逗号分隔,title:课程的名称, Boolean pass 是否发布}
*/
const publish = function (data) {
return ajax.post("/systemapi/xboe/m/course/manage/publish", data);
};
const auditAndPublish = function (data) {
return ajax.post("/systemapi/xboe/m/course/manage/audit-publish", data);
};
/**
* 设置top
* @param {Object} query {ids:课程id,多个使用逗号分隔,title:课程的名称,Boolean top 是否置顶}
*/
const setTop = function (data) {
return ajax.post("/systemapi/xboe/m/course/manage/top", data);
};
/**
* 管理员的设置启用停用
* @param {Object} query {ids:课程id,多个使用逗号分隔,title:课程的名称, Boolean enabled 是否启用}
*/
const setEnabled = function (data) {
return ajax.post("/systemapi/xboe/m/course/manage/enabled", data);
};
/**
* 管理员的删除课程
* erasable 此值是课程信息带过来的,直接传就可以
* @param {Object} query {id:课程id,多个使用逗号分隔,Boolean erasable 是否物理删除,title:课程的名称, remark 备注}
*/
const del = function (data) {
return ajax.post("/systemapi/xboe/m/course/manage/delete", data);
};
/*
详情
*/
const detailFew = function (id) {
return ajax.get("/systemapi/xboe/m/course/portal/detail-few?id=" + id);
};
/*
直接审核,教师提交审核
*/
const sumbits = function (data) {
return ajax.post("/systemapi/xboe/m/course/manage/sumbits", data);
};
/*
教师授课记录
*/
const teacherCourse = function (teacherId) {
return ajax.get(
"/systemapi/xboe/m/course/manage/teacher-course?teacherId=" + teacherId
);
};
/*
教师授课记录导出
@param teacherId 教师id
*/
const exportTeacherCourse = function (teacherId) {
return ajax.post(
"/systemapi/xboe/m/course/manage/export-teacher-course?teacherId=" +
teacherId
);
};
/*
*待审核课程记录导出
* resOwner1:资源归属一级的id
resOwner2:资源归属二级的id
resOwner3:资源归属三级的id
types授课方式多个使用 - 分隔
scenes应用场景多个使用 - 分隔
publishtrue/false 是否发布,空值或不传就是全部
aid创建人 aid
sysCreateUser: 创建人姓名
keyword查询关键词
sysTypes系统的分类多级使用 - 分隔,注一期功能是分类的最后一级值,不支持多个的查询
orderField排序字段 id s
orderAsctrue/false 是否是正序,从小到大
status状态,多个使用 - 分隔 1代表待审核 5代表已审核 1 未提交 2 已提交 5 审核完成
type课程类型10微课21在线课(直播)20:在线课( 录播)30:面授课40:混合式,
name 课程名称
*/
const exportCourseAudit = function (query) {
return ajax.post("/systemapi/xboe/m/course/manage/exportCourseAudit", query);
};
/*
参数同上待审核课程记录导出
课程的导出和已审核的课程导出
*/
const exportCourse = function (query) {
return ajax.post("/systemapi/xboe/m/course/manage/exportCourse", query);
};
//判断受众id是否有关联
const queryCrowd = function (query) {
return ajax.postJson("/systemapi/xboe/m/course/manage/queryCrowd", query);
};
/**
* 二次查询
* @param{
* ids
* }
* */
const ids = function (data) {
return ajax.postJson("/systemapi/xboe/m/course/manage/ids", data);
};
const saveTip = function () {
return ajax.postJson("/systemapi/xboe/m/course/manage/saveTip");
};
export default {
saveBase,
submitCourse,
revokeSubmit,
copyCourse,
findUpdateLogs,
getUpdateLog,
detail,
getDictIds,
saveContent,
pageList,
setEnabled,
del,
publish,
saveSection,
getHomework,
countWaitAudit,
auditList,
teacherAuditList,
auditAppoint,
getAuditInfo,
audit,
auditPageRecords,
auditCourseRecords,
auditAndPublish,
getAssess,
setTop,
delSection,
getExam,
delContent,
updateContentName,
updateContentOrders,
saveOnlyCourse,
isRedoName,
isCourseName,
detailFew,
sumbits,
teacherCourse,
exportTeacherCourse,
exportCourseAudit,
exportCourse,
queryCrowd,
ids,
saveTip,
};

View File

@@ -789,6 +789,9 @@ textarea {
margin-left: $i * 1px;
margin-right: $i * 1px;
}
.m#{$i} {
margin: $i * 1px;
}
.fs#{$i} {
font-size: $i * 1px;
}
@@ -812,7 +815,28 @@ textarea {
padding-left: $i * 1px;
padding-right: $i * 1px;
}
.p#{$i} {
padding: $i * 1px;
}
.w#{$i} {
width: $i * 1px;
}
}
.flex {
display: flex;
}
.align-center {
align-items: center;
}
.justify-content-c {
justify-content: center;
}
.justify-content-b {
justify-content: space-between;
}
.justify-content-e {
justify-content: flex-end;
}
.justify-content-s {
justify-content: flex-start;
}

View File

@@ -118,18 +118,41 @@ const columns = [
];
// 事件发射
const emit = defineEmits(["chooseItem", "choosePreviewItem"]);
const emit = defineEmits(["chooseItem", "choosePreviewItem", "chooseCusExam"]);
// 处理选择项目
const handleChooseItem = (row) => {
emit("chooseItem", {
switch (props.resType) {
case 61:
console.log(row);
if (row.counts === 0) {
ElMessage.error("此试卷无试题内容,请重新选择");
return;
}
break;
default:
// emit("chooseItem", {
// ...row,
// isDrag: false,
// completeSetup: 0,
// setupTage: "",
// resType: props.resType,
// dir: props.resType === 50 ? "scorm" : "course",
// });
break;
}
let obj = {
...row,
isDrag: false,
completeSetup: 0,
setupTage: "",
resType: props.resType,
dir: props.resType === 50 ? "scorm" : "course",
});
};
if (props.resType === 61) {
obj.paperType = 2;
}
emit("chooseItem", obj);
};
const handlePreviewItem = (row) => {
@@ -267,6 +290,14 @@ const handleBeforeUpload = (file) => {
return true;
};
const openCusExam = () => {
emit("chooseCusExam", {
resType: props.resType,
dir: props.resType === 50 ? "scorm" : "course",
paperType: 1,
name: "自定义考试",
});
};
// 生命周期
onMounted(() => {
@@ -292,7 +323,10 @@ onMounted(() => {
>上传新{{ getType(props.resType) }}</el-button
>
</el-upload>
<el-button v-if="[61].includes(props.resType)" type="primary"
<el-button
v-if="[61].includes(props.resType)"
type="primary"
@click="openCusExam"
>自定义考试</el-button
>
<span class="desc ml10" v-if="![61].includes(props.resType)"

View File

@@ -0,0 +1,417 @@
<template>
<div class="simple-paper">
<!-- 顶部操作栏 -->
<div class="toolbar">
<div class="toolbar-buttons">
<el-button-group>
<el-button
type="primary"
@click="addQuestion(101)"
:icon="Plus"
size="small"
:disabled="disabled"
>单选</el-button
>
<el-button
type="primary"
@click="addQuestion(102)"
:icon="Plus"
size="small"
:disabled="disabled"
>多选</el-button
>
<el-button
type="primary"
@click="addQuestion(103)"
:icon="Plus"
size="small"
:disabled="disabled"
>判断</el-button
>
</el-button-group>
</div>
<div class="toolbar-tip">点题干编辑</div>
<div class="toolbar-info">
<el-checkbox v-model="optShow" :disabled="disabled"
>显示选项</el-checkbox
>
<span class="toolbar-stats">
<span class="bigred"> {{ data.items.length }} </span>
<span class="bigred">{{ total }}</span>
</span>
</div>
</div>
<!-- 试卷内容 -->
<div class="paper-container">
<div
v-for="(item, idx) in data.items"
:key="idx"
class="paper-item"
@mouseenter="showOptions(item)"
@mouseleave="hideOptions(item)"
>
<div class="paper-item-content">
<div class="paper-item-main">
<!-- 显示模式 -->
<div
v-if="editIndex !== idx"
class="paper-item-question"
@click="handleEditItem(item, idx)"
>
{{ idx + 1 }}.
<span v-if="item.type === 101">单选题</span>
<span v-if="item.type === 102">多选题</span>
<span v-if="item.type === 103">判断题</span>
{{ item.content }}
</div>
<!-- 选项显示悬停或全局显示 -->
<div v-if="optShow || item.optShow" class="paper-options">
<div
v-for="(opt, optIdx) in item.options"
:key="optIdx"
class="paper-option"
:class="{ 'paper-option-selected': opt.answer }"
@click="setOptAnswer(item, opt)"
>
{{ String.fromCharCode(65 + optIdx) }}. {{ opt.content }}
<i v-if="opt.answer" class="el-icon-check option-checkmark"></i>
</div>
</div>
<!-- 编辑模式 -->
<div v-if="editIndex === idx" class="paper-item-editor">
<el-input v-model="item.content" placeholder="试题的内容" />
<div class="editor-tip">一行一个选项</div>
<el-input
v-model="curTextOptions"
type="textarea"
:rows="5"
placeholder="每行一个选项内容"
class="editor-textarea"
/>
<el-button
@click="handleSaveItem(item)"
type="warning"
size="small"
class="editor-save-btn"
>
编辑完成
</el-button>
</div>
</div>
<!-- 分数与删除 -->
<div class="paper-item-actions">
<el-input
:disabled="disabled"
v-model="item.score"
class="score-input"
size="small"
placeholder="分数"
/>
<el-button
:disabled="disabled"
@click="removeQuestion(idx)"
type="danger"
size="small"
:icon="Delete"
class="remove-btn"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import { Plus, Delete } from "@element-plus/icons-vue";
import { snowflakeGenerator } from "snowflake-id-js";
import { ElButtonGroup, ElButton, ElInput, ElCheckbox } from "element-plus";
// Props
const props = defineProps({
data: {
type: Object,
default: () => ({
items: [],
}),
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:data"]);
// 监听 data 变化并触发 update 事件
watch(
() => props.data,
(newVal) => {
emit("update:data", newVal);
},
{ deep: true }
);
// 响应式状态
const editIndex = ref(-1);
const curTextOptions = ref("");
const optShow = ref(true);
const generator = ref(null);
// 默认测试题
const qdata = [
{
id: "1",
type: 101,
score: 5,
checked: false,
optShow: false,
content: "点击编辑试题内容",
options: [
{ id: "11", content: "选项", answer: false },
{ id: "12", content: "选项", answer: true },
{ id: "13", content: "选项", answer: false },
{ id: "14", content: "选项", answer: false },
],
},
];
// 初始化
onMounted(() => {
const seed = Math.floor(Math.random() * 50) + 1;
generator.value = snowflakeGenerator(seed);
initItem();
});
// 总分计算
const total = computed(() => {
return props.data.items.reduce((sum, item) => {
return sum + (parseFloat(item.score) || 0);
}, 0);
});
// 初始化测试数据(仅当 items 为空)
function initItem() {
if (props.data.items.length === 0) {
qdata.forEach((item) => {
props.data.items.push({ ...item });
});
}
}
// 显示/隐藏选项
function showOptions(item) {
item.optShow = true;
}
function hideOptions(item) {
item.optShow = false;
}
// 设置答案(单选/多选逻辑)
function setOptAnswer(item, opt) {
if (props.disabled) return;
if (item.type !== 102) {
// 单选或判断:清空其他选项
item.options.forEach((o) => (o.answer = false));
}
// 切换当前选项
opt.answer = !opt.answer;
}
// 进入编辑
function handleEditItem(item, idx) {
if (props.disabled) return;
editIndex.value = idx;
// 拼接选项为文本(每行一个)
const text = item.options.map((opt) => opt.content).join("\n");
curTextOptions.value = text;
}
// 保存编辑
function handleSaveItem(item) {
if (props.disabled) return;
const lines = curTextOptions.value
.trim()
.split("\n")
.filter((line) => line.trim() !== "");
if (lines.length === 0) return;
// 保存当前选中的索引
const oldAnswers = item.options
.map((opt, i) => (opt.answer ? i : -1))
.filter((i) => i !== -1);
// 重建 options
const newOptions = lines.map((content, i) => {
const id = generator.value.next().value;
const isAnswer = oldAnswers.includes(i);
return { id, content, answer: isAnswer };
});
item.options = newOptions;
editIndex.value = -1;
}
// 添加题目
function addQuestion(type) {
const id = generator.value.next().value;
let options = [];
if (type === 101 || type === 102) {
// 单选/多选3个默认选项
options = Array.from({ length: 3 }, (_, i) => ({
id: generator.value.next().value,
content: "选项",
answer: false,
}));
} else if (type === 103) {
// 判断题
options = [
{ id: generator.value.next().value, content: "正确", answer: false },
{ id: generator.value.next().value, content: "错误", answer: false },
];
}
const newItem = {
id,
type,
score: 5,
optShow: false,
content: "点击编辑试题内容",
options,
};
props.data.items.push(newItem);
}
// 删除题目
function removeQuestion(index) {
props.data.items.splice(index, 1);
}
</script>
<style lang="scss" scoped>
.simple-paper {
.toolbar {
display: flex;
justify-content: space-between;
padding: 5px;
align-items: center;
.toolbar-buttons {
flex: 1;
}
.toolbar-tip {
flex: 1;
text-align: center;
padding-top: 10px;
color: #919191;
}
.toolbar-info {
flex: 1;
line-height: 40px;
text-align: right;
font-size: 16px;
.toolbar-stats {
margin-left: 8px;
}
}
}
.paper-container {
border: 1px solid #dfdfdf;
padding: 5px 0;
//height: 500px;
//overflow: auto;
.paper-item {
padding: 5px 10px;
border-bottom: 1px solid #cccccc;
.paper-item-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 5px;
.paper-item-main {
flex: 1;
padding-right: 5px;
.paper-item-question {
cursor: pointer;
margin-bottom: 5px;
}
.paper-options {
padding: 5px 0;
.paper-option {
line-height: 25px;
cursor: pointer;
padding: 2px 5px;
&:hover {
background-color: #f5f5f5;
}
}
.paper-option-selected {
color: green;
font-weight: bold;
}
.option-checkmark {
float: right;
color: green;
}
}
.paper-item-editor {
.editor-tip {
color: red;
margin-top: 4px;
}
.editor-textarea {
margin-top: 4px;
}
.editor-save-btn {
margin-top: 8px;
}
}
}
.paper-item-actions {
width: 110px;
display: flex;
align-items: center;
.score-input {
width: 50px;
}
.remove-btn {
margin-left: 6px;
}
}
}
}
}
}
.bigred {
color: red;
font-size: 20px;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,214 @@
<script setup>
import {
ElForm,
ElFormItem,
ElInput,
ElRadioGroup,
ElRadio,
ElCheckbox,
ElInputNumber,
} from "element-plus";
import { onMounted, ref } from "vue";
import apiCourse from "@/api/modules/course";
import apiExamPaper from "@/api/modules/paper";
import SimplePaper from "./ChildrenComponent/SimplePaper.vue";
defineOptions({
resType: 61,
});
const props = defineProps({
dialogVideoForm: {
type: Object,
default: () => ({
name: "",
filePath: "",
isDrag: true,
completeSetup: 0,
setupTage: 0,
openType: "",
}),
},
isPreview: {
type: Boolean,
default: false,
},
classId: {
type: String,
default: "",
},
});
import { useMediaComponent } from "@/hooks/useMediaComponent";
// Emit updates to parent component
const emit = defineEmits(["update:dialogVideoForm"]);
// 使用hook处理公共逻辑
const { localDialogVideoForm, updateFormValue, fileBaseUrl } =
useMediaComponent(props, emit);
const loadExamFile = () => {
// 查询课程详情,在查询课程考试内
// console.log(props, "props");
// apiCourse.getExam(props.classId).then((rs) => {
// console.log(rs);
// });
// apiExamPaper.detail(localDialogVideoForm.value.id).then((res) => {
// if (res.status === 200) {
// console.log(res);
// localDialogVideoForm.value.counts = res.result.counts;
// localDialogVideoForm.value.name = res.result.testName;
// localDialogVideoForm.value.paperId = res.result.id;
// }
// });
};
onMounted(() => {
console.log(localDialogVideoForm.value, 123);
if (localDialogVideoForm.value.paperType === 1) {
if (!localDialogVideoForm.value.id) {
localDialogVideoForm.value = Object.assign(localDialogVideoForm.value, {
testDuration: localDialogVideoForm.value.testDuration || 30,
passLine: localDialogVideoForm.value.passLine || 60,
scoringType: localDialogVideoForm.value.scoringType || 1,
percentScore: localDialogVideoForm.value.percentScore || false,
randomMode: localDialogVideoForm.value.randomMode || false,
qnum: localDialogVideoForm.value.qnum || 1,
counts: localDialogVideoForm.value.counts || 1,
paperJson: localDialogVideoForm.value.paperJson || {
items: [],
},
});
}
} else {
// 选择的试卷 还没做
loadExamFile();
}
});
</script>
<template>
<el-form label-position="right" label-width="100px">
<el-form-item label="考试名称">
<el-input
v-model="localDialogVideoForm.name"
:disabled="isPreview"
@update:modelValue="(val) => updateFormValue('name', val)"
></el-input>
</el-form-item>
<el-form-item label="考试时长">
<el-input
v-model="localDialogVideoForm.testDuration"
:disabled="isPreview"
@update:modelValue="(val) => updateFormValue('testDuration', val)"
>
<template #append>分钟</template>
</el-input>
</el-form-item>
<el-form-item label="及格线">
<el-input
v-model="localDialogVideoForm.passLine"
:disabled="isPreview"
@update:modelValue="(val) => updateFormValue('passLine', val)"
></el-input>
</el-form-item>
<el-form-item label="打开方式">
<el-radio-group
v-model="localDialogVideoForm.scoringType"
@update:modelValue="(val) => updateFormValue('scoringType', val)"
:disabled="isPreview"
>
<el-radio :value="1" :label="1">最高一次</el-radio>
<el-radio :value="2" :label="2">最后一次</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="百分制">
<el-checkbox
:label="true"
:value="true"
v-model="localDialogVideoForm.percentScore"
:disabled="isPreview"
@update:modelValue="(val) => updateFormValue('percentScore', val)"
>实际成绩*100/实际总分</el-checkbox
>
</el-form-item>
<el-form-item label="考试说明">
<el-input
v-model="localDialogVideoForm.info"
type="textarea"
:disabled="isPreview"
@update:modelValue="(val) => updateFormValue('info', val)"
></el-input>
</el-form-item>
<el-form-item label="随机模式">
<el-radio-group
:disabled="isPreview"
v-model="localDialogVideoForm.randomMode"
@update:modelValue="(val) => updateFormValue('randomMode', val)"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="数量"
v-if="
localDialogVideoForm.randomMode && localDialogVideoForm.paperType === 1
"
>
<div class="flex align-center">
<el-input-number
:disabled="isPreview"
v-model="localDialogVideoForm.qnum"
@update:modelValue="(val) => updateFormValue('qnum', val)"
:min="1"
:max="
localDialogVideoForm.paperType === 1
? localDialogVideoForm.paperJson.items.length
: localDialogVideoForm.counts
"
>
</el-input-number>
<span
style="margin-left: 10px"
v-if="
localDialogVideoForm.paperType === 1
? localDialogVideoForm.paperJson.items.length <= 0
: localDialogVideoForm.counts <= 0
"
>先{{
localDialogVideoForm.paperType === 1 ? "添加试题" : "选择试卷"
}}</span
>
<span
style="margin-left: 10px"
v-if="
localDialogVideoForm.paperType === 1
? localDialogVideoForm.paperJson.items.length > 0
: localDialogVideoForm.counts > 0
"
>试卷有
{{
localDialogVideoForm.paperType === 1
? localDialogVideoForm.paperJson.items.length
: localDialogVideoForm.counts
}}
道试题</span
>
</div>
</el-form-item>
<div class="parer-comp" v-if="localDialogVideoForm.paperType === 1">
<SimplePaper
v-model:data="localDialogVideoForm.paperJson"
:disabled="isPreview"
@update:data="(val) => updateFormValue('paperJson', val)"
></SimplePaper>
</div>
<!-- <div v-if="localDialogVideoForm.paperType === 2">-->
<!-- <div class="flex align-center">-->
<!-- <span style="padding-right: 12px; width: 100px; text-align: right"-->
<!-- >试卷</span-->
<!-- >-->
<!-- {{ localDialogVideoForm.name }}-->
<!-- </div>-->
<!-- </div>-->
</el-form>
</template>
<style scoped lang="scss"></style>

View File

@@ -251,6 +251,7 @@ export function useCourseData() {
const courseList = ref([
{
title: "课程1",
id: "1441803797035380736",
data: [],
},
{

View File

@@ -2,7 +2,7 @@
import dragCollapse from "./dragCollapse.vue";
import { ElButton, ElCheckbox, ElDialog, ElMessageBox } from "element-plus";
import dragTable from "./dragTable.vue";
import { ref, reactive } from "vue";
import { ref, watch } from "vue";
defineOptions({
name: "CreateCourse",
});
@@ -14,6 +14,7 @@ import EditorComp from "@/components/CreatedCourse/preview/EditorComp.vue";
import DocComp from "@/components/CreatedCourse/preview/DocComp.vue";
import LinkComp from "@/components/CreatedCourse/preview/LinkComp.vue";
import ScormComp from "@/components/CreatedCourse/preview/ScormComp.vue";
import PaperComp from "@/components/CreatedCourse/preview/PaperComp.vue";
import { getType } from "@/hooks/useCreateCourseMaps";
const mapComponents = [
VideoComp,
@@ -22,6 +23,7 @@ const mapComponents = [
DocComp,
LinkComp,
ScormComp,
PaperComp,
];
// 使用课程数据hook
@@ -30,10 +32,24 @@ const { courseMetadata, courseList, courseActionButtons, addChapter } =
const isPreview = ref(false);
const chooseItemData = ref({});
const showSettingDialog = ref(false);
// 添加操作的时候 弹窗是否弹出对应类型表单
const isNext = ref(true);
const showTablePreview = ref(false);
// 定义表格列
const showDialog = ref(false);
const classId = ref("");
watch(
() => courseMetadata.chooseIndex,
(newVal) => {
console.log(newVal);
console.log(courseList.value[newVal]);
classId.value = courseList.value[newVal].id;
console.log(classId.value);
}
);
// 课程操作映射
const courseOperations = {
addVideo: () => {
@@ -67,6 +83,7 @@ const courseOperations = {
addExam: () => {
courseMetadata.resType = 61;
showDialog.value = true;
isNext.value = false;
},
addHomework: () => {
console.log("添加作业功能调用");
@@ -104,6 +121,8 @@ const choosePreviewItem = (data) => {
};
// 保存
const saveContent = () => {
console.log(chooseItemData.value);
if (courseMetadata.selectionIndex !== null) {
courseList.value[courseMetadata.chooseIndex].data[
courseMetadata.selectionIndex
@@ -116,6 +135,8 @@ const saveContent = () => {
}
showDialog.value = false;
showSettingDialog.value = false;
// 可以调用保存方法 保存考试
};
const deleteRow = (data) => {
courseMetadata.chooseIndex = data.index;
@@ -145,6 +166,12 @@ const previewRow = (data) => {
isPreview.value = true;
showSettingDialog.value = true;
};
// 自定义考试
const chooseCusExam = (data) => {
chooseItemData.value = data;
showSettingDialog.value = true;
};
</script>
<template>
@@ -201,6 +228,7 @@ const previewRow = (data) => {
@choosePreviewItem="choosePreviewItem"
:resType="courseMetadata.resType"
:showTablePreview="showTablePreview"
@chooseCusExam="chooseCusExam"
></chooseFileList>
</el-dialog>
@@ -209,15 +237,19 @@ const previewRow = (data) => {
v-model="showSettingDialog"
:title="isPreview ? '预览' : getType(chooseItemData.resType)"
>
<div v-for="item in mapComponents">
<component
v-if="
Number(chooseItemData.resType) === item.resType && showSettingDialog
"
:is="item"
v-model:dialogVideoForm="chooseItemData"
:isPreview="isPreview"
></component>
<div style="max-height: 600px; overflow: auto">
<template v-for="item in mapComponents">
<component
v-if="
Number(chooseItemData.resType) === item.resType &&
showSettingDialog
"
:is="item"
v-model:dialogVideoForm="chooseItemData"
:isPreview="isPreview"
:classId="classId"
></component>
</template>
</div>
<template #footer>