feat(course): 实现课程标签管理功能

- 新增课程标签API模块,支持标签分页查询、创建、修改状态等操作
- 开发课程标签组件,支持标签搜索、创建、删除和数量限制
- 集成标签组件到专业模式页面,替换原有标签选择器
- 优化课程创建组件,重构表单状态管理和操作流程
- 升级Element Plus组件版本,支持el-select-v2等新组件
- 添加lodash依赖用于防抖搜索功能
- 调整样式和布局,优化标签显示和交互体验
This commit is contained in:
陈昱达
2025-11-26 19:00:06 +08:00
parent 2158c7f0f1
commit 8a20689aeb
11 changed files with 1019 additions and 230 deletions

View File

@@ -4,9 +4,7 @@ import { ElButton, ElCheckbox, ElDialog, ElMessageBox } from "element-plus";
import { $message } from "@/utils/useMessage";
import dragTable from "./dragTable.vue";
import { ref, watch } from "vue";
defineOptions({
name: "CreateCourse",
});
import { getType } from "@/hooks/useCreateCourseMaps";
import { useCourseData } from "@/hooks/useCourseData";
import chooseFileList from "@/components/CreatedCourse/chooseFileList.vue";
import VideoComp from "@/components/CreatedCourse/preview/VideoComp.vue";
@@ -18,7 +16,10 @@ import ScormComp from "@/components/CreatedCourse/preview/ScormComp.vue";
import PaperComp from "@/components/CreatedCourse/preview/PaperComp.vue";
import HomeWorkComp from "@/components/CreatedCourse/preview/HomeWorkComp.vue";
import AccessComp from "@/components/CreatedCourse/preview/AccessComp.vue";
import { getType } from "@/hooks/useCreateCourseMaps";
defineOptions({
name: "CreateCourse",
});
// 组件映射
const mapComponents = [
VideoComp,
AudioComp,
@@ -34,22 +35,16 @@ const mapComponents = [
// 使用课程数据hook
const { courseMetadata, courseList, courseActionButtons, addChapter } =
useCourseData();
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("");
const copyChooseItemData = ref({});
// 监听课程索引变化更新classId
watch(
() => courseMetadata.chooseIndex,
(newVal) => {
classId.value = courseList.value[newVal].id;
if (courseList.value[newVal]) {
courseMetadata.classId = courseList.value[newVal].id || "";
}
}
);
@@ -57,46 +52,46 @@ watch(
const courseOperations = {
addVideo: () => {
courseMetadata.resType = 10;
showDialog.value = true;
courseMetadata.showDialog = true;
},
addAudio: () => {
courseMetadata.resType = 20;
showDialog.value = true;
courseMetadata.showDialog = true;
},
addDocument: () => {
courseMetadata.resType = 40;
showDialog.value = true;
isNext.value = false;
courseMetadata.showDialog = true;
courseMetadata.isNext = false;
},
addImageText: () => {
courseMetadata.resType = 41;
chooseItemData.value.resType = 41;
showSettingDialog.value = true;
courseMetadata.showSettingDialog = true;
},
addExternalLink: () => {
courseMetadata.resType = 52;
chooseItemData.value.resType = 52;
showSettingDialog.value = true;
courseMetadata.showSettingDialog = true;
},
addScorm: () => {
courseMetadata.resType = 50;
showDialog.value = true;
isNext.value = false;
courseMetadata.showDialog = true;
courseMetadata.isNext = false;
},
addExam: () => {
courseMetadata.resType = 61;
showDialog.value = true;
isNext.value = false;
courseMetadata.showDialog = true;
courseMetadata.isNext = false;
},
addHomework: () => {
courseMetadata.resType = 60;
chooseItemData.value.resType = 60;
showSettingDialog.value = true;
courseMetadata.showSettingDialog = true;
},
addAssessment: () => {
courseMetadata.resType = 62;
chooseItemData.value.resType = 62;
showSettingDialog.value = true;
courseMetadata.showSettingDialog = true;
},
};
@@ -105,9 +100,10 @@ const executeCourseOperation = (operationName, data) => {
courseMetadata.chooseIndex = data;
courseMetadata.selectionIndex = null;
copyChooseItemData.value = {};
isPreview.value = false;
isNext.value = true;
courseMetadata.isPreview = false;
courseMetadata.isNext = true;
chooseItemData.value = {};
if (courseOperations[operationName]) {
courseOperations[operationName](data);
} else {
@@ -115,92 +111,130 @@ const executeCourseOperation = (operationName, data) => {
}
};
// 选择项目处理
const chooseItem = (data) => {
console.log(data);
chooseItemData.value = data;
if (!isNext.value) {
// 如果不需要下一步,则直接保存
if (!courseMetadata.isNext) {
saveContent();
return;
}
showSettingDialog.value = true;
courseMetadata.showSettingDialog = true;
};
// 预览项目处理
const choosePreviewItem = (data) => {
chooseItemData.value = data;
showSettingDialog.value = true;
isPreview.value = true;
courseMetadata.showSettingDialog = true;
courseMetadata.isPreview = true;
};
// 取消保存
const cancelSave = () => {
showSettingDialog.value = false;
courseMetadata.showSettingDialog = false;
// 恢复原始数据
chooseItemData.value = copyChooseItemData.value;
courseList.value[courseMetadata.chooseIndex].data[
courseMetadata.selectionIndex
] = chooseItemData.value;
console.log(chooseItemData);
};
// 保存
const saveContent = () => {
console.log(chooseItemData.value);
if (courseMetadata.selectionIndex !== null) {
if (
courseMetadata.chooseIndex !== null &&
courseMetadata.selectionIndex !== null &&
courseList.value[courseMetadata.chooseIndex] &&
courseList.value[courseMetadata.chooseIndex].data[
courseMetadata.selectionIndex
]
) {
courseList.value[courseMetadata.chooseIndex].data[
courseMetadata.selectionIndex
] = chooseItemData.value;
} else {
courseList.value[courseMetadata.chooseIndex].data.push({
resType: courseMetadata.resType,
...chooseItemData.value,
});
}
showDialog.value = false;
showSettingDialog.value = false;
// 可以调用保存方法 保存考试
};
// 保存内容
const saveContent = () => {
if (courseMetadata.selectionIndex !== null) {
// 更新已有项
if (
courseList.value[courseMetadata.chooseIndex] &&
courseList.value[courseMetadata.chooseIndex].data[
courseMetadata.selectionIndex
]
) {
courseList.value[courseMetadata.chooseIndex].data[
courseMetadata.selectionIndex
] = {
...courseList.value[courseMetadata.chooseIndex].data[
courseMetadata.selectionIndex
],
...chooseItemData.value,
};
}
} else {
// 添加新项
if (courseList.value[courseMetadata.chooseIndex]) {
courseList.value[courseMetadata.chooseIndex].data.push({
resType: courseMetadata.resType,
...chooseItemData.value,
});
}
}
courseMetadata.showDialog = false;
courseMetadata.showSettingDialog = false;
};
// 删除行
const deleteRow = (data) => {
courseMetadata.chooseIndex = data.index;
courseMetadata.selectionIndex = data.selectionIndex;
ElMessageBox.confirm(`确定删除${data.record.name}吗?`, "删除确认", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "error",
}).then(() => {
courseList.value[courseMetadata.chooseIndex].data.splice(
courseMetadata.selectionIndex,
1
);
});
})
.then(() => {
if (
courseList.value[courseMetadata.chooseIndex] &&
courseList.value[courseMetadata.chooseIndex].data
) {
courseList.value[courseMetadata.chooseIndex].data.splice(
courseMetadata.selectionIndex,
1
);
}
})
.catch(() => {
// 用户取消删除
});
};
// 设置行
const settingRow = (data) => {
courseMetadata.chooseIndex = data.index;
courseMetadata.selectionIndex = data.selectionIndex;
chooseItemData.value = data.record;
chooseItemData.value = { ...data.record }; // 创建副本避免直接引用
copyChooseItemData.value = JSON.parse(JSON.stringify(data.record));
isPreview.value = false;
showSettingDialog.value = true;
courseMetadata.isPreview = false;
courseMetadata.showSettingDialog = true;
};
// 预览行
const previewRow = (data) => {
courseMetadata.chooseIndex = data.index;
courseMetadata.selectionIndex = data.selectionIndex;
chooseItemData.value = data.record;
chooseItemData.value = { ...data.record }; // 创建副本避免直接引用
copyChooseItemData.value = JSON.parse(JSON.stringify(data.record));
isPreview.value = true;
showSettingDialog.value = true;
courseMetadata.isPreview = true;
courseMetadata.showSettingDialog = true;
};
// 自定义考试
const chooseCusExam = (data) => {
chooseItemData.value = data;
showSettingDialog.value = true;
courseMetadata.showSettingDialog = true;
};
// 下一步处理
const handleNext = () => {
console.log($message);
$message.success("213");
};
</script>
@@ -212,7 +246,7 @@ const handleNext = () => {
<span>创建时间{{ courseMetadata.createTime }}</span>
</div>
<div class="course-content">
<div style="padding: 10px">
<div class="course-actions">
<el-button @click="addChapter">添加章</el-button>
<el-checkbox style="margin-left: 10px">顺序学习</el-checkbox>
</div>
@@ -220,73 +254,85 @@ const handleNext = () => {
<div>
<dragCollapse v-model:courseList="courseList">
<template #title="{ course }">{{ course.title }}</template>
<template #desc="{ course }"
>若课程只有一个章节将不在学员端显示该章节名称</template
>
<template #desc="{ course }">
若课程只有一个章节将不在学员端显示该章节名称
</template>
<template #default="{ course, index }">
<div class="drag-course-btn-content">
<el-button
v-for="btn in courseActionButtons"
:key="btn.fun"
type="primary"
class="btn-item"
plain
@click="executeCourseOperation(btn.fun, index)"
>{{ btn.label }}</el-button
>
{{ btn.label }}
</el-button>
</div>
<div>
<!-- 修改添加 groupId tableId 属性以支持跨表格拖拽 -->
<!-- 添加 groupId tableId 属性以支持跨表格拖拽 -->
<dragTable
:data="course.data"
:group-id="'course-chapters'"
group-id="course-chapters"
:table-id="'chapter-' + index"
:index="index"
@delete="deleteRow"
@setting="settingRow"
@preview="previewRow"
></dragTable>
/>
</div>
</template>
</dragCollapse>
</div>
</div>
<!-- 选择文件列表-->
<el-dialog v-model="showDialog" :title="getType(courseMetadata.resType)">
<!-- 选择文件列表 -->
<el-dialog
v-model="courseMetadata.showDialog"
:title="getType(courseMetadata.resType)"
>
<chooseFileList
v-if="showDialog"
v-if="courseMetadata.showDialog"
@chooseItem="chooseItem"
@choosePreviewItem="choosePreviewItem"
:resType="courseMetadata.resType"
:showTablePreview="showTablePreview"
:showTablePreview="courseMetadata.showTablePreview"
@chooseCusExam="chooseCusExam"
></chooseFileList>
/>
</el-dialog>
<!-- 设置预览弹窗 -->
<!-- 设置预览弹窗 -->
<el-dialog
v-model="showSettingDialog"
:title="isPreview ? '预览' : getType(chooseItemData.resType)"
v-model="courseMetadata.showSettingDialog"
:title="
courseMetadata.isPreview ? '预览' : getType(chooseItemData.resType)
"
:fullscreen="chooseItemData.resType === 41"
>
<div style="max-height: 600px; overflow: auto">
<template v-for="item in mapComponents">
<div class="component-preview">
<template v-for="item in mapComponents" :key="item.name">
<component
v-if="
Number(chooseItemData.resType) === item.resType &&
showSettingDialog
courseMetadata.showSettingDialog
"
:is="item"
v-model:dialogVideoForm="chooseItemData"
:isPreview="isPreview"
:classId="classId"
></component>
:isPreview="courseMetadata.isPreview"
:classId="courseMetadata.classId"
/>
</template>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelSave()">取消</el-button>
<el-button type="primary" @click="saveContent()" v-if="!isPreview">
<el-button
type="primary"
@click="saveContent()"
v-if="!courseMetadata.isPreview"
>
保存
</el-button>
</div>
@@ -305,20 +351,32 @@ const handleNext = () => {
.create-course {
width: 100%;
padding: 10px 20px;
.course-header {
display: flex;
justify-content: space-between;
.title {
font-size: 16px;
font-weight: 600;
}
}
.course-actions {
padding: 10px;
}
.drag-course-btn-content {
padding: 0 10px;
.btn-item + .btn-item {
margin-left: 10px;
}
}
.component-preview {
max-height: 600px;
overflow: auto;
}
}
</style>