办公V3
This commit is contained in:
231
klp-ui/src/components/PdfStamper/index.vue
Normal file
231
klp-ui/src/components/PdfStamper/index.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="pdf-stamper">
|
||||
<div class="toolbar">
|
||||
<el-button size="mini" @click="prevPage" :disabled="pageNo <= 1">上一页</el-button>
|
||||
<span class="page">第 {{ pageNo }} / {{ pageCount }} 页</span>
|
||||
<el-button size="mini" @click="nextPage" :disabled="pageNo >= pageCount">下一页</el-button>
|
||||
<span class="split"></span>
|
||||
<el-input-number size="mini" v-model="stampWidth" :min="10" :max="600" :step="5" />
|
||||
<span class="wh">宽</span>
|
||||
<el-input-number size="mini" v-model="stampHeight" :min="10" :max="600" :step="5" />
|
||||
<span class="wh">高</span>
|
||||
<span class="hint">在 PDF 上单击选择盖章位置(以页面左下为原点)</span>
|
||||
</div>
|
||||
|
||||
<div class="canvas-wrap" ref="wrap" @click="onClick">
|
||||
<canvas ref="canvas"></canvas>
|
||||
<div
|
||||
v-if="hasStamp"
|
||||
class="stamp-box"
|
||||
:style="stampBoxStyle"
|
||||
title="盖章位置预览"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as pdfjsLib from 'pdfjs-dist'
|
||||
// worker
|
||||
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.js'
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
|
||||
|
||||
export default {
|
||||
name: 'PdfStamper',
|
||||
props: {
|
||||
/** PDF 的可访问 URL(OSS url) */
|
||||
pdfUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
/** 初始页 */
|
||||
initialPage: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
pdf: null,
|
||||
pageCount: 1,
|
||||
pageNo: 1,
|
||||
// 渲染缩放
|
||||
scale: 1,
|
||||
// pdf 页面像素尺寸(渲染后)
|
||||
viewportWidth: 0,
|
||||
viewportHeight: 0,
|
||||
// 盖章尺寸(像素,基于当前 scale 渲染像素)
|
||||
stampWidth: 160,
|
||||
stampHeight: 160,
|
||||
// 盖章位置(以 canvas 左上为原点的像素)
|
||||
stampXTopLeft: 0,
|
||||
stampYTopLeft: 0,
|
||||
hasStamp: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pdfUrl: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
pageNo() {
|
||||
this.renderPage()
|
||||
},
|
||||
stampWidth() {
|
||||
this.emitStamp()
|
||||
},
|
||||
stampHeight() {
|
||||
this.emitStamp()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
if (!this.pdfUrl) return
|
||||
this.loading = true
|
||||
this.hasStamp = false
|
||||
try {
|
||||
this.pdf = await pdfjsLib.getDocument({ url: this.pdfUrl }).promise
|
||||
this.pageCount = this.pdf.numPages || 1
|
||||
this.pageNo = Math.min(Math.max(this.initialPage, 1), this.pageCount)
|
||||
await this.$nextTick()
|
||||
await this.renderPage()
|
||||
} catch (e) {
|
||||
console.error('PDF加载失败', e)
|
||||
this.$emit('error', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async renderPage() {
|
||||
if (!this.pdf) return
|
||||
const page = await this.pdf.getPage(this.pageNo)
|
||||
const wrap = this.$refs.wrap
|
||||
const canvas = this.$refs.canvas
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 自适应容器宽度
|
||||
const unscaledViewport = page.getViewport({ scale: 1 })
|
||||
const maxWidth = (wrap && wrap.clientWidth) ? wrap.clientWidth : 900
|
||||
this.scale = maxWidth / unscaledViewport.width
|
||||
const viewport = page.getViewport({ scale: this.scale })
|
||||
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
this.viewportWidth = viewport.width
|
||||
this.viewportHeight = viewport.height
|
||||
|
||||
// 清空
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
await page.render({ canvasContext: ctx, viewport }).promise
|
||||
|
||||
// 切页后清空盖章框(避免误用上一页坐标)
|
||||
this.hasStamp = false
|
||||
this.emitStamp()
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if (this.pageNo > 1) this.pageNo -= 1
|
||||
},
|
||||
nextPage() {
|
||||
if (this.pageNo < this.pageCount) this.pageNo += 1
|
||||
},
|
||||
|
||||
onClick(e) {
|
||||
const rect = this.$refs.canvas.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
// 让点击点成为盖章框中心
|
||||
this.stampXTopLeft = Math.max(0, Math.min(x - this.stampWidth / 2, this.viewportWidth - this.stampWidth))
|
||||
this.stampYTopLeft = Math.max(0, Math.min(y - this.stampHeight / 2, this.viewportHeight - this.stampHeight))
|
||||
this.hasStamp = true
|
||||
this.emitStamp()
|
||||
},
|
||||
|
||||
emitStamp() {
|
||||
// 输出给父组件的坐标必须是 PDFBox 期望的坐标:左下为原点
|
||||
// 当前 stampXTopLeft / stampYTopLeft 是左上原点
|
||||
const xPx = this.stampXTopLeft
|
||||
const yPx = this.viewportHeight - this.stampYTopLeft - this.stampHeight
|
||||
|
||||
this.$emit('change', {
|
||||
pageNo: this.pageNo,
|
||||
xPx: Math.round(xPx),
|
||||
yPx: Math.round(yPx),
|
||||
// 位置换算需要:当前渲染后的 viewport 尺寸(像素)
|
||||
viewportWidth: Math.round(this.viewportWidth),
|
||||
viewportHeight: Math.round(this.viewportHeight),
|
||||
// 方案B:后端严格用原图大小,这里不再强依赖 widthPx/heightPx(可留给预览)
|
||||
widthPx: Math.round(this.stampWidth),
|
||||
heightPx: Math.round(this.stampHeight),
|
||||
ready: this.hasStamp
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
stampBoxStyle() {
|
||||
return {
|
||||
width: this.stampWidth + 'px',
|
||||
height: this.stampHeight + 'px',
|
||||
left: this.stampXTopLeft + 'px',
|
||||
top: this.stampYTopLeft + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pdf-stamper {
|
||||
width: 100%;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.page {
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
}
|
||||
.split {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: #e6e8ed;
|
||||
margin: 0 6px;
|
||||
}
|
||||
.wh {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
}
|
||||
.canvas-wrap {
|
||||
position: relative;
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
.stamp-box {
|
||||
position: absolute;
|
||||
border: 2px dashed #e6a23c;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
background: rgba(230, 162, 60, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user