本文将介绍如何使用uniApp和u-upload组件封装一个自动添加水印并上传图片的功能。整个过程涵盖了图片上传、canvas画布操作、水印绘制等多个技术点。
使用u-upload组件来处理图片选择和上传。同时准备了一个隐藏的 canvas 元素,用于绘制图片水印
<template>
<view style="width: 100%">
<u-upload
ref="uUpload"
:action="action"
:source-type="sourceType"
:file-list="defaultValue"
:auto-upload="!isWatermark"
@on-choose-complete="onChooseComplete"
/>
<!-- Canvas画布,用于添加水印 -->
<canvas
canvas-id="watermarkCanvas"
v-if="isCanvasShow"
:style="{
position: 'absolute',
top: '-9999px',
left: '-9999px',
width: canvasWidth + 'px',
height: canvasHeight + 'px',
}"
></canvas>
<!-- 模拟提示 -->
<u-toast ref="uToast" />
</view>
</template>
当用户选择图片后,触发 onChooseComplete 方法,开始处理图片水印。
methods: {
// 当选择图片后触发的事件
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) {
try {
const url = list[index].url;
const size = list[index].file.size;
// 获取当前时间:年-月-日 时:分:秒 星期
this.currentTime = parseTime(new Date(), "{y}-{m}-{d} {h}:{i}:{s} {a}");
// 设置mask: true,避免水印加载过程中用户执行其他操作
uni.showLoading({ title: "加载水印中", mask: true });
await this.getCurrentAddress(); // 获取当前地址
const { width, height, path } = await this.getImageInfo(url); // 获取图片信息
// 设置画布宽高
this.canvasWidth = width;
this.canvasHeight = height;
this.isCanvasShow = true;
// 等待 canvas 元素创建
this.$nextTick(() => {
let ctx = uni.createCanvasContext("watermarkCanvas", this);
// 清除画布
ctx.clearRect(0, 0, width, height);
// 绘制图片
ctx.drawImage(path, 0, 0, width, height);
// 绘制地址水印
this.drawAddressWatermark(ctx, width, height);
// 绘制竖线水印、时间、创建人等
this.drawOtherWatermarks(ctx, width, height);
// 绘制结束后生成临时文件并上传
ctx.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: "watermarkCanvas",
quality: 0.6, // 图片质量,范围0-1,1为最高质量
width: width, // 画布宽度
height: height, // 画布高度
destWidth: width, // 输出图片宽度
destHeight: height, // 输出图片高度
success: (data) => {
// 替换原始图片为带水印图片
this.$refs.uUpload.lists[index] = {
size: size,
thumb: data.tempFilePath,
type: "png",
url: data.tempFilePath,
};
this.$refs.uUpload.upload(); // 手动上传图片
// 隐藏画布
this.isCanvasShow = false;
uni.hideLoading();
},
fail: (err) => {
console.error("canvasToTempFilePath failed", err);
uni.hideLoading();
},
},this); // 加上 this 确保 canvas 正确关联当前页面或组件,避免报错 "canvasToTempFilePath: fail canvas is empty"
});
});
} catch (error) {
console.error("drawWatermark error:", error);
uni.hideLoading();
}
},
}
getCurrentAddress() {
return new Promise((resolve, reject) => {
uni.authorize({
scope: "scope.userLocation",
success: () => {
uni.getLocation({
type: "gcj02",
success: (res) => {
const lat = res.latitude;
const lng = res.longitude;
// 通过腾讯地图 API 获取用户的当前位置(请将以下的key值替换为真实的key值)
const URL = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=ABCDE-FGHIJ-KLMNO-PQRST-UVWXY`;
wx.request({
url: URL,
success: (result) => {
const Res_Data = result.data.result;
this.currentAddress =
Res_Data.address +
" (" +
Res_Data.formatted_addresses.recommend +
")";
resolve();
},
fail: () => {
this.currentAddress = "";
this.$refs.uToast.show({
title: "加载定位失败!",
type: "error",
icon: true,
duration: 2000,
});
reject();
},
});
},
fail: (err) => {
reject(err);
},
});
},
fail: () => {
this.$refs.uToast.show({
title: "用户拒绝授权!",
type: "error",
icon: true,
duration: 2000,
});
reject();
},
});
});
}
为了确保水印的美观,动态计算了文字在画布上的换行位置,并且通过图片宽高对比区分横屏\竖屏,以此调整文字的高度。
// 绘制地址水印
drawAddressWatermark(ctx, width, height) {
// 文字大小
const fontSize = 30;
ctx.font = 'bold 30px "Microsoft YaHei"';
// 通过width、height对比判断照片:横屏\竖屏,调整绘制的高度
const addressHeight = width > height ? 40 : 0;
const maxWidth = width - 45; // 45是留给定位图标的空间
const iconX = 10;
const initialX = 45; // 地址文字起始x位置
let y = height - 120 + addressHeight;
let currentLine = "";
let currentLineWidth = 0; // 当前行的总宽度
// 绘制定位图标
this.fillText(ctx, '?', iconX, y, {
font: 'bold 30px "Microsoft YaHei"',
color: '#fff'
});
// 循环处理每个字符
for (let i = 0; i < this.currentAddress.length; i++) {
const char = this.currentAddress[i];
const charWidth = ctx.measureText(char).width;
// 预测添加字符后的总宽度
if (currentLineWidth + charWidth <= maxWidth) {
// 如果不会超出行宽,继续添加到当前行
currentLine += char;
currentLineWidth += charWidth;
} else {
// 当前行满了,绘制并换行
this.fillText(ctx, currentLine, initialX, y, {
font: 'bold 30px "Microsoft YaHei"',
color: "#fff",
});
// 重置行参数
currentLine = char;
currentLineWidth = charWidth;
y += fontSize + 10; // 换行
}
}
// 绘制最后一行
if (currentLine.length > 0) {
this.fillText(ctx, currentLine, initialX, y, {
font: 'bold 30px "Microsoft YaHei"',
color: "#fff",
});
}
},
// 绘制其他水印内容(时间、姓名等)
drawOtherWatermarks(ctx, width, height) {
const currentTime = this.currentTime;
const time = currentTime.substring(11, 16);
const day = `${currentTime.substring(0, 4)}年${currentTime.substring(
5,
7
)}月${currentTime.substring(8, 10)}日 星期${currentTime.substring(
currentTime.length - 1
)}`;
// 通过width、height对比判断照片:横屏\竖屏,调整绘制的 ? 创建人名称 水印的高度
const addressHeight = width > height ? 40 : 0;
// 竖线水印
this.fillRectangle(ctx, 40, 60, 8, 120, "#fff");
// 时间水印
this.fillText(ctx, time, 80, 100, {
font: 'bold 50px "Microsoft YaHei"',
color: "#fff",
});
this.fillText(ctx, day, 80, 150, {
font: 'bold 40px "Microsoft YaHei"',
color: "#fff",
});
this.fillText(ctx, "?", width - 195, height - (160 - addressHeight), {
font: 'bold 30px "Microsoft YaHei"',
color: "#fff",
});
const createByName = this.$store.state.userInfo.userName; // 获取创建人名称
this.fillText(
ctx,
createByName,
width - 150,
height - (160 - addressHeight),
{ font: 'bold 35px "Microsoft YaHei"', color: "#fff" }
);
},
<template>
<view style="width: 100%">
<!-- 上传组件 -->
<u-upload
ref="uUpload"
:action="action"
:source-type="sourceType"
:file-list="defaultValue"
:auto-upload="!isWatermark"
@on-choose-complete="onChooseComplete"
/>
<!-- Canvas画布,用于添加水印 -->
<canvas
canvas-id="watermarkCanvas"
v-if="isCanvasShow"
:style="{
position: 'absolute',
top: '-9999px',
left: '-9999px',
width: canvasWidth + 'px',
height: canvasHeight + 'px',
}"
></canvas>
<!-- 模拟提示 -->
<u-toast ref="uToast" />
</view>
</template>
<script>
import { parseTime } from "@/utils";
export default {
name: "my-upload",
props: {
value: {
type: Array,
default: () => [],
},
// 选择图片的来源,album-从相册选图,camera-使用相机
sourceType: {
type: Array,
default: () => ["album", "camera"],
},
// 是否需要添加水印,控制自动上传
isWatermark: {
type: Boolean,
default: false,
},
},
data() {
return {
action: "http://www.example.com/upload", // 上传地址
currentTime: "", // 当前时间
currentAddress: "", // 当前地址
isCanvasShow: false, // 是否显示画布
canvasWidth: 300, // 画布宽度
canvasHeight: 225, // 画布高度
};
},
computed: {
// 显示的图片列表,确保为数组类型
defaultValue() {
return Array.isArray(this.value) ? this.value : [];
},
},
methods: {
// 当选择图片后触发的事件
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) {
try {
const url = list[index].url;
const size = list[index].file.size;
// 获取当前时间:年-月-日 时:分:秒 星期
this.currentTime = parseTime(new Date(), "{y}-{m}-{d} {h}:{i}:{s} {a}");
// 设置mask: true,避免水印加载过程中用户执行其他操作
uni.showLoading({ title: "加载水印中", mask: true });
await this.getCurrentAddress(); // 获取当前地址
const { width, height, path } = await this.getImageInfo(url); // 获取图片信息
// 设置画布宽高
this.canvasWidth = width;
this.canvasHeight = height;
this.isCanvasShow = true;
// 等待 canvas 元素创建
this.$nextTick(() => {
let ctx = uni.createCanvasContext("watermarkCanvas", this);
// 清除画布
ctx.clearRect(0, 0, width, height);
// 绘制图片
ctx.drawImage(path, 0, 0, width, height);
// 绘制地址水印
this.drawAddressWatermark(ctx, width, height);
// 绘制竖线水印、时间、创建人等
this.drawOtherWatermarks(ctx, width, height);
// 绘制结束后生成临时文件并上传
ctx.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: "watermarkCanvas",
quality: 0.6, // 图片质量,范围0-1,1为最高质量
width: width, // 画布宽度
height: height, // 画布高度
destWidth: width, // 输出图片宽度(默认为 width * 屏幕像素密度)
destHeight: height, // 输出图片高度(默认为 height * 屏幕像素密度)
success: (data) => {
// 替换原始图片为带水印图片
this.$refs.uUpload.lists[index] = {
size: size,
thumb: data.tempFilePath,
type: "png",
url: data.tempFilePath,
};
this.$refs.uUpload.upload(); // 手动上传图片
// 隐藏画布
this.isCanvasShow = false;
uni.hideLoading();
},
fail: (err) => {
console.error("canvasToTempFilePath failed", err);
uni.hideLoading();
},
},this); // 注意这里要加this(确保 canvas 与当前页面或者组件正确关联)否则报错:"canvasToTempFilePath: fail canvas is empty"
});
});
} catch (error) {
console.error("drawWatermark error:", error);
uni.hideLoading();
}
},
// 绘制地址水印
drawAddressWatermark(ctx, width, height) {
// 文字大小
const fontSize = 30;
ctx.font = 'bold 30px "Microsoft YaHei"';
// 通过width、height对比判断照片:横屏\竖屏,调整绘制的高度
const addressHeight = width > height ? 40 : 0;
const maxWidth = width - 45; // 45是留给定位图标的空间
const iconX = 10;
const initialX = 45; // 地址文字起始x位置
let y = height - 120 + addressHeight;
let currentLine = "";
let currentLineWidth = 0; // 当前行的总宽度
// 绘制定位图标
this.fillText(ctx, '?', iconX, y, {
font: 'bold 30px "Microsoft YaHei"',
color: '#fff'
});
// 循环处理每个字符
for (let i = 0; i < this.currentAddress.length; i++) {
const char = this.currentAddress[i];
const charWidth = ctx.measureText(char).width;
// 预测添加字符后的总宽度
if (currentLineWidth + charWidth <= maxWidth) {
// 如果不会超出行宽,继续添加到当前行
currentLine += char;
currentLineWidth += charWidth;
} else {
// 当前行满了,绘制并换行
this.fillText(ctx, currentLine, initialX, y, {
font: 'bold 30px "Microsoft YaHei"',
color: "#fff",
});
// 重置行参数
currentLine = char;
currentLineWidth = charWidth;
y += fontSize + 10; // 换行
}
}
// 绘制最后一行
if (currentLine.length > 0) {
this.fillText(ctx, currentLine, initialX, y, {
font: 'bold 30px "Microsoft YaHei"',
color: "#fff",
});
}
},
// 绘制其他水印(竖线、时间等)
drawOtherWatermarks(ctx, width, height) {
const currentTime = this.currentTime;
const time = currentTime.substring(11, 16);
const day = `${currentTime.substring(0, 4)}年${currentTime.substring(
5,
7
)}月${currentTime.substring(8, 10)}日 星期${currentTime.substring(
currentTime.length - 1
)}`;
// 通过width、height对比判断照片:横屏\竖屏,调整绘制的 ? 创建人名称 水印的高度
const addressHeight = width > height ? 40 : 0;
// 竖线水印
this.fillRectangle(ctx, 40, 60, 8, 120, "#fff");
// 时间水印
this.fillText(ctx, time, 80, 100, {
font: 'bold 50px "Microsoft YaHei"',
color: "#fff",
});
this.fillText(ctx, day, 80, 150, {
font: 'bold 40px "Microsoft YaHei"',
color: "#fff",
});
this.fillText(ctx, "?", width - 195, height - (160 - addressHeight), {
font: 'bold 30px "Microsoft YaHei"',
color: "#fff",
});
const createByName = this.$store.state.userInfo.userName; // 获取创建人名称
this.fillText(
ctx,
createByName,
width - 150,
height - (160 - addressHeight),
{ font: 'bold 35px "Microsoft YaHei"', color: "#fff" }
);
},
// 绘制文字
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,
});
});
},
// 获取当前地址信息
getCurrentAddress() {
return new Promise((resolve, reject) => {
uni.authorize({
scope: "scope.userLocation",
success: () => {
uni.getLocation({
type: "gcj02",
success: (res) => {
const lat = res.latitude;
const lng = res.longitude;
// 通过腾讯地图 API 获取用户的当前位置
const URL = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=ABCDE-FGHIJ-KLMNO-PQRST-UVWXY`;
wx.request({
url: URL,
success: (result) => {
const Res_Data = result.data.result;
this.currentAddress =
Res_Data.address +
" (" +
Res_Data.formatted_addresses.recommend +
")";
resolve();
},
fail: () => {
this.currentAddress = "";
this.$refs.uToast.show({
title: "加载定位失败!",
type: "error",
icon: true,
duration: 2000,
});
reject();
},
});
},
fail: (err) => {
reject(err);
},
});
},
fail: () => {
this.$refs.uToast.show({
title: "用户拒绝授权!",
type: "error",
icon: true,
duration: 2000,
});
reject();
},
});
});
},
},
};
在需要用到的页面引用该组件即可,设置sourceType的值为camera(相机拍摄),isWatermark值为true(开启水印加载)
<my-upload
v-model="imgList"
:source-type="['camera']"
:is-watermark="true"
/>