fix: 文件上传压缩功能修复。
This commit is contained in:
171
src/utils/imageCompress.ts
Normal file
171
src/utils/imageCompress.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 跨端图片压缩工具
|
||||
*
|
||||
* 用途:上传附件前压缩图片,避免大图(特别是手机原图 4MB+)导致上传卡死
|
||||
*
|
||||
* - H5:用 Canvas 重绘 + toDataURL('image/jpeg', quality)
|
||||
* - 小程序/APP:用 uni.compressImage API(quality + compressedWidth)
|
||||
*
|
||||
* 策略:
|
||||
* - 原图小于 500KB 不压(避免无谓耗时)
|
||||
* - 最大宽度限制 1280px(横屏照片)或按比例缩放
|
||||
* - JPEG 质量 0.75(体积约 1/5 ~ 1/10,肉眼几乎无差)
|
||||
* - 透明 PNG 也压成 JPEG(牺牲透明通道换取体积),如需透明可改 quality 调用
|
||||
*
|
||||
* 用法:
|
||||
* import { compressImage } from '@/utils/imageCompress'
|
||||
* const compressed = await compressImage(filePath)
|
||||
* uploadFile({ filePath: compressed, ... })
|
||||
*/
|
||||
|
||||
/** 不压缩的体积阈值(字节),800KB(小图直接跳过,避免无谓耗时) */
|
||||
const SKIP_COMPRESS_SIZE = 800 * 1024
|
||||
/** 最大边长,超过按比例缩放(保留宽高比)。1920px 兼顾清晰度与体积 */
|
||||
const MAX_SIDE = 1920
|
||||
/** JPEG 压缩质量 0~1。0.85 可保证医疗单据/药品说明书文字清晰 */
|
||||
const JPEG_QUALITY = 0.85
|
||||
|
||||
/**
|
||||
* H5 端压缩:Canvas 重绘 + toBlob
|
||||
*/
|
||||
function compressH5(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 计算缩放后的尺寸
|
||||
let { width, height } = img
|
||||
const longSide = Math.max(width, height)
|
||||
if (longSide > MAX_SIDE) {
|
||||
const scale = MAX_SIDE / longSide
|
||||
width = Math.round(width * scale)
|
||||
height = Math.round(height * scale)
|
||||
}
|
||||
// 创建 canvas 重绘
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
resolve(filePath) // 极端情况降级到原图
|
||||
return
|
||||
}
|
||||
// 白底(防止 PNG 透明通道转 JPEG 后变黑)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
// 转成 Blob 返回 ObjectURL(避免 base64 体积膨胀)
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
resolve(filePath)
|
||||
return
|
||||
}
|
||||
// 释放旧的 ObjectURL(如果是 blob: URL)
|
||||
if (filePath.startsWith('blob:')) {
|
||||
try { URL.revokeObjectURL(filePath) } catch (e) { /* ignore */ }
|
||||
}
|
||||
resolve(URL.createObjectURL(blob))
|
||||
},
|
||||
'image/jpeg',
|
||||
JPEG_QUALITY
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn('[imageCompress] H5 压缩失败,降级原图', err)
|
||||
resolve(filePath)
|
||||
}
|
||||
}
|
||||
img.onerror = (err) => {
|
||||
console.warn('[imageCompress] H5 图片加载失败,降级原图', err)
|
||||
resolve(filePath)
|
||||
}
|
||||
img.src = filePath
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序/APP 端压缩:uni.compressImage
|
||||
*/
|
||||
function compressMP(filePath: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
uni.compressImage({
|
||||
src: filePath,
|
||||
quality: Math.round(JPEG_QUALITY * 100), // 75
|
||||
compressedWidth: MAX_SIDE,
|
||||
success: (res) => resolve(res.tempFilePath),
|
||||
fail: (err) => {
|
||||
console.warn('[imageCompress] uni.compressImage 失败,降级原图', err)
|
||||
resolve(filePath) // 失败降级
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小(字节)。
|
||||
* - H5: blob: / data: URL 用 fetch 解析
|
||||
* - 小程序/APP: uni.getFileInfo
|
||||
*/
|
||||
function getFileSize(filePath: string): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
// #ifdef H5
|
||||
try {
|
||||
if (filePath.startsWith('blob:') || filePath.startsWith('data:')) {
|
||||
fetch(filePath)
|
||||
.then(r => r.blob())
|
||||
.then(b => resolve(b.size))
|
||||
.catch(() => resolve(0))
|
||||
return
|
||||
}
|
||||
} catch (e) { /* fallthrough */ }
|
||||
resolve(0) // 无法获取则不压缩(保守策略)
|
||||
return
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.getFileInfo({
|
||||
filePath,
|
||||
success: (res) => resolve(res.size),
|
||||
fail: () => resolve(0)
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片入口(跨端)
|
||||
* @param filePath 图片临时路径
|
||||
* @returns 压缩后的图片路径(如失败则返回原路径)
|
||||
*/
|
||||
export async function compressImage(filePath: string): Promise<string> {
|
||||
if (!filePath) return filePath
|
||||
try {
|
||||
// 检查体积,小图直接跳过
|
||||
const size = await getFileSize(filePath)
|
||||
if (size > 0 && size < SKIP_COMPRESS_SIZE) {
|
||||
return filePath
|
||||
}
|
||||
// #ifdef H5
|
||||
return await compressH5(filePath)
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return await compressMP(filePath)
|
||||
// #endif
|
||||
} catch (err) {
|
||||
console.warn('[imageCompress] 压缩过程异常,降级原图', err)
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量压缩图片(串行执行,避免一次性占用过多内存)
|
||||
* @param filePaths 图片路径数组
|
||||
* @returns 压缩后的路径数组(与入参顺序一致)
|
||||
*/
|
||||
export async function compressImages(filePaths: string[]): Promise<string[]> {
|
||||
const result: string[] = []
|
||||
for (const p of filePaths) {
|
||||
result.push(await compressImage(p))
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user