fix: 文件上传压缩功能修复。

This commit is contained in:
tianyongbao
2026-07-08 10:10:38 +08:00
parent 6aaf564800
commit aeb1e39920
5 changed files with 445 additions and 261 deletions

171
src/utils/imageCompress.ts Normal file
View File

@@ -0,0 +1,171 @@
/**
* 跨端图片压缩工具
*
* 用途:上传附件前压缩图片,避免大图(特别是手机原图 4MB+)导致上传卡死
*
* - H5用 Canvas 重绘 + toDataURL('image/jpeg', quality)
* - 小程序/APP用 uni.compressImage APIquality + 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
}