findme-miniprogram-frontend/subpackages/media/utils/media-manager.js
2025-12-27 17:16:03 +08:00

837 lines
20 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.

// 媒体文件管理器 - 微信小程序专用
// 处理图片、视频、音频、文件的上传、下载、预览、缓存等
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;