mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/fe-manage.git
synced 2025-12-11 03:46:45 +08:00
refactor(courselibrary): 重构在线课程管理页面为iframe嵌入模式
- 移除原有复杂的搜索表单和表格组件 - 替换为嵌入远程课程管理页面的iframe - 简化页面结构和样式代码 - 保留必要的学生模型组件引用 - 更新页面标题注释和文件路径信息
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user