- 预设海报模板,可以选择任一模板,在模板基础上添加自定义的内容
- 支持上传图片素材,用户可以拖拽图片至画布中
- 支持添加文本至画布,设置文本相关样式(字体颜色,字体大小,粗体,斜体等)
- 输入x,y坐标,设置元素在画布中的位置
- 添加文本对齐方式(左对齐,居中,右对齐),仅支持文本,不支持图片
- 优化----实现图片在鼠标松开时直接出现在最终位置且效果更加丝滑,见代码二:
代码一:
<template>
<div class="warp-card">
<div class="certificate-editor">
<!-- 左侧工具栏 -->
<div class="toolbar">
<!-- 模板选择 -->
<div class="tool-section">
<h3>
<el-icon
title="Select a template and customize the content based on the existing template"
><InfoFilled
/></el-icon>
Default Template
</h3>
<div class="template-list" v-if="templates && templates.length">
<div
v-for="(template, index) in templates"
:key="index"
class="template-item"
@click="applyTemplate(template)"
>
<el-image
:src="template.logo"
fit="contain"
style="border-bottom: 1px solid #dcdfe6"
/>
<div class="template-name">{{ template.name }}</div>
<div
class="delete-icon"
@click.stop="deleteItem(index, template)"
>
x
</div>
</div>
</div>
</div>
<div class="tool-section">
<h3>
<el-icon
title="Users can customize and upload the image materials they need"
><InfoFilled
/></el-icon>
Image Tools
</h3>
<div class="image-grid" v-if="images && images.length">
<div
v-for="(img, index) in images"
:key="index"
class="image-item"
draggable="true"
@dragstart="onImageDragStart($event, img)"
>
<el-image
:src="img.src"
fit="contain"
:preview-src-list="[img.src]"
:initial-index="0"
preview-teleported
title="Click to Preview the Image"
>
<!-- <template #error>
<div class="image-slot">
<el-icon><icon-picture /></el-icon>
</div>
</template> -->
</el-image>
<div class="delete-icon2" @click.stop="deleteImgItem(img)">x</div>
</div>
</div>
<el-upload
:http-request="handleUploadFile"
style="margin-top: 10px"
class="upload-image"
action="/upload"
:on-success="handleUploadSuccess"
:show-file-list="false"
accept="image/*"
:before-upload="beforeUpload"
>
<el-button type="primary" :loading="upLoading">Add Image</el-button>
<template #tip>
<div class="el-upload__tip">
Click the add image button to begin upload [.jpg, .jpeg, .gif,
.png]
</div>
</template>
</el-upload>
</div>
<div class="tool-section">
<h3>
<el-icon
title="Add Text You can add a text and set the font color, font style, and font size for the text"
><InfoFilled
/></el-icon>
Text Tools
</h3>
<el-form :model="textStyle" label-width="80px">
<el-form-item label="sizeStyle">
<div class="size-buttons">
<el-button
@click="setTextSize('h1')"
link
size="small"
class="btn"
>H1</el-button
>
<el-button
@click="setTextSize('h2')"
link
size="small"
class="btn"
>H2</el-button
>
<el-button
@click="setTextSize('h3')"
link
size="small"
class="btn"
>H3</el-button
>
<el-button
@click="setTextSize('h4')"
link
size="small"
class="btn"
>H4</el-button
>
<el-button
@click="setTextSize('h5')"
link
size="small"
class="btn"
>H5</el-button
>
<el-button
@click="setTextSize('h6')"
link
size="small"
class="btn"
>H6</el-button
>
</div>
</el-form-item>
<el-form-item label="fontSize">
<el-input-number
v-model="textStyle.fontSize"
:min="12"
:max="72"
@change="updateTextStyle"
/>
</el-form-item>
<!-- <el-form-item label="字体">
<el-select v-model="textStyle.fontFamily" @change="updateTextStyle">
<el-option label="微软雅黑" value="Microsoft YaHei" />
<el-option label="宋体" value="SimSun" />
<el-option label="黑体" value="SimHei" />
</el-select>
</el-form-item> -->
<el-form-item
label="fontStyle"
style="display: flex; justify-content: center"
>
<!-- 方法一: -->
<!-- <el-switch
v-model="textStyle.bold"
active-text="加粗"
@change="updateTextStyle"
/>
<el-switch
v-model="textStyle.italic"
active-text="斜体"
@change="updateTextStyle"
/> -->
<!-- 方法二: -->
<div style="display: flex; align-items: center">
<el-button @click="toggleBold" link type="info">
<img
src="~@/assets/font-b.png"
alt="icon"
width="20"
height="20"
title="bold"
/></el-button>
<el-button @click="toggleItalic" link type="info"
><img
src="~@/assets/font-I.png"
alt="icon"
width="20"
height="20"
title="italic"
/></el-button>
</div>
</el-form-item>
<el-form-item label="color">
<el-color-picker
v-model="textStyle.color"
@change="updateTextStyle"
show-alpha
:predefine="predefineColors"
/>
</el-form-item>
</el-form>
<!-- <div class="tool-section">
<h3>Text Box</h3>
<div class="size-buttons">
<el-button @click="setTextSize('h1')" link size="small"
>H1</el-button
>
<el-button @click="setTextSize('h2')" link size="small"
>H2</el-button
>
<el-button @click="setTextSize('h3')" link size="small"
>H3</el-button
>
<el-button @click="setTextSize('h4')" link size="small"
>H4</el-button
>
<el-button @click="setTextSize('h5')" link size="small"
>H5</el-button
>
<el-button @click="setTextSize('h6')" link size="small"
>H6</el-button
>
</div>
</div> -->
<el-button type="primary" @click="addText" class="add-text-btn">
Add Text
</el-button>
<div class="tool-section">
<h3>
<el-icon
title="Enter the x and y coordinates respectively and click move to adjust the position of the element in the canvas"
><InfoFilled
/></el-icon>
Set Coordinates
</h3>
<div>
X:
<el-input
v-model="positionX"
:title="positionX"
placeholder="X 坐标"
style="width: 60px"
clearable
/>
Y:
<el-input
v-model="positionY"
:title="positionY"
placeholder="Y 坐标"
style="width: 60px"
clearable
/>
<el-button
@click="moveElementToPosition"
type="primary"
style="margin-left: 5px"
>Move</el-button
>
</div>
</div>
<div class="tool-section">
<h3>
<el-icon
title="Only left-justified, center-justified, and right-justified text is supported"
><InfoFilled
/></el-icon>
Text Alignment
</h3>
<el-button @click="alignText('left')">Left</el-button>
<el-button @click="alignText('center')">Center</el-button>
<el-button @click="alignText('right')">Right</el-button>
</div>
</div>
<!-- <div class="tool-section">
<h3>画布设置</h3>
<div class="background-settings">
<el-upload
class="background-uploader"
:show-file-list="false"
:on-success="handleBackgroundSuccess"
:before-upload="beforeBackgroundUpload"
action="/api/upload"
>
<el-image
v-if="canvasBackground"
:src="canvasBackground"
class="background-preview"
/>
<el-icon v-else class="background-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="background-actions">
<el-button size="small" @click="resetBackground">恢复默认背景</el-button>
</div>
</div>
</div> -->
</div>
<!-- 右侧画布区域 -->
<div class="right-content">
<div class="canvas-area">
<div
ref="canvasRef"
class="canvas"
@dragover.prevent
@drop="onDrop"
@click="onCanvasClick"
:style="{
backgroundImage: `url(${canvasBackground})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
width: '100%',
height: '800px',
}"
>
<div
v-for="(element, index) in elements"
:key="index"
class="canvas-element"
:class="{ selected: selectedIndex === index }"
:style="getElementStyle(element)"
@click.stop="selectElement(index)"
@mousedown="startDragging($event, index)"
@mousemove="onDragging"
@mouseup="stopDragging"
@mouseleave="stopDragging"
>
<template v-if="element.type === 'image'">
<div class="element-wrapper">
<el-image :src="element.src" fit="contain" />
<div class="element-actions" v-if="selectedIndex === index">
<el-button
type="danger"
size="small"
circle
icon="Delete"
@click.stop="deleteElement(index)"
/>
<!-- 添加调整图层顺序的按钮 -->
<!-- <el-button
type="primary"
size="small"
circle
icon="ArrowUp"
@click.stop="moveLayerUp(index)"
:disabled="index === elements.length - 1"
/>
<el-button
type="primary"
size="small"
circle
icon="ArrowDown"
@click.stop="moveLayerDown(index)"
:disabled="index === 0"
/> -->
</div>
<div
v-if="selectedIndex === index"
class="resize-handle"
@mousedown.stop="startResizing($event, index)"
>
<el-icon><Rank /></el-icon>
</div>
</div>
</template>
<template v-else-if="element.type === 'text'">
<div class="element-wrapper">
<div
class="text-element"
:style="getTextStyle(element)"
@dblclick="startEditing(index)"
>
<el-input
v-if="editingIndex === index"
v-model="tempTextContent"
@blur="handleBlur(index)"
type="textarea"
autosize
/>
<span v-else>{{ element.content }}</span>
</div>
<div class="element-actions" v-if="selectedIndex === index">
<el-button
type="danger"
size="small"
circle
icon="Delete"
@click.stop="deleteElement(index)"
/>
<!-- 添加调整图层顺序的按钮 -->
<!-- <el-button
type="primary"
size="small"
circle
icon="ArrowUp"
@click.stop="moveLayerUp(index)"
:disabled="index === elements.length - 1"
/>
<el-button
type="primary"
size="small"
circle
icon="ArrowDown"
@click.stop="moveLayerDown(index)"
:disabled="index === 0"
/> -->
</div>
<div
v-if="selectedIndex === index"
class="resize-handle"
@mousedown.stop="startResizing($event, index)"
>
<el-icon><Rank /></el-icon>
</div>
</div>
</template>
</div>
</div>
</div>
<div class="canvas-actions">
<!-- 调整重叠图层的顺序 -->
<el-button
@click="moveLayerUp"
:disabled="isLayerAtTop()"
title="If two or more layers overlap, click move up to adjust the layer to the top and move down to adjust the layer to the bottom"
>Move Up</el-button
>
<el-button
@click="moveLayerDown"
:disabled="isLayerAtBottom()"
title="If two or more layers overlap, click move up to adjust the layer to the top and move down to adjust the layer to the bottom"
>Move Down</el-button
>
<!-- <el-button @click="undo">Undo</el-button>
<el-button @click="redo">Redo</el-button>
<el-button @click="reset">Reset</el-button> -->
<!-- <el-button type="primary" :disabled="!canGenerate">
Generate Certificate
</el-button> -->
<el-button
type="primary"
@click="generateImage"
:disabled="!canGenerate"
title="Click Generate Certificate to generate a.png image of all the elements in the canvas for easy viewing"
>
Generate Certificate
</el-button>
<el-button
@click="saveCurrentTemplate"
:disabled="!canGenerate"
title="Click save as template to save all the elements in the canvas as a new template, which is easy to use next time"
>Save As Template</el-button
>
</div>
</div>
</div>
</div>
<!-- 保存模板时上传封面图片----先注释 -->
<!-- <div>
<el-dialog v-model="showModal" title="Tips" width="500">
<el-form>
<el-form-item label="Template Name">
<el-input
v-model="templateName"
placeholder="Please enter a template name"
></el-input>
</el-form-item>
<el-form-item label="Thumbnail">
<el-upload
action="/upload"
:on-success="handleThumbnailUploadSuccess"
:show-file-list="false"
>
<el-button type="primary">Upload Thumbnail</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showModal = false">Cancel</el-button>
<el-button type="primary" @click="handleSaveTemplate"
>Confirm</el-button
>
</template>
</el-dialog>
</div> -->
</template>
<script setup>
import {
ref,
computed,
onMounted,
onBeforeUnmount,
nextTick,
watch,
} from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Picture as IconPicture } from "@element-plus/icons-vue";
import { Delete, Plus, Rank } from "@element-plus/icons-vue";
// import * as html2canvas from "html2canvas";
import html2canvas from "html2canvas"; // 从 node_modules 中引入
import {
getTemplatesDefault,
getTemplatesSave,
getTemplates,
getImageUpload,
getImages,
deleteTemplates,
deleteImages,
} from "@/api/poster/index";
import {
journalHeadImg,
bgImg,
bgImg2,
leftTopImg,
rightTopImg,
leftBottomImg,
rightBottomImg,
// journalHeadImg1,
share1,
share2,
share3,
share4,
share6,
// Round,
} from "@/views/sectionManagement/ts/img";
import { useRouter, useRoute } from "vue-router";
const route = useRoute();
const id = route.query.id;
// 预设模板数据
// const templates = ref([
// {
// name: "Editorial Board Example",
// logo: journalHeadImg, // 期刊模板封面图
// background: bgImg,
// elements: [
// // 先注释
// // {
// // type: 'image',
// // src: '/journal-bg.png', // 期刊背景图
// // isBackground: true,
// // style: {
// // left: '0',
// // top: '0',
// // // width: '100%',
// // // height: '100%',
// // zIndex: -1
// // }
// // },
// // 左上角图标
// {
// type: "image",
// src: leftTopImg,
// style: {
// left: "60px",
// top: "80px",
// // width: '80px',
// // height: '80px'
// width: "300px",
// height: "100px",
// },
// },
// // 右上角期刊图片
// {
// type: "image",
// src: rightTopImg,
// style: {
// // 这里先使用占位符,后续会替换为具体值
// left: "placeholderLeft",
// top: "80px",
// width: "300px",
// height: "100px",
// },
// },
// // 右上角期刊名
// // {
// // type: 'text',
// // content: '期刊名称',
// // style: {
// // right: '30px',
// // top: '200px',
// // width: '120px',
// // fontSize: '16px',
// // color: '#333333',
// // fontFamily: 'Microsoft YaHei',
// // textAlign: 'center'
// // }
// // },
// // 中间内容区域
// {
// type: "text",
// content: "在此输入正文内容",
// style: {
// left: "50%",
// top: "50%",
// transform: "translate(-50%, -50%)",
// // width: '60%',
// fontSize: "18px",
// color: "#333333",
// fontFamily: "Microsoft YaHei",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// // 左下角电子签名
// {
// type: "image",
// src: leftBottomImg,
// style: {
// left: "60px",
// // 这里先使用占位符,后续会替换为具体值
// top: "placeholderTop",
// // left: "60px",
// // top: "680px", // 800 - 120 // 假设画布高度为 800px
// // width: '150px',
// // height: '60px'
// width: "300px",
// height: "100px",
// },
// },
// // 右下角logo
// {
// type: "image",
// src: rightBottomImg,
// style: {
// // 这里先使用占位符,后续会替换为具体值
// left: "placeholderLeft",
// // 这里先使用占位符,后续会替换为具体值
// top: "placeholderTop",
// width: "150px",
// height: "140px",
// },
// },
// ],
// },
// {
// name: "Reviewer Example",
// logo: journalHeadImg,
// background: bgImg,
// elements: [
// {
// type: "image",
// src: leftTopImg,
// style: {
// // left: "0px",
// // top: "0px",
// // width: "100%",
// left: "60px",
// top: "80px",
// // width: '80px',
// // height: '80px'
// width: "300px",
// height: "100px",
// },
// },
// {
// type: "text",
// content: "荣誉证书",
// style: {
// left: "50%",
// top: "30%",
// transform: "translate(-50%, -50%)",
// fontSize: "36px",
// color: "#000000",
// fontFamily: "Microsoft YaHei",
// fontWeight: "bold",
// },
// },
// {
// type: "text",
// content: "兹证明\n[姓名]\n在[项目名称]中表现优异\n特发此证",
// style: {
// left: "50%",
// top: "50%",
// transform: "translate(-50%, -50%)",
// fontSize: "24px",
// color: "#333333",
// fontFamily: "Microsoft YaHei",
// textAlign: "center",
// whiteSpace: "pre-wrap",
// },
// },
// ],
// },
// {
// name: "Guest Editor Example",
// logo: journalHeadImg, // 海报模板封面图
// background: bgImg2,
// elements: [
// // 左上角
// {
// type: "image",
// src: rightTopImg,
// style: {
// left: "60px",
// top: "80px",
// width: "300px",
// height: "100px",
// },
// },
// // 右上角
// {
// type: "image",
// src: Round,
// style: {
// // 这里先使用占位符,后续会替换为具体值
// left: "placeholderLeft",
// top: "80px",
// width: "100px",
// height: "100px",
// },
// },
// {
// type: "text",
// content: "CALL FOR PAPERS",
// style: {
// left: "50%",
// top: "20%",
// transform: "translate(-50%, -50%)",
// fontSize: "32px",
// color: "#000",
// fontFamily: "Microsoft YaHei",
// fontWeight: "bold",
// },
// },
// ],
// },
// {
// name: "Acceptance Example",
// logo: journalHeadImg, // 海报模板封面图
// background: journalHeadImg1,
// elements: [
// {
// type: "image",
// src: leftTopImg,
// style: {
// left: "60px",
// top: "80px",
// // width: '80px',
// // height: '80px'
// width: "300px",
// height: "100px",
// },
// },
// {
// type: "text",
// content: "活动主题22",
// style: {
// left: "50%",
// top: "20%",
// transform: "translate(-50%, -50%)",
// fontSize: "32px",
// color: "#000",
// fontFamily: "Microsoft YaHei",
// fontWeight: "bold",
// },
// },
// ],
// },
// ]);
const templates = ref([
{
name: "SI Proposal Example",
logo: journalHeadImg, // 期刊模板封面图
background: bgImg2,
// elements: [
// // 左上角 ======================
// {
// type: "image",
// src: leftTopImg,
// style: {
// left: "60px",
// top: "80px",
// width: "300px",
// height: "100px",
// },
// },
// // 右上角 ======================
// {
// type: "image",
// src: rightTopImg,
// style: {
// left: "placeholderLeft",
// top: "80px",
// width: "100px",
// height: "100px",
// },
// },
// {
// type: "image",
// src: rightTopImg,
// style: {
// left: "placeholderLeftPlus100",
// top: "80px",
// width: "100px",
// height: "100px",
// },
// },
// // {
// // type: "text",
// // content: "2023\nIMPACT\nFACTOR",
// // style: {
// // left: "placeholderLeft",
// // top: "120px",
// // transform: "translate(-40%, -50%)",
// // fontSize: "12px",
// // color: "",
// // fontWeight: "",
// // textAlign: "left",
// // lineHeight: "1.8",
// // whiteSpace: "pre-wrap",
// // },
// // },
// // 中间 ======================
// {
// type: "text",
// content: "CALL FOR PAPERS",
// style: {
// left: "50%",
// top: "20%",
// transform: "translate(-50%, -50%)",
// fontSize: "40px",
// color: "#36767d",
// fontWeight: "bold",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// {
// type: "text",
// content:
// "SI: Renewable Energy Commnuity(REC) Engineering towards\n Sustainable Development and Energy Poverty Reduction",
// style: {
// left: "50%",
// top: "30%",
// transform: "translate(-50%, -50%)",
// fontSize: "24px",
// color: "#000",
// fontWeight: "bold",
// textAlign: "center",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// {
// type: "text",
// content: "ISSN: 1546-2226(online)",
// style: {
// left: "70px",
// top: "160px",
// fontSize: "16px",
// color: "#000",
// fontWeight: "",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// {
// type: "text",
// content: "ISSN: 1546-2218(print)",
// style: {
// left: "70px",
// top: "180px",
// fontSize: "16px",
// color: "#000",
// fontWeight: "",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// {
// type: "text",
// content: "Guest Editors:",
// style: {
// left: "70px",
// top: "350px",
// fontSize: "20px",
// color: "#36767d",
// fontWeight: "bold",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// {
// type: "text",
// content: "Dr. Mariacristina Roscia, University of Bergamo",
// style: {
// left: "70px",
// top: "380px",
// fontSize: "20px",
// color: "#000",
// fontWeight: "",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// {
// type: "text",
// content: "Dr. Korhan Kayisli, Gazi University",
// style: {
// left: "70px",
// top: "410px",
// fontSize: "20px",
// color: "#000",
// fontWeight: "",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// {
// type: "text",
// content: "Submission Deadline:",
// style: {
// left: "70px",
// top: "440px",
// fontSize: "20px",
// color: "#36767d",
// fontWeight: "bold",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// {
// type: "text",
// content: "01 October 2025",
// style: {
// left: "70px",
// top: "470px",
// fontSize: "20px",
// color: "#000",
// fontWeight: "",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// // 左下角 ======================
// {
// type: "text",
// content: "https://www.techscience.com/journal/CMC",
// style: {
// left: "60px",
// top: "750px",
// fontSize: "20px",
// color: "#FFF",
// fontWeight: "",
// textAlign: "left",
// lineHeight: "1.8",
// whiteSpace: "pre-wrap",
// },
// },
// // 右下角 ======================
// // {
// // type: "image",
// // src: rightBottomImg,
// // style: {
// // left: "placeholderLeft",
// // top: "placeholderTop",
// // width: "150px",
// // height: "140px",
// // },
// // },
// ],
elements: "",
},
]);
// 图片库数据
const images = ref([
{ src: share1 },
{ src: share2 },
{ src: share3 },
{ src: share4 },
{ src: share6 },
{ src: leftTopImg },
{ src: leftBottomImg },
{ src: rightBottomImg },
{ src: rightTopImg },
]);
// 文本样式
const textStyle = ref({
fontSize: 16,
color: "#000000",
fontFamily: "Microsoft YaHei",
bold: false,
italic: false,
});
// 画布元素
const elements = ref([]);
const selectedIndex = ref(-1);
const editingIndex = ref(-1);
const canvasRef = ref(null);
const textInput = ref(null);
const isDragging = ref(false);
const dragStartPos = ref({ x: 0, y: 0 });
const elementStartPos = ref({ x: 0, y: 0 });
const draggingIndex = ref(-1);
// 判断是否可以生成证书
const canGenerate = computed(() => {
return elements.value.length > 0;
});
// 应用模板
const applyTemplate = (template) => {
// console.log("应用", template.elements);
elements.value = template.elements ? JSON.parse(template.elements) : [];
canvasBackground.value = template.background;
ElMessage.success("Template application successful");
};
const deleteItem = (index, row) => {
ElMessageBox.confirm(
"Are you sure you want to delete this template?",
"Tips",
{
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
type: "warning",
}
)
.then(() => {
// templates.value.splice(index, 1);
if (row.isDefault)
return ElMessage({
type: "error",
message: "The default template cannot be deleted",
});
deleteTemplates({
id: row.id,
})
.then((res) => {
if (res.code == 2000) {
ElMessage({
type: "success",
message: res.message,
});
getTempDatas();
} else {
ElMessage.error(res.message);
}
})
.catch((err) => {
ElMessage({
type: "error",
message: err.message,
});
});
})
.catch(() => {
ElMessage({
type: "info",
message: "Canceled",
});
});
};
const deleteImgItem = (row) => {
ElMessageBox.confirm(
"Are you sure you want to delete this picture?",
"Tips",
{
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
type: "warning",
}
)
.then(() => {
deleteImages({
id: row.id,
})
.then((res) => {
if (res.code == 2000) {
ElMessage({
type: "success",
message: res.message,
});
getImageDatas();
} else {
ElMessage.error(res.message);
}
})
.catch((err) => {
ElMessage({
type: "error",
message: err.message,
});
});
})
.catch(() => {
ElMessage({
type: "info",
message: "Canceled",
});
});
};
// 删除元素
const deleteElement = (index) => {
elements.value.splice(index, 1);
selectedIndex.value = -1;
editingIndex.value = -1;
};
// 拖拽相关
const onImageDragStart = (event, image) => {
event.dataTransfer.setData(
"application/json",
JSON.stringify({
type: "image",
src: image.src,
})
);
};
const onDrop = (event) => {
event.preventDefault();
const rect = canvasRef.value.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
try {
const data = event.dataTransfer.getData("application/json");
if (!data) return;
const parsedData = JSON.parse(data);
if (parsedData.type === "image") {
elements.value.push({
type: "image",
src: parsedData.src,
style: {
left: `${x}px`,
top: `${y}px`,
width: "200px",
},
});
}
} catch (error) {
console.log("Drop error:", error);
}
};
// 开始拖拽元素
// const startDragging = (event, index) => {
// // 只有在非编辑状态下才允许拖拽
// if (editingIndex.value === -1) {
// isDragging.value = true;
// draggingIndex.value = index;
// dragStartPos.value = {
// x: event.clientX,
// y: event.clientY,
// };
// const element = elements.value[index];
// const left = element.style.left;
// const top = element.style.top;
// // 处理百分比位置转换为像素
// if (left && left.includes("%")) {
// const canvasWidth = canvasRef.value.offsetWidth;
// const leftPercent = parseFloat(left);
// elementStartPos.value.x = (canvasWidth * leftPercent) / 100;
// } else {
// elementStartPos.value.x = parseFloat(left);
// }
// if (top && top.includes("%")) {
// const canvasHeight = canvasRef.value.offsetHeight;
// const topPercent = parseFloat(top);
// elementStartPos.value.y = (canvasHeight * topPercent) / 100;
// } else {
// elementStartPos.value.y = parseFloat(top);
// }
// // 处理transform偏移
// if (
// element.style.transform &&
// element.style.transform.includes("translate")
// ) {
// const el = event.currentTarget;
// const rect = el.getBoundingClientRect();
// elementStartPos.value = {
// x: rect.left - canvasRef.value.getBoundingClientRect().left,
// y: rect.top - canvasRef.value.getBoundingClientRect().top,
// };
// }
// }
// };
// 在拖拽图片时出现卡顿的情况,可能是由于以下几个原因造成的:
// 图片加载问题:图片加载需要时间,如果图片较大或者网络较慢,可能会导致拖拽卡顿。
// 计算量过大:在拖拽过程中,可能存在大量的计算,如位置计算、边界检查等,这些计算可能会影响性能。
// DOM 操作频繁:频繁的 DOM 操作会导致浏览器重排和重绘,从而影响性能。
const startDragging = (event, index) => {
if (editingIndex.value === -1) {
isDragging.value = true;
draggingIndex.value = index;
dragStartPos.value = {
x: event.clientX,
y: event.clientY,
};
const element = elements.value[index];
const left = element.style.left;
const top = element.style.top;
// 当前代码在处理拖拽时,只考虑了 left 和 top 属性,而没有对 bottom 和 right 属性进行相应的处理。
// 当元素使用 bottom 和 right 定位时,计算元素的位置和偏移量的逻辑就会出错,从而导致拖拽失效。
// 为了解决这个问题,我们需要对 startDragging 和 onDragging 函数进行修改,使其能够同时处理 left、top、bottom 和 right 属性。具体步骤如下:
// 在 startDragging 函数中,获取元素的 left、top、bottom 和 right 属性,并将其转换为像素值。
// 在 onDragging 函数中,根据元素的定位属性(left、top、bottom 或 right)来更新元素的位置。
const right = element.style.right;
const bottom = element.style.bottom;
// 处理百分比位置转换为像素
const canvasWidth = canvasRef.value.offsetWidth;
const canvasHeight = canvasRef.value.offsetHeight;
if (left?.includes("%")) {
const leftPercent = parseFloat(left);
elementStartPos.value.x = (canvasWidth * leftPercent) / 100;
} else if (right?.includes("%")) {
const rightPercent = parseFloat(right);
elementStartPos.value.x =
canvasWidth - (canvasWidth * rightPercent) / 100;
} else {
elementStartPos.value.x = left
? parseFloat(left)
: canvasWidth - (right ? parseFloat(right) : 0);
}
if (top?.includes("%")) {
const topPercent = parseFloat(top);
elementStartPos.value.y = (canvasHeight * topPercent) / 100;
} else if (bottom?.includes("%")) {
const bottomPercent = parseFloat(bottom);
elementStartPos.value.y =
canvasHeight - (canvasHeight * bottomPercent) / 100;
} else {
elementStartPos.value.y = top
? parseFloat(top)
: canvasHeight - (bottom ? parseFloat(bottom) : 0);
}
// 处理transform偏移
if (
element.style.transform &&
element.style.transform.includes("translate")
) {
const el = event.currentTarget;
const rect = el.getBoundingClientRect();
elementStartPos.value = {
x: rect.left - canvasRef.value.getBoundingClientRect().left,
y: rect.top - canvasRef.value.getBoundingClientRect().top,
};
}
// 缓存 DOM 元素
const canvas = canvasRef.value;
const elementRef = document.querySelector(
`.canvas-element:nth-child(${index + 1})`
);
// 在 onDragging 中使用缓存的 DOM 元素
const onDragging = (event) => {
if (!isDragging.value) return;
const dx = event.clientX - dragStartPos.value.x;
const dy = event.clientY - dragStartPos.value.y;
const element = elements.value[draggingIndex.value];
let newX = elementStartPos.value.x + dx;
let newY = elementStartPos.value.y + dy;
// 缓存画布和元素的尺寸
const canvasRect = canvas.getBoundingClientRect();
const elementRect = elementRef.getBoundingClientRect();
const canvasWidth = canvasRect.width;
const canvasHeight = canvasRect.height;
const elementWidth = elementRect.width;
const elementHeight = elementRect.height;
// 限制范围
newX = Math.max(0, Math.min(newX, canvasWidth - elementWidth));
newY = Math.max(0, Math.min(newY, canvasHeight - elementHeight));
// 统一使用 left 和 top 更新位置
element.style.left = `${newX}px`;
element.style.top = `${newY}px`;
// 移除transform,避免位置计算问题
element.style.transform = "";
// 移除可能导致计算偏差的 right 和 bottom 属性
element.style.right = "";
element.style.bottom = "";
};
}
};
const resetConfig = () => {
textStyle.value.fontSize = 16;
textStyle.value.bold = false;
textStyle.value.italic = false;
};
// 文本相关
const addText = () => {
const canvasRect = canvasRef.value.getBoundingClientRect();
const centerX = canvasRect.width / 2;
const centerY = canvasRect.height / 2;
resetConfig();
// elements.value.push({
// type: 'text',
// content: '双击编辑文本',
// style: {
// position: 'absolute',
// left: `${centerX}px`,
// top: `${centerY}px`,
// transform: 'translate(-50%, -50%)',
// fontSize: `${textStyle.value.fontSize}px`,
// color: textStyle.value.color,
// fontFamily: textStyle.value.fontFamily,
// fontWeight: textStyle.value.bold ? 'bold' : 'normal',
// fontStyle: textStyle.value.italic ? 'italic' : 'normal',
// whiteSpace: 'pre-wrap',
// wordBreak: 'break-word',
// maxWidth: '500px'
// }
// })
console.log("elements", elements.value);
elements.value.push({
type: "text",
content: "Double-click to edit text",
style: {
left: `${centerX}px`,
top: `${centerY}px`,
fontSize: "16px",
color: "#000000",
fontFamily: "Microsoft YaHei",
fontWeight: "normal",
fontStyle: "normal",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
maxWidth: "800px",
},
});
// saveState(); // 保存状态
};
// 修改拖拽中的处理
const onDragging = (event) => {
if (!isDragging.value) return;
const dx = event.clientX - dragStartPos.value.x;
const dy = event.clientY - dragStartPos.value.y;
const element = elements.value[draggingIndex.value];
// 确保元素不会拖出画布
const canvasRect = canvasRef.value.getBoundingClientRect();
const elementRect = event.target.getBoundingClientRect();
let newX = elementStartPos.value.x + dx;
let newY = elementStartPos.value.y + dy;
// 限制范围
newX = Math.max(0, Math.min(newX, canvasRect.width - elementRect.width));
newY = Math.max(0, Math.min(newY, canvasRect.height - elementRect.height));
element.style.left = `${newX}px`;
element.style.top = `${newY}px`;
// 移除transform,避免位置计算问题
element.style.transform = "";
checkOverlappingLayers();
};
// 停止拖拽
const stopDragging = () => {
isDragging.value = false;
draggingIndex.value = -1;
};
const textElementRefs = ref([]);
const savedRange = ref(null);
const tempTextContent = ref(""); // 临时变量用于存储编辑内容
// 文本相关
const startEditing = (index) => {
editingIndex.value = index;
tempTextContent.value = elements.value[index].content; // 初始化临时变量
};
const stopEditing = () => {
editingIndex.value = -1;
// 保存当前选区
const sel = window.getSelection();
if (sel.rangeCount > 0) {
savedRange.value = sel.getRangeAt(0);
}
};
const updateTextContent = (index, target) => {
// 保存当前选区
const sel = window.getSelection();
if (sel.rangeCount > 0) {
savedRange.value = sel.getRangeAt(0);
}
const newContent = target.textContent;
elements.value[index].content = newContent;
// 恢复选区
if (savedRange.value) {
sel.removeAllRanges();
sel.addRange(savedRange.value);
}
};
// 元素样式
const getElementStyle = (element) => {
return {
position: "absolute",
...element.style,
};
};
const getTextStyle = (element) => {
return {
fontSize: `${element.style.fontSize}px`,
color: element.style.color,
fontFamily: element.style.fontFamily,
fontWeight: element.style.fontWeight,
fontStyle: element.style.fontStyle,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
maxWidth: "800px",
};
};
// 选择元素
const selectElement = (index) => {
selectedIndex.value = index;
};
const onCanvasClick = () => {
selectedIndex.value = -1;
};
// 更新文本样式// 更新文本样式
const updateTextStyle = () => {
if (
selectedIndex.value !== -1 &&
elements.value[selectedIndex.value].type === "text"
) {
elements.value[selectedIndex.value].style = {
...elements.value[selectedIndex.value].style,
// 使用当前 textStyle.value.fontSize 更新元素的 fontSize
fontSize: `${textStyle.value.fontSize}px`,
color: textStyle.value.color,
fontFamily: textStyle.value.fontFamily,
fontWeight: textStyle.value.bold ? "bold" : "normal",
fontStyle: textStyle.value.italic ? "italic" : "normal",
};
// 强制更新文本元素的样式
const textElement = document.querySelector(
`.canvas-element:nth-child(${selectedIndex.value + 1}) .text-element`
);
if (textElement) {
textElement.style.fontSize = `${textStyle.value.fontSize}px`;
}
const textSpan = textElement.querySelector("span");
if (textSpan) {
textSpan.style.fontSize = `${textStyle.value.fontSize}px`;
}
const textInput = textElement.querySelector("el-input");
if (textInput) {
textInput.style.fontSize = `${textStyle.value.fontSize}px`;
}
}
};
// 设置文本大小
const setTextSize = (size) => {
if (
selectedIndex.value !== -1 &&
elements.value[selectedIndex.value].type === "text"
) {
const newSize =
size === "h1"
? 32
: size === "h2"
? 24
: size === "h3"
? 18
: size === "h4"
? 16
: size === "h5"
? 14
: 12;
// 更新 element.style 中的 fontSize
elements.value[selectedIndex.value].style.fontSize = `${newSize}px`;
// 更新 textStyle.value.fontSize
textStyle.value.fontSize = newSize;
// 强制更新文本元素的样式
const textElement = document.querySelector(
`.canvas-element:nth-child(${selectedIndex.value + 1}) .text-element`
);
if (textElement) {
textElement.style.fontSize = `${newSize}px`;
}
const textSpan = textElement.querySelector("span");
if (textSpan) {
textSpan.style.fontSize = `${newSize}px`;
}
const textInput = textElement.querySelector("el-input");
if (textInput) {
textInput.style.fontSize = `${newSize}px`;
}
}
};
// 生成图片
const generateImage = async () => {
try {
const canvas = await html2canvas(canvasRef.value, {
useCORS: true, // 允许跨域图片
allowTaint: true, // 允许跨域图片
scale: 2, // 提高输出质量
logging: false,
imageTimeout: 0, // 禁用图片加载超时
onclone: (clonedDoc) => {
// 获取克隆的画布元素
const clonedCanvas = clonedDoc.querySelector(".canvas");
if (clonedCanvas) {
// 处理克隆画布中的所有图片元素
const images = clonedCanvas.querySelectorAll(".el-image__inner");
images.forEach((img) => {
// 移除可能影响图片显示的样式限制
img.style.maxWidth = "none";
img.style.maxHeight = "none";
img.style.width = img.parentElement.style.width;
img.style.height = img.parentElement.style.height;
img.style.objectFit = "contain";
});
}
},
});
// 创建下载链接
const link = document.createElement("a");
link.download = "certificate.png";
link.href = canvas.toDataURL("image/png", 1.0);
link.click();
ElMessage.success("successfully");
} catch (error) {
ElMessage.error("failed");
}
};
// 背景图相关
const DEFAULT_BACKGROUND = bgImg;
// const canvasBackground = ref(DEFAULT_BACKGROUND);
const canvasBackground = ref(
templates.value[0].background || DEFAULT_BACKGROUND
);
const handleBackgroundSuccess = (response) => {
canvasBackground.value = response.url;
ElMessage.success("背景图更新成功");
};
const beforeBackgroundUpload = (file) => {
const isImage = file.type.startsWith("image/");
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) {
ElMessage.error("只能上传图片文件!");
return false;
}
if (!isLt2M) {
ElMessage.error("图片大小不能超过 2MB!");
return false;
}
return true;
};
const resetBackground = () => {
canvasBackground.value = DEFAULT_BACKGROUND;
ElMessage.success("已恢复默认背景");
};
// 调整大小相关状态
const isResizing = ref(false);
const resizingIndex = ref(-1);
const initialSize = ref({ width: 0, height: 0 });
const initialMousePos = ref({ x: 0, y: 0 });
const startResizing = (event, index) => {
event.preventDefault();
isResizing.value = true;
resizingIndex.value = index;
const element = elements.value[index];
initialSize.value = {
width: parseFloat(element.style.width),
height: parseFloat(element.style.height),
};
initialMousePos.value = {
x: event.clientX,
y: event.clientY,
};
if (element.type === "text") {
// 确保正确记录初始字体大小
initialFontSize.value = parseFloat(element.style.fontSize) || 16;
// 同步 textStyle 中的 fontSize
textStyle.value.fontSize = initialFontSize.value;
}
document.addEventListener("mousemove", onResizing);
document.addEventListener("mouseup", stopResizing);
};
const initialFontSize = ref(0);
// 添加一个标志变量,用于记录是否已经提示过最大字号限制
const hasReachedMaxSize = ref(false);
const onResizing = (event) => {
if (!isResizing.value) return;
const deltaX = event.clientX - initialMousePos.value.x;
const deltaY = event.clientY - initialMousePos.value.y;
const element = elements.value[resizingIndex.value];
if (element.type === "image") {
let newWidth = Math.max(50, initialSize.value.width + deltaX);
let newHeight = Math.max(50, initialSize.value.height + deltaY);
const canvas = canvasRef.value;
const canvasRect = canvas.getBoundingClientRect();
const canvasWidth = canvasRect.width;
const canvasHeight = canvasRect.height;
const elementLeft = parseFloat(element.style.left);
const elementTop = parseFloat(element.style.top);
newWidth = Math.min(newWidth, canvasWidth - elementLeft);
newHeight = Math.min(newHeight, canvasHeight - elementTop);
element.style = {
...element.style,
width: `${newWidth}px`,
height: `${newHeight}px`,
};
const imgElement = document.querySelector(
`.canvas-element:nth-child(${resizingIndex.value + 1}) .el-image__inner`
);
if (imgElement) {
imgElement.style.width = `${newWidth}px`;
imgElement.style.height = `${newHeight}px`;
}
} else if (element.type === "text") {
// 计算字体大小的变化量,这里简单以 deltaX 为例,你可以根据需要调整
const fontSizeChange = deltaX * 0.1;
let newFontSize = Math.max(12, initialFontSize.value + fontSizeChange);
// 取整操作
newFontSize = Math.round(newFontSize);
// 检查字体大小是否超过 72
// if (newFontSize > 72) {
// newFontSize = 72;
// ElMessage.warning("最大字号为 72,不能继续缩放了");
// }
// 检查字体大小是否超过 72
if (newFontSize > 72) {
newFontSize = 72;
if (!hasReachedMaxSize.value) {
ElMessage.warning("最大字号只能为 72,不能继续缩放了");
hasReachedMaxSize.value = true; // 设置标志为已提示
}
} else {
hasReachedMaxSize.value = false; // 当字体大小小于 72 时,重置标志
}
element.style = {
...element.style,
fontSize: `${newFontSize}px`,
};
const textElement = document.querySelector(
`.canvas-element:nth-child(${resizingIndex.value + 1}) .text-element`
);
if (textElement) {
textElement.style.fontSize = `${newFontSize}px`;
}
// 更新文本元素的实际内容样式
const textSpan = textElement.querySelector("span");
if (textSpan) {
textSpan.style.fontSize = `${newFontSize}px`;
}
const textInput = textElement.querySelector("el-input");
if (textInput) {
textInput.style.fontSize = `${newFontSize}px`;
}
// 同步更新 textStyle 中的 fontSize
textStyle.value.fontSize = newFontSize;
}
};
const stopResizing = () => {
isResizing.value = false;
resizingIndex.value = -1;
document.removeEventListener("mousemove", onResizing);
document.removeEventListener("mouseup", stopResizing);
};
const handleUploadSuccess = (response) => {
// 处理上传成功的逻辑
};
const positionX = ref(100); // 用于存储 X 坐标
const positionY = ref(200); // 用于存储 Y 坐标
// // 根据坐标x,y更新图片,文本位置
// const moveElementToPosition = () => {
// // 检查是否有选中的元素
// if (selectedIndex.value === -1) {
// ElMessage.error("请先选中一个元素");
// return;
// }
// const x = parseFloat(positionX.value); // 获取输入的 X 坐标
// const y = parseFloat(positionY.value); // 获取输入的 Y 坐标
// // 验证输入的坐标是否有效
// if (!isNaN(x) && !isNaN(y) && selectedIndex.value !== -1) {
// const element = elements.value[selectedIndex.value];
// element.style.left = `${x}px`; // 更新 X 位置
// element.style.top = `${y}px`; // 更新 Y 位置
// } else {
// ElMessage.error("请输入有效的坐标"); // 提示用户输入有效坐标
// }
// };
// 优化:如果坐标有效且在范围内,则更新元素的位置;否则,提示用户输入的坐标超出了画布范围。=======================================
const moveElementToPosition = () => {
// 检查是否有选中的元素
if (selectedIndex.value === -1) {
ElMessage.error("请先选中一个元素");
return;
}
const x = parseFloat(positionX.value); // 获取输入的 X 坐标
const y = parseFloat(positionY.value); // 获取输入的 Y 坐标
// 获取画布的宽度和高度
const canvas = canvasRef.value;
const canvasRect = canvas.getBoundingClientRect();
const canvasWidth = canvasRect.width;
const canvasHeight = canvasRect.height;
// 获取选中元素的宽度和高度
const element = elements.value[selectedIndex.value];
const elementRef = document.querySelector(
`.canvas-element:nth-child(${selectedIndex.value + 1})`
);
const elementRect = elementRef.getBoundingClientRect();
const elementWidth = elementRect.width;
const elementHeight = elementRect.height;
// 验证输入的坐标是否有效且在画布范围内
if (!isNaN(x) && !isNaN(y) && selectedIndex.value !== -1) {
if (
x >= 0 &&
x + elementWidth <= canvasWidth &&
y >= 0 &&
y + elementHeight <= canvasHeight
) {
element.style.left = `${x}px`; // 更新 X 位置
element.style.top = `${y}px`; // 更新 Y 位置
} else {
ElMessage.error("该坐标超出了画布范围,请重新输入");
return;
}
} else {
ElMessage.error("请输入有效的坐标"); // 提示用户输入有效坐标
}
};
// const alignText = (alignment) => {
// if (selectedIndex.value !== -1) {
// const element = elements.value[selectedIndex.value];
// const canvasWidth = canvasRef.value.offsetWidth; // 获取画布宽度
// const textWidth = calculateTextWidth(element.content); // 计算文本宽度
// // 根据对齐方式设置文本位置
// if (alignment === "left") {
// element.style.left = "0"; // 靠左
// } else if (alignment === "center") {
// element.style.left = `${(canvasWidth - textWidth) / 2}px`; // 居中
// } else if (alignment === "right") {
// element.style.left = `${canvasWidth - textWidth - 100}px`; // 靠右,设置右边距
// }
// element.style.textAlign = alignment; // 更新文本对齐方式
// }
// };
// 优化位置计算: =========================================================================================
// 左对齐:设置 left 为 0,right 为 auto。
// 居中对齐:计算文本宽度与画布宽度的差值,然后将差值的一半作为 left 值,right 为 auto。
// 右对齐:设置 right 为 0,left 为 auto,避免文本换行。
const alignText = (alignment) => {
if (selectedIndex.value !== -1) {
const element = elements.value[selectedIndex.value];
if (element.type === "text") {
const canvas = canvasRef.value;
const canvasRect = canvas.getBoundingClientRect();
const textElement = document.createElement("span");
textElement.textContent = element.content;
textElement.style.fontSize = element.style.fontSize;
textElement.style.fontFamily = element.style.fontFamily;
textElement.style.fontWeight = element.style.fontWeight;
textElement.style.fontStyle = element.style.fontStyle;
textElement.style.whiteSpace = "pre-wrap";
textElement.style.wordBreak = "break-word";
textElement.style.maxWidth = "800px";
document.body.appendChild(textElement);
const textWidth = textElement.offsetWidth;
document.body.removeChild(textElement);
if (alignment === "left") {
element.style.left = "0";
} else if (alignment === "center") {
element.style.left = `${(canvasRect.width - textWidth) / 2}px`;
} else if (alignment === "right") {
element.style.left = `${canvasRect.width - textWidth}px`;
}
element.style.textAlign = alignment;
// 移除可能导致计算偏差的 right 和 bottom 属性
element.style.right = "";
element.style.bottom = "";
} else {
return ElMessage.error("仅支持文本对齐");
}
}
};
// 计算文本宽度的辅助函数
const calculateTextWidth = (content) => {
if (!Array.isArray(content)) {
console.error("Content is not an array:", content);
return 0; // 返回 0 或其他默认值
}
// 假设每个字符的平均宽度为 8px,您可以根据实际字体和大小进行调整
const averageCharWidth = 8;
const totalWidth = content.reduce((acc, char) => {
if (typeof char.text === "string") {
return acc + char.text.length * averageCharWidth; // 计算宽度
}
return acc; // 如果不是字符串,保持累加值不变
}, 0);
return totalWidth;
};
// const handleKeyDown = (event) => {
// console.log("event.target.tagName ", event.target.tagName);
// // 允许输入框的输入 // (event.preventDefault会把输入框的输入事件也阻止了,需要排除掉)
// if (event.target.tagName == "INPUT" || event.target.tagName == "TEXTAREA") {
// return; // 如果是输入框,直接返回
// }
// // 阻止默认行为,防止页面滚动(加这个是为了只想操作画布中的元素,而不是这个屏幕的)
// event.preventDefault();
// if (selectedIndex.value !== -1) {
// const element = elements.value[selectedIndex.value];
// const step = 5; // 每次移动的像素步长
// switch (event.key) {
// case "ArrowUp":
// element.style.top = `${parseFloat(element.style.top) - step}px`; // 向上移动
// break;
// case "ArrowDown":
// element.style.top = `${parseFloat(element.style.top) + step}px`; // 向下移动
// break;
// case "ArrowLeft":
// element.style.left = `${parseFloat(element.style.left) - step}px`; // 向左移动
// break;
// case "ArrowRight":
// element.style.left = `${parseFloat(element.style.left) + step}px`; // 向右移动
// break;
// }
// }
// };
// 优化上下左右箭头边界判断的准确性,避免移动到画布边缘时出现误判的问题
const handleKeyDown = (event) => {
// console.log("event.target.tagName ", event.target.tagName);
// 允许输入框的输入 (event.preventDefault会把输入框的输入事件也阻止了,需要排除掉)
if (
event.target.tagName === "INPUT" ||
event.target.tagName === "TEXTAREA" ||
event.target.tagName === "DIV"
) {
return; // 如果是输入框,直接返回
}
// 阻止默认行为,防止页面滚动(加这个是为了只想操作画布中的元素,而不是这个屏幕的)
event.preventDefault();
if (selectedIndex.value !== -1) {
const element = elements.value[selectedIndex.value];
const step = 5; // 每次移动的像素步长
const canvas = canvasRef.value;
const canvasRect = canvas.getBoundingClientRect();
const elementRef = document.querySelector(
`.canvas-element:nth-child(${selectedIndex.value + 1})`
);
const elementRect = elementRef.getBoundingClientRect();
let newLeft = parseFloat(element.style.left);
let newTop = parseFloat(element.style.top);
switch (event.key) {
case "ArrowUp":
newTop = newTop - step;
if (newTop < 0) {
ElMessage.error("元素不能移动到画布上方以外");
return;
}
break;
case "ArrowDown":
newTop = newTop + step;
if (newTop + elementRect.height > canvasRect.height) {
ElMessage.error("元素不能移动到画布下方以外");
return;
}
break;
case "ArrowLeft":
newLeft = newLeft - step;
if (newLeft < 0) {
ElMessage.error("元素不能移动到画布左方以外");
return;
}
break;
case "ArrowRight":
newLeft = newLeft + step;
if (newLeft + elementRect.width > canvasRect.width) {
ElMessage.error("元素不能移动到画布右方以外");
return;
}
break;
}
element.style.left = `${newLeft}px`;
element.style.top = `${newTop}px`;
}
};
// 1. 在拖拽图片时出现卡顿的情况=============》预加载图片
// 在数据初始化时预加载图片
const preloadImages = () => {
const allImages = [
...images.value.map((img) => img.src),
...templates.value.flatMap(
(template) =>
template.elements &&
template.elements
.filter((el) => el.type === "image")
.map((el) => el.src)
),
canvasBackground.value,
];
allImages.forEach((src) => {
const img = new Image();
img.src = src;
});
};
const getImageDatas = async () => {
// images.value = [];
// 获取图片列表
await getImages().then((res) => {
if (res.code == 2000) {
if (res.data && res.data.data) {
images.value = res.data.data.custom;
}
}
});
};
const getTempDatas = async () => {
templates.value = [];
// 获取默认模版数据、获取自定义模版列表
await Promise.all([getTemplatesDefault({ sid: id }), getTemplates()])
.then(([res1, res2]) => {
const newDatas = [...res1.data.data, ...res2.data.data];
for (const item of newDatas) {
templates.value.push({
name: item.name,
// logo: item.logo,
logo:
newDatas && newDatas.length && newDatas[0]
? newDatas[0].logo
: "/api/file/download/logo/CMC.png",
background: item.background,
elements: item.elements,
id: item.id,
isDefault: item.isDefault,
});
}
})
.catch((error) => {});
};
const getDataList = async () => {
getImageDatas();
getTempDatas();
};
onMounted(async () => {
window.addEventListener("keydown", handleKeyDown); // 添加键盘事件监听
await getDataList();
// 初始化文本元素引用
elements.value.forEach((_, index) => {
textElementRefs.value[index] = null;
});
// 更新模板中元素的位置 -----------start
// 加这段的原因:templates元素定位会使用right,bottom,会存在计算误差问题,这里定位我只用left,top,因此要计算在组件挂载时获取画布的宽度和高度,
// 并用placeholderLeft,placeholderTop作为点位符来替换后续计算的真实值
// const canvasWidth = canvasRef.value.offsetWidth;
// const canvasHeight = canvasRef.value.offsetHeight;
// templates.value.forEach((template) => {
// if (template.elements) {
// JSON.parse(template.elements).forEach((element) => {
// if (element.style.left === "placeholderLeft") {
// // 计算右上角和右下角元素的 left 值
// if (element.src === rightTopImg) {
// // element.style.left = `${canvasWidth - 120 - 300}px`;
// element.style.left = `${canvasWidth - 50 - 170}px`;
// } else if (element.src === rightBottomImg) {
// element.style.left = `${canvasWidth - 50 - 150}px`;
// }
// }
// if (element.style.top === "placeholderTop") {
// // 计算左下角和右下角元素的 top 值
// if (element.src === leftBottomImg) {
// element.style.top = `${canvasHeight - 120}px`;
// } else if (element.src === rightBottomImg) {
// element.style.top = `${canvasHeight - 60 - 140}px`;
// }
// }
// // 右上角放了两张图片
// if (element.style.left == "placeholderLeftPlus100") {
// element.style.left = `${canvasWidth - 50 - 60}px`;
// }
// });
// }
// });
// 更新模板中元素的位置 -----------end
// 加载模板数据
// const savedTemplates = localStorage.getItem("templates");
// if (savedTemplates) {
// templates.value = JSON.parse(savedTemplates);
// }
// preloadImages();
});
onBeforeUnmount(() => {
window.removeEventListener("keydown", handleKeyDown); // 移除键盘事件监听
});
// 加粗,斜体-----方法二
const toggleBold = () => {
if (
selectedIndex.value !== -1 &&
elements.value[selectedIndex.value].type === "text"
) {
const currentStyle = elements.value[selectedIndex.value].style.fontWeight; // 获取当前字体粗细
elements.value[selectedIndex.value].style.fontWeight =
currentStyle === "bold" ? "normal" : "bold"; // 切换加粗
// 更新 textStyle.value.bold
textStyle.value.bold =
elements.value[selectedIndex.value].style.fontWeight === "bold";
}
};
const toggleItalic = () => {
if (
selectedIndex.value !== -1 &&
elements.value[selectedIndex.value].type === "text"
) {
const currentStyle = elements.value[selectedIndex.value].style.fontStyle; // 获取当前字体样式
elements.value[selectedIndex.value].style.fontStyle =
currentStyle === "italic" ? "normal" : "italic"; // 切换斜体
// 更新 textStyle.value.italic
textStyle.value.italic =
elements.value[selectedIndex.value].style.fontStyle === "italic";
}
};
// ----------------------------------undo , redo 的功能
// const history = ref([]); // 用于保存历史状态
// const currentStep = ref(-1); // 当前状态指针
// const saveState = () => {
// // 保存当前状态
// if (currentStep.value < history.value.length - 1) {
// history.value.splice(currentStep.value + 1); // 清除 redo 的历史
// }
// history.value.push(JSON.stringify(elements.value)); // 保存当前元素状态
// currentStep.value++;
// };
// const undo = () => {
// if (currentStep.value > 0) {
// currentStep.value--;
// elements.value = JSON.parse(history.value[currentStep.value]); // 恢复到上一个状态
// }
// };
// const redo = () => {
// if (currentStep.value < history.value.length - 1) {
// currentStep.value++;
// elements.value = JSON.parse(history.value[currentStep.value]); // 恢复到下一个状态
// }
// };
const reset = () => {
// 重置操作的逻辑
};
// const saveCurrentTemplate = () => {
// // 获取当前时间
// const now = new Date();
// const year = now.getFullYear();
// const month = String(now.getMonth() + 1).padStart(2, "0");
// const day = String(now.getDate()).padStart(2, "0");
// const hour = String(now.getHours()).padStart(2, "0");
// const minute = String(now.getMinutes()).padStart(2, "0");
// const second = String(now.getSeconds()).padStart(2, "0");
// // 生成模板名称
// const templateName = `${year}${month}${day}${hour}${minute}${second}`;
// const newTemplate = {
// // name: `新模板 ${Date.now()}`,
// name: `新模板 ${templateName}`,
// logo: "/path/to/thumbnail.png",
// // background: "/cerificationFile/bg.png",
// background: canvasBackground.value,
// elements: elements.value,
// };
// templates.value.push(newTemplate);
// ElMessage.success("保存模板成功");
// console.log("保存的模板", templates.value);
// localStorage.setItem("templates", JSON.stringify(templates.value));
// };
const predefineColors = ref([
"#ff4500",
"#ff8c00",
"#ffd700",
"#90ee90",
"#00ced1",
"#1e90ff",
"#c71585",
"rgba(255, 69, 0, 0.68)",
"rgb(255, 120, 0)",
"hsv(51, 100, 98)",
"hsva(120, 40, 94, 0.5)",
"hsl(181, 100%, 37%)",
"hsla(209, 100%, 56%, 0.73)",
"#c7158577",
]);
// =======================================保存模板时上传封面图片
const showModal = ref(false);
const templateName = ref("");
const customThumbnail = ref("");
const saveCurrentTemplate = () => {
ElMessageBox.prompt("Please enter a template name", "Save template", {
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
inputValidator: (value) => {
if (value.trim() === "") {
return "The template name cannot be empty";
}
return true;
},
})
.then(({ value }) => {
// 创建新模板对象
const newTemplate = {
name: value,
// logo: "/path/to/thumbnail.png", // 这里可以根据需要设置默认缩略图
logo: "/api/file/download/logo/CMC.png",
background: canvasBackground.value,
elements:
elements.value && elements.value.length > 0
? JSON.stringify(elements.value)
: "",
};
// 将新模板添加到 templates 数组中
// templates.value.push(newTemplate);
// localStorage.setItem("templates", JSON.stringify(templates.value));
// ElMessage.success("save successfully.");
getTemplatesSave(newTemplate).then((res) => {
if (res.code == 2000) {
ElMessage.success(res.message);
getDataList();
}
});
})
.catch(() => {
ElMessage.info("cancel.");
});
// showModal.value = true;
};
const handleThumbnailUploadSuccess = (response) => {
customThumbnail.value = response.url;
ElMessage.success("Thumbnail upload successful");
};
// const handleSaveTemplate = () => {
// if (templateName.value.trim() === "") {
// ElMessage.error("The template name cannot be empty");
// return;
// }
// const newTemplate = {
// name: templateName.value,
// logo: customThumbnail.value || "",
// background: canvasBackground.value,
// elements: JSON.parse(JSON.stringify(elements.value)),
// };
// templates.value.push(newTemplate);
// localStorage.setItem("templates", JSON.stringify(templates.value));
// ElMessage.success("save successfully.");
// showModal.value = false;
// templateName.value = "";
// customThumbnail.value = "";
// };
// 防抖函数
const debounce = (func, delay) => {
let timer = null;
return function () {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
};
// 失焦处理函数
const handleBlur = (index) => {
elements.value[index].content = tempTextContent.value; // 在失去焦点时赋值
editingIndex.value = -1; // 结束编辑状态
};
/**
* 自定义图片上传
* @param options
*/
let upLoading = ref(false);
const handleUploadFile = async (options) => {
upLoading.value = true;
const { data, code, message } = await getImageUpload(options.file);
if (code == 2000) {
ElMessage({
type: "success",
message: message,
});
upLoading.value = false;
getImageDatas();
} else {
ElMessage({
type: "error",
message: message,
});
upLoading.value = false;
}
// 保存文件的 ID 到文件对象的 id 属性中
// options.file.id = data.content.fileId;
};
const beforeUpload = (file) => {
const isImage = file.type.startsWith("image/"); // 文件类型必须是 image
if (!isImage) {
ElMessage.error("Upload image files only (jpg, png, jpeg, gif)");
return false;
}
// 可选:限制文件大小(例如最大 5MB)
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
ElMessage.error("Upload picture size cannot exceed 5MB!");
return false;
}
return true;
};
// // 添加调整图层顺序的方法
// const moveLayerUp = (index) => {
// if (index < elements.value.length - 1) {
// const temp = elements.value[index];
// elements.value[index] = elements.value[index + 1];
// elements.value[index + 1] = temp;
// }
// };
// const moveLayerDown = (index) => {
// if (index > 0) {
// const temp = elements.value[index];
// elements.value[index] = elements.value[index - 1];
// elements.value[index - 1] = temp;
// }
// };
// 判断当前选中的图层是否是最上方
const isLayerAtTop = () => {
return (
// selectedIndex.value === elements.value.length - 1 ||
// elements.value.length <= 0 ||
!isDisabledLayerPos.value
);
};
// 判断当前选中的图层是否是最下方
const isLayerAtBottom = () => {
return (
// selectedIndex.value === 0 ||
// elements.value.length <= 0 ||
!isDisabledLayerPos.value
);
};
// 向上移动图层
const moveLayerUp = () => {
if (!isLayerAtTop()) {
const currentIndex = selectedIndex.value;
const nextIndex = currentIndex + 1;
const temp = elements.value[currentIndex];
elements.value[currentIndex] = elements.value[nextIndex];
elements.value[nextIndex] = temp;
selectedIndex.value = nextIndex;
}
};
// 向下移动图层
const moveLayerDown = () => {
if (!isLayerAtBottom()) {
const currentIndex = selectedIndex.value;
const prevIndex = currentIndex - 1;
const temp = elements.value[currentIndex];
elements.value[currentIndex] = elements.value[prevIndex];
elements.value[prevIndex] = temp;
selectedIndex.value = prevIndex;
}
};
// 假设这里有一个监听选中索引变化的逻辑
watch(selectedIndex, () => {
checkOverlappingLayers();
});
// 加一个优化:判断出图层有重叠时,才可以move up, move down
const isDisabledLayerPos = ref(false);
// 判断两个矩形区域是否有交集
const isRectanglesIntersecting = (rect1, rect2) => {
return (
rect1.left < rect2.right &&
rect1.right > rect2.left &&
rect1.top < rect2.bottom &&
rect1.bottom > rect2.top
);
};
// 判断选中的图层身上是否有其他交集的图层
const checkOverlappingLayers = () => {
if (selectedIndex.value === -1) return; // 如果没有选中的图层,直接返回
const selectedElement = document.querySelector(
`.canvas-element:nth-child(${selectedIndex.value + 1})`
);
if (!selectedElement) return;
const selectedRect = selectedElement.getBoundingClientRect();
for (let i = 0; i < elements.value.length; i++) {
if (i === selectedIndex.value) continue; // 跳过选中的图层
const otherElement = document.querySelector(
`.canvas-element:nth-child(${i + 1})`
);
if (!otherElement) continue;
const otherRect = otherElement.getBoundingClientRect();
// console.log("重叠", isRectanglesIntersecting(selectedRect, otherRect));
if (isRectanglesIntersecting(selectedRect, otherRect)) {
// ElMessage.warning("选中的图层与其他图层有重叠!");
isDisabledLayerPos.value = true;
// return;
} else {
isDisabledLayerPos.value = false;
}
}
};
// ... existing code ...
</script>
<style lang="scss" scoped>
.certificate-editor {
display: flex;
height: 100vh;
background-color: #f5f7fa;
}
.toolbar {
width: 300px;
padding: 0 20px;
background-color: white;
border-right: 1px solid #dcdfe6;
overflow-y: auto;
.tool-section {
margin-bottom: 30px;
h3 {
font-size: 16px;
color: #303133;
margin-bottom: 15px;
}
}
.template-list {
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 15px;
.template-item {
position: relative;
cursor: pointer;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
transition: all 0.3s;
&:hover {
border-color: #409eff;
transform: translateY(-2px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-image {
width: 100%;
height: 150px;
object-fit: cover;
}
.template-name {
padding: 8px;
text-align: center;
color: #606266;
font-size: 14px;
}
}
.template-item::before {
z-index: 100;
content: "";
width: 0;
height: 0;
border: 40px solid transparent;
// border-right: 60px solid #17b899;
border-right: 60px solid #fc5531;
transform: rotate(135deg);
position: absolute;
right: -61px;
top: -61px;
cursor: pointer;
opacity: 0; /* 默认隐藏 */
transition: opacity 0.3s ease;
pointer-events: none; /* 避免隐藏时阻挡鼠标事件 */
}
.template-item:hover::before {
opacity: 1; /* 鼠标移入时显示 */
pointer-events: auto; /* 允许鼠标点击 */
}
// .template-item::after {
// z-index: 999;
// content: "x";
// width: 40px;
// height: 30px;
// color: #fff;
// // transform: rotate(45deg);
// position: absolute;
// right: -24px;
// top: -1px;
// font-weight: bold;
// letter-spacing: 2px;
// cursor: pointer;
// }
.delete-icon {
z-index: 999;
content: "x";
width: 40px;
height: 30px;
color: #fff;
// transform: rotate(45deg);
position: absolute;
right: -24px;
top: -1px;
font-weight: bold;
letter-spacing: 2px;
cursor: pointer;
opacity: 0; /* 默认隐藏 */
transition: opacity 0.3s ease;
pointer-events: none;
}
.template-item:hover .delete-icon {
opacity: 1; /* 鼠标移入时显示 */
pointer-events: auto; /* 允许鼠标点击 */
}
}
.image-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
.image-item {
cursor: move;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
aspect-ratio: 1;
transition: all 0.3s;
position: relative;
&:hover {
border-color: #409eff;
transform: translateY(-2px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
// &::before {
// content: "";
// position: absolute;
// top: 0;
// left: 0;
// right: 0;
// bottom: 0;
// background: rgba(0, 0, 0, 0.03);
// opacity: 0;
// transition: opacity 0.3s;
// }
// &:hover::before {
// opacity: 1;
// }
.el-image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.image-item::before {
z-index: 100;
content: "";
width: 0;
height: 0;
border: 35px solid transparent;
border-right: 60px solid #fc5531;
transform: rotate(135deg);
position: absolute;
right: -61px;
top: -61px;
cursor: pointer;
opacity: 0; /* 默认隐藏 */
transition: opacity 0.3s ease;
pointer-events: none; /* 避免隐藏时阻挡鼠标事件 */
}
.image-item:hover::before {
opacity: 1; /* 鼠标移入时显示 */
pointer-events: auto; /* 允许鼠标点击 */
}
.delete-icon2 {
z-index: 999;
content: "x";
font-weight: bold;
letter-spacing: 2px;
cursor: pointer;
opacity: 0;
position: absolute;
right: 1px;
top: -5px;
transition: opacity 0.3s ease;
color: #fff;
}
.image-item:hover .delete-icon2 {
opacity: 1; /* 鼠标移入时显示 */
pointer-events: auto; /* 允许鼠标点击 */
}
}
.add-text-btn {
width: 100%;
}
.background-settings {
margin-top: 10px;
.background-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 100%;
height: 120px;
display: flex;
justify-content: center;
align-items: center;
&:hover {
border-color: var(--el-color-primary);
}
}
.background-preview {
width: 100%;
height: 100%;
object-fit: cover;
}
.background-uploader-icon {
font-size: 28px;
color: #8c939d;
}
.background-actions {
margin-top: 10px;
text-align: center;
}
}
}
.right-content {
display: flex;
flex-direction: column;
flex: 1;
.canvas-area {
// flex: 1;
padding: 20px;
// display: flex;
// flex-direction: column;
.canvas {
position: relative;
background-color: #fff;
border: 1px solid #ddd;
// margin: 20px;
width: 300px;
height: 210px;
overflow: hidden;
background-repeat: no-repeat;
}
}
.canvas-actions {
margin-top: 10px;
text-align: center;
.el-button {
padding: 12px 30px;
font-size: 16px;
&.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
}
}
.canvas-element {
position: absolute;
user-select: none;
&.selected {
outline: 2px solid var(--el-color-primary);
}
&:hover {
cursor: move;
}
}
.element-wrapper {
position: relative;
width: 100%;
height: 100%;
.element-actions {
position: absolute;
top: -20px;
right: -20px;
z-index: 100;
display: flex;
gap: 5px;
}
}
.resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
background-color: var(--el-color-primary);
border: 2px solid #fff;
border-radius: 4px;
cursor: se-resize;
z-index: 100;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
transform: translate(50%, 50%);
.el-icon {
font-size: 12px;
color: #fff;
transform: rotate(45deg);
}
&:hover {
transform: translate(50%, 50%) scale(1.1);
transition: transform 0.2s;
}
}
:deep .el-image__inner {
// 移除或调整 CSS 中的最大尺寸限制:在 CSS 中,检
// max-width 和 max-height 属性会阻止图片超过指定的尺寸。查是否有 max-width 和 max-height 属性限制了图片的尺寸。如果有,可以将其移除或设置为一个更大的值。
// max-width: 300px !important;
// max-height: 300px !important;
// 移除最大宽度和最大高度限制
max-width: none !important;
max-height: none !important;
width: 100% !important;
height: 100% !important;
object-fit: contain;
}
.size-buttons {
.btn {
padding: 0;
margin: 0 4px;
}
}
// 设置滚动条样式 ==============================
*::-webkit-scrollbar {
width: 14px;
height: 14px;
}
*::-webkit-scrollbar-button {
width: 0;
height: 0;
display: none;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
min-height: 12px;
border: 4px solid transparent;
background-clip: content-box;
border-radius: 7px;
background-color: #3c8dbc;
}
*::-webkit-scrollbar-thumb:hover {
background-color: #a8bbcf;
}
*::-webkit-scrollbar-thumb:active {
background-color: #87a2bd;
}
*::-webkit-scrollbar-track {
background-color: transparent;
}
*::-webkit-scrollbar-track-piece {
background-color: transparent;
}
// .image-slot {
// display: flex;
// justify-content: center;
// align-items: center;
// width: 100%;
// height: 100%;
// background: var(--el-fill-color-light);
// color: var(--el-text-color-secondary);
// font-size: 30px;
// }
// .image-slot .el-icon {
// font-size: 30px;
// }
</style>
代码二:
<div
v-for="(element, index) in elements"
:key="index"
class="canvas-element"
:class="{ selected: selectedIndex === index }"
:style="getElementStyle(element)"
@click.stop="selectElement(index)"
@mousedown="
element.type === 'image'
? startImageDragging($event, index)
: startDragging($event, index)
"
@mousemove="
element.type === 'image'
? onImageDragging($event)
: onDragging($event)
"
@mouseup="
element.type === 'image'
? stopImageDragging($event)
: stopDragging()
"
@mouseleave="
element.type === 'image'
? stopImageDragging($event)
: stopDragging()
"
>
<template v-if="element.type === 'image'">
<div class="element-wrapper">
<el-image :src="element.src" fit="contain" />
<div class="element-actions" v-if="selectedIndex === index">
<el-button
type="danger"
size="small"
circle
icon="Delete"
@click.stop="deleteElement(index)"
/>
<!-- 添加调整图层顺序的按钮 -->
<!-- <el-button
type="primary"
size="small"
circle
icon="ArrowUp"
@click.stop="moveLayerUp(index)"
:disabled="index === elements.length - 1"
/>
<el-button
type="primary"
size="small"
circle
icon="ArrowDown"
@click.stop="moveLayerDown(index)"
:disabled="index === 0"
/> -->
</div>
<div
v-if="selectedIndex === index"
class="resize-handle"
@mousedown.stop="startResizing($event, index)"
>
<el-icon><Rank /></el-icon>
</div>
</div>
</template>
<template v-else-if="element.type === 'text'">
<div class="element-wrapper">
<div
class="text-element"
:style="getTextStyle(element)"
@dblclick="startEditing(index)"
>
<el-input
v-if="editingIndex === index"
v-model="tempTextContent"
@blur="handleBlur(index)"
type="textarea"
autosize
/>
<span v-else>{{ element.content }}</span>
</div>
<div class="element-actions" v-if="selectedIndex === index">
<el-button
type="danger"
size="small"
circle
icon="Delete"
@click.stop="deleteElement(index)"
/>
<!-- 添加调整图层顺序的按钮 -->
<!-- <el-button
type="primary"
size="small"
circle
icon="ArrowUp"
@click.stop="moveLayerUp(index)"
:disabled="index === elements.length - 1"
/>
<el-button
type="primary"
size="small"
circle
icon="ArrowDown"
@click.stop="moveLayerDown(index)"
:disabled="index === 0"
/> -->
</div>
<div
v-if="selectedIndex === index"
class="resize-handle"
@mousedown.stop="startResizing($event, index)"
>
<el-icon><Rank /></el-icon>
</div>
</div>
</template>
</div>
// ============================================ 3.26优化 ============================================
// 实现图片在鼠标松开时直接出现在最终位置且效果更加丝滑
// startDragging 、onDragging 、stopDragging 这三个方法修改了
// 新增图片拖拽相关变量
const isImageDragging = ref(false); // 用于标记图片是否正在被拖拽
const imageDragStartPos = ref({ x: 0, y: 0 }); // 记录图片拖拽开始时的鼠标位置
const imageElementStartPos = ref({ x: 0, y: 0 }); // 记录图片拖拽开始时元素的位置
const imageDraggingIndex = ref(-1); // 记录正在被拖拽的图片元素的索引
// 开始拖拽图片元素: 处理图片开始拖拽的逻辑
const startImageDragging = (event, index) => {
if (editingIndex.value === -1) {
isImageDragging.value = true;
imageDraggingIndex.value = index;
imageDragStartPos.value = {
x: event.clientX,
y: event.clientY,
};
const element = elements.value[index];
const left = element.style.left;
const top = element.style.top;
const right = element.style.right;
const bottom = element.style.bottom;
// 处理百分比位置转换为像素
const canvasWidth = canvasRef.value.offsetWidth;
const canvasHeight = canvasRef.value.offsetHeight;
if (left?.includes("%")) {
const leftPercent = parseFloat(left);
imageElementStartPos.value.x = (canvasWidth * leftPercent) / 100;
} else if (right?.includes("%")) {
const rightPercent = parseFloat(right);
imageElementStartPos.value.x =
canvasWidth - (canvasWidth * rightPercent) / 100;
} else {
imageElementStartPos.value.x = left
? parseFloat(left)
: canvasWidth - (right ? parseFloat(right) : 0);
}
if (top?.includes("%")) {
const topPercent = parseFloat(top);
imageElementStartPos.value.y = (canvasHeight * topPercent) / 100;
} else if (bottom?.includes("%")) {
const bottomPercent = parseFloat(bottom);
imageElementStartPos.value.y =
canvasHeight - (canvasHeight * bottomPercent) / 100;
} else {
imageElementStartPos.value.y = top
? parseFloat(top)
: canvasHeight - (bottom ? parseFloat(bottom) : 0);
}
// 处理transform偏移
if (
element.style.transform &&
element.style.transform.includes("translate")
) {
const el = event.currentTarget;
const rect = el.getBoundingClientRect();
imageElementStartPos.value = {
x: rect.left - canvasRef.value.getBoundingClientRect().left,
y: rect.top - canvasRef.value.getBoundingClientRect().top,
};
}
}
};
// 图片拖拽中的处理: 处理图片拖拽过程中的逻辑,这里不更新元素位置
const onImageDragging = (event) => {
// 拖拽过程中不更新元素位置
if (!isImageDragging.value) return;
};
// 停止拖拽图片: 处理图片停止拖拽的逻辑,计算并更新图片的最终位置
const stopImageDragging = (event) => {
if (isImageDragging.value) {
isImageDragging.value = false;
const index = imageDraggingIndex.value;
imageDraggingIndex.value = -1;
const element = elements.value[index];
const canvasRect = canvasRef.value.getBoundingClientRect();
const elementRef = document.querySelector(
`.canvas-element:nth-child(${index + 1})`
);
if (elementRef) {
const mouseEndPos = {
x: event.clientX,
y: event.clientY,
};
const dx = mouseEndPos.x - imageDragStartPos.value.x;
const dy = mouseEndPos.y - imageDragStartPos.value.y;
let newX = imageElementStartPos.value.x + dx;
let newY = imageElementStartPos.value.y + dy;
const elementRect = elementRef.getBoundingClientRect();
// 限制范围
newX = Math.max(0, Math.min(newX, canvasRect.width - elementRect.width));
newY = Math.max(
0,
Math.min(newY, canvasRect.height - elementRect.height)
);
element.style.left = `${newX}px`;
element.style.top = `${newY}px`;
// 移除transform,避免位置计算问题
element.style.transform = "";
// 移除可能导致计算偏差的 right 和 bottom 属性
element.style.right = "";
element.style.bottom = "";
}
}
};