mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/fe-manage.git
synced 2025-12-24 02:02:55 +08:00
feat(course): 新增音视频与图文组件支持
- 新增 AudioComp.vue 组件,支持音频播放与设置 - 新增 EditorComp.vue 组件,集成富文本编辑器用于图文内容 - 修改 chooseFileList.vue,增加文件上传功能与类型适配 - 更新 createCourse.vue,完善课程章节内容管理逻辑 - 升级 useCourseData.js 和 useCreateCourseMaps.js,增强类型映射与数据结构 - 优化 BasicTable.vue,移除调试日志并调整样式 - 引入 quill 及相关插件依赖以支持富文本编辑功能
This commit is contained in:
@@ -1,24 +1,21 @@
|
||||
<script setup>
|
||||
import { reactive, onMounted, ref, h, watch } from "vue";
|
||||
import {
|
||||
ElButton,
|
||||
ElInput,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadioGroup,
|
||||
ElRadio,
|
||||
ElInputNumber,
|
||||
} from "element-plus";
|
||||
import { ElButton, ElInput, ElUpload } from "element-plus";
|
||||
import BasicTable from "@/components/BasicElTable/BasicTable.vue";
|
||||
import { getPageListByType } from "@/hooks/useCreateCourseMaps";
|
||||
|
||||
const props = defineProps({});
|
||||
import { getType, getMapsItem } from "@/hooks/useCreateCourseMaps";
|
||||
import Cookies from "vue-cookies";
|
||||
const props = defineProps({
|
||||
resType: {
|
||||
type: Number,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const tableData = ref([]);
|
||||
const form = reactive({
|
||||
name: "",
|
||||
resType: 10,
|
||||
resType: props.resType,
|
||||
});
|
||||
let pagination = reactive({
|
||||
pageSize: 10,
|
||||
@@ -74,7 +71,7 @@ const columns = [
|
||||
isDrag: false,
|
||||
completeSetup: 0,
|
||||
setupTage: "",
|
||||
resType: 10,
|
||||
resType: props.resType,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -108,25 +105,31 @@ const chooseItem = (type) => {
|
||||
showDialog.value = false;
|
||||
emit("chooseItem", {
|
||||
...dialogVideoForm,
|
||||
type: 10,
|
||||
type: props.resType,
|
||||
});
|
||||
// postData.content=this.cware.content;
|
||||
// this.cwareChange.content = deepClone(this.cware.content)
|
||||
// if(this.cware.content.contentType==52){
|
||||
// if(this.cware.linkInfo.url==''){
|
||||
// this.$message.error("请填写外连URL地址");
|
||||
// return;
|
||||
// }
|
||||
// postData.content.content=JSON.stringify(this.cware.linkInfo);
|
||||
// this.cwareChange.linkInfo = deepClone(this.cware.linkInfo)
|
||||
// }else if(this.cware.content.contentType==10 || this.cware.content.contentType==20){
|
||||
// if(this.cware.curriculumData.url==''){
|
||||
// this.$message.error("请选择课件");
|
||||
// return;
|
||||
// }
|
||||
// postData.content.content=JSON.stringify(this.cware.curriculumData);
|
||||
// this.cwareChange.curriculumData = deepClone(this.cware.curriculumData)
|
||||
// }
|
||||
};
|
||||
|
||||
const uploadData = reactive({
|
||||
headers: {
|
||||
"XBOE-Access-Token": Cookies.get("token"),
|
||||
},
|
||||
data: {
|
||||
dir: "course",
|
||||
},
|
||||
actionUrl: process.env.VUE_APP_SYS_API + "/xboe/sys/xuploader/file/upload",
|
||||
});
|
||||
|
||||
const fileList = ref([]);
|
||||
const uploadSuccess = (result) => {
|
||||
if (result.status === 200) {
|
||||
emit("chooseItem", {
|
||||
...dialogVideoForm,
|
||||
name: result.result.displayName,
|
||||
resType: props.resType,
|
||||
...result.result,
|
||||
});
|
||||
fileList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -135,20 +138,36 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="add-video" v-if="!isPreview && !isSetting">
|
||||
<div class="add-video">
|
||||
<div class="add-vide-header">
|
||||
<div>
|
||||
<el-button>上传新视频</el-button>
|
||||
<span class="desc ml10">文件大小限制:1G,支持的文件类型:mp4 </span>
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-upload
|
||||
:action="uploadData.actionUrl"
|
||||
:headers="uploadData.headers"
|
||||
:data="uploadData.data"
|
||||
:on-success="uploadSuccess"
|
||||
:file-list="fileList"
|
||||
>
|
||||
<el-button v-if="[10, 20].includes(props.resType)" type="primary"
|
||||
>上传新{{ getType(props.resType) }}</el-button
|
||||
>
|
||||
</el-upload>
|
||||
<span class="desc ml10"
|
||||
>文件大小限制:{{
|
||||
getMapsItem(props.resType).uploadSizeName
|
||||
}},支持的文件类型:{{ getMapsItem(props.resType).fileType.join(",") }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-input
|
||||
style="width: 150px"
|
||||
placeholder="请输入视频名称"
|
||||
:placeholder="`请输入${getType(props.resType)}名称`"
|
||||
v-model="form.name"
|
||||
clearable
|
||||
></el-input>
|
||||
<el-button class="ml10" @click="getVideoList">查询</el-button>
|
||||
<el-button class="ml10" @click="getVideoList" type="primary"
|
||||
>查询</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt10">
|
||||
|
||||
106
src/components/CreatedCourse/preview/AudioComp.vue
Normal file
106
src/components/CreatedCourse/preview/AudioComp.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
} from "element-plus";
|
||||
defineOptions({
|
||||
resType: 20,
|
||||
});
|
||||
const props = defineProps({
|
||||
dialogVideoForm: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: "",
|
||||
filePath: "",
|
||||
isDrag: true,
|
||||
completeSetup: 0,
|
||||
setupTage: 0,
|
||||
}),
|
||||
},
|
||||
isPreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
import { ref, watch } from "vue";
|
||||
// Create a reactive copy of the prop for local modifications
|
||||
const localDialogVideoForm = ref({ ...props.dialogVideoForm });
|
||||
|
||||
// Watch for changes in the prop and update the local copy
|
||||
watch(
|
||||
() => props.dialogVideoForm,
|
||||
(newVal) => {
|
||||
Object.assign(localDialogVideoForm.value, newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Emit updates to parent component
|
||||
const emit = defineEmits(["update:dialogVideoForm"]);
|
||||
|
||||
// Update form values and emit changes
|
||||
const updateFormValue = (field, value) => {
|
||||
localDialogVideoForm.value[field] = value;
|
||||
emit("update:dialogVideoForm", { ...localDialogVideoForm.value });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form>
|
||||
<el-form-item label="视频名称" v-if="!isPreview">
|
||||
<el-input
|
||||
v-model="localDialogVideoForm.name"
|
||||
@update:modelValue="(val) => updateFormValue('name', val)"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<!-- Added video type prop -->
|
||||
<audio
|
||||
controls
|
||||
style="width: 100%; max-height: 400px"
|
||||
class="mb10"
|
||||
:key="localDialogVideoForm.filePath"
|
||||
:src="'http://home.hzer.xyz:9960/upload/' + localDialogVideoForm.filePath"
|
||||
>
|
||||
您的浏览器不支持video
|
||||
</audio>
|
||||
<el-form-item label="是否允许拖拽" v-if="!isPreview">
|
||||
<el-radio-group
|
||||
:model-value="localDialogVideoForm.isDrag"
|
||||
@update:modelValue="(val) => updateFormValue('isDrag', val)"
|
||||
size="small"
|
||||
>
|
||||
<el-radio :label="true" border>是</el-radio>
|
||||
<el-radio :label="false" border>否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="完成规则设置" v-if="!isPreview">
|
||||
<el-radio-group
|
||||
:model-value="localDialogVideoForm.completeSetup"
|
||||
@update:modelValue="(val) => updateFormValue('completeSetup', val)"
|
||||
>
|
||||
<el-radio :label="0">默认(系统自动控制)</el-radio>
|
||||
<el-radio :label="1">
|
||||
按进度
|
||||
<el-input-number
|
||||
:disabled="localDialogVideoForm.completeSetup === 0"
|
||||
:model-value="localDialogVideoForm.setupTage"
|
||||
@update:modelValue="(val) => updateFormValue('setupTage', val)"
|
||||
size="mini"
|
||||
:min="0"
|
||||
:max="100"
|
||||
label="描述文字"
|
||||
controls-position="right"
|
||||
></el-input-number>
|
||||
%
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
160
src/components/CreatedCourse/preview/EditorComp.vue
Normal file
160
src/components/CreatedCourse/preview/EditorComp.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import { ElForm, ElFormItem, ElInput } from "element-plus";
|
||||
import Quill from "quill";
|
||||
import "quill/dist/quill.core.css";
|
||||
import "quill/dist/quill.snow.css";
|
||||
import "quill/dist/quill.bubble.css";
|
||||
import { ImageDrop } from "quill-image-drop-module";
|
||||
import BlotFormatter from "quill-blot-formatter";
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
|
||||
|
||||
// 注册Quill模块
|
||||
Quill.register("modules/imageDrop", ImageDrop);
|
||||
Quill.register("modules/blotFormatter", BlotFormatter);
|
||||
|
||||
const Size = Quill.import("attributors/style/size");
|
||||
Size.whitelist = ["15px", "18px"];
|
||||
Quill.register(Size, true);
|
||||
|
||||
// 只有在Parchment存在时才创建lineHeightAttributor
|
||||
let lineHeightStyle = null;
|
||||
try {
|
||||
const Parchment = Quill.import("parchment");
|
||||
if (Parchment && Parchment.Attributor && Parchment.Attributor.Style) {
|
||||
class lineHeightAttributor extends Parchment.Attributor.Style {}
|
||||
lineHeightStyle = new lineHeightAttributor("lineHeight", "line-height", {
|
||||
scope: Parchment.Scope.INLINE,
|
||||
whitelist: ["1", "1.5", "2", "3", "4"],
|
||||
});
|
||||
Quill.register({ "formats/lineHeight": lineHeightStyle }, true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to register lineHeight formatter:", e);
|
||||
}
|
||||
|
||||
const toolbarOptions = [
|
||||
["bold", "italic", "underline", "strike"],
|
||||
["blockquote", "code-block"],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ indent: "-1" }, { indent: "+1" }],
|
||||
[{ size: [false, "18px"] }],
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ align: [] }],
|
||||
[{ lineheight: ["initial", "1", "1.5", "2", "3", "4"] }],
|
||||
["clean"],
|
||||
["link", "image"],
|
||||
["selectPicture"],
|
||||
];
|
||||
|
||||
// 随机数 八位
|
||||
function uuid() {
|
||||
return Math.random().toString(36).substring(2, 8);
|
||||
}
|
||||
|
||||
const quillClass = "editor_" + uuid();
|
||||
let quill = null;
|
||||
|
||||
defineOptions({
|
||||
resType: 41,
|
||||
});
|
||||
const props = defineProps({
|
||||
dialogVideoForm: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: "",
|
||||
filePath: "",
|
||||
isDrag: true,
|
||||
completeSetup: 0,
|
||||
setupTage: 0,
|
||||
}),
|
||||
},
|
||||
isPreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Create a reactive copy of the prop for local modifications
|
||||
const localDialogVideoForm = ref({ ...props.dialogVideoForm });
|
||||
|
||||
// Watch for changes in the prop and update the local copy
|
||||
watch(
|
||||
() => props.dialogVideoForm,
|
||||
(newVal) => {
|
||||
Object.assign(localDialogVideoForm.value, newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Emit updates to parent component
|
||||
const emit = defineEmits(["update:dialogVideoForm"]);
|
||||
|
||||
// Update form values and emit changes
|
||||
const updateFormValue = (field, value) => {
|
||||
localDialogVideoForm.value[field] = value;
|
||||
emit("update:dialogVideoForm", { ...localDialogVideoForm.value });
|
||||
};
|
||||
|
||||
const editor = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (editor.value) {
|
||||
quill = new Quill(`.${quillClass}`, {
|
||||
modules: {
|
||||
toolbar: toolbarOptions,
|
||||
imageDrop: true,
|
||||
blotFormatter: {
|
||||
overlay: {
|
||||
// 自定义图片调整大小的样式
|
||||
},
|
||||
},
|
||||
},
|
||||
theme: "snow",
|
||||
});
|
||||
|
||||
// 如果有内容,设置内容
|
||||
if (props.dialogVideoForm.filePath) {
|
||||
quill.root.innerHTML = props.dialogVideoForm.filePath;
|
||||
}
|
||||
|
||||
// 监听文本变化
|
||||
quill.on("text-change", () => {
|
||||
const content = quill.root.innerHTML;
|
||||
updateFormValue("filePath", content);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (quill) {
|
||||
quill = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听内容变化
|
||||
watch(
|
||||
() => props.dialogVideoForm.filePath,
|
||||
(newContent) => {
|
||||
if (quill && newContent !== quill.root.innerHTML) {
|
||||
quill.root.innerHTML = newContent || "";
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form>
|
||||
<el-form-item label="名称" v-if="!isPreview">
|
||||
<el-input
|
||||
v-model="localDialogVideoForm.name"
|
||||
@update:model-value="(val) => updateFormValue('name', val)"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<div :class="quillClass" ref="editor" style="min-height: 300px"></div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
ElRadioGroup,
|
||||
} from "element-plus";
|
||||
|
||||
defineOptions({
|
||||
resType: 10,
|
||||
});
|
||||
const props = defineProps({
|
||||
dialogVideoForm: {
|
||||
type: Object,
|
||||
|
||||
Reference in New Issue
Block a user