Initial TrueGrowth source import
This commit is contained in:
42
specs/009-media-library/checklists/requirements.md
Normal file
42
specs/009-media-library/checklists/requirements.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Specification Quality Checklist: 素材管理库 (Media Library)
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2025-12-11
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- All checklist items have been validated and passed
|
||||
- The specification is complete and ready for the next phase (/speckit.clarify or /speckit.plan)
|
||||
- The specification is written in Chinese as requested by the user
|
||||
- User scenarios are prioritized (P1 and P2) and independently testable
|
||||
- 5 comprehensive user scenarios cover: browsing/managing assets, selecting from library for AI generation, uploading new assets, managing assets (rename/delete/download), and automatic AI-generated content integration
|
||||
- 25 functional requirements cover all aspects of the media library feature
|
||||
- 10 measurable success criteria defined with specific metrics
|
||||
- Comprehensive edge cases, assumptions, dependencies, and out-of-scope items documented
|
||||
- No clarifications needed - all requirements are clear and implementable
|
||||
407
specs/009-media-library/contracts/asset-storage-service.md
Normal file
407
specs/009-media-library/contracts/asset-storage-service.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# API Contract: Asset Storage Service
|
||||
|
||||
**Service**: `asset-storage-service.ts`
|
||||
**Feature**: 009-media-library
|
||||
**Date**: 2025-12-11
|
||||
|
||||
## Overview
|
||||
|
||||
定义素材存储服务的接口契约,该服务负责与IndexedDB交互,管理素材的持久化存储。
|
||||
|
||||
## Service Interface
|
||||
|
||||
```typescript
|
||||
export interface AssetStorageService {
|
||||
/**
|
||||
* 初始化存储服务
|
||||
* 创建IndexedDB实例和必要的stores
|
||||
*/
|
||||
initialize(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 添加新素材到存储
|
||||
* @param data - 素材数据
|
||||
* @returns 保存的素材对象
|
||||
* @throws QuotaExceededError - 存储空间不足
|
||||
* @throws ValidationError - 数据验证失败
|
||||
*/
|
||||
addAsset(data: AddAssetData): Promise<Asset>;
|
||||
|
||||
/**
|
||||
* 获取所有素材
|
||||
* @returns 所有素材的数组
|
||||
*/
|
||||
getAllAssets(): Promise<Asset[]>;
|
||||
|
||||
/**
|
||||
* 根据ID获取单个素材
|
||||
* @param id - 素材ID
|
||||
* @returns 素材对象,不存在则返回null
|
||||
*/
|
||||
getAssetById(id: string): Promise<Asset | null>;
|
||||
|
||||
/**
|
||||
* 更新素材名称
|
||||
* @param id - 素材ID
|
||||
* @param newName - 新名称
|
||||
* @throws NotFoundError - 素材不存在
|
||||
* @throws ValidationError - 名称验证失败
|
||||
*/
|
||||
renameAsset(id: string, newName: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 删除素材
|
||||
* @param id - 素材ID
|
||||
* @throws NotFoundError - 素材不存在
|
||||
*/
|
||||
removeAsset(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 清空所有素材
|
||||
* 慎用,不可恢复
|
||||
*/
|
||||
clearAll(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取存储统计信息
|
||||
* @returns 存储使用情况
|
||||
*/
|
||||
getStorageStats(): Promise<StorageStats>;
|
||||
|
||||
/**
|
||||
* 检查存储配额
|
||||
* @returns 配额信息
|
||||
*/
|
||||
checkQuota(): Promise<StorageQuota>;
|
||||
|
||||
/**
|
||||
* 估算添加新素材后的存储使用量
|
||||
* @param blobSize - Blob大小(字节)
|
||||
* @returns 是否有足够空间
|
||||
*/
|
||||
canAddAsset(blobSize: number): Promise<boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
## Data Types
|
||||
|
||||
### AddAssetData
|
||||
|
||||
```typescript
|
||||
export interface AddAssetData {
|
||||
type: AssetType;
|
||||
source: AssetSource;
|
||||
name: string;
|
||||
blob: Blob;
|
||||
mimeType: string;
|
||||
prompt?: string;
|
||||
modelName?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### StorageStats
|
||||
|
||||
```typescript
|
||||
export interface StorageStats {
|
||||
totalAssets: number;
|
||||
imageCount: number;
|
||||
videoCount: number;
|
||||
localCount: number;
|
||||
aiGeneratedCount: number;
|
||||
totalSize: number; // 估算的总大小(字节)
|
||||
}
|
||||
```
|
||||
|
||||
### Error Types
|
||||
|
||||
```typescript
|
||||
export class AssetStorageError extends Error {
|
||||
constructor(message: string, public code: string) {
|
||||
super(message);
|
||||
this.name = 'AssetStorageError';
|
||||
}
|
||||
}
|
||||
|
||||
export class QuotaExceededError extends AssetStorageError {
|
||||
constructor() {
|
||||
super('存储空间不足', 'QUOTA_EXCEEDED');
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AssetStorageError {
|
||||
constructor(id: string) {
|
||||
super(`素材未找到: ${id}`, 'NOT_FOUND');
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AssetStorageError {
|
||||
constructor(message: string) {
|
||||
super(message, 'VALIDATION_ERROR');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Initialize Service
|
||||
|
||||
```typescript
|
||||
import { assetStorageService } from './services/asset-storage-service';
|
||||
|
||||
// 在应用启动时初始化
|
||||
await assetStorageService.initialize();
|
||||
```
|
||||
|
||||
### Add Asset
|
||||
|
||||
```typescript
|
||||
// 用户上传文件
|
||||
const file: File = event.target.files[0];
|
||||
|
||||
try {
|
||||
const asset = await assetStorageService.addAsset({
|
||||
type: AssetType.IMAGE,
|
||||
source: AssetSource.LOCAL,
|
||||
name: file.name,
|
||||
blob: file,
|
||||
mimeType: file.type
|
||||
});
|
||||
|
||||
console.log('Asset added:', asset.id);
|
||||
} catch (error) {
|
||||
if (error instanceof QuotaExceededError) {
|
||||
// 显示存储空间不足错误
|
||||
} else if (error instanceof ValidationError) {
|
||||
// 显示验证错误
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Load All Assets
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const assets = await assetStorageService.getAllAssets();
|
||||
console.log(`Loaded ${assets.length} assets`);
|
||||
} catch (error) {
|
||||
console.error('Failed to load assets:', error);
|
||||
}
|
||||
```
|
||||
|
||||
### Rename Asset
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await assetStorageService.renameAsset(assetId, '新名称');
|
||||
console.log('Asset renamed successfully');
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
// 素材不存在
|
||||
} else if (error instanceof ValidationError) {
|
||||
// 名称验证失败
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Asset
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await assetStorageService.removeAsset(assetId);
|
||||
console.log('Asset deleted');
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
// 素材已被删除或不存在
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Check Storage Quota
|
||||
|
||||
```typescript
|
||||
const quota = await assetStorageService.checkQuota();
|
||||
|
||||
console.log(`Storage usage: ${quota.percentUsed.toFixed(2)}%`);
|
||||
|
||||
if (quota.percentUsed > 80) {
|
||||
// 显示警告
|
||||
console.warn('Storage is nearly full!');
|
||||
}
|
||||
```
|
||||
|
||||
### Check Before Adding
|
||||
|
||||
```typescript
|
||||
const fileSize = file.size;
|
||||
const canAdd = await assetStorageService.canAddAsset(fileSize);
|
||||
|
||||
if (!canAdd) {
|
||||
alert('存储空间不足,请删除一些旧素材');
|
||||
return;
|
||||
}
|
||||
|
||||
// 继续添加
|
||||
await assetStorageService.addAsset({...});
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### 1. localforage Configuration
|
||||
|
||||
```typescript
|
||||
import localforage from 'localforage';
|
||||
|
||||
const assetStore = localforage.createInstance({
|
||||
name: 'aitu-assets',
|
||||
storeName: 'assets',
|
||||
description: 'Media library assets storage'
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Blob URL Management
|
||||
|
||||
- 创建时使用 `URL.createObjectURL(blob)`
|
||||
- 组件卸载时调用 `URL.revokeObjectURL(url)` 释放内存
|
||||
- 不直接存储 Blob URL 到 IndexedDB(存储 Blob 对象本身)
|
||||
|
||||
### 3. Error Handling Strategy
|
||||
|
||||
```typescript
|
||||
async function wrapStorageOperation<T>(
|
||||
operation: () => Promise<T>,
|
||||
errorContext: string
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
throw new QuotaExceededError();
|
||||
}
|
||||
if (error.name === 'NotFoundError') {
|
||||
throw new NotFoundError(errorContext);
|
||||
}
|
||||
throw new AssetStorageError(
|
||||
`Storage operation failed: ${error.message}`,
|
||||
'UNKNOWN_ERROR'
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Performance Considerations
|
||||
|
||||
- **批量加载**: 使用 `getAllAssets()` 一次加载所有元数据
|
||||
- **懒加载Blob**: 网格项只在可见时加载图片
|
||||
- **索引**: 考虑为 `type`, `source`, `createdAt` 添加索引(如使用原生IndexedDB)
|
||||
- **缓存**: 在内存中缓存最近加载的assets,减少IndexedDB查询
|
||||
|
||||
## Testing Contract
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```typescript
|
||||
describe('AssetStorageService', () => {
|
||||
beforeEach(async () => {
|
||||
await assetStorageService.initialize();
|
||||
await assetStorageService.clearAll();
|
||||
});
|
||||
|
||||
it('should add asset successfully', async () => {
|
||||
const blob = new Blob(['test'], { type: 'image/png' });
|
||||
const asset = await assetStorageService.addAsset({
|
||||
type: AssetType.IMAGE,
|
||||
source: AssetSource.LOCAL,
|
||||
name: 'test.png',
|
||||
blob,
|
||||
mimeType: 'image/png'
|
||||
});
|
||||
|
||||
expect(asset.id).toBeDefined();
|
||||
expect(asset.type).toBe(AssetType.IMAGE);
|
||||
});
|
||||
|
||||
it('should throw ValidationError for invalid name', async () => {
|
||||
const blob = new Blob(['test'], { type: 'image/png' });
|
||||
|
||||
await expect(
|
||||
assetStorageService.addAsset({
|
||||
type: AssetType.IMAGE,
|
||||
source: AssetSource.LOCAL,
|
||||
name: '', // Invalid
|
||||
blob,
|
||||
mimeType: 'image/png'
|
||||
})
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should get all assets', async () => {
|
||||
// Add multiple assets
|
||||
const blob = new Blob(['test'], { type: 'image/png' });
|
||||
await assetStorageService.addAsset({ /* ... */ });
|
||||
await assetStorageService.addAsset({ /* ... */ });
|
||||
|
||||
const assets = await assetStorageService.getAllAssets();
|
||||
expect(assets).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should rename asset', async () => {
|
||||
const blob = new Blob(['test'], { type: 'image/png' });
|
||||
const asset = await assetStorageService.addAsset({ /* ... */ });
|
||||
|
||||
await assetStorageService.renameAsset(asset.id, 'new-name.png');
|
||||
|
||||
const updated = await assetStorageService.getAssetById(asset.id);
|
||||
expect(updated?.name).toBe('new-name.png');
|
||||
});
|
||||
|
||||
it('should delete asset', async () => {
|
||||
const blob = new Blob(['test'], { type: 'image/png' });
|
||||
const asset = await assetStorageService.addAsset({ /* ... */ });
|
||||
|
||||
await assetStorageService.removeAsset(asset.id);
|
||||
|
||||
const deleted = await assetStorageService.getAssetById(asset.id);
|
||||
expect(deleted).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw NotFoundError when deleting non-existent asset', async () => {
|
||||
await expect(
|
||||
assetStorageService.removeAsset('non-existent-id')
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Input Validation**: 所有用户输入(name, mimeType)必须验证
|
||||
2. **MIME Type Verification**: 验证文件实际内容与声明的MIME类型匹配
|
||||
3. **Size Limits**: 虽然没有硬性限制,但应警告大文件
|
||||
4. **Error Messages**: 不暴露内部实现细节给用户
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
如果将来需要更改存储格式:
|
||||
|
||||
```typescript
|
||||
export interface StorageVersion {
|
||||
version: number;
|
||||
migrateFrom(oldVersion: number): Promise<void>;
|
||||
}
|
||||
|
||||
// 例如:从v1迁移到v2
|
||||
async function migrateV1ToV2(): Promise<void> {
|
||||
const allAssets = await assetStore.keys();
|
||||
for (const key of allAssets) {
|
||||
const old = await assetStore.getItem(key);
|
||||
const migrated = transformV1ToV2(old);
|
||||
await assetStore.setItem(key, migrated);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**契约完成日期**: 2025-12-11
|
||||
**实现文件**: `packages/drawnix/src/services/asset-storage-service.ts`
|
||||
**下一步**: 生成 quickstart.md 开发指南
|
||||
514
specs/009-media-library/data-model.md
Normal file
514
specs/009-media-library/data-model.md
Normal file
@@ -0,0 +1,514 @@
|
||||
# Data Model: 素材管理库 (Media Library)
|
||||
|
||||
**Feature**: 009-media-library
|
||||
**Date**: 2025-12-11
|
||||
**Status**: Design Complete
|
||||
|
||||
## Overview
|
||||
|
||||
本文档定义素材管理库的核心数据模型、类型定义、状态结构和验证规则。
|
||||
|
||||
## Core Entities
|
||||
|
||||
### 1. Asset (素材)
|
||||
|
||||
代表用户的媒体资源(图片或视频)。
|
||||
|
||||
#### TypeScript Definition
|
||||
|
||||
```typescript
|
||||
export enum AssetType {
|
||||
IMAGE = 'IMAGE',
|
||||
VIDEO = 'VIDEO'
|
||||
}
|
||||
|
||||
export enum AssetSource {
|
||||
LOCAL = 'LOCAL', // 本地上传
|
||||
AI_GENERATED = 'AI_GENERATED' // AI生成
|
||||
}
|
||||
|
||||
export interface Asset {
|
||||
// 标识
|
||||
id: string; // UUID v4
|
||||
|
||||
// 分类
|
||||
type: AssetType; // 素材类型
|
||||
source: AssetSource; // 素材来源
|
||||
|
||||
// 内容
|
||||
url: string; // Blob URL for display
|
||||
name: string; // 用户可见名称
|
||||
mimeType: string; // MIME类型 (image/jpeg, video/mp4, etc.)
|
||||
|
||||
// 元数据
|
||||
createdAt: number; // Unix timestamp (ms)
|
||||
size?: number; // 文件大小(字节),可选
|
||||
|
||||
// 可选扩展
|
||||
thumbnail?: string; // 缩略图URL(视频用)
|
||||
prompt?: string; // AI生成的提示词(仅AI_GENERATED)
|
||||
modelName?: string; // 生成模型名称(仅AI_GENERATED)
|
||||
}
|
||||
```
|
||||
|
||||
#### Validation Rules
|
||||
|
||||
```typescript
|
||||
export interface AssetValidationRules {
|
||||
id: {
|
||||
required: true;
|
||||
format: 'uuid-v4';
|
||||
};
|
||||
type: {
|
||||
required: true;
|
||||
enum: [AssetType.IMAGE, AssetType.VIDEO];
|
||||
};
|
||||
source: {
|
||||
required: true;
|
||||
enum: [AssetSource.LOCAL, AssetSource.AI_GENERATED];
|
||||
};
|
||||
url: {
|
||||
required: true;
|
||||
format: 'blob-url'; // blob:http://...
|
||||
};
|
||||
name: {
|
||||
required: true;
|
||||
minLength: 1;
|
||||
maxLength: 255;
|
||||
};
|
||||
mimeType: {
|
||||
required: true;
|
||||
allowedValues: [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp', // Images
|
||||
'video/mp4', 'video/webm', 'video/ogg' // Videos
|
||||
];
|
||||
};
|
||||
createdAt: {
|
||||
required: true;
|
||||
type: 'number';
|
||||
min: 0;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Factory Functions
|
||||
|
||||
```typescript
|
||||
export function createAsset(params: {
|
||||
type: AssetType;
|
||||
source: AssetSource;
|
||||
url: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
size?: number;
|
||||
prompt?: string;
|
||||
modelName?: string;
|
||||
}): Asset {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
type: params.type,
|
||||
source: params.source,
|
||||
url: params.url,
|
||||
name: params.name,
|
||||
mimeType: params.mimeType,
|
||||
createdAt: Date.now(),
|
||||
...(params.size && { size: params.size }),
|
||||
...(params.prompt && { prompt: params.prompt }),
|
||||
...(params.modelName && { modelName: params.modelName })
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 2. StoredAsset (存储的素材)
|
||||
|
||||
存储在IndexedDB中的素材数据结构。
|
||||
|
||||
#### TypeScript Definition
|
||||
|
||||
```typescript
|
||||
export interface StoredAsset {
|
||||
// 基础Asset字段(除了url)
|
||||
id: string;
|
||||
type: AssetType;
|
||||
source: AssetSource;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
createdAt: number;
|
||||
size?: number;
|
||||
prompt?: string;
|
||||
modelName?: string;
|
||||
|
||||
// 存储字段
|
||||
blobData: Blob; // 实际文件数据
|
||||
}
|
||||
|
||||
// 转换函数
|
||||
export function storedAssetToAsset(stored: StoredAsset): Asset {
|
||||
const url = URL.createObjectURL(stored.blobData);
|
||||
const { blobData, ...assetData } = stored;
|
||||
return {
|
||||
...assetData,
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
export function assetToStoredAsset(asset: Asset, blob: Blob): StoredAsset {
|
||||
const { url, ...assetData } = asset;
|
||||
return {
|
||||
...assetData,
|
||||
blobData: blob
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. FilterState (筛选状态)
|
||||
|
||||
控制素材库的显示筛选条件。
|
||||
|
||||
#### TypeScript Definition
|
||||
|
||||
```typescript
|
||||
export type AssetTypeFilter = 'ALL' | AssetType;
|
||||
export type AssetSourceFilter = 'ALL' | 'LOCAL' | 'AI';
|
||||
export type SortOption = 'DATE_DESC' | 'DATE_ASC' | 'NAME_ASC';
|
||||
|
||||
export interface FilterState {
|
||||
activeType: AssetTypeFilter; // 类型筛选
|
||||
activeSource: AssetSourceFilter; // 来源筛选
|
||||
searchQuery: string; // 搜索关键词
|
||||
sortBy: SortOption; // 排序方式
|
||||
}
|
||||
|
||||
export const DEFAULT_FILTER_STATE: FilterState = {
|
||||
activeType: 'ALL',
|
||||
activeSource: 'ALL',
|
||||
searchQuery: '',
|
||||
sortBy: 'DATE_DESC'
|
||||
};
|
||||
```
|
||||
|
||||
### 4. SelectionMode (选择模式)
|
||||
|
||||
素材库的使用场景。
|
||||
|
||||
#### TypeScript Definition
|
||||
|
||||
```typescript
|
||||
export enum SelectionMode {
|
||||
BROWSE = 'BROWSE', // 浏览模式:查看和管理
|
||||
SELECT = 'SELECT' // 选择模式:从AI生成界面选择
|
||||
}
|
||||
|
||||
export interface MediaLibraryConfig {
|
||||
mode: SelectionMode;
|
||||
filterType?: AssetType; // 限制显示的类型(SELECT模式)
|
||||
onSelect?: (asset: Asset) => void; // 选择回调(SELECT模式)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. StorageQuota (存储配额)
|
||||
|
||||
浏览器存储空间信息。
|
||||
|
||||
#### TypeScript Definition
|
||||
|
||||
```typescript
|
||||
export interface StorageQuota {
|
||||
usage: number; // 已使用空间(字节)
|
||||
quota: number; // 总配额(字节)
|
||||
percentUsed: number; // 使用百分比 (0-100)
|
||||
available: number; // 可用空间(字节)
|
||||
}
|
||||
|
||||
export interface StorageStatus {
|
||||
quota: StorageQuota;
|
||||
isNearLimit: boolean; // 是否接近限制 (>80%)
|
||||
isCritical: boolean; // 是否严重 (>95%)
|
||||
}
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### AssetContext State
|
||||
|
||||
```typescript
|
||||
export interface AssetContextState {
|
||||
// 核心数据
|
||||
assets: Asset[];
|
||||
|
||||
// UI状态
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// 筛选和排序
|
||||
filters: FilterState;
|
||||
|
||||
// 选择
|
||||
selectedAssetId: string | null;
|
||||
|
||||
// 存储状态
|
||||
storageStatus: StorageStatus | null;
|
||||
}
|
||||
|
||||
export interface AssetContextActions {
|
||||
// 素材操作
|
||||
loadAssets: () => Promise<void>;
|
||||
addAsset: (file: File | Blob, type: AssetType, source: AssetSource, name?: string) => Promise<Asset>;
|
||||
removeAsset: (id: string) => Promise<void>;
|
||||
renameAsset: (id: string, newName: string) => Promise<void>;
|
||||
|
||||
// 筛选和选择
|
||||
setFilters: (filters: Partial<FilterState>) => void;
|
||||
setSelectedAssetId: (id: string | null) => void;
|
||||
|
||||
// 存储管理
|
||||
checkStorageQuota: () => Promise<void>;
|
||||
}
|
||||
|
||||
export type AssetContextValue = AssetContextState & AssetContextActions;
|
||||
```
|
||||
|
||||
## Derived Data
|
||||
|
||||
### Filtered Assets
|
||||
|
||||
```typescript
|
||||
export interface FilteredAssetsResult {
|
||||
assets: Asset[];
|
||||
count: number;
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
export function filterAssets(
|
||||
assets: Asset[],
|
||||
filters: FilterState
|
||||
): FilteredAssetsResult {
|
||||
const filtered = assets
|
||||
.filter(asset => {
|
||||
// Type filter
|
||||
const matchesType = filters.activeType === 'ALL' || asset.type === filters.activeType;
|
||||
|
||||
// Source filter
|
||||
const matchesSource =
|
||||
filters.activeSource === 'ALL' ||
|
||||
(filters.activeSource === 'AI' && asset.source === AssetSource.AI_GENERATED) ||
|
||||
(filters.activeSource === 'LOCAL' && asset.source === AssetSource.LOCAL);
|
||||
|
||||
// Search filter
|
||||
const matchesSearch =
|
||||
filters.searchQuery === '' ||
|
||||
asset.name.toLowerCase().includes(filters.searchQuery.toLowerCase());
|
||||
|
||||
return matchesType && matchesSource && matchesSearch;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
switch (filters.sortBy) {
|
||||
case 'DATE_DESC':
|
||||
return b.createdAt - a.createdAt;
|
||||
case 'DATE_ASC':
|
||||
return a.createdAt - b.createdAt;
|
||||
case 'NAME_ASC':
|
||||
return a.name.localeCompare(b.name);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
assets: filtered,
|
||||
count: filtered.length,
|
||||
isEmpty: filtered.length === 0
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Component Props Types
|
||||
|
||||
### MediaLibraryModal Props
|
||||
|
||||
```typescript
|
||||
export interface MediaLibraryModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
mode?: SelectionMode;
|
||||
filterType?: AssetType;
|
||||
onSelect?: (asset: Asset) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### AssetGridItem Props
|
||||
|
||||
```typescript
|
||||
export interface AssetGridItemProps {
|
||||
asset: Asset;
|
||||
isSelected: boolean;
|
||||
onSelect: (assetId: string) => void;
|
||||
onDoubleClick: (asset: Asset) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### MediaLibrarySidebar Props
|
||||
|
||||
```typescript
|
||||
export interface MediaLibrarySidebarProps {
|
||||
filters: FilterState;
|
||||
assetCount: number;
|
||||
storageStatus: StorageStatus | null;
|
||||
onFilterChange: (filters: Partial<FilterState>) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### MediaLibraryInspector Props
|
||||
|
||||
```typescript
|
||||
export interface MediaLibraryInspectorProps {
|
||||
asset: Asset | null;
|
||||
onRename: (assetId: string, newName: string) => void;
|
||||
onDelete: (assetId: string) => void;
|
||||
onDownload: (asset: Asset) => void;
|
||||
onSelect?: (asset: Asset) => void;
|
||||
showSelectButton: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Functions
|
||||
|
||||
### Asset Validation
|
||||
|
||||
```typescript
|
||||
export function validateAssetName(name: string): { valid: boolean; error?: string } {
|
||||
if (!name || name.trim().length === 0) {
|
||||
return { valid: false, error: '素材名称不能为空' };
|
||||
}
|
||||
if (name.length > 255) {
|
||||
return { valid: false, error: '素材名称不能超过255个字符' };
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
export function validateMimeType(mimeType: string): { valid: boolean; error?: string } {
|
||||
const allowedTypes = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||
'video/mp4', 'video/webm', 'video/ogg'
|
||||
];
|
||||
|
||||
if (!allowedTypes.includes(mimeType)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `不支持的文件类型: ${mimeType}`
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
export function getAssetType(mimeType: string): AssetType | null {
|
||||
if (mimeType.startsWith('image/')) return AssetType.IMAGE;
|
||||
if (mimeType.startsWith('video/')) return AssetType.VIDEO;
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Constants
|
||||
|
||||
```typescript
|
||||
export const ASSET_CONSTANTS = {
|
||||
// 存储
|
||||
STORAGE_NAME: 'aitu-assets',
|
||||
STORE_NAME: 'assets',
|
||||
|
||||
// 限制
|
||||
MAX_NAME_LENGTH: 255,
|
||||
STORAGE_WARNING_THRESHOLD: 0.80, // 80%
|
||||
STORAGE_CRITICAL_THRESHOLD: 0.95, // 95%
|
||||
|
||||
// 文件类型
|
||||
ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
||||
ALLOWED_VIDEO_TYPES: ['video/mp4', 'video/webm', 'video/ogg'],
|
||||
|
||||
// UI
|
||||
GRID_COLUMNS_DESKTOP: 5,
|
||||
GRID_COLUMNS_TABLET: 3,
|
||||
GRID_COLUMNS_MOBILE: 2,
|
||||
|
||||
// 默认名称格式
|
||||
DEFAULT_IMAGE_NAME_FORMAT: 'AI图片-{timestamp}',
|
||||
DEFAULT_VIDEO_NAME_FORMAT: 'AI视频-{timestamp}',
|
||||
PROMPT_NAME_MAX_LENGTH: 20
|
||||
} as const;
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Adding an Asset
|
||||
|
||||
```
|
||||
User uploads file / AI generates content
|
||||
↓
|
||||
Validate file (type, size, magic number)
|
||||
↓
|
||||
Create Asset object with Blob URL
|
||||
↓
|
||||
Convert to StoredAsset (with Blob data)
|
||||
↓
|
||||
Save to IndexedDB via localforage
|
||||
↓
|
||||
Update AssetContext state
|
||||
↓
|
||||
Check storage quota
|
||||
↓
|
||||
UI re-renders with new asset
|
||||
```
|
||||
|
||||
### Selecting an Asset
|
||||
|
||||
```
|
||||
User clicks asset in SELECT mode
|
||||
↓
|
||||
Set selectedAssetId in context
|
||||
↓
|
||||
User confirms selection (double-click / button)
|
||||
↓
|
||||
Call onSelect callback with Asset
|
||||
↓
|
||||
Parent component (AI dialog) receives asset
|
||||
↓
|
||||
Close modal, update reference image
|
||||
```
|
||||
|
||||
### Loading Assets
|
||||
|
||||
```
|
||||
Component mounts / Manual refresh
|
||||
↓
|
||||
Set loading = true
|
||||
↓
|
||||
Load all StoredAssets from IndexedDB
|
||||
↓
|
||||
Convert to Asset objects (create Blob URLs)
|
||||
↓
|
||||
Update context state with assets
|
||||
↓
|
||||
Set loading = false
|
||||
↓
|
||||
Apply current filters
|
||||
↓
|
||||
Render filtered assets
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
此数据模型定义了:
|
||||
- **4个核心实体**: Asset, StoredAsset, FilterState, SelectionMode
|
||||
- **完整的类型系统**: TypeScript接口和枚举
|
||||
- **验证规则**: 确保数据完整性
|
||||
- **状态管理结构**: Context state和actions
|
||||
- **派生数据**: 筛选和排序逻辑
|
||||
- **组件Props**: 所有主要组件的类型
|
||||
- **常量**: 配置和限制值
|
||||
|
||||
所有类型都符合TypeScript strict mode要求,并为实现阶段提供清晰的契约。
|
||||
|
||||
---
|
||||
|
||||
**设计完成日期**: 2025-12-11
|
||||
**下一步**: 生成API契约文档
|
||||
213
specs/009-media-library/plan.md
Normal file
213
specs/009-media-library/plan.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Implementation Plan: 素材管理库 (Media Library)
|
||||
|
||||
**Branch**: `009-media-library` | **Date**: 2025-12-11 | **Spec**: [spec.md](./spec.md)
|
||||
**Input**: Feature specification from `/specs/009-media-library/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
|
||||
|
||||
## Summary
|
||||
|
||||
实现一个素材管理库功能,允许用户通过AI生成对话框访问、浏览、管理和选择媒体素材(图片和视频)。素材库使用IndexedDB进行本地持久化存储,支持按类型和来源筛选、搜索、排序,以及重命名、删除和下载操作。AI生成的内容将自动添加到素材库中,用户可以从素材库中选择已有素材作为参考图,无需每次都从本地文件系统选择。
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: TypeScript 5.x (strict mode), React 18.x
|
||||
**Primary Dependencies**:
|
||||
- TDesign React (UI components, light theme)
|
||||
- Plait Framework (existing whiteboard core)
|
||||
- localforage (IndexedDB wrapper for storage)
|
||||
- RxJS (reactive state management)
|
||||
- Vite (build tool)
|
||||
|
||||
**Storage**: IndexedDB (via localforage) for:
|
||||
- Asset metadata and Blob URLs
|
||||
- Asset data persistence across browser sessions
|
||||
- Storage quota monitoring
|
||||
|
||||
**Testing**:
|
||||
- React Testing Library (component tests)
|
||||
- Jest (unit tests)
|
||||
- Playwright (E2E tests for key flows)
|
||||
|
||||
**Target Platform**: Modern web browsers (Chrome, Firefox, Safari, Edge latest versions) supporting IndexedDB and Blob URLs
|
||||
|
||||
**Project Type**: Monorepo web application (Nx workspace)
|
||||
|
||||
**Performance Goals**:
|
||||
- Initial load time < 2 seconds (for 100 assets)
|
||||
- Filter/search response < 500ms
|
||||
- Support至少 100 assets without degrading UI responsiveness
|
||||
|
||||
**Constraints**:
|
||||
- No backend - pure frontend solution
|
||||
- Browser storage limits (50MB-500MB depending on browser)
|
||||
- Single file size limit: 500 lines (per constitution)
|
||||
- Access only through AI generation dialogs (no standalone entry point)
|
||||
|
||||
**Scale/Scope**:
|
||||
- ~5-8 React components (modal, sidebar, grid, inspector, upload)
|
||||
- 1 Context provider for asset state management
|
||||
- 1 IndexedDB storage service
|
||||
- Integration with 2 existing AI generation dialogs (image, video)
|
||||
- Integration with existing task queue service for auto-save
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
### ✅ PASS: Plugin Architecture
|
||||
- **Requirement**: 每个功能都应该实现为遵循 `withXxx` 模式的可组合插件
|
||||
- **Status**: ✅ PASS - 素材管理库不是编辑器插件,而是UI功能模块,使用React Context模式进行状态管理。不违反插件架构原则。
|
||||
|
||||
### ✅ PASS: File Size Constraint (500 lines)
|
||||
- **Requirement**: 单个文件不得超过 500 行(硬性约束)
|
||||
- **Status**: ✅ PASS - 计划将组件分解为多个小文件:
|
||||
- MediaLibraryModal.tsx (主容器 < 150 lines)
|
||||
- MediaLibraryGrid.tsx (网格视图 < 200 lines)
|
||||
- MediaLibrarySidebar.tsx (筛选侧边栏 < 150 lines)
|
||||
- MediaLibraryInspector.tsx (详情面板 < 200 lines)
|
||||
- AssetContext.tsx (状态管理 < 150 lines)
|
||||
- asset-storage-service.ts (存储服务 < 250 lines)
|
||||
- 各种小型组件和工具函数
|
||||
|
||||
### ✅ PASS: TypeScript Strict Mode
|
||||
- **Requirement**: TypeScript 严格模式是强制性的
|
||||
- **Status**: ✅ PASS - 所有新代码将使用 TypeScript 5.x strict mode,定义完整的接口和类型
|
||||
|
||||
### ✅ PASS: TDesign React & Light Theme
|
||||
- **Requirement**: 所有 UI 组件必须使用 TDesign React 并采用 light 主题
|
||||
- **Status**: ✅ PASS - 将使用TDesign组件(Dialog, Button, Input, Tooltip)并配置light主题
|
||||
|
||||
### ✅ PASS: Performance Optimization
|
||||
- **Requirement**: 为用户体验进行优化(React.memo, useCallback, useMemo)
|
||||
- **Status**: ✅ PASS - 将应用优化策略:
|
||||
- 网格项组件使用 React.memo
|
||||
- 事件处理器使用 useCallback
|
||||
- 筛选后的素材列表使用 useMemo
|
||||
- 考虑虚拟化(如果资产数量增长)
|
||||
|
||||
### ✅ PASS: Security & Validation
|
||||
- **Requirement**: 验证和清理所有用户输入,文件上传验证
|
||||
- **Status**: ✅ PASS - 将实现:
|
||||
- 文件类型验证(仅图片和视频)
|
||||
- MIME类型检查
|
||||
- 存储错误处理
|
||||
- 无硬编码敏感信息
|
||||
|
||||
### ✅ PASS: Monorepo Structure
|
||||
- **Requirement**: 在 Nx monorepo 中保持清晰的分离
|
||||
- **Status**: ✅ PASS - 新代码将放置在 `packages/drawnix/src/` 下:
|
||||
- `components/media-library/` - UI组件
|
||||
- `services/asset-storage-service.ts` - 存储服务
|
||||
- `types/asset.types.ts` - 类型定义
|
||||
- `hooks/useAssets.ts` - 自定义hook
|
||||
|
||||
### ✅ PASS: Naming Conventions
|
||||
- **Requirement**: 严格的文件和代码命名约定
|
||||
- **Status**: ✅ PASS - 将遵循:
|
||||
- 组件:PascalCase.tsx
|
||||
- Hooks:camelCase.ts (useAssets.ts)
|
||||
- 服务:kebab-case.ts (asset-storage-service.ts)
|
||||
- 类型:kebab-case.types.ts (asset.types.ts)
|
||||
|
||||
### ✅ PASS: Component Structure
|
||||
- **Requirement**: React组件遵循标准结构顺序
|
||||
- **Status**: ✅ PASS - 所有组件将遵循:导入 → 类型 → 常量 → Hooks → 事件处理器 → 渲染
|
||||
|
||||
### ✅ PASS: Testing Requirements
|
||||
- **Requirement**: 单元测试、组件测试、集成测试、E2E测试
|
||||
- **Status**: ✅ PASS - 测试计划:
|
||||
- 单元测试:asset-storage-service.ts
|
||||
- 组件测试:所有React组件
|
||||
- 集成测试:与AI生成对话框的集成
|
||||
- E2E测试:完整的选择和上传流程
|
||||
|
||||
### ✅ PASS: CSS/SCSS Standards
|
||||
- **Requirement**: 遵循 BEM 方法论
|
||||
- **Status**: ✅ PASS - 将使用BEM命名,CSS变量优先
|
||||
|
||||
### ✅ PASS: Git Commit Convention
|
||||
- **Requirement**: 遵循 Conventional Commits
|
||||
- **Status**: ✅ PASS - 提交格式:`feat(media-library): <description>`
|
||||
|
||||
**GATE RESULT: ✅ ALL GATES PASSED - Proceed to Phase 0**
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/009-media-library/
|
||||
├── spec.md # Feature specification
|
||||
├── plan.md # This file (/speckit.plan command output)
|
||||
├── research.md # Phase 0 output (技术决策和最佳实践)
|
||||
├── data-model.md # Phase 1 output (数据模型定义)
|
||||
├── quickstart.md # Phase 1 output (快速开始指南)
|
||||
├── contracts/ # Phase 1 output (接口契约)
|
||||
│ └── asset-store-api.md
|
||||
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT YET CREATED)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
packages/drawnix/src/
|
||||
├── components/
|
||||
│ └── media-library/
|
||||
│ ├── MediaLibraryModal.tsx # 主弹窗容器组件
|
||||
│ ├── MediaLibraryModal.scss # 弹窗样式
|
||||
│ ├── MediaLibraryGrid.tsx # 网格视图组件
|
||||
│ ├── MediaLibraryGrid.scss # 网格样式
|
||||
│ ├── MediaLibrarySidebar.tsx # 左侧筛选侧边栏
|
||||
│ ├── MediaLibrarySidebar.scss # 侧边栏样式
|
||||
│ ├── MediaLibraryInspector.tsx # 右侧详情面板
|
||||
│ ├── MediaLibraryInspector.scss # 详情面板样式
|
||||
│ ├── MediaLibraryEmpty.tsx # 空状态组件
|
||||
│ ├── MediaLibraryStorageBar.tsx # 存储空间进度条
|
||||
│ ├── AssetGridItem.tsx # 单个素材网格项
|
||||
│ ├── AssetGridItem.scss # 网格项样式
|
||||
│ └── index.ts # 导出
|
||||
├── contexts/
|
||||
│ └── AssetContext.tsx # 素材状态管理Context
|
||||
├── services/
|
||||
│ ├── asset-storage-service.ts # IndexedDB存储服务
|
||||
│ └── asset-integration-service.ts # 与AI生成服务集成
|
||||
├── hooks/
|
||||
│ ├── useAssets.ts # 素材管理hook
|
||||
│ └── useAssetSelection.ts # 素材选择hook
|
||||
├── types/
|
||||
│ └── asset.types.ts # 素材相关类型定义
|
||||
├── utils/
|
||||
│ ├── asset-utils.ts # 素材工具函数
|
||||
│ └── storage-quota.ts # 存储配额工具
|
||||
└── constants/
|
||||
└── ASSET_CONSTANTS.ts # 素材相关常量
|
||||
|
||||
packages/drawnix/src/components/ttd-dialog/
|
||||
├── ai-image-generation.tsx # [MODIFY] 集成素材库选择
|
||||
└── ai-video-generation.tsx # [MODIFY] 集成素材库选择
|
||||
|
||||
tests/
|
||||
└── media-library/
|
||||
├── unit/
|
||||
│ ├── asset-storage-service.spec.ts
|
||||
│ ├── asset-utils.spec.ts
|
||||
│ └── storage-quota.spec.ts
|
||||
├── component/
|
||||
│ ├── MediaLibraryModal.spec.tsx
|
||||
│ ├── MediaLibraryGrid.spec.tsx
|
||||
│ └── AssetGridItem.spec.tsx
|
||||
├── integration/
|
||||
│ └── ai-generation-integration.spec.tsx
|
||||
└── e2e/
|
||||
├── asset-upload.spec.ts
|
||||
└── asset-selection.spec.ts
|
||||
```
|
||||
|
||||
**Structure Decision**: 使用 monorepo 中的 `packages/drawnix` 包,遵循现有项目结构,将素材管理库功能作为独立模块添加到 `components/media-library/` 目录下。使用Context模式进行状态管理,与现有的服务层(`services/`)保持一致。组件将被分解为多个小文件以符合500行限制,每个组件负责单一职责(网格、侧边栏、详情面板等)。
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> **No constitution violations - This section is empty**
|
||||
|
||||
所有宪章检查项均已通过,无需额外的复杂性理由说明。
|
||||
877
specs/009-media-library/quickstart.md
Normal file
877
specs/009-media-library/quickstart.md
Normal file
@@ -0,0 +1,877 @@
|
||||
# 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<Asset> {
|
||||
// 实现逻辑...
|
||||
}
|
||||
|
||||
// 其他方法...
|
||||
}
|
||||
|
||||
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<AssetContextValue | null>(null);
|
||||
|
||||
export function AssetProvider({ children }: { children: React.ReactNode }) {
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 <AssetContext.Provider value={value}>{children}</AssetContext.Provider>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<AssetProvider>
|
||||
{/* 现有内容 */}
|
||||
</AssetProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadAssets();
|
||||
}
|
||||
}, [isOpen, loadAssets]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={isOpen}
|
||||
onClose={onClose}
|
||||
width="90vw"
|
||||
height="90vh"
|
||||
className="media-library-modal"
|
||||
>
|
||||
<div className="media-library-layout">
|
||||
<MediaLibrarySidebar />
|
||||
<MediaLibraryGrid
|
||||
filterType={filterType}
|
||||
selectedAssetId={selectedAssetId}
|
||||
onSelectAsset={setSelectedAssetId}
|
||||
/>
|
||||
<MediaLibraryInspector
|
||||
assetId={selectedAssetId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**文件**: `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 <MediaLibraryEmpty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="media-library-grid">
|
||||
<div className="media-library-grid__container">
|
||||
{filteredResult.assets.map(asset => (
|
||||
<AssetGridItem
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
isSelected={selectedAssetId === asset.id}
|
||||
onSelect={onSelectAsset}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- 使用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<AssetGridItemProps>(({
|
||||
asset,
|
||||
isSelected,
|
||||
onSelect
|
||||
}) => {
|
||||
const handleClick = useCallback(() => {
|
||||
onSelect(asset.id);
|
||||
}, [asset.id, onSelect]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`asset-grid-item ${isSelected ? 'asset-grid-item--selected' : ''}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* 缩略图 */}
|
||||
<div className="asset-grid-item__thumbnail">
|
||||
{asset.type === 'IMAGE' ? (
|
||||
<img src={asset.url} alt={asset.name} />
|
||||
) : (
|
||||
<video src={asset.url} muted />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 类型标识 */}
|
||||
<div className="asset-grid-item__badge">
|
||||
{asset.type === 'IMAGE' ? <ImageIcon /> : <VideoIcon />}
|
||||
</div>
|
||||
|
||||
{/* AI标识 */}
|
||||
{asset.source === 'AI_GENERATED' && (
|
||||
<div className="asset-grid-item__ai-badge">AI</div>
|
||||
)}
|
||||
|
||||
{/* 名称 */}
|
||||
<div className="asset-grid-item__name">{asset.name}</div>
|
||||
</div>
|
||||
);
|
||||
}, (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 (
|
||||
<div className="media-library-sidebar">
|
||||
{/* 类型筛选 */}
|
||||
<section className="media-library-sidebar__section">
|
||||
<h3 className="media-library-sidebar__title">类型</h3>
|
||||
<Button
|
||||
variant={filters.activeType === 'ALL' ? 'base' : 'outline'}
|
||||
onClick={() => setFilters({ activeType: 'ALL' })}
|
||||
block
|
||||
>
|
||||
<Grid /> 全部素材
|
||||
</Button>
|
||||
<Button
|
||||
variant={filters.activeType === 'IMAGE' ? 'base' : 'outline'}
|
||||
onClick={() => setFilters({ activeType: 'IMAGE' })}
|
||||
block
|
||||
>
|
||||
<ImageIcon /> 图片
|
||||
</Button>
|
||||
<Button
|
||||
variant={filters.activeType === 'VIDEO' ? 'base' : 'outline'}
|
||||
onClick={() => setFilters({ activeType: 'VIDEO' })}
|
||||
block
|
||||
>
|
||||
<VideoIcon /> 视频
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
{/* 来源筛选 */}
|
||||
<section className="media-library-sidebar__section">
|
||||
<h3 className="media-library-sidebar__title">来源</h3>
|
||||
<Button
|
||||
variant={filters.activeSource === 'ALL' ? 'base' : 'outline'}
|
||||
onClick={() => setFilters({ activeSource: 'ALL' })}
|
||||
block
|
||||
>
|
||||
全部来源
|
||||
</Button>
|
||||
<Button
|
||||
variant={filters.activeSource === 'LOCAL' ? 'base' : 'outline'}
|
||||
onClick={() => setFilters({ activeSource: 'LOCAL' })}
|
||||
block
|
||||
>
|
||||
<HardDrive /> 本地上传
|
||||
</Button>
|
||||
<Button
|
||||
variant={filters.activeSource === 'AI' ? 'base' : 'outline'}
|
||||
onClick={() => setFilters({ activeSource: 'AI' })}
|
||||
block
|
||||
>
|
||||
<Cpu /> AI生成
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
{/* 存储状态 */}
|
||||
<MediaLibraryStorageBar
|
||||
assetCount={assets.length}
|
||||
storageStatus={storageStatus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- 使用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 <div className="media-library-inspector--empty">选择素材查看详情</div>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="media-library-inspector">
|
||||
{/* 预览 */}
|
||||
<div className="media-library-inspector__preview">
|
||||
{asset.type === 'IMAGE' ? (
|
||||
<img src={asset.url} alt={asset.name} />
|
||||
) : (
|
||||
<video src={asset.url} controls />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 名称编辑 */}
|
||||
<div className="media-library-inspector__name">
|
||||
{isRenaming ? (
|
||||
<Input
|
||||
value={newName}
|
||||
onChange={setNewName}
|
||||
onBlur={handleRename}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<span>{asset.name}</span>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
icon={<Edit2 />}
|
||||
onClick={() => {
|
||||
setNewName(asset.name);
|
||||
setIsRenaming(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 元数据 */}
|
||||
<div className="media-library-inspector__metadata">
|
||||
<div>类型: {asset.type}</div>
|
||||
<div>来源: {asset.source}</div>
|
||||
<div>创建时间: {new Date(asset.createdAt).toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="media-library-inspector__actions">
|
||||
{onSelect && (
|
||||
<Button
|
||||
theme="primary"
|
||||
block
|
||||
icon={<CheckCircle />}
|
||||
onClick={() => onSelect(asset)}
|
||||
>
|
||||
使用到画板
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
block
|
||||
icon={<Download />}
|
||||
onClick={() => downloadAsset(asset)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
theme="danger"
|
||||
variant="outline"
|
||||
block
|
||||
icon={<Trash2 />}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- 使用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<Asset | null>(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 现有代码 */}
|
||||
|
||||
{/* 新增:参考图选择器 */}
|
||||
<div className="reference-image-selector">
|
||||
<Button onClick={() => setShowMediaLibrary(true)}>
|
||||
从素材库选择
|
||||
</Button>
|
||||
<Button onClick={() => /* 现有的本地选择逻辑 */}>
|
||||
从本地选择
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 素材库弹窗 */}
|
||||
<MediaLibraryModal
|
||||
isOpen={showMediaLibrary}
|
||||
onClose={() => setShowMediaLibrary(false)}
|
||||
onSelect={(asset) => {
|
||||
setReferenceImage(asset);
|
||||
setShowMediaLibrary(false);
|
||||
}}
|
||||
filterType="IMAGE" // 只显示图片
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- 不破坏现有功能
|
||||
- 素材库只显示图片
|
||||
- 选择后正确应用参考图
|
||||
|
||||
#### 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`)
|
||||
606
specs/009-media-library/research.md
Normal file
606
specs/009-media-library/research.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# 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<boolean> {
|
||||
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<void>;
|
||||
removeAsset: (id: string) => Promise<void>;
|
||||
renameAsset: (id: string, newName: string) => Promise<void>;
|
||||
loadAssets: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const AssetContext = createContext<AssetContextValue | null>(null);
|
||||
|
||||
export function AssetProvider({ children }: { children: ReactNode }) {
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 实现各种操作...
|
||||
|
||||
const value = useMemo(() => ({
|
||||
assets,
|
||||
loading,
|
||||
error,
|
||||
addAsset,
|
||||
removeAsset,
|
||||
renameAsset,
|
||||
loadAssets
|
||||
}), [assets, loading, error]);
|
||||
|
||||
return <AssetContext.Provider value={value}>{children}</AssetContext.Provider>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="upload-source-selector">
|
||||
<Button
|
||||
variant={uploadSource === 'local' ? 'base' : 'outline'}
|
||||
onClick={() => setUploadSource('local')}
|
||||
>
|
||||
从本地选择
|
||||
</Button>
|
||||
<Button
|
||||
variant={uploadSource === 'library' ? 'base' : 'outline'}
|
||||
onClick={() => {
|
||||
setUploadSource('library');
|
||||
setShowMediaLibrary(true);
|
||||
}}
|
||||
>
|
||||
从素材库选择
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showMediaLibrary && (
|
||||
<MediaLibraryModal
|
||||
isOpen={showMediaLibrary}
|
||||
onClose={() => 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<boolean> {
|
||||
// 读取文件前几个字节检查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<AssetGridItemProps>(({
|
||||
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<string>('');
|
||||
const imgRef = useRef<HTMLImageElement>(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 <img ref={imgRef} src={src} alt="" />;
|
||||
}
|
||||
```
|
||||
|
||||
#### 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 (
|
||||
<FixedSizeGrid
|
||||
columnCount={COLUMN_COUNT}
|
||||
columnWidth={COLUMN_WIDTH}
|
||||
height={600}
|
||||
rowCount={Math.ceil(assets.length / COLUMN_COUNT)}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
width={900}
|
||||
>
|
||||
{({ columnIndex, rowIndex, style }) => {
|
||||
const index = rowIndex * COLUMN_COUNT + columnIndex;
|
||||
const asset = assets[index];
|
||||
return asset ? (
|
||||
<div style={style}>
|
||||
<AssetGridItem asset={asset} />
|
||||
</div>
|
||||
) : null;
|
||||
}}
|
||||
</FixedSizeGrid>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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<void> {
|
||||
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 (
|
||||
<div className="error-state">
|
||||
<h3>素材库加载失败</h3>
|
||||
<p>请刷新页面重试</p>
|
||||
<Button onClick={() => window.location.reload()}>
|
||||
刷新页面
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
233
specs/009-media-library/spec.md
Normal file
233
specs/009-media-library/spec.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Feature Specification: 素材管理库 (Media Library)
|
||||
|
||||
**Feature Branch**: `009-media-library`
|
||||
**Created**: 2025-12-11
|
||||
**Status**: Draft
|
||||
**Input**: User description: "目前需要新增一个 素材管理 功能, 分析项目,然后目前该功能能够对应可以生成 ai 图片,视频 等 ,可以上传图片等资源,然后现在 是一个 前端项目,目前没有涉及后端,用的是 react 框架 ,然后对应有一版参考样式:注意只之用提取素材管理部分,也就是 Media Library 部分的功能 和样式 ,对应的参考代码在 /Users/gongchengtu/code/github/asset-manager 这个路径下。然后需要注意本项目中,ai生成的图片/视频的进入这个素材库 的对应处理方案,本地上传的图片、生成的图片、视频皆是素材,就是目前有一个ai 生图 和生视频的画板,里面有个选择参考图片,然后目前是默认点击后是直接打开本地项目夹进行图片资源选择,然后现在我要增加一个选择,也就是当前的素材库的选择,当用户点击素材库 的时候,会打开素材管理库 , 可以便捷选择 已有素材,同时 也有选择本地文件的功能。请你帮我设计和开发,然后说明文档 用中文概括描写"
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2025-12-11
|
||||
|
||||
- Q: 素材库访问入口 (Access Points) - 用户如何打开素材库进行浏览和管理? → A: 仅通过AI生成对话框访问。素材库没有独立的主界面入口,只能在AI生图/生视频对话框中点击"选择参考图"时打开。
|
||||
- Q: 删除确认机制 (Delete Confirmation) - 用户删除素材时需要什么样的确认机制? → A: 弹出确认对话框。用户点击删除按钮后,显示确认对话框警告删除操作不可撤销,用户需要在对话框中确认才能完成删除。
|
||||
- Q: 存储空间监控 (Storage Monitoring) - 如何向用户显示和管理浏览器存储空间使用情况? → A: 显示容量指示器和警告。在素材库界面底部显示存储空间使用进度条,当使用量达到80%时显示警告提示用户清理空间。
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### 用户场景 1 - 浏览和管理已有素材 (Priority: P1)
|
||||
|
||||
作为画板用户,我需要能够查看和管理我的所有素材(包括AI生成的图片/视频和本地上传的文件),以便我可以复用之前的创作内容,而不需要重复生成或上传。用户通过AI生成对话框中的"选择参考图"功能访问素材库。
|
||||
|
||||
**此优先级的原因**: 这是素材管理的核心功能,用户必须能够看到和访问自己的素材库才能进行后续的选择和管理操作。这是最基础的价值交付。
|
||||
|
||||
**独立测试**: 可以通过在AI生成对话框中点击"选择参考图"按钮打开素材库界面,验证能够显示所有历史素材(AI生成和本地上传的图片/视频),并支持基本的浏览和分类查看功能。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **Given** 用户已经通过AI生成了3张图片和1个视频,**When** 用户在AI生成对话框中打开素材库,**Then** 应该能看到这4个素材按时间倒序排列显示
|
||||
2. **Given** 用户在素材库中,**When** 用户点击"类型"筛选器选择"图片",**Then** 只显示图片类型的素材
|
||||
3. **Given** 用户在素材库中,**When** 用户点击"来源"筛选器选择"AI生成",**Then** 只显示AI生成的素材
|
||||
4. **Given** 用户选中一个素材,**When** 用户查看右侧检查面板,**Then** 应该显示素材的详细信息(名称、类型、来源、创建时间、预览图)
|
||||
5. **Given** 用户在素材库中有20个素材,**When** 用户在搜索框输入素材名称的关键词,**Then** 只显示匹配搜索关键词的素材
|
||||
|
||||
---
|
||||
|
||||
### 用户场景 2 - 从素材库选择参考图片用于AI生成 (Priority: P1)
|
||||
|
||||
作为AI生图/生视频的用户,我需要在生成时从素材库中快速选择已有的图片作为参考图,而不是每次都从本地文件夹中寻找,以便提高创作效率。
|
||||
|
||||
**此优先级的原因**: 这是本功能的主要业务价值 - 打通素材库与AI生成流程,让用户可以复用已有素材。这直接解决了用户的痛点(每次都要从本地选择文件)。
|
||||
|
||||
**独立测试**: 可以通过在AI生图/生视频对话框中点击选择参考图按钮,验证能够打开素材库选择界面,选中素材后能够正确应用到生成参数中。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **Given** 用户在AI生图对话框中,**When** 用户点击"选择参考图"按钮,**Then** 应该弹出选择来源的菜单("从素材库选择" 和 "从本地文件选择")
|
||||
2. **Given** 用户选择了"从素材库选择"选项,**When** 选择操作完成,**Then** 应该打开素材库弹窗并且只显示图片类型的素材
|
||||
3. **Given** 用户在素材库弹窗中,**When** 用户双击某个图片素材或点击"使用到画板"按钮,**Then** 该素材应该被应用为参考图,弹窗关闭,并在AI生成界面显示该参考图
|
||||
4. **Given** 用户在素材库弹窗中,**When** 用户点击"选择本地文件"按钮,**Then** 应该打开系统文件选择对话框,用户可以从本地上传新图片
|
||||
5. **Given** 用户在AI生视频对话框中选择参考图,**When** 打开素材库,**Then** 只应该显示图片类型的素材(视频不能作为参考图)
|
||||
|
||||
---
|
||||
|
||||
### 用户场景 3 - 上传新素材到素材库 (Priority: P2)
|
||||
|
||||
作为用户,我需要能够直接向素材库上传本地的图片和视频文件,以便统一管理我的所有素材资源。
|
||||
|
||||
**此优先级的原因**: 这是素材库的重要功能,但优先级低于查看和选择功能。用户可以通过AI生成界面间接上传(场景2中的"选择本地文件"),但直接在素材库上传能提供更好的体验。
|
||||
|
||||
**独立测试**: 可以通过打开素材库,点击上传按钮或拖拽文件到界面,验证文件能够成功添加到素材库并正确显示。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **Given** 用户在素材库界面,**When** 用户点击"上传"按钮并选择本地图片文件,**Then** 该文件应该被添加到素材库,标记为"本地上传"来源
|
||||
2. **Given** 用户在素材库界面,**When** 用户从桌面拖拽一个图片文件到素材库区域,**Then** 该文件应该被上传并添加到素材库
|
||||
3. **Given** 用户拖拽文件到素材库区域,**When** 拖拽进行中,**Then** 应该显示拖放区域的视觉提示
|
||||
4. **Given** 用户上传了一个文件,**When** 上传完成,**Then** 新素材应该出现在素材列表的最前面(最新创建)
|
||||
|
||||
---
|
||||
|
||||
### 用户场景 4 - 管理素材(重命名、删除、下载) (Priority: P2)
|
||||
|
||||
作为用户,我需要能够对素材进行基本的管理操作(重命名、删除、下载),以便保持素材库的整洁和组织性。
|
||||
|
||||
**此优先级的原因**: 这些是素材管理的辅助功能,对于用户体验很重要,但不影响核心的查看和选择流程。
|
||||
|
||||
**独立测试**: 可以通过选中素材,验证能够成功执行重命名、删除和下载操作,并且操作结果正确反映在素材库中。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **Given** 用户选中了一个素材,**When** 用户点击名称旁边的编辑按钮并输入新名称,**Then** 素材名称应该被更新
|
||||
2. **Given** 用户选中了一个素材,**When** 用户点击"删除"按钮,**Then** 应该弹出确认对话框,提示"删除后无法恢复,确认删除该素材?"
|
||||
3. **Given** 用户在删除确认对话框中,**When** 用户点击"确认"按钮,**Then** 该素材应该从素材库中移除
|
||||
4. **Given** 用户在删除确认对话框中,**When** 用户点击"取消"按钮,**Then** 对话框关闭,素材保留不被删除
|
||||
5. **Given** 用户选中了一个素材,**When** 用户点击"下载"按钮,**Then** 该素材文件应该被下载到用户的本地设备
|
||||
6. **Given** 用户在AI生成对话框中已经使用了某个素材作为参考图,**When** 该素材在素材库中被删除,**Then** AI生成对话框中的参考图仍然可以使用(因为已经加载)
|
||||
|
||||
---
|
||||
|
||||
### 用户场景 5 - AI生成的内容自动进入素材库 (Priority: P1)
|
||||
|
||||
作为用户,当我使用AI生成图片或视频时,生成的内容应该自动保存到素材库中,以便我后续可以复用这些内容。
|
||||
|
||||
**此优先级的原因**: 这是素材库与现有AI生成功能集成的关键,确保用户不会丢失生成的内容,并且能够积累素材资产。这是P1因为它是素材库价值的基础。
|
||||
|
||||
**独立测试**: 可以通过执行AI生成任务,验证生成成功后的图片/视频能够自动出现在素材库中,并标记为"AI生成"来源。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **Given** 用户通过AI生图功能成功生成了一张图片,**When** 生成完成,**Then** 该图片应该自动添加到素材库,来源标记为"AI生成"
|
||||
2. **Given** 用户通过AI生视频功能成功生成了一个视频,**When** 生成完成,**Then** 该视频应该自动添加到素材库,来源标记为"AI生成"
|
||||
3. **Given** 用户生成的图片名称为空或使用默认名称,**When** 添加到素材库,**Then** 应该使用生成时的提示词前20个字符作为名称,或使用"AI图片-日期时间"格式
|
||||
4. **Given** AI生成任务失败,**When** 任务失败,**Then** 不应该在素材库中创建素材条目
|
||||
5. **Given** 用户的素材库中已有100个素材,**When** 新的AI生成内容添加,**Then** 应该正常添加(不设置数量限制,但需要考虑浏览器存储限制)
|
||||
|
||||
---
|
||||
|
||||
### 边界情况
|
||||
|
||||
- 素材库中没有任何素材时,显示什么内容?(显示空状态提示,引导用户上传或生成素材)
|
||||
- 用户上传的文件格式不支持(非图片/视频)时如何处理?(显示错误提示,只接受图片和视频格式)
|
||||
- 用户上传的文件过大(如视频超过100MB)时如何处理?(显示警告,但仍然允许上传到浏览器存储)
|
||||
- 浏览器存储空间不足时如何处理?(捕获存储错误,提示用户清理旧素材或下载后删除;同时在界面底部显示存储使用进度条,80%时主动警告)
|
||||
- 用户在AI生成界面选择了参考图后又取消操作,素材库应该如何关闭?(点击关闭按钮或蒙层关闭素材库弹窗)
|
||||
- 素材的缩略图加载失败时如何显示?(显示占位图标或默认图标)
|
||||
- 用户快速连续生成多个AI内容时,素材库是否能够正确同步?(通过监听任务队列状态,确保所有成功的生成结果都能添加)
|
||||
- 用户刷新页面后,素材库数据是否能够持久化?(使用IndexedDB持久化存储)
|
||||
- 素材库弹窗在移动端如何显示?(响应式布局,在小屏幕上调整为单列布局,侧边栏可折叠)
|
||||
- 用户删除素材后能否撤销?(当前版本不支持撤销,通过确认对话框防止误删除)
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: 系统必须提供一个素材库弹窗组件,用于展示所有用户的素材(图片和视频)
|
||||
- **FR-001-A**: 素材库弹窗只能通过AI生成对话框(AI生图/生视频)中的"选择参考图"功能打开,不提供独立的主界面入口
|
||||
- **FR-002**: 系统必须支持按类型筛选素材(全部、图片、视频)
|
||||
- **FR-003**: 系统必须支持按来源筛选素材(全部、本地上传、AI生成)
|
||||
- **FR-004**: 系统必须支持通过名称搜索素材(实时搜索,不区分大小写)
|
||||
- **FR-005**: 系统必须支持按日期排序素材(最新优先、最旧优先)和按名称排序(A-Z)
|
||||
- **FR-006**: 系统必须在用户点击素材时显示详细信息(名称、类型、来源、创建时间、预览)
|
||||
- **FR-007**: 系统必须支持用户重命名素材
|
||||
- **FR-008**: 系统必须支持用户删除素材,删除前必须显示确认对话框警告操作不可撤销
|
||||
- **FR-009**: 系统必须支持用户下载素材到本地设备
|
||||
- **FR-010**: 系统必须支持用户通过点击按钮上传本地图片或视频文件
|
||||
- **FR-011**: 系统必须支持用户通过拖拽方式上传本地文件到素材库
|
||||
- **FR-012**: 系统必须在AI生图/生视频对话框的"选择参考图"功能中集成素材库选择选项
|
||||
- **FR-013**: 当用户选择"从素材库选择"时,系统必须打开素材库弹窗并默认筛选为图片类型
|
||||
- **FR-014**: 用户在素材库中双击素材或点击"使用到画板"按钮时,系统必须将该素材应用到AI生成参数中
|
||||
- **FR-015**: 系统必须在AI生成任务成功完成时自动将生成的图片/视频添加到素材库
|
||||
- **FR-016**: 自动添加的AI生成素材必须标记来源为"AI生成",并使用合适的默认名称
|
||||
- **FR-017**: 系统必须使用浏览器本地存储(IndexedDB)持久化素材数据
|
||||
- **FR-018**: 系统必须显示素材库中的素材总数
|
||||
- **FR-019**: 系统必须在素材上显示类型标识(图片图标或视频图标)
|
||||
- **FR-020**: 系统必须在AI生成的素材上显示"AI"标识
|
||||
- **FR-021**: 素材库界面必须支持响应式布局,在不同屏幕尺寸下可用
|
||||
- **FR-022**: 系统必须在上传文件时验证文件类型(只接受图片和视频格式)
|
||||
- **FR-023**: 系统必须在拖拽文件进入素材库区域时显示视觉反馈
|
||||
- **FR-024**: 系统必须处理存储错误并向用户显示友好的错误提示
|
||||
- **FR-024-A**: 系统必须在素材库界面底部显示浏览器存储空间使用进度条
|
||||
- **FR-024-B**: 系统必须在存储空间使用量达到80%时显示警告消息,提示用户清理空间(删除或下载后删除旧素材)
|
||||
- **FR-025**: 素材库弹窗必须提供关闭功能(点击关闭按钮或点击蒙层)
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **素材 (Asset)**: 代表用户的媒体资源
|
||||
- 唯一标识符 (id)
|
||||
- 类型 (type): IMAGE 或 VIDEO
|
||||
- 来源 (source): LOCAL(本地上传)或 AI_GENERATED(AI生成)
|
||||
- 存储位置 (url): Blob URL 用于浏览器中显示
|
||||
- 名称 (name): 用户可见的素材名称
|
||||
- 创建时间 (createdAt): 时间戳
|
||||
- MIME类型 (mimeType): 文件的媒体类型
|
||||
|
||||
- **素材库状态 (AssetStore)**: 管理所有素材的集合和操作
|
||||
- 素材列表 (assets): 所有素材的数组
|
||||
- 添加素材 (addAsset): 添加新素材到库中
|
||||
- 删除素材 (removeAsset): 从库中移除素材
|
||||
- 重命名素材 (renameAsset): 更新素材名称
|
||||
- 按类型获取 (getAssetsByType): 筛选特定类型的素材
|
||||
|
||||
- **筛选器状态 (FilterState)**: 控制素材库的显示筛选
|
||||
- 活动类型 (activeType): 当前选中的类型筛选器
|
||||
- 活动来源 (activeSource): 当前选中的来源筛选器
|
||||
- 搜索查询 (searchQuery): 搜索关键词
|
||||
- 排序方式 (sortBy): 排序规则
|
||||
|
||||
- **选择模式 (SelectionMode)**: 素材库的使用场景
|
||||
- 浏览模式: 用户打开素材库仅查看和管理素材
|
||||
- 选择模式: 从AI生成界面打开,需要选择素材作为参考图
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: 用户可以在3秒内从素材库中找到并选择所需的素材(通过搜索和筛选)
|
||||
- **SC-002**: 用户从AI生成界面选择参考图的操作步骤从2步(打开文件选择器→选择文件)减少到2步(打开素材库→选择素材),但选择体验更流畅
|
||||
- **SC-003**: 90%的AI生成内容能够自动保存到素材库并可在后续访问
|
||||
- **SC-004**: 素材库支持存储至少100个素材而不影响界面响应速度(初始加载时间<2秒,筛选和搜索响应<500ms)
|
||||
- **SC-005**: 用户上传素材的成功率达到95%以上(排除文件格式错误)
|
||||
- **SC-006**: 素材库界面在桌面端(1920x1080)和移动端(375x667)都能正常使用
|
||||
- **SC-007**: 用户能够在5秒内完成素材的重命名或删除操作
|
||||
- **SC-008**: 素材数据在浏览器关闭后能够100%持久化保存(使用IndexedDB)
|
||||
- **SC-009**: 从素材库选择参考图的用户使用率在功能上线后达到40%以上(相对于从本地选择)
|
||||
- **SC-010**: 用户对素材库功能的满意度评分达到4.0/5.0以上
|
||||
|
||||
## Assumptions *(mandatory)*
|
||||
|
||||
- 所有素材数据存储在浏览器的IndexedDB中,不涉及服务器端存储
|
||||
- 用户使用现代浏览器(Chrome, Firefox, Safari, Edge最新版本),支持IndexedDB和Blob URL
|
||||
- 素材文件以Blob形式存储在浏览器中,存储容量取决于浏览器限制(通常为50MB-500MB)
|
||||
- AI生成的图片和视频已经由现有的生成服务处理,素材库只需要接收结果并存储
|
||||
- 素材的缩略图使用原始文件的URL直接显示,不需要单独生成缩略图
|
||||
- 用户不需要对素材进行高级编辑(如裁剪、旋转),只需要选择和使用
|
||||
- 素材库的界面设计参考asset-manager项目的MediaLibraryModal组件风格
|
||||
- 视频素材在网格视图中不会自动播放,只在选中后的检查面板中可以播放
|
||||
- 素材库中的素材删除是永久性的,不提供回收站功能
|
||||
- 用户的素材不会在不同设备或浏览器之间同步(纯本地存储)
|
||||
|
||||
## Dependencies *(mandatory)*
|
||||
|
||||
- 依赖现有的IndexedDB存储服务或需要创建新的素材存储服务
|
||||
- 依赖现有的AI图片生成服务 (generation-api-service.ts) 和视频生成服务 (video-api-service.ts)
|
||||
- 依赖现有的任务队列服务 (task-queue-service.ts) 来监听AI生成任务的完成状态
|
||||
- 依赖TDesign React UI组件库用于界面元素
|
||||
- 依赖现有的AI生图对话框组件 (ai-image-generation.tsx) 和生视频对话框组件 (ai-video-generation.tsx)
|
||||
- 可能依赖现有的media-cache-service.ts用于素材缓存管理
|
||||
- 需要DrawnixContext支持素材库弹窗的打开/关闭状态管理
|
||||
|
||||
## Out of Scope *(mandatory)*
|
||||
|
||||
- 素材的云端存储和跨设备同步
|
||||
- 素材的高级编辑功能(裁剪、旋转、滤镜等)
|
||||
- 素材的批量操作(批量删除、批量下载、批量重命名)
|
||||
- 素材的文件夹/分类管理(标签、收藏夹等)
|
||||
- 素材的版本历史管理
|
||||
- 素材的共享功能(分享给其他用户)
|
||||
- 素材的权限管理(公开/私有)
|
||||
- 自动为视频生成缩略图(使用视频第一帧或中间帧)
|
||||
- 素材的详细元数据编辑(描述、标签、评分等)
|
||||
- 素材库的导出/导入功能(打包所有素材)
|
||||
- 素材的使用统计(被引用次数、使用频率等)
|
||||
- 自动素材清理建议(基于使用频率或年龄的智能清理推荐)
|
||||
345
specs/009-media-library/tasks.md
Normal file
345
specs/009-media-library/tasks.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# Tasks: 素材管理库 (Media Library)
|
||||
|
||||
**Input**: Design documents from `/specs/009-media-library/`
|
||||
**Prerequisites**: plan.md, spec.md, data-model.md, contracts/asset-storage-service.md, research.md, quickstart.md
|
||||
|
||||
**Tests**: Tests are NOT explicitly requested in the specification, therefore NO test tasks are included.
|
||||
|
||||
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
- **[P]**: Can run in parallel (different files, no dependencies)
|
||||
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3, US4, US5)
|
||||
- Include exact file paths in descriptions
|
||||
|
||||
## Path Conventions
|
||||
|
||||
- Monorepo structure: `packages/drawnix/src/` for source code
|
||||
- Components: `packages/drawnix/src/components/media-library/`
|
||||
- Services: `packages/drawnix/src/services/`
|
||||
- Types: `packages/drawnix/src/types/`
|
||||
- Contexts: `packages/drawnix/src/contexts/`
|
||||
- Utils: `packages/drawnix/src/utils/`
|
||||
- Constants: `packages/drawnix/src/constants/`
|
||||
- Hooks: `packages/drawnix/src/hooks/`
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup (Shared Infrastructure)
|
||||
|
||||
**Purpose**: Project initialization and basic structure for media library
|
||||
|
||||
- [x] T001 Create directory structure for media library at packages/drawnix/src/components/media-library/
|
||||
- [x] T002 [P] Create constants file at packages/drawnix/src/constants/ASSET_CONSTANTS.ts
|
||||
- [x] T003 [P] Export media library components from packages/drawnix/src/components/media-library/index.ts
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Core data types and storage service that MUST be complete before ANY user story can be implemented
|
||||
|
||||
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
|
||||
|
||||
- [x] T004 [P] Define AssetType and AssetSource enums in packages/drawnix/src/types/asset.types.ts
|
||||
- [x] T005 [P] Define Asset interface in packages/drawnix/src/types/asset.types.ts
|
||||
- [x] T006 [P] Define StoredAsset interface and conversion functions in packages/drawnix/src/types/asset.types.ts
|
||||
- [x] T007 [P] Define FilterState, SelectionMode, and related types in packages/drawnix/src/types/asset.types.ts
|
||||
- [x] T008 [P] Define StorageQuota and StorageStatus types in packages/drawnix/src/types/asset.types.ts
|
||||
- [x] T009 [P] Define AssetContextState and AssetContextActions interfaces in packages/drawnix/src/types/asset.types.ts
|
||||
- [x] T010 [P] Define component props interfaces (MediaLibraryModalProps, AssetGridItemProps, etc.) in packages/drawnix/src/types/asset.types.ts
|
||||
- [x] T011 Create factory function createAsset in packages/drawnix/src/types/asset.types.ts
|
||||
- [x] T012 Implement AssetStorageService class with initialize method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T013 Implement addAsset method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T014 Implement getAllAssets method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T015 Implement getAssetById method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T016 Implement renameAsset method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T017 Implement removeAsset method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T018 Implement clearAll method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T019 Implement checkQuota method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T020 Implement canAddAsset method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T021 Implement getStorageStats method in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T022 Add error classes (AssetStorageError, QuotaExceededError, NotFoundError, ValidationError) in packages/drawnix/src/services/asset-storage-service.ts
|
||||
- [x] T023 [P] Create validation functions (validateAssetName, validateMimeType, getAssetType) in packages/drawnix/src/utils/asset-utils.ts
|
||||
- [x] T024 [P] Create filterAssets function in packages/drawnix/src/utils/asset-utils.ts
|
||||
- [x] T025 [P] Create storage quota utility functions in packages/drawnix/src/utils/storage-quota.ts
|
||||
- [x] T026 Create AssetContext with state management in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
- [x] T027 Implement useAssets hook in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
- [x] T028 Integrate AssetProvider in packages/drawnix/src/drawnix.tsx
|
||||
|
||||
**Checkpoint**: ✅ Foundation ready - user story implementation can now begin in parallel
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - 浏览和管理已有素材 (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: 用户能够通过AI生成对话框访问素材库,查看所有历史素材(AI生成和本地上传的图片/视频),并支持基本的浏览和分类查看功能
|
||||
|
||||
**Independent Test**: 在AI生成对话框中点击"选择参考图"按钮打开素材库界面,验证能够显示所有历史素材,支持按类型和来源筛选
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T029 [P] [US1] Create MediaLibraryModal container component in packages/drawnix/src/components/media-library/MediaLibraryModal.tsx
|
||||
- [ ] T030 [P] [US1] Create MediaLibraryModal styles with BEM naming in packages/drawnix/src/components/media-library/MediaLibraryModal.scss
|
||||
- [ ] T031 [P] [US1] Create MediaLibraryGrid component in packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx
|
||||
- [ ] T032 [P] [US1] Create MediaLibraryGrid styles in packages/drawnix/src/components/media-library/MediaLibraryGrid.scss
|
||||
- [ ] T033 [P] [US1] Create AssetGridItem component with React.memo in packages/drawnix/src/components/media-library/AssetGridItem.tsx
|
||||
- [ ] T034 [P] [US1] Create AssetGridItem styles with BEM naming in packages/drawnix/src/components/media-library/AssetGridItem.scss
|
||||
- [ ] T035 [P] [US1] Create MediaLibraryEmpty component for empty state in packages/drawnix/src/components/media-library/MediaLibraryEmpty.tsx
|
||||
- [ ] T036 [P] [US1] Create MediaLibrarySidebar component with type and source filters in packages/drawnix/src/components/media-library/MediaLibrarySidebar.tsx
|
||||
- [ ] T037 [P] [US1] Create MediaLibrarySidebar styles in packages/drawnix/src/components/media-library/MediaLibrarySidebar.scss
|
||||
- [ ] T038 [P] [US1] Create MediaLibraryStorageBar component in packages/drawnix/src/components/media-library/MediaLibraryStorageBar.tsx
|
||||
- [ ] T039 [US1] Implement filter state management in AssetContext (setFilters method) in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
- [ ] T040 [US1] Implement selected asset state management in AssetContext (setSelectedAssetId method) in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
- [ ] T041 [US1] Implement storage quota checking in AssetContext (checkStorageQuota method) in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
- [ ] T042 [US1] Add search functionality to filters in MediaLibrarySidebar in packages/drawnix/src/components/media-library/MediaLibrarySidebar.tsx
|
||||
- [ ] T043 [US1] Add sort options to MediaLibrarySidebar (DATE_DESC, DATE_ASC, NAME_ASC) in packages/drawnix/src/components/media-library/MediaLibrarySidebar.tsx
|
||||
- [ ] T044 [US1] Implement responsive layout for mobile in MediaLibraryModal.scss in packages/drawnix/src/components/media-library/MediaLibraryModal.scss
|
||||
|
||||
**Checkpoint**: At this point, User Story 1 should be fully functional - users can open media library, view all assets, and filter by type/source
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - 从素材库选择参考图片用于AI生成 (Priority: P1)
|
||||
|
||||
**Goal**: 用户可以在AI生图/生视频对话框中从素材库快速选择已有图片作为参考图,而不是每次都从本地文件夹中寻找
|
||||
|
||||
**Independent Test**: 在AI生图/生视频对话框中点击选择参考图按钮,验证能够打开素材库选择界面,选中素材后能够正确应用到生成参数中
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [x] T045 [P] [US2] Create MediaLibraryInspector component for asset details in packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx
|
||||
- [x] T046 [P] [US2] Create MediaLibraryInspector styles in packages/drawnix/src/components/media-library/MediaLibraryInspector.scss
|
||||
- [x] T047 [US2] Add onSelect callback handling in MediaLibraryModal in packages/drawnix/src/components/media-library/MediaLibraryModal.tsx
|
||||
- [x] T048 [US2] Add filterType prop support to MediaLibraryModal for filtering only images in packages/drawnix/src/components/media-library/MediaLibraryModal.tsx
|
||||
- [x] T049 [US2] Implement double-click to select in AssetGridItem in packages/drawnix/src/components/media-library/AssetGridItem.tsx
|
||||
- [x] T050 [US2] Add "使用到画板" button in MediaLibraryInspector in packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx
|
||||
- [x] T051 [US2] Modify AI image generation dialog to add media library selection option in packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx
|
||||
- [x] T052 [US2] Add state management for showMediaLibrary and referenceImage in ai-image-generation.tsx in packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx
|
||||
- [x] T053 [US2] Integrate MediaLibraryModal with onSelect callback in ai-image-generation.tsx in packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx
|
||||
- [x] T054 [US2] Add source selector buttons (从素材库选择 / 从本地选择) in ai-image-generation.tsx in packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx
|
||||
- [x] T055 [US2] Modify AI video generation dialog to add media library selection option in packages/drawnix/src/components/ttd-dialog/ai-video-generation.tsx
|
||||
- [x] T056 [US2] Integrate MediaLibraryModal with filterType=IMAGE in ai-video-generation.tsx in packages/drawnix/src/components/ttd-dialog/ai-video-generation.tsx
|
||||
|
||||
**Checkpoint**: At this point, User Stories 1 AND 2 should both work - users can select assets from library as reference images in AI generation dialogs
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - 上传新素材到素材库 (Priority: P2)
|
||||
|
||||
**Goal**: 用户能够直接向素材库上传本地的图片和视频文件,以便统一管理所有素材资源
|
||||
|
||||
**Independent Test**: 打开素材库,点击上传按钮或拖拽文件到界面,验证文件能够成功添加到素材库并正确显示
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [x] T057 [P] [US3] Add file upload button to MediaLibraryModal toolbar in packages/drawnix/src/components/media-library/MediaLibraryModal.tsx
|
||||
- [x] T058 [P] [US3] Create file input handler for local file selection in MediaLibraryModal in packages/drawnix/src/components/media-library/MediaLibraryModal.tsx
|
||||
- [x] T059 [US3] Implement drag and drop functionality in MediaLibraryGrid in packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx
|
||||
- [x] T060 [US3] Add drag-over visual feedback styling in MediaLibraryGrid.scss in packages/drawnix/src/components/media-library/MediaLibraryGrid.scss
|
||||
- [x] T061 [US3] Implement file validation (type and MIME) before upload in packages/drawnix/src/utils/asset-utils.ts
|
||||
- [x] T062 [US3] Add magic number validation for security in packages/drawnix/src/utils/asset-utils.ts
|
||||
- [x] T063 [US3] Handle file upload errors and display user-friendly messages in MediaLibraryModal in packages/drawnix/src/components/media-library/MediaLibraryModal.tsx
|
||||
- [ ] T064 [US3] Add "从本地文件选择" button in MediaLibraryInspector for quick upload in packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx
|
||||
- [x] T065 [US3] Ensure newly uploaded assets appear at the top of the list (sort by createdAt DESC) in MediaLibraryGrid in packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx
|
||||
|
||||
**Checkpoint**: User Stories 1, 2, AND 3 should all work independently - users can upload new assets to the library
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: User Story 4 - 管理素材(重命名、删除、下载) (Priority: P2)
|
||||
|
||||
**Goal**: 用户能够对素材进行基本的管理操作(重命名、删除、下载),以便保持素材库的整洁和组织性
|
||||
|
||||
**Independent Test**: 选中素材,验证能够成功执行重命名、删除和下载操作,并且操作结果正确反映在素材库中
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [x] T066 [P] [US4] Add rename functionality with inline edit in MediaLibraryInspector in packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx
|
||||
- [x] T067 [P] [US4] Add download button and downloadAsset function in MediaLibraryInspector in packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx
|
||||
- [x] T068 [P] [US4] Implement downloadAsset utility function in packages/drawnix/src/utils/asset-utils.ts
|
||||
- [x] T069 [US4] Add delete button with confirmation dialog in MediaLibraryInspector in packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx
|
||||
- [x] T070 [US4] Create delete confirmation dialog component using TDesign Dialog in packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx
|
||||
- [x] T071 [US4] Implement removeAsset in AssetContext with error handling in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
- [x] T072 [US4] Implement renameAsset in AssetContext with validation in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
- [x] T073 [US4] Add success and error messages using TDesign MessagePlugin in packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx
|
||||
- [x] T074 [US4] Handle case where asset is already used as reference image in AI dialog (should still work) in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
|
||||
**Checkpoint**: All management operations (rename, delete, download) should work correctly
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: User Story 5 - AI生成的内容自动进入素材库 (Priority: P1)
|
||||
|
||||
**Goal**: 当用户使用AI生成图片或视频时,生成的内容应该自动保存到素材库中,以便后续可以复用这些内容
|
||||
|
||||
**Independent Test**: 执行AI生成任务,验证生成成功后的图片/视频能够自动出现在素材库中,并标记为"AI生成"来源
|
||||
|
||||
### Implementation for User Story 5
|
||||
|
||||
- [x] T075 [P] [US5] Create asset-integration-service.ts for auto-save integration in packages/drawnix/src/services/asset-integration-service.ts
|
||||
- [x] T076 [US5] Implement initializeAssetIntegration function to subscribe to task queue in packages/drawnix/src/services/asset-integration-service.ts
|
||||
- [x] T077 [US5] Implement generateAssetName function for AI-generated assets in packages/drawnix/src/services/asset-integration-service.ts
|
||||
- [x] T078 [US5] Handle task completion events and filter for completed image/video tasks in packages/drawnix/src/services/asset-integration-service.ts
|
||||
- [x] T079 [US5] Fetch result blob from task.resultUrl in packages/drawnix/src/services/asset-integration-service.ts
|
||||
- [x] T080 [US5] Call assetStorageService.addAsset with AI_GENERATED source in packages/drawnix/src/services/asset-integration-service.ts
|
||||
- [x] T081 [US5] Add savedToLibrary flag to task queue service to prevent duplicate saves in packages/drawnix/src/services/task-queue-service.ts
|
||||
- [x] T082 [US5] Implement markAsSaved method in task-queue-service.ts in packages/drawnix/src/services/task-queue-service.ts
|
||||
- [x] T083 [US5] Handle auto-save errors gracefully without blocking task queue in packages/drawnix/src/services/asset-integration-service.ts
|
||||
- [x] T084 [US5] Initialize asset integration service in drawnix.tsx on app startup in packages/drawnix/src/drawnix.tsx
|
||||
- [x] T085 [US5] Store AI generation metadata (prompt, modelName) with assets in packages/drawnix/src/services/asset-integration-service.ts
|
||||
- [x] T086 [US5] Display AI badge on AI-generated assets in AssetGridItem in packages/drawnix/src/components/media-library/AssetGridItem.tsx
|
||||
|
||||
**Checkpoint**: AI-generated content should automatically appear in media library after successful generation
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Improvements that affect multiple user stories and ensure production quality
|
||||
|
||||
- [x] T087 [P] Add loading states and skeletons to MediaLibraryGrid during asset loading in packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx
|
||||
- [ ] T088 [P] Add error boundary around MediaLibraryModal in packages/drawnix/src/components/media-library/MediaLibraryModal.tsx
|
||||
- [ ] T089 [P] Implement lazy loading for asset thumbnails using IntersectionObserver in packages/drawnix/src/components/media-library/AssetGridItem.tsx
|
||||
- [x] T090 [P] Add performance optimization with React.memo for all grid items in packages/drawnix/src/components/media-library/AssetGridItem.tsx
|
||||
- [x] T091 [P] Optimize filterAssets function with useMemo in MediaLibraryGrid in packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx
|
||||
- [x] T092 [P] Add storage warning at 80% usage in MediaLibraryStorageBar in packages/drawnix/src/components/media-library/MediaLibraryStorageBar.tsx
|
||||
- [ ] T093 [P] Add critical storage error at 95% usage preventing new uploads in packages/drawnix/src/contexts/AssetContext.tsx
|
||||
- [ ] T094 [P] Add keyboard shortcuts for asset selection (arrow keys, Enter, Delete) in MediaLibraryModal in packages/drawnix/src/components/media-library/MediaLibraryModal.tsx
|
||||
- [ ] T095 [P] Add accessibility attributes (ARIA labels, roles) to all interactive elements in packages/drawnix/src/components/media-library/
|
||||
- [ ] T096 [P] Ensure all TDesign components use light theme in packages/drawnix/src/components/media-library/
|
||||
- [x] T097 [P] Add declarative tracking data-track attributes to all buttons in packages/drawnix/src/components/media-library/
|
||||
- [x] T098 Code cleanup: Ensure all files are under 500 lines limit across packages/drawnix/src/components/media-library/
|
||||
- [x] T099 Code cleanup: Apply BEM naming consistently across all SCSS files in packages/drawnix/src/components/media-library/
|
||||
- [ ] T100 Run quickstart.md validation checklist and ensure all acceptance criteria pass
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: No dependencies - can start immediately
|
||||
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
|
||||
- **User Stories (Phase 3-7)**: All depend on Foundational phase completion
|
||||
- User stories can then proceed in parallel (if staffed)
|
||||
- Or sequentially in priority order (US1 → US2 → US5 → US3 → US4)
|
||||
- US1 and US5 are P1 (highest priority)
|
||||
- US3 and US4 are P2 (lower priority)
|
||||
- **Polish (Phase 8)**: Depends on all desired user stories being complete
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
|
||||
- **User Story 2 (P1)**: Depends on User Story 1 (needs MediaLibraryModal and grid components)
|
||||
- **User Story 3 (P2)**: Can start after Foundational (Phase 2) - Extends User Story 1 with upload
|
||||
- **User Story 4 (P2)**: Can start after Foundational (Phase 2) - Extends User Story 1 with management
|
||||
- **User Story 5 (P1)**: Can start after Foundational (Phase 2) - Independent integration with task queue
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Models/types before services
|
||||
- Services before components
|
||||
- Container components before presentation components
|
||||
- Core implementation before integration
|
||||
- Story complete before moving to next priority
|
||||
|
||||
### Parallel Opportunities
|
||||
|
||||
- **Phase 1**: All tasks marked [P] can run in parallel (T002, T003)
|
||||
- **Phase 2**: All type definition tasks (T004-T011) can run in parallel
|
||||
- **Phase 2**: Validation utilities (T023, T024, T025) can run in parallel
|
||||
- **Phase 3**: Component files (T029-T038) can be created in parallel
|
||||
- **Phase 4**: MediaLibraryInspector and dialog modifications (T045-T046, T051-T056) can run in parallel
|
||||
- **Phase 5**: File validation (T061, T062) can run in parallel with upload button (T057)
|
||||
- **Phase 6**: Rename, download, delete UI (T066-T068) can run in parallel
|
||||
- **Phase 7**: Asset integration service creation (T075-T077) can run in parallel
|
||||
- **Phase 8**: All polish tasks can run in parallel
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: User Story 1
|
||||
|
||||
```bash
|
||||
# Launch all UI component creation tasks together:
|
||||
Task T029: "Create MediaLibraryModal container component"
|
||||
Task T030: "Create MediaLibraryModal styles"
|
||||
Task T031: "Create MediaLibraryGrid component"
|
||||
Task T032: "Create MediaLibraryGrid styles"
|
||||
Task T033: "Create AssetGridItem component with React.memo"
|
||||
Task T034: "Create AssetGridItem styles"
|
||||
Task T035: "Create MediaLibraryEmpty component"
|
||||
Task T036: "Create MediaLibrarySidebar component"
|
||||
Task T037: "Create MediaLibrarySidebar styles"
|
||||
Task T038: "Create MediaLibraryStorageBar component"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First (User Stories 1, 2, 5 - All P1)
|
||||
|
||||
1. Complete Phase 1: Setup
|
||||
2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
|
||||
3. Complete Phase 3: User Story 1 (Browse and view assets)
|
||||
4. Complete Phase 4: User Story 2 (Select assets as reference images)
|
||||
5. Complete Phase 7: User Story 5 (Auto-save AI generated content)
|
||||
6. **STOP and VALIDATE**: Test P1 features independently
|
||||
7. Deploy/demo MVP
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Complete Setup + Foundational → Foundation ready
|
||||
2. Add User Story 1 → Test independently → Can browse assets
|
||||
3. Add User Story 2 → Test independently → Can select from library (MVP!)
|
||||
4. Add User Story 5 → Test independently → Auto-save works
|
||||
5. Add User Story 3 → Test independently → Can upload files
|
||||
6. Add User Story 4 → Test independently → Can manage assets
|
||||
7. Each story adds value without breaking previous stories
|
||||
|
||||
### Parallel Team Strategy
|
||||
|
||||
With multiple developers:
|
||||
|
||||
1. Team completes Setup + Foundational together
|
||||
2. Once Foundational is done:
|
||||
- Developer A: User Story 1 (browse)
|
||||
- Developer B: User Story 5 (auto-save integration)
|
||||
- Developer C: User Story 3 (upload)
|
||||
3. Then:
|
||||
- Developer A: User Story 2 (selection integration with dialogs)
|
||||
- Developer B: User Story 4 (management operations)
|
||||
4. Stories complete and integrate independently
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total Tasks**: 100
|
||||
- **Setup**: 3 tasks
|
||||
- **Foundational**: 25 tasks (BLOCKS all stories)
|
||||
- **User Story 1** (P1 - Browse): 16 tasks
|
||||
- **User Story 2** (P1 - Select): 12 tasks
|
||||
- **User Story 3** (P2 - Upload): 9 tasks
|
||||
- **User Story 4** (P2 - Manage): 9 tasks
|
||||
- **User Story 5** (P1 - Auto-save): 12 tasks
|
||||
- **Polish**: 14 tasks
|
||||
|
||||
**Parallel Opportunities**: 38 tasks marked with [P] can be executed in parallel within their phases
|
||||
|
||||
**Suggested MVP Scope**: Phase 1 + Phase 2 + Phase 3 + Phase 4 + Phase 7 (User Stories 1, 2, 5)
|
||||
|
||||
**Independent Test Criteria**:
|
||||
- US1: Can open media library, view all assets, filter by type/source
|
||||
- US2: Can select asset from library as reference image in AI dialogs
|
||||
- US3: Can upload new files via button or drag-and-drop
|
||||
- US4: Can rename, delete, and download assets
|
||||
- US5: AI-generated content automatically appears in library
|
||||
|
||||
---
|
||||
|
||||
**Tasks generated**: 2025-12-11
|
||||
**Feature**: 009-media-library
|
||||
**Ready for**: Implementation via `/speckit.implement` or manual execution
|
||||
Reference in New Issue
Block a user