refactor(courselibrary): 重构在线课程管理页面为iframe嵌入模式

- 移除原有复杂的搜索表单和表格组件
- 替换为嵌入远程课程管理页面的iframe
- 简化页面结构和样式代码
- 保留必要的学生模型组件引用
- 更新页面标题注释和文件路径信息
This commit is contained in:
陈昱达
2025-12-08 13:17:07 +08:00
parent 473b39b32b
commit d3b393ed73
4 changed files with 183 additions and 90 deletions

View File

@@ -8,7 +8,7 @@
ref="cropCanvas"
>
<cropper-image
:src="props.img"
:src="props.img.path"
alt="Picture"
rotatable
scalable
@@ -18,7 +18,7 @@
></cropper-image>
<cropper-shade hidden></cropper-shade>
<cropper-handle action="move" plain></cropper-handle>
<cropper-selection initial-coverage="0.5" outlined>
<cropper-selection initial-coverage="0.5" outlined ref="selection">
<cropper-grid role="grid" covered></cropper-grid>
<cropper-crosshair centered></cropper-crosshair>
<cropper-handle
@@ -48,15 +48,10 @@
<script setup>
import { ref, watch, onBeforeUnmount } from "vue";
import "cropperjs";
// import "cropperjs/dist/cropper.css";
import { useMediaComponent } from "@/hooks/useMediaComponent";
import { $message } from "@/utils/useMessage";
import { ElDialog, ElButton } from "element-plus";
const imageRef = ref(null);
const imgSrc = ref("");
const cropper = ref(null);
const resultUrl = ref("");
const scale = ref(1);
const cropCanvas = ref(null);
const props = defineProps({
img: {
type: String,
@@ -66,10 +61,16 @@ const props = defineProps({
type: Boolean,
default: false,
},
fileName: {
type: String,
default: "image",
},
});
const { fileBaseUrl } = useMediaComponent(props);
const show = ref(props.show);
const emit = defineEmits(["update:show", "crop-success"]);
const selection = ref(null);
watch(
() => show.value,
@@ -85,57 +86,103 @@ watch(
}
);
// 文件选择
const onFileChange = (e) => {
const file = e.target.files?.[0];
if (!file) return;
const dataURLtoFile = (dataurl, filename) => {
let arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
const reader = new FileReader();
reader.onload = (event) => {
imgSrc.value = event.target.result;
// 延迟初始化 cropper
setTimeout(initCropper, 50);
};
reader.readAsDataURL(file);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
// 清空 input
e.target.value = "";
// 创建临时 canvas 元素用于压缩图片
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
return new Promise((resolve) => {
img.src = dataurl;
img.onload = () => {
// 限制最大宽度或高度为 800px
const maxWidthOrHeight = 800;
let width = img.width;
let height = img.height;
if (width > height) {
if (width > maxWidthOrHeight) {
height *= maxWidthOrHeight / width;
width = maxWidthOrHeight;
}
} else {
if (height > maxWidthOrHeight) {
width *= maxWidthOrHeight / height;
height = maxWidthOrHeight;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// 将 canvas 转换为 base64质量设置为 0.8
const compressedDataUrl = canvas.toDataURL(mime, 0.8);
// 将压缩后的 base64 转换为文件
const compressedArr = compressedDataUrl.split(",");
const compressedBstr = atob(compressedArr[1]);
const compressedN = compressedBstr.length;
const compressedU8arr = new Uint8Array(compressedN);
for (let i = 0; i < compressedN; i++) {
compressedU8arr[i] = compressedBstr.charCodeAt(i);
}
resolve(new File([compressedU8arr], filename, { type: mime }));
};
});
};
// 初始化 cropper
const initCropper = () => {
const image = imageRef.value;
};
// 缩放
const onScale = (e) => {
const value = parseFloat(e.target.value);
scale.value = value;
cropper.value?.zoomTo(value);
};
// 旋转
const rotate = (deg) => {
cropper.value?.rotate(deg);
};
// 裁剪并输出
const crop = async () => {
try {
const canvas = await cropCanvas.value.$toCanvas();
// 获取裁剪区域的画布
await selection.value.$toCanvas().then((canvas) => {
const image = canvas.toDataURL();
dataURLtoFile(image, props.img.name).then((res) => {
// 文件生成 bolburl
const blobUrl = URL.createObjectURL(res);
console.log(canvas);
// 使用toBlob方法替代toDataURL解决跨域问题
canvas.toBlob(
(blob) => {
const dataUrl = URL.createObjectURL(blob);
console.log(dataUrl);
emit("crop-success", dataUrl);
show.value = false;
},
"image/jpeg",
0.9
);
// 使用 FormData 格式上传文件
const formData = new FormData();
formData.append("file", res);
const uploadFileUrl = "/systemapi/xboe/sys/xuploader/file/upload";
fetch(uploadFileUrl, {
method: "POST",
body: formData,
})
.then((r) => {
return r.json();
})
.then((result) => {
if (result.status !== 200) {
$message.error(result.result.message);
return;
}
// 将结果传递给父组件
emit("crop-success", {
path: fileBaseUrl + result.result.filePath,
name: result.result.displayName,
});
show.value = false;
})
.catch((error) => {
console.error("上传失败:", error);
});
});
});
} catch (error) {
console.error("裁剪失败:", error);
}
@@ -143,10 +190,7 @@ const crop = async () => {
// 销毁实例
onBeforeUnmount(() => {
if (cropper.value) {
cropper.value.destroy();
cropper.value = null;
}
// cropper 实例会在组件销毁时自动清理
});
</script>

View File

@@ -1,7 +1,5 @@
import { ref } from "vue";
import { $message, ElMessage } from "@/utils/useMessage";
import { Loading, Plus } from "@element-plus/icons-vue";
/**
* 文件上传相关hook
* @returns
@@ -10,7 +8,10 @@ export function useUpload() {
// 上传相关
const fileList = ref([]);
const loading = ref(false);
const courseCoverurl = ref("");
const courseCover = ref({
path: "",
name: "",
});
// 获取图片base64
const getBase64 = (img, callback) => {
@@ -21,21 +22,33 @@ export function useUpload() {
// 上传状态变化处理
const handleChange = (info) => {
if (info.file.status === "uploading") {
loading.value = true;
return;
}
if (info.file.status === "done") {
// Get this url from response in real world.
getBase64(info.file.originFileObj, (base64Url) => {
courseCoverurl.value = base64Url;
loading.value = false;
});
}
if (info.file.status === "error") {
console.log(info);
courseCover.value = {
...info,
path: info.url,
};
getBase64(info.raw, (base64Url) => {
courseCover.value.path = base64Url;
courseCover.value.name = info.name;
loading.value = false;
$message.error("upload error");
}
});
// if (info.file.status === "uploading") {
// loading.value = true;
// return;
// }
// if (info.file.status === "done") {
// // Get this url from response in real world.
// getBase64(info.file.originFileObj, (base64Url) => {
// courseCover.value.path = base64Url;
// courseCover.value.name = info.file.name;
// loading.value = false;
// });
// }
// if (info.file.status === "error") {
// loading.value = false;
// $message.error("upload error");
// }
};
// 上传前检查
@@ -48,6 +61,18 @@ export function useUpload() {
if (!isLt2M) {
$message.error("Image must smaller than 2MB!");
}
if (isJpgOrPng && isLt2M) {
getBase64(file, (base64Url) => {
courseCover.value = {
raw: file,
path: base64Url,
name: file.name,
};
loading.value = false;
});
}
return isJpgOrPng && isLt2M;
};
@@ -55,8 +80,7 @@ export function useUpload() {
// 数据
fileList,
loading,
courseCoverurl,
courseCover,
// 方法
getBase64,
handleChange,

View File

@@ -303,12 +303,12 @@ const handleNext = () => {
</el-dialog>
<!-- 设置预览弹窗 -->
<!-- :fullscreen="chooseItemData.resType === 41"-->
<el-dialog
v-model="courseMetadata.showSettingDialog"
:title="
courseMetadata.isPreview ? '预览' : getType(chooseItemData.resType)
"
:fullscreen="chooseItemData.resType === 41"
>
<div class="component-preview">
<template v-for="item in mapComponents" :key="item.name">

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, defineOptions, onMounted } from "vue";
import { ref, defineOptions, onMounted, watch } from "vue";
import { Plus, Loading } from "@element-plus/icons-vue";
import courseTag from "./courseTag.vue";
import { $message } from "@/utils/useMessage";
@@ -28,7 +28,8 @@ defineOptions({
name: "ProfessionalMode",
});
// 使用上传hook
const { fileList, loading, handleChange, beforeUpload } = useUpload();
const { fileList, loading, handleChange, beforeUpload, courseCover } =
useUpload();
// 使用表单hook
const { formRef, formState, visibilityOptions, resetForm } = useCourseForm();
import { useFetchCourseList } from "@/hooks/useFetchCourseList";
@@ -46,28 +47,33 @@ const {
import { useMediaComponent } from "@/hooks/useMediaComponent";
const { fileBaseUrl } = useMediaComponent({});
const chooseImage = ref({
path: "",
name: "",
});
const handleTagsChange = (tags) => {
console.log("父组件:", tags);
// 限制最多5个标签
if (tags.length > 5) {
this.$message.warning("最多只能选择5个标签");
$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 dlgFileShow = ref(false);
// 方法定义
const chooseFile = () => {
dlgFileShow.value = true;
};
const dlgCutShow = ref(false);
const changeCourseImage = (img) => {
@@ -75,7 +81,12 @@ const changeCourseImage = (img) => {
return;
}
dlgFileShow.value = false;
formState.coverImg = fileBaseUrl + img.path;
chooseImage.value = {
...img,
path: fileBaseUrl + img.path,
};
dlgCutShow.value = true;
};
@@ -84,9 +95,24 @@ const choseChoose = () => {
};
const success = (e) => {
console.log(e);
formState.coverImg = e;
formState.coverImg = e.path;
};
watch(
() => courseCover.value,
(n) => {
if (n.path) {
dlgCutShow.value = true;
chooseImage.value = {
...n,
};
}
},
{
deep: true,
}
);
// 表单提交
const handleSubmit = () => {
// formRef.value
@@ -107,8 +133,8 @@ const handleSubmit = () => {
const handleReset = () => {
resetForm(fileList);
};
const next = () => {
// 注意这里的路由跳转需要正确引入和使用vue-router
router.push({
path: "/createcourse",
query: {
@@ -118,6 +144,7 @@ const next = () => {
},
});
};
// 调用接口
onMounted(() => {
fetchClassTree(1);
@@ -268,9 +295,7 @@ onMounted(() => {
list-type="picture-card"
class="avatar-uploader"
:show-file-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
:on-change="handleChange"
>
<img
v-if="formState.coverImg"
@@ -342,7 +367,7 @@ onMounted(() => {
<!-- 裁切-->
<Cropper
:img="formState.coverImg"
:img="chooseImage"
v-model:show="dlgCutShow"
@crop-success="success"
></Cropper>