mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/fe-manage.git
synced 2025-12-09 10:56:46 +08:00
feat(cropper): 添加图片裁剪组件及依赖
- 新增 Cropper 组件,支持图片裁剪、缩放、旋转功能 - 集成 cropperjs 库及其相关元素组件 - 在专业模式页面中引入并使用裁剪组件 - 更新 package.json 和 lock 文件以包含新依赖 - 优化登录过期弹窗按钮文本显示
This commit is contained in:
118
package-lock.json
generated
118
package-lock.json
generated
@@ -1246,6 +1246,115 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element/-/element-2.1.0.tgz",
|
||||
"integrity": "sha512-2zELddqHQNmlvkPoiYzE5nxEjPE+C8nXoTPuvV3FvLp3YjBinc7qb73Icg9UXP0o9qC4+h9q96JgGo0AyMO/Ng==",
|
||||
"requires": {
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element-canvas": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-canvas/-/element-canvas-2.1.0.tgz",
|
||||
"integrity": "sha512-el+rfJpZxsD2q5XxDBA4fRczcrOqB65Lb7roqXOq8LKufwf4bPWA9C6DjNJJahh/TP94dsLIEy3tSkgRMDv3Aw==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element-crosshair": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-crosshair/-/element-crosshair-2.1.0.tgz",
|
||||
"integrity": "sha512-0V589dAx8uZAfvJwdINLn76gfPQEafPH94ukjJ76uX0FCUovLaAVX+VRD/MDSYn0Mza/xejzmL9Dhd1DfemvmA==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element-grid": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-grid/-/element-grid-2.1.0.tgz",
|
||||
"integrity": "sha512-dEnk0rO+vp553LMvsPYgfrqVFcYXeVFrgFeavBYYEhAXtO40p7kN4rmLYLMMjaN+T/Mx2BATv6kUQpALKy2HLw==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element-handle": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-handle/-/element-handle-2.1.0.tgz",
|
||||
"integrity": "sha512-8BklWA4C/2GGAULupIWleSnGutECvYt3vx9flodqDfZpDEozws4LgLqmmzVuQmVkRVUdLnXdtx28kjgWLtzkHg==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element-image": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-image/-/element-image-2.1.0.tgz",
|
||||
"integrity": "sha512-mXOV8ixJvG0XtTxLebYAKDjEkFbFOQnsF02hXPZk1yQSV0J+LLhN7a2NePrtKnoTsEV19fhhX3UorMoyGGxvzg==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/element-canvas": "^2.1.0",
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element-selection": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-selection/-/element-selection-2.1.0.tgz",
|
||||
"integrity": "sha512-mtFtBl6HIa/s9TWohXw+Z5eJoeYTqylrIcHvS7oVv0uM7IyeRwBW65Q7z+KtLfq/LW+2Sw/XDyvR+VN/DawBPw==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/element-canvas": "^2.1.0",
|
||||
"@cropper/element-image": "^2.1.0",
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element-shade": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-shade/-/element-shade-2.1.0.tgz",
|
||||
"integrity": "sha512-zMdyqbb0lc0Vd1oj2Z1miIZvhyZG41OXMHvrNt0hNwblh0dVdrvtw48lnFDgRv+672vt2CNx7Q04GuvCQfPlgg==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/element-canvas": "^2.1.0",
|
||||
"@cropper/element-selection": "^2.1.0",
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/element-viewer": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-viewer/-/element-viewer-2.1.0.tgz",
|
||||
"integrity": "sha512-XnxlQuqHitd1FOFZ6E0yXAF5NYd/LyIvONLLHI9p1rJw747WYKUPxQaSYtFKF7IOizJu/8mMj++Zc1dZ5ZP3YQ==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/element-canvas": "^2.1.0",
|
||||
"@cropper/element-image": "^2.1.0",
|
||||
"@cropper/element-selection": "^2.1.0",
|
||||
"@cropper/utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/elements": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/elements/-/elements-2.1.0.tgz",
|
||||
"integrity": "sha512-qvzlYDn3VQgPPpsCu6Gi1XUO0v3vpXQFSjjxcVijbXeNsl/eiKrN7H9/CEiRgi5vr8kXfd7ZvgYxBjUBbH+y+w==",
|
||||
"requires": {
|
||||
"@cropper/element": "^2.1.0",
|
||||
"@cropper/element-canvas": "^2.1.0",
|
||||
"@cropper/element-crosshair": "^2.1.0",
|
||||
"@cropper/element-grid": "^2.1.0",
|
||||
"@cropper/element-handle": "^2.1.0",
|
||||
"@cropper/element-image": "^2.1.0",
|
||||
"@cropper/element-selection": "^2.1.0",
|
||||
"@cropper/element-shade": "^2.1.0",
|
||||
"@cropper/element-viewer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@cropper/utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/utils/-/utils-2.1.0.tgz",
|
||||
"integrity": "sha512-wLtpZ4/UWgo+fGmG8NBWge8x5ehjfDe9ovleDfLy8kpwFaw43XXOEXQtRL1UNr0u4JZxaeO8FcXcolRWUUrlRQ=="
|
||||
},
|
||||
"@ctrl/tinycolor": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz",
|
||||
@@ -3906,6 +4015,15 @@
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"cropperjs": {
|
||||
"version": "2.0.0-rc.2",
|
||||
"resolved": "https://registry.npmmirror.com/cropperjs/-/cropperjs-2.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-BTuz+UeZphGOEnBCuQiNT4rk1uFfKJaKmTgoH9XU7Q8IMkLdodW7YPWINmXJXwWMt1nXiKze5qKADVbz9xtVFg==",
|
||||
"requires": {
|
||||
"@cropper/elements": "^2.0.0-rc.2",
|
||||
"@cropper/utils": "^2.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"axios": "^1.1.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"core-js": "^3.8.3",
|
||||
"cropperjs": "^2.0.0-rc.2",
|
||||
"dayjs": "^1.11.6",
|
||||
"echarts": "^5.4.1",
|
||||
"element-plus": "^2.2.17",
|
||||
|
||||
@@ -43,12 +43,13 @@ jsonRequest.interceptors.response.use(
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "重新登录",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
).then(() => {
|
||||
window.location.href = ReLoginUrl;
|
||||
});
|
||||
} else if (code === 403) {
|
||||
ElMessage.error("当前操作没有权限");
|
||||
$message.error("当前操作没有权限");
|
||||
} else if (code === 302) {
|
||||
window.location.href = ReLoginUrl;
|
||||
} else {
|
||||
@@ -113,6 +114,7 @@ formRequest.interceptors.response.use(
|
||||
ElMessageBox.confirm("登录状态无效,即将跳转至登录页", "登录已过期", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
}).then(() => {
|
||||
window.location.href = ReLoginUrl;
|
||||
});
|
||||
|
||||
187
src/components/Cropper/Cropper.vue
Normal file
187
src/components/Cropper/Cropper.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<el-dialog class="image-cropper" v-model="show" width="800px">
|
||||
<!-- 图像容器 -->
|
||||
<div class="img-container">
|
||||
<cropper-canvas
|
||||
background
|
||||
style="width: 100%; height: 100%"
|
||||
ref="cropCanvas"
|
||||
>
|
||||
<cropper-image
|
||||
:src="props.img"
|
||||
alt="Picture"
|
||||
rotatable
|
||||
scalable
|
||||
skewable
|
||||
translatable
|
||||
crossorigin="anonymous"
|
||||
></cropper-image>
|
||||
<cropper-shade hidden></cropper-shade>
|
||||
<cropper-handle action="move" plain></cropper-handle>
|
||||
<cropper-selection initial-coverage="0.5" outlined>
|
||||
<cropper-grid role="grid" covered></cropper-grid>
|
||||
<cropper-crosshair centered></cropper-crosshair>
|
||||
<cropper-handle
|
||||
action="move"
|
||||
theme-color="rgba(255, 255, 255, 0.35)"
|
||||
></cropper-handle>
|
||||
<cropper-handle action="n-resize"></cropper-handle>
|
||||
<cropper-handle action="e-resize"></cropper-handle>
|
||||
<cropper-handle action="s-resize"></cropper-handle>
|
||||
<cropper-handle action="w-resize"></cropper-handle>
|
||||
<cropper-handle action="ne-resize"></cropper-handle>
|
||||
<cropper-handle action="nw-resize"></cropper-handle>
|
||||
<cropper-handle action="se-resize"></cropper-handle>
|
||||
<cropper-handle action="sw-resize"></cropper-handle>
|
||||
</cropper-selection>
|
||||
</cropper-canvas>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div slot="footer">
|
||||
<el-button @click="show = false">取消</el-button>
|
||||
<el-button type="primary" @click="crop">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onBeforeUnmount } from "vue";
|
||||
import "cropperjs";
|
||||
// import "cropperjs/dist/cropper.css";
|
||||
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,
|
||||
default: "",
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const show = ref(props.show);
|
||||
const emit = defineEmits(["update:show", "crop-success"]);
|
||||
|
||||
watch(
|
||||
() => show.value,
|
||||
(n) => {
|
||||
emit("update:show", n);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(n) => {
|
||||
show.value = n;
|
||||
}
|
||||
);
|
||||
|
||||
// 文件选择
|
||||
const onFileChange = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
imgSrc.value = event.target.result;
|
||||
// 延迟初始化 cropper
|
||||
setTimeout(initCropper, 50);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// 清空 input
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
// 初始化 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();
|
||||
|
||||
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
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("裁剪失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 销毁实例
|
||||
onBeforeUnmount(() => {
|
||||
if (cropper.value) {
|
||||
cropper.value.destroy();
|
||||
cropper.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-cropper {
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.img-container {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.controls input[type="range"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -16,8 +16,10 @@ import {
|
||||
ElSelectV2,
|
||||
ElTreeSelect,
|
||||
ElOption,
|
||||
ElDialog,
|
||||
} from "element-plus";
|
||||
import FieldCloud from "@/components/FileCloud/index.vue";
|
||||
import Cropper from "@/components/Cropper/Cropper.vue";
|
||||
import { useUpload } from "@/hooks/useUpload";
|
||||
import { useCourseForm } from "@/hooks/useCourseForm";
|
||||
import { useRouter } from "vue-router";
|
||||
@@ -66,6 +68,7 @@ const dlgFileShow = ref(false);
|
||||
const chooseFile = () => {
|
||||
dlgFileShow.value = true;
|
||||
};
|
||||
const dlgCutShow = ref(false);
|
||||
|
||||
const changeCourseImage = (img) => {
|
||||
if (!img.path) {
|
||||
@@ -73,12 +76,17 @@ const changeCourseImage = (img) => {
|
||||
}
|
||||
dlgFileShow.value = false;
|
||||
formState.coverImg = fileBaseUrl + img.path;
|
||||
dlgCutShow.value = true;
|
||||
};
|
||||
|
||||
const choseChoose = () => {
|
||||
dlgFileShow.value = false;
|
||||
};
|
||||
|
||||
const success = (e) => {
|
||||
console.log(e);
|
||||
formState.coverImg = e;
|
||||
};
|
||||
// 表单提交
|
||||
const handleSubmit = () => {
|
||||
// formRef.value
|
||||
@@ -331,6 +339,13 @@ onMounted(() => {
|
||||
@choose="changeCourseImage"
|
||||
@close="choseChoose"
|
||||
></field-cloud>
|
||||
|
||||
<!-- 裁切-->
|
||||
<Cropper
|
||||
:img="formState.coverImg"
|
||||
v-model:show="dlgCutShow"
|
||||
@crop-success="success"
|
||||
></Cropper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user