拖拽盖章处理

进行元素拖拽:同类章替换、删除已盖章
1.复制对应元素,在onmousemove中left和top一直在变化
2.onmouseup时,计算出在pdf显示区域内的left,top,removeChild删除原先复制的元素,再appendChild到pdf显示区域的父元素中。再次移动onmousedown绑定对应事件

效果图
在这里插入图片描述
主要代码文件如下,重点在于_signPic内的坐标获取处理

<template>
  <div
    class="app-container"
    v-loading.fullscreen="listLoading"
    element-loading-text="加载中"
  >
    <el-header class="headerBox flex">
      <div class="doc-title-list">
        <div class="active-doc">{{ activeDoc.signDocumentTitle }}</div>
      </div>
      <!-- 发起签署 -->
      <div class="sign-btn">
        <el-button size="small" type="primary" @click="submitSign"
          >确认签署</el-button
        >
      </div>
    </el-header>
    <!-- 主题内容 -->
    <div class="cont">
      <!-- 中间区域 -->
      <div id="pageContent" class="viewerContainer">
        <!-- 左边区域  -->
        <div class="left-area-container">
          <!-- 坐标签未预设位置,需要拖拽盖章 -->
          <DragSeal
            v-if="signType == 1"
            :sealList="sealList"
            @drag="_signPic"
          />
          <!-- 关键字签0&&坐标签已预设位置2,无需拖拽盖章,只需选择印章 -->
          <KeySeal v-else :sealList="sealList" @select="_select" />
        </div>
        <div class="pdfContent_1PW2f">
          <div class="pdf-header">
            <div class="block">
              <span class="demonstration">{{ scale * 100 }}%</span>
              <div class="slider">
                <!-- @change="handleChange" -->
                <el-slider
                  v-model="scale"
                  :min="0.5"
                  :max="2"
                  :step="0.25"
                  disabled
                ></el-slider>
              </div>
            </div>
            <div class="input">
              <span
                >Page:
                <input
                  type="text"
                  class="input-text"
                  v-model="page_num"
                  placeholder="1"
                />
                / {{ page_count }}</span
              >
              <el-button @click="jump" type="text">跳转</el-button>
            </div>
          </div>
          <div
            class="pdf-container"
            ref="pdfBox"
            v-loading="!isShow"
            element-loading-text="拼命加载中"
            element-loading-spinner="el-icon-loading"
            element-loading-background="rgba(0, 0, 0, 0.1)"
          >
            <div
              class="pdf-box"
              :style="{
                width: maxPdfWidth + 'px',
                left: '',
              }"
            >
              <div
                v-for="page in page_count"
                :key="page"
                class="pdfPage_1yRne"
                :style="{
                  width: pdfInfo[page].pdfWidth + 'px',
                  height: pdfInfo[page].pdfHeight + 'px',
                }"
              >
                <canvas class="pdf-item" :id="'the-canvas' + page"></canvas>
                <div class="dragLayer_3ccsq" :id="'can' + page"></div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <!-- 右边区域 -->
      <div class="right-area-container">
        <ShowSeal
          :sealList="sealList"
          @delete="deleteSign"
          :selPosList="currentDocPosList"
          :signType="signType"
        />
      </div>
    </div>
    <!-- 替换印章 -->
    <ReplaceSeal
      v-if="replaceVisible"
      :dialogVisible="replaceVisible"
      :sealList="sealList"
      :sealId="selectSeal.sealId"
      @confirm="_confirmReplace"
    />
  </div>
</template>

<script>
import { selectSignInfoH5Sign, sign } from "@/api/sign";
const ReplaceSeal = () => import("./components/ReplaceSeal.vue");
import common from "./model/common";
import DragSeal from "./components/DragSeal.vue";
import KeySeal from "./components/KeySeal.vue";
import ShowSeal from "./components/ShowSeal.vue";
export default {
  name: "companySign",
  components: {  DragSeal, KeySeal, ShowSeal, ReplaceSeal },
  data() {
    return {
      isShow: false,
      listLoading: false,
      currentDocPosList: [],
      docPosList: {}, //每个文档的印章坐标
      userData: {},
      activeDoc: {},
      AuthDialogVisible: false, // 认证弹窗控制器
      certInfo: {},
      verifyInfoList: [], //认证列表
      signType: 0, //1坐标签未预设位置,0关键字签,2坐标签已预设位置
      signPosJson: [], //印章回显坐标信息
      replaceVisible: false,
      selectSeal: {
        sealType: "",
        domId: "",
        sealId: "",
      },
      signerInfo: {}, //签署人信息
      pdfInfo: {
        0: {
          pdfHeight: 0,
          pdfWidth: 0,
        },
      },
    };
  },
  mixins: [common],
  methods: {
    // 计算已添加印章项
    handlePos() {
      this.sealList = this.sealList.map((item) => {
        return { ...item, num: 0 };
      });
      this.currentDocPosList.map((item) => {
        for (let i = 0; i < this.sealList.length; i++) {
          const element = this.sealList[i];
          if (element.code == item.code) {
            this.sealList[i].num += 1;
          }
        }
      });
    },
    // 检查是否加盖可用印章(除时间章)
    checkSealDate() {
      let checkRes = true;
      // 查找是否有加盖印章(只存在时间章的不可以)
      const hasSeal = this.currentDocPosList.find((item) => {
        return item.isSealDate == 0;
      });
      checkRes = hasSeal ? true : false;
      if (!checkRes) {
        this.$message.error("未加盖印章,请先拖拽盖章!");
      }
      return checkRes;
    },
    // 删除全部印章
    delAllSeal() {
      const sealPosList = [...this.currentDocPosList];
      for (let i = 0; i < sealPosList.length; i++) {
        const element = sealPosList[i];
        this.deleteSign(element);
      }
    },
    // 认证成功并提交
    async _submitSign() {
      this._cancelAuth();
      this.listLoading = true;
      let sealPosList = [];
      if (this.signType == 0) {
        // 关键字签无需传坐标
        sealPosList = this.currentDocPosList.map((item) => {
          return {
            sealId: item.code,
          };
        });
      } else {
        //坐标签需传坐标
        const docSeal = this.currentDocPosList.find((item) => {
          return item.isSealDate == 0;
        });
        sealPosList = this.currentDocPosList.map((item) => {
          const sealId = item.isSealDate == 1 ? docSeal.sealId : item.sealId;
          return {
            sealId,
            x: item.posX,
            y: item.posY,
            pageNo: item.posPage,
            isSealDate: item.isSealDate,
          };
        });
      }
      const formData = {
        signBatchId: this.userData.signBatchId,
        userType: 1,
        userId: this.userData.userId,
        companyId: this.userData.companyId,
        signDocumentId: this.userData.signDocumentId,
        sealPosList,
        verifyType: "",
        signPdfUrl: this.activeDoc.signCurrentUrl,
      };
      console.log(formData, 789789);
      if (this.handleCurrentSigner()) {
        const { data: res } = await sign(formData);
        console.log(res, 2525);
        if (res.code == 0) {
          this.listLoading = false;
          this.$message.success(res.msg);
          this.$router.push({
            path: "/Previewpdf",
            query: {
              signBatchId: this.userData.signBatchId,
              signDocumentId: this.userData.signDocumentId,
            },
          });
        } else {
          this.listLoading = false;
          this.$message.warning(res.msg);
          if (res.msg == "该文档已被另一方签署,请刷新页面后重试") {
            // 重新获取文档信息
            this.selectSignInfo("reload");
          }
        }
      } else {
        this.listLoading = false;
      }
    },
    // 删除印章
    deleteSign(seal) {
      let pic = document.querySelector("#" + seal.domId);
      pic.parentNode.removeChild(pic);
      let index = this.currentDocPosList.findIndex((i) => {
        return i.domId == seal.domId;
      });
      if (index > -1) {
        //大于0 代表存在,
        this.currentDocPosList.splice(index, 1); //存在就删除
        this.handlePos();
      }
    },
    // 印象回显处理
    _select(value) {
      console.log(value, 3636);
      if (this.currentDocPosList.length > 0) {
        this.delAllSeal();
      }
      this.handlePos();
      const sealList = this.signPosJson.map((item, index) => {
        return {
          ...value[0],
          domId: "dom-" + value[0].code + index,
          posPage: item.pageNo || item.posPage,
          posX: item.centerX || item.posX,
          posY: item.centerY || item.posY,
        };
      });
      let _sealList = [];
      if (this.signType == 0) {
        // 关键字签署类型
        switch (this.signerInfo.keywordSignType) {
          case 0: //首个
            _sealList = [sealList[0]];
            break;
          case 1: //倒数第一个
            _sealList = [sealList[sealList.length - 1]];
            break;
          case 2: //全部
            _sealList = sealList;
            break;
        }
      } else {
        _sealList = sealList;
      }
      this.currentDocPosList = [..._sealList];
      this.showBackSeal();
      this.handlePos();
    },
    //印章回显
    showBackSeal() {
      for (let i = 0; i < this.currentDocPosList.length; i++) {
        const element = this.currentDocPosList[i];
        console.log(element, 45454);
        const _box = document.getElementById(element.domId);
        // 如果已存在就不用添加了
        if (_box) break;
        const dom = document.createElement("div");
        dom.className = `sign-img back-seal drag-img`;
        dom.id = element.domId;
        dom.style.width = this.scale * element.seal_W + "px";
        dom.style.height = this.scale * element.seal_H + "px";
        dom.style["line-height"] = this.scale * 50 + "px";
        const wh = (this.scale * element.seal_W) / 2;
        dom.style.left = Number(element.posX) * this.scale - wh + "px";
        dom.style.bottom = Number(element.posY) * this.scale - wh + "px";
        dom.style["background-image"] = `url(${element.sealImg})`;
        dom.title = element.sealTypeName;
        dom.style["background-repeat"] = `no-repeat`;
        dom.style["background-size"] = `contain`;
        // 坐标签已预设位置的,印章回显后可进行印章替换操作
        if (this.signType == 2) {
          dom.onclick = this.changeBg;
        }
        dom.style.cursor = "pointer";
        document.querySelector("#can" + element.posPage).appendChild(dom);
      }
    },
    // 打开替换弹窗
    changeBg(e) {
      const _id = e.currentTarget.id;
      const isDom = this.currentDocPosList.find((item) => {
        return item.domId == _id;
      });
      this.selectSeal = {
        sealType: isDom.sealType,
        domId: isDom.domId,
        sealId: isDom.sealId,
      };
      this.replaceVisible = true;
    },
    // 印章替换处理
    _confirmReplace(visible, value) {
      this.replaceVisible = false;
      // 替换印章
      if (visible) {
        const dom = document.getElementById(this.selectSeal.domId);
        dom.style.backgroundImage = "url(" + value.sealImg + ")";
        const signedPicList = this.currentDocPosList.map((item) => {
          const isDom = item.domId == this.selectSeal.domId;
          let query = {
            ...item,
          };
          if (isDom) {
            query = { ...item, ...value, domId: this.selectSeal.domId };
          }
          return query;
        });
        this.currentDocPosList = [...signedPicList];
      }
    },
    _getPageIndex(y, dom) {
      let maxH = 0;
      let index = 1;
      let pdfHeight = 0;
      let _y = y || 0;
      let pdfWidth = 0;
      let margin = 0;
      let _space = 0;
      let pdfH = [];
      for (const key in this.pdfInfo) {
        if (_y < 0) {
          break;
        }
        const element = this.pdfInfo[key];
        pdfH.push(element.pdfHeight);
        if (element.index > 1) {
          // 外边距为10
          margin = (element.index - 1) * 10;
        }
        // 高度累计
        const _maxH = pdfH.reduce((last, i) => {
          return last + i;
        }, 0);
        maxH = _maxH + margin;
        // 当前页
        pdfWidth = element.pdfWidth;
        index = element.index;
        // 判断是处于空白区域(外边距)top为负数
        if (y > maxH && y < _space && element.index > 1) {
          _y = maxH - y + margin;
          break;
        }
        if (y < maxH) {
          if (element.index > 1) {
            _y = maxH - y - element.pdfHeight;
          }
          pdfHeight = element.pdfHeight - Math.abs(_y);
          break;
        }
        _space = maxH + margin;
      }
      return {
        index,
        top: Math.abs(_y),
        bottom: pdfHeight - dom.clientHeight,
        pdfWidth,
      };
    },
    // ed.clientX:鼠标焦点距离浏览器右侧的距离
    // dom.clientWidth / 2:印章图片宽度的一半
    // isMove:是否是再次移动的
    _getPageNum(ed, dom, isMove) {
      let isOver = true;
      const _clientWidth = parseInt(dom.clientWidth / 2);
      const _clientHeight = parseInt(dom.clientHeight / 2);
      const _left = ed.clientX - _clientWidth;
      const _top = ed.clientY - _clientHeight;
      // pdf展示区域距离左侧的距离+横向滚动条
      const _pdfBox = document.querySelector(".pdf-box");
      // 拿到左边坐标
      const _xLeft = _left + this.$refs.pdfBox.scrollLeft - _pdfBox.offsetLeft;
      const _yTop = _top + this.$refs.pdfBox.scrollTop - _pdfBox.offsetTop;
      const _index = this._getPageIndex(_yTop, dom);
      const _xRight = _index.pdfWidth - _xLeft - parseInt(dom.clientWidth);
      if (
        _xLeft >= 0 &&
        _index.bottom >= 0 &&
        _index.top >= 0 &&
        _xRight >= 0
      ) {
        isOver = true;
      } else {
        isOver = false;
      }
      // 判断再次移动的时候是否出界
      if (isMove) {
        const _clientHeight = document.querySelector(
          "#can" + _index.index
        ).clientHeight;
        const _distance = _clientHeight - dom.clientHeight - _index.bottom;
        isOver = _distance > 0 ? isOver && true : false;
      }
      return {
        isOver,
        top: _index.top,
        left: _xLeft,
        index: _index.index,
        bottom: _index.bottom,
        posX: (_xLeft + _clientWidth) / this.scale,
        posY: (_index.bottom + _clientHeight) / this.scale,
      };
    },
    // 坐标签拖拽处理
    _signPic(e, seal) {
      let checkRes = true;
      // 判断是否已加盖印章
      if (seal.sealType == "DATE_SEAL") {
        checkRes = this.checkSealDate();
      }
      if (!checkRes) return;
      // 印章的唯一标识
      const timerCount = new Date().getTime() + "";
      this.count++;
      let dom = document.createElement("div");
      dom.style.left = 0 + "px";
      dom.style.top = e.clientY + 1.5 * dom.clientHeight + "px";
      if (seal.sealType == "DATE_SEAL") {
        dom.style.height = this.scale * 16 + "px";
        dom.style.width = this.scale * 105 + "px";
        const getNowFormatDate = this.getNowFormatDate();
        dom.innerHTML = `<div class="delete" @click.stop="deleteIt">删除</div><span style="font-weight:bolder">${getNowFormatDate}</span>`;
      } else {
        dom.style.width = this.scale * seal.seal_W + "px";
        dom.style.height = this.scale * seal.seal_H + "px";
        dom.style["background-image"] = `url(${seal.sealImg})`;
        dom.innerHTML = `<div class="delete" @click.stop="deleteIt">删除</div>`;
      }
      dom.className = `sign-img drag-img`;
      const signDocumentId = this.activeDoc.signDocumentId.slice(
        this.activeDoc.signDocumentId.length - 6,
        this.activeDoc.signDocumentId.length
      );
      const domId = seal.sealType + signDocumentId + timerCount;
      dom.id = domId;
      document.querySelector("#pageContent").appendChild(dom);
      document.onmousemove = (e) => {
        dom.style.left = e.clientX - parseInt(dom.clientWidth / 2) + "px";
        dom.style.top = e.clientY - parseInt(dom.clientHeight / 2) + "px";
      };
      // 鼠标抬开
      document.onmouseup = (e) => {
        // 判断是否越界,未越界测返回坐标
        const _dom = this._getPageNum(e, dom);
        document.querySelector("#pageContent").removeChild(dom);
        if (!_dom.isOver) {
          document.onmousemove = null;
          document.onmouseup = null;
          this.$message.error("请将印章拖拽到合同区域");
          return;
        } else {
          dom.style.left = _dom.left + "px";
          dom.style.top = _dom.top + "px";
          document.querySelector("#can" + _dom.index).appendChild(dom);
          let docId = this.activeDoc.signDocumentId;
          // 若为加盖时间印章,则该时间印章的sealId、sealType拿可用印章第一个印章
          let selectSeal = this.sealList[0];
          let sealId =
            seal.sealType == "DATE_SEAL" ? selectSeal.sealId : seal.sealId;
          let sealType =
            seal.sealType == "DATE_SEAL" ? selectSeal.sealType : seal.sealType;
          let pos = {
            domId: domId,
            left: _dom.left * this.scale,
            top: _dom.top * this.scale,
            posX: _dom.posX,
            posY: _dom.posY,
            posPage: _dom.index,
            signDocumentId: docId,
            ...seal,
            sealId,
            sealType,
          };
          this.currentDocPosList.push(pos);
          this.handlePos();
        }
        document.onmousemove = null;
        document.onmouseup = null;
        dom.onmouseenter = this.mouseenter;
        dom.onmouseleave = this.mouseleave;
        dom.onmousedown = this.moveTo;
        dom.childNodes[0].onclick = this.deleteIt;
      };
    },
    moveTo(e) {
      let odiv = e.currentTarget; //获取目标元素
      const _domId = this.currentDocPosList.find((item) => {
        return item.domId == odiv.id;
      });
      let dragDom = "";
      document.onmousemove = (e) => {
        dragDom = this._getPageNum(e, odiv, true);
        odiv.style.left = dragDom.left + "px";
        odiv.style.top = dragDom.top + "px";
      };
      document.onmouseup = (e) => {
        document.onmousemove = null;
        document.onmouseup = null;
        if (dragDom && !dragDom.isOver) {
          odiv.style.left = _domId.left / this.scale + "px";
          odiv.style.top = _domId.top / this.scale + "px";
          this.$message.error("请将印章拖拽到合同区域");
          return;
        } else {
          const _index = this.currentDocPosList.findIndex((item) => {
            return item.domId == odiv.id;
          });
          this.currentDocPosList[_index].left = dragDom.left * this.scale;
          this.currentDocPosList[_index].top = dragDom.top * this.scale;
          this.currentDocPosList[_index].posX = dragDom.posX;
          this.currentDocPosList[_index].posY = dragDom.posY;
        }
      };
    },
    // 查询详情
    async selectSignInfo(code) {
      this.listLoading = true;
      const query = {
        signBatchId: this.userData.signBatchId,
      };
      const { data: res } = await selectSignInfoH5Sign(query);
      if (res.code == 0) {
        const document = res.data.documentList.filter((item) => {
          return item.signDocumentId == this.userData.signDocumentId;
        })[0];
        this._getFileSteam(document.signCurrentUrl);
      }
      this.listLoading = false;
    },
  },
  computed: {
    currentCompanyId() {
      return this.$store.state.currentCompanyId;
    },
  },
  created() {
    const query = this.$route.query;
    this._getAnalysisCode(query.code);
  },
  mounted() {
    this.$nextTick(function () {
      this.$refs.pdfBox.addEventListener("scroll", this.handleScroll, false);
    });
  },
};
</script>
<style lang="scss">
.back-seal {
  position: absolute;
  cursor: move;
  z-index: 7;
  // overflow: hidden;
}
.delete {
  width: 100%;
  position: absolute;
  bottom: -20px;
  text-align: center;
  color: #fff;
  font-size: 16px;
  background-color: rgba(236, 49, 49, 0.7);
  cursor: pointer;
  display: none;
}
.nameBox {
  margin-top: 10px;
  width: 100%;
  font-size: 14px;
  text-align: center;
  color: #333;
}
.sign-img {
  position: absolute;
  cursor: move;
  z-index: 7;
  // overflow: hidden;
  background-repeat: no-repeat;
  background-size: contain;
  background-position: 100%;
}
</style>

<style lang="scss">
.headerBox {
  // position: fixed;
  // top: 0;
  width: 100%;
  background: #fff;
  z-index: 999;
  .doc-title-list {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 60px;
    .active-doc {
      border: none;
      color: #444;
      font-size: 22px;
    }
  }
  .page-title {
    margin-left: 15px;
  }
  .sign-btn {
    position: absolute;
    top: 12px;
    right: 20px;
  }
}
.cont {
  margin-top: 20px;
  display: flex;
  .item {
    margin: 0 0 25px 0;
  }
}
.left-area-container,
.right-area-container {
  // width: 340px;
  width: 20%;
  overflow: auto;
  font-size: 14px;
  background: #fff;
}
.sign-title {
  height: 55px;
  line-height: 55px;
  background: #0091ff;
  border-radius: 5px;
  color: #fff;
  margin-bottom: 5px;
  font-size: 16px;
}
.sign-parties {
  padding: 0 5px;
}

.instructBox {
  text-align: left;
  color: #999;
  line-height: 22px;
  p {
    margin-top: 5px;
  }
}
.signed-sum li {
  text-align: left;
  cursor: auto;
}
.signed-sum-number {
  font-size: 16px;
  color: #0096ff;
}
.right-area-container .sign-title {
  li {
    width: 33.33%;
  }
}
.right-area-container .pages {
  ul {
    background: #fff;
    border-radius: 5px;
    margin-bottom: 5px;
    li {
      width: 33.33%;
      margin: 0;
    }
  }
}

.delete {
  width: 100%;
  position: absolute;
  bottom: -20px;
  text-align: center;
  color: #fff;
  font-size: 16px;
  background-color: rgba(236, 49, 49, 0.7);
  cursor: pointer;
  display: none;
}
.nameBox {
  margin-top: 10px;
  width: 100%;
  font-size: 14px;
  text-align: center;
  color: #333;
}

.viewerContainer {
  display: flex;
  height: 91vh;
  width: 80%;
  overflow: auto;
  user-select: none;
  /*firefox浏览器*/
  -moz-user-select: none;
  /*safari、chrome浏览器*/
  -webkit-user-select: none; /*Safari中不支持该属性值,只能使用none或者text,或者是在html的标签属性中使用*/
  .input {
    border: none;
    outline: none;
    width: 100%;
  }
}

.pdfContent_1PW2f {
  width: 700px;
  flex: 1;
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: auto;
  // background: #f5f5f5;
  .pdf-header {
    /*position: fixed;*/
    top: 55px;
    left: 240px;
    right: 240px;
    height: 40px;
    border-bottom: 1px solid #eee;
    background-color: #fff;
    z-index: 100;
    text-align: center;
    display: flex;
    .block {
      flex: 1;
      display: flex;
      .demonstration,
      .slider {
        flex: 0 0 150px;
        line-height: 40px;
        width: 150px;
        text-align: right;
      }
      .demonstration {
        flex: 1;
        margin-right: 20px;
      }
    }
    .input {
      flex: 1;
      text-align: left;
      line-height: 40px;
      margin-left: 20px;
      .input-text {
        border: none;
        outline: none;
        border-bottom: 1px solid #e4e7ed;
        margin-left: 10px;
        width: 25px;
        text-align: center;
      }
    }
  }
  .pdf-container {
    display: inline-block;
    overflow: auto;
    height: 100%;
    width: 100%;
    .pdf-box {
      position: relative;
      margin: 0 auto;
      .sign-img {
        z-index: 4;
      }
    }
  }
  .pdfPage_1yRne {
    /*transition: left .3s;*/
    position: relative;
    margin: 10px 0;
  }
  .dragLayer_3ccsq {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
  }
}
.signed-list {
  padding: 0 15px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  span {
    flex: 1;
  }
}
.signed-item {
  margin: 10px 0;
  border: 1px dashed rgba(0, 0, 0, 0.3);
}
</style>

组件代码DragSeal:

<template>
  <div>
    <div class="sign-fields item">
      <div class="sign-title">请选择印章类型</div>
      <div class="sign-list">
        <div class="seal-box-wrap">
          <div
            v-for="seal in sealList"
            :key="seal.code"
            v-bind:id="seal.code"
            @mousedown="_dragSeal($event, seal)"
          >
            <template v-if="seal.isSealDate == 0">
              <div
                class="seal-img"
                :style="{
                  height: 130 + 'px',
                  width: 130 + 'px',
                  backgroundImage: 'url(' + seal.sealImg + ')',
                }"
              ></div>
            </template>
            <div
              v-else
              class="seal-date"
              :style="{
                height: 130 + 'px',
                width: 130 + 'px',
                lineHeight: 130 + 'px',
              }"
            >
              {{ getNowFormatDate() }}
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="sign-fields item instructBox">
      <div>
        <div>设置签章位置说明</div>
        <p>1、告知签署方需要在文件具体哪一处进行加盖电子印章</p>
      </div>
      <div>
        操作指示:鼠标选中上方签约方信息,鼠标左键单击选择印章/时间戳,查找正文内容盖章位置单击左键
      </div>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    sealList: {
      type: Array,
      default: () => {
        return [];
      },
    },
  },
  components: {},
  data() {
    return {};
  },
  methods: {
    _dragSeal(e, seal) {
      this.$emit("drag", e, seal);
    },
    getNowFormatDate() {
      let date = new Date(),
        year = date.getFullYear(), //获取完整的年份(4位)
        month = date.getMonth() + 1, //获取当前月份(0-11,0代表1月)
        strDate = date.getDate(); // 获取当前日(1-31)
      if (month < 10) month = `0${month}`; // 如果月份是个位数,在前面补0
      if (strDate < 10) strDate = `0${strDate}`; // 如果日是个位数,在前面补0
      return `${year}${month}${strDate}`;
    },
  },
  mounted() {},
};
</script>
<style lang='scss'>
.sign-fields .sign-list {
  padding: 0 20px;
}
.seal-box-wrap {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  .seal-box {
    width: 100%;
    border-radius: 5px;
    padding: 10px;
    margin-top: 15px;
    background-color: #fff;
  }
  .seal-img {
    background-size: contain;
    background-repeat: no-repeat;
    background-position: 100%;
    border: 1px solid rgb(225, 225, 226);
    margin: 8px 0;
  }
  .seal-date {
    text-align: center;
    font-weight: bolder;
    border: 1px solid rgb(225, 225, 226);
  }
}
</style>

组件代码KeySeal:

<template>
  <div class="sign-fields item">
    <div class="sign-title">请选择印章类型</div>
    <div class="sign-list-vertical">
      <el-radio-group v-model="selectSeal" @input="_selectSeal">
        <el-radio
          v-for="seal in showList"
          :key="seal.code"
          v-bind:id="seal.code"
          v-model="seal.code"
          :label="seal.code"
          border
        >
          <template v-if="seal.sealType != 'DATE_SEAL'">
            <span class="seal-name">{{ seal.sealTypeName }}</span>
            <div
              class="seal-img"
              :style="{
                height: 100 + 'px',
                width: 100 + 'px',
                backgroundImage: 'url(' + seal.sealImg + ')',
              }"
            ></div>
          </template>
        </el-radio>
      </el-radio-group>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    sealList: {
      type: Array,
      default: () => {
        return [];
      },
    },
  },
  components: {},
  data() {
    return {
      selectSeal: "",
      showList: [],
    };
  },
  watch: {
    sealList: {
      handler(newVal, oldVal) {
        this.showList = newVal.filter((item) => {
          return item.isSealDate == 0;
        });
      },
      immediate: true,
      deep: true,
    },
  },
  methods: {
    _selectSeal(value) {
      const _seal = this.sealList
        .filter((item) => {
          return item.code == value;
        })
        .map((item) => {
          return {
            ...item,
            posX: 0,
            posY: 0,
            posPage: 0,
          };
        });
      this.$emit("select", _seal);
    },
    getNowFormatDate() {
      let date = new Date(),
        year = date.getFullYear(), //获取完整的年份(4位)
        month = date.getMonth() + 1, //获取当前月份(0-11,0代表1月)
        strDate = date.getDate(); // 获取当前日(1-31)
      if (month < 10) month = `0${month}`; // 如果月份是个位数,在前面补0
      if (strDate < 10) strDate = `0${strDate}`; // 如果日是个位数,在前面补0
      return `${year}${month}${strDate}`;
    },
  },
  mounted() {},
};
</script>
<style lang='scss'>
.sign-list-vertical {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  margin-top: 10px;
  .seal-item {
    margin-bottom: 15px;
  }
  .el-radio.is-bordered {
    width: 100%;
    height: auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    padding: 10px;
  }
  .el-radio__label {
    display: flex;
    justify-content: space-around;
    align-items: center;
  }
}
.seal-name {
  margin-right: 15px;
}
.seal-img {
  background-size: contain;
  background-repeat: no-repeat;
  background-position: 100%;
}
.el-radio.is-bordered + .el-radio.is-bordered {
  margin-left: 0;
}
</style>

组件代码ReplaceSeal:

<template>
  <BaseDialog
    v-if="dialogVisible"
    :dialogVisible="dialogVisible"
    title="替换印章"
    @closeDialog="_closeDialog"
    @confirmDialog="_confirmSeal"
  >
    <span>选择印章:</span>
    <el-select
      v-model="changeSeal"
      placeholder="请选择"
      style="width: 80%"
      value-key="sealId"
    >
      <el-option
        v-for="item in showList"
        :key="item.sealId"
        :label="item.sealTypeName"
        :value="item"
        :disabled="item.sealId == sealId"
      >
        <span style="float: left">{{ item.sealTypeName }}</span>
        <span style="float: right; color: #8492a6; font-size: 13px">{{
          item.sealId == sealId ? "当前印章" : ""
        }}</span>
      </el-option>
    </el-select></BaseDialog
  >
</template>
<script>
const BaseDialog = () => import("@/components/BaseDialog.vue");
export default {
  props: {
    dialogVisible: {
      type: Boolean,
      default: false,
    },
    sealList: {
      type: Array,
      default: () => {
        return [];
      },
    },
    sealId: {
      type: String,
      default: "",
    },
  },
  components: { BaseDialog },
  data() {
    return {
      changeSeal: "",
      showList: [],
    };
  },
  watch: {
    sealList: {
      handler(newVal, oldVal) {
        this.showList = newVal.filter((item) => {
          return item.isSealDate == 0;
        });
      },
      immediate: true,
      deep: true,
    },
  },
  methods: {
    _closeDialog() {
      this.$emit("confirm", false);
    },
    _confirmSeal() {
      if (!this.changeSeal) {
        this.$message.warning("请先选择印章!");
        return;
      }
      this.$emit("confirm", true, this.changeSeal);
    },
  },
  mounted() {},
};
</script>
<style lang='scss' scoped></style>

组件代码ShowSeal:

<template>
  <div>
    <div class="sign-parties item">
      <div class="sign-title">{{ signType == 1 ? "添加" : "选" }}印章项</div>
      <div class="sign-list">
        <div v-for="seal in sealList" :key="seal.code" class="show-seal">
          <template v-if="seal.num > 0">
            <span> {{ seal.sealTypeName }} </span>
            <span class="primaryText"> {{ seal.num }} </span></template>
        </div>
      </div>
    </div>
    <div class="sign-fields item">
      <div class="sign-title signed-list">
        <span>签章类型</span>
        <span>页码</span>
        <span>操作</span>
      </div>
      <div class="sign-list pages">
        <div
          class="signed-list signed-item"
          v-for="a in selPosList"
          :key="a.domId"
          :title="a.sealTypeName"
        >
          <span
            ><span> {{ a.sealTypeName }}</span>
            <span v-if="a.code == 'date'">-盖章时间</span></span
          >
          <span>{{ a.posPage }}</span>
          <span
            v-if="signType == 1"
            class="el-icon-delete"
            @click="deleteSign(a)"
          ></span>
          <span v-else></span>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    sealList: {
      type: Array,
      default: () => {
        return [];
      },
    },
    selPosList: {
      type: Array,
      default: () => {
        return [];
      },
    },
    signType: {
      type: Number,
      default: 1,
    },
  },
  components: {},
  data() {
    return {};
  },
  methods: {
    deleteSign(a) {
      this.$emit("delete", a);
    },
  },
  mounted() {},
};
</script>
<style lang='scss' scoped>
.sign-list {
  max-height: 580px;
  overflow-y: auto;
  .show-seal {
    padding-left: 10px;
    text-align: left;
    margin-bottom: 5px;
    font-size: 16px;
  }
}
</style>
<think>我们被要求设计一个在Vue应用中实现拖拽PDF并进行盖章功能的技术方案。根据引用[1],我们可以使用vue-pdf库来处理PDF的预览和操作。同时,引用[3]提到需要支持拖拽上传,并且要求开源免费,支持多种环境。 技术方案步骤: 1. 拖拽上传PDF:使用HTML5的拖拽API或第三方Vue拖拽组件(如vuedraggable)来实现文件拖拽上传。 2. 预览PDF:使用vue-pdf库(基于pdf.js)来预览PDF文件。 3. 添加电子章:在PDF预览上叠加一个可拖拽的电子章图片,并记录其位置,然后使用pdf.js的API将电子章绘制到PDF的指定位置,最后生成新的PDF文件。 详细步骤: 步骤1:拖拽上传PDF - 在Vue组件中,创建一个区域用于拖拽上传。 - 监听拖拽事件(dragover, dragleave, drop)来获取文件。 - 使用FileReader读取PDF文件,并转换为ArrayBuffer或URL对象以供预览。 步骤2:使用vue-pdf预览PDF - 安装vue-pdf:`npm install vue-pdf --save` - 在组件中引入vue-pdf的pdf组件,并将上传的PDF文件传递给该组件进行预览。 步骤3:添加电子章 - 准备一个电子章图片(透明背景的PNG)。 - 在预览区域上方叠加一个可拖拽的电子章图标(使用绝对定位,通过拖拽事件改变其位置)。 - 记录电子章在预览页面上的位置(相对于PDF页面的坐标)。 - 使用pdf.js的API(通过vue-pdf暴露的pdf对象)获取PDF文档对象,然后在指定页的指定位置绘制电子章图片。 - 将修改后的PDF保存为新的文件。 注意:由于在浏览器中直接编辑PDF比较复杂,我们可能需要使用pdf-lib(一个强大的PDF操作库)来添加电子章,因为vue-pdf本身主要提供预览功能,编辑功能有限。 因此,我们调整步骤3: - 当用户拖拽电子章到预览的PDF页面上时,记录电子章在页面上的位置(相对于PDF页面的坐标,需要将屏幕坐标转换为PDF页面的坐标)。 - 使用pdf-lib库来加载原始PDF,然后在指定页的指定位置绘制电子章图片,并保存修改后的PDF。 具体实现: 1. 拖拽上传区域(使用HTML5拖拽): ```html <div @dragover.prevent="dragover" @dragleave="dragleave" @drop.prevent="drop" :class="{'dragover': isDragover}" > 拖拽PDF文件到这里 </div> ``` 2. 预览PDF(使用vue-pdf): ```html <pdf :src="pdfUrl" v-for="page in numPages" :key="page" :page="page"></pdf> ``` 3. 电子章拖拽放置: - 在预览区域上放置一个透明的电子章图片,并使其可拖拽(使用v-drag指令或直接监听鼠标事件)。 - 当电子章被拖拽到预览区域时,计算电子章相对于当前PDF页面的位置(注意:PDF页面可能被缩放,需要转换坐标)。 4. 使用pdf-lib添加电子章: - 安装pdf-lib:`npm install pdf-lib` - 在用户确认盖章位置后,获取原始PDF的ArrayBuffer,然后使用pdf-lib加载。 - 将电子章图片绘制到PDF的指定页面和指定位置(需要知道PDF页面的尺寸和电子章的位置比例)。 - 保存修改后的PDF。 代码结构示例: 组件模板: ```html <template> <div> <!-- 拖拽上传区域 --> <div class="drop-area" @dragover.prevent="onDragover" @dragleave="onDragleave" @drop.prevent="onDrop" :class="{ 'dragover': isDragover }" > 拖拽PDF文件到此处上传 </div> <!-- 预览区域 --> <div v-if="pdfDoc" class="preview-area" ref="previewArea"> <pdf v-for="page in numPages" :key="page" :src="pdfUrl" :page="page" @num-pages="numPages = $event" @loaded="onPageLoaded(page, $event)" ref="pdfPages" ></pdf> <!-- 电子章,可拖拽,使用v-show控制显示,初始不显示,当有PDF时显示 --> <img v-show="showStamp" src="@/assets/stamp.png" class="stamp" :style="{ left: stampPosition.x + 'px', top: stampPosition.y + 'px' }" draggable="true" @dragstart="onStampDragStart" @drag="onStampDrag" @dragend="onStampDragEnd" /> </div> <button @click="addStamp">确认盖章</button> </div> </template> ``` 组件脚本: ```javascript import { pdfjsLib } from 'vue-pdf' import { PDFDocument } from 'pdf-lib' export default { data() { return { isDragover: false, pdfUrl: null, pdfDoc: null, numPages: 0, stampPosition: { x: 0, y: 0 }, // 电子章在预览区域的位置 stampPage: 1, // 电子章所在的页码 showStamp: false, // 是否显示电子章 // 其他数据... } }, methods: { onDragover() { this.isDragover = true }, onDragleave() { this.isDragover = false }, onDrop(event) { this.isDragover = false const files = event.dataTransfer.files if (files.length && files[0].type === 'application/pdf') { const file = files[0] const fileUrl = URL.createObjectURL(file) this.pdfUrl = fileUrl // 存储文件对象,后续用于pdf-lib this.pdfFile = file this.showStamp = true // 显示电子章 } }, onPageLoaded(page, pdfPage) { // 可以在这里获取页面信息,比如页面尺寸 const viewport = pdfPage.getViewport({ scale: 1 }) this.pageInfo = { width: viewport.width, height: viewport.height } }, onStampDragStart(event) { // 记录拖拽开始的位置 this.dragStart = { x: event.clientX, y: event.clientY } }, onStampDrag(event) { if (event.clientX !== 0 && event.clientY !== 0) { // 计算移动距离 const dx = event.clientX - this.dragStart.x const dy = event.clientY - this.dragStart.y // 更新电子章位置 this.stampPosition.x += dx this.stampPosition.y += dy // 更新拖拽起点 this.dragStart = { x: event.clientX, y: event.clientY } } }, onStampDragEnd() { // 拖拽结束,可以记录最终位置 }, async addStamp() { if (!this.pdfFile) return // 读取PDF文件 const arrayBuffer = await this.pdfFile.arrayBuffer() const pdfDoc = await PDFDocument.load(arrayBuffer) // 加载电子章图片 const stampImage = await this.loadStampImage() // 这个方法需要实现,返回一个图片的ArrayBuffer或URL,然后使用pdfDoc嵌入图片 const pngImage = await pdfDoc.embedPng(stampImage) // 获取当前PDF页面的尺寸(注意:这里我们使用之前记录的页面信息,但注意pdf-lib的页面尺寸单位是点(1/72英寸)) const pages = pdfDoc.getPages() // 假设我们只在第一页盖章 const page = pages[0] // 页码从0开始 const { width, height } = page.getSize() // 将电子章在预览区域的位置转换为PDF页面上的位置(需要坐标转换) // 预览区域显示的PDF页面尺寸可能与原始尺寸不同(有缩放),所以需要转换 // 转换公式:x_pdf = (x_preview / previewWidth) * width, y_pdf = height - (y_preview / previewHeight) * height - stampHeight (因为PDF坐标原点在左下角,而预览区域在左上角) const previewRect = this.$refs.previewArea.getBoundingClientRect() const stampImg = new Image() stampImg.src = require('@/assets/stamp.png') const stampWidth = stampImg.width const stampHeight = stampImg.height // 计算在PDF页面上的位置 // 注意:预览区域可能包含多个页面,这里我们假设预览区域当前只显示第一页(或者我们记录的是第一页上的位置) const x = (this.stampPosition.x / previewRect.width) * width // 由于预览区域原点在左上角,而PDF原点在左下角,所以y坐标需要翻转 const y = height - (this.stampPosition.y / previewRect.height) * height - (stampHeight * (width / previewRect.width)) // 注意:这里我们假设预览区域高度和PDF页面高度成比例,并且电子章高度需要按比例转换 // 在PDF页面上绘制电子章 page.drawImage(pngImage, { x: x, y: y, width: stampWidth * (width / previewRect.width), // 调整电子章大小 height: stampHeight * (height / previewRect.height) }) // 保存修改后的PDF const pdfBytes = await pdfDoc.save() // 将pdfBytes转换为Blob并提供下载 const blob = new Blob([pdfBytes], { type: 'application/pdf' }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = 'stamped.pdf' link.click() }, async loadStampImage() { // 假设电子章图片在public目录下 const response = await fetch('/stamp.png') return response.arrayBuffer() } } } ``` 注意:坐标转换是难点,因为预览的PDF可能被缩放,而且预览区域可能包含多个页面。上面的转换是一个简化的示例,实际中可能需要更精确的转换,特别是当预览区域有滚动或多页时。 另外,电子章的拖拽功能也可以使用第三方拖拽库(如vuedraggable)来简化。 引用: [^1]: 我们参考了vue-pdf的集成方法,用于PDF的预览。 [^2]: 拖拽上传的实现参考了HTML5拖拽API的标准用法。 [^3]: 电子章的位置转换和添加参考了pdf-lib的官方文档。 相关问题: 1. 如何精确计算电子章在PDF页面上的位置? 2.Vue中如何实现多页PDF的预览和分别盖章? 3. 除了pdf-lib,还有哪些库可以在浏览器中编辑PDF?</think>### Vue 实现拖拽上传PDF并添加电子章的技术方案 #### 一、核心功能实现方案 1. **拖拽上传PDF** - 使用 `vuedraggable` + `dropzone` 实现拖拽区域 ```vue <div @drop.prevent="handleDrop" @dragover.prevent> <p>拖拽PDF文件到此处</p> </div> ``` ```javascript handleDrop(e) { const files = e.dataTransfer.files; if (files[0]?.type === 'application/pdf') { this.pdfFile = files[0]; this.renderPDF(); // 渲染PDF预览 } } ``` 2. **PDF预览与操作** - 使用 **vue-pdf-embed** 库(基于 PDF.js) ```bash npm install vue-pdf-embed ``` ```vue <vue-pdf-embed :source="pdfUrl" ref="pdfRef" /> ``` 3. **电子章功能实现** ```vue <!-- 电子章拖拽组件 --> <div v-for="(stamp, index) in stamps" :key="index" draggable @dragstart="startDrag($event, stamp)" > <img :src="stamp.image" width="80"/> </div> <!-- PDF预览区域 --> <div @dragover.prevent @drop="onDropStamp"> <vue-pdf-embed :source="pdfUrl" /> </div> ``` ```javascript onDropStamp(e) { const stamp = JSON.parse(e.dataTransfer.getData('stamp')); const rect = e.target.getBoundingClientRect(); const position = { x: e.clientX - rect.left, y: e.clientY - rect.top, page: this.currentPage // 当前PDF页码 }; this.addStampToPDF(stamp, position); } ``` #### 二、后端集成方案 1. **电子章生成** - 使用 **pdf-lib** 库添加电子章图层 ```javascript import { PDFDocument } from 'pdf-lib'; async addStampToPDF(stamp, position) { const pdfDoc = await PDFDocument.load(this.pdfData); const page = pdfDoc.getPage(position.page - 1); const stampImage = await pdfDoc.embedPng(stamp.imageData); page.drawImage(stampImage, { x: position.x, y: page.getHeight() - position.y - stamp.height, width: stamp.width, height: stamp.height }); this.modifiedPDF = await pdfDoc.save(); // 保存修改后的PDF } ``` 2. **数字签名验证(可选)** - 使用 **node-forge** 生成数字证书 - 添加时间戳服务器验证 #### 三、完整技术栈推荐 | 功能 | 推荐方案 | 说明 | |---------------|-----------------------------------|-------------------------------| | PDF操作 | pdf-lib + vue-pdf-embed | 浏览器端PDF处理 | | 拖拽交互 | vuedraggable + HTML5 Drag API | 支持跨元素拖拽 | | 电子章管理 | Canvas 生成动态印章 | 可自定义文本/图形 | | 文件存储 | AWS S3/MinIO + 签名URL | 安全存储PDF文件 | | 数字签名 | node-forge + 时间戳服务 | 符合电子签名法要求 | #### 四、安全增强措施 1. **前端安全** ```javascript // 文件类型校验 if (!file.type.match('application/pdf.*')) { alert('仅支持PDF文件'); return; } ``` 2. **后端验证** - 使用 JWT 验证用户操作权限 - 电子章添加记录存证区块链(Hyperledger Fabric) #### 五、部署注意事项 1. **信创环境适配** - 龙芯架构需编译原生模块:`npm rebuild --arch=loong64` - 麒麟OS使用容器化部署确保兼容性 2. 性能优化 - Web Worker 处理大型PDF - 分页加载(每页单独渲染) > **实现效果**:用户拖拽PDF到指定区域 → 预览PDF → 拖拽电子章到文档指定位置 → 生成带数字签名的PDF文件 → 下载/分享[^1][^2] --- ### 相关问题 1. 如何实现电子章的数字签名合法性验证? 2. 在信创环境下(如龙芯架构)部署PDF处理服务需要注意什么? 3. 如何优化大尺寸PDF(100+页)在浏览器中的渲染性能? 4. 有哪些开源的电子签章协议标准可以集成? 5. 如何防止PDF文件被篡改?有哪些水印技术可用? [^1]: vue-pdf-embed 提供了灵活的PDF渲染API,支持页面级操作和自定义图层 [^2]: pdf-lib 的文档操作API支持精确到坐标点的内容添加,符合电子签章定位需求
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值