使用拖拽生成活动海报(vue项目)

news2025/1/16 23:58:53
<template>
  <div class="poster-editor">
    <div class="toolbar" v-if="edit == '1'">
      <div style="text-align: center;font-size: 14px;font-weight: 700;margin-bottom: 10px;">工具栏</div>
      <div class="toolbar-item" v-for="(item, index) in toolbarItems" :key="index"
        @dragstart="onDragStart($event, item)" draggable="true">
        <span>{{ item.label }}</span>
        <el-popover placement="top" width="350" trigger="click">
          <div style="margin-bottom: 5px;">
            <el-input v-model="item.fontSize" placeholder="文字大小" @input="item.fontSize = integer(item.fontSize)"
              @blur="clickFontSizeColor(item)"  @change="clickFontSizeColor(item)">
              <template slot="prepend">文字大小</template>
            </el-input>
          </div>
          <div style="display: flex;">
            <el-input v-model="item.color" placeholder="颜色" @blur="clickFontSizeColor(item)" @change="clickFontSizeColor(item)">
              <template slot="prepend">文字颜色</template>
            </el-input>
            <el-color-picker v-model="item.color" @change="clickFontSizeColor(item)" style="margin-left: 10px;"></el-color-picker>
          </div>
          <el-button v-if="item.type == 'text'" slot="reference" type="text" icon="el-icon-edit"></el-button>
        </el-popover>
      </div>
    </div>

    <div class="poster-canvas" ref="posterCanvas" @drop="onDrop($event)" @dragover="onDragOver($event)">
      <div class="background" :style="{ backgroundImage: `url(${backgroundImage})` }"></div>
      <div v-for="(element, index) in elements" :key="index" class="poster-element" :style="{
      top: element.top + 'px',
      left: element.left + 'px',
      width: element.width + 'px',
      height: element.height + 'px',
      transform: `rotate(${element.rotation}deg)`
    }" @mousedown="onMouseDown($event, index)">
        <button class="delete-btn" @click.stop="onDeleteElement(index)" v-if="edit == '1'">x</button>
        <img v-if="element.type === 'image'" :src="element.content" class="draggable-image" />
        <div v-else :style="{ fontSize: element.fontSize + 'px', color: element.color }" class="draggable-text">{{
      element.content }}</div>
      </div>
    </div>
  </div>
</template>

<script>
// import html2canvas from 'html2canvas'
export default {
  props: {
    backgroundImage: {
      type: String,
      default: ''
    },
    edit: {
      type: String,
      default: ''
    }
  },
  watch: {

  },
  data() {
    return {
      toolbarItems: [
        { label: '二维码', type: 'image', content: require("../../../../assets/img/card_hm.png") },
        { label: '名称', type: 'text', content: '123', fontSize: 14, color: '#000000' },
        { label: '电话', type: 'text', content: '123', fontSize: 14, color: '#000000' },
        { label: '地址', type: 'text', content: '123', fontSize: 14, color: '#000000' }
      ],
      elements: [],
      currentElementIndex: null,
      offsetX: 0,
      offsetY: 0,
      isDragging: false,
      isResizing: false,
      isRotating: false,
      startWidth: 0,
      startHeight: 0,
      startFontSize: 16,
      startRotation: 0,
      resizeStartX: 0,
      resizeStartY: 0,
      rotateStartX: 0,
      rotateStartY: 0,
      // backgroundImage: '', // 背景图片
    };
  },
  methods: {
    onDragStart(event, item) {
      const itemJSON = JSON.stringify(item);
      event.dataTransfer.setData('element', itemJSON);
    },
    onDragOver(event) {
      event.preventDefault();
    },
    onDrop(event) {
      event.preventDefault();
      const canvasRect = this.$refs.posterCanvas.getBoundingClientRect();
      const elementDataString = event.dataTransfer.getData('element');

      if (!elementDataString) {
        console.error('No data found in drag event');
        return; // 提早返回,避免解析错误
      }

      let elementData;
      try {
        elementData = JSON.parse(elementDataString);
      } catch (error) {
        console.error('Failed to parse element data:', error);
        return; // 处理解析错误
      }

      // console.log('elementData:', elementData);

      let newElement = {
        ...elementData,
        top: event.clientY - canvasRect.top,
        left: event.clientX - canvasRect.left,
        width: elementData.type === 'image' ? 100 : 150,
        height: elementData.type === 'image' ? 100 : 40,
        fontSize: elementData.type === 'text' ? elementData.fontSize : '',
        color: elementData.type === 'text' ? elementData.color : '',
        rotation: 0, // 新增旋转属性
      };
      this.elements.push(newElement);
      this.$emit('savePoster', this.elements);
    },
    onMouseDown(event, index) {
      // console.log(event, index,'event, index')
      this.currentElementIndex = index;
      this.offsetX = event.clientX - (this.elements[index].left);
      this.offsetY = event.clientY - (this.elements[index].top);
      this.isDragging = true;
      document.addEventListener('mousemove', this.onMouseMove);
      document.addEventListener('mouseup', this.onMouseUp);
      // 停止并更新
      this.$emit('savePoster', this.elements);
    },
    onMouseMove(event) {
      if (this.isDragging && this.currentElementIndex !== null) {
        const fixedWidth = 400; // 固定宽度
        const fixedHeight = 800; // 固定高度
        const element = this.elements[this.currentElementIndex];

        // 计算新的位置
        let newTop = event.clientY - this.offsetY;
        let newLeft = event.clientX - this.offsetX;

        // 限制移动范围(上下左右)
        if (newTop < 0) newTop = 0; // 上边界
        if (newLeft < 0) newLeft = 0; // 左边界

        console.log()
        if (newTop + element.height > fixedHeight) {
          newTop = fixedHeight - element.height; // 下边界
        }
        if (newLeft + element.width > fixedWidth) {
          newLeft = fixedWidth - element.width; // 右边界
        }

        // 更新元素位置
        this.$set(this.elements, this.currentElementIndex, {
          ...element,
          top: newTop,
          left: newLeft,
        });
      }
    },
    onMouseUp() {
      this.isDragging = false;
      this.isResizing = false;
      this.isRotating = false;
      document.removeEventListener('mousemove', this.onMouseMove);
      document.removeEventListener('mouseup', this.onMouseUp);
    },
    onResizeStart(event, index) {
      this.currentElementIndex = index;
      this.isResizing = true;
      const element = this.elements[index];
      this.startWidth = element.width;
      this.startHeight = element.height;
      this.resizeStartX = event.clientX;
      this.resizeStartY = event.clientY;
      document.addEventListener('mousemove', this.onMouseMove);
      document.addEventListener('mouseup', this.onMouseUp);
    },
    onRotateStart(event, index) {
      this.currentElementIndex = index;
      this.isRotating = true;
      const element = this.elements[index];
      this.startRotation = element.rotation;
      this.rotateStartX = event.clientX;
      this.rotateStartY = event.clientY;
      document.addEventListener('mousemove', this.onMouseMove);
      document.addEventListener('mouseup', this.onMouseUp);
    },
    onDeleteElement(index) {
      this.elements.splice(index, 1);
    },
    //修改文字大小 
    clickFontSizeColor(item) {
      if (this.elements.length > 0) {
        this.elements.forEach(element => {
          if (element.label == item.label) {
            element.fontSize = item.fontSize
            element.color = item.color
          }
        });
        this.$emit('savePoster', this.elements);
      }
    },
  },
};
</script>

<style lang="less" scoped>
.poster-editor {
  display: flex;
  padding: 20px;
  margin-top: 20px;
}

.toolbar {
  width: 125px;
  background-color: #f5f5f5;
  padding: 10px;
}

.toolbar-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  text-align: left;
  margin-bottom: 10px;
  padding: 10px;
  background-color: #e1e1e1;
  border-radius: 5px;
  cursor: grab;
}

.poster-canvas {
  position: relative;
  width: 400px;
  height: 800px;
  background-color: #ffffff;
  border: 1px solid #ddd;
  border-radius: 40px;
  overflow: hidden;
  margin-left: 20px;
}


.background {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  // width: 400px;
  // height: 800px;
  background-size: 100% 100%;
  background-position: center;
}

.poster-element {
  position: absolute;
  cursor: move;
  box-sizing: border-box;
}

.poster-element:hover {
  .delete-btn {
    display: block;
  }

  .draggable-tex {
    border: 1px solid #ccc;
    background-color: rgba(240, 240, 240, 0.8);
  }
}

.draggable-image {
  width: 100%;
  height: 100%;
}

.draggable-text {
  text-align: left;
  // padding: 10px;
  // line-height: 20px;
  // 下面才是重点关注代码
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  text-overflow: ellipsis;
  white-space: normal;
  -webkit-line-clamp: 4;
  /* 设置最大显示行数 */
}

.delete-btn {
  position: absolute;
  top: -10px;
  right: -10px;
  background-color: red;
  color: white;
  border: none;
  border-radius: 50%;
  width: 20px;
  height: 20px;
  cursor: pointer;
  font-size: 12px;
  display: none;
}

.resize-handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 10px;
  height: 10px;
  background-color: blue;
  cursor: se-resize;
}

.rotate-handle {
  position: absolute;
  top: -10px;
  left: 50%;
  transform: translateX(-50%);
  width: 10px;
  height: 10px;
  background-color: green;
  cursor: grab;
}

.controls {
  margin: 20px;
}
</style>

使用

// 拖拽画布

import Dragcanvas from "./components/dragcanvas.vue";
 <Dragcanvas @savePoster='savePoster' :backgroundImage="ruleForm.cover" :edit="$route.query.edit"></Dragcanvas>
 components: {
    Dragcanvas
  },


    // 活码拖拽
    savePoster(item) {
      let width = 400 // 固定宽
      let height = 800 // 固定高度
      let ary = []
      item.map(it => {
        ary.push({
          content: it.content,
          fontSize: it.fontSize,
          label: it.label,
          rotation: it.rotation,
          type: it.type,
          width: it.width,
          height: it.height,
          left: it.left,
          top: it.top,
          fixedwidth: width,
          fixedheight: height,
          leftpercentage: (it.left / width) * 100,
          toppercentage: (it.top / height) * 100,
          color: it.color,
        })
      })
      console.log(ary, '拖拽的数据')
    }

展示效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2214525.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WPF组件的自定义模板和触发器全面解析

Windows Presentation Foundation&#xff08;WPF&#xff09;是微软提供的一个用于构建桌面客户端应用程序的UI框架。其依赖于XAML&#xff08;Extensible Application Markup Language&#xff09;进行用户界面设计&#xff0c;提供了一套强大的控件和组件模型。在WPF开发中&…

C++ 的存储类型与新的 thread_local

1 C 的存储类型 1.1 存储周期&#xff08;Storage duration&#xff09; 存储周期表示一个变量的存储空间持续的时间&#xff0c;它应该与对象的语义生命周期一致&#xff08;或至少不小于对象的语义生命周期&#xff09;。C 98从 C 继承了三种存储周期&#xff0c;分别是静态…

【黑马点评】项目知识点及面经整理

【黑马点评】项目知识点及面经整理 1 短信登录&#xff08;Session&#xff0c;Redis&#xff0c;JWT验证&#xff09;1.1 JWT和Session的区别1.2 Session1.3 Redis1.3.1 基于Redis实现登录验证1.3.2 登录拦截 1.4 JWT1.4.1 JWT的有效验证1.4.2 JWT定义 2 热点数据缓存2.1 缓存…

区块链-智能合约Solidity编程

文章目录 一、ubuntu安装二、FISCO BCOS安装五、 WeBASE安装5.1 WeBASE简介5.2 节点前置服务搭建5.3 调用HelloWorld合约 七、Solidity极简入门7.1. 值类型7.2. 变量数据存储和作用域7.3. 函数7.4 控制流7.5 数组&映射7.6 结构体7.7 修饰符7.8 事件7.9 面向对象7.10 抽象合…

SoC芯片中Clock Gen和Reset Gen的时钟树综合

社区目前已经开设了下面列举的前四大数字后端实战课程&#xff0c;均为直播课&#xff0c;且均是小编本人亲自授课&#xff01;遇到项目问题&#xff0c;都可以远程一对一指导解决具体问题。小编本人是一线12年后端经验的数字后端工程师。想找一线IC后端技术专家亲自带你做后端…

Java Fork-Join框架学习

概述 Fork/Join是Java7提供的一个用于并行执行任务的框架&#xff0c;是一个把大任务分割成若干个小任务&#xff0c;最终汇总每个小任务结果后得到大任务结果的框架。Fork负责把一个大任务切分为若干并行执行的子任务&#xff0c;Join负责合并这些子任务的执行结果&#xff0…

Ubuntu系衍生版手动修改配置网卡的配置总结

一、Ubuntu系的IP地址配置文件的目录&#xff1a; sudo vim /etc/network/interfaces 二、以DHCP方式配置网卡&#xff1a; 在以上配置文件中添加以下两行&#xff1a; auto enp3s0 iface enp3s0 inet dhcp 三、为网卡配置静态IP地址&#xff1a; 在以上配置文件中添…

实验3,网络地址转换

实验3&#xff1a;网络地址转换 实验目的及要求&#xff1a; 通过实验&#xff0c;掌握NAT技术的工作原理&#xff0c;了解三种不同类型NAT技术的主要作用以及各自的主要应用环境。能够完成静态NAT和复用NAT技术的应用&#xff0c;并熟练掌握NAT技术相关的配置命令。 实验设…

el-date-picker选择时间后标准时间少1小时问题

问题 前端开发中发现Element的时间组件el-date-picker在选择选择部分时间后js对象的标准时间少1小时&#xff0c;如果选择的小时为0&#xff0c;会导致部分转换条件下结果少1天。 比如组件中选择的本地时间为&#xff1a; 1988-08-01 00:00:00 而js对象获取到是标准时间是&am…

ubuntu 安装kali命令补全功能

输入命令时&#xff0c;之前的命令会以阴影显示&#xff0c;按下右键或 Tab 键可以直接补全 安装zsh-autosuggestions sudo apt install zsh-autosuggestions编辑 ~/.zshrc环境变量 if [ -f /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]; then. /usr/share/zs…

【从零开始的LeetCode-算法】945. 使数组唯一的最小增量

给你一个整数数组 nums 。每次 move 操作将会选择任意一个满足 0 < i < nums.length 的下标 i&#xff0c;并将 nums[i] 递增 1。 返回使 nums 中的每个值都变成唯一的所需要的最少操作次数。 生成的测试用例保证答案在 32 位整数范围内。 示例 1&#xff1a; 输入&am…

【Hadoop】HDFS基本操作

参考&#xff1a;3.HDFS基本操作_哔哩哔哩_bilibili 创建目录 hadoop fs -mkdir -p /training/qiang查看当前根目录下文件 hadoop fs -ls /hadoop fs -ls /training/目录授权 hadoop fs -chmod -R 777 /training/qm777是最大权限&#xff0c;读写 4、2、1 上传文件 先创…

YZ系列工具之YZ09:VBA_Excel之读心术

我给VBA下的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套一部VBA手册&#xff0c;教程分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的…

Python自动化脚本裁剪图片为1:1比例

一、创建输入文件夹&#xff08;in&#xff09;和输出文件夹&#xff08;out&#xff09;&#xff0c;将原始图片放在输入文件夹&#xff08;in&#xff09;里 二、 安装对应的Python库 pip install Pillow 三、编写自动化脚本代码 import os from PIL import Imagedef crop…

Axure RP电商系统商城PC+app+后台买家卖端高保真原型模板及元件库

AxureRP电商商城PCapp后台买家卖端高保真原型模板本套包含三份原型图素材 APP买家端原型简介&#xff1a; 包含了用户中心、会员成长、优惠券、积分、互动社区、运营推广、内容推荐、商品展示、订单流程、订单管理、售后及服务等完整的电商体系功能架构和业务流程。 本模板由…

Spring中ApplicationEvent事件的实现

1&#xff09;简介 Spring Event (接口名为Aplication Event) 观察者设计模式&#xff0c;由事件发起者publisher发布事件&#xff08;指定事件名&#xff09;&#xff0c;事件监听者监听事件&#xff08;指定事件名&#xff09;。 好比&#xff0c; A 说了一句话&#xff0c…

安卓14无法安装应用解决历程

客户手机基本情况&#xff1a; 安卓14&#xff0c;对应的 targetSdkVersion 34 前天遇到了安卓14适配问题&#xff0c;客户发来的截图是这样的 描述&#xff1a;无法安装我们公司的B应用。 型号&#xff1a;三星google美版 解决步骤&#xff1a; 1、寻找其他安卓14手机测试…

从源码到平台:使用视频美颜SDK构建高性能直播美颜系统详解

本文将深入探讨如何从源码出发&#xff0c;借助视频美颜SDK构建一套高性能的直播美颜系统&#xff0c;涵盖技术架构、核心功能的实现以及性能优化等方面的详解。 一、视频美颜SDK的作用与选择 视频美颜SDK是开发直播美颜系统的基础&#xff0c;它能够提供实时美颜处理功能&am…

STM32的GPIO片上外设

一、STM32的片上外设 片上外设是集成在 MCU 芯片内部的硬件模块&#xff0c;它们通过片内总线与 CPU 直接通信&#xff0c;而不是通过外部引脚或接口连接的设备。 片上外设&#xff1a; 包括 UART、I2C、SPI、ADC、定时器、GPIO、DMA、RTC 等集成在 MCU 内部的模块。这些外设…

《Linux从小白到高手》综合应用篇:深入理解Linux磁盘及IO优化

1. 前言 其实磁盘优化和IO优化&#xff0c;我在前面的其他Linux调优博文中已经讲述过或者涉及过了&#xff0c;但是太过零碎&#xff0c;所以本篇就来集中深入讨论下Linux磁盘和IO调优。 2.磁盘调优 结合我多年的经验&#xff0c;本人认为磁盘调优最重要的是读写性能的提升和…