# Research: 素材管理库 (Media Library) - Technical Decisions **Feature**: 009-media-library **Date**: 2025-12-11 **Status**: Completed ## Overview 本文档记录素材管理库功能的技术研究和决策,包括存储策略、状态管理、UI架构和最佳实践。 ## 1. IndexedDB Storage Strategy ### Decision 使用 **localforage** 库作为 IndexedDB 的包装器进行素材数据持久化存储。 ### Rationale 1. **简化API**: localforage 提供类似 localStorage 的简单 API,同时利用 IndexedDB 的强大功能 2. **跨浏览器兼容**: 自动降级到 WebSQL 或 localStorage(如果 IndexedDB 不可用) 3. **Promise-based**: 原生支持 async/await,易于与 React 集成 4. **项目已使用**: 现有代码库已经使用 localforage(media-cache-service.ts, storage-service.ts) 5. **Blob 支持**: 原生支持存储 Blob 对象,适合媒体文件 ### Alternatives Considered - **直接使用 IndexedDB API**: 过于底层,需要更多样板代码 - **Dexie.js**: 功能强大但增加bundle大小,对于我们的用例过度设计 - **idb**: 轻量但缺少 localforage 的自动降级特性 ### Implementation Details ```typescript // 存储配置 const assetStore = localforage.createInstance({ name: 'aitu-assets', storeName: 'assets', description: 'Media library assets storage' }); // 数据结构 interface StoredAsset { id: string; type: AssetType; source: AssetSource; name: string; mimeType: string; createdAt: number; blobData: Blob; // 存储实际文件数据 } ``` ### Best Practices 1. **分离元数据和 Blob**: 考虑使用两个store(metadata + blobs)以提高查询性能 2. **错误处理**: 包装所有 storage 操作在 try-catch 中,处理 QuotaExceededError 3. **清理策略**: 提供手动删除和下载功能,让用户管理空间 4. **批量操作**: 对于初始加载,批量读取元数据而不是逐个加载 ## 2. Storage Quota Management ### Decision 实现主动的存储配额监控,在达到 80% 时警告用户。 ### Rationale 1. **用户体验**: 主动警告优于突然的存储失败 2. **规范支持**: Storage API 的 `navigator.storage.estimate()` 广泛支持 3. **可操作性**: 用户可以在空间不足前采取行动(删除或下载旧素材) ### Implementation Details ```typescript // 存储配额检查 async function checkStorageQuota(): Promise<{ usage: number; quota: number; percentUsed: number; }> { if ('storage' in navigator && 'estimate' in navigator.storage) { const estimate = await navigator.storage.estimate(); const usage = estimate.usage || 0; const quota = estimate.quota || 0; return { usage, quota, percentUsed: (usage / quota) * 100 }; } return { usage: 0, quota: 0, percentUsed: 0 }; } // 在添加素材前检查 async function canAddAsset(fileSize: number): Promise { const { usage, quota } = await checkStorageQuota(); return (usage + fileSize) < quota * 0.95; // 保留5%缓冲 } ``` ### Best Practices 1. **定期检查**: 在组件挂载和每次添加素材后检查配额 2. **视觉反馈**: 使用进度条直观显示使用量 3. **警告阈值**: 80% 显示警告,95% 阻止新上传 4. **降级处理**: 如果 API 不可用,显示估计值或隐藏进度条 ## 3. React State Management with Context ### Decision 使用 **React Context API** 进行素材库状态管理,而不是引入Redux或其他状态管理库。 ### Rationale 1. **简单性**: 素材库状态相对独立,不需要全局状态管理的复杂性 2. **项目一致性**: 现有代码使用 DrawnixContext 模式 3. **性能**: 使用 useMemo 和 useCallback 优化,避免不必要的重渲染 4. **局部性**: 状态只在素材库弹窗中使用,不需要跨应用共享 ### Alternatives Considered - **Redux**: 过度设计,增加复杂性和bundle大小 - **Zustand**: 轻量但增加新依赖,Context足够 - **组件状态**: 无法在多个组件间共享,难以维护 ### Implementation Pattern ```typescript interface AssetContextValue { assets: Asset[]; loading: boolean; error: string | null; addAsset: (file: File | Blob, type: AssetType, source: AssetSource) => Promise; removeAsset: (id: string) => Promise; renameAsset: (id: string, newName: string) => Promise; loadAssets: () => Promise; } export const AssetContext = createContext(null); export function AssetProvider({ children }: { children: ReactNode }) { const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 实现各种操作... const value = useMemo(() => ({ assets, loading, error, addAsset, removeAsset, renameAsset, loadAssets }), [assets, loading, error]); return {children}; } export function useAssets() { const context = useContext(AssetContext); if (!context) throw new Error('useAssets must be used within AssetProvider'); return context; } ``` ### Best Practices 1. **拆分Context**: 如果状态变大,考虑拆分为多个Context(AssetsContext, FiltersContext) 2. **Memoization**: 对过滤/搜索结果使用 useMemo 3. **错误边界**: 包装Provider在ErrorBoundary中 4. **加载状态**: 提供细粒度的加载状态(loading, error, data) ## 4. Component Architecture ### Decision 采用**容器/展示组件模式**,将素材库分解为职责明确的小组件。 ### Rationale 1. **500行限制**: 符合项目宪章的文件大小约束 2. **可维护性**: 每个组件职责单一,易于理解和测试 3. **可复用性**: 小组件可以在其他上下文中复用 4. **性能**: 细粒度的组件边界便于React.memo优化 ### Component Hierarchy ``` MediaLibraryModal (容器) ├── MediaLibrarySidebar (展示) │ ├── FilterSection (展示) │ └── StorageBar (展示) ├── MediaLibraryGrid (容器) │ ├── MediaLibraryEmpty (展示) │ └── AssetGridItem (展示) × N └── MediaLibraryInspector (容器) ├── AssetPreview (展示) ├── AssetMetadata (展示) └── AssetActions (展示) ``` ### Best Practices 1. **容器组件**: 处理状态和逻辑,使用hooks 2. **展示组件**: 只接收props,使用React.memo包装 3. **Props drilling**: 最多3层,超过则考虑Context或组合模式 4. **事件处理**: 在容器组件中定义,用useCallback包装后传递 ## 5. Integration with AI Generation Dialogs ### Decision 在AI生成对话框中添加**下拉菜单或按钮组**,让用户选择素材来源(素材库 vs 本地文件)。 ### Rationale 1. **用户控制**: 明确的选择点,用户知道他们在做什么 2. **渐进式增强**: 保留现有本地文件选择功能 3. **清晰的UX**: 两个选项并排,易于发现 4. **最小侵入**: 不需要重构现有对话框结构 ### Implementation Approach ```typescript // 在 ai-image-generation.tsx 中 function ImageUploadSection() { const [uploadSource, setUploadSource] = useState<'local' | 'library'>('local'); const [showMediaLibrary, setShowMediaLibrary] = useState(false); return ( <>
{showMediaLibrary && ( setShowMediaLibrary(false)} onSelect={(asset) => { setReferenceImage(asset); setShowMediaLibrary(false); }} filterType="IMAGE" // 只显示图片 /> )} ); } ``` ### Best Practices 1. **类型筛选**: 传递 filterType prop 限制显示的素材类型 2. **回调清晰**: onSelect 回调接收Asset对象,包含所有必要信息 3. **状态同步**: 选择后自动关闭弹窗,更新引用图片预览 4. **错误处理**: 处理素材已被删除的情况 ## 6. Auto-save AI Generated Content ### Decision 在任务队列服务中监听AI生成任务完成事件,自动将成功的结果添加到素材库。 ### Rationale 1. **自动化**: 用户不需要手动保存,减少操作步骤 2. **集成点清晰**: 任务队列是唯一的生成任务管理点 3. **错误处理**: 只保存成功的生成结果,失败任务不添加 4. **可追溯**: 保留提示词信息作为素材名称 ### Implementation Approach ```typescript // 在 asset-integration-service.ts 中 import { taskQueueService } from './task-queue-service'; function initializeAutoSave() { // 订阅任务完成事件 taskQueueService.tasks$.subscribe((tasks) => { tasks.forEach(async (task) => { if (task.status === 'completed' && task.resultUrl && !task.savedToLibrary) { // 根据任务类型确定素材类型 const assetType = task.type === 'image-generation' ? AssetType.IMAGE : AssetType.VIDEO; // 获取结果 Blob const response = await fetch(task.resultUrl); const blob = await response.blob(); // 生成名称(使用提示词前20字符) const name = task.prompt ? `${task.prompt.substring(0, 20)}...` : `AI${assetType}-${new Date().toISOString()}`; // 添加到素材库 await assetStorageService.addAsset({ type: assetType, source: AssetSource.AI_GENERATED, name, blob, mimeType: blob.type }); // 标记已保存 taskQueueService.markAsSaved(task.id); } }); }); } ``` ### Best Practices 1. **幂等性**: 使用 savedToLibrary 标志避免重复添加 2. **错误容忍**: 保存失败不应影响任务队列正常运行 3. **命名策略**: 提供有意义的默认名称,用户可以后续修改 4. **元数据**: 考虑保存提示词、模型名称等额外元数据 ## 7. File Validation and Security ### Decision 实现多层文件验证:MIME类型检查、文件扩展名检查和Magic Number验证。 ### Rationale 1. **安全性**: 防止恶意文件伪装成图片/视频 2. **用户体验**: 早期验证,提供清晰的错误消息 3. **存储效率**: 避免存储无效文件 4. **类型安全**: 确保所有素材都是有效的媒体文件 ### Implementation Details ```typescript const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg']; async function validateFile(file: File): Promise<{ valid: boolean; error?: string; }> { // 1. MIME类型检查 const allowedTypes = [...ALLOWED_IMAGE_TYPES, ...ALLOWED_VIDEO_TYPES]; if (!allowedTypes.includes(file.type)) { return { valid: false, error: `不支持的文件类型: ${file.type}。只支持图片(JPG, PNG, GIF, WebP)和视频(MP4, WebM, OGG)。` }; } // 2. 文件大小检查(警告但不阻止) const maxSize = 100 * 1024 * 1024; // 100MB if (file.size > maxSize) { console.warn(`文件较大 (${(file.size / 1024 / 1024).toFixed(2)}MB),可能影响性能`); } // 3. Magic Number验证(可选但推荐) const isValidMagicNumber = await validateMagicNumber(file); if (!isValidMagicNumber) { return { valid: false, error: '文件内容与声明的类型不匹配。' }; } return { valid: true }; } async function validateMagicNumber(file: File): Promise { // 读取文件前几个字节检查magic number const buffer = await file.slice(0, 12).arrayBuffer(); const bytes = new Uint8Array(buffer); // 检查常见格式的magic number // JPEG: FF D8 FF if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) return true; // PNG: 89 50 4E 47 if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) return true; // MP4: 根据 ftyp box // ... 更多格式检查 return false; } ``` ### Best Practices 1. **用户友好错误**: 提供具体的错误消息和支持的格式列表 2. **渐进增强**: Magic number验证可选,基础MIME检查必须 3. **性能**: 只读取必要的字节数进行验证 4. **日志记录**: 记录验证失败,但不要暴露给用户敏感信息 ## 8. Performance Optimization Strategies ### Decision 实施多层性能优化:组件memo化、虚拟滚动(如需要)、图片懒加载和渐进式加载。 ### Rationale 1. **初始加载**: 100个素材需要快速渲染 2. **交互响应**: 筛选和搜索必须流畅 3. **内存效率**: 避免同时加载所有Blob数据 4. **用户体验**: 平滑的滚动和即时的反馈 ### Optimization Techniques #### 1. Component Memoization ```typescript export const AssetGridItem = React.memo(({ asset, isSelected, onSelect, onDoubleClick }) => { // 组件实现 }, (prevProps, nextProps) => { // 自定义比较函数 return prevProps.asset.id === nextProps.asset.id && prevProps.isSelected === nextProps.isSelected; }); ``` #### 2. Lazy Loading Images ```typescript function AssetThumbnail({ url }: { url: string }) { const [src, setSrc] = useState(''); const imgRef = useRef(null); useEffect(() => { const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { setSrc(url); observer.disconnect(); } }); if (imgRef.current) { observer.observe(imgRef.current); } return () => observer.disconnect(); }, [url]); return ; } ``` #### 3. Filtered List Memoization ```typescript const filteredAssets = useMemo(() => { return assets .filter(asset => { const matchesType = activeType === 'ALL' || asset.type === activeType; const matchesSource = activeSource === 'ALL' || asset.source === activeSource; const matchesSearch = asset.name.toLowerCase().includes(searchQuery.toLowerCase()); return matchesType && matchesSource && matchesSearch; }) .sort((a, b) => { if (sortBy === 'DATE_DESC') return b.createdAt - a.createdAt; if (sortBy === 'DATE_ASC') return a.createdAt - b.createdAt; if (sortBy === 'NAME_ASC') return a.name.localeCompare(b.name); return 0; }); }, [assets, activeType, activeSource, searchQuery, sortBy]); ``` #### 4. Virtual Scrolling (如需要) ```typescript // 如果素材数量>200,考虑使用 react-window import { FixedSizeGrid } from 'react-window'; function VirtualizedGrid({ assets }: { assets: Asset[] }) { const COLUMN_COUNT = 5; const ROW_HEIGHT = 200; const COLUMN_WIDTH = 180; return ( {({ columnIndex, rowIndex, style }) => { const index = rowIndex * COLUMN_COUNT + columnIndex; const asset = assets[index]; return asset ? (
) : null; }}
); } ``` ### Best Practices 1. **测量先行**: 使用 React DevTools Profiler 识别瓶颈 2. **渐进优化**: 先实现基本功能,再根据实际性能问题优化 3. **避免过早优化**: 虚拟滚动只在>200素材时考虑 4. **监控指标**: 跟踪初始加载时间、筛选响应时间 ## 9. Error Handling and User Feedback ### Decision 实施分层错误处理:存储层错误、网络错误、验证错误和UI错误状态。 ### Rationale 1. **用户体验**: 清晰的错误消息帮助用户理解问题 2. **可恢复性**: 区分可重试和永久性错误 3. **调试**: 详细的错误日志帮助定位问题 4. **安全性**: 不向用户暴露敏感信息 ### Error Categories and Handling #### 1. Storage Errors ```typescript async function handleStorageError(error: Error): Promise { if (error.name === 'QuotaExceededError') { MessagePlugin.error({ content: '存储空间已满。请删除一些旧素材或下载后删除以释放空间。', duration: 5000 }); } else if (error.name === 'NotFoundError') { MessagePlugin.warning({ content: '素材未找到,可能已被删除。', duration: 3000 }); } else { console.error('Storage error:', error); MessagePlugin.error({ content: '存储操作失败,请刷新页面重试。', duration: 3000 }); } } ``` #### 2. Validation Errors ```typescript function showValidationError(message: string) { MessagePlugin.warning({ content: message, duration: 3000, theme: 'warning' }); } ``` #### 3. Global Error Boundary ```typescript class MediaLibraryErrorBoundary extends React.Component< { children: ReactNode }, { hasError: boolean; error?: Error } > { state = { hasError: false, error: undefined }; static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('MediaLibrary error:', error, errorInfo); } render() { if (this.state.hasError) { return (

素材库加载失败

请刷新页面重试

); } return this.props.children; } } ``` ### Best Practices 1. **用户语言**: 使用非技术术语的错误消息 2. **可操作**: 提供具体的解决步骤 3. **日志记录**: 详细记录错误到控制台供开发调试 4. **优雅降级**: 错误时显示有用的回退UI ## Summary of Decisions | 领域 | 决策 | 理由 | |------|------|------| | 存储 | localforage + IndexedDB | 简单API,已在项目中使用,Blob支持好 | | 配额管理 | 主动监控(80%警告) | 改善用户体验,避免突然失败 | | 状态管理 | React Context | 简单、局部、符合项目模式 | | 组件架构 | 容器/展示分离 | 符合500行限制,可维护性高 | | AI集成 | 在对话框中添加选择器 | 最小侵入,保留现有功能 | | 自动保存 | 订阅任务队列事件 | 自动化,集成点清晰 | | 文件验证 | MIME + Magic Number | 安全性和用户体验平衡 | | 性能优化 | Memo + 懒加载 + 虚拟滚动 | 多层优化,渐进式应用 | | 错误处理 | 分层处理 + Error Boundary | 用户友好,便于调试 | ## Next Steps 1. ✅ Research完成 2. → 进入 Phase 1: 设计数据模型和API契约 3. → 生成 quickstart.md 开发指南 4. → 更新 agent context 5. → 进入 Phase 2: 生成任务分解 (tasks.md) --- **研究完成日期**: 2025-12-11 **准备进入**: Phase 1 - Design & Contracts