# Quick Start: 素材管理库 (Media Library) 开发指南 **Feature**: 009-media-library **Date**: 2025-12-11 ## 概览 本指南帮助开发者快速开始实现素材管理库功能。按照优先级顺序进行开发,确保每个阶段都有可工作的增量交付。 ## 前置条件 - Node.js 18+ 和 npm - 熟悉 TypeScript, React 18, 和 React Hooks - 了解 IndexedDB 和 localforage - 熟悉 TDesign React 组件库 - 了解项目宪章(`constitution.md`)和编码标准 ## 开发环境设置 ### 1. 克隆并安装依赖 ```bash cd /Users/gongchengtu/code/github/aitu git checkout 009-media-library # 安装依赖(如果尚未安装) npm install ``` ### 2. 验证开发服务器 ```bash npm start # 启动开发服务器在 localhost:7200 ``` ### 3. 运行测试 ```bash npm test # 运行所有测试 ``` ## 实现顺序(按优先级) ### Phase 1: 核心数据层 (P1) **目标**: 实现存储服务和数据模型 #### 1.1 创建类型定义 **文件**: `packages/drawnix/src/types/asset.types.ts` ```typescript // 参考 data-model.md 中的完整定义 export enum AssetType { IMAGE = 'IMAGE', VIDEO = 'VIDEO' } export enum AssetSource { LOCAL = 'LOCAL', AI_GENERATED = 'AI_GENERATED' } export interface Asset { /* ... */ } export interface StoredAsset { /* ... */ } // ... 其他类型 ``` **验收标准**: - TypeScript strict mode 编译通过 - 所有枚举和接口导出 - 文件 < 200 行 #### 1.2 实现存储服务 **文件**: `packages/drawnix/src/services/asset-storage-service.ts` ```typescript import localforage from 'localforage'; import type { Asset, StoredAsset, AddAssetData } from '../types/asset.types'; class AssetStorageService { private store: LocalForage; async initialize() { this.store = localforage.createInstance({ name: 'aitu-assets', storeName: 'assets' }); } async addAsset(data: AddAssetData): Promise { // 实现逻辑... } // 其他方法... } export const assetStorageService = new AssetStorageService(); ``` **验收标准**: - 所有接口方法实现(参考 `contracts/asset-storage-service.md`) - 错误处理完善 - 单元测试覆盖率 > 80% - 文件 < 250 行 #### 1.3 编写存储服务测试 **文件**: `tests/media-library/unit/asset-storage-service.spec.ts` ```typescript describe('AssetStorageService', () => { beforeEach(async () => { await assetStorageService.initialize(); await assetStorageService.clearAll(); }); // 参考 contracts/asset-storage-service.md 中的测试用例 }); ``` ### Phase 2: 状态管理 (P1) **目标**: 实现React Context和自定义Hook #### 2.1 创建AssetContext **文件**: `packages/drawnix/src/contexts/AssetContext.tsx` ```typescript import React, { createContext, useContext, useState, useCallback, useMemo } from 'react'; import { assetStorageService } from '../services/asset-storage-service'; import type { Asset, AssetContextValue } from '../types/asset.types'; const AssetContext = createContext(null); export function AssetProvider({ children }: { children: React.ReactNode }) { const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const loadAssets = useCallback(async () => { setLoading(true); setError(null); try { const loaded = await assetStorageService.getAllAssets(); setAssets(loaded); } catch (err) { setError(err.message); } finally { setLoading(false); } }, []); const addAsset = useCallback(async (/* ... */) => { // 实现... }, []); // 其他操作... const value = useMemo(() => ({ assets, loading, error, loadAssets, addAsset, // ... 其他方法 }), [assets, loading, error, loadAssets, addAsset]); return {children}; } export function useAssets() { const context = useContext(AssetContext); if (!context) throw new Error('useAssets must be used within AssetProvider'); return context; } ``` **验收标准**: - Context正确提供所有state和actions - 使用useMemo和useCallback优化性能 - 文件 < 150 行 #### 2.2 在应用中集成Provider **文件**: `packages/drawnix/src/drawnix.tsx` (修改) ```typescript import { AssetProvider } from './contexts/AssetContext'; export function Drawnix() { return ( {/* 现有内容 */} ); } ``` ### Phase 3: UI 组件 - 基础结构 (P1) **目标**: 实现核心UI组件 #### 3.1 MediaLibraryModal(容器) **文件**: `packages/drawnix/src/components/media-library/MediaLibraryModal.tsx` ```typescript import React, { useState, useEffect } from 'react'; import { Dialog } from 'tdesign-react'; import { useAssets } from '../../contexts/AssetContext'; import MediaLibrarySidebar from './MediaLibrarySidebar'; import MediaLibraryGrid from './MediaLibraryGrid'; import MediaLibraryInspector from './MediaLibraryInspector'; import './MediaLibraryModal.scss'; interface MediaLibraryModalProps { isOpen: boolean; onClose: () => void; onSelect?: (asset: Asset) => void; filterType?: AssetType; } export function MediaLibraryModal({ isOpen, onClose, onSelect, filterType }: MediaLibraryModalProps) { const { assets, loadAssets } = useAssets(); const [selectedAssetId, setSelectedAssetId] = useState(null); useEffect(() => { if (isOpen) { loadAssets(); } }, [isOpen, loadAssets]); if (!isOpen) return null; return (
); } ``` **文件**: `packages/drawnix/src/components/media-library/MediaLibraryModal.scss` ```scss .media-library-modal { .media-library-layout { display: flex; height: 90vh; background: var(--color-bg); // 使用BEM命名 &__sidebar { width: 260px; border-right: 1px solid var(--color-border); } &__main { flex: 1; display: flex; flex-direction: column; } &__inspector { width: 320px; border-left: 1px solid var(--color-border); } // 响应式 @media (max-width: 768px) { flex-direction: column; &__sidebar, &__inspector { width: 100%; border: none; } } } } ``` **验收标准**: - 使用TDesign Dialog组件 - 响应式布局(桌面/移动) - 文件 < 150 行(tsx), < 100 行(scss) #### 3.2 MediaLibraryGrid(网格视图) **文件**: `packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx` ```typescript import React, { useMemo } from 'react'; import { useAssets } from '../../contexts/AssetContext'; import { filterAssets } from '../../utils/asset-utils'; import AssetGridItem from './AssetGridItem'; import MediaLibraryEmpty from './MediaLibraryEmpty'; interface MediaLibraryGridProps { filterType?: AssetType; selectedAssetId: string | null; onSelectAsset: (id: string) => void; } export function MediaLibraryGrid({ filterType, selectedAssetId, onSelectAsset }: MediaLibraryGridProps) { const { assets, filters } = useAssets(); // 应用筛选和排序 const filteredResult = useMemo(() => { const merged = filterType ? { ...filters, activeType: filterType } : filters; return filterAssets(assets, merged); }, [assets, filters, filterType]); if (filteredResult.isEmpty) { return ; } return (
{filteredResult.assets.map(asset => ( ))}
); } ``` **验收标准**: - 使用useMemo优化筛选 - 正确显示空状态 - 网格布局响应式 - 文件 < 150 行 #### 3.3 AssetGridItem(网格项) **文件**: `packages/drawnix/src/components/media-library/AssetGridItem.tsx` ```typescript import React, { useCallback } from 'react'; import { ImageIcon, VideoIcon } from 'lucide-react'; import type { Asset } from '../../types/asset.types'; interface AssetGridItemProps { asset: Asset; isSelected: boolean; onSelect: (id: string) => void; } export const AssetGridItem = React.memo(({ asset, isSelected, onSelect }) => { const handleClick = useCallback(() => { onSelect(asset.id); }, [asset.id, onSelect]); return (
{/* 缩略图 */}
{asset.type === 'IMAGE' ? ( {asset.name} ) : (
{/* 类型标识 */}
{asset.type === 'IMAGE' ? : }
{/* AI标识 */} {asset.source === 'AI_GENERATED' && (
AI
)} {/* 名称 */}
{asset.name}
); }, (prev, next) => ( prev.asset.id === next.asset.id && prev.isSelected === next.isSelected )); ``` **验收标准**: - 使用React.memo优化 - 自定义比较函数 - BEM命名的样式 - 文件 < 120 行 ### Phase 4: UI 组件 - 侧边栏和详情面板 (P1) #### 4.1 MediaLibrarySidebar(筛选侧边栏) **文件**: `packages/drawnix/src/components/media-library/MediaLibrarySidebar.tsx` ```typescript import React from 'react'; import { Button } from 'tdesign-react'; import { Grid, ImageIcon, VideoIcon, Cpu, HardDrive } from 'lucide-react'; import { useAssets } from '../../contexts/AssetContext'; import MediaLibraryStorageBar from './MediaLibraryStorageBar'; export function MediaLibrarySidebar() { const { filters, setFilters, assets, storageStatus } = useAssets(); return (
{/* 类型筛选 */}

类型

{/* 来源筛选 */}

来源

{/* 存储状态 */}
); } ``` **验收标准**: - 使用TDesign Button组件(light主题) - 图标来自lucide-react - 文件 < 150 行 #### 4.2 MediaLibraryInspector(详情面板) **文件**: `packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx` ```typescript import React, { useState } from 'react'; import { Button, Input, MessagePlugin } from 'tdesign-react'; import { Download, Trash2, Edit2, CheckCircle } from 'lucide-react'; import { useAssets } from '../../contexts/AssetContext'; interface MediaLibraryInspectorProps { assetId: string | null; onSelect?: (asset: Asset) => void; } export function MediaLibraryInspector({ assetId, onSelect }: MediaLibraryInspectorProps) { const { assets, renameAsset, removeAsset } = useAssets(); const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(''); const asset = assets.find(a => a.id === assetId); if (!asset) { return
选择素材查看详情
; } const handleRename = async () => { try { await renameAsset(asset.id, newName); setIsRenaming(false); MessagePlugin.success('重命名成功'); } catch (error) { MessagePlugin.error('重命名失败'); } }; const handleDelete = async () => { // 显示确认对话框 const confirmed = await showDeleteConfirmation(); if (confirmed) { await removeAsset(asset.id); MessagePlugin.success('删除成功'); } }; return (
{/* 预览 */}
{asset.type === 'IMAGE' ? ( {asset.name} ) : (
{/* 名称编辑 */}
{isRenaming ? ( ) : (
{asset.name}
)}
{/* 元数据 */}
类型: {asset.type}
来源: {asset.source}
创建时间: {new Date(asset.createdAt).toLocaleString()}
{/* 操作按钮 */}
{onSelect && ( )}
); } ``` **验收标准**: - 使用TDesign组件 - 删除前显示确认对话框 - 文件 < 200 行 ### Phase 5: 集成AI生成对话框 (P1) **目标**: 在AI生成对话框中添加素材库选择功能 #### 5.1 修改AI生图对话框 **文件**: `packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx` (修改) ```typescript import { MediaLibraryModal } from '../media-library/MediaLibraryModal'; function AIImageGeneration() { const [showMediaLibrary, setShowMediaLibrary] = useState(false); const [referenceImage, setReferenceImage] = useState(null); return (
{/* 现有代码 */} {/* 新增:参考图选择器 */}
{/* 素材库弹窗 */} setShowMediaLibrary(false)} onSelect={(asset) => { setReferenceImage(asset); setShowMediaLibrary(false); }} filterType="IMAGE" // 只显示图片 />
); } ``` **验收标准**: - 不破坏现有功能 - 素材库只显示图片 - 选择后正确应用参考图 #### 5.2 类似修改AI生视频对话框 **文件**: `packages/drawnix/src/components/ttd-dialog/ai-video-generation.tsx` (修改) ### Phase 6: 自动保存AI生成内容 (P1) **目标**: AI生成完成后自动添加到素材库 #### 6.1 创建集成服务 **文件**: `packages/drawnix/src/services/asset-integration-service.ts` ```typescript import { taskQueueService } from './task-queue-service'; import { assetStorageService } from './asset-storage-service'; export function initializeAssetIntegration() { // 订阅任务队列 taskQueueService.tasks$.subscribe(async (tasks) => { for (const task of tasks) { if (task.status === 'completed' && task.resultUrl && !task.savedToLibrary) { try { // 下载结果 const response = await fetch(task.resultUrl); const blob = await response.blob(); // 确定类型 const assetType = task.type === 'image-generation' ? AssetType.IMAGE : AssetType.VIDEO; // 生成名称 const name = generateAssetName(task.prompt, assetType); // 保存到素材库 await assetStorageService.addAsset({ type: assetType, source: AssetSource.AI_GENERATED, name, blob, mimeType: blob.type, prompt: task.prompt, modelName: task.modelName }); // 标记已保存 taskQueueService.markAsSaved(task.id); } catch (error) { console.error('Failed to save AI generated asset:', error); // 不阻塞任务队列 } } } }); } function generateAssetName(prompt: string | undefined, type: AssetType): string { if (prompt && prompt.length > 0) { const truncated = prompt.substring(0, 20); return truncated.length < prompt.length ? `${truncated}...` : truncated; } const timestamp = new Date().toISOString(); return `AI${type}-${timestamp}`; } ``` #### 6.2 在应用启动时初始化 **文件**: `packages/drawnix/src/drawnix.tsx` (修改) ```typescript import { initializeAssetIntegration } from './services/asset-integration-service'; useEffect(() => { // 初始化素材集成 initializeAssetIntegration(); }, []); ``` **验收标准**: - 生成成功后自动添加 - 失败不影响任务队列 - 避免重复添加 ## 测试策略 ### Unit Tests ```bash # 运行单元测试 npm test -- asset-storage-service.spec.ts npm test -- asset-utils.spec.ts ``` ### Component Tests ```bash # 运行组件测试 npm test -- MediaLibraryModal.spec.tsx npm test -- AssetGridItem.spec.tsx ``` ### E2E Tests ```bash # 运行E2E测试 npx playwright test media-library ``` ## 调试技巧 ### 1. 查看IndexedDB数据 ```javascript // 在浏览器控制台 const store = localforage.createInstance({ name: 'aitu-assets' }); const keys = await store.keys(); console.log('Stored assets:', keys); const asset = await store.getItem(keys[0]); console.log('First asset:', asset); ``` ### 2. React DevTools 使用React DevTools查看: - AssetContext的state - 组件重渲染情况 - Props传递链 ### 3. Network Tab 监控AI生成任务的网络请求,确保结果正确下载。 ## 常见问题 ### Q: 如何清除所有素材? ```typescript await assetStorageService.clearAll(); ``` ### Q: 如何处理存储空间不足? ```typescript try { await assetStorageService.addAsset({...}); } catch (error) { if (error instanceof QuotaExceededError) { // 提示用户删除旧素材 MessagePlugin.error('存储空间不足,请删除一些旧素材'); } } ``` ### Q: 如何优化大量素材的加载? - 使用虚拟滚动(react-window) - 懒加载图片(IntersectionObserver) - 分页加载(如果素材 > 500) ## 部署检查清单 - [ ] 所有单元测试通过 - [ ] 所有组件测试通过 - [ ] E2E测试通过 - [ ] TypeScript类型检查通过 - [ ] 所有文件 < 500行 - [ ] 使用TDesign组件(light主题) - [ ] BEM命名规范 - [ ] 性能指标达标(2s加载,500ms筛选) - [ ] 存储配额监控工作正常 - [ ] AI生成内容自动保存 - [ ] 删除确认对话框显示 - [ ] 响应式布局在移动端正常 ## 下一步 完成实现后: 1. 运行 `/speckit.tasks` 生成详细任务分解 2. 创建pull request 3. 代码审查 4. 部署到预发布环境 5. 用户验收测试 --- **快速开始指南完成日期**: 2025-12-11 **准备进入**: Phase 2 - Task Generation (运行 `/speckit.tasks`)