一、APP的数据需求
1、调用手机相机(需求是拍照、当然选取相册也是可以的,只是需求要求),拍照后将所需信息和二维码图片作为水印放置在图片上,然后具有水印的图片,可以通过接口参数传给后端,进行后续逻辑的处理。
2、可以上传多个照片上传,但是水印必须是一个一个绘制,
3、可以作为单独的组件,可以在任何需要的功能页面直接引入。
二、大致方案
1、使用u-upload(uniapp本身提供)作为图片上传的组件
2、将图片和水印内容,重新绘制在canvas上,在所有内容绘制完成后,调用uni.canvasToTempFilePath,将canvas转化为文件格式,方便后续作为参数,想后端进行参数传递。三、撰写水印组件(water-mark-upload.vue)
<template>
<view style="width: 100%;">
<!-- 上传组件 -->
<!-- "u-upload"可以通过$refs获取u-upload实例,可以获取u-upload,上传的图片 -->
<!-- "uplpadAction"如果是自动上传,就需要配置上传地址 -->
<!-- "sourceType"配置上选择图片的来源,album-从相册选图,camera-使用相机-->
<!-- "fileList"上传的图片数据-->
<!-- "autoUpload"是否自动上传-->
<!-- "beforeUpload"上传前处理图片文件,判断文件大小、文件数据-->
<!-- "onChooseComplete"上传前处理图片文件,判断文件大小、文件数据-->
<!-- "remove"删除上传的图片-->
<u-upload ref="uUpload" :action="uplpadAction" :source-type="sourceType" :file-list="fileList" max-count="10"
:auto-upload="autoUpload" :before-upload="beforeUpload" @on-choose-complete="onChooseComplete" @on-remove="remove" />
<!-- Canvas画布,用于添加水印 -->
<!-- "isCanvasShow" 是否展示canvas -->
<!-- "canvasWidth、canvasHeight" 根据上传图片的宽高,来设置canvas的宽高 -->
<canvas canvas-id="watermarkCanvas" v-if="isCanvasShow" :style="{
position: 'absolute',
top: '-9999px',
left: '-9999px',
width: canvasWidth + 'px',
height: canvasHeight + 'px',
}"></canvas>
</view>
</template>
<script>
//处理时间格式
import dayjs from "dayjs";
//原来使用这个插件,用url生产二维码,但是uniapp打包后会有问题,选择用url作为参数,让后端生成二维码,不建议使
// import QRCode from "qrcode";
// 后端接口,生成二维码用,如有需求自行实现
import {
FindFamQRCode
} from "@/src/api/apiContent/allStationWorks.js";
export default {
name: "water-mark-upload",
props: {
//电站的经度-水印内容-需求需要,如果不需要可以穿空
currentLongitude: {
type: Number,
default: () => {
return ''
}
},
//电站的纬度-水印内容-需求需要,如果不需要可以穿空
currentLatitude: {
type: Number,
default: () => {
return ''
}
},
//电站的地址-水印内容-需求需要,如果不需要可以穿空
currentAddress: {
type: String,
default: () => {
return ''
}
},
//上传组件自动上传时候,需要传入的
uplpadAction: {
type: String,
default: () => {
return ''
}
},
// 电站的一些基本信息-根据实际需求来定
stationInfo: {
type: Object,
default: () => {
return {};
}
},
//是否自动上传
autoUpload: {
type: Boolean,
default: false
},
//文件列表
fileList: {
type: Array,
default: () => {
return [];
}
},
// 选择图片的来源,album-从相册选图,camera-使用相机
sourceType: {
type: Array,
default: () => {
return ["camera"];
}
},
// 是否需要添加水印,控制自动上传
isWatermark: {
type: Boolean,
default: true
}
},
onShow() {
const THAT = this;
},
data() {
return {
// 状态栏高度,H5中,此值为0,因为H5不可操作状态栏
statusBarHeight: uni.getSystemInfoSync().statusBarHeight, // 导航栏内容区域高度,不包括状态栏高度在内
navbarHeight: 44,
isCanvasShow: false, // 是否显示画布
currentTime: "", // 当前时间
// currentAddress: "陕西省西安市大寨路中航华府", // 当前地址
canvasWidth: 300, // 画布宽度
canvasHeight: 225 // 画布高度
};
},
methods: {
beforeUpload(index, lists) {
const THAT = this;
//if (index > 9) {
// this.$refs.uToast.show({
// title: '最多上传10张图片'
// })
// return false;
// }
// if (lists[index].size > 5 * 1024) {
// this.$refs.uToast.show({
// title: '图片不能超过5M'
// })
// }
},
//删除图片
remove(index, lists, name){
const THAT = this;
//触发父组件对上传文件进行删除
THAT.$emit('remove',index, lists, name)
},
// 当选择图片后触发的事件
async onChooseComplete(list, name) {
if (this.isWatermark) {
try {
const index = list.length - 1;
//对图片进行水印处理
await this.drawWatermark(list, index); // 添加水印
} catch (error) {
console.error("水印处理失败", error);
}
}
},
// 处理水印并手动上传
async drawWatermark(list, index) {
let THAT = this;
try {
const url = list[index].url;
const size = list[index].file.size;
// 获取当前时间:年-月-日 时:分:秒 星期
THAT.currentTime = dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss");
// 设置mask: true,避免水印加载过程中用户执行其他操作
uni.showLoading({
title: "加载水印中",
mask: true
});
//获取图片的宽高和path信息
const {
width,
height,
path
} = await THAT.getImageInfo(url); // 获取图片信息
//宽高比
const ratio = width / height
//高德提供的根据这个url生成二维码,扫码可以调用高德地图进行导航
const codeUrl =
`http://uri.amap.com/navigation?from=${THAT.currentLongitude},${THAT.currentLatitude},${THAT.currentAddress}&to=${THAT.stationInfo.longitude},${THAT.stationInfo.latitude},${THAT.stationInfo.address}&mode=car&policy=1&src=mypage&callnative=0`
//获取二维码地址
const QRData = await FindFamQRCode({
url: codeUrl
})
let qrString = ''
console.log(QRData.code)
if (QRData.code === 0 && QRData.data) {
qrString = QRData.data
}
// 设置画布宽高
THAT.canvasWidth = width;
THAT.canvasHeight = height;
// #ifdef APP-PLUS
console.log(plus.os.name)
//对ios单独处理,ios图片水印,如果使用原来图片宽高会出现,水印异常问题,只会出现图片的一个角,图片被放大
if (plus.os.name == "iOS") {
const maxWidth = uni.upx2px(960);
const maxHeight = uni.upx2px(720);
/**
* 规则:1.照片宽度大于最大宽度,以最大宽度为准
* 规则:2.照片高度大于最大高度,以最大高度为准
* 规则:3.照片高度或照片宽度任意一个超出最值范围,以最大高度和宽度为准
*/
if (width > maxWidth) {
THAT.canvasWidth = maxWidth;
THAT.canvasHeight = THAT.canvasWidth / ratio;
} else if (height > maxHeight) {
THAT.canvasHeight = maxHeight;
THAT.canvasWidth = THAT.canvasHeight * ratio;
} else {
THAT.canvasWidth = width;
THAT.canvasHeight = height;
}
console.log('THAT.canvasWidth',THAT.canvasWidth)
console.log('THAT.canvasHeight',THAT.canvasHeight)
}
// #endif
THAT.isCanvasShow = true;
// 等待 canvas 元素创建
THAT.$nextTick(() => {
let ctx = uni.createCanvasContext("watermarkCanvas", THAT);
console.log(ctx)
// 清除画布
ctx.clearRect(0, 0, THAT.canvasWidth, THAT.canvasHeight);
// 绘制整个图片,将原始图片绘制在canvas上面
ctx.drawImage(path, 0, 0, THAT.canvasWidth, THAT.canvasHeight);
// 绘制竖线水印、时间、创建人等
// 判断是浏览器还是终端安卓 或者IOS
// 说明是浏览器
if(navigator){
THAT.drawOtherWatermarks(ctx, THAT.canvasWidth, THAT.canvasHeight,'Internet');
// 绘制二维码图片
if (qrString) {
const yHeight =
THAT.canvasWidth > THAT.canvasHeight ? THAT.canvasHeight - 380 + 25 : THAT
.canvasHeight - 380 + 25;
if (THAT.canvasWidth > THAT.canvasHeight) {
ctx.drawImage(qrString, THAT.canvasWidth - 300, yHeight, 260, 260);
} else {
ctx.drawImage(qrString, THAT.canvasWidth - 330, yHeight, 285, 285);
}
}
}else{
if (plus.os.name == "Android"){
THAT.drawOtherWatermarks(ctx, THAT.canvasWidth, THAT.canvasHeight,'Android');
// 绘制二维码图片
if (qrString) {
const yHeight =
THAT.canvasWidth > THAT.canvasHeight ? THAT.canvasHeight - 380 + 25 : THAT
.canvasHeight - 380 + 25;
if (THAT.canvasWidth > THAT.canvasHeight) {
ctx.drawImage(qrString, THAT.canvasWidth - 300, yHeight, 260, 260);
} else {
ctx.drawImage(qrString, THAT.canvasWidth - 330, yHeight, 285, 285);
}
}
}else if(plus.os.name == "iOS") {
THAT.drawOtherWatermarks(ctx, THAT.canvasWidth, THAT.canvasHeight,'iOS');
// 绘制二维码图片
if (qrString) {
const yHeight =
THAT.canvasWidth > THAT.canvasHeight ? THAT.canvasHeight - 190 + 12 : THAT
.canvasHeight - 190 + 12;
if (THAT.canvasWidth > THAT.canvasHeight) {
ctx.drawImage(qrString, THAT.canvasWidth - 150, yHeight, 130, 130);
} else {
ctx.drawImage(qrString, THAT.canvasWidth - 150, yHeight, 130, 130);
}
}
}
}
// 绘制结束后生成临时文件并上传
setTimeout(() => {
ctx.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: "watermarkCanvas",
quality: 0.6, // 图片质量,范围0-1,1为最高质量
width: THAT.canvasWidth, // 画布宽度
height: THAT.canvasHeight, // 画布高度
destWidth: THAT
.canvasWidth, // 输出图片宽度(默认为 width * 屏幕像素密度)
destHeight: THAT
.canvasHeight, // 输出图片高度(默认为 height * 屏幕像素密度)
success: data => {
console.log('水印成功')
// 隐藏画布
THAT.isCanvasShow = false;
// 替换原始图片为带水印图片
THAT.$refs.uUpload.lists[index] = {
size: size,
thumb: data.tempFilePath,
type: "png",
url: data.tempFilePath
};
// 手动上传图片
// this.$refs.uUpload.upload();
THAT.$nextTick(() => {
THAT.$emit(
'before-file-list-change',
THAT.$refs.uUpload
.lists)
})
uni.hideLoading();
},
fail: err => {
THAT.$nextTick(() => {
THAT.$emit(
'before-file-list-change',
THAT.$refs.uUpload
.lists)
})
uni.hideLoading();
console.error(
"canvasToTempFilePath failed", err);
}
},
THAT
); // 注意这里要加this(确保 canvas 与当前页面或者组件正确关联)否则报错:"canvasToTempFilePath: fail canvas is empty"
});
}, 1000)
});
} catch (error) {
console.error("drawWatermark error:", error);
uni.hideLoading();
}
},
// 绘制其他水印(竖线、时间等)里面出现的数字就是设置水印在图片上面位置,可以根据实际情况设定
drawOtherWatermarks(ctx, width, height , deviceType) {
const THAT = this;
const currentTime = "时间:" + THAT.currentTime;
const stationAddress = `地点:${
THAT.stationInfo.address ? THAT.stationInfo.address : "-"
}`;
const stationOwer = `业主:${
THAT.stationInfo.owner ? THAT.stationInfo.owner : "-"
}`;
const jw = `经纬度:${
THAT.stationInfo.longitude ? THAT.stationInfo.longitude : "-"
}°E ${THAT.stationInfo.latitude ? THAT.stationInfo.latitude : "-"}°N`;
const operationName = `运维商:${
THAT.stationInfo.operationName ? THAT.stationInfo.operationName : "-"
}`;
// 通过width、height对比判断照片:横屏\竖屏,调整绘制的 👤 创建人名称 水印的高度
// 绘制背景色
if(deviceType === 'Internet' || deviceType === 'Android'){
const nowYHeight = width > height ? height - 380 : height - 380;
if (stationAddress.length > 14) {
THAT.fillGrayBakcground(ctx, 40, nowYHeight, width - 70, 350, {
color: "rgba(0, 0, 0, 0.4)",
opacity: "0.4"
});
// 时间水印
THAT.fillText(ctx, currentTime, 60, nowYHeight + 60, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
const addressLength = stationAddress.length;
// 地址水印01
THAT.fillText(ctx, stationAddress.slice(0, 14), 60, nowYHeight + 110, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
// 地址水印02
THAT.fillText(ctx, stationAddress.slice(14, addressLength), 60, nowYHeight + 160, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
// 业主信息
THAT.fillText(ctx, stationOwer, 60, nowYHeight + 210, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
// 经纬度
THAT.fillText(ctx, jw, 60, nowYHeight + 260, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
// 运维商
THAT.fillText(ctx, operationName, 60, nowYHeight + 310, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
} else {
THAT.fillGrayBakcground(ctx, 40, nowYHeight, width - 70, 300, {
color: "rgba(0, 0, 0, 0.4)",
opacity: "0.4"
});
// 时间水印
THAT.fillText(ctx, currentTime, 60, nowYHeight + 60, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
// 地址水印
THAT.fillText(ctx, stationAddress, 60, nowYHeight + 110, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
// 业主信息
THAT.fillText(ctx, stationOwer, 60, nowYHeight + 160, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
// 经纬度
THAT.fillText(ctx, jw, 60, nowYHeight + 210, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
// 运维商
THAT.fillText(ctx, operationName, 60, nowYHeight + 260, {
font: 'bold 30px "PingFang SC"',
color: "#fff"
});
}
}else if(deviceType === 'iOS'){
const nowYHeight = width > height ? height - 190 : height - 190;
if (stationAddress.length > 14) {
THAT.fillGrayBakcground(ctx, 20, nowYHeight, width - 30, 166, {
color: "rgba(0, 0, 0, 0.4)",
opacity: "0.4"
});
// 时间水印
THAT.fillText(ctx, currentTime, 30, nowYHeight + 30, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
const addressLength = stationAddress.length;
// 地址水印01
THAT.fillText(ctx, stationAddress.slice(0, 14), 30, nowYHeight + 55, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
// 地址水印02
THAT.fillText(ctx, stationAddress.slice(14, addressLength), 30, nowYHeight + 80, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
// 业主信息
THAT.fillText(ctx, stationOwer, 30, nowYHeight + 105, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
// 经纬度
THAT.fillText(ctx, jw, 30, nowYHeight + 130, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
// 运维商
THAT.fillText(ctx, operationName, 30, nowYHeight + 155, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
} else {
THAT.fillGrayBakcground(ctx, 20, nowYHeight, width - 30, 150, {
color: "rgba(0, 0, 0, 0.4)",
opacity: "0.4"
});
// 时间水印
THAT.fillText(ctx, currentTime, 30, nowYHeight + 30, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
// 地址水印
THAT.fillText(ctx, stationAddress, 30, nowYHeight + 55, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
// 业主信息
THAT.fillText(ctx, stationOwer, 30, nowYHeight + 80, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
// 经纬度
THAT.fillText(ctx, jw, 30, nowYHeight + 105, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
// 运维商
THAT.fillText(ctx, operationName, 30, nowYHeight + 130, {
font: 'bold 16px "PingFang SC"',
color: "#fff"
});
}
}
},
// 灰色绘制背景
fillGrayBakcground(cxt, x, y, width, height, style) {
cxt.save();
//背景色
cxt.fillStyle = style.color;
cxt.fillRect(x, y, width, height);
cxt.restore();
},
// 绘制文字
fillText(cxt, content, x, y, style) {
cxt.save();
cxt.font = style.font;
cxt.fillStyle = style.color;
cxt.fillText(content, x, y);
cxt.restore();
},
// 绘制矩形竖线水印
fillRectangle(cxt, x, y, width, height, style) {
cxt.save();
cxt.fillStyle = style;
cxt.fillRect(x, y, width, height);
cxt.restore();
},
// 获取图片信息封装为 Promise,便于异步处理
getImageInfo(src) {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: src,
success: resolve,
fail: reject
});
});
}
}
};
</script>
四、外部父组件调用
<water-mark-upload
:station-info="stationInfo"
uplpad-action=""
:auto-upload="false"
current-longitude="currentLongitude"
:current-latitude="currentLatitude"
:current-address="currentAddress"
:fileList="fileListBefore"
@remove="removeBeforeList"
@before-file-list-change="fileListBeforeChange" />
五、最终效果