feat(course): 新增音视频与图文组件支持

- 新增 AudioComp.vue 组件,支持音频播放与设置
- 新增 EditorComp.vue 组件,集成富文本编辑器用于图文内容
- 修改 chooseFileList.vue,增加文件上传功能与类型适配
- 更新 createCourse.vue,完善课程章节内容管理逻辑
- 升级 useCourseData.js 和 useCreateCourseMaps.js,增强类型映射与数据结构
- 优化 BasicTable.vue,移除调试日志并调整样式
- 引入 quill 及相关插件依赖以支持富文本编辑功能
This commit is contained in:
陈昱达
2025-11-24 14:07:33 +08:00
parent 6528491334
commit cc1af6a11e
10 changed files with 526 additions and 100 deletions

View 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>

View 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>

View File

@@ -8,6 +8,9 @@ import {
ElRadioGroup,
} from "element-plus";
defineOptions({
resType: 10,
});
const props = defineProps({
dialogVideoForm: {
type: Object,