feat(course): 添加文档类型资源支持

- 新增文档组件 DocComp.vue,支持 PDF 预览功能
- 在 createCourse.vue 中集成文档类型处理逻辑
- 更新 chooseFileList.vue 支持文档类型上传
- 重构音视频组件,提取公共逻辑至 useMediaComponent hook
- 统一文件路径前缀为 fileBaseUrl,替换硬编码地址
- 调整拖拽表格操作按钮样式及显示逻辑
- 添加 pdf-vue3 依赖用于 PDF 渲染支持
- TODO
- 上传文件目前缺少id 无法进行预览 这是一个问题!
This commit is contained in:
陈昱达
2025-11-24 16:06:54 +08:00
parent cc1af6a11e
commit 7d18bc73ea
9 changed files with 5228 additions and 97 deletions

5109
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@
"json-bigint": "^1.0.0",
"mitt": "^3.0.0",
"moment": "^2.29.4",
"pdf-vue3": "^1.0.12",
"qrcode.vue": "^3.3.3",
"qs": "^6.11.0",
"quill": "^2.0.3",

View File

@@ -148,7 +148,7 @@ onMounted(() => {
:on-success="uploadSuccess"
:file-list="fileList"
>
<el-button v-if="[10, 20].includes(props.resType)" type="primary"
<el-button v-if="[10, 20, 40].includes(props.resType)" type="primary"
>上传新{{ getType(props.resType) }}</el-button
>
</el-upload>

View File

@@ -7,6 +7,7 @@ import {
ElRadio,
ElRadioGroup,
} from "element-plus";
defineOptions({
resType: 20,
});
@@ -27,27 +28,13 @@ const props = defineProps({
},
});
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 }
);
import { useMediaComponent } from "@/hooks/useMediaComponent";
// 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 });
};
// 使用hook处理公共逻辑
const { localDialogVideoForm, updateFormValue, fileBaseUrl } =
useMediaComponent(props, emit);
</script>
<template>
@@ -64,7 +51,7 @@ const updateFormValue = (field, value) => {
style="width: 100%; max-height: 400px"
class="mb10"
:key="localDialogVideoForm.filePath"
:src="'http://home.hzer.xyz:9960/upload/' + localDialogVideoForm.filePath"
:src="fileBaseUrl + localDialogVideoForm.filePath"
>
您的浏览器不支持video
</audio>

View File

@@ -0,0 +1,114 @@
<script setup>
import apiCourseFile from "@/api/modules/courseFile";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import PDF from "pdf-vue3";
defineOptions({
resType: 40,
});
const props = defineProps({
dialogVideoForm: {
type: Object,
default: () => ({
name: "",
filePath: "",
isDrag: true,
completeSetup: 0,
setupTage: 0,
}),
},
isPreview: {
type: Boolean,
default: false,
},
scrollToID: {
type: String,
default: "pdf-perView",
},
autoScroll: {
type: Boolean,
default: false,
},
});
import { useMediaComponent } from "@/hooks/useMediaComponent";
// Emit updates to parent component
const emit = defineEmits(["update:dialogVideoForm"]);
// 使用hook处理公共逻辑
const { localDialogVideoForm, updateFormValue, fileBaseUrl } =
useMediaComponent(props, emit);
// 添加响应式pdf路径变量
const pdfPath = ref("");
const loadPdfFile = () => {
console.log(localDialogVideoForm.value);
//检查当前是否是pdf如果是pdf那么就不需要再查询一次了
let fname = localDialogVideoForm.value.filePath;
if (fname && fname.indexOf(".pdf") > -1) {
localDialogVideoForm.value.pdfPath = localDialogVideoForm.value.filePath;
pdfPath.value = localDialogVideoForm.value.filePath;
console.log(123123);
} else {
apiCourseFile.detail(localDialogVideoForm.value.id).then((rs) => {
if (rs.status == 200) {
if (
rs.result.previewFilePath == "" &&
rs.result.filePath.indexOf("pdf") > -1
) {
pdfPath.value = rs.result.filePath;
} else {
pdfPath.value = rs.result.previewFilePath;
}
}
});
}
};
// 定义响应式数据
const currentPage = ref(0);
// 加载进度
const loadedRatio = ref(0);
const showProgress = ref(true);
// 定义 refs
const pdfRef = ref(null);
// PDF 加载处理
const loadPdfHandler = (e) => {
currentPage.value = 1; // 加载的时候先加载第一页
};
// 监听 loadedRatio 变化
watch(loadedRatio, (newVal) => {
// 直接使用loadedRatio控制进度条没有加载效果
if (newVal == 1) {
setTimeout(() => {
showProgress.value = false;
}, 500);
}
});
onMounted(() => {
loadPdfFile();
});
onBeforeUnmount(() => {});
</script>
<template>
<div class="pdf-perView" id="pdf-perView">
<div class="pdf-box" style="height: 505px; overflow: hidden">
<PDF
style="max-height: 500px; overflow: scroll"
ref="pdfRef"
:src="fileBaseUrl + pdfPath"
v-if="pdfPath"
@onComplete="loadPdfHandler"
></PDF>
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -75,26 +75,12 @@ const props = defineProps({
},
});
// 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 }
);
import { useMediaComponent } from "@/hooks/useMediaComponent";
// 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 });
};
// 使用hook处理公共逻辑
const { localDialogVideoForm, updateFormValue } = useMediaComponent(props, emit);
const editor = ref(null);
@@ -157,4 +143,4 @@ watch(
</el-form>
</template>
<style scoped lang="scss"></style>
<style scoped lang="scss"></style>

View File

@@ -28,27 +28,13 @@ const props = defineProps({
},
});
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 }
);
import { useMediaComponent } from "@/hooks/useMediaComponent";
// 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 });
};
// 使用hook处理公共逻辑
const { localDialogVideoForm, updateFormValue, fileBaseUrl } =
useMediaComponent(props, emit);
</script>
<template>
@@ -67,9 +53,7 @@ const updateFormValue = (field, value) => {
:key="localDialogVideoForm.filePath"
>
<source
:src="
'http://home.hzer.xyz:9960/upload/' + localDialogVideoForm.filePath
"
:src="fileBaseUrl + localDialogVideoForm.filePath"
type="video/mp4"
/>
您的浏览器不支持video

View File

@@ -11,8 +11,9 @@ import chooseFileList from "@/components/CreatedCourse/chooseFileList.vue";
import VideoComp from "@/components/CreatedCourse/preview/VideoComp.vue";
import AudioComp from "@/components/CreatedCourse/preview/AudioComp.vue";
import EditorComp from "@/components/CreatedCourse/preview/EditorComp.vue";
import DocComp from "@/components/CreatedCourse/preview/DocComp.vue";
import { getType } from "@/hooks/useCreateCourseMaps";
const mapComponents = [VideoComp, AudioComp, EditorComp];
const mapComponents = [VideoComp, AudioComp, EditorComp, DocComp];
// 使用课程数据hook
const { courseMetadata, courseList, courseActionButtons, addChapter } =
@@ -20,6 +21,8 @@ const { courseMetadata, courseList, courseActionButtons, addChapter } =
const isPreview = ref(false);
const chooseItemData = ref({});
const showSettingDialog = ref(false);
const isNext = ref(true);
const showTablePreview = ref(false);
// 定义表格列
const showDialog = ref(false);
// 课程操作映射
@@ -32,7 +35,11 @@ const courseOperations = {
courseMetadata.resType = 20;
showDialog.value = true;
},
addDocument: () => {},
addDocument: () => {
courseMetadata.resType = 40;
showDialog.value = true;
isNext.value = false;
},
addImageText: () => {
courseMetadata.resType = 41;
chooseItemData.value.resType = 41;
@@ -67,6 +74,10 @@ const executeCourseOperation = (operationName, data) => {
};
const chooseItem = (data) => {
chooseItemData.value = data;
if (!isNext.value) {
saveContent();
return;
}
showSettingDialog.value = true;
};
// 保存
@@ -166,6 +177,7 @@ const previewRow = (data) => {
v-if="showDialog"
@chooseItem="chooseItem"
:resType="courseMetadata.resType"
:showTablePreview="showTablePreview"
></chooseFileList>
</el-dialog>
@@ -175,9 +187,10 @@ const previewRow = (data) => {
:title="isPreview ? '预览' : getType(chooseItemData.resType)"
>
<div v-for="item in mapComponents">
{{ chooseItemData.resType }}, {{ item }}
<component
v-if="Number(chooseItemData.resType) === item.resType"
v-if="
Number(chooseItemData.resType) === item.resType && showSettingDialog
"
:is="item"
v-model:dialogVideoForm="chooseItemData"
:isPreview="isPreview"

View File

@@ -230,7 +230,7 @@ const renderActionColumn = () => {
return h(
"span",
{
style: { display: "flex", justifyContent: "center", gap: "12px" },
style: { display: "flex", justifyContent: "flex-end", gap: "12px" },
},
[
// 设置
@@ -239,6 +239,9 @@ const renderActionColumn = () => {
{
href: "javascript:void(0)",
onClick: () => handleSetting(index, record),
style: {
display: [40].includes(record.resType) ? "none" : "",
},
},
[
createVNode(SettingOutlined, {