Browse Source

水印管理优化

master
xuhuajiao 1 month ago
parent
commit
ff76997ac2
  1. 23
      src/utils/waterMark.js
  2. 146
      src/views/preview/index.vue
  3. 7
      src/views/system/watermarkManage/index.vue

23
src/utils/waterMark.js

@ -130,7 +130,8 @@ export const watermark = (settings, className, type) => {
mask_div.style.position = defaultSettings.watermark_position mask_div.style.position = defaultSettings.watermark_position
mask_div.style.left = x + 'px' mask_div.style.left = x + 'px'
mask_div.style.top = y + 'px' mask_div.style.top = y + 'px'
mask_div.style.overflow = 'hidden'
mask_div.style.overflow = 'visible'
mask_div.style.whiteSpace = 'nowrap'
mask_div.style.zIndex = '9999' mask_div.style.zIndex = '9999'
mask_div.style.pointerEvents = 'none' // pointer-events:none 让水印不遮挡页面的点击事件 mask_div.style.pointerEvents = 'none' // pointer-events:none 让水印不遮挡页面的点击事件
mask_div.style.opacity = defaultSettings.watermark_alpha mask_div.style.opacity = defaultSettings.watermark_alpha
@ -142,18 +143,20 @@ export const watermark = (settings, className, type) => {
mask_div.style.height = defaultSettings.watermark_height + 'px' mask_div.style.height = defaultSettings.watermark_height + 'px'
mask_div.style.display = 'block' mask_div.style.display = 'block'
oTemp.appendChild(mask_div) oTemp.appendChild(mask_div)
const parent = document.getElementsByClassName(className)[0]
const child = parent.getElementsByClassName('mask_div')
if (child.length > 0) {
for (let i = 0; i < child.length; i++) {
parent.removeChild(child[i])
}
}
parent.appendChild(oTemp.cloneNode(true))
} }
} }
} }
// 文本类型:循环结束后统一清除旧水印再渲染,避免循环内重复 clone 导致水印叠加、颜色深浅不一
if (type !== 'img') {
const parents = document.getElementsByClassName(className)
for (let p = 0; p < parents.length; p++) {
const child = parents[p].getElementsByClassName('mask_div')
for (let k = child.length - 1; k >= 0; k--) {
parents[p].removeChild(child[k])
}
parents[p].appendChild(oTemp.cloneNode(true))
}
}
// document.body.appendChild(oTemp) // document.body.appendChild(oTemp)
} }

146
src/views/preview/index.vue

@ -118,7 +118,11 @@ export default {
fileCurrent: null, fileCurrent: null,
fileParentInfo: null, fileParentInfo: null,
pdfsArray: [], pdfsArray: [],
documentId: null
documentId: null,
// 水印防篡改:MutationObserver + 定时兜底
watermarkObserver: null,
watermarkTimer: null,
watermarkRerenderLock: false
} }
}, },
computed: { computed: {
@ -160,6 +164,9 @@ export default {
} }
this.getBase64ByFileId() this.getBase64ByFileId()
}, },
beforeDestroy() {
this.stopWatermarkGuard()
},
methods: { methods: {
getFileSize(fileSize) { getFileSize(fileSize) {
// 1. 先将接口返回的KB值转为数字,处理非数字/空值情况 // 1. 先将接口返回的KB值转为数字,处理非数字/空值情况
@ -240,7 +247,6 @@ export default {
'bucketType': 2 'bucketType': 2
} }
FetchMinioReDocumentBase64ByFileId(params).then(res => { FetchMinioReDocumentBase64ByFileId(params).then(res => {
console.log('res', res)
var base64String = res var base64String = res
var fileName = this.fileCurrent && this.fileCurrent.file_name var fileName = this.fileCurrent && this.fileCurrent.file_name
var mimeType = this.fileCurrent && this.fileCurrent.file_type var mimeType = this.fileCurrent && this.fileCurrent.file_type
@ -284,39 +290,149 @@ export default {
}) })
}, },
getWatermark() { getWatermark() {
getInitWatermark().then((res) => {
return getInitWatermark().then((res) => {
this.watermarkInfo = res this.watermarkInfo = res
// this.isEnable = this.watermarkInfo.isEnable
if (res.watermarkType === '1') { if (res.watermarkType === '1') {
watermark({ watermark({
watermark_txt: this.watermarkInfo.context, watermark_txt: this.watermarkInfo.context,
watermark_alpha: parseFloat(this.watermarkInfo.transparency) / 100 watermark_alpha: parseFloat(this.watermarkInfo.transparency) / 100
}, 'water-mask', 'img') }, 'water-mask', 'img')
} else if (res.watermarkType === '0') { } else if (res.watermarkType === '0') {
// if (parent.length > 0) {
// for (let i = 0; i < parent.length; i++) {
// const childImg = parent[i].getElementsByClassName('weterbox')
// if (childImg.length > 0) {
// for (let i = 0; i < childImg.length; i++) {
// parent[i].removeChild(childImg[i])
// }
// }
// }
// }
watermark({ watermark({
watermark_txt: this.watermarkInfo.context, watermark_txt: this.watermarkInfo.context,
watermark_color: this.watermarkInfo.transparency, watermark_color: this.watermarkInfo.transparency,
watermark_fontsize: this.watermarkInfo.watermarkFont + 'px' watermark_fontsize: this.watermarkInfo.watermarkFont + 'px'
}, 'water-mask', 'text') }, 'water-mask', 'text')
// 第二层:canvas 水印,独立于 DOM 水印,增加绕过难度
this.drawCanvasWatermark()
} }
if (document.getElementById('error-tip')) { if (document.getElementById('error-tip')) {
document.getElementsByClassName('water-mask')[0].innerHTML = '' document.getElementsByClassName('water-mask')[0].innerHTML = ''
} }
// 首次渲染后启动防篡改守卫(幂等:已存在则不重复启动)
if (!this.watermarkObserver) {
this.startWatermarkGuard()
}
}).catch(err => { }).catch(err => {
console.log(err) console.log(err)
}) })
}, },
// canvas 层水印:用 canvas API 平铺绘制,与 DOM 水印独立,用户需同时隐藏两层
drawCanvasWatermark() {
if (!this.watermarkInfo) return
const container = document.querySelector('.water-mask')
if (!container) return
const oldCanvas = container.querySelector('canvas.wm-canvas')
if (oldCanvas) oldCanvas.remove()
const txt = this.watermarkInfo.context || ''
if (!txt) return
const canvas = document.createElement('canvas')
canvas.className = 'wm-canvas'
canvas.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:1;'
container.appendChild(canvas)
const dpr = window.devicePixelRatio || 1
const rect = container.getBoundingClientRect()
if (rect.width === 0 || rect.height === 0) return
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
ctx.font = `${this.watermarkInfo.watermarkFont || 15}px 微软雅黑`
ctx.fillStyle = this.watermarkInfo.watermarkType === '0' ? (this.watermarkInfo.transparency || '#000') : '#000'
ctx.globalAlpha = 0.12
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
const stepX = 180
const stepY = 120
for (let y = -stepY; y < rect.height + stepY; y += stepY) {
for (let x = -stepX; x < rect.width + stepX; x += stepX) {
ctx.save()
ctx.translate(x, y)
ctx.rotate(-15 * Math.PI / 180)
ctx.fillText(txt, 0, 0)
ctx.restore()
}
}
},
// 启动防篡改守卫:MutationObserver 监听 DOM 篡改 + 定时兜底检查
startWatermarkGuard() {
this.stopWatermarkGuard()
const container = document.querySelector('.water-mask')
if (!container) return
this.watermarkObserver = new MutationObserver((mutations) => {
if (this.watermarkRerenderLock) return
let needRerender = false
for (const m of mutations) {
if (m.type === 'childList' && m.removedNodes.length > 0) {
needRerender = true
break
}
if (m.type === 'attributes' && m.target && m.target.style) {
const s = m.target.style
if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) {
needRerender = true
break
}
}
}
if (needRerender) this.rerenderWatermark()
})
this.watermarkObserver.observe(container, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
})
// 定时兜底:observer 监听不到容器自身被隐藏的情况,用定时器补位
this.watermarkTimer = setInterval(() => {
this.checkWatermarkAlive()
}, 2000)
},
stopWatermarkGuard() {
if (this.watermarkObserver) {
this.watermarkObserver.disconnect()
this.watermarkObserver = null
}
if (this.watermarkTimer) {
clearInterval(this.watermarkTimer)
this.watermarkTimer = null
}
},
// 定时检查:容器或水印元素被隐藏/删除时重新渲染
checkWatermarkAlive() {
if (this.watermarkRerenderLock) return
const container = document.querySelector('.water-mask')
if (!container) return
const cs = getComputedStyle(container)
if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) {
// 容器被隐藏:强制恢复显示再重渲染
container.style.setProperty('display', 'block', 'important')
this.rerenderWatermark()
return
}
const masks = container.querySelectorAll('.mask_div, .weterbox, canvas.wm-canvas')
if (masks.length === 0) {
this.rerenderWatermark()
}
},
// 重新渲染水印(断开 observer 避免重入,渲染完再接回)
rerenderWatermark() {
if (this.watermarkRerenderLock) return
this.watermarkRerenderLock = true
if (this.watermarkObserver) this.watermarkObserver.disconnect()
this.getWatermark().then(() => {
const container = document.querySelector('.water-mask')
if (container && this.watermarkObserver) {
this.watermarkObserver.observe(container, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
})
}
this.watermarkRerenderLock = false
})
},
async handleChange(e) { async handleChange(e) {
try { try {
clearTimeout(this.timer) clearTimeout(this.timer)

7
src/views/system/watermarkManage/index.vue

@ -101,10 +101,9 @@ export default {
if (parent.length > 0) { if (parent.length > 0) {
for (let i = 0; i < parent.length; i++) { for (let i = 0; i < parent.length; i++) {
const childImg = parent[i].getElementsByClassName('weterbox') const childImg = parent[i].getElementsByClassName('weterbox')
if (childImg.length > 0) {
for (let i = 0; i < childImg.length; i++) {
parent[i].removeChild(childImg[i])
}
// 倒序删除避免 live HTMLCollection 索引偏移,用 j 避免遮蔽外层 i
for (let j = childImg.length - 1; j >= 0; j--) {
parent[i].removeChild(childImg[j])
} }
} }
} }

Loading…
Cancel
Save