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" ref="cropCanvas"
> >
<cropper-image <cropper-image
:src="props.img" :src="props.img.path"
alt="Picture" alt="Picture"
rotatable rotatable
scalable scalable
@@ -18,7 +18,7 @@
></cropper-image> ></cropper-image>
<cropper-shade hidden></cropper-shade> <cropper-shade hidden></cropper-shade>
<cropper-handle action="move" plain></cropper-handle> <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-grid role="grid" covered></cropper-grid>
<cropper-crosshair centered></cropper-crosshair> <cropper-crosshair centered></cropper-crosshair>
<cropper-handle <cropper-handle
@@ -48,15 +48,10 @@
<script setup> <script setup>
import { ref, watch, onBeforeUnmount } from "vue"; import { ref, watch, onBeforeUnmount } from "vue";
import "cropperjs"; import "cropperjs";
// import "cropperjs/dist/cropper.css"; import { useMediaComponent } from "@/hooks/useMediaComponent";
import { $message } from "@/utils/useMessage";
import { ElDialog, ElButton } from "element-plus"; 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 cropCanvas = ref(null);
const props = defineProps({ const props = defineProps({
img: { img: {
type: String, type: String,
@@ -66,10 +61,16 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
fileName: {
type: String,
default: "image",
},
}); });
const { fileBaseUrl } = useMediaComponent(props);
const show = ref(props.show); const show = ref(props.show);
const emit = defineEmits(["update:show", "crop-success"]); const emit = defineEmits(["update:show", "crop-success"]);
const selection = ref(null);
watch( watch(
() => show.value, () => show.value,
@@ -85,57 +86,103 @@ watch(
} }
); );
// 文件选择 const dataURLtoFile = (dataurl, filename) => {
const onFileChange = (e) => { let arr = dataurl.split(","),
const file = e.target.files?.[0]; mime = arr[0].match(/:(.*?);/)[1],
if (!file) return; bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
const reader = new FileReader(); while (n--) {
reader.onload = (event) => { u8arr[n] = bstr.charCodeAt(n);
imgSrc.value = event.target.result; }
// 延迟初始化 cropper
setTimeout(initCropper, 50);
};
reader.readAsDataURL(file);
// 清空 input // 创建临时 canvas 元素用于压缩图片
e.target.value = ""; 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 () => { const crop = async () => {
try { 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); // 使用 FormData 格式上传文件
// 使用toBlob方法替代toDataURL解决跨域问题 const formData = new FormData();
canvas.toBlob( formData.append("file", res);
(blob) => {
const dataUrl = URL.createObjectURL(blob); const uploadFileUrl = "/systemapi/xboe/sys/xuploader/file/upload";
console.log(dataUrl); fetch(uploadFileUrl, {
emit("crop-success", dataUrl); method: "POST",
show.value = false; body: formData,
}, })
"image/jpeg", .then((r) => {
0.9 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) { } catch (error) {
console.error("裁剪失败:", error); console.error("裁剪失败:", error);
} }
@@ -143,10 +190,7 @@ const crop = async () => {
// 销毁实例 // 销毁实例
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (cropper.value) { // cropper 实例会在组件销毁时自动清理
cropper.value.destroy();
cropper.value = null;
}
}); });
</script> </script>

View File

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

View File

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

View File

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