gift/common/utils.js
2025-07-31 20:06:59 +08:00

295 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /**
// * 获取文件大小Promise封装
// * @param {string} filePath
// * @returns {Promise<number>} 文件字节数
// */
// const getFileSize = (filePath) => {
// return new Promise(resolve => {
// uni.getFileInfo({
// filePath,
// success: (res) => resolve(res.size),
// fail: () => resolve(Infinity)
// });
// });
// };
/**
* 获取文件大小兼容新版API
* @param {string} filePath
* @returns {Promise<number>} 文件字节数
*/
const getFileSize = async (filePath) => {
try {
const res = await new Promise((resolve, reject) => {
wx.getFileSystemManager().getFileInfo({
filePath,
success: resolve,
fail: reject
});
});
return res.size || Infinity;
} catch (err) {
console.error('获取文件大小失败:', err);
return Infinity;
}
};
/**
* 执行图片压缩Promise封装
* @param {string} src
* @param {number} quality
* @returns {Promise<string>} 压缩后的临时路径
*/
const doCompressImage = (src, quality) => {
return new Promise(resolve => {
uni.compressImage({
src,
quality,
success: (res) => resolve(res.tempFilePath),
fail: () => resolve(src) // 失败返回原路径
});
});
};
/**
* 智能图片压缩(支持多平台)
* @param {string} tempFilePath - 图片临时路径
* @param {number} maxSize - 目标最大字节数默认500KB
* @param {number} [initialQuality=80] - 初始压缩质量(1-100)
* @returns {Promise<string>} 压缩后的临时路径
*/
export const compressImage = async (tempFilePath, maxSize = 500 * 1024, initialQuality = 80) => {
let quality = initialQuality;
let compressedPath = tempFilePath;
let currentSize = Infinity;
// 二分法压缩参数
let low = 10;
let high = initialQuality;
while (low <= high) {
quality = Math.floor((low + high) / 2);
// 使用提取的Promise方法
compressedPath = await doCompressImage(tempFilePath, quality);
currentSize = await getFileSize(compressedPath);
if (currentSize <= maxSize) {
low = quality + 1; // 尝试更高质量
} else {
high = quality - 1; // 降低质量
}
if (high - low < 5) break; // 安全阀
}
// 最终强制压缩(如果仍未达标)
if (currentSize > maxSize) {
compressedPath = await doCompressImage(tempFilePath, 10);
}
return compressedPath;
};
/**
* 精准图片压缩(优化版)
* @param {string} tempFilePath - 图片临时路径
* @param {number} targetSize - 目标字节数默认500KB
* @param {number} [initialQuality=85] - 初始压缩质量(1-100)
* @returns {Promise<{path: string, quality: number, iterations: number, finalSize: number}>}
*/
export const preciseCompress = async (tempFilePath, targetSize = 500 * 1024, initialQuality = 85) => {
let quality = initialQuality;
let compressedPath = tempFilePath;
let currentSize = Infinity;
let iterations = 0;
const maxIterations = 10; // 增加最大迭代次数
const history = []; // 记录每次压缩结果用于分析
// 首先检查原图大小,如果已经满足直接返回
const originalSize = await getFileSize(tempFilePath);
if (originalSize <= targetSize) {
return { path: tempFilePath, quality: 100, iterations: 0, finalSize: originalSize };
}
// 智能预测初始质量范围(新增)
const predictQualityRange = (originalSize, targetSize) => {
const ratio = targetSize / originalSize;
if (ratio > 0.8) return { low: 70, high: 95 };
if (ratio > 0.5) return { low: 45, high: 75 };
if (ratio > 0.3) return { low: 25, high: 50 };
if (ratio > 0.1) return { low: 10, high: 30 };
return { low: 5, high: 15 }; // 极限压缩
};
const { low: predictedLow, high: predictedHigh } = predictQualityRange(originalSize, targetSize);
let low = predictedLow;
let high = Math.min(predictedHigh, initialQuality);
console.log(`预测质量范围: ${low}-${high} (原图${(originalSize/1024).toFixed(2)}KB → 目标${(targetSize/1024).toFixed(2)}KB)`);
// 动态调整二分法边界(优化版)
const adjustBounds = (currentQuality, currentSize, targetSize) => {
const sizeRatio = currentSize / targetSize;
const qualityDelta = Math.max(2, Math.floor(currentQuality * 0.1)); // 动态调整幅度
if (sizeRatio > 2.5) {
return { lowDelta: -qualityDelta * 2, highDelta: -qualityDelta };
} else if (sizeRatio > 1.5) {
return { lowDelta: -qualityDelta, highDelta: -Math.floor(qualityDelta / 2) };
} else if (sizeRatio > 1.1) {
return { lowDelta: -3, highDelta: -1 };
} else {
return { lowDelta: -1, highDelta: 0 };
}
};
let bestResult = null; // 记录最佳结果
while (iterations < maxIterations && low <= high) {
iterations++;
quality = Math.floor((low + high) / 2);
// 跳过已经测试过的quality值
if (history.some(item => item.quality === quality)) {
break;
}
compressedPath = await doCompressImage(tempFilePath, quality);
currentSize = await getFileSize(compressedPath);
history.push({ quality, size: currentSize });
console.log(`${iterations}次尝试: quality=${quality}${(currentSize / 1024).toFixed(2)}KB (目标${(targetSize/1024).toFixed(2)}KB)`);
// 更新最佳结果
if (!bestResult || Math.abs(currentSize - targetSize) < Math.abs(bestResult.size - targetSize)) {
bestResult = { quality, size: currentSize, path: compressedPath };
}
// 精准匹配检查容差范围调整为3%
if (Math.abs(currentSize - targetSize) < targetSize * 0.03) {
console.log('✅ 达到精准匹配,提前结束');
break;
}
if (currentSize > targetSize) {
const { lowDelta, highDelta } = adjustBounds(quality, currentSize, targetSize);
high = Math.max(5, quality + highDelta); // 确保不会低于最小值
} else {
low = quality + 1; // 当小于目标时,尝试更高质量
}
// 收敛检查
if (high - low <= 1) break;
}
// 如果最佳结果仍然超标,尝试极限压缩
if (bestResult && bestResult.size > targetSize * 1.1) {
console.log('🔥 尝试极限压缩...');
const extremePath = await doCompressImage(tempFilePath, 5);
const extremeSize = await getFileSize(extremePath);
console.log(`极限压缩结果: quality=5 → ${(extremeSize / 1024).toFixed(2)}KB`);
if (extremeSize < bestResult.size) {
bestResult = { quality: 5, size: extremeSize, path: extremePath };
}
}
const finalResult = bestResult || { quality: 10, size: currentSize, path: compressedPath };
console.log(`🎯 最终结果quality=${finalResult.quality}${(finalResult.size / 1024).toFixed(2)}KB` +
`(迭代${iterations}次,压缩率${((1 - finalResult.size / originalSize) * 100).toFixed(1)}%`);
return {
path: finalResult.path,
quality: finalResult.quality,
iterations,
finalSize: finalResult.size
};
};
/**
* 图片转Base64兼容多平台
* @param {string} filePath - 图片临时路径
* @returns {Promise<string>} Base64字符串带data:image/前缀)
*/
export const imageToBase64 = (filePath) => {
return new Promise((resolve, reject) => {
// 先获取图片类型
uni.getImageInfo({
src: filePath,
success: (info) => {
const type = info.type || 'jpeg';
// 读取文件为Base64
uni.getFileSystemManager().readFile({
filePath,
encoding: 'base64',
success: (res) => resolve(`data:image/${type};base64,${res.data}`),
fail: reject
});
},
fail: () => {
// 类型获取失败时默认jpeg
uni.getFileSystemManager().readFile({
filePath,
encoding: 'base64',
success: (res) => resolve(`data:image/jpeg;base64,${res.data}`),
fail: reject
});
}
});
});
};
/**
* 完整处理流程(智能判断是否压缩):
* 选择图片 → 检查大小 → 按需压缩 → 转Base64
* @returns {Promise<{ base64: string, path: string, compressed: boolean }>}
*/
export const processImage = async (maxSize = 500 * 1024) => {
// 1. 选择图片
const chooseRes = await new Promise(resolve => {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
success: resolve,
fail: () => resolve(null)
});
});
if (!chooseRes) throw new Error('选择图片失败');
const tempFilePath = chooseRes.tempFilePaths[0];
let finalPath = tempFilePath;
let isCompressed = false;
// 2. 检查文件大小
const fileSize = await getFileSize(tempFilePath);
console.log(`原图大小: ${(fileSize / 1024).toFixed(2)}KB`);
// 3. 按需压缩
if (fileSize > maxSize) {
console.log('🔄 开始智能压缩...');
const compressResult = await preciseCompress(tempFilePath, maxSize);
finalPath = compressResult.path;
isCompressed = true;
// 验证压缩结果
console.log(`✅ 压缩完成: ${(fileSize / 1024).toFixed(2)}KB → ${(compressResult.finalSize / 1024).toFixed(2)}KB` +
` (节省${((1 - compressResult.finalSize / fileSize) * 100).toFixed(1)}%)`);
}
// 4. 转换为Base64
const base64 = await imageToBase64(finalPath);
console.log(`📄 Base64长度: ${base64.length} 字符`);
return {
base64, // Base64字符串
path: finalPath, // 最终文件路径(可能是原图或压缩图)
compressed: isCompressed // 是否经过压缩
};
};