uniapp使用Canvas给图片加水印把临时文件上传到服务器

生成的临时路径是没有完整的路径没办法上传到服务器

16:37:40.993 添加水印后的路径,  _doc/uniapp_temp_1710923708347/canvas/17109238597881.png 
16:37:41.041 添加水印后的完整路径,  file://storage/emulated/0/Android/data/com.jingruan.zjd/apps/__UNI__BE4B000/doc/uniapp_temp_1710923708347/canvas/17109238597881.png

使用以下代码得到完整的路径

javascript 复制代码
let path = 'file:/' + plus.io.convertLocalFileSystemURL(tempFilePath);

完整代码如下 使用的插件市场的hpy-watermark组件 一共2个

效果是

其他页面调用方式

javascript 复制代码
<!-- 
增加水印上传villageReviewForm.preciseAddr 是通过高德获取的定位地址 
-->
<hpy-watermark ref="uploadImage" :address="villageReviewForm.preciseAddr" @waterMark="waterMark"></hpy-watermark>	

高德获取定位

javascript 复制代码
		uni.getLocation({
					type: 'gcj02',
					geocode: true,
					isHighAccuracy: true,
					success: res => { 
						const {
							province,
							city,
							district,
							street,
							streetNum,
							poiName
						} = res.address;
						this.villageReviewForm.preciseAddr =
							`${district}${street}${streetNum}${poiName}${res.longitude},${res.latitude}`;
						console.log("经纬度地点",this.villageReviewForm.preciseAddr)
						// 数据渲染
						this.$forceUpdate();
					}
				});

获取上传数据结果

javascript 复制代码
const fileList = this.$refs.uploadImage.fileList

组件样式

hpy-watermark.vue

javascript 复制代码
<template>
	<view>
	<view class="watermark-content">
		<canvas canvas-id="watermarkCanvas" id="watermarkCanvas" :style="{width:canvasWidth + 'px', height:canvasHeight + 'px'}"></canvas>
	</view>
	<upload-image   v-model="fileList" style="margin-left: 15rpx"   :image-styles="imageStyles" :files-list="filesList"   :delIcon="delIcon"   @choose="chooseImage" @delFile="delFile">
		<slot>
			<view class="is-add">
				<view class="icon-add"></view>
				<view class="icon-add rotate"></view>
			</view>
		</slot>
	</upload-image>
	</view>
</template>

<script>
	import {
		fileServerIp
	} from "@/common/utils/config.js"
	import Session from "@/common/Session";
	import uploadImage from './upload-image.vue'
	export default {
		components: {
			uploadImage, 
		},
		name:'hpy-watermark',
		props:{
			address:{
				type:String,
				default:''
			},
			delIcon: {
				type: Boolean,
				default: true
			},
		 
			listStyles: {
				type: Object,
				default () {
					return {
						// 是否显示边框
						border: true,
						// 是否显示分隔线
						dividline: true,
						// 线条样式
						borderStyle: {}
					}
				}
			},
			imageStyles: {
				type: Object,
				default () {
					return {
						width: 'auto',
						height: 'auto'
					}
				}
			},
			/**
			 * 文字文字位置(默认:左下角)可选值:左上角:topLeft、右上角:topRight、左下角:bottomLeft、右下角:bottomRight
			 */
			markAlign:{
				type:String,
				default:function(){
					return 'bottomLeft'
				}
			},
			/**
			 * 设置文本的水平对齐方式,默认:start,文本在指定的位置开始。
			 * end	文本在指定的位置结束。
			 * center 文本的中心被放置在指定的位置。
			 * left	文本左对齐。
			 * right	文本右对齐。
			 */
			textAlign:{
				type:String,
				default:function(){
					return 'start';
				}
			},
			/**
			 * 设置文本的垂直对齐方式,默认:alphabetic文本基线是普通的字母基线。
			 * top	文本基线是 em 方框的顶端。
			 * hanging	文本基线是悬挂基线。
			 * middle	文本基线是 em 方框的正中。
			 * ideographic	文本基线是表意基线。
			 * bottom	文本基线是 em 方框的底端。
			 */
			textBaseline:{
				type:String,
				default:function(){
					return 'alphabetic';
				}
			},
			/**
			 * 文字大小
			 */
			fontSize:{
				type:[Number, String],
				default:30
			},
			/**
			 * 文字颜色
			 */
			fontColor:{
				type:String,
				default:function(){
					return 'red'
				}
			},
			/**
			 * 阴影颜色
			 */
			shadowColor:{
				type:String,
				default:function(){
					return 'rgba(0, 0, 0, 1.0)';
				}
			},
			/**
			 * 阴影边框大小
			 */
			shadowWidth:{
				type:[Number, String],
				default:2
			},
			/**
			 * 图片的质量,取值范围为 (0, 1],不在范围内时当作1处理
			 */
			quality:{
				type:[Number, String],
				default:1
			},
			/**
			 * 目标文件的类型,只支持 'jpg' 或 'png'。默认为 'png'
			 */
			fileType:{
				type:String,
				default:function(){
					return 'png'
				}
			}
		},
		data() {
			return {
				fileList: [],
				files: [],
				filesList:[],
				canvasWidth:0,
				canvasHeight:0
			};
		},
		watch: { 
			fileList: {
				handler(newVal, oldVal) { 
					this.filesList=newVal;
				},
				immediate: true
			},
		 
		},
		methods: {
			
			// 选择图片
				chooseImage() {
					if(this.isEmpty(this.address)){
						uni.showToast({
							icon:'none',
							title:'请打开定位或者重新获取'
						});
						return;
					}
					uni.chooseImage({
						count: this.limit,              // 限制的图片数量
						sizeType: ['compressed'],       // original 原图,compressed 压缩图,默认二者都有 
						sourceType: [ 'camera'],// album 从相册选图,camera 使用相机,默认二者都有
						success: (res) => {
							var imgPathList = res.tempFilePaths;
							if(imgPathList.length > 0){
								this.addImages(imgPathList);
							}
						},
						fail: (err) => {
							console.log('chooseImage fail', err)
							if("chooseImage:fail cancel" == err.errMsg){
								uni.showToast({
									icon:'none',
									title:'取消了选择'
								});
							}else{
						
							}
						}
					});
				},
				// 添加图片
				addImages(filePaths){
					
					if(filePaths.length > 0){
						var fillTexts = ["地址:"+this.address];
						fillTexts.push("时间:" + this.getNowTime());
						// 添加水印
						this.addWaterMark({
							filePaths,
							fillTexts
						});
					}
				},
				/**
				 * 水印添加回调,在H5平台下,filePath 为 base64
				 */
				waterMark(filePath){
					this.imageList.push(filePath);
				},
				/**
				 * 获取当前时间
				 */
				getNowTime(){
					var date = new Date(),
					year = date.getFullYear(),
					month = date.getMonth() + 1,
					day = date.getDate(),
					hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(),
					minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(),
					second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
					month >= 1 && month <= 9 ? (month = "0" + month) : "";
					day >= 0 && day <= 9 ? (day = "0" + day) : "";
					return (year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
				},
			 
			/**
			 * 删除文件
			 * @param {Object} index
			 */
			delFile(index) {
				this.$emit('delete', {
					tempFile: this.filesList[index],
					tempFilePath: this.filesList[index].url
				})
				this.filesList.splice(index, 1) 
			 	
				 
			},
			/**
			 * 增加水印
			 * @param {Object} {filePaths:['图片地址1', '图片地址2'], fillTexts:['水印1', '水印2']}
			 */
			async addWaterMark({ filePaths = [], fillTexts = [] }) {
				
				try{
					for (const filePath of filePaths) {
						await this.drawImage(filePath, fillTexts.reverse());
					}
				}catch(e){
					// TODO handle the exception
				}finally{
					// uni.hideLoading();
				}
			},
			/**
			 * 绘制单个图片
			 */
			async drawImage(filePath, fillTexts){
				uni.showLoading({title:'图片处理中···'});
				const ctx = uni.createCanvasContext('watermarkCanvas', this);
				return new Promise(resolve => {
					uni.getImageInfo({
						src: filePath,
						success: (image) => {
							this.canvasWidth = image.width;
							this.canvasHeight = image.height;
							ctx.clearRect(0, 0, image.width, image.height);
							setTimeout(()=>{
								ctx.drawImage(image.path, 0, 0, image.width, image.height);
								ctx.setFontSize(this.fontSize);
								ctx.setFillStyle(this.fontColor);
								// 设置阴影
								let shadowWidth = Number(this.shadowWidth + "");
								if(shadowWidth > 0){
									ctx.shadowColor = this.shadowColor;
									ctx.shadowOffsetX = shadowWidth;
									ctx.shadowOffsetY = shadowWidth;
								}
								// 设置水平对齐方式
								ctx.textAlign = this.textAlign;
								// 设置垂直对齐方式
								ctx.textBaseline = this.textBaseline;
								const maxText = fillTexts.reduce((text, val) => {
									return text.length >= val.length ? text : val;
								});
								fillTexts.forEach((mark, index) => {
									if(this.markAlign == "bottomRight"){
										ctx.fillText(mark, image.width - (ctx.measureText(maxText).width+60), image.height - (index*60+60));
									}else if(this.markAlign == "topLeft"){
										ctx.fillText(mark, 20, (index*60+60));
									}else if(this.markAlign == "topRight"){
										ctx.fillText(mark, image.width - (ctx.measureText(maxText).width+60), (index*60+60));
									}else{
										ctx.fillText(mark, 20, image.height - (index*60+60));
									}
								});
								ctx.draw(false, (() => {
									setTimeout(()=>{
										uni.canvasToTempFilePath({
											canvasId: 'watermarkCanvas',
											fileType:this.fileType,
											quality:Number(this.quality + "" || "1"),
											success: (res) => { 
												console.log("添加水印后的路径",res.tempFilePath )
												this.saveUploadImage(res.tempFilePath )
											},
											fail:(err) => {
												uni.hideLoading();
												console.log(err)
											},
											complete: () => {
												resolve();
											}
										}, this);
									}, 300);
								})());
							}, 200);
						},
						fail: (e) => {
							resolve();
						}
					});
				});
			},
			saveUploadImage(tempFilePath){
				uni.showLoading({title:'图片上传中···'});
				// #ifdef APP-PLUS
				var p = plus.io.convertLocalFileSystemURL(tempFilePath);
				this.url = 'file:/' + p
				console.log("添加水印后的完整路径",this.url )
				// #endif
				uni.uploadFile({
					url: fileServerIp + 'common/upload',
					name: "file",
					// #ifdef H5
					filePath: tempFilePath,
					// #endif
					// #ifdef APP-PLUS
					filePath: this.url,
					// #endif
					header: {
						Authorization: "Bearer " + Session.getValue('token')
					},
					success: uploadFileRes => {
						uni.hideLoading();
						const {
							data
						} = JSON.parse(uploadFileRes.data)
						this.filesList.push({
							url: data.url,
							name: data.fileName,
							extname: 'png'
						}) 
							this.$emit('waterMark',{
							url: data.url,
							name: data.fileName,
							extname: 'png'
						});
					},
					fail: error => {
						uni.hideLoading();
						uni.showToast({
							title: '上传失败!',
							icon: 'error',
							duration: 2000
						});
					}
				})
			}
		}
	}
</script>

<style scoped>
	.watermark-content{width: 0;height: 0;overflow: hidden;}
	.uni-file-picker__container {
		/* #ifndef APP-NVUE */
		display: flex;
		box-sizing: border-box;
		/* #endif */
		flex-wrap: wrap;
		margin: -5px;
	}
	.rotate {
		position: absolute;
		transform: rotate(90deg);
	}
	
	.icon-add {
		width: 50px;
		height: 5px;
		background-color: #f1f1f1;
		border-radius: 2px;
	}
</style>

upload-image.vue

javascript 复制代码
<template>
	<view class="uni-file-picker__container">
		<view class="file-picker__box" v-for="(item,index) in filesList" :key="index" :style="boxStyle">
			<view class="file-picker__box-content" :style="borderStyle">
				<image class="file-image" :src="item.url" mode="aspectFill" @click.stop="prviewImage(item,index)"></image>
				<view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)">
					<view class="icon-del"></view>
					<view class="icon-del rotate"></view>
				</view>
				<view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress">
					<progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4"
					 :backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" />
				</view>
				<view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item,index)">
					点击重试
				</view>
			</view>
		</view>
		<view v-if="filesList.length < limit && !readonly" class="file-picker__box" :style="boxStyle">
			<view class="file-picker__box-content is-add" :style="borderStyle" @click="choose">
				<slot>
					<view class="icon-add"></view>
					<view class="icon-add rotate"></view>
				</slot>
			</view>
		</view>
	</view>
</template>

<script>
	export default {
		name: "uploadImage",
		emits:['uploadFiles','choose','delFile'],
		props: {
			filesList: {
				type: Array,
				default () {
					return []
				}
			},
			disabled:{
				type: Boolean,
				default: false
			},
			disablePreview: {
				type: Boolean,
				default: false
			},
			limit: {
				type: [Number, String],
				default: 9
			},
			imageStyles: {
				type: Object,
				default () {
					return {
						width: 'auto',
						height: 'auto',
						border: {}
					}
				}
			},
			delIcon: {
				type: Boolean,
				default: true
			},
			readonly:{
				type:Boolean,
				default:false
			}
		},
		computed: {
			styles() {
				let styles = {
					width: 'auto',
					height: 'auto',
					border: {}
				}
				return Object.assign(styles, this.imageStyles)
			},
			boxStyle() {
				const {
					width = 'auto',
						height = 'auto'
				} = this.styles
				let obj = {}
				if (height === 'auto') {
					if (width !== 'auto') {
						obj.height = this.value2px(width)
						obj['padding-top'] = 0
					} else {
						obj.height = 0
					}
				} else {
					obj.height = this.value2px(height)
					obj['padding-top'] = 0
				}

				if (width === 'auto') {
					if (height !== 'auto') {
						obj.width = this.value2px(height)
					} else {
						obj.width = '33.3%'
					}
				} else {
					obj.width = this.value2px(width)
				}

				let classles = ''
				for(let i in obj){
					classles+= `${i}:${obj[i]};`
				}
				return classles
			},
			borderStyle() {
				let {
					border
				} = this.styles
				let obj = {}
				const widthDefaultValue = 1
				const radiusDefaultValue = 3
				if (typeof border === 'boolean') {
					obj.border = border ? '1px #eee solid' : 'none'
				} else {
					let width = (border && border.width) || widthDefaultValue
					width = this.value2px(width)
					let radius = (border && border.radius) || radiusDefaultValue
					radius = this.value2px(radius)
					obj = {
						'border-width': width,
						'border-style': (border && border.style) || 'solid',
						'border-color': (border && border.color) || '#eee',
						'border-radius': radius
					}
				}
				let classles = ''
				for(let i in obj){
					classles+= `${i}:${obj[i]};`
				}
				return classles
			}
		},
		methods: {
			uploadFiles(item, index) {
				this.$emit("uploadFiles", item)
			},
			choose() {
				this.$emit("choose")
			},
			delFile(index) {
				this.$emit('delFile', index)
			},
			prviewImage(img, index) {
				let urls = []
				if(Number(this.limit) === 1&&this.disablePreview&&!this.disabled){
					this.$emit("choose")
				}
				if(this.disablePreview) return
				this.filesList.forEach(i => {
					urls.push(i.url)
				})

				uni.previewImage({
					urls: urls,
					current: index
				});
			},
			value2px(value) {
				if (typeof value === 'number') {
					value += 'px'
				} else {
					if (value.indexOf('%') === -1) {
						value = value.indexOf('px') !== -1 ? value : value + 'px'
					}
				}
				return value
			}
		}
	}
</script>

<style lang="scss">
	.uni-file-picker__container {
		/* #ifndef APP-NVUE */
		display: flex;
		box-sizing: border-box;
		/* #endif */
		flex-wrap: wrap;
		margin: -5px;
	}

	.file-picker__box {
		position: relative;
		// flex: 0 0 33.3%;
		width: 33.3%;
		height: 0;
		padding-top: 33.33%;
		/* #ifndef APP-NVUE */
		box-sizing: border-box;
		/* #endif */
	}

	.file-picker__box-content {
		position: absolute;
		top: 0;
		right: 0;
		bottom: 0;
		left: 0;
		margin: 5px;
		border: 1px #eee solid;
		border-radius: 5px;
		overflow: hidden;
	}

	.file-picker__progress {
		position: absolute;
		bottom: 0;
		left: 0;
		right: 0;
		/* border: 1px red solid; */
		z-index: 2;
	}

	.file-picker__progress-item {
		width: 100%;
	}

	.file-picker__mask {
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		justify-content: center;
		align-items: center;
		position: absolute;
		right: 0;
		top: 0;
		bottom: 0;
		left: 0;
		color: #fff;
		font-size: 12px;
		background-color: rgba(0, 0, 0, 0.4);
	}

	.file-image {
		width: 100%;
		height: 100%;
	}

	.is-add {
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		align-items: center;
		justify-content: center;
	}

	.icon-add {
		width: 50px;
		height: 5px;
		background-color: #f1f1f1;
		border-radius: 2px;
	}

	.rotate {
		position: absolute;
		transform: rotate(90deg);
	}

	.icon-del-box {
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		align-items: center;
		justify-content: center;
		position: absolute;
		top: 3px;
		right: 3px;
		height: 26px;
		width: 26px;
		border-radius: 50%;
		background-color: rgba(0, 0, 0, 0.5);
		z-index: 2;
		transform: rotate(-45deg);
	}

	.icon-del {
		width: 15px;
		height: 2px;
		background-color: #fff;
		border-radius: 2px;
	}
</style>
相关推荐
我开心就好o3 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
Random_index3 小时前
#Uniapp篇:支持纯血鸿蒙&发布&适配&UIUI
uni-app·harmonyos
初遇你时动了情11 小时前
uniapp 城市选择插件
开发语言·javascript·uni-app
小小黑00713 小时前
uniapp+vue3+ts H5端使用Quill富文本插件以及解决上传图片反显的问题
uni-app·vue
草字13 小时前
uniapp input限制输入负数,以及保留小数点两位.
java·前端·uni-app
前端小胡兔14 小时前
uniapp rpx兼容平板
uni-app
荔枝吖14 小时前
uniapp实现开发遇到过的问题(持续更新中....)
uni-app
艾小逗14 小时前
uniapp将图片url转换成base64支持app和h5
uni-app·base64·imagetobase64
halo141616 小时前
uni-app 界面TabBar中间大图标设置的两种方法
开发语言·javascript·uni-app
赣州云智科技的技术铺子2 天前
uni app下开发AI运动小程序解决方案
uni-app·ai运动·ai运动识别·运动识别引擎·uni ai运动解决方案·ai运动会·ai体测