Penny Lens OSS文件管理模块详解
2026年6月24日 · 10 分钟阅读 · ixNieStudio
详细介绍Penny Lens的OSS文件管理模块,包括文件上传、预览、管理、云存储等功能。
OSS管理模块
☁️ 概述
OSS管理模块是 Penny Lens 应用的文件存储管理模块,负责用户头像、图片、文档等文件的云存储管理。支持文件上传、下载、删除、预览等功能,提供统一的文件管理接口。
🏗️ 模块架构
核心组件
src/├── services/api/upload.ts # 文件上传API服务├── utils/upload.ts # 文件上传工具├── utils/file.ts # 文件处理工具├── types/upload.ts # 上传类型定义└── components/FileUploader/ # 文件上传组件 ├── FileUploader.vue # 文件上传器 ├── ImageUploader.vue # 图片上传器 └── DocumentUploader.vue # 文档上传器📁 文件类型支持
支持的文件类型
// 文件类型枚举const FileType = { IMAGE: 'image', // 图片 DOCUMENT: 'document', // 文档 AUDIO: 'audio', // 音频 VIDEO: 'video', // 视频 OTHER: 'other' // 其他} as const
type FileType = typeof FileType[keyof typeof FileType]
// 文件类型配置interface FileTypeConfig { type: FileType; name: string; extensions: string[]; maxSize: number; // 最大文件大小(字节) mimeTypes: string[]; icon: string;}
// 文件类型配置映射const FILE_TYPE_CONFIGS: Record<FileType, FileTypeConfig> = { [FileType.IMAGE]: { type: FileType.IMAGE, name: '图片', extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'], maxSize: 5 * 1024 * 1024, // 5MB mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'], icon: 'i-mdi-image' }, [FileType.DOCUMENT]: { type: FileType.DOCUMENT, name: '文档', extensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'], maxSize: 10 * 1024 * 1024, // 10MB mimeTypes: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], icon: 'i-mdi-file-document' }, [FileType.AUDIO]: { type: FileType.AUDIO, name: '音频', extensions: ['mp3', 'wav', 'aac', 'ogg'], maxSize: 20 * 1024 * 1024, // 20MB mimeTypes: ['audio/mpeg', 'audio/wav', 'audio/aac', 'audio/ogg'], icon: 'i-mdi-music' }, [FileType.VIDEO]: { type: FileType.VIDEO, name: '视频', extensions: ['mp4', 'avi', 'mov', 'wmv'], maxSize: 50 * 1024 * 1024, // 50MB mimeTypes: ['video/mp4', 'video/avi', 'video/quicktime', 'video/x-ms-wmv'], icon: 'i-mdi-video' }, [FileType.OTHER]: { type: FileType.OTHER, name: '其他', extensions: [], maxSize: 5 * 1024 * 1024, // 5MB mimeTypes: [], icon: 'i-mdi-file' }}🔧 核心功能
1. 文件上传
功能描述: 支持单文件和批量文件上传
实现流程:
- 选择文件
- 验证文件类型和大小
- 压缩图片(如需要)
- 上传到OSS
- 返回文件URL
代码示例:
// 文件上传请求interface UploadRequest { file: File; type: FileType; category?: string; // 文件分类 metadata?: Record<string, any>; // 文件元数据}
// 文件上传响应interface UploadResponse { url: string; // 文件访问URL filename: string; // 文件名 size: number; // 文件大小 type: FileType; // 文件类型 mimeType: string; // MIME类型 uploadedAt: number; // 上传时间}
// 文件上传服务export const uploadService = { // 上传单个文件 async uploadFile(request: UploadRequest): Promise<UploadResponse> { // 验证文件 const validation = this.validateFile(request.file, request.type) if (!validation.isValid) { throw new Error(validation.errors.join(', ')) }
// 处理文件 const processedFile = await this.processFile(request.file, request.type)
// 上传到OSS const response = await uploadApi.uploadFile({ file: processedFile, type: request.type, category: request.category, metadata: request.metadata })
return response.data },
// 批量上传文件 async uploadFiles(requests: UploadRequest[]): Promise<UploadResponse[]> { const results: UploadResponse[] = [] const errors: string[] = []
for (const request of requests) { try { const result = await this.uploadFile(request) results.push(result) } catch (error) { errors.push(`${request.file.name}: ${error.message}`) } }
if (errors.length > 0) { console.warn('部分文件上传失败:', errors) }
return results },
// 验证文件 validateFile(file: File, type: FileType): ValidationResult { const config = FILE_TYPE_CONFIGS[type] const errors: string[] = []
// 检查文件大小 if (file.size > config.maxSize) { errors.push(`文件大小不能超过${this.formatFileSize(config.maxSize)}`) }
// 检查文件类型 if (config.mimeTypes.length > 0 && !config.mimeTypes.includes(file.type)) { errors.push(`不支持的文件类型: ${file.type}`) }
// 检查文件扩展名 const extension = this.getFileExtension(file.name) if (config.extensions.length > 0 && !config.extensions.includes(extension)) { errors.push(`不支持的文件扩展名: ${extension}`) }
return { isValid: errors.length === 0, errors } },
// 处理文件 async processFile(file: File, type: FileType): Promise<File> { if (type === FileType.IMAGE) { return await this.compressImage(file) } return file },
// 压缩图片 async compressImage(file: File, quality: number = 0.8): Promise<File> { return new Promise((resolve, reject) => { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') const img = new Image()
img.onload = () => { // 计算压缩后的尺寸 const maxWidth = 1920 const maxHeight = 1080 let { width, height } = img
if (width > maxWidth || height > maxHeight) { const ratio = Math.min(maxWidth / width, maxHeight / height) width *= ratio height *= ratio }
canvas.width = width canvas.height = height
// 绘制压缩后的图片 ctx?.drawImage(img, 0, 0, width, height)
canvas.toBlob( (blob) => { if (blob) { const compressedFile = new File([blob], file.name, { type: file.type, lastModified: Date.now() }) resolve(compressedFile) } else { reject(new Error('图片压缩失败')) } }, file.type, quality ) }
img.onerror = () => reject(new Error('图片加载失败')) img.src = URL.createObjectURL(file) }) },
// 获取文件扩展名 getFileExtension(filename: string): string { return filename.split('.').pop()?.toLowerCase() || '' },
// 格式化文件大小 formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes'
const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] }}2. 文件预览
功能描述: 支持多种文件类型的在线预览
代码示例:
// 文件预览服务export const previewService = { // 获取文件预览URL getPreviewUrl(fileUrl: string, type: FileType): string { switch (type) { case FileType.IMAGE: return fileUrl case FileType.DOCUMENT: return `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(fileUrl)}` case FileType.VIDEO: return fileUrl case FileType.AUDIO: return fileUrl default: return fileUrl } },
// 检查是否支持预览 isPreviewSupported(type: FileType): boolean { return [FileType.IMAGE, FileType.DOCUMENT, FileType.VIDEO, FileType.AUDIO].includes(type) },
// 获取文件类型 getFileType(filename: string): FileType { const extension = this.getFileExtension(filename)
for (const [type, config] of Object.entries(FILE_TYPE_CONFIGS)) { if (config.extensions.includes(extension)) { return type as FileType } }
return FileType.OTHER },
// 获取文件图标 getFileIcon(type: FileType): string { return FILE_TYPE_CONFIGS[type].icon }}3. 文件管理
功能描述: 提供文件的增删改查功能
代码示例:
// 文件管理服务export const fileManagerService = { // 获取文件列表 async getFileList(params: FileListRequest): Promise<FileListResponse> { const response = await uploadApi.getFileList(params) return response.data },
// 删除文件 async deleteFile(fileId: string): Promise<void> { const response = await uploadApi.deleteFile(fileId) if (response.code !== 200) { throw new Error(response.message) } },
// 更新文件信息 async updateFileInfo(fileId: string, data: Partial<FileInfo>): Promise<FileInfo> { const response = await uploadApi.updateFileInfo(fileId, data) return response.data },
// 获取文件信息 async getFileInfo(fileId: string): Promise<FileInfo> { const response = await uploadApi.getFileInfo(fileId) return response.data }}
// 文件信息接口interface FileInfo { id: string; filename: string; url: string; type: FileType; size: number; mimeType: string; category?: string; metadata?: Record<string, any>; uploadedAt: number; updatedAt: number;}🎨 文件上传组件
通用文件上传器
功能描述: 提供统一的文件上传界面
代码示例:
<template> <view class="file-uploader"> <!-- 上传区域 --> <view class="upload-area" :class="{ 'upload-area--dragover': isDragOver, 'upload-area--disabled': disabled }" @click="openFileDialog" @dragover="handleDragOver" @dragleave="handleDragLeave" @drop="handleDrop" > <view class="upload-content"> <Iconify :icon="uploadIcon" size="48" /> <text class="upload-text">{{ uploadText }}</text> <text class="upload-hint">{{ uploadHint }}</text> </view> </view>
<!-- 文件列表 --> <view class="file-list" v-if="fileList.length > 0"> <view v-for="(file, index) in fileList" :key="file.id || index" class="file-item" > <view class="file-info"> <Iconify :icon="getFileIcon(file)" size="20" /> <view class="file-details"> <text class="file-name">{{ file.name }}</text> <text class="file-size">{{ formatFileSize(file.size) }}</text> </view> </view>
<view class="file-actions"> <view class="progress" v-if="file.uploading"> <view class="progress-bar" :style="{ width: file.progress + '%' }"></view> </view> <view class="action-buttons" v-else> <button class="btn-icon" @click="previewFile(file)"> <Iconify icon="i-mdi-eye" size="16" /> </button> <button class="btn-icon" @click="removeFile(index)"> <Iconify icon="i-mdi-delete" size="16" /> </button> </view> </view> </view> </view>
<!-- 隐藏的文件输入 --> <input ref="fileInput" type="file" :multiple="multiple" :accept="accept" @change="handleFileSelect" style="display: none" /> </view></template>
<script setup lang="ts">interface Props { type: FileType; multiple?: boolean; maxFiles?: number; disabled?: boolean; category?: string;}
interface Emits { upload: [files: UploadResponse[]]; error: [error: string]; progress: [progress: number];}
const props = withDefaults(defineProps<Props>(), { multiple: false, maxFiles: 10, disabled: false})
const emit = defineEmits<Emits>()
// 响应式数据const fileList = ref<FileItem[]>([])const isDragOver = ref(false)const uploading = ref(false)
// 计算属性const uploadIcon = computed(() => { const config = FILE_TYPE_CONFIGS[props.type] return config.icon})
const uploadText = computed(() => { if (uploading.value) return '上传中...' if (props.disabled) return '上传已禁用' return `点击或拖拽上传${FILE_TYPE_CONFIGS[props.type].name}`})
const uploadHint = computed(() => { const config = FILE_TYPE_CONFIGS[props.type] return `支持 ${config.extensions.join(', ')} 格式,最大 ${formatFileSize(config.maxSize)}`})
const accept = computed(() => { const config = FILE_TYPE_CONFIGS[props.type] return config.mimeTypes.join(',')})
// 文件项接口interface FileItem { id?: string; name: string; size: number; file?: File; uploading?: boolean; progress?: number; url?: string; error?: string;}
// 打开文件选择对话框function openFileDialog() { if (props.disabled) return fileInput.value?.click()}
// 处理文件选择async function handleFileSelect(event: Event) { const target = event.target as HTMLInputElement const files = Array.from(target.files || [])
if (files.length === 0) return
await processFiles(files)
// 清空input target.value = ''}
// 处理拖拽function handleDragOver(event: DragEvent) { event.preventDefault() isDragOver.value = true}
function handleDragLeave(event: DragEvent) { event.preventDefault() isDragOver.value = false}
async function handleDrop(event: DragEvent) { event.preventDefault() isDragOver.value = false
if (props.disabled) return
const files = Array.from(event.dataTransfer?.files || []) if (files.length > 0) { await processFiles(files) }}
// 处理文件async function processFiles(files: File[]) { // 检查文件数量限制 if (fileList.value.length + files.length > props.maxFiles) { emit('error', `最多只能上传${props.maxFiles}个文件`) return }
// 添加文件到列表 const newFiles: FileItem[] = files.map(file => ({ name: file.name, size: file.size, file, uploading: true, progress: 0 }))
fileList.value.push(...newFiles)
// 开始上传 uploading.value = true const uploadResults: UploadResponse[] = []
try { for (let i = 0; i < newFiles.length; i++) { const fileItem = newFiles[i] const fileIndex = fileList.value.findIndex(item => item === fileItem)
try { const result = await uploadService.uploadFile({ file: fileItem.file!, type: props.type, category: props.category })
// 更新文件项 fileList.value[fileIndex] = { ...fileItem, id: result.filename, url: result.url, uploading: false, progress: 100 }
uploadResults.push(result) } catch (error) { // 标记上传失败 fileList.value[fileIndex] = { ...fileItem, uploading: false, error: error.message } } }
if (uploadResults.length > 0) { emit('upload', uploadResults) } } finally { uploading.value = false }}
// 移除文件function removeFile(index: number) { fileList.value.splice(index, 1)}
// 预览文件function previewFile(file: FileItem) { if (file.url) { uni.previewImage({ urls: [file.url], current: file.url }) }}</script>图片上传器
功能描述: 专门用于图片上传的组件
代码示例:
<template> <view class="image-uploader"> <!-- 图片网格 --> <view class="image-grid"> <view v-for="(image, index) in imageList" :key="image.id || index" class="image-item" > <image :src="image.url || image.preview" class="image" mode="aspectFill" @click="previewImage(index)" />
<view class="image-overlay" v-if="image.uploading"> <view class="progress"> <view class="progress-text">{{ image.progress }}%</view> </view> </view>
<view class="image-actions"> <button class="btn-icon" @click="removeImage(index)"> <Iconify icon="i-mdi-delete" size="16" /> </button> </view> </view>
<!-- 添加按钮 --> <view class="add-button" @click="openImagePicker" v-if="imageList.length < maxImages" > <Iconify icon="i-mdi-plus" size="24" /> <text>添加图片</text> </view> </view> </view></template>
<script setup lang="ts">interface Props { maxImages?: number; category?: string;}
interface Emits { upload: [images: UploadResponse[]]; error: [error: string];}
const props = withDefaults(defineProps<Props>(), { maxImages: 9})
const emit = defineEmits<Emits>()
const imageList = ref<ImageItem[]>([])
interface ImageItem { id?: string; url?: string; preview?: string; uploading?: boolean; progress?: number; error?: string;}
// 打开图片选择器function openImagePicker() { uni.chooseImage({ count: props.maxImages - imageList.value.length, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (res) => { processImages(res.tempFilePaths) } })}
// 处理图片async function processImages(tempFilePaths: string[]) { for (const tempFilePath of tempFilePaths) { const imageItem: ImageItem = { preview: tempFilePath, uploading: true, progress: 0 }
imageList.value.push(imageItem)
try { // 上传图片 const result = await uploadService.uploadFile({ file: tempFilePath as any, // 小程序环境下的文件对象 type: FileType.IMAGE, category: props.category })
// 更新图片项 const index = imageList.value.findIndex(item => item === imageItem) imageList.value[index] = { ...imageItem, id: result.filename, url: result.url, uploading: false, progress: 100 } } catch (error) { // 标记上传失败 const index = imageList.value.findIndex(item => item === imageItem) imageList.value[index] = { ...imageItem, uploading: false, error: error.message } } }}
// 预览图片function previewImage(index: number) { const urls = imageList.value .filter(img => img.url) .map(img => img.url!)
uni.previewImage({ urls, current: urls[index] })}
// 移除图片function removeImage(index: number) { imageList.value.splice(index, 1)}</script>🔄 状态管理
Pinia Store
功能描述: 使用Pinia管理文件上传状态
代码示例:
// 文件上传状态管理export const useUploadStore = defineStore('upload', () => { // 状态 const uploadQueue = ref<UploadTask[]>([]) const uploadHistory = ref<UploadResponse[]>([]) const isUploading = ref(false)
// 计算属性 const totalProgress = computed(() => { if (uploadQueue.value.length === 0) return 0
const totalProgress = uploadQueue.value.reduce((sum, task) => sum + task.progress, 0) return totalProgress / uploadQueue.value.length })
const completedTasks = computed(() => uploadQueue.value.filter(task => task.status === 'completed') )
const failedTasks = computed(() => uploadQueue.value.filter(task => task.status === 'failed') )
// 操作 const addUploadTask = (task: UploadTask): void => { uploadQueue.value.push(task) }
const removeUploadTask = (taskId: string): void => { uploadQueue.value = uploadQueue.value.filter(task => task.id !== taskId) }
const updateTaskProgress = (taskId: string, progress: number): void => { const task = uploadQueue.value.find(task => task.id === taskId) if (task) { task.progress = progress } }
const updateTaskStatus = (taskId: string, status: UploadTaskStatus): void => { const task = uploadQueue.value.find(task => task.id === taskId) if (task) { task.status = status } }
const clearCompletedTasks = (): void => { uploadQueue.value = uploadQueue.value.filter(task => task.status !== 'completed') }
const addToHistory = (response: UploadResponse): void => { uploadHistory.value.unshift(response)
// 限制历史记录数量 if (uploadHistory.value.length > 100) { uploadHistory.value = uploadHistory.value.slice(0, 100) } }
return { uploadQueue, uploadHistory, isUploading, totalProgress, completedTasks, failedTasks, addUploadTask, removeUploadTask, updateTaskProgress, updateTaskStatus, clearCompletedTasks, addToHistory }})
// 上传任务接口interface UploadTask { id: string; file: File; type: FileType; category?: string; progress: number; status: 'pending' | 'uploading' | 'completed' | 'failed'; error?: string; result?: UploadResponse;}📚 最佳实践
1. 文件安全
- 验证文件类型和大小
- 扫描恶意文件
- 设置访问权限
- 定期清理过期文件
2. 性能优化
- 图片压缩和缩略图
- 分片上传大文件
- CDN加速访问
- 缓存策略优化
3. 用户体验
- 拖拽上传支持
- 实时上传进度
- 预览功能
- 错误提示友好
4. 可维护性
- 统一的文件管理接口
- 完整的类型定义
- 模块化组件设计
- 充分的测试覆盖
Penny Lens OSS管理模块 - 让文件存储更安全、更高效 ☁️