|
|
|
|
@ -4,13 +4,33 @@
|
|
|
|
|
class="tui-image-editor-wrap"
|
|
|
|
|
v-loading="loading"
|
|
|
|
|
>
|
|
|
|
|
<div id="tui-image-editor"></div>
|
|
|
|
|
<div
|
|
|
|
|
ref="tuiImageEditorRef"
|
|
|
|
|
id="tui-image-editor"
|
|
|
|
|
></div>
|
|
|
|
|
<el-button
|
|
|
|
|
circle
|
|
|
|
|
:icon="Close"
|
|
|
|
|
class="close-btn"
|
|
|
|
|
@click="closeEditor"
|
|
|
|
|
></el-button>
|
|
|
|
|
<el-button
|
|
|
|
|
:loading="sending"
|
|
|
|
|
type="primary"
|
|
|
|
|
class="save-btn"
|
|
|
|
|
@click="save"
|
|
|
|
|
>保存图片</el-button
|
|
|
|
|
>
|
|
|
|
|
<el-icon
|
|
|
|
|
class="left-btn"
|
|
|
|
|
@click="changeImage('prev')"
|
|
|
|
|
><ArrowLeftBold
|
|
|
|
|
/></el-icon>
|
|
|
|
|
<el-icon
|
|
|
|
|
class="right-btn"
|
|
|
|
|
@click="changeImage('next')"
|
|
|
|
|
><ArrowRightBold
|
|
|
|
|
/></el-icon>
|
|
|
|
|
</div>
|
|
|
|
|
</Teleport>
|
|
|
|
|
</template>
|
|
|
|
|
@ -18,15 +38,21 @@
|
|
|
|
|
<script setup>
|
|
|
|
|
import '../css/tui-color-picker.css';
|
|
|
|
|
import '../css/tui-image-editor.css';
|
|
|
|
|
import { ref, onMounted, watch } from 'vue';
|
|
|
|
|
import { Close } from '@element-plus/icons-vue';
|
|
|
|
|
import { ref, onMounted, onUnmounted } from 'vue';
|
|
|
|
|
import { Close, ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue';
|
|
|
|
|
|
|
|
|
|
const emits = defineEmits(['close']);
|
|
|
|
|
const props = defineProps(['imgList']);
|
|
|
|
|
const currentIndex = ref(0);
|
|
|
|
|
const imageEditor = ref(null);
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const sending = ref(false);
|
|
|
|
|
const scale = ref(1);
|
|
|
|
|
const tuiImageEditorRef = ref(null);
|
|
|
|
|
const imgSize = {
|
|
|
|
|
width: 750,
|
|
|
|
|
height: 500,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function createScriptElement(src) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
@ -41,17 +67,22 @@ function createScriptElement(src) {
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
// 加载同级目录下的js文件
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/fabric.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/tui-code-snippet.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/tui-color-picker.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/FileSaver.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/tui-image-editor.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/white-theme.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/black-theme.js');
|
|
|
|
|
if (!window.hasOwnProperty('tui')) {
|
|
|
|
|
// 加载同级目录下的js文件
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/fabric.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/tui-code-snippet.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/tui-color-picker.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/FileSaver.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/tui-image-editor.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/white-theme.js');
|
|
|
|
|
await createScriptElement('/src/views/demo/tuiImageEditor/js/black-theme.js');
|
|
|
|
|
}
|
|
|
|
|
initTuiImageEditor();
|
|
|
|
|
addEventListenerFN();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {});
|
|
|
|
|
|
|
|
|
|
const locale_zh = {
|
|
|
|
|
Resize: '调整宽高',
|
|
|
|
|
Crop: '裁剪',
|
|
|
|
|
@ -152,53 +183,183 @@ const initTuiImageEditor = () => {
|
|
|
|
|
// Image editor
|
|
|
|
|
imageEditor.value = new tui.ImageEditor('#tui-image-editor', {
|
|
|
|
|
includeUI: {
|
|
|
|
|
loadImage: {},
|
|
|
|
|
loadImage: {
|
|
|
|
|
path: props.imgList[currentIndex.value].path,
|
|
|
|
|
name: props.imgList[currentIndex.value].name,
|
|
|
|
|
},
|
|
|
|
|
locale: locale_zh,
|
|
|
|
|
theme: theme,
|
|
|
|
|
menu: ['resize', 'crop', 'rotate', 'draw', 'shape', 'icon', 'text'],
|
|
|
|
|
},
|
|
|
|
|
cssMaxWidth: 700,
|
|
|
|
|
cssMaxHeight: 500,
|
|
|
|
|
cssMaxWidth: imgSize.width,
|
|
|
|
|
cssMaxHeight: imgSize.height,
|
|
|
|
|
usageStatistics: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
imageEditor.value.loadImageFromURL(props.imgList[currentIndex.value].path, props.imgList[currentIndex.value].name).then((res) => {
|
|
|
|
|
console.log(res);
|
|
|
|
|
imageEditor.value.ui._actions.main.zoomIn = () => {
|
|
|
|
|
zoomSize('zoomIn');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
imageEditor.value.ui._actions.main.zoomOut = () => {
|
|
|
|
|
zoomSize('zoomOut');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log(imageEditor.value);
|
|
|
|
|
imageEditor.value.ui._actions.main.zoomIn = () => {
|
|
|
|
|
console.log('zoomIn');
|
|
|
|
|
zoomSize('zoomIn');
|
|
|
|
|
};
|
|
|
|
|
const defaultColor = '#ff4040';
|
|
|
|
|
|
|
|
|
|
imageEditor.value.ui._actions.main.zoomOut = () => {
|
|
|
|
|
console.log('zoomOut');
|
|
|
|
|
zoomSize('zoomOut');
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
console.log(imageEditor.value);
|
|
|
|
|
// 自定义画笔颜色
|
|
|
|
|
imageEditor.value.ui.draw.color = defaultColor;
|
|
|
|
|
imageEditor.value.ui.draw._els.drawColorPicker.colorElement.style.backgroundColor = defaultColor;
|
|
|
|
|
|
|
|
|
|
window.onresize = function () {
|
|
|
|
|
imageEditor.value.ui.resizeEditor();
|
|
|
|
|
};
|
|
|
|
|
// 自定义形状颜色
|
|
|
|
|
imageEditor.value.ui.shape.options.stroke = defaultColor;
|
|
|
|
|
imageEditor.value.ui.shape._els.strokeColorpicker.colorElement.style.backgroundColor = defaultColor;
|
|
|
|
|
|
|
|
|
|
// 自定义图标颜色
|
|
|
|
|
imageEditor.value.ui.icon._els.iconColorpicker._color = defaultColor;
|
|
|
|
|
imageEditor.value.ui.icon._els.iconColorpicker.colorElement.style.backgroundColor = defaultColor;
|
|
|
|
|
|
|
|
|
|
// 自定义文字颜色
|
|
|
|
|
imageEditor.value.ui.text._els.textColorpicker.color = defaultColor;
|
|
|
|
|
imageEditor.value.ui.text._els.textColorpicker.colorElement.style.backgroundColor = defaultColor;
|
|
|
|
|
// 更改默认文本,在源代码中tui-image-editor.js的 addText方法 50592行
|
|
|
|
|
|
|
|
|
|
// window.onresize = function () {
|
|
|
|
|
// imageEditor.value.ui.resizeEditor();
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
// 监听添加文本事件,动态设置默认文本,这里有bug
|
|
|
|
|
// imageEditor.value.on('addText', (pos) => {
|
|
|
|
|
// let obj = imageEditor.value._graphics._objects;
|
|
|
|
|
// let arr = Reflect.ownKeys(obj);
|
|
|
|
|
// arr.forEach((item) => {
|
|
|
|
|
// if (obj[item].text === '请输入') {
|
|
|
|
|
// let defaultText = '自定义文本';
|
|
|
|
|
// obj[item].text = defaultText;
|
|
|
|
|
// obj[item].textLines[0] = defaultColor;
|
|
|
|
|
// obj[item]._text = defaultColor.split(',');
|
|
|
|
|
// obj[item]._textBeforeEdit = defaultColor;
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// console.log(imageEditor.value);
|
|
|
|
|
// });
|
|
|
|
|
loading.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addEventListenerFN = () => {
|
|
|
|
|
document.querySelector('.tui-image-editor-wrap').addEventListener('click', handleClickOutside);
|
|
|
|
|
window.addEventListener('wheel', handleWheelEvent);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleClickOutside = (event) => {
|
|
|
|
|
const getBtnDom = (claseeName) => {
|
|
|
|
|
return document.querySelector(claseeName);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
getBtnDom('.save-btn').contains(event.target) ||
|
|
|
|
|
getBtnDom('.left-btn').contains(event.target) ||
|
|
|
|
|
getBtnDom('.right-btn').contains(event.target)
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (tuiImageEditorRef.value && !(tuiImageEditorRef.value.contains(event.target) || tuiImageEditorRef.value === event.target)) {
|
|
|
|
|
closeEditor();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleWheelEvent = (event) => {
|
|
|
|
|
if (event.deltaY < 0) {
|
|
|
|
|
zoomSize('zoomIn');
|
|
|
|
|
} else {
|
|
|
|
|
zoomSize('zoomOut');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const zoomSize = (type) => {
|
|
|
|
|
console.log(type);
|
|
|
|
|
if ((scale.value > 1.5 && type === 'zoomIn') || (scale.value < 0.8 && type === 'zoomOut')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (type === 'zoomIn') {
|
|
|
|
|
scale.value = scale.value + 0.1;
|
|
|
|
|
} else {
|
|
|
|
|
scale.value = scale.value - 0.1;
|
|
|
|
|
}
|
|
|
|
|
imageEditor.value.resizeCanvasDimension({
|
|
|
|
|
width: 750 * scale.value,
|
|
|
|
|
height: 500 * scale.value,
|
|
|
|
|
width: imgSize.width * scale.value,
|
|
|
|
|
height: imgSize.height * scale.value,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const save = () => {
|
|
|
|
|
sending.value = true;
|
|
|
|
|
const base64String = imageEditor.value.toDataURL({ format: 'jpeg', quality: 0.8 });
|
|
|
|
|
|
|
|
|
|
//base64转file对象
|
|
|
|
|
const data = window.atob(base64String.split(',')[1]);
|
|
|
|
|
const arr = new Uint8Array(data.length);
|
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
|
arr[i] = data.charCodeAt(i);
|
|
|
|
|
}
|
|
|
|
|
const blob = new Blob([arr], { type: 'image/jpeg' }); //bolb对象
|
|
|
|
|
const file = new File([arr], new Date().getTime() + '.jpg', { type: blob.type }); //file对象
|
|
|
|
|
const objectURL = URL.createObjectURL(file); //file对象url
|
|
|
|
|
|
|
|
|
|
// 上传图片
|
|
|
|
|
// 走服务器上传逻辑
|
|
|
|
|
|
|
|
|
|
// 下载图片
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = objectURL;
|
|
|
|
|
a.download = new Date().getTime() + '.jpg';
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(objectURL);
|
|
|
|
|
|
|
|
|
|
sending.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const changeImage = (type) => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
let index;
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'prev':
|
|
|
|
|
index = currentIndex.value - 1;
|
|
|
|
|
if (index < 0) {
|
|
|
|
|
index = props.imgList.length - 1;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'next':
|
|
|
|
|
index = currentIndex.value + 1;
|
|
|
|
|
if (index >= props.imgList.length) {
|
|
|
|
|
index = 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
imageEditor.value
|
|
|
|
|
.loadImageFromURL(props.imgList[index].path, props.imgList[index].name)
|
|
|
|
|
.then(() => {
|
|
|
|
|
currentIndex.value = index;
|
|
|
|
|
refreshImageShow();
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const refreshImageShow = () => {
|
|
|
|
|
scale.value = 1;
|
|
|
|
|
imageEditor.value.resizeCanvasDimension({
|
|
|
|
|
width: imgSize.width,
|
|
|
|
|
height: imgSize.height,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeEditor = () => {
|
|
|
|
|
document.querySelector('.tui-image-editor-wrap').removeEventListener('click', handleClickOutside);
|
|
|
|
|
imageEditor.value.destroy();
|
|
|
|
|
imageEditor.value = null;
|
|
|
|
|
window.fabric = null;
|
|
|
|
|
emits('close');
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
@ -219,16 +380,41 @@ const closeEditor = () => {
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.close-btn {
|
|
|
|
|
.close-btn,
|
|
|
|
|
.save-btn {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 10px;
|
|
|
|
|
right: calc(50% - 400px + 18px);
|
|
|
|
|
z-index: 9;
|
|
|
|
|
}
|
|
|
|
|
.save-btn {
|
|
|
|
|
top: unset;
|
|
|
|
|
bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.left-btn,
|
|
|
|
|
.right-btn {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: calc(50% - 400px + 10px);
|
|
|
|
|
top: 50%;
|
|
|
|
|
margin-top: -12px;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
.right-btn {
|
|
|
|
|
left: unset;
|
|
|
|
|
right: calc(50% - 400px + 10px);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
body > svg:last-of-type {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 选择颜色隐藏输入颜色区域
|
|
|
|
|
.tui-image-editor-container div.tui-colorpicker-clearfix {
|
|
|
|
|
display: none !important;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|