gaoluyang
5 天以前 c7566bba45e6f90594d36ec4b80ac4ff89f577ee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
<template>
  <div class="up-pdf-stamp">
    <div v-if="isUpFile" class="work" style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;">
      <input ref="fileInput" type="file" accept="application/pdf" @change="handleFileUpload" />
    </div>
    <div v-if="canvasNumPages > 0" style="color: red;font-size: 12px;margin: 16px 0;">提示:在文件范围内,单击鼠标盖章,双击鼠标已盖好章处可删除当前章</div>
    <canvas
      v-for="(item, index) in canvasNumPages"
      :key="index"
      :ref="el => setCanvasRef(el, index)"
      style="border: 1px solid #000;"
      @click="e => handleCanvasClick(e, index)"
      @dblclick="e => removeStamp(e, index)"
    ></canvas>
  </div>
</template>
 
<script>
import jsPDF from 'jspdf'
 
// 原实现用 require("@/assets/stamps/" + name + ".png"),Vite 里没有 require;
// 用 import.meta.glob 把印章图打成一张 名字→URL 的表,效果等价。
const STAMP_MODULES = import.meta.glob('@/assets/stamps/*.png', { eager: true, query: '?url', import: 'default' })
 
export default {
  props: ['isUpFile'],
  data() {
    return {
      pdfDoc: null, // 存储上传的 PDF 数据
      stamps: [], // 记录盖章的位置
      contextList: [], //canvas列表
      canvasEls: [], // canvas 元素列表(Vue3 的 v-for + ref 不再自动收集成数组,改函数 ref 收集)
      canvasNumPages: 0, // 存储 PDF 总页数
      stampWidth: 120, // 盖章宽度
      stampHeight: 80, // 盖章高度
      stampsName: '',
      stampsList: ['主任', '质量负责人', '技术负责人'],
      fileName: '文件名'
    }
  },
  methods: {
    // Vue3 里 v-for 上的 ref 必须用函数形式,否则 $refs.pdfCanvas 不会是数组
    setCanvasRef(el, index) {
      // markRaw:canvas 是 DOM 节点,进了响应式系统会变成 Proxy,
      // 之后 canvas.getContext() 会抛 "Illegal invocation"
      this.canvasEls[index] = el ? markRaw(el) : null
    },
    handleFileUpload(event) {
      const file = event.target.files[0]
      if (file.size > 20 * 1024 * 1024) {
        this.$refs.fileInput.value = '' // 清空文件输入框内容
        return this.$message.error('文件大小不能超过20M')
      }
      this.lookFile(file)
    },
    lookFile(file, currentStamp) {
      this.fileName = file.name
      if (currentStamp) {
        this.stampsName = currentStamp
      } else {
        // 原实现写的是 indexOf(回调),数组的 indexOf 不做函数匹配,结果恒为 -1,
        // 于是这里永远落到“综合室”。按作者本意改为 findIndex,按文件名匹配印章。
        const index = this.stampsList.findIndex(m => file.name.includes(m))
        if (index > -1) {
          this.stampsName = this.stampsList[index]
        } else {
          this.stampsName = '综合室'
        }
      }
      if (file && file.type === 'application/pdf') {
        const reader = new FileReader()
        reader.onload = e => {
          const typedArray = new Uint8Array(e.target.result)
          this.loadPDF(typedArray)
        }
        reader.readAsArrayBuffer(file)
      } else {
        this.$message.error('请选择 PDF 文件')
      }
    },
    loadPDF(typedArray) {
      pdfjsLib.getDocument(typedArray).promise.then(pdfDoc_ => {
        this.pdfDoc = pdfDoc_
        this.canvasNumPages = this.pdfDoc._pdfInfo.numPages
        this.stamps = []
        this.contextList = []
        for (let i = 1; i <= this.canvasNumPages; i++) {
          this.$nextTick(() => {
            this.renderPage(i) // 渲染页面
          })
          this.stamps.push([])
        }
      })
    },
    // 渲染指定页面
    renderPage(pageNum) {
      this.pdfDoc.getPage(pageNum).then(page => {
        const canvas = this.canvasEls[pageNum - 1]
        if (!canvas) return
        // 原实现是 push,位置取决于各页 getPage 的 Promise 完成顺序,
        // 而其它方法一律按 [pageNum-1] 取值;改成按下标写,顺序就无关了。
        this.contextList[pageNum - 1] = canvas.getContext('2d')
        const viewport = page.getViewport({ scale: 1.5 })
 
        canvas.width = viewport.width
        canvas.height = viewport.height
 
        page
          .render({
            canvasContext: this.contextList[pageNum - 1],
            viewport: viewport
          })
          .promise.then(() => {
            this.stamps[pageNum - 1].forEach(m => {
              this.drawStamps(m.x, m.y, pageNum - 1)
            })
          })
      })
    },
    // 单击--添加章
    handleCanvasClick(event, i) {
      const x = event.offsetX
      const y = event.offsetY
      const index = this.stamps[i].findIndex(stamp => {
        const x0 = x - stamp.x
        const y0 = y - stamp.y
        return x0 > 0 && x0 < this.stampWidth && y0 > 0 && y0 < this.stampHeight
      })
      if (index > -1) return
      this.drawStamps(x, y, i)
      this.stamps[i].push({ x, y })
    },
    // 双击--删除盖章
    removeStamp(event, i) {
      const x = event.offsetX
      const y = event.offsetY
      // 查找被双击的盖章
      const index = this.stamps[i].findIndex(stamp => {
        const x0 = x - stamp.x
        const y0 = y - stamp.y
        return x0 > 0 && x0 < this.stampWidth && y0 > 0 && y0 < this.stampHeight
      })
      if (index === -1) return
      this.stamps[i].splice(index, 1) // 删除指定的盖章
      this.contextList[i].clearRect(0, 0, this.contextList[i].width, this.contextList[i].height)
      this.renderPage(i + 1)
    },
    // 渲染章
    drawStamps(x, y, index) {
      const src = this.stampUrl(this.stampsName)
      if (!src) {
        console.warn(`[UpPdfStamp] 未找到印章图片:${this.stampsName}.png`)
        return
      }
      const img = new Image()
      img.src = src
      const that = this
      img.onload = function () {
        // 图片加载完成后,将图片绘制到canvas上
        that.contextList[index].drawImage(img, x, y, that.stampWidth, that.stampHeight)
      }
    },
    stampUrl(name) {
      const suffix = `/${name}.png`
      const key = Object.keys(STAMP_MODULES).find(k => k.endsWith(suffix))
      return key ? STAMP_MODULES[key] : ''
    },
    // 生成 PDF 的函数
    async generatePDF() {
      if (this.contextList.length === 0) {
        this.$message({ message: '请先上传PDF文件', type: 'error' })
        this.$emit('uploadPDFErr')
        return false
      }
      const pdf = new jsPDF('p', 'mm', 'a4')
      for (let i = 0; i < this.contextList.length; i++) {
        const imgData = this.canvasEls[i].toDataURL('image/jpeg', 0.7)
        const pdfWidth = pdf.internal.pageSize.getWidth()
        const pdfHeight = (this.canvasEls[i].height * pdfWidth) / this.canvasEls[i].width
        pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight) // 将图片添加到 PDF
        if (i !== this.contextList.length - 1) {
          pdf.addPage() // 添加新的一页
        }
      }
 
      // 将 PDF 文件保存或上传
      const pdfOutput = pdf.output('blob') // 获取 PDF 文件的 Blob 对象
 
      // 上传到后端
      return this.$emit('uploadPDF', pdfOutput, this.fileName)
    }
  }
}
</script>
 
<style scoped></style>