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