日常事务-处分管理:添加全局水印功能和印章图片替换

- 在审批任务对话框中添加了水印显示和隐藏功能
- 替换了静态印章图片为动态获取的印章base64数据
- 添加了下载处分送达书的按钮和相关处理逻辑
- 优化了印章样式布局,支持多种类型的印章展示
- 引入了html2canvas库用于将内容转为带水印的图片下载
- 添加了水印画布创建和背景图生成功能
```
This commit is contained in:
2026-03-20 11:44:31 +08:00
parent 567c023661
commit db6af26906
4 changed files with 283 additions and 43 deletions

View File

@@ -5,11 +5,11 @@
"author": "srs",
"license": "MIT",
"scripts": {
"dev": "cross-env vue-cli-service serve",
"build:prod": "cross-env NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build",
"build:stage": "cross-env NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build --mode staging",
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider vite build",
"preview": "cross-env NODE_OPTIONS=--openssl-legacy-provider node build/index.js --preview",
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"build": "vite build",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix"
},

View File

@@ -757,7 +757,7 @@
</el-tabs>
<!--审批任务-->
<el-dialog :title="completeTitle" class="certificate-service" :visible.sync="completeOpen" width="60%"
append-to-body>
append-to-body @opened="showGlobalWatermark" @closed="hideGlobalWatermark">
<!-- 处分决定书 -->
<el-card v-if="showLetterService">
<div class="flex justify-center items-center min-h-screen">
@@ -766,19 +766,16 @@
<p class="mb-4 text-zinc-600 dark:text-zinc-300">{{ form.stuName }}同学:</p>
<p class="mb-4 text-zinc-600 dark:text-zinc-300 desc">{{ form.letterService }}</p>
<div class="stamp">
<img
src="https://gss0.baidu.com/7Po3dSag_xI4khGko9WTAnF6hhy/zhidao/wh%3D450%2C600/sign=debd0a4bb2fd5266a77e34109e28bb1d/8d5494eef01f3a297283d36e9d25bc315d607cc2.jpg"
alt="Stamp">
<!-- <img src="https://placehold.co/100x100" alt="Stamp" /> -->
<div>
<div>学生工作处</div>
<div>{{ form.violationDate }}</div>
</div>
<img v-if="stampBase64" :src="stampBase64" width="200" height="200">
</div>
</div>
</div>
<!-- <pdf :src="pdfURL"> </pdf> -->
<el-button type="danger" @click="fileUpload">下载处分下文</el-button>
</el-card>
<el-button v-if="showFileDowload" type="danger" @click="fileUpload">下载解除处分下文</el-button>
@@ -790,13 +787,13 @@
}}学生,学号:{{ form.stuNo }}.该生于个人原因-{{ form.reasonApplying }},申请休学.经学校研究,同意休学,时间从{{ form.quitStartTime }}{{
form.quitEndTime }}.</p>
<p>抄送:教务处财务处{{ form.departmentName }}</p>
<div class="stamp">
<img
src="https://gss0.baidu.com/7Po3dSag_xI4khGko9WTAnF6hhy/zhidao/wh%3D450%2C600/sign=debd0a4bb2fd5266a77e34109e28bb1d/8d5494eef01f3a297283d36e9d25bc315d607cc2.jpg"
alt="Stamp">
<div>
<span>学生工作处</span>
<span>{{ form.quitStartTime }}</span>
<div class="stamp quit-school-stamp">
<div class="quit-school-stamp-layer">
<img v-if="stampBase64" class="quit-school-stamp-image" :src="stampBase64" width="200" height="200">
<div class="quit-school-stamp-text">
<span>学生工作处</span>
<span>{{ form.quitStartTime }}</span>
</div>
</div>
</div>
</div>
@@ -893,6 +890,7 @@
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="danger" @click="fileUpload">下载处分送达书</el-button>
<el-button @click="completeOpen = false"> </el-button>
<el-button type="primary" @click="taskComplete"> </el-button>
</span>
@@ -938,6 +936,7 @@ import { flowXmlAndNode, getProcessVariables } from '@/api/flowable/definition'
import { flowRecord } from '@/api/flowable/finished'
import { complete, delegate, flowTaskForm, getNextFlowNode, rejectTask, returnList, returnTask } from '@/api/flowable/todo'
import { getDisciplinaryApplicationByProcInsId, getStuInfo, updateDisciplinaryApplication } from '@/api/routine/disciplinaryApplication'
import { getStampBase64 } from '@/api/common/stamp'
import { getLeaveApplicationByProcInsId, getStuInfoByStuId } from '@/api/routine/leaveApplication'
import { getStuDisciplinaryRelieveByProcInsId, updateRelieve } from '@/api/routine/relieve'
import { getRtStuQuitSchoolByProcInsId, updateRtStuQuitSchool } from '@/api/routine/rtStuQuitSchool'
@@ -1098,6 +1097,9 @@ export default {
basicForm: false,//退伍复学表单
BasicTestData: 0, // 新增默认0仅退回学生申请时改为1
user: [], // 当前登录用户
stampBase64: '', // 印章Base64数据
globalWatermarkEl: null,
globalWatermarkText: '\u5e7f\u897f\u6c34\u5229\u7535\u529b\u804c\u4e1a\u6280\u672f\u5b66\u9662\u5b66\u5de5\u7cfb\u7edf'
}
},
created() {
@@ -1120,9 +1122,11 @@ export default {
}
if ((this.taskName == '学生接收' || this.taskName == '申请人(辅导员)接收') && this.category == 'disposal') {
this.showLetterService = true
this.getStampBase64()
}
if ((this.taskName == '学生接收' || this.taskName == '辅导员接收') && this.category == 'quitSchool') {
this.showQuitSchoolProve = true
this.getStampBase64()
}
if (this.taskName == '学生接收' && this.category == 'relieve') {
this.showFileDowload = true
@@ -1177,7 +1181,86 @@ export default {
}
},
methods: {
createWatermarkCanvas(text, options = {}) {
const {
width = 300,
height = 200,
angle = -25,
fontSize = 18,
alpha = 0.2
} = options
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) {
return null
}
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((angle * Math.PI) / 180)
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.font = `${fontSize}px Microsoft YaHei`
ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`
ctx.fillText(text, 0, 0)
return canvas
},
createWatermarkDataUrl(text, options = {}) {
const canvas = this.createWatermarkCanvas(text, options)
if (!canvas) {
return ''
}
return canvas.toDataURL('image/png')
},
showGlobalWatermark() {
if (typeof document === 'undefined') {
return
}
// 检查是否有显示处分决定送达书的卡片且包含.stamp元素
const letterServiceCard = document.querySelector('.el-card:has(.stamp)')
if (!letterServiceCard) {
// 如果没有找到符合条件的元素,则不显示水印
return
}
if (!this.globalWatermarkEl) {
this.globalWatermarkEl = document.createElement('div')
this.globalWatermarkEl.className = 'global-dialog-watermark-mask'
}
const topZIndex = Array.from(document.querySelectorAll('body *'))
.map(item => Number(window.getComputedStyle(item).zIndex) || 0)
.reduce((max, value) => Math.max(max, value), 2002)
const watermarkUrl = this.createWatermarkDataUrl(this.globalWatermarkText)
this.globalWatermarkEl.style.cssText = `
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: ${topZIndex + 1};
background-image: url(${watermarkUrl});
background-size: 300px 200px;
background-repeat: repeat;
background-position: 0 0;
`
if (!document.body.contains(this.globalWatermarkEl)) {
document.body.appendChild(this.globalWatermarkEl)
}
},
hideGlobalWatermark() {
if (this.globalWatermarkEl && this.globalWatermarkEl.parentNode) {
this.globalWatermarkEl.parentNode.removeChild(this.globalWatermarkEl)
}
},
getStampBase64() {
getStampBase64().then(res => {
this.stampBase64 = res.msg
})
},
handleChange1(value) {
if (value && value.length === 3) {
const [gradeId, majorId, classId] = value;
@@ -1525,7 +1608,7 @@ export default {
// this.completeOpen = true;
// this.completeTitle = "流程审批";
this.submitForm(null)
// 获取当前用户的签名并赋值给this.taskForm.variables.signature传入监听器
// 获取当前用户的签名并赋值给this.taskForm.variables传入监听器
this.taskForm.variables.signature = this.user.signature
},
/** 用户审批任务 */
@@ -2380,7 +2463,53 @@ export default {
})
},
fileUpload() {
download.resource(this.pdfURL)
// 使用html2canvas将处分决定书卡片内容转换为图片并下载
import('html2canvas').then((html2canvas) => {
// 优先选择处分决定书的卡片内容,如果找不到再尝试通用的.el-card__body
const element = document.querySelector('.certificate-service .el-card__body') ||
document.querySelector('.certificate-service') ||
document.querySelector('.el-card__body');
if (element) {
html2canvas.default(element).then((canvas) => {
// 创建一个新的canvas用于添加水印
const watermarkedCanvas = document.createElement('canvas');
const ctx = watermarkedCanvas.getContext('2d');
watermarkedCanvas.width = canvas.width;
watermarkedCanvas.height = canvas.height;
// 绘制原始canvas内容
ctx.drawImage(canvas, 0, 0);
// 使用平铺水印块,避免文字重叠并降低透明度
const watermarkCanvas = this.createWatermarkCanvas(this.globalWatermarkText, {
width: 320,
height: 220,
angle: -25,
fontSize: 22,
alpha: 0.3
})
if (watermarkCanvas) {
ctx.save()
const pattern = ctx.createPattern(watermarkCanvas, 'repeat')
if (pattern) {
ctx.fillStyle = pattern
ctx.fillRect(0, 0, watermarkedCanvas.width, watermarkedCanvas.height)
}
ctx.restore()
}
// 创建下载链接
const link = document.createElement('a');
link.download = '处分决定送达书.png'; // 更具描述性的文件名
link.href = watermarkedCanvas.toDataURL('image/png');
link.click();
});
} else {
console.error('找不到需要截图的元素');
}
}).catch((error) => {
console.error('html2canvas 加载失败:', error);
});
},
// 初始化入伍保留学籍申请审核意见参数
initApproval() {
@@ -2470,6 +2599,9 @@ export default {
})
}
},
beforeDestroy() {
this.hideGlobalWatermark()
}
}
</script>
<style lang="scss" scoped>
@@ -2532,15 +2664,34 @@ export default {
.stamp {
text-align: right;
position: relative;
min-height: 120px;
padding-top: 10px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
pointer-events: none;
&>div {
margin-top: -70px;
position: relative;
z-index: 2;
margin-top: 10px;
margin-right: 25px;
}
img {
width: 120px;
height: 120px;
position: absolute;
right: 0;
top: -55px;
width: 150px;
height: 200px;
z-index: 1;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
pointer-events: none;
}
}
}
@@ -2567,28 +2718,76 @@ export default {
.stamp {
position: relative;
height: 115px;
height: 190px;
margin-top: 50px;
display: flex;
flex-direction: column;
color: #333;
line-height: 35px;
letter-spacing: 3px;
align-items: flex-end;
align-items: center;
&>div {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
margin-right: 28px;
align-items: center;
margin-right: 0;
text-align: center;
}
img {
width: 180px;
height: 180px;
top: -54px;
position: absolute;
z-index: -1;
left: 50%;
top: 10px;
transform: translateX(-50%);
width: 200px;
height: 200px;
z-index: 2;
}
}
.quit-school-stamp {
height: 260px;
margin-top: 24px;
}
.quit-school-stamp-layer {
position: relative;
width: 250px;
height: 250px;
margin: 0 auto;
}
.quit-school-stamp-image {
position: absolute !important;
inset: 0;
width: 250px !important;
height: 250px !important;
z-index: 1 !important;
object-fit: contain;
}
.quit-school-stamp-text {
position: absolute !important;
inset: 0;
z-index: 2 !important;
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
line-height: 1.15;
font-size: 24px;
font-weight: 500;
color: #222;
letter-spacing: 0;
transform: translateY(18px);
}
.quit-school-stamp-text span + span {
margin-top: 2px;
}
}
</style>

View File

@@ -194,11 +194,11 @@
<p>{{ form.stuName }},{{ form.gender }},{{ form.mz }},{{ form.birthday }}出生,{{ form.jg }},{{ form.className }}学生,学号:{{ form.stuNo }}.该生于个人原因-{{ form.reasonApplying }},申请休学.经学校研究,同意休学,时间从{{ form.quitStartTime }}{{ form.quitEndTime }}.</p>
<p>抄送:教务处财务处{{ form.departmentName }}</p>
<div class="stamp">
<img src="https://gss0.baidu.com/7Po3dSag_xI4khGko9WTAnF6hhy/zhidao/wh%3D450%2C600/sign=debd0a4bb2fd5266a77e34109e28bb1d/8d5494eef01f3a297283d36e9d25bc315d607cc2.jpg" alt="Stamp">
<div>
<span>学生工作处</span>
<span>{{ form.quitStartTime }}</span>
</div>
<img v-if="stampBase64" :src="stampBase64" width="200" height="200">
</div>
</div>
</div>
@@ -289,6 +289,7 @@ import { flowXmlAndNode, getProcessVariables } from '@/api/flowable/definition'
import { flowRecord } from '@/api/flowable/finished'
import { complete, delegate, flowTaskForm, getNextFlowNode, rejectTask, returnList, returnTask } from '@/api/flowable/todo'
import { getRtStuQuitSchoolByProcInsId, updateRtStuQuitSchool } from '@/api/routine/rtStuQuitSchool'
import { getStampBase64 } from '@/api/common/stamp'
import FlowRole from '@/components/flow/Role'
import FlowUser from '@/components/flow/User'
import Parser from '@/components/parser/Parser'
@@ -403,6 +404,7 @@ export default {
quitSchoolForm: false, // 休学申请表单
showQuitSchoolProve: false, //休学证明
quitSchoolGLKSHShow: false, // 学生教育管理科审核
stampBase64: '', // 印章Base64数据
}
},
created() {
@@ -418,6 +420,7 @@ export default {
// 如果任务名是其中的两个,则改变审批意见的输入框内容
if ((this.taskName == '学生接收' || this.taskName == '辅导员接收') && this.category == 'quitSchool') {
this.showQuitSchoolProve = true
this.getStampBase64()
}
if (this.category == 'quitSchool') {
this.quitSchoolForm = true
@@ -439,6 +442,12 @@ export default {
}
},
methods: {
getStampBase64() {
getStampBase64().then(res => {
this.stampBase64 = res.msg
})
},
reentryYearMethodFormat(row, column) {
return this.selectDictLabel(this.dict.type.sys_teacher_kpi_filling_year, row.quitYear)
},
@@ -765,14 +774,24 @@ export default {
.stamp {
text-align: right;
position: relative;
min-height: 120px;
padding-top: 10px;
& > div {
margin-top: -70px;
position: relative;
z-index: 2;
margin-top: 10px;
margin-right: 25px;
}
img {
width: 120px;
height: 120px;
position: absolute;
right: 0;
top: 10px;
width: 200px;
height: 200px;
z-index: 1;
}
}
}

View File

@@ -213,11 +213,11 @@
</p>
<p>抄送教务处财务处{{ form.departmentName }}</p>
<div class="stamp">
<img src="https://gss0.baidu.com/7Po3dSag_xI4khGko9WTAnF6hhy/zhidao/wh%3D450%2C600/sign=debd0a4bb2fd5266a77e34109e28bb1d/8d5494eef01f3a297283d36e9d25bc315d607cc2.jpg" alt="Stamp">
<div>
<span>学生工作处</span>
<span>{{ form.reentryTime }}</span>
</div>
<img v-if="stampBase64" :src="stampBase64" width="200" height="200">
</div>
</div>
</div>
@@ -322,6 +322,7 @@ import { flowXmlAndNode, getProcessVariables } from '@/api/flowable/definition'
import { flowRecord } from '@/api/flowable/finished'
import { complete, delegate, flowTaskForm, getNextFlowNode, rejectTask, returnList, returnTask } from '@/api/flowable/todo'
import { getRtStuReentrySchoolByProcInsId, updateRtStuReentrySchool } from '@/api/routine/rtStuReentrySchool'
import { getStampBase64 } from '@/api/common/stamp'
import { getClass } from '@/api/stuCQS/basedata/class'
import { getClassName } from '@/api/stuCQS/basedata/student'
import FlowRole from '@/components/flow/Role'
@@ -442,6 +443,7 @@ export default {
classVlue2: [], //班级添加修改选择
ClassNameList: [], //班级名称
showReentryProve: false, // 复学证明
stampBase64: '', // 印章Base64数据
}
},
created() {
@@ -466,6 +468,7 @@ export default {
}
if (this.taskName == '辅导员接收' || this.taskName == '学生接收') {
this.showReentryProve = true
this.getStampBase64()
}
}
// 流程任务获取变量信息
@@ -480,6 +483,12 @@ export default {
this.getClassNameList()
},
methods: {
getStampBase64() {
getStampBase64().then(res => {
this.stampBase64 = res.msg
})
},
reentryYearMethodFormat(row, column) {
return this.selectDictLabel(this.dict.type.sys_teacher_kpi_filling_year, row.reentryYear)
},
@@ -837,14 +846,24 @@ export default {
.stamp {
text-align: right;
position: relative;
min-height: 120px;
padding-top: 10px;
& > div {
margin-top: -70px;
position: relative;
z-index: 2;
margin-top: 10px;
margin-right: 25px;
}
img {
width: 120px;
height: 120px;
position: absolute;
right: 0;
top: 10px;
width: 200px;
height: 200px;
z-index: 1;
}
}
}
@@ -880,17 +899,20 @@ export default {
align-items: flex-end;
& > div {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
margin-right: 28px;
}
img {
width: 180px;
height: 180px;
top: -54px;
position: absolute;
z-index: -1;
right: 0;
top: 0;
width: 200px;
height: 200px;
z-index: 1;
}
}
}