<template>
|
<div>
|
<el-dialog
|
v-model="dialogVisible"
|
width="75%"
|
title="签名归档"
|
:before-close="handleClose"
|
:close-on-press-escape="false"
|
:close-on-click-modal="false"
|
class="myCustomDialog"
|
custom-class="pdfDialog"
|
z-index="2010"
|
>
|
<!-- 背景文件切换按钮 -->
|
<div class="top">
|
<div class="top-left">
|
<h3>签名列表</h3>
|
<img
|
v-for="(image, index) in signatureImages"
|
:src="image"
|
:key="index"
|
@click="selectSignature(image)"
|
/>
|
</div>
|
<div v-if="pageNum>1">
|
<span style="margin-right: 15px">{{currentBackgroundIndex + 1}}/{{pageNum}}</span>
|
<el-button icon="arrow-left" @click="prevBackground">上一页</el-button>
|
<el-button icon="arrow-right" @click="nextBackground">上一页</el-button>
|
</div>
|
</div>
|
|
<!-- 已放置的签名 -->
|
<div class="bottom" v-if="placedSignaturesByBackground[currentBackgroundIndex] && placedSignaturesByBackground[currentBackgroundIndex].length>0">
|
<h3>已放签名:</h3>
|
<div v-for="(sig, index) in placedSignaturesByBackground[currentBackgroundIndex]" :key="index" class="controlBtns">
|
<el-tag size="large" type="primary">签名{{index + 1}}</el-tag>
|
<el-button type="primary" @click="rotateSignature(index)" :icon="RefreshRight" circle/>
|
<el-button type="danger" @click="removeSignature(index)" :icon="Delete" circle/>
|
</div>
|
</div>
|
<!-- 画布 -->
|
<canvas ref="canvasRef" v-if="dialogVisible"></canvas>
|
|
<!-- 导出PDF按钮 -->
|
<el-button type="primary" @click="saveAsPDF" style="margin-top: 20px">确认归档</el-button>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted,nextTick } from "vue";
|
import {signArchive} from "@/api/signAgreement/signProject";
|
import { jsPDF } from "jspdf"; // 导入 jsPDF
|
import {ElMessage, ElMessageBox,ElLoading} from "element-plus";
|
import {RefreshRight,Delete} from '@element-plus/icons-vue'
|
import * as pdfjsLib from "pdfjs-dist";
|
import {getToken} from "@/utils/auth";
|
import axios from 'axios';
|
import Cookies from "js-cookie";
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
"@/assets/pdf.worker.min.js",
|
import.meta.url
|
).href;
|
const dialogVisible = ref(false)
|
const emit = defineEmits(["getList"]);
|
// 画布引用
|
const canvasRef = ref();
|
let ctx;
|
|
// 拖拽和缩放状态
|
let isDragging = false;
|
let offsetX, offsetY;
|
let isResizing = false;
|
let selectedSignature = null;
|
|
// 签名图列表
|
const signatureImages = ref([])
|
const pdfFileUrl = ref('')
|
// 背景文件列表(由后端返回)
|
const backgroundFileUrls = ref([])
|
const pageNum = ref()
|
const backgroundImages = ref([]); // 存储加载后的背景图片
|
const currentBackgroundIndex = ref(0); // 当前显示的背景文件索引
|
// 为每个背景文件单独存储签名图
|
const placedSignaturesByBackground = ref([]);
|
// 最大画布宽度(根据屏幕大小设置)
|
const maxCanvasWidth = window.innerWidth * 1; // 屏幕宽度的 75%
|
|
const form = reactive({
|
itemId: null,
|
userId: null,
|
filePath: '',
|
status: 3
|
})
|
|
const openDialog= async (val)=>{
|
dialogVisible.value = true
|
signatureImages.value = val.signatureFlows?.map(i=>import.meta.env.VITE_APP_BASE_API + i.signFile)
|
pdfFileUrl.value = import.meta.env.VITE_APP_BASE_API + val.tempFile
|
form.itemId = val.id
|
form.userId = JSON.parse(Cookies.get('userInfo')).userId
|
await nextTick()
|
const loadingInstance = ElLoading.service(
|
{target: '.pdfDialog',background: 'rgba(255,255,255,.4)',text: '正在加载文件,请稍后...'}
|
)
|
const canvas = canvasRef.value;
|
if (canvas) {
|
ctx = canvas.getContext("2d");
|
// 替换为实际的 PDF 文件地址
|
await loadPDF(pdfFileUrl.value);
|
loadingInstance.close()
|
// 监听鼠标事件
|
canvas.addEventListener("mousedown", (e) => {
|
const mouseX = e.offsetX;
|
const mouseY = e.offsetY;
|
|
const currentSignatures = placedSignaturesByBackground.value[currentBackgroundIndex.value];
|
for (let i = 0; i < currentSignatures.length; i++) {
|
const sig = currentSignatures[i];
|
|
// 检查鼠标是否在签名图上
|
if (
|
mouseX >= sig.x &&
|
mouseX <= sig.x + sig.width &&
|
mouseY >= sig.y &&
|
mouseY <= sig.y + sig.height
|
) {
|
// 检查是否在缩放手柄上(右下角)
|
const resizeHandleX = sig.x + sig.width - 10;
|
const resizeHandleY = sig.y + sig.height - 10;
|
if (
|
mouseX >= resizeHandleX &&
|
mouseX <= resizeHandleX + 10 &&
|
mouseY >= resizeHandleY &&
|
mouseY <= resizeHandleY + 10
|
) {
|
isResizing = true;
|
selectedSignature = sig;
|
break;
|
} else {
|
isDragging = true;
|
selectedSignature = sig;
|
offsetX = mouseX - sig.x;
|
offsetY = mouseY - sig.y;
|
break;
|
}
|
}
|
}
|
});
|
|
canvas.addEventListener("mousemove", (e) => {
|
if (isDragging && selectedSignature) {
|
const mouseX = e.offsetX;
|
const mouseY = e.offsetY;
|
selectedSignature.x = mouseX - offsetX;
|
selectedSignature.y = mouseY - offsetY;
|
redrawCanvas();
|
}
|
|
if (isResizing && selectedSignature) {
|
const mouseX = e.offsetX;
|
// 计算缩放比例
|
const scaleX = (mouseX - selectedSignature.x) / selectedSignature.width;
|
// 保持比例
|
const aspectRatio =
|
selectedSignature.image.width / selectedSignature.image.height;
|
const newWidth = selectedSignature.width * scaleX;
|
const newHeight = newWidth / aspectRatio;
|
// 计算图片的中心点
|
const centerX = selectedSignature.x + selectedSignature.width / 2;
|
const centerY = selectedSignature.y + selectedSignature.height / 2;
|
// 更新图像的宽度和高度
|
selectedSignature.width = newWidth;
|
selectedSignature.height = newHeight;
|
// 调整图片位置,保持图片的中心点不变
|
selectedSignature.x = centerX - newWidth / 2;
|
selectedSignature.y = centerY - newHeight / 2;
|
|
redrawCanvas();
|
}
|
});
|
|
canvas.addEventListener("mouseup", () => {
|
isDragging = false;
|
isResizing = false;
|
selectedSignature = null;
|
});
|
|
canvas.addEventListener("mouseleave", () => {
|
isDragging = false;
|
isResizing = false;
|
selectedSignature = null;
|
});
|
}
|
|
}
|
|
|
defineExpose({
|
openDialog
|
});
|
|
// 加载 PDF 文件并渲染为图片
|
// 加载 PDF 文件并渲染为图片
|
const loadPDF = async (pdfUrl) => {
|
try {
|
const loadingTask = pdfjsLib.getDocument(pdfUrl);
|
const pdf = await loadingTask.promise;
|
pageNum.value = pdf.numPages
|
// 清空背景图片和签名图列表
|
backgroundImages.value = [];
|
placedSignaturesByBackground.value = [];
|
|
// 遍历每一页
|
for (let i = 1; i <= pdf.numPages; i++) {
|
const page = await pdf.getPage(i);
|
// 设置缩放比例
|
const viewport = page.getViewport({ scale: 2 }); // 缩放比例为 1,避免过大
|
// 创建画布
|
const canvas = canvasRef.value;
|
const ctx = canvas.getContext("2d");
|
// 设置画布大小
|
canvas.width = viewport.width;
|
canvas.height = viewport.height;
|
// 渲染 PDF 页面到画布
|
const renderContext = {
|
canvasContext: ctx,
|
viewport: viewport,
|
};
|
await page.render(renderContext).promise;
|
// 将画布内容转换为图片
|
const img = new Image();
|
img.crossOrigin = 'anonymous'; // 设置跨域
|
img.src = canvas.toDataURL("image/jpeg");
|
|
// 等待图片加载完成
|
await new Promise((resolve) => {
|
img.onload = () => {
|
backgroundImages.value.push(img);
|
placedSignaturesByBackground.value.push([]); // 为每一页初始化签名图列表
|
resolve();
|
};
|
});
|
}
|
|
// 默认显示第一页
|
redrawCanvas();
|
} catch (error) {
|
console.error("PDF 文件加载或渲染失败:", error);
|
}
|
};
|
|
|
|
const handleClose = () =>{
|
currentBackgroundIndex.value = 0
|
placedSignaturesByBackground.value = []
|
signatureImages.value = []
|
pageNum.value = 0
|
dialogVisible.value = false
|
}
|
|
// 切换到上一个背景文件
|
const prevBackground = () => {
|
if (currentBackgroundIndex.value > 0) {
|
currentBackgroundIndex.value--;
|
redrawCanvas();
|
}
|
};
|
|
// 切换到下一个背景文件
|
const nextBackground = () => {
|
if (currentBackgroundIndex.value < backgroundImages.value.length - 1) {
|
currentBackgroundIndex.value++;
|
redrawCanvas();
|
}
|
};
|
|
// 选择签名图
|
const selectSignature = (imageSrc) => {
|
const selectedImage = new Image();
|
selectedImage.crossOrigin = 'anonymous'; // 设置跨域
|
selectedImage.src = imageSrc;
|
|
selectedImage.onload = () => {
|
placedSignaturesByBackground.value[currentBackgroundIndex.value].push({
|
image: selectedImage,
|
x: 100,
|
y: 100,
|
width: selectedImage.width, // 初始宽度为图片的原始宽度
|
height: selectedImage.height, // 初始高度为图片的原始高度
|
rotation: 0 // 初始旋转角度为 0
|
});
|
redrawCanvas();
|
};
|
|
selectedImage.onerror = () => {
|
alert("图片加载失败!");
|
};
|
};
|
|
// 删除签名图
|
const removeSignature = (index) => {
|
placedSignaturesByBackground.value[currentBackgroundIndex.value].splice(index, 1);
|
redrawCanvas();
|
};
|
|
// 旋转签名图
|
const rotateSignature = (index) => {
|
const currentSignatures = placedSignaturesByBackground.value[currentBackgroundIndex.value];
|
if (currentSignatures[index]) {
|
currentSignatures[index].rotation = (currentSignatures[index].rotation + 90) % 360; // 每次旋转 90 度
|
redrawCanvas();
|
}
|
};
|
|
// 重新绘制画布
|
const redrawCanvas = (excludeHandle = false) => {
|
const canvas = canvasRef.value;
|
if (!canvas || !backgroundImages.value[currentBackgroundIndex.value]) return;
|
|
const currentBackground = backgroundImages.value[currentBackgroundIndex.value];
|
|
// 根据屏幕大小动态调整画布宽高
|
const aspectRatio = currentBackground.width / currentBackground.height;
|
let canvasWidth = currentBackground.width;
|
let canvasHeight = currentBackground.height;
|
|
// 如果画布宽度超过最大宽度,则按比例缩放
|
if (canvasWidth > maxCanvasWidth) {
|
canvasWidth = maxCanvasWidth;
|
canvasHeight = canvasWidth / aspectRatio;
|
}
|
|
// 设置画布宽高
|
canvas.width = canvasWidth;
|
canvas.height = canvasHeight;
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空画布
|
ctx.drawImage(currentBackground, 0, 0, canvas.width, canvas.height); // 重新绘制当前背景
|
|
const currentSignatures = placedSignaturesByBackground.value[currentBackgroundIndex.value];
|
currentSignatures.forEach((sig) => {
|
// 保存当前画布状态
|
ctx.save();
|
|
// 移动到签名图的中心点
|
ctx.translate(sig.x + sig.width / 2, sig.y + sig.height / 2);
|
|
// 旋转画布
|
ctx.rotate((sig.rotation * Math.PI) / 180);
|
|
|
// 绘制签名图
|
ctx.drawImage(sig.image, -sig.width / 2, -sig.height / 2, sig.width, sig.height);
|
|
// 恢复画布状态
|
ctx.restore();
|
|
// 只有在非导出状态下才绘制缩放手柄
|
if (!excludeHandle) {
|
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
ctx.fillRect(
|
sig.x + sig.width - 10,
|
sig.y + sig.height - 10,
|
10,
|
10
|
); // 右下角手柄
|
}
|
});
|
};
|
|
// 导出PDF
|
const saveAsPDF = async () => {
|
const canvas = canvasRef.value;
|
if (!canvas || backgroundImages.value.length === 0) return;
|
const pdf = new jsPDF();
|
backgroundImages.value.forEach((background, index) => {
|
if (index > 0) {
|
pdf.addPage(); // 添加新页面
|
}
|
// 设置画布大小与背景图片一致
|
const aspectRatio = background.width / background.height;
|
let canvasWidth = background.width;
|
let canvasHeight = background.height;
|
|
// 如果画布宽度超过最大宽度,则按比例缩放
|
if (canvasWidth > maxCanvasWidth) {
|
canvasWidth = maxCanvasWidth;
|
canvasHeight = canvasWidth / aspectRatio;
|
}
|
canvas.width = canvasWidth;
|
canvas.height = canvasHeight;
|
// 绘制当前背景和签名图
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.drawImage(background, 0, 0, canvas.width, canvas.height);
|
const currentSignatures = placedSignaturesByBackground.value[index];
|
currentSignatures.forEach((sig) => {
|
// 保存当前画布状态
|
ctx.save();
|
|
// 移动到签名图的中心点
|
ctx.translate(sig.x + sig.width / 2, sig.y + sig.height / 2);
|
|
// 旋转画布
|
ctx.rotate((sig.rotation * Math.PI) / 180);
|
|
// 绘制签名图
|
ctx.drawImage(sig.image, -sig.width / 2, -sig.height / 2, sig.width, sig.height);
|
|
// 恢复画布状态
|
ctx.restore();
|
});
|
// 将画布内容添加到PDF
|
const imgData = canvas.toDataURL("image/jpeg");
|
const pdfWidth = pdf.internal.pageSize.getWidth();
|
const pdfHeight = pdf.internal.pageSize.getHeight();
|
const scale = Math.min(pdfWidth / canvas.width, pdfHeight / canvas.height);
|
const imgWidth = canvas.width * scale;
|
const imgHeight = canvas.height * scale;
|
const offsetX = (pdfWidth - imgWidth) / 2;
|
const offsetY = (pdfHeight - imgHeight) / 2;
|
pdf.addImage(imgData, "JPEG", offsetX, offsetY, imgWidth, imgHeight);
|
});
|
// pdf.save("canvas.pdf");
|
|
// 将 PDF 转换为二进制数据
|
const pdfData = pdf.output("arraybuffer");
|
// 上传 PDF 到后端
|
const formData = new FormData();
|
formData.append("file", new Blob([pdfData]), "file.pdf");
|
try {
|
const response = await axios.post(import.meta.env.VITE_APP_BASE_API + '/common/upload', formData, {
|
headers: {
|
'Content-Type': 'multipart/form-data',
|
'Authorization': getToken()
|
}
|
});
|
if(response.data.code == 200){
|
form.filePath = response.data.fileName
|
const res = await signArchive(form)
|
if(res.code == 200){
|
ElMessage.success('项目归档成功')
|
handleClose()
|
emit("getList")
|
}else{
|
ElMessage.warning(res.message)
|
}
|
}
|
} catch (error) {
|
console.error("PDF 上传失败", error);
|
}
|
};
|
</script>
|
|
<style scoped>
|
canvas {
|
border: 1px solid #ccc;
|
max-width: 100%; /* 确保画布不会溢出屏幕 */
|
}
|
</style>
|
|
<style lang="scss">
|
.myCustomDialog{
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
|
.el-dialog__body{
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
.top{
|
width: 100%;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
|
.top-left{
|
display: flex;
|
align-items: center;
|
|
h3{
|
margin-right: 20px;
|
}
|
img{
|
margin-right: 20px;
|
width: 120px;
|
height: auto;
|
border: 1px solid #f0f0f0;
|
cursor: pointer;
|
box-sizing: border-box;
|
|
&:hover{
|
border: 2px solid #2563EB;
|
}
|
}
|
}
|
}
|
|
.bottom{
|
width: 100%;
|
display: flex;
|
align-items: center;
|
margin-bottom: 20px;
|
|
.controlBtns{
|
margin-right: 20px;
|
|
.el-button{
|
margin: 0 0 0 5px;
|
}
|
|
}
|
}
|
}
|
</style>
|