mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/fe-manage.git
synced 2025-12-09 02:46:45 +08:00
feat(course): 实现课程标签管理功能
- 新增课程标签API模块,支持标签分页查询、创建、修改状态等操作 - 开发课程标签组件,支持标签搜索、创建、删除和数量限制 - 集成标签组件到专业模式页面,替换原有标签选择器 - 优化课程创建组件,重构表单状态管理和操作流程 - 升级Element Plus组件版本,支持el-select-v2等新组件 - 添加lodash依赖用于防抖搜索功能 - 调整样式和布局,优化标签显示和交互体验
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"jquery": "^3.6.1",
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"pdf-vue3": "^1.0.12",
|
||||
|
||||
64
src/api/modules/courseTag.js
Normal file
64
src/api/modules/courseTag.js
Normal 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,
|
||||
};
|
||||
121
src/api/modules/userbasic.js
Normal file
121
src/api/modules/userbasic.js
Normal 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,
|
||||
};
|
||||
@@ -737,6 +737,7 @@ textarea {
|
||||
}
|
||||
|
||||
.el-select,
|
||||
.el-select-v2,
|
||||
.el-cascader {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ export function useCourseData() {
|
||||
sectionIndex: "",
|
||||
resType: 0,
|
||||
selectionIndex: null,
|
||||
// 添加课程操作相关状态
|
||||
isPreview: false,
|
||||
showSettingDialog: false,
|
||||
isNext: true, // 添加操作的时候 弹窗是否弹出对应类型表单
|
||||
showTablePreview: false,
|
||||
showDialog: false,
|
||||
classId: "",
|
||||
});
|
||||
|
||||
// 课程列表数据
|
||||
@@ -112,4 +119,4 @@ export function useCourseData() {
|
||||
courseActionButtons,
|
||||
addChapter,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,49 +2,47 @@ import { reactive, ref } from "vue";
|
||||
|
||||
/**
|
||||
* 课程表单相关hook
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export function useCourseForm() {
|
||||
// 表单相关
|
||||
const formRef = ref();
|
||||
// 表单相关
|
||||
const formState = reactive({
|
||||
courseName: "", // 课程名称
|
||||
courseCategory: [], // 课程分类
|
||||
resourceBelong: undefined, // 资源归属
|
||||
lecturer: undefined, // 授课教师
|
||||
targetGroup: "", // 目标人群
|
||||
courseTags: [], // 课程标签
|
||||
audience: undefined, // 受众
|
||||
visibility: "Apple", // 可见性
|
||||
coverIntro: "", // 封面介绍
|
||||
courseValue: "", // 课程价值
|
||||
courseIntro: "", // 课程简介
|
||||
name: "",
|
||||
device: 3,
|
||||
crowds: [],
|
||||
courseTags: [],
|
||||
courseCategory: [],
|
||||
orgName: "",
|
||||
forUsers: "",
|
||||
lecturer: [],
|
||||
coverImg: "",
|
||||
courseValue: "",
|
||||
summary: "",
|
||||
});
|
||||
|
||||
// 可见性选项
|
||||
const visibilityOptions = [
|
||||
{ label: "PC端可见", value: "Apple" },
|
||||
{ label: "移动端可见", value: "Pear" },
|
||||
{ label: "多端可见", value: "Orange", disabled: false },
|
||||
{ label: "PC端可见", value: 1 },
|
||||
{ label: "移动端可见", value: 2 },
|
||||
{ label: "多端可见", value: 3 },
|
||||
];
|
||||
|
||||
// 表单重置
|
||||
const resetForm = (courseCoverurl, fileList) => {
|
||||
const resetForm = (fileList) => {
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
if (courseCoverurl) {
|
||||
courseCoverurl.value = "";
|
||||
}
|
||||
|
||||
if (fileList) {
|
||||
fileList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
formRef,
|
||||
formState,
|
||||
visibilityOptions,
|
||||
resetForm
|
||||
resetForm,
|
||||
formRef,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
111
src/hooks/useFetchCourseList.js
Normal file
111
src/hooks/useFetchCourseList.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -21,10 +21,12 @@ export function useMediaComponent(props, emit) {
|
||||
// Update form values and emit changes
|
||||
const updateFormValue = (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 {
|
||||
localDialogVideoForm,
|
||||
|
||||
440
src/views/courselibrary/components/courseTag.vue
Normal file
440
src/views/courselibrary/components/courseTag.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<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 {
|
||||
ElForm,
|
||||
@@ -11,9 +13,11 @@ import {
|
||||
ElInput,
|
||||
ElCascader,
|
||||
ElSelect,
|
||||
ElSelectV2,
|
||||
ElTreeSelect,
|
||||
ElOption,
|
||||
} from "element-plus";
|
||||
import { getClassTree } from "@/api/modules/newApi";
|
||||
import filecloud from "@/components/FileCloud/index.vue";
|
||||
import FieldCloud from "@/components/FileCloud/index.vue";
|
||||
import { useUpload } from "@/hooks/useUpload";
|
||||
import { useCourseForm } from "@/hooks/useCourseForm";
|
||||
import { useRouter } from "vue-router";
|
||||
@@ -21,68 +25,58 @@ const router = useRouter();
|
||||
defineOptions({
|
||||
name: "ProfessionalMode",
|
||||
});
|
||||
|
||||
// 使用上传hook
|
||||
const { fileList, loading, courseCoverurl, handleChange, beforeUpload } =
|
||||
useUpload();
|
||||
|
||||
const { fileList, loading, handleChange, beforeUpload } = useUpload();
|
||||
// 使用表单hook
|
||||
const { formRef, formState, visibilityOptions, resetForm } = useCourseForm();
|
||||
import { useFetchCourseList } from "@/hooks/useFetchCourseList";
|
||||
const {
|
||||
fetchClassTree,
|
||||
getUserGroupList,
|
||||
teachersList,
|
||||
sysTypeListMap,
|
||||
courseTags,
|
||||
loadOrgNode,
|
||||
curCourseId,
|
||||
userGroupList,
|
||||
sysTypeList,
|
||||
} = useFetchCourseList();
|
||||
|
||||
// 表单相关
|
||||
const labelCol = { style: { width: "80px" } };
|
||||
|
||||
// 数据相关
|
||||
const data = ref({
|
||||
typeOption: [],
|
||||
});
|
||||
|
||||
import { useMediaComponent } from "@/hooks/useMediaComponent";
|
||||
const { fileBaseUrl } = useMediaComponent({});
|
||||
const handleTagsChange = (tags) => {
|
||||
console.log("父组件:", tags);
|
||||
// 限制最多5个标签
|
||||
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({
|
||||
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 dlgFileShow = ref(false);
|
||||
// 方法定义
|
||||
const chooseFile = () => {
|
||||
dlgFileChoose.value.show = true;
|
||||
dlgFileShow.value = true;
|
||||
};
|
||||
|
||||
const changeCourseImage = (img) => {
|
||||
if (!img.path) {
|
||||
return;
|
||||
}
|
||||
dlgFileChoose.value.show = false;
|
||||
courseInfo.value.coverImg = img.path;
|
||||
courseCoverurl.value = fileUrl + img.path;
|
||||
dlgFileShow.value = false;
|
||||
formState.coverImg = fileBaseUrl + img.path;
|
||||
};
|
||||
|
||||
const choseChoose = () => {
|
||||
dlgFileChoose.value.show = false;
|
||||
dlgFileShow.value = false;
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
@@ -103,18 +97,8 @@ const handleSubmit = () => {
|
||||
|
||||
// 表单重置
|
||||
const handleReset = () => {
|
||||
resetForm(courseCoverurl, fileList);
|
||||
resetForm(fileList);
|
||||
};
|
||||
|
||||
// API调用
|
||||
const fetchApi = {
|
||||
getClassTree: () => {
|
||||
return getClassTree(1).then((res) => {
|
||||
data.value.typeOption = res.result;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
// 注意:这里的路由跳转需要正确引入和使用vue-router
|
||||
router.push({
|
||||
@@ -126,8 +110,10 @@ const next = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
// 调用接口
|
||||
onMounted(() => {
|
||||
fetchApi.getClassTree();
|
||||
fetchClassTree(1);
|
||||
getUserGroupList({ keyword: "", page: 1, pageSize: 500 });
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -148,12 +134,12 @@ onMounted(() => {
|
||||
<div class="professional-mode-form">
|
||||
<el-form-item
|
||||
label="课程名称"
|
||||
prop="courseName"
|
||||
prop="name"
|
||||
:rules="[{ required: true, message: '请输入课程名称' }]"
|
||||
>
|
||||
<el-input
|
||||
size="large"
|
||||
v-model="formState.courseName"
|
||||
v-model="formState.name"
|
||||
placeholder="请输入课程名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -167,7 +153,7 @@ onMounted(() => {
|
||||
size="large"
|
||||
v-model="formState.courseCategory"
|
||||
placeholder="请选择课程分类"
|
||||
:options="data.typeOption"
|
||||
:options="sysTypeListMap"
|
||||
:props="{
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
@@ -177,17 +163,19 @@ onMounted(() => {
|
||||
|
||||
<el-form-item
|
||||
label="资源归属"
|
||||
prop="resourceBelong"
|
||||
prop="orgName"
|
||||
:rules="[{ required: true, message: '请选择资源归属' }]"
|
||||
>
|
||||
<el-select
|
||||
<el-tree-select
|
||||
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="请选择资源归属"
|
||||
>
|
||||
<el-option label="选项1" value="1"></el-option>
|
||||
<el-option label="选项2" value="2"></el-option>
|
||||
</el-select>
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
@@ -195,67 +183,65 @@ onMounted(() => {
|
||||
prop="lecturer"
|
||||
:rules="[{ required: true, message: '请选择授课教师' }]"
|
||||
>
|
||||
<el-select
|
||||
<el-select-v2
|
||||
size="large"
|
||||
filterable
|
||||
multiple
|
||||
v-model="formState.lecturer"
|
||||
:options="teachersList"
|
||||
placeholder="请选择授课教师"
|
||||
>
|
||||
<el-option label="教师1" value="1"></el-option>
|
||||
<el-option label="教师2" value="2"></el-option>
|
||||
</el-select>
|
||||
</el-select-v2>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="目标人群"
|
||||
prop="targetGroup"
|
||||
prop="forUsers"
|
||||
:rules="[{ required: true, message: '请输入目标人群' }]"
|
||||
>
|
||||
<el-input
|
||||
size="large"
|
||||
v-model="formState.targetGroup"
|
||||
v-model="formState.forUsers"
|
||||
placeholder="请输入目标人群"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="课程标签" prop="courseTags">
|
||||
<el-select
|
||||
size="large"
|
||||
v-model="formState.courseTags"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
placeholder="请选择或输入课程标签"
|
||||
>
|
||||
<el-option label="标签1" value="标签1"></el-option>
|
||||
<el-option label="标签2" value="标签2"></el-option>
|
||||
</el-select>
|
||||
<courseTag
|
||||
:courseId="curCourseId"
|
||||
:sysTypeList="sysTypeList"
|
||||
:initialTags="courseTags"
|
||||
@change="handleTagsChange"
|
||||
@focus="onTagFocus"
|
||||
style="width: 100%"
|
||||
></courseTag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="受众" prop="audience">
|
||||
<el-form-item label="受众" prop="crowds">
|
||||
<el-select
|
||||
size="large"
|
||||
v-model="formState.audience"
|
||||
v-model="formState.crowds"
|
||||
placeholder="请选择受众"
|
||||
multiple
|
||||
value-key="id"
|
||||
>
|
||||
<el-option label="受众1" value="1"></el-option>
|
||||
<el-option label="受众2" value="2"></el-option>
|
||||
<el-option
|
||||
v-for="item in userGroupList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="可见性" prop="visibility">
|
||||
<el-radio-group v-model="formState.visibility">
|
||||
<el-form-item label="观看设置" prop="device">
|
||||
<el-radio-group v-model="formState.device">
|
||||
<el-radio
|
||||
v-for="item in visibilityOptions"
|
||||
:key="item.value"
|
||||
:label="item.value"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
color:
|
||||
formState.visibility === item.value ? '#409eff' : '#000',
|
||||
}"
|
||||
>{{ item.label }}</span
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
@@ -279,18 +265,18 @@ onMounted(() => {
|
||||
:on-change="handleChange"
|
||||
>
|
||||
<img
|
||||
v-if="courseCoverurl"
|
||||
:src="courseCoverurl"
|
||||
v-if="formState.coverImg"
|
||||
:src="formState.coverImg"
|
||||
alt="avatar"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div v-else>
|
||||
<el-icon v-if="loading"><Loading /></el-icon>
|
||||
<el-icon v-else><Plus /></el-icon>
|
||||
<div v-else class="text-center">
|
||||
<Loading v-if="loading" class="w30" />
|
||||
<Plus v-else class="w30" />
|
||||
<div class="el-upload-text">上传图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<el-button type="primary" link @click="chooseFile"
|
||||
<el-button type="primary" link @click="chooseFile" class="ml10"
|
||||
>选择封面</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -316,13 +302,13 @@ onMounted(() => {
|
||||
|
||||
<el-form-item
|
||||
label="课程简介"
|
||||
prop="courseIntro"
|
||||
prop="summary"
|
||||
:rules="[{ required: true, message: '请输入课程简介' }]"
|
||||
>
|
||||
<el-input
|
||||
type="textarea"
|
||||
size="large"
|
||||
v-model="formState.courseIntro"
|
||||
v-model="formState.summary"
|
||||
:rows="4"
|
||||
placeholder="请输入课程简介"
|
||||
/>
|
||||
@@ -340,11 +326,11 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<filecloud
|
||||
:show="dlgFileChoose.show"
|
||||
<field-cloud
|
||||
:show="dlgFileShow"
|
||||
@choose="changeCourseImage"
|
||||
@close="choseChoose"
|
||||
></filecloud>
|
||||
></field-cloud>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user