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

@@ -26,6 +26,7 @@
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jquery": "^3.6.1", "jquery": "^3.6.1",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"pdf-vue3": "^1.0.12", "pdf-vue3": "^1.0.12",

View File

@@ -0,0 +1,64 @@
/**课程标签模块的相关处理*/
import ajax from "./xajax.js";
/**
* 分页查询:标签列表
* @param {Object} query
*/
const portalPageList = function (query) {
return ajax.post("/systemapi/xboe/m/coursetag/page", query);
};
//改变标签的公共属性
const changeTagPublic = function (row) {
// 返回 Promise 的 API 调用
return ajax.post("/systemapi/xboe/m/coursetag/changePublicStatus", {
id: row.id,
isPublic: row.isPublic,
});
};
//改变标签的热点属性
const changeTagHot = function (row) {
// 返回 Promise 的 API 调用
return ajax.post("/systemapi/xboe/m/coursetag/changeHotStatus", {
id: row.id,
isHot: row.isHot,
});
};
//查询指定id的标签关联的所有课程
const showCourseByTag = function (query) {
return ajax.post("/systemapi/xboe/m/coursetag/showCourseByTag", query);
};
//解除指定id的课程和某个标签之间的关联关系
const unbindCourseTagRelation = function (params) {
return ajax.post("/systemapi/xboe/m/coursetag/unbind", params);
};
//编辑课程:标签模糊查询
const searchTags = function (params) {
return ajax.post("/systemapi/xboe/m/coursetag/searchTags", params);
};
//编辑课程:创建标签(与当前课程关联)
const createTag = function (params) {
return ajax.post("/systemapi/xboe/m/coursetag/createTag", params);
};
//获取最新前10个热点标签
const getHotTagList = function (params) {
return ajax.post("/systemapi/xboe/m/coursetag/getHotTagList", params);
};
export default {
portalPageList,
changeTagPublic,
changeTagHot,
showCourseByTag,
unbindCourseTagRelation,
searchTags,
createTag,
getHotTagList,
};

View File

@@ -0,0 +1,121 @@
/**对应用户中心新的接口*/
import ajax from "./xajax";
//const baseURL = process.env.VUE_APP_CESOURCE_BASE_API;
const baseURL = "/userbasic";
/**【未使用】用于本地测试*/
const login = function () {
return ajax.post(baseURL + "/org/userParentOrg", {});
};
/** 2023年6月新增加退出接口*/
const logout = function () {
return ajax.postJson(baseURL + "/logout", { from: "pc" });
};
/**
* 【此接口已经不再使用】获取用户的组织机构
* organization_id
*/
const userParentOrg = function () {
return ajax.post(baseURL + "/org/userParentOrg", {});
};
/**
* /userbasic/org/list
* 根据关键字查询机构
*/
const findOrgsByKeyword = function (keyword) {
console.log(12312);
return ajax.postJson(baseURL + "/org/list", { keyword });
};
/**
* 【此接口已经不再使用】
*/
const findOrgTreeByOrgId = function (orgId) {
return ajax.postJson(baseURL + "/org/childOrgs", { orgId });
};
/** 获取机构信息 */
const getOrgInfo = function (orgId) {
return ajax.postJson(baseURL + "/org/info", { orgId });
};
/**【已接口已经不再使用】根据用户id获取用户的信息*/
const getUserInfoById = function (id) {
return ajax.postJson(baseURL + "/user/list", { id });
};
/**
* https://u-pre.boe.com/userbasic/audience/userAudiences
* 【当前代码中未查询到】获取当前用户受众信息
*/
const getUserCrowds = function () {
return ajax.postJson(baseURL + "/audience/userAudiences", {});
};
/**
* 获取用户过滤后的受众,只是查询已发布的
* {"page":1,pageSize:100,"keyword":""}
*/
const getUserAudiences = function (data) {
return ajax.postJson(baseURL + "/audience/userAudiencesFilter", data);
};
/**
* 重要接口获取hrbp数据课程审核。
* 此接口中的问题返回的机构名称namePath要是orgId的邮件中体现
*/
const getOrgHrbpInfo = function (orgId) {
return ajax.postJson(baseURL + "/org/orgHrbpInfo", { orgId });
};
/**
* 修改密码已转化为userbasic接口
* {newPassword:'',oldPassword:''}
*/
const modifyPassword = function (data) {
return ajax.postJson(baseURL + "/user/resetPassword", data);
};
/**获取加入的受众的id集合*/
const getInAudienceIds = function () {
return ajax.post(baseURL + "/audience/audienceByUser", {});
};
/**
* 2023年6月新增加
* 更新用户信息,当前只是列新三个信息,根据aid来更新
* aid
* avatar
* sign
*/
const updateUser = function (data) {
return ajax.postJson(baseURL + "/user/updateUserMessage", data);
};
/**
* 2023年6月新增加
* 根据用户的id集合获取用户的姓名工号头像组织机构签名等信息
* ids: 用户的id数组集合
*/
const getUsersByIds = function (ids) {
return ajax.postJson(baseURL + "/user/getUserMessageToDai", ids);
};
export default {
userParentOrg,
findOrgsByKeyword,
getOrgInfo,
findOrgTreeByOrgId,
getUserInfoById,
getUserCrowds,
getUserAudiences,
getOrgHrbpInfo,
modifyPassword,
getInAudienceIds,
getUsersByIds,
updateUser,
logout,
};

View File

@@ -737,6 +737,7 @@ textarea {
} }
.el-select, .el-select,
.el-select-v2,
.el-cascader { .el-cascader {
width: 100%; width: 100%;
} }

View File

@@ -30,6 +30,13 @@ export function useCourseData() {
sectionIndex: "", sectionIndex: "",
resType: 0, resType: 0,
selectionIndex: null, selectionIndex: null,
// 添加课程操作相关状态
isPreview: false,
showSettingDialog: false,
isNext: true, // 添加操作的时候 弹窗是否弹出对应类型表单
showTablePreview: false,
showDialog: false,
classId: "",
}); });
// 课程列表数据 // 课程列表数据

View File

@@ -5,46 +5,44 @@ import { reactive, ref } from "vue";
* @returns * @returns
*/ */
export function useCourseForm() { export function useCourseForm() {
// 表单相关
const formRef = ref(); const formRef = ref();
// 表单相关
const formState = reactive({ const formState = reactive({
courseName: "", // 课程名称 name: "",
courseCategory: [], // 课程分类 device: 3,
resourceBelong: undefined, // 资源归属 crowds: [],
lecturer: undefined, // 授课教师 courseTags: [],
targetGroup: "", // 目标人群 courseCategory: [],
courseTags: [], // 课程标签 orgName: "",
audience: undefined, // 受众 forUsers: "",
visibility: "Apple", // 可见性 lecturer: [],
coverIntro: "", // 封面介绍 coverImg: "",
courseValue: "", // 课程价值 courseValue: "",
courseIntro: "", // 课程简介 summary: "",
}); });
// 可见性选项 // 可见性选项
const visibilityOptions = [ const visibilityOptions = [
{ label: "PC端可见", value: "Apple" }, { label: "PC端可见", value: 1 },
{ label: "移动端可见", value: "Pear" }, { label: "移动端可见", value: 2 },
{ label: "多端可见", value: "Orange", disabled: false }, { label: "多端可见", value: 3 },
]; ];
// 表单重置 // 表单重置
const resetForm = (courseCoverurl, fileList) => { const resetForm = (fileList) => {
if (formRef.value) { if (formRef.value) {
formRef.value.resetFields(); formRef.value.resetFields();
} }
if (courseCoverurl) {
courseCoverurl.value = "";
}
if (fileList) { if (fileList) {
fileList.value = []; fileList.value = [];
} }
}; };
return { return {
formRef,
formState, formState,
visibilityOptions, visibilityOptions,
resetForm resetForm,
formRef,
}; };
} }

View File

@@ -0,0 +1,111 @@
import { ref, onMounted } from "vue";
import { getTeacherList } from "@/api/Lecturer";
import { getClassTree } from "@/api/modules/newApi";
import apiUserBasic from "@/api/modules/userbasic";
export function useFetchCourseList() {
const teachersList = ref([]);
const loading = ref(false);
// 分类列表
const sysTypeListMap = ref([]);
const sysTypeList = ref([]);
const courseTags = ref([]);
const curCourseId = ref("");
const orgList = ref([]);
const userGroupList = ref([]);
const fetchClassTree = async (data) => {
try {
const res = await getClassTree(data);
sysTypeListMap.value = res.result;
} catch (error) {
sysTypeListMap.value = [];
}
};
// 获取教师列表
const fetchTeacherList = async (data) => {
try {
loading.value = true;
const res = await getTeacherList({});
teachersList.value = res.result?.records || [];
} catch (error) {
console.error("获取讲师列表失败:", error);
teachersList.value = [];
} finally {
loading.value = false;
}
};
// 资源归属懒加载
const loadOrgNode = async (node, resolve) => {
try {
// 根节点level 0返回虚拟根
// if (node.level === 0) {
// resolve([{ name: "组织机构树", id: "-1" }]);
// return;
// }
// 一级节点level 1加载所有顶级组织
if (node.level === 0) {
const res = await apiUserBasic.findOrgsByKeyword("");
const treeList = (res.result || []).map((item) => ({
id: item.id,
name: item.name,
hrbpId: item.hrbpId,
children: [], // 假设有子节点(可根据后端字段优化)
}));
resolve(treeList);
return;
}
// 更深层级:根据 parentId 加载直接子组织
const parentId = node.data.id;
const res = await apiUserBasic.getOrgInfo(parentId);
if (res.status === 200 && Array.isArray(res.result?.directChildList)) {
const treeList = res.result.directChildList.map((item) => ({
id: item.id,
name: item.name,
hrbpId: item.hrbpId,
children: [], // 或根据 item.hasChildren / item.childCount > 0 动态设置
}));
resolve(treeList);
} else {
resolve([]); // 无子节点
}
} catch (error) {
console.error("加载组织树失败:", error);
// 出错时返回空数组,避免树组件卡住或报错
resolve([]);
}
};
// 受众列表
const getUserGroupList = async (data) => {
userGroupList.value = [];
try {
const res = await apiUserBasic.getUserAudiences(data);
res.result.list.forEach((item) => {
userGroupList.value.push({
id: item.id,
name: item.audienceName,
disabled: false,
});
});
} catch (error) {
console.error("获取用户组列表失败:", error);
userGroupList.value = [];
}
};
return {
fetchTeacherList,
fetchClassTree,
loadOrgNode,
getUserGroupList,
teachersList,
sysTypeListMap,
courseTags,
orgList,
curCourseId,
userGroupList,
sysTypeList,
};
}

View File

@@ -21,10 +21,12 @@ export function useMediaComponent(props, emit) {
// Update form values and emit changes // Update form values and emit changes
const updateFormValue = (field, value) => { const updateFormValue = (field, value) => {
localDialogVideoForm.value[field] = value; localDialogVideoForm.value[field] = value;
emit("update:dialogVideoForm", { ...localDialogVideoForm.value }); if (emit) {
emit("update:dialogVideoForm", { ...localDialogVideoForm.value });
}
}; };
const fileBaseUrl = `${process.env.VUE_APP_BOE_API_URL}/upload`; const fileBaseUrl = `${process.env.VUE_APP_BOE_API_URL}${process.env.VUE_APP_FILE_PATH}`;
return { return {
localDialogVideoForm, localDialogVideoForm,

View File

@@ -0,0 +1,440 @@
<template>
<div class="tag-container" @click="handleContainerClick">
<el-select
size="large"
style="width: 100%"
v-model="selectedTags"
multiple
filterable
value-key="id"
remote
reserve-keyword
:remote-method="debouncedSearch"
:loading="loading"
placeholder="回车创建新标签"
:no-data-text="'无此标签,按回车键创建'"
@remove-tag="handleTagRemove"
@change="handleSelectionChange"
@keyup.enter="handleEnterKey"
@keyup.delete="handleDeleteKey"
@focus="handleFocus"
ref="tagSelectRef"
>
<el-option
v-for="item in searchResults"
:key="item.id"
:label="item.tagName"
:value="item"
:disabled="isTagDisabled(item)"
/>
</el-select>
<!-- 添加标签计数显示 -->
<div class="tag-count">{{ selectedTags.length }}/5</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from "vue";
import { debounce } from "lodash";
import { $message } from "@/utils/useMessage";
import apiCourseTag from "@/api/modules/courseTag.js";
import { useStore } from "vuex";
import { ElSelect, ElOption } from "element-plus";
// Props
const props = defineProps({
courseId: {
type: String,
required: true,
},
sysTypeList: {
type: Array,
required: true,
default: () => [],
},
maxTags: {
type: Number,
default: 5,
},
// 添加接收初始标签数据的props
initialTags: {
type: Array,
default: () => [],
},
});
// Emits
const emit = defineEmits(["change", "focus"]);
// Store
const store = useStore();
const userInfo = computed(() => store.getters.userInfo);
// Refs
const tagSelectRef = ref(null);
// Reactive data
const selectedTags = ref([]);
const searchResults = ref([]);
const loading = ref(false);
const tagMap = reactive(new Map());
const params = reactive({});
const tag = reactive({});
// 添加临时存储用于回滚
const previousTags = ref([]);
const displayTags = computed(() => {
return selectedTags.value
.map((tag) => (typeof tag === "object" ? tag : tagMap.get(tag)))
.filter(Boolean);
});
// 创建防抖搜索函数
let debouncedSearch = debounce(doSearch, 500);
// Lifecycle hooks
onMounted(() => {
console.log(
"----------sysTypeList.length---------->" + props.sysTypeList.length
);
console.log(
"----------sysTypeList.length---------->" + (props.sysTypeList.length === 0)
);
if (props.initialTags && props.initialTags.length > 0) {
selectedTags.value = props.initialTags;
searchResults.value = props.initialTags;
// 将初始标签添加到tagMap中确保删除功能正常
props.initialTags.forEach((tag) => {
if (tag.id) {
tagMap.set(tag.id, tag);
}
});
}
});
// Watchers
watch(
() => props.courseId,
(newVal) => {
resetTagState();
}
);
watch(
() => props.initialTags,
(newVal) => {
selectedTags.value = newVal || [];
searchResults.value = newVal || [];
// 清空旧缓存
tagMap.clear();
newVal.forEach((tag) => {
if (tag.id) tagMap.set(tag.id, tag);
});
}
);
watch(
() => props.sysTypeList,
() => {
// 只有在已选择分类且有焦点时才重新加载
if (
props.sysTypeList.length > 0 &&
tagSelectRef.value &&
tagSelectRef.value.visible
) {
doSearch("");
}
},
{ deep: true }
);
// Methods
// 新增:检查标签是否应该被禁用
function isTagDisabled(tag) {
// 如果标签已经被选中,不应该禁用(允许取消选择)
const isSelected = selectedTags.value.some(
(selectedTag) => selectedTag.id === tag.id
);
if (isSelected) {
return false;
}
// 如果标签未被选中且已达到最大数量,则禁用
return selectedTags.value.length >= props.maxTags;
}
// 新增:处理输入框获得焦点事件
async function handleFocus() {
previousTags.value = [...selectedTags.value];
// 当输入框获得焦点时,加载默认的搜索结果
if (props.sysTypeList.length > 0) {
await doSearch("");
}
emit("focus");
}
function handleContainerClick() {
// 容器点击时也触发焦点事件
emit("focus");
}
// 新增:重置标签状态的方法
function resetTagState() {
selectedTags.value = [];
searchResults.value = [];
tagMap.clear();
loading.value = false;
// 清空 params 对象
Object.keys(params).forEach((key) => delete params[key]);
}
function handleTagRemove(tagId) {
selectedTags.value = selectedTags.value.filter((id) => id !== tagId);
emit("change", displayTags.value);
clearInput();
}
function removeTag(tagId) {
handleTagRemove(tagId);
}
// 新增:处理删除键事件
function handleDeleteKey(event) {
// 如果输入框内容为空,不执行任何搜索
if (!event.target.value.trim()) {
searchResults.value = [];
}
}
//按回车键,创建新标签
function handleEnterKey(event) {
const inputVal = event.target.value?.trim();
if (!inputVal) return;
// 检查是否与已选择的标签重复
const isDuplicate = selectedTags.value.some(
(tag) => tag.tagName === inputVal
);
if (isDuplicate) {
$message.warning("该标签已存在,无需重复创建");
event.target.value = "";
return;
}
if (!isDuplicate && inputVal && selectedTags.value.length < props.maxTags) {
createNewTag(event.target.value.trim());
clearInput();
} else if (selectedTags.value.length >= props.maxTags) {
$message.warning("最多只能添加5个标签");
clearInput();
} else {
clearInput();
}
}
// 新增:处理选择变化事件
function handleSelectionChange(newValues) {
// 检查每个标签对象是否完整
newValues.forEach((tag, index) => {
if (!tag.tagName) {
console.error(`${index}个标签缺少tagName:`, tag);
}
});
// 检查数量限制
if (newValues.length > props.maxTags) {
$message.warning(`最多只能选择${props.maxTags}个标签`);
// 回滚到之前的状态
selectedTags.value = [...previousTags.value];
return;
}
// 更新前保存当前状态
previousTags.value = [...newValues];
emit("change", displayTags.value);
clearInput();
nextTick(() => {
if (tagSelectRef.value) {
tagSelectRef.value.visible = false;
}
});
}
function clearInput() {
if (tagSelectRef.value) {
const input = tagSelectRef.value.$refs.input;
if (input) {
input.value = "";
}
}
}
//创建新标签
async function createNewTag(tagName) {
// 标签不能超过八个字
if (tagName.length > 8) {
$message.error("标签不能超过8个字");
return;
}
// 检查标签是否在下拉框中已存在
const isExistInSearch = searchResults.value.some(
(tag) => tag.tagName === tagName
);
if (isExistInSearch) {
$message.warning("已存在此标签,请选择");
return;
}
// 首先检查是否与已选择的标签重复
const isDuplicate = selectedTags.value.some((tag) => tag.tagName === tagName);
if (isDuplicate) {
$message.warning("该标签已存在,无需重复创建");
return;
}
// 标签格式验证:仅支持中文、英文、数字、下划线、中横线
const tagPattern = /^[\u4e00-\u9fa5a-zA-Z0-9_-]+$/;
if (!tagPattern.test(tagName)) {
$message.error(
"标签名称仅支持中文、英文、数字、下划线(_)和中横线(-),不支持空格、点和特殊字符"
);
return;
}
// 添加标签数量限制检查
if (selectedTags.value.length >= props.maxTags) {
$message.warning("最多只能添加5个标签");
return;
}
loading.value = true;
try {
params.courseId = props.courseId;
params.tagName = tagName;
// 分类
if (props.sysTypeList.length > 0) {
params.sysType1 = props.sysTypeList[0]; //一级的id
}
if (props.sysTypeList.length > 1) {
params.sysType2 = props.sysTypeList[1]; //二级的id
}
if (props.sysTypeList.length > 2) {
params.sysType3 = props.sysTypeList[2]; //三级的id
}
const { result: newTag } = await apiCourseTag.createTag(params);
$message.success("标签创建成功");
selectedTags.value = [...selectedTags.value, newTag];
// 更新搜索结果的逻辑保持不变
searchResults.value = [newTag, ...searchResults.value];
tagMap.set(newTag.id, newTag);
emit("change", displayTags.value);
nextTick(() => {
// 强制重新设置selectedTags来触发更新
const tempTags = [...selectedTags.value];
selectedTags.value = [];
nextTick(() => {
selectedTags.value = tempTags;
});
if (tagSelectRef.value) {
tagSelectRef.value.visible = false;
}
});
} finally {
loading.value = false;
}
}
// 修改doSearch方法添加搜索结果为空时的提示
async function doSearch(query) {
// 不再在空查询时清空搜索结果
// if (!query.trim()) {
// searchResults.value = []
// return
// }
console.log("---- doSearch ------ query = " + query);
loading.value = true;
try {
// 获取 typeId取 sysTypeList 最后一个有效的值
const typeId =
props.sysTypeList.length > 2
? props.sysTypeList[2]
: props.sysTypeList.length > 1
? props.sysTypeList[1]
: props.sysTypeList.length > 0
? props.sysTypeList[0]
: null;
console.log(
"---- doSearch searchTags ------ query = " +
query +
" , typeId = " +
typeId
);
const { result: tags } = await apiCourseTag.searchTags({
tagName: query,
typeId: typeId,
});
console.log("-- searchTags 查询结果 tags = " + tags);
tags.forEach((item) => {
tagMap.set(item.id, item);
});
searchResults.value = tags;
// 当搜索结果为空时,提示用户可以按回车键创建标签
if (tags.length === 0) {
// $message.info('无此标签,按回车键创建')
}
} finally {
loading.value = false;
}
}
// 导出方法供外部调用
defineExpose({
removeTag,
});
</script>
<style scoped>
.tag-container {
position: relative;
}
/* 添加标签计数样式 */
.tag-count {
position: absolute;
right: 10px;
top: 47%;
transform: translateY(-40%);
font-size: 12px;
color: #999;
background: white;
padding: 0 5px;
pointer-events: none;
/* 添加高度限制 */
height: 25px;
line-height: 25px; /* 垂直居中文字 */
box-sizing: border-box; /* 确保padding包含在height内 */
}
::v-deep(.el-select__tags) {
display: flex;
flex-wrap: wrap;
align-items: center;
}
::v-deep(.el-tag) {
flex: 1 1 auto; /* 自动调整宽度 */ //min-width: 30%; /* 设置最小宽度 */
//max-width: 48%; /* 设置最大宽度,留出边距 */
box-sizing: border-box;
margin-right: 8px;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
justify-content: center;
text-align: center;
}
::v-deep(.el-select__input) {
flex: 1;
min-width: 60px;
}
</style>

View File

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

View File

@@ -1,5 +1,7 @@
<script setup> <script setup>
import { ref, defineOptions, reactive, onMounted } from "vue"; import { ref, defineOptions, onMounted } from "vue";
import { Plus, Loading } from "@element-plus/icons-vue";
import courseTag from "./courseTag.vue";
import { $message } from "@/utils/useMessage"; import { $message } from "@/utils/useMessage";
import { import {
ElForm, ElForm,
@@ -11,9 +13,11 @@ import {
ElInput, ElInput,
ElCascader, ElCascader,
ElSelect, ElSelect,
ElSelectV2,
ElTreeSelect,
ElOption,
} from "element-plus"; } from "element-plus";
import { getClassTree } from "@/api/modules/newApi"; import FieldCloud from "@/components/FileCloud/index.vue";
import filecloud from "@/components/FileCloud/index.vue";
import { useUpload } from "@/hooks/useUpload"; import { useUpload } from "@/hooks/useUpload";
import { useCourseForm } from "@/hooks/useCourseForm"; import { useCourseForm } from "@/hooks/useCourseForm";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@@ -21,68 +25,58 @@ const router = useRouter();
defineOptions({ defineOptions({
name: "ProfessionalMode", name: "ProfessionalMode",
}); });
// 使用上传hook // 使用上传hook
const { fileList, loading, courseCoverurl, handleChange, beforeUpload } = const { fileList, loading, handleChange, beforeUpload } = useUpload();
useUpload();
// 使用表单hook // 使用表单hook
const { formRef, formState, visibilityOptions, resetForm } = useCourseForm(); const { formRef, formState, visibilityOptions, resetForm } = useCourseForm();
import { useFetchCourseList } from "@/hooks/useFetchCourseList";
const {
fetchClassTree,
getUserGroupList,
teachersList,
sysTypeListMap,
courseTags,
loadOrgNode,
curCourseId,
userGroupList,
sysTypeList,
} = useFetchCourseList();
// 表单相关 import { useMediaComponent } from "@/hooks/useMediaComponent";
const labelCol = { style: { width: "80px" } }; const { fileBaseUrl } = useMediaComponent({});
const handleTagsChange = (tags) => {
// 数据相关 console.log("父组件:", tags);
const data = ref({ // 限制最多5个标签
typeOption: [], if (tags.length > 5) {
}); this.$message.warning("最多只能选择5个标签");
// 强制限制为5个
tags = tags.slice(0, 5);
return;
}
let ids = "";
tags.forEach((tag) => {
console.log("父组件name : ", tag.tagName);
ids += tag.id + ",";
});
};
const onTagFocus = () => {};
// 文件选择对话框 // 文件选择对话框
const dlgFileChoose = ref({ const dlgFileShow = ref(false);
show: false,
});
// 课程信息
const courseInfo = ref({
id: "",
name: "",
orderStudy: false,
type: 10,
orgId: "",
coverImg: "",
source: 1,
forUsers: "",
forScene: "",
value: "",
tags: "",
keywords: "",
device: 3,
status: 1,
summary: "",
overview: "",
visible: true,
refId: "",
refType: "",
});
const fileUrl = process.env.VUE_APP_BASE_API1 + process.env.VUE_APP_FILE_PATH;
// 方法定义 // 方法定义
const chooseFile = () => { const chooseFile = () => {
dlgFileChoose.value.show = true; dlgFileShow.value = true;
}; };
const changeCourseImage = (img) => { const changeCourseImage = (img) => {
if (!img.path) { if (!img.path) {
return; return;
} }
dlgFileChoose.value.show = false; dlgFileShow.value = false;
courseInfo.value.coverImg = img.path; formState.coverImg = fileBaseUrl + img.path;
courseCoverurl.value = fileUrl + img.path;
}; };
const choseChoose = () => { const choseChoose = () => {
dlgFileChoose.value.show = false; dlgFileShow.value = false;
}; };
// 表单提交 // 表单提交
@@ -103,18 +97,8 @@ const handleSubmit = () => {
// 表单重置 // 表单重置
const handleReset = () => { const handleReset = () => {
resetForm(courseCoverurl, fileList); resetForm(fileList);
}; };
// API调用
const fetchApi = {
getClassTree: () => {
return getClassTree(1).then((res) => {
data.value.typeOption = res.result;
});
},
};
const next = () => { const next = () => {
// 注意这里的路由跳转需要正确引入和使用vue-router // 注意这里的路由跳转需要正确引入和使用vue-router
router.push({ router.push({
@@ -126,8 +110,10 @@ const next = () => {
}, },
}); });
}; };
// 调用接口
onMounted(() => { onMounted(() => {
fetchApi.getClassTree(); fetchClassTree(1);
getUserGroupList({ keyword: "", page: 1, pageSize: 500 });
}); });
</script> </script>
@@ -148,12 +134,12 @@ onMounted(() => {
<div class="professional-mode-form"> <div class="professional-mode-form">
<el-form-item <el-form-item
label="课程名称" label="课程名称"
prop="courseName" prop="name"
:rules="[{ required: true, message: '请输入课程名称' }]" :rules="[{ required: true, message: '请输入课程名称' }]"
> >
<el-input <el-input
size="large" size="large"
v-model="formState.courseName" v-model="formState.name"
placeholder="请输入课程名称" placeholder="请输入课程名称"
/> />
</el-form-item> </el-form-item>
@@ -167,7 +153,7 @@ onMounted(() => {
size="large" size="large"
v-model="formState.courseCategory" v-model="formState.courseCategory"
placeholder="请选择课程分类" placeholder="请选择课程分类"
:options="data.typeOption" :options="sysTypeListMap"
:props="{ :props="{
label: 'name', label: 'name',
value: 'id', value: 'id',
@@ -177,17 +163,19 @@ onMounted(() => {
<el-form-item <el-form-item
label="资源归属" label="资源归属"
prop="resourceBelong" prop="orgName"
:rules="[{ required: true, message: '请选择资源归属' }]" :rules="[{ required: true, message: '请选择资源归属' }]"
> >
<el-select <el-tree-select
size="large" size="large"
v-model="formState.resourceBelong" check-strictly
lazy
:load="loadOrgNode"
:render-after-expand="false"
v-model="formState.orgName"
:props="{ value: 'id', label: 'name', children: 'children' }"
placeholder="请选择资源归属" placeholder="请选择资源归属"
> />
<el-option label="选项1" value="1"></el-option>
<el-option label="选项2" value="2"></el-option>
</el-select>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
@@ -195,67 +183,65 @@ onMounted(() => {
prop="lecturer" prop="lecturer"
:rules="[{ required: true, message: '请选择授课教师' }]" :rules="[{ required: true, message: '请选择授课教师' }]"
> >
<el-select <el-select-v2
size="large" size="large"
filterable
multiple
v-model="formState.lecturer" v-model="formState.lecturer"
:options="teachersList"
placeholder="请选择授课教师" placeholder="请选择授课教师"
> >
<el-option label="教师1" value="1"></el-option> </el-select-v2>
<el-option label="教师2" value="2"></el-option>
</el-select>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
label="目标人群" label="目标人群"
prop="targetGroup" prop="forUsers"
:rules="[{ required: true, message: '请输入目标人群' }]" :rules="[{ required: true, message: '请输入目标人群' }]"
> >
<el-input <el-input
size="large" size="large"
v-model="formState.targetGroup" v-model="formState.forUsers"
placeholder="请输入目标人群" placeholder="请输入目标人群"
/> />
</el-form-item> </el-form-item>
<el-form-item label="课程标签" prop="courseTags"> <el-form-item label="课程标签" prop="courseTags">
<el-select <courseTag
size="large" :courseId="curCourseId"
v-model="formState.courseTags" :sysTypeList="sysTypeList"
multiple :initialTags="courseTags"
filterable @change="handleTagsChange"
allow-create @focus="onTagFocus"
placeholder="请选择或输入课程标签" style="width: 100%"
> ></courseTag>
<el-option label="标签1" value="标签1"></el-option>
<el-option label="标签2" value="标签2"></el-option>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="受众" prop="audience"> <el-form-item label="受众" prop="crowds">
<el-select <el-select
size="large" size="large"
v-model="formState.audience" v-model="formState.crowds"
placeholder="请选择受众" placeholder="请选择受众"
multiple
value-key="id"
> >
<el-option label="受众1" value="1"></el-option> <el-option
<el-option label="受众2" value="2"></el-option> v-for="item in userGroupList"
:key="item.id"
:label="item.name"
:value="item"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="可见性" prop="visibility"> <el-form-item label="观看设置" prop="device">
<el-radio-group v-model="formState.visibility"> <el-radio-group v-model="formState.device">
<el-radio <el-radio
v-for="item in visibilityOptions" v-for="item in visibilityOptions"
:key="item.value" :key="item.value"
:label="item.value" :label="item.value"
> >
<span {{ item.label }}
:style="{
color:
formState.visibility === item.value ? '#409eff' : '#000',
}"
>{{ item.label }}</span
>
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
@@ -279,18 +265,18 @@ onMounted(() => {
:on-change="handleChange" :on-change="handleChange"
> >
<img <img
v-if="courseCoverurl" v-if="formState.coverImg"
:src="courseCoverurl" :src="formState.coverImg"
alt="avatar" alt="avatar"
style="width: 100%" style="width: 100%"
/> />
<div v-else> <div v-else class="text-center">
<el-icon v-if="loading"><Loading /></el-icon> <Loading v-if="loading" class="w30" />
<el-icon v-else><Plus /></el-icon> <Plus v-else class="w30" />
<div class="el-upload-text">上传图片</div> <div class="el-upload-text">上传图片</div>
</div> </div>
</el-upload> </el-upload>
<el-button type="primary" link @click="chooseFile" <el-button type="primary" link @click="chooseFile" class="ml10"
>选择封面</el-button >选择封面</el-button
> >
</div> </div>
@@ -316,13 +302,13 @@ onMounted(() => {
<el-form-item <el-form-item
label="课程简介" label="课程简介"
prop="courseIntro" prop="summary"
:rules="[{ required: true, message: '请输入课程简介' }]" :rules="[{ required: true, message: '请输入课程简介' }]"
> >
<el-input <el-input
type="textarea" type="textarea"
size="large" size="large"
v-model="formState.courseIntro" v-model="formState.summary"
:rows="4" :rows="4"
placeholder="请输入课程简介" placeholder="请输入课程简介"
/> />
@@ -340,11 +326,11 @@ onMounted(() => {
</div> </div>
</div> </div>
</el-form> </el-form>
<filecloud <field-cloud
:show="dlgFileChoose.show" :show="dlgFileShow"
@choose="changeCourseImage" @choose="changeCourseImage"
@close="choseChoose" @close="choseChoose"
></filecloud> ></field-cloud>
</div> </div>
</template> </template>