其中还包含了图片数据在storage中和本地目录保存的逻辑
javascript
<template>
<!-- #ifdef APP -->
<view style="position: relative;">
<scroll-view class="page-scroll-view">
<!-- #endif -->
<view class="uni-common-mt">
<view class="uni-list list-pd" style="padding: 15px;">
{{date}}
<view class="uni-flex" style="margin-bottom: 10px;">
<view style="margin-left: auto;">
<text class="click-t">{{ previewImgList.length }}/{{ count }}</text>
</view>
</view>
<view class="uni-flex" style="flex-wrap: wrap;">
<view class="uni-flex">
<view v-for="(item, index) in previewImgList" :key="index" class="uni-uploader__input-box"
style="border: 0;">
<image style="width: 104px; height: 104px;" :src="item.path" :data-src="item.path"
@tap="previewImage(index)">
</image>
<text class="status-text">{{item.status}}</text>
<image src="/static/plus.png" class="image-remove" @click="removeImage(index)"></image>
</view>
</view>
<image class="uni-uploader__input-box" @tap="chooseImage" src="/static/plus.png"></image>
</view>
</view>
<button style="width: 160px;" @click="startUpload">开始上传</button>
</view>
<canvas id="canvas" ref="canvasRef" class="canvas-element"> </canvas>
<!-- #ifdef APP -->
</scroll-view>
</view>
<!-- #endif -->
</template>
<script>
// import dayjs from 'dayjs'
// 高清适配(Retina 屏 / 高像素密度屏幕) 的核心逻辑,解决 Canvas 绘制内容模糊的问题
// function hidpi(canvas : UniCanvasElement) {
// const context = canvas.getContext("2d")!;
// const dpr = uni.getWindowInfo().pixelRatio;
// canvas.width = canvas.offsetWidth * dpr;
// canvas.height = canvas.offsetHeight * dpr;
// context.scale(dpr, dpr);
// }
type ImageList = {
id : number,
updateTime : string
path : string
status : string
}
export default {
data() {
return {
// 响应式数据
// date: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
date: "2026-01-01",
previewImgList: [] as ImageList[],
count: 9,
isContinue: false,
// canvas相关
canvas: null as UniCanvasElement | null,
canvasContext: null as CanvasContext | null,
renderingContext: null as CanvasRenderingContext2D | null,
// #ifndef MP
basePath: uni.env.USER_DATA_PATH,
copyToBasePath: uni.env.USER_DATA_PATH,
globalTempPath: uni.env.CACHE_PATH,
globalRootPath: uni.env.SANDBOX_PATH,
globalUserDataPath: uni.env.USER_DATA_PATH,
// #endif
// 照片存-文件读取路径/* */
localFilePath: `${uni.env.USER_DATA_PATH}/path`,
// 创建文件管理器
fileSystemManager: null as FileSystemManager | null,
uploadTask: null as UploadTask | null,
}
},
onLoad() {
this.fileSystemManager = uni.getFileSystemManager()
// console.log('fileSystemManager',this.fileSystemManager)
// console.log('uni.env.USER_DATA_PATH', uni.env.USER_DATA_PATH, uni.env.CACHE_PATH)
// 清空调试数据专用
// if(this.fileSystemManager!==null){
// this.fileSystemManager.rmdirSync(this.basePath as string,true)
// }
// uni.removeStorageSync('waitUploadImg')
// HBuilderX 4.25+
// 异步调用方式, 跨平台写法
uni.createCanvasContextAsync({
id: 'canvas',
component: this,
success: (context : CanvasContext) => {
this.canvasContext = context;
this.renderingContext = context.getContext('2d')!; //Canvas 2D上下文
this.canvas = this.renderingContext.canvas;
// console.log('this.renderingContext', this.renderingContext)
}
})
},
onShow() {
this.previewImgList = []
// 获取缓存,存入imgList,展示图片
// 1.读取storage中的元数据,--避免漏传\重复上传(校验状态:成功跳过,失败重置校验次数,更改为待上传,检查文件是否存在)
// uni.setStorageSync('waitUploadImg', [{ id: '11', updateTime: '10:20', status: 'success', path: `${this.basePath}/img1.png` }, { id: '22', updateTime: '10:22', status: 'error', path: `${this.basePath}/img.png` }])
uni.getStorage({
key: "waitUploadImg",
success: (res : GetStorageSuccess) => {
if (res.data !== null) {
// 从 storage 获取的是 UTSJSONObject 数组
const stor = res.data as UTSJSONObject[]
// 转换为 ImageList 数组
const imageListArray : ImageList[] = stor.map((item : UTSJSONObject) => {
// 根据你的 ImageList 结构进行转换
return {
id: item['id'] as number,
updateTime: item['updateTime'] as string,
path: item['path'] as string,
status: item['status'] as string,
} as ImageList
})
this.previewImgList.push(...imageListArray)
// this.previewImgList = imageListArray
console.log('获取storage--onShow', this.previewImgList)
}
},
fail: () => {
},
complete: (res : any) => {
// console.log('complete', res)
}
})
// 请求接口,获取文件列表
setTimeout(() => {
this.previewImgList.concat([] as ImageList[])
}, 300)
},
onUnload() {
console.log('onUnload')
},
onHide() {
console.log('隐藏页面', uni.getStorageSync('waitUploadImg'))
this.uploadTask?.abort()
// 根据业务逻辑判断是否删除
// 根据元数据的success数据删除文件管理器中的文件,然后删除元数据中成功的数据
const success = this.previewImgList.filter(i => i.status == 'success')
if (success.length > 0) {
// 删除文件
success.forEach(i => {
this.fileSystemManager!.removeSavedFile({
filePath: i.path,
success: () => {
},
fail: () => {
}
})
})
// 跟新元数据
const unSuccess = this.previewImgList.filter(i => i.status != 'success')
uni.setStorageSync('waitUploadImg', unSuccess)
console.log('移除成功的缓存', uni.getStorageSync('waitUploadImg'))
}
},
methods: {
/**
* 绘制水印核心逻辑(原图+文字水印+LOGO水印)
*/
getTimeString() : string {
const d = new Date();
const h = d.getHours() > 9 ? d.getHours().toString() : '0' + d.getHours();
const m = d.getMinutes() > 9 ? d.getMinutes().toString() : '0' + d.getMinutes();
return h + ":" + m;
},
async drawWatermark(imageInfo : GetImageInfoSuccess) {
uni.createCanvasContextAsync({
id: 'canvas',
component: this,
success: (context : CanvasContext) => {
this.canvasContext = context;
this.renderingContext = context.getContext('2d')!; //Canvas 2D上下文
this.canvas = this.renderingContext.canvas;
console.log('this.renderingContext', this.renderingContext)
let width = imageInfo['width'] as number
let height = imageInfo['height'] as number
const path = imageInfo['path'] as string
const canvasDom = uni.getElementById('canvas') as UniCanvasElement
if (this.canvas !== null) {
const dpr = uni.getWindowInfo().pixelRatio;
console.log('width', width, width / dpr)
console.log('height', height, height / dpr)
this.canvas.width = width / dpr;
this.canvas.height = height / dpr;
// this.canvas.width = 300 ;
// this.canvas.height = 300 ;
// this.renderingContext.scale(3,3)
canvasDom.style.setProperty('width', width / dpr + 'px');
canvasDom.style.setProperty('height', height / dpr + 'px');
}
setTimeout(() => {
const ctx = canvasDom.getContext('2d')!;
console.log('canvasDomSize', width, height, ctx)
try {
// 清空画布
ctx.clearRect(0, 0, width, height);
// 1. 绘制原图到Canvas
let image = this.canvasContext!.createImage();
image.src = path
image.onload = () => {
ctx.drawImage(image, 0, 0, width, height);
const fontSize = width / 25;
ctx.font = "bold " + fontSize + "px sans-serif";
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.textAlign = "right";
ctx.textBaseline = "bottom";
const text = "Watermark " + this.getTimeString();
const padding = fontSize;
ctx.fillText(text, width - padding, height - padding);
console.log('ctx is', ctx)
setTimeout(() => {
canvasDom!.takeSnapshot({
success: (snapshotRes) => {
// console.log("水印成功:", snapshotRes);
// uni.getImageInfo({
// src: snapshotRes.tempFilePath,
// success: (r1) => {
// console.log('截图成功后图片信息', r1)
// console.log('截图成功后Canvas信息', this.renderingContext)
// }
// })
this.saveFile(snapshotRes.tempFilePath)
// 1.把this.imgList的数据存储到storage中,成功一个从storage中移除一个
uni.setStorageSync('waitUploadImg', this.previewImgList)
},
fail: (err) => {
console.error("截图失败", err);
}
});
}, 300)
// uni.showToast({ title: '水印添加成功', icon: 'success' });
}
} catch (err) {
console.error('绘制水印失败:', err);
uni.showToast({ title: '水印添加失败', icon: 'none' });
}
}, 0)
}
})
},
removeImage(index : number) {
this.previewImgList.splice(index, 1)
},
chooseImage() {
if (this.previewImgList.length >= this.count) {
uni.showToast({
position: "bottom",
title: `已经有 ${this.count} 张图片了,请删除部分图片之后重新选择`
})
return
}
uni.chooseImage({
sourceType: ['camera'],
sizeType: ['compressed'],
count: this.count - this.previewImgList.length,
success: (res) => {
// 文件的临时路径,在应用本次启动期间可以正常使用,如需持久保存,需在主动调用 uni.saveFile,在应用下次启动时才能访问得到。
const imagePath = res.tempFilePaths[0];
uni.getImageInfo({
src: imagePath,
success: (r) => {
// 2.canvas绘制水印,并合成图片
this.drawWatermark(r);
}
})
},
fail: (err) => {
console.log("err: ", JSON.stringify(err))
uni.showToast({
title: `choose image error.code:${err.errCode};message:${err.errMsg}`,
position: "bottom"
})
}
})
},
/**
* 开始/继续上传图片
*/
startUpload() {
this.handleBatchUpload(this.previewImgList)
},
async handleBatchUpload(dataList : ImageList[]) {
const list = dataList.filter(i => (i.status == 'error' || i.status == 'ready')) as ImageList[];
for (const i of list) {
const index = this.previewImgList.findIndex(j => j.id == i.id)
try {
const uploadRes = await this.upload(i.path as string, index, { time: i.updateTime }) as boolean;
if (uploadRes) {
// 上传成功 --更新上传状态,持久化存储
if (index != -1) {
this.previewImgList[index].status = 'success'
}
} else {
// 上传失败
if (index != -1) {
this.previewImgList[index].status = 'error'
}
}
// 即时更新元数据
uni.setStorage({
key: 'waitUploadImg',
// data:JSON.stringify(this.previewImgList)
data: this.previewImgList
})
} catch (error) {
console.log('error', error)
}
}
// 所有文件上传完成后的逻辑
uni.showToast({ title: '批量上传完成', icon: 'success' });
},
upload(path : string, index : number, params : UTSJSONObject) {
console.log('upload', path)
return new Promise((resolve, reject) => {
this.uploadTask = uni.uploadFile({
url: 'http://192.168.101.220:5071/lmy/upload',
filePath: path,
formData: { testPr: params.time },
name: 'file',
success: (result : UploadFileSuccess) => {
console.log('上传成功', result)
// 再根据响应值判断是否上传成功
resolve(true)
}, fail: (result : UploadFileFail) => {
console.log('上传失败', result)
resolve(false)
},
complete: (res : any) => {
console.log('上传完成complete', res)
this.uploadTask = null
}
})
this.uploadTask.onProgressUpdate((result : OnProgressUpdateResult) => {
console.log('上传进度result', result.progress)
// // 监听上传进度--标记状态为上传中
if (result.progress > 0 && result.progress < 1) {
if (index != -1) {
this.previewImgList[index].status = 'uploading'
}
}
})
})
},
saveFile(tempPath : string) {
// 创建文件夹
// 保存文件到用户目录 保存成功后临时路径会失效
const pathArr = tempPath.split('/')
const fileName = pathArr[pathArr.length - 1]
if (this.fileSystemManager !== null) {
function saveFn() {
this.fileSystemManager!.saveFile({
tempFilePath: `${tempPath}`,
filePath: `${this.localFilePath}/${fileName}`,
success: (res1 : SaveFileSuccessResult) => {
console.log('res1', res1)
this.previewImgList.push({
id: Math.random(), //后面改-英文数字随机32位id
updateTime: this.getTimeString(), //new Date().getTime()
status: 'ready', //准备(待)上传ready,上传中uploading,上传成功success,上传失败error
path: `${this.localFilePath}/${fileName}`
} as ImageList)
}, fail: (res2 : FileSystemManagerFail) => {
console.log('res2', res2)
},
} as SaveFileOptions)
}
this.fileSystemManager.readdir({
dirPath: `${this.localFilePath}`,
success: (res : ReadDirSuccessResult) => {
console.log('文件管理器路径存在666', res)
saveFn()
},
fail: () => {
this.fileSystemManager!.mkdirSync(`${this.localFilePath}`, true)
saveFn()
}
})
}
},
/**
* 预览指定索引的图片
* @param index 图片索引
*/
previewImage(index : number) {
uni.previewImage({
current: index,
urls: this.previewImgList.map(i => i.path)
})
}
},
unmounted() {
// 组件卸载时的清理逻辑
}
}
</script>
<style>
.page-scroll-view {
max-height: 1000px;
}
.uni-flex {
display: flex;
}
.uni-uploader__input-box {
margin: 5px;
width: 104px;
height: 104px;
border: 1px solid #D9D9D9;
}
.uni-uploader__input {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
}
.image-remove {
transform: rotate(45deg);
width: 25px;
height: 25px;
position: absolute;
top: 0;
right: 0;
border-radius: 13px;
background-color: rgba(200, 200, 200, 0.8);
}
.status-text {
position: absolute;
bottom: 0;
right: 50%;
transform: translateX(-50%);
background-color: rgba(200, 200, 200, 0.8);
color: #fff;
}
.item_width {
width: 130px;
}
.crop-option {
margin-left: 11px;
margin-right: 11px;
border-radius: 11px;
background-color: #eee;
transition-property: height, margin-bottom;
transition-duration: 200ms;
}
.canvas-element {
/* position: absolute;
left: 1000px;
bottom: 0;
min-width: 375px;
min-height: 500px; */
}
</style>