findme-miniprogram-frontend/subpackages/media/utils/media-manager.js

838 lines
20 KiB
JavaScript
Raw Permalink Normal View History

2025-12-27 17:16:03 +08:00
// 媒体文件管理器 - 微信小程序专用
// 处理图片、视频、音频、文件的上传、下载、预览、缓存等
const apiClient = require('../../../utils/api-client.js');
/**
* 媒体文件管理器
* 功能
* 1. 文件选择和上传
* 2. 媒体文件预览
* 3. 文件下载和缓存
* 4. 文件压缩和优化
* 5. 云存储管理
* 6. 文件类型检测
*/
class MediaManager {
constructor() {
this.isInitialized = false;
// 媒体配置
this.mediaConfig = {
// 图片配置
image: {
maxSize: 10 * 1024 * 1024, // 10MB
maxCount: 9, // 最多选择9张
quality: 80, // 压缩质量
formats: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
compressWidth: 1080 // 压缩宽度
},
// 视频配置
video: {
maxSize: 100 * 1024 * 1024, // 100MB
maxDuration: 300, // 最长5分钟
formats: ['mp4', 'mov', 'avi'],
compressQuality: 'medium'
},
// 音频配置
audio: {
maxSize: 20 * 1024 * 1024, // 20MB
maxDuration: 600, // 最长10分钟
formats: ['mp3', 'wav', 'aac', 'm4a'],
sampleRate: 16000
},
// 文件配置
file: {
maxSize: 50 * 1024 * 1024, // 50MB
allowedTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar'],
maxCount: 5
},
// 缓存配置
cache: {
maxSize: 200 * 1024 * 1024, // 200MB
expireTime: 7 * 24 * 60 * 60 * 1000, // 7天
cleanupInterval: 24 * 60 * 60 * 1000 // 24小时清理一次
}
};
// 文件缓存
this.fileCache = new Map();
// 上传队列
this.uploadQueue = [];
// 下载队列
this.downloadQueue = [];
// 当前上传任务
this.currentUploads = new Map();
// 缓存统计
this.cacheStats = {
totalSize: 0,
fileCount: 0,
lastCleanup: 0
};
this.init();
}
// 初始化媒体管理器
async init() {
if (this.isInitialized) return;
try {
// 加载文件缓存信息
await this.loadCacheInfo();
// 检查存储权限
await this.checkStoragePermission();
// 启动缓存清理
this.startCacheCleanup();
this.isInitialized = true;
} catch (error) {
console.error('❌ 媒体文件管理器初始化失败:', error);
}
}
// 📷 ===== 图片处理 =====
// 选择图片
async chooseImages(options = {}) {
try {
const {
count = this.mediaConfig.image.maxCount,
sizeType = ['compressed', 'original'],
sourceType = ['album', 'camera']
} = options;
const result = await new Promise((resolve, reject) => {
wx.chooseImage({
count: count,
sizeType: sizeType,
sourceType: sourceType,
success: resolve,
fail: reject
});
});
// 验证和处理图片
const processedImages = await this.processImages(result.tempFilePaths);
return {
success: true,
images: processedImages
};
} catch (error) {
console.error('❌ 选择图片失败:', error);
return {
success: false,
error: error.errMsg || '选择图片失败'
};
}
}
// 处理图片
async processImages(tempFilePaths) {
const processedImages = [];
for (const tempPath of tempFilePaths) {
try {
// 获取图片信息
const imageInfo = await this.getImageInfo(tempPath);
// 检查文件大小
if (imageInfo.size > this.mediaConfig.image.maxSize) {
console.warn('⚠️ 图片过大,需要压缩:', imageInfo.size);
// 压缩图片
const compressedPath = await this.compressImage(tempPath);
imageInfo.tempFilePath = compressedPath;
}
// 生成缩略图
const thumbnailPath = await this.generateThumbnail(imageInfo.tempFilePath);
processedImages.push({
...imageInfo,
thumbnailPath: thumbnailPath,
type: 'image',
status: 'ready'
});
} catch (error) {
console.error('❌ 处理图片失败:', error);
}
}
return processedImages;
}
// 获取图片信息
async getImageInfo(src) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src: src,
success: (res) => {
// 获取文件大小
wx.getFileInfo({
filePath: src,
success: (fileInfo) => {
resolve({
...res,
size: fileInfo.size,
tempFilePath: src
});
},
fail: () => {
resolve({
...res,
size: 0,
tempFilePath: src
});
}
});
},
fail: reject
});
});
}
// 压缩图片
// 图片压缩处理函数
compressImage(tempFilePath) {
return new Promise((resolve, reject) => {
// 先获取图片信息,用于计算压缩比例
wx.getImageInfo({
src: tempFilePath,
success: (info) => {
// 根据图片尺寸计算合适的压缩比例
let quality = 0.6; // 初始质量
let width = info.width;
let height = info.height;
// 对于过大的图片先缩小尺寸
if (width > 1000 || height > 1000) {
const scale = 1000 / Math.max(width, height);
width = Math.floor(width * scale);
height = Math.floor(height * scale);
quality = 0.5; // 大图片适当降低质量
} else if (width > 500 || height > 500) {
quality = 0.55; // 中等图片适度压缩
}
// 执行压缩
wx.compressImage({
src: tempFilePath,
quality: quality, // 质量0-100这里用0.6表示60%质量
width: width, // 压缩后的宽度
height: height, // 压缩后的高度
success: (res) => {
resolve(res.tempFilePath);
},
fail: (err) => {
reject(err);
}
});
},
fail: (err) => {
reject(err);
}
});
});
}
// 生成缩略图
async generateThumbnail(src) {
try {
// 创建canvas生成缩略图
const canvas = wx.createOffscreenCanvas({ type: '2d' });
const ctx = canvas.getContext('2d');
// 设置缩略图尺寸
const thumbnailSize = 200;
canvas.width = thumbnailSize;
canvas.height = thumbnailSize;
// 加载图片
const image = canvas.createImage();
return new Promise((resolve) => {
image.onload = () => {
// 计算绘制尺寸
const { drawWidth, drawHeight, drawX, drawY } = this.calculateDrawSize(
image.width,
image.height,
thumbnailSize,
thumbnailSize
);
// 绘制缩略图
ctx.drawImage(image, drawX, drawY, drawWidth, drawHeight);
// 导出为临时文件
wx.canvasToTempFilePath({
canvas: canvas,
success: (res) => {
resolve(res.tempFilePath);
},
fail: () => {
resolve(src); // 生成失败返回原图
}
});
};
image.onerror = () => {
resolve(src); // 加载失败返回原图
};
image.src = src;
});
} catch (error) {
console.error('❌ 生成缩略图失败:', error);
return src;
}
}
// 计算绘制尺寸
calculateDrawSize(imageWidth, imageHeight, canvasWidth, canvasHeight) {
const imageRatio = imageWidth / imageHeight;
const canvasRatio = canvasWidth / canvasHeight;
let drawWidth, drawHeight, drawX, drawY;
if (imageRatio > canvasRatio) {
// 图片更宽,以高度为准
drawHeight = canvasHeight;
drawWidth = drawHeight * imageRatio;
drawX = (canvasWidth - drawWidth) / 2;
drawY = 0;
} else {
// 图片更高,以宽度为准
drawWidth = canvasWidth;
drawHeight = drawWidth / imageRatio;
drawX = 0;
drawY = (canvasHeight - drawHeight) / 2;
}
return { drawWidth, drawHeight, drawX, drawY };
}
// 🎬 ===== 视频处理 =====
// 选择视频
async chooseVideo(options = {}) {
try {
const {
sourceType = ['album', 'camera'],
maxDuration = this.mediaConfig.video.maxDuration,
camera = 'back'
} = options;
const result = await new Promise((resolve, reject) => {
wx.chooseVideo({
sourceType: sourceType,
maxDuration: maxDuration,
camera: camera,
success: resolve,
fail: reject
});
});
// 验证和处理视频
const processedVideo = await this.processVideo(result);
return {
success: true,
video: processedVideo
};
} catch (error) {
console.error('❌ 选择视频失败:', error);
return {
success: false,
error: error.errMsg || '选择视频失败'
};
}
}
// 处理视频
async processVideo(videoResult) {
try {
// 检查文件大小
if (videoResult.size > this.mediaConfig.video.maxSize) {
throw new Error('视频文件过大');
}
// 检查时长
if (videoResult.duration > this.mediaConfig.video.maxDuration) {
throw new Error('视频时长超出限制');
}
// 生成视频缩略图
const thumbnailPath = await this.generateVideoThumbnail(videoResult.tempFilePath);
return {
tempFilePath: videoResult.tempFilePath,
duration: videoResult.duration,
size: videoResult.size,
width: videoResult.width,
height: videoResult.height,
thumbnailPath: thumbnailPath,
type: 'video',
status: 'ready'
};
} catch (error) {
console.error('❌ 处理视频失败:', error);
throw error;
}
}
// 生成视频缩略图
async generateVideoThumbnail(videoPath) {
try {
// 微信小程序暂不支持视频帧提取,使用默认图标
return null;
} catch (error) {
console.error('❌ 生成视频缩略图失败:', error);
return null;
}
}
// 📄 ===== 文件处理 =====
// 选择文件
async chooseFile(options = {}) {
try {
const {
count = this.mediaConfig.file.maxCount,
type = 'all'
} = options;
const result = await new Promise((resolve, reject) => {
wx.chooseMessageFile({
count: count,
type: type,
success: resolve,
fail: reject
});
});
// 验证和处理文件
const processedFiles = await this.processFiles(result.tempFiles);
return {
success: true,
files: processedFiles
};
} catch (error) {
console.error('❌ 选择文件失败:', error);
return {
success: false,
error: error.errMsg || '选择文件失败'
};
}
}
// 处理文件
async processFiles(tempFiles) {
const processedFiles = [];
for (const file of tempFiles) {
try {
// 检查文件大小
if (file.size > this.mediaConfig.file.maxSize) {
console.warn('⚠️ 文件过大:', file.name, file.size);
continue;
}
// 检查文件类型
const fileExtension = this.getFileExtension(file.name);
if (!this.isAllowedFileType(fileExtension)) {
console.warn('⚠️ 不支持的文件类型:', fileExtension);
continue;
}
processedFiles.push({
tempFilePath: file.path,
name: file.name,
size: file.size,
type: 'file',
extension: fileExtension,
status: 'ready'
});
} catch (error) {
console.error('❌ 处理文件失败:', error);
}
}
return processedFiles;
}
// 获取文件扩展名
getFileExtension(fileName) {
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex === -1) return '';
return fileName.substring(lastDotIndex + 1).toLowerCase();
}
// 检查文件类型是否允许
isAllowedFileType(extension) {
return this.mediaConfig.file.allowedTypes.includes(extension);
}
// 📤 ===== 文件上传 =====
// 上传文件
async uploadFile(file, options = {}) {
try {
const {
onProgress,
onSuccess,
onError
} = options;
// 生成上传ID
const uploadId = this.generateUploadId();
// 添加到上传队列
const uploadTask = {
id: uploadId,
file: file,
status: 'uploading',
progress: 0,
onProgress: onProgress,
onSuccess: onSuccess,
onError: onError
};
this.currentUploads.set(uploadId, uploadTask);
// 执行上传
const result = await this.performUpload(uploadTask);
// 移除上传任务
this.currentUploads.delete(uploadId);
return result;
} catch (error) {
console.error('❌ 上传文件失败:', error);
return {
success: false,
error: error.message
};
}
}
// 执行上传
async performUpload(uploadTask) {
return new Promise((resolve, reject) => {
const uploadTask_wx = wx.uploadFile({
url: `${apiClient.baseURL}/api/v1/files/upload`,
filePath: uploadTask.file.tempFilePath,
name: 'file',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
formData: {
type: uploadTask.file.type,
name: uploadTask.file.name || 'unknown'
},
success: (res) => {
try {
const data = JSON.parse(res.data);
if (data.success) {
const result = {
success: true,
data: {
url: data.data.url,
fileId: data.data.fileId,
fileName: uploadTask.file.name,
fileSize: uploadTask.file.size,
fileType: uploadTask.file.type
}
};
if (uploadTask.onSuccess) {
uploadTask.onSuccess(result);
}
resolve(result);
} else {
throw new Error(data.error || '上传失败');
}
} catch (error) {
reject(error);
}
},
fail: (error) => {
console.error('❌ 上传请求失败:', error);
if (uploadTask.onError) {
uploadTask.onError(error);
}
reject(error);
}
});
// 监听上传进度
uploadTask_wx.onProgressUpdate((res) => {
uploadTask.progress = res.progress;
if (uploadTask.onProgress) {
uploadTask.onProgress({
progress: res.progress,
totalBytesSent: res.totalBytesSent,
totalBytesExpectedToSend: res.totalBytesExpectedToSend
});
}
});
// 保存上传任务引用
uploadTask.wxTask = uploadTask_wx;
});
}
// 取消上传
cancelUpload(uploadId) {
const uploadTask = this.currentUploads.get(uploadId);
if (uploadTask && uploadTask.wxTask) {
uploadTask.wxTask.abort();
this.currentUploads.delete(uploadId);
}
}
// 生成上传ID
generateUploadId() {
return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// 📥 ===== 文件下载和缓存 =====
// 下载文件
async downloadFile(url, options = {}) {
try {
const {
fileName,
onProgress,
useCache = true
} = options;
// 检查缓存
if (useCache) {
const cachedPath = this.getCachedFilePath(url);
if (cachedPath) {
return {
success: true,
tempFilePath: cachedPath,
cached: true
};
}
}
// 执行下载
const result = await this.performDownload(url, { fileName, onProgress });
// 缓存文件
if (result.success && useCache) {
this.cacheFile(url, result.tempFilePath);
}
return result;
} catch (error) {
console.error('❌ 下载文件失败:', error);
return {
success: false,
error: error.message
};
}
}
// 执行下载
async performDownload(url, options = {}) {
return new Promise((resolve, reject) => {
const downloadTask = wx.downloadFile({
url: url,
success: (res) => {
if (res.statusCode === 200) {
resolve({
success: true,
tempFilePath: res.tempFilePath,
cached: false
});
} else {
reject(new Error(`下载失败: ${res.statusCode}`));
}
},
fail: reject
});
// 监听下载进度
if (options.onProgress) {
downloadTask.onProgressUpdate((res) => {
options.onProgress({
progress: res.progress,
totalBytesWritten: res.totalBytesWritten,
totalBytesExpectedToWrite: res.totalBytesExpectedToWrite
});
});
}
});
}
// 检查存储权限
async checkStoragePermission() {
try {
const storageInfo = wx.getStorageInfoSync();
return true;
} catch (error) {
console.error('❌ 检查存储权限失败:', error);
return false;
}
}
// 加载缓存信息
async loadCacheInfo() {
try {
const cacheInfo = wx.getStorageSync('mediaCacheInfo') || {};
this.cacheStats = {
totalSize: cacheInfo.totalSize || 0,
fileCount: cacheInfo.fileCount || 0,
lastCleanup: cacheInfo.lastCleanup || 0
};
} catch (error) {
console.error('❌ 加载缓存信息失败:', error);
}
}
// 保存缓存信息
async saveCacheInfo() {
try {
wx.setStorageSync('mediaCacheInfo', this.cacheStats);
} catch (error) {
console.error('❌ 保存缓存信息失败:', error);
}
}
// 获取缓存文件路径
getCachedFilePath(url) {
const cacheKey = this.generateCacheKey(url);
return this.fileCache.get(cacheKey);
}
// 缓存文件
cacheFile(url, filePath) {
const cacheKey = this.generateCacheKey(url);
this.fileCache.set(cacheKey, filePath);
// 更新缓存统计
this.cacheStats.fileCount++;
this.saveCacheInfo();
}
// 生成缓存键
generateCacheKey(url) {
// 使用URL的hash作为缓存键
let hash = 0;
for (let i = 0; i < url.length; i++) {
const char = url.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}
return `cache_${Math.abs(hash)}`;
}
// 启动缓存清理
startCacheCleanup() {
setInterval(() => {
this.performCacheCleanup();
}, this.mediaConfig.cache.cleanupInterval);
}
// 执行缓存清理
performCacheCleanup() {
try {
const now = Date.now();
const expireTime = this.mediaConfig.cache.expireTime;
// 清理过期缓存
for (const [key, filePath] of this.fileCache) {
try {
const stats = wx.getFileInfo({ filePath });
if (now - stats.createTime > expireTime) {
this.fileCache.delete(key);
// 删除文件
wx.removeSavedFile({ filePath });
}
} catch (error) {
// 文件不存在,从缓存中移除
this.fileCache.delete(key);
}
}
// 更新清理时间
this.cacheStats.lastCleanup = now;
this.saveCacheInfo();
} catch (error) {
console.error('❌ 缓存清理失败:', error);
}
}
// 获取媒体管理器状态
getStatus() {
return {
isInitialized: this.isInitialized,
uploadCount: this.currentUploads.size,
cacheStats: { ...this.cacheStats },
config: this.mediaConfig
};
}
// 清除所有缓存
clearAllCache() {
this.fileCache.clear();
this.cacheStats = {
totalSize: 0,
fileCount: 0,
lastCleanup: Date.now()
};
this.saveCacheInfo();
}
// 重置管理器
reset() {
// 取消所有上传任务
for (const [uploadId] of this.currentUploads) {
this.cancelUpload(uploadId);
}
this.clearAllCache();
}
}
// 创建全局实例
const mediaManager = new MediaManager();
module.exports = mediaManager;