This commit is contained in:
zpc 2026-03-26 10:00:23 +08:00
parent c021e982c6
commit ba09440728
2 changed files with 163 additions and 74 deletions

View File

@ -108,11 +108,26 @@
<text class="submit-btn-text">{{ submitting ? '提交中...' : '提交故障' }}</text>
</view>
</view>
<!-- 离屏 canvas 用于水印绘制APP端需要在屏幕内才能渲染 -->
<canvas
canvas-id="watermarkCanvas"
:style="{
position: 'fixed',
left: '0px',
top: '0px',
width: canvasW + 'px',
height: canvasH + 'px',
opacity: 0,
pointerEvents: 'none',
zIndex: -1
}"
/>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, getCurrentInstance, nextTick } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { addFault } from '@/services/trunk'
import { addWatermark } from '@/utils/watermark'
@ -121,6 +136,9 @@ const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const photoList = ref([])
const cableId = ref('')
const submitting = ref(false)
const canvasW = ref(100)
const canvasH = ref(100)
const instance = getCurrentInstance()
const form = reactive({
faultTime: '',
@ -288,14 +306,25 @@ async function handleSubmit() {
try {
//
const watermarkText = `${form.faultTime} ${form.personnel}`
const watermarkLines = [
`${form.faultTime} ${form.personnel}`,
`经度:${form.longitude} 纬度:${form.latitude}`
]
const watermarkedPhotos = []
for (const photo of photoList.value) {
try {
const result = await addWatermark(photo, watermarkText)
const result = await addWatermark(photo, watermarkLines, {
canvasId: 'watermarkCanvas',
proxy: instance.proxy,
setSize(w, h) {
canvasW.value = w
canvasH.value = h
}
})
watermarkedPhotos.push(result)
// canvas
await nextTick()
} catch (err) {
// 使
watermarkedPhotos.push(photo)
}
}

View File

@ -1,81 +1,141 @@
/**
* 在照片左下角叠加水印文字
* 在照片左下角叠加多行水印文字
* @param {string} imagePath - 原始图片路径
* @param {string} text - 水印文字 "2025/06/15 12:00 张三"
* @returns {Promise<string>} 带水印的临时文件路径H5 返回 base64 dataURL
* @param {string|string[]} lines - 水印文字字符串或字符串数组
* @param {object} [canvasCtx] - APP端传入 { canvasId, proxy, setSize }
* @returns {Promise<string>} 带水印的图片路径失败时返回原图
*/
export function addWatermark(imagePath, text) {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
uni.getImageInfo({
src: imagePath,
success(imgInfo) {
const width = imgInfo.width
const height = imgInfo.height
const bitmap = new plus.nativeObj.Bitmap('watermark')
bitmap.load(imagePath, () => {
const canvas = new plus.nativeObj.View('watermarkView', {
left: '0px', top: '0px',
width: width + 'px', height: height + 'px'
})
canvas.drawBitmap(bitmap, {}, { left: '0px', top: '0px', width: width + 'px', height: height + 'px' })
const fontSize = Math.max(Math.floor(width * 0.03), 14)
const padding = Math.floor(fontSize * 0.8)
const textX = padding
const bgHeight = fontSize + padding * 2
canvas.drawRect(
{ color: 'rgba(0,0,0,0.4)' },
{ left: '0px', top: (height - bgHeight) + 'px', width: width + 'px', height: bgHeight + 'px' }
)
canvas.drawText(text, {
left: textX + 'px',
top: (height - bgHeight + padding) + 'px',
width: (width - textX * 2) + 'px',
height: fontSize + 'px'
}, { size: fontSize + 'px', color: '#ffffff' })
const tempPath = `_doc/watermark_${Date.now()}.jpg`
canvas.toBitmap(tempPath, {}, () => {
bitmap.clear()
resolve(tempPath)
}, (err) => {
bitmap.clear()
reject(err)
})
}, (err) => reject(err))
},
fail: reject
})
// #endif
export function addWatermark(imagePath, lines, canvasCtx) {
if (typeof lines === 'string') {
lines = [lines]
}
// #ifdef H5
return new Promise((resolve) => {
// 超时保护10秒
const timer = setTimeout(() => {
console.warn('[watermark] timeout')
resolve(imagePath)
}, 10000)
_doWatermark(imagePath, lines, canvasCtx)
.then((r) => { clearTimeout(timer); resolve(r || imagePath) })
.catch((e) => { clearTimeout(timer); console.warn('[watermark]', e); resolve(imagePath) })
})
}
function _doWatermark(imagePath, lines, canvasCtx) {
// #ifdef H5
return _h5(imagePath, lines)
// #endif
// #ifdef APP-PLUS
return _app(imagePath, lines, canvasCtx)
// #endif
// eslint-disable-next-line no-unreachable
return Promise.resolve(imagePath)
}
// ========== H5 ==========
// #ifdef H5
function _h5(imagePath, lines) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const width = img.naturalWidth
const height = img.naturalHeight
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, width, height)
const fontSize = Math.max(Math.floor(width * 0.03), 14)
const padding = Math.floor(fontSize * 0.8)
const bgHeight = fontSize + padding * 2
ctx.fillStyle = 'rgba(0,0,0,0.4)'
ctx.fillRect(0, height - bgHeight, width, bgHeight)
ctx.fillStyle = '#ffffff'
ctx.font = `${fontSize}px sans-serif`
ctx.textBaseline = 'middle'
ctx.fillText(text, padding, height - bgHeight / 2)
resolve(canvas.toDataURL('image/jpeg', 0.9))
try {
const w = img.naturalWidth, h = img.naturalHeight
const c = document.createElement('canvas')
c.width = w; c.height = h
const ctx = c.getContext('2d')
ctx.drawImage(img, 0, 0, w, h)
_stamp(ctx, w, h, lines, false)
resolve(c.toDataURL('image/jpeg', 0.9))
} catch (e) { reject(e) }
}
img.onerror = (err) => reject(err || new Error('图片加载失败'))
img.onerror = reject
img.src = imagePath
// #endif
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
// 小程序端:跳过水印,直接返回原图
// resolve(imagePath)
// #endif
})
}
// #endif
// ========== APP ==========
// #ifdef APP-PLUS
function _app(imagePath, lines, cc) {
if (!cc || !cc.canvasId || !cc.proxy) {
return Promise.reject(new Error('missing canvasCtx'))
}
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: imagePath,
success(info) {
try {
// 限制 canvas 宽度 ≤ 1200避免内存问题
let dw = info.width, dh = info.height
if (dw > 1200) { const r = 1200 / dw; dw = 1200; dh = Math.round(info.height * r) }
// 更新页面 canvas 尺寸
if (typeof cc.setSize === 'function') cc.setSize(dw, dh)
// 等 DOM 刷新200ms 足够 APP webview 更新)
setTimeout(() => {
try {
const ctx = uni.createCanvasContext(cc.canvasId, cc.proxy)
ctx.drawImage(imagePath, 0, 0, dw, dh)
_stamp(ctx, dw, dh, lines, true)
let done = false
const exp = () => {
if (done) return
done = true
uni.canvasToTempFilePath({
canvasId: cc.canvasId,
x: 0, y: 0, width: dw, height: dh,
destWidth: info.width, destHeight: info.height,
fileType: 'jpg', quality: 0.9,
success: (r) => resolve(r.tempFilePath),
fail: reject
}, cc.proxy)
}
ctx.draw(false, () => setTimeout(exp, 200))
// 兜底
setTimeout(exp, 1200)
} catch (e) { reject(e) }
}, 200)
} catch (e) { reject(e) }
},
fail: reject
})
})
}
// #endif
/**
* canvas context 上绘制水印条
* @param {boolean} isUni - true uni canvas APIfalse H5 canvas API
*/
function _stamp(ctx, w, h, lines, isUni) {
const fs = Math.max(Math.floor(w * 0.03), 14)
const pad = Math.floor(fs * 0.8)
const lh = fs + pad
const bgH = lh * lines.length + pad
if (isUni) {
ctx.setFillStyle('rgba(0,0,0,0.4)')
ctx.fillRect(0, h - bgH, w, bgH)
ctx.setFillStyle('#ffffff')
ctx.setFontSize(fs)
lines.forEach((t, i) => {
// uni canvas 的 fillText y 是文字顶部基线
ctx.fillText(t, pad, h - bgH + pad + lh * i)
})
} else {
ctx.fillStyle = 'rgba(0,0,0,0.4)'
ctx.fillRect(0, h - bgH, w, bgH)
ctx.fillStyle = '#ffffff'
ctx.font = `${fs}px sans-serif`
ctx.textBaseline = 'middle'
lines.forEach((t, i) => {
ctx.fillText(t, pad, h - bgH + pad / 2 + lh * i + fs / 2)
})
}
}