vue3+uniapp适配鸿蒙版本拍照压缩后转为base64存储在本地(适用于脱网使用)
使用场景内网环境移动端存储,防止图片过大批量储存压力
·
父组件调用:
<ImageCompressor ref="compressorRef" :showResult="false" :target-size="800 * 1024"
@init-success="onInitSuccess" @init-fail="onInitFail" @compress-success="onCompressSuccess">
<template #trigger="{ captureCompress, isInited }">
<button class="sync-btn" size="mini" :disabled="!isInited" @click="captureCompress">
{{ isInited ? '拍照' : '初始化中...' }}
</button>
</template>
</ImageCompressor>
import ImageCompressor from '@/components/ImageCompressor/ImageCompressor.vue'
const compressorRef = ref(null)
// 初始化成功
const onInitSuccess = () => {
console.log('组件初始化完成')
}
// 初始化失败
const onInitFail = (err) => {
console.error('组件初始化失败:', err)
}
// 压缩成功
const onCompressSuccess = (result) => {
console.log('base64结果:', result.base64);
console.log('原临时文件路径:', result.tempFilePath);
console.log('其他原有字段:', result.size, result.quality);
if (result.base64Error) {
console.error('base64转换失败:', result.base64Error);
}
}
子组件:开箱即用
<template>
<view class="image-compressor">
<canvas
canvas-id="compressCanvas"
class="canvas-core"
style="width: 400px; height: 400px; opacity: 0; position: fixed; top: 0; left: 0; z-index: -999;"
></canvas>
<!-- 外部可自定义触发区域 -->
<slot
name="trigger"
:captureCompress="captureAndCompress"
:isInited="isInited"
></slot>
<!-- 压缩结果展示(可选) -->
<view v-if="showResult && compressResult" class="result-panel">
<text class="result-title">压缩完成</text>
<text class="result-desc">体积:{{ (compressResult.size / 1024).toFixed(2) }} KB</text>
<text class="result-desc">质量:{{ compressResult.quality * 100 }}%</text>
<image :src="compressResult.tempFilePath" class="result-img" mode="widthFix"></image>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, nextTick, defineProps, defineEmits, defineExpose, getCurrentInstance } from 'vue'
// ========== 组件配置项 ==========
const props = defineProps({
// 压缩目标体积(300KB)
targetSize: {
type: Number,
default: 300 * 1024
},
// 是否显示压缩结果
showResult: {
type: Boolean,
default: true
},
// 初始压缩质量
initQuality: {
type: Number,
default: 0.8
},
// 最大缩放尺寸(rpx)
maxSizeRpx: {
type: Object,
default: () => ({ width: 400, height: 400 })
}
})
// ========== 组件事件 ==========
const emit = defineEmits([
'init-success',
'init-fail',
'compress-success',
'compress-fail'
])
// ========== 核心状态 ==========
const isInited = ref(false)
const canvasCtx = ref(null)
const compressResult = ref(null)
const systemInfo = ref(null)
// ========== 初始化逻辑 ==========
onMounted(async () => {
try {
systemInfo.value = uni.getSystemInfoSync()
await nextTick()
await forceInitCanvas()
} catch (err) {
emit('init-fail', '系统信息获取失败:' + err.message)
}
})
const forceInitCanvas = async (retry = 0) => {
const maxRetry = 5
const retryDelay = 300
try {
const ctx = uni.createCanvasContext('compressCanvas', getCurrentInstance())
if (!ctx) throw new Error('创建Canvas上下文失败')
// 验证核心方法存在
if (typeof ctx.draw !== 'function' || typeof ctx.drawImage !== 'function') {
throw new Error('Canvas上下文方法缺失')
}
canvasCtx.value = ctx
isInited.value = true
emit('init-success')
console.log('[ImageCompressor] 初始化完成')
return
} catch (err) {
console.warn(`[ImageCompressor] 初始化失败(${retry+1}/${maxRetry}):`, err.message)
if (retry < maxRetry) {
await new Promise(resolve => setTimeout(resolve, retryDelay))
await forceInitCanvas(retry + 1)
} else {
isInited.value = false
emit('init-fail', 'Canvas初始化失败(已重试5次):' + err.message)
}
}
}
/**
* rpx转px工具函数
*/
const rpxToPx = (rpx) => {
return (rpx * systemInfo.value.screenWidth) / 750
}
/**
* 将文件路径转换为base64
*/
const filePathToBase64 = (filePath) => {
return new Promise((resolve, reject) => {
uni.getFileSystemManager().readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
resolve(`data:image/jpeg;base64,${res.data}`)
},
fail: (err) => {
reject(`转换base64失败:${err.message}`)
}
})
})
}
/**
* 拍照并压缩
*/
const captureAndCompress = () => {
if (!isInited.value || !canvasCtx.value) {
const errMsg = '组件未完成初始化,请稍后重试'
emit('compress-fail', errMsg)
uni.showToast({ title: errMsg, icon: 'none' })
return
}
uni.chooseImage({
count: 1,
sizeType: ['original'],
sourceType: ['camera'],
success: (res) => {
uni.getImageInfo({
src: res.tempFilePaths[0],
success: (imgInfo) => {
compressToTarget(res.tempFilePaths[0], imgInfo)
.then(async (result) => {
// 转换为base64
try {
const base64 = await filePathToBase64(result.tempFilePath)
// 给结果对象添加base64字段
result.base64 = base64
} catch (e) {
result.base64 = ''
result.base64Error = e.message
}
compressResult.value = result
emit('compress-success', result) // 返回包含base64的结果
uni.showToast({ title: '压缩完成', icon: 'success' })
})
.catch((err) => {
emit('compress-fail', err)
uni.showToast({ title: '压缩失败', icon: 'none' })
})
},
fail: (err) => emit('compress-fail', '获取图片信息失败:' + err.message)
})
},
fail: (err) => emit('compress-fail', '拍照失败:' + err.message)
})
}
/**
* 压缩核心逻辑等比缩放
*/
const compressToTarget = (tempFilePath, imgInfo) => {
return new Promise((resolve, reject) => {
let quality = props.initQuality
const minQuality = 0.1
const qualityStep = 0.05
// 转换最大尺寸为px
const maxWidth = rpxToPx(props.maxSizeRpx.width)
const maxHeight = rpxToPx(props.maxSizeRpx.height)
const maxSize = props.targetSize
let compressCount = 0
// 计算等比缩放尺寸
const calculateScaledSize = () => {
let { width, height } = imgInfo
// 如果图片本身小于最大尺寸,直接使用原图尺寸
if (width <= maxWidth && height <= maxHeight) {
return { width, height }
}
// 计算缩放比例
const scaleRatio = Math.min(maxWidth / width, maxHeight / height)
// 等比缩放
return {
width: width * scaleRatio,
height: height * scaleRatio
}
}
// 递归压缩
const compress = () => {
compressCount++
try {
// 获取等比缩放后的尺寸
const { width: drawWidth, height: drawHeight } = calculateScaledSize()
// 清空画布
canvasCtx.value.clearRect(0, 0, drawWidth, drawHeight)
// 绘制图片
canvasCtx.value.drawImage(
tempFilePath,
0, 0, // 绘制起点:左上角,无偏移
drawWidth, // 等比缩放后的宽度
drawHeight // 等比缩放后的高度
)
// 绘制完成后转换临时文件
canvasCtx.value.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: 'compressCanvas',
x: 0,
y: 0,
width: drawWidth, // 使用实际绘制宽度
height: drawHeight, // 使用实际绘制高度
destWidth: drawWidth, // 输出宽度 = 绘制宽度
destHeight: drawHeight, // 输出高度 = 绘制高度
quality: quality,
fileType: 'jpg',
success: (res) => {
// 精准校验体积(300KB以内)
uni.getFileInfo({
filePath: res.tempFilePath,
success: (fileInfo) => {
const size = fileInfo.size
const sizeKB = (size / 1024).toFixed(2)
// 达标判断
if (size <= maxSize || quality <= minQuality) {
resolve({
tempFilePath: res.tempFilePath,
size,
quality,
width: drawWidth, // 实际输出宽度
height: drawHeight, // 实际输出高度
originWidth: imgInfo.width, // 原图宽度
originHeight: imgInfo.height, // 原图高度
compressCount,
finalSizeKB: sizeKB
})
} else {
// 降低质量继续压缩
quality -= qualityStep
quality = Math.max(quality, minQuality)
setTimeout(compress, 100)
}
},
fail: (err) => reject(`获取文件体积失败:${err.message}`)
})
},
fail: (err) => reject(`Canvas转临时文件失败:${err.message}`)
}, getCurrentInstance()) // 传入实例确保路径正确
})
} catch (e) {
reject(`压缩第${compressCount}次失败:${e.message}`)
}
}
compress()
})
}
defineExpose({
captureAndCompress,
forceInitCanvas,
isInited
})
</script>
css:
<style scoped>
.image-compressor {
width: 100%;
box-sizing: border-box;
}
.canvas-core {
width: 1000px !important;
height: 1000px !important;
opacity: 0 !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
z-index: -999 !important;
pointer-events: none !important;
display: block !important;
visibility: hidden !important;
background: transparent !important;
}
.result-panel {
width: 90%;
margin: 20rpx auto;
padding: 20rpx;
background: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}
.result-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
display: block;
}
.result-desc {
font-size: 24rpx;
color: #666;
line-height: 1.5;
display: block;
margin-bottom: 5rpx;
}
.result-img {
width: 100%;
max-width: 400rpx;
max-height: auto;
border-radius: 8rpx;
margin-top: 15rpx;
background: #f5f5f5;
}
</style>
更多推荐



所有评论(0)