diff --git a/.promptx/README.md b/.promptx/README.md new file mode 100644 index 0000000..17e9201 --- /dev/null +++ b/.promptx/README.md @@ -0,0 +1,215 @@ +# MixVideo 开发规范文档 + +本目录包含 MixVideo 项目的完整开发规范和指南,旨在确保代码质量、开发效率和团队协作的一致性。 + +## 📁 文档结构 + +### 核心规范文档 + +#### 1. [项目概览](./project-overview.md) +- 项目简介和技术架构 +- 核心功能模块介绍 +- 四层架构设计详解 +- 开发环境配置 +- 核心设计原则 + +#### 2. [前端开发规范](./frontend-coding-standards.md) +- React + TypeScript 开发规范 +- 组件设计模式和最佳实践 +- 状态管理 (Zustand) 规范 +- 服务层设计原则 +- 样式规范 (TailwindCSS) +- 自定义 Hooks 开发指南 +- 测试策略和规范 +- 性能优化技巧 + +#### 3. [后端开发规范](./backend-coding-standards.md) +- Rust + Tauri 开发规范 +- 四层架构实现指南 +- 数据模型设计原则 +- 仓库模式实现 +- 业务服务层设计 +- Tauri 命令开发规范 +- 错误处理策略 +- 测试和性能优化 + +#### 4. [数据库设计规范](./database-design-standards.md) +- SQLite 数据库设计规范 +- 表结构设计原则 +- 索引优化策略 +- 数据库迁移管理 +- 查询性能优化 +- 备份和恢复策略 +- 安全和审计规范 + +#### 5. [UI/UX 设计规范](./ui-ux-design-standards.md) +- 视觉设计系统 +- 组件设计规范 +- 布局和响应式设计 +- 交互设计原则 +- 动画和过渡效果 +- 无障碍设计指南 +- 性能优化规范 + +#### 6. [开发指南](./development-guide.md) +- 快速开始指南 +- 开发工作流程 +- 核心开发模式 +- 测试策略 +- 调试技巧 +- 性能优化 +- 常见问题解决 +- 部署指南 + +## 🎯 使用指南 + +### 新开发者入门 +1. 首先阅读 [项目概览](./project-overview.md) 了解项目整体架构 +2. 根据开发角色阅读对应的规范文档: + - 前端开发者:[前端开发规范](./frontend-coding-standards.md) + - 后端开发者:[后端开发规范](./backend-coding-standards.md) + - 全栈开发者:阅读所有技术规范 +3. 查看 [开发指南](./development-guide.md) 了解具体开发流程 +4. 参考 [UI/UX 设计规范](./ui-ux-design-standards.md) 确保界面一致性 + +### 日常开发参考 +- **开发新功能前**:查看相关规范确保设计符合项目标准 +- **代码审查时**:使用规范文档中的检查清单 +- **遇到问题时**:查看开发指南中的常见问题解决方案 +- **性能优化时**:参考各规范文档中的性能优化章节 + +### 规范更新 +- 规范文档会随着项目发展持续更新 +- 重大变更会在团队会议中讨论 +- 所有开发者都应关注规范文档的更新 + +## 🔧 技术栈概览 + +### 前端技术栈 +- **框架**: React 18 + TypeScript 5.8 +- **构建工具**: Vite 6.0 +- **状态管理**: Zustand 4.4 +- **UI框架**: TailwindCSS 3.4 +- **图标**: Lucide React + Heroicons +- **路由**: React Router DOM 6.20 +- **测试**: Vitest + Testing Library + +### 后端技术栈 +- **框架**: Tauri 2.0 + Rust 1.70+ +- **数据库**: SQLite (WAL模式) +- **AI集成**: Google Gemini API +- **多媒体**: FFmpeg +- **异步**: Tokio + async/await +- **序列化**: Serde + JSON + +### 开发工具 +- **包管理**: PNPM 8.15 (Workspace) +- **代码检查**: ESLint + Clippy +- **格式化**: Prettier + rustfmt +- **版本控制**: Git + GitHub + +## 📋 开发原则 + +### 1. 安全第一 +- 严格的输入验证 +- 安全的数据存储 +- 防范常见安全漏洞 +- 敏感信息保护 + +### 2. 性能优化 +- 数据库查询优化 +- 前端渲染优化 +- 内存使用优化 +- 网络请求优化 + +### 3. 用户体验 +- 响应式设计 +- 流畅的交互 +- 清晰的反馈 +- 无障碍支持 + +### 4. 代码质量 +- 清晰的架构设计 +- 完善的测试覆盖 +- 详细的文档说明 +- 一致的编码风格 + +### 5. 可维护性 +- 模块化设计 +- 松耦合架构 +- 标准化接口 +- 版本化管理 + +## 🚀 快速参考 + +### 常用命令 +```bash +# 开发环境 +pnpm tauri:dev + +# 构建应用 +pnpm tauri:build + +# 运行测试 +pnpm test + +# 代码检查 +pnpm lint + +# 格式化代码 +pnpm format +``` + +### 项目结构 +``` +mixvideo/ +├── .promptx/ # 开发规范文档 +├── apps/desktop/ # 主应用 +│ ├── src/ # React前端 +│ └── src-tauri/ # Rust后端 +├── docs/ # 项目文档 +└── packages/ # 共享包 +``` + +### 重要链接 +- [项目仓库](https://github.com/mixvideo/mixvideo) +- [问题追踪](https://github.com/mixvideo/mixvideo/issues) +- [发布记录](https://github.com/mixvideo/mixvideo/releases) +- [贡献指南](../CONTRIBUTING.md) + +## 📞 支持和反馈 + +### 获取帮助 +- **技术问题**: 在项目仓库创建 Issue +- **规范建议**: 通过 Pull Request 提交改进建议 +- **团队讨论**: 参加定期的技术分享会议 + +### 贡献规范 +1. Fork 项目仓库 +2. 创建功能分支 +3. 遵循开发规范 +4. 提交 Pull Request +5. 通过代码审查 + +### 联系方式 +- **项目负责人**: imeepos +- **技术讨论**: 项目 Discussions +- **紧急问题**: 项目 Issues + +## 📝 更新日志 + +### v1.0.0 (2024-01-01) +- 初始版本发布 +- 完整的开发规范体系 +- 四层架构设计指南 +- 前后端开发规范 +- UI/UX 设计系统 + +### 后续更新 +- 规范文档会根据项目发展持续更新 +- 重大变更会在版本发布说明中体现 +- 建议定期查看文档更新 + +--- + +**注意**: 本规范文档是项目开发的重要参考,所有开发者都应该熟悉并遵循这些规范。如有疑问或建议,欢迎通过项目仓库进行讨论。 diff --git a/.promptx/backend-coding-standards.md b/.promptx/backend-coding-standards.md new file mode 100644 index 0000000..bde0a5b --- /dev/null +++ b/.promptx/backend-coding-standards.md @@ -0,0 +1,950 @@ +# MixVideo 后端开发规范 (Rust/Tauri) + +## 技术栈规范 + +### 核心技术 +- **Tauri 2.0**: 跨平台桌面应用框架 +- **Rust 1.70+**: 系统编程语言 +- **SQLite**: 嵌入式数据库 (WAL模式) +- **Tokio**: 异步运行时 +- **Serde**: 序列化/反序列化 +- **anyhow/thiserror**: 错误处理 + +### 依赖管理 +```toml +[workspace.dependencies] +# Tauri 相关 +tauri = { version = "2.0", features = ["api-all"] } +tauri-build = { version = "2.0", features = [] } + +# 异步和并发 +tokio = { version = "1.0", features = ["full"] } +futures = "0.3" +async-trait = "0.1" + +# 序列化 +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# 错误处理 +anyhow = "1.0" +thiserror = "1.0" + +# 数据库 +rusqlite = { version = "0.29", features = ["bundled", "chrono"] } +chrono = { version = "0.4", features = ["serde"] } + +# 日志 +tracing = "0.1" +tracing-subscriber = "0.3" +``` + +## 四层架构设计 + +### 1. 基础设施层 (Infrastructure) +```rust +// src/infrastructure/mod.rs +pub mod database; +pub mod logging; +pub mod file_system; +pub mod gemini_service; +pub mod ffmpeg; +pub mod event_bus; +pub mod performance; + +// 基础设施层职责: +// - 数据库连接管理 +// - 外部服务集成 +// - 文件系统操作 +// - 日志和监控 +// - 事件总线 +``` + +### 2. 数据访问层 (Data) +```rust +// src/data/mod.rs +pub mod models; +pub mod repositories; + +// 数据访问层职责: +// - 数据模型定义 +// - 数据库操作封装 +// - 查询优化 +// - 事务管理 +``` + +### 3. 业务逻辑层 (Business) +```rust +// src/business/mod.rs +pub mod services; +pub mod errors; + +// 业务逻辑层职责: +// - 核心业务逻辑 +// - 业务规则验证 +// - 工作流编排 +// - 领域模型 +``` + +### 4. 表现层 (Presentation) +```rust +// src/presentation/mod.rs +pub mod commands; + +// 表现层职责: +// - Tauri命令定义 +// - 参数验证 +// - 响应格式化 +// - 错误转换 +``` + +## 代码组织规范 + +### 模块结构 +```rust +// lib.rs - 库入口 +extern crate lazy_static; + +// 四层架构模块 +pub mod infrastructure; +pub mod data; +pub mod business; +pub mod presentation; +pub mod services; + +// 应用配置 +pub mod app_state; +pub mod config; + +use app_state::AppState; +use presentation::commands; +use tauri::Manager; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_dialog::init()) + .manage(AppState::new()) + .invoke_handler(tauri::generate_handler![ + // 注册所有命令 + commands::project_commands::create_project, + commands::material_commands::get_all_materials, + // ... 其他命令 + ]) + .setup(|app| { + // 应用初始化逻辑 + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +### 文件命名规范 +- **模块文件**: snake_case (如 `material_service.rs`) +- **结构体**: PascalCase (如 `MaterialRepository`) +- **函数**: snake_case (如 `create_material`) +- **常量**: SCREAMING_SNAKE_CASE (如 `DEFAULT_PAGE_SIZE`) + +## 数据模型规范 + +### 实体模型设计 +```rust +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; + +/// 素材实体模型 +/// 遵循 Tauri 开发规范的数据模型设计原则 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Material { + pub id: String, + pub name: String, + pub path: String, + pub material_type: MaterialType, + pub size: u64, + pub duration: Option, + pub project_id: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub is_active: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MaterialType { + Video, + Audio, + Image, +} + +impl Material { + /// 创建新的素材实例 + pub fn new(request: CreateMaterialRequest) -> Self { + let now = Utc::now(); + Self { + id: uuid::Uuid::new_v4().to_string(), + name: request.name, + path: request.path, + material_type: request.material_type, + size: request.size, + duration: request.duration, + project_id: request.project_id, + created_at: now, + updated_at: now, + is_active: true, + } + } + + /// 验证素材数据 + pub fn validate(&self) -> Result<(), String> { + if self.name.trim().is_empty() { + return Err("素材名称不能为空".to_string()); + } + + if self.path.trim().is_empty() { + return Err("素材路径不能为空".to_string()); + } + + Ok(()) + } + + /// 更新素材信息 + pub fn update(&mut self, request: UpdateMaterialRequest) { + if let Some(name) = request.name { + self.name = name; + } + + if let Some(material_type) = request.material_type { + self.material_type = material_type; + } + + self.updated_at = Utc::now(); + } +} + +/// 创建素材请求 +#[derive(Debug, Deserialize)] +pub struct CreateMaterialRequest { + pub name: String, + pub path: String, + pub material_type: MaterialType, + pub size: u64, + pub duration: Option, + pub project_id: String, +} + +/// 更新素材请求 +#[derive(Debug, Deserialize)] +pub struct UpdateMaterialRequest { + pub name: Option, + pub material_type: Option, +} +``` + +### 模型设计原则 +1. **不可变性**: 优先使用不可变字段 +2. **验证**: 内置数据验证方法 +3. **序列化**: 支持JSON序列化 +4. **文档**: 详细的文档注释 +5. **测试**: 为模型编写单元测试 + +## 仓库模式规范 + +### 仓库接口设计 +```rust +use async_trait::async_trait; +use anyhow::Result; + +/// 素材仓库接口 +/// 定义素材数据访问的抽象接口 +#[async_trait] +pub trait MaterialRepositoryTrait { + async fn create(&self, material: &Material) -> Result<()>; + async fn get_by_id(&self, id: &str) -> Result>; + async fn get_all(&self) -> Result>; + async fn get_by_project_id(&self, project_id: &str) -> Result>; + async fn update(&self, material: &Material) -> Result<()>; + async fn delete(&self, id: &str) -> Result<()>; + async fn search(&self, query: &MaterialQueryParams) -> Result>; +} + +/// 素材仓库实现 +/// 遵循 Tauri 开发规范的仓库模式 +pub struct MaterialRepository { + database: Arc, +} + +impl MaterialRepository { + pub fn new(database: Arc) -> Self { + Self { database } + } +} + +#[async_trait] +impl MaterialRepositoryTrait for MaterialRepository { + async fn create(&self, material: &Material) -> Result<()> { + let conn = self.database.get_connection().await?; + + conn.execute( + r#" + INSERT INTO materials ( + id, name, path, material_type, size, duration, + project_id, created_at, updated_at, is_active + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) + "#, + params![ + material.id, + material.name, + material.path, + serde_json::to_string(&material.material_type)?, + material.size, + material.duration, + material.project_id, + material.created_at.to_rfc3339(), + material.updated_at.to_rfc3339(), + material.is_active, + ], + )?; + + Ok(()) + } + + async fn get_by_id(&self, id: &str) -> Result> { + let conn = self.database.get_read_connection().await?; + + let mut stmt = conn.prepare( + r#" + SELECT id, name, path, material_type, size, duration, + project_id, created_at, updated_at, is_active + FROM materials + WHERE id = ?1 AND is_active = 1 + "# + )?; + + let material = stmt.query_row(params![id], |row| { + Ok(Material { + id: row.get(0)?, + name: row.get(1)?, + path: row.get(2)?, + material_type: serde_json::from_str(&row.get::<_, String>(3)?) + .map_err(|e| rusqlite::Error::InvalidColumnType(3, "material_type".to_string(), rusqlite::types::Type::Text))?, + size: row.get(4)?, + duration: row.get(5)?, + project_id: row.get(6)?, + created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(7)?) + .map_err(|e| rusqlite::Error::InvalidColumnType(7, "created_at".to_string(), rusqlite::types::Type::Text))? + .with_timezone(&Utc), + updated_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(8)?) + .map_err(|e| rusqlite::Error::InvalidColumnType(8, "updated_at".to_string(), rusqlite::types::Type::Text))? + .with_timezone(&Utc), + is_active: row.get(9)?, + }) + }).optional()?; + + Ok(material) + } +} +``` + +### 仓库设计原则 +1. **接口分离**: 定义清晰的仓库接口 +2. **异步操作**: 所有数据库操作都是异步的 +3. **错误处理**: 统一的错误处理机制 +4. **连接管理**: 合理使用数据库连接 +5. **查询优化**: 优化SQL查询性能 + +## 业务服务规范 + +### 服务层设计 +```rust +use anyhow::{anyhow, Result}; +use crate::data::repositories::material_repository::MaterialRepository; +use crate::data::models::material::{Material, CreateMaterialRequest, UpdateMaterialRequest}; + +/// 素材管理服务 +/// 遵循 Tauri 开发规范的业务服务设计 +pub struct MaterialService; + +impl MaterialService { + /// 创建素材 + pub async fn create_material( + repository: &MaterialRepository, + request: CreateMaterialRequest, + ) -> Result { + // 业务验证 + if request.name.trim().is_empty() { + return Err(anyhow!("素材名称不能为空")); + } + + if !std::path::Path::new(&request.path).exists() { + return Err(anyhow!("素材文件不存在")); + } + + // 创建素材实例 + let material = Material::new(request); + + // 验证素材数据 + material.validate() + .map_err(|e| anyhow!("素材验证失败: {}", e))?; + + // 保存到数据库 + repository.create(&material).await + .map_err(|e| anyhow!("创建素材失败: {}", e))?; + + Ok(material) + } + + /// 获取素材详情 + pub async fn get_material_by_id( + repository: &MaterialRepository, + id: &str, + ) -> Result> { + repository.get_by_id(id).await + .map_err(|e| anyhow!("获取素材详情失败: {}", e)) + } + + /// 更新素材 + pub async fn update_material( + repository: &MaterialRepository, + id: &str, + request: UpdateMaterialRequest, + ) -> Result { + // 获取现有素材 + let mut material = repository.get_by_id(id).await? + .ok_or_else(|| anyhow!("素材不存在"))?; + + // 更新素材信息 + material.update(request); + + // 验证更新后的数据 + material.validate() + .map_err(|e| anyhow!("素材验证失败: {}", e))?; + + // 保存更新 + repository.update(&material).await + .map_err(|e| anyhow!("更新素材失败: {}", e))?; + + Ok(material) + } + + /// 删除素材 + pub async fn delete_material( + repository: &MaterialRepository, + id: &str, + ) -> Result<()> { + // 检查素材是否存在 + let material = repository.get_by_id(id).await? + .ok_or_else(|| anyhow!("素材不存在"))?; + + // 执行删除 + repository.delete(id).await + .map_err(|e| anyhow!("删除素材失败: {}", e))?; + + Ok(()) + } +} +``` + +### 服务设计原则 +1. **业务封装**: 封装复杂的业务逻辑 +2. **验证**: 完整的业务规则验证 +3. **事务**: 合理使用数据库事务 +4. **错误处理**: 详细的错误信息 +5. **测试**: 完整的单元测试覆盖 + +## Tauri命令规范 + +### 命令定义 +```rust +use tauri::{command, State}; +use crate::app_state::AppState; +use crate::business::services::material_service::MaterialService; +use crate::data::models::material::{Material, CreateMaterialRequest, UpdateMaterialRequest}; + +/// 创建素材命令 +/// 遵循 Tauri 开发规范的命令设计模式 +#[command] +pub async fn create_material( + state: State<'_, AppState>, + request: CreateMaterialRequest, +) -> Result { + let repository_guard = state.get_material_repository() + .map_err(|e| format!("获取素材仓库失败: {}", e))?; + + let repository = repository_guard.as_ref() + .ok_or("素材仓库未初始化")?; + + MaterialService::create_material(repository, request) + .await + .map_err(|e| e.to_string()) +} + +/// 获取所有素材命令 +#[command] +pub async fn get_all_materials( + state: State<'_, AppState>, +) -> Result, String> { + let repository_guard = state.get_material_repository() + .map_err(|e| format!("获取素材仓库失败: {}", e))?; + + let repository = repository_guard.as_ref() + .ok_or("素材仓库未初始化")?; + + repository.get_all() + .await + .map_err(|e| e.to_string()) +} + +/// 根据ID获取素材命令 +#[command] +pub async fn get_material_by_id( + state: State<'_, AppState>, + id: String, +) -> Result, String> { + let repository_guard = state.get_material_repository() + .map_err(|e| format!("获取素材仓库失败: {}", e))?; + + let repository = repository_guard.as_ref() + .ok_or("素材仓库未初始化")?; + + MaterialService::get_material_by_id(repository, &id) + .await + .map_err(|e| e.to_string()) +} + +/// 更新素材命令 +#[command] +pub async fn update_material( + state: State<'_, AppState>, + id: String, + request: UpdateMaterialRequest, +) -> Result { + let repository_guard = state.get_material_repository() + .map_err(|e| format!("获取素材仓库失败: {}", e))?; + + let repository = repository_guard.as_ref() + .ok_or("素材仓库未初始化")?; + + MaterialService::update_material(repository, &id, request) + .await + .map_err(|e| e.to_string()) +} + +/// 删除素材命令 +#[command] +pub async fn delete_material( + state: State<'_, AppState>, + id: String, +) -> Result<(), String> { + let repository_guard = state.get_material_repository() + .map_err(|e| format!("获取素材仓库失败: {}", e))?; + + let repository = repository_guard.as_ref() + .ok_or("素材仓库未初始化")?; + + MaterialService::delete_material(repository, &id) + .await + .map_err(|e| e.to_string()) +} +``` + +### 命令设计原则 +1. **参数验证**: 严格验证输入参数 +2. **错误转换**: 将内部错误转换为用户友好的消息 +3. **状态管理**: 正确使用Tauri状态管理 +4. **异步处理**: 所有命令都是异步的 +5. **文档**: 详细的命令文档 + +## 错误处理规范 + +### 错误类型定义 +```rust +use thiserror::Error; + +/// 业务错误类型 +#[derive(Error, Debug)] +pub enum BusinessError { + #[error("验证错误: {message}")] + ValidationError { message: String }, + + #[error("资源不存在: {resource}")] + NotFound { resource: String }, + + #[error("权限不足: {operation}")] + PermissionDenied { operation: String }, + + #[error("业务规则冲突: {rule}")] + BusinessRuleViolation { rule: String }, + + #[error("外部服务错误: {service} - {message}")] + ExternalServiceError { service: String, message: String }, +} + +/// 数据库错误类型 +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("连接错误: {0}")] + ConnectionError(#[from] rusqlite::Error), + + #[error("事务错误: {message}")] + TransactionError { message: String }, + + #[error("查询错误: {query}")] + QueryError { query: String }, + + #[error("数据完整性错误: {constraint}")] + IntegrityError { constraint: String }, +} +``` + +### 错误处理模式 +```rust +use anyhow::{Context, Result}; + +/// 带上下文的错误处理 +pub async fn process_material(id: &str) -> Result { + let material = repository.get_by_id(id) + .await + .with_context(|| format!("获取素材失败: {}", id))? + .ok_or_else(|| anyhow!("素材不存在: {}", id))?; + + validate_material(&material) + .with_context(|| "素材验证失败")?; + + Ok(material) +} + +/// 错误转换 +impl From for String { + fn from(error: BusinessError) -> Self { + match error { + BusinessError::ValidationError { message } => { + format!("输入验证失败: {}", message) + } + BusinessError::NotFound { resource } => { + format!("找不到资源: {}", resource) + } + _ => error.to_string(), + } + } +} +``` + +## 测试规范 + +### 单元测试模板 +```rust +#[cfg(test)] +mod tests { + use super::*; + use crate::infrastructure::database::Database; + use std::sync::Arc; + + async fn setup_test_database() -> Arc { + let db = Database::new_in_memory().unwrap(); + db.migrate().await.unwrap(); + Arc::new(db) + } + + #[tokio::test] + async fn test_create_material_success() { + // Arrange + let db = setup_test_database().await; + let repository = MaterialRepository::new(db); + let request = CreateMaterialRequest { + name: "Test Material".to_string(), + path: "/test/path".to_string(), + material_type: MaterialType::Video, + size: 1024, + duration: Some(60), + project_id: "project-1".to_string(), + }; + + // Act + let result = MaterialService::create_material(&repository, request).await; + + // Assert + assert!(result.is_ok()); + let material = result.unwrap(); + assert_eq!(material.name, "Test Material"); + assert_eq!(material.size, 1024); + } + + #[tokio::test] + async fn test_create_material_validation_error() { + // Arrange + let db = setup_test_database().await; + let repository = MaterialRepository::new(db); + let request = CreateMaterialRequest { + name: "".to_string(), // 空名称应该失败 + path: "/test/path".to_string(), + material_type: MaterialType::Video, + size: 1024, + duration: Some(60), + project_id: "project-1".to_string(), + }; + + // Act + let result = MaterialService::create_material(&repository, request).await; + + // Assert + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("名称不能为空")); + } +} +``` + +### 集成测试 +```rust +#[cfg(test)] +mod integration_tests { + use super::*; + use crate::app_state::AppState; + use tauri::test::{mock_app, MockRuntime}; + + #[tokio::test] + async fn test_material_crud_workflow() { + // 设置测试应用 + let app = mock_app(); + let state = AppState::new(); + + // 测试创建 + let create_request = CreateMaterialRequest { + name: "Integration Test Material".to_string(), + path: "/test/integration".to_string(), + material_type: MaterialType::Video, + size: 2048, + duration: Some(120), + project_id: "integration-project".to_string(), + }; + + let created = create_material(state.clone(), create_request).await.unwrap(); + + // 测试读取 + let retrieved = get_material_by_id(state.clone(), created.id.clone()).await.unwrap(); + assert!(retrieved.is_some()); + + // 测试更新 + let update_request = UpdateMaterialRequest { + name: Some("Updated Material".to_string()), + material_type: None, + }; + + let updated = update_material(state.clone(), created.id.clone(), update_request).await.unwrap(); + assert_eq!(updated.name, "Updated Material"); + + // 测试删除 + delete_material(state.clone(), created.id).await.unwrap(); + } +} +``` + +## 性能优化规范 + +### 数据库优化 +```rust +// 使用连接池 +pub struct Database { + pool: Arc, +} + +impl Database { + pub async fn get_connection(&self) -> Result { + self.pool.get().await + .map_err(|e| anyhow!("获取数据库连接失败: {}", e)) + } + + // 读写分离 + pub async fn get_read_connection(&self) -> Result { + self.pool.get_read_only().await + .map_err(|e| anyhow!("获取只读连接失败: {}", e)) + } +} + +// 批量操作优化 +impl MaterialRepository { + pub async fn batch_create(&self, materials: &[Material]) -> Result<()> { + let conn = self.database.get_connection().await?; + let tx = conn.transaction()?; + + for material in materials { + tx.execute( + "INSERT INTO materials (...) VALUES (...)", + // 参数 + )?; + } + + tx.commit()?; + Ok(()) + } +} +``` + +### 内存优化 +```rust +// 使用流式处理大量数据 +pub async fn process_large_dataset( + repository: &MaterialRepository, + processor: F, +) -> Result<()> +where + F: Fn(&Material) -> Result<()>, +{ + let mut offset = 0; + const BATCH_SIZE: usize = 100; + + loop { + let materials = repository.get_batch(offset, BATCH_SIZE).await?; + if materials.is_empty() { + break; + } + + for material in materials { + processor(&material)?; + } + + offset += BATCH_SIZE; + } + + Ok(()) +} +``` + +## 日志和监控规范 + +### 结构化日志 +```rust +use tracing::{info, warn, error, debug, instrument}; + +#[instrument(skip(repository))] +pub async fn create_material( + repository: &MaterialRepository, + request: CreateMaterialRequest, +) -> Result { + info!( + material_name = %request.name, + project_id = %request.project_id, + "开始创建素材" + ); + + let material = Material::new(request); + + match repository.create(&material).await { + Ok(_) => { + info!( + material_id = %material.id, + material_name = %material.name, + "素材创建成功" + ); + Ok(material) + } + Err(e) => { + error!( + error = %e, + material_name = %material.name, + "素材创建失败" + ); + Err(e) + } + } +} +``` + +### 性能监控 +```rust +use std::time::Instant; + +pub struct PerformanceMonitor { + metrics: Arc>>>, +} + +impl PerformanceMonitor { + pub fn record_operation(&self, operation: &str, f: F) -> R + where + F: FnOnce() -> R, + { + let start = Instant::now(); + let result = f(); + let duration = start.elapsed(); + + let mut metrics = self.metrics.lock().unwrap(); + metrics.entry(operation.to_string()) + .or_insert_with(Vec::new) + .push(duration); + + result + } +} +``` + +## 安全规范 + +### 输入验证 +```rust +use validator::{Validate, ValidationError}; + +#[derive(Debug, Deserialize, Validate)] +pub struct CreateMaterialRequest { + #[validate(length(min = 1, max = 255, message = "名称长度必须在1-255字符之间"))] + pub name: String, + + #[validate(custom = "validate_file_path")] + pub path: String, + + #[validate(range(min = 1, message = "文件大小必须大于0"))] + pub size: u64, +} + +fn validate_file_path(path: &str) -> Result<(), ValidationError> { + if !std::path::Path::new(path).exists() { + return Err(ValidationError::new("文件不存在")); + } + + // 防止路径遍历攻击 + if path.contains("..") { + return Err(ValidationError::new("非法路径")); + } + + Ok(()) +} +``` + +### SQL注入防护 +```rust +// ✅ 使用参数化查询 +conn.execute( + "SELECT * FROM materials WHERE name = ?1 AND project_id = ?2", + params![name, project_id], +)?; + +// ❌ 避免字符串拼接 +// let sql = format!("SELECT * FROM materials WHERE name = '{}'", name); +``` + +## 代码质量规范 + +### Clippy配置 +```toml +# Cargo.toml +[lints.clippy] +all = "warn" +pedantic = "warn" +nursery = "warn" +cargo = "warn" + +# 允许的lint +too_many_arguments = "allow" +module_name_repetitions = "allow" +``` + +### 代码审查清单 +- [ ] 是否遵循四层架构设计 +- [ ] 错误处理是否完整 +- [ ] 是否有适当的日志记录 +- [ ] 数据库操作是否优化 +- [ ] 是否有相应的测试覆盖 +- [ ] 文档注释是否完整 +- [ ] 是否符合Rust最佳实践 +- [ ] 输入验证是否完整 +- [ ] 是否存在安全漏洞 diff --git a/.promptx/database-design-standards.md b/.promptx/database-design-standards.md new file mode 100644 index 0000000..3bef5a6 --- /dev/null +++ b/.promptx/database-design-standards.md @@ -0,0 +1,579 @@ +# MixVideo 数据库设计规范 + +## 数据库技术栈 + +### 核心技术 +- **SQLite 3.40+**: 嵌入式数据库 +- **WAL模式**: 提高并发性能 +- **外键约束**: 保证数据一致性 +- **连接池**: 支持高并发访问 +- **读写分离**: 优化查询性能 + +### 数据库配置 +```sql +-- 启用WAL模式 +PRAGMA journal_mode = WAL; + +-- 启用外键约束 +PRAGMA foreign_keys = ON; + +-- 设置同步模式 +PRAGMA synchronous = NORMAL; + +-- 设置缓存大小 +PRAGMA cache_size = 10000; + +-- 设置超时时间 +PRAGMA busy_timeout = 5000; + +-- 启用安全删除 +PRAGMA secure_delete = ON; + +-- 设置临时存储 +PRAGMA temp_store = MEMORY; +``` + +## 表设计规范 + +### 命名规范 +- **表名**: snake_case复数形式 (如 `materials`, `projects`) +- **字段名**: snake_case (如 `created_at`, `project_id`) +- **索引名**: `idx_表名_字段名` (如 `idx_materials_project_id`) +- **外键名**: `fk_表名_引用表名` (如 `fk_materials_projects`) + +### 字段设计规范 +```sql +-- 主键设计 +id TEXT PRIMARY KEY NOT NULL, -- 使用UUID字符串 + +-- 时间戳字段 +created_at TEXT NOT NULL DEFAULT (datetime('now')), +updated_at TEXT NOT NULL DEFAULT (datetime('now')), + +-- 软删除字段 +is_active INTEGER NOT NULL DEFAULT 1, + +-- 外键字段 +project_id TEXT NOT NULL, +FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + +-- 枚举字段(使用TEXT存储JSON) +material_type TEXT NOT NULL CHECK (material_type IN ('video', 'audio', 'image')), + +-- 可选字段 +description TEXT, +duration INTEGER, -- 可为NULL + +-- 文件路径字段 +path TEXT NOT NULL, +size INTEGER NOT NULL CHECK (size > 0), +``` + +## 核心表结构 + +### 1. 项目表 (projects) +```sql +CREATE TABLE projects ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + path TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + is_active INTEGER NOT NULL DEFAULT 1 +); + +-- 索引 +CREATE INDEX idx_projects_name ON projects(name); +CREATE INDEX idx_projects_path ON projects(path); +CREATE INDEX idx_projects_created_at ON projects(created_at); +``` + +### 2. 素材表 (materials) +```sql +CREATE TABLE materials ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + material_type TEXT NOT NULL CHECK (material_type IN ('video', 'audio', 'image')), + size INTEGER NOT NULL CHECK (size > 0), + duration INTEGER, -- 视频/音频时长(秒) + width INTEGER, -- 图片/视频宽度 + height INTEGER, -- 图片/视频高度 + fps REAL, -- 视频帧率 + project_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + is_active INTEGER NOT NULL DEFAULT 1, + + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +); + +-- 索引 +CREATE INDEX idx_materials_project_id ON materials(project_id); +CREATE INDEX idx_materials_type ON materials(material_type); +CREATE INDEX idx_materials_name ON materials(name); +CREATE INDEX idx_materials_created_at ON materials(created_at); +CREATE UNIQUE INDEX idx_materials_path_project ON materials(path, project_id); +``` + +### 3. 模特表 (models) +```sql +CREATE TABLE models ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + stage_name TEXT, + gender TEXT NOT NULL CHECK (gender IN ('male', 'female', 'other')), + age INTEGER CHECK (age > 0 AND age < 150), + height INTEGER CHECK (height > 0 AND height < 300), -- 厘米 + weight INTEGER CHECK (weight > 0 AND weight < 500), -- 公斤 + measurements TEXT, -- JSON格式存储三围 + description TEXT, + tags TEXT, -- JSON数组格式存储标签 + avatar_path TEXT, + contact_info TEXT, -- JSON格式存储联系信息 + social_media TEXT, -- JSON格式存储社交媒体 + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'archived')), + rating REAL CHECK (rating >= 1.0 AND rating <= 5.0), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + is_active INTEGER NOT NULL DEFAULT 1 +); + +-- 索引 +CREATE INDEX idx_models_name ON models(name); +CREATE INDEX idx_models_gender ON models(gender); +CREATE INDEX idx_models_status ON models(status); +CREATE INDEX idx_models_rating ON models(rating); +``` + +### 4. 模特照片表 (model_photos) +```sql +CREATE TABLE model_photos ( + id TEXT PRIMARY KEY NOT NULL, + model_id TEXT NOT NULL, + photo_type TEXT NOT NULL CHECK (photo_type IN ('portrait', 'full_body', 'casual', 'formal', 'swimwear', 'other')), + path TEXT NOT NULL, + description TEXT, + is_primary INTEGER NOT NULL DEFAULT 0, -- 是否为主要照片 + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + is_active INTEGER NOT NULL DEFAULT 1, + + FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE +); + +-- 索引 +CREATE INDEX idx_model_photos_model_id ON model_photos(model_id); +CREATE INDEX idx_model_photos_type ON model_photos(photo_type); +CREATE INDEX idx_model_photos_primary ON model_photos(is_primary); +CREATE UNIQUE INDEX idx_model_photos_path ON model_photos(path); +``` + +### 5. 模板表 (templates) +```sql +CREATE TABLE templates ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + canvas_config TEXT NOT NULL, -- JSON格式存储画布配置 + duration INTEGER NOT NULL, -- 模板总时长(微秒) + fps REAL NOT NULL, + materials TEXT, -- JSON格式存储模板素材 + tracks TEXT, -- JSON格式存储轨道信息 + import_status TEXT NOT NULL DEFAULT 'pending' CHECK (import_status IN ('pending', 'processing', 'completed', 'failed')), + source_file_path TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + is_active INTEGER NOT NULL DEFAULT 1 +); + +-- 索引 +CREATE INDEX idx_templates_name ON templates(name); +CREATE INDEX idx_templates_status ON templates(import_status); +CREATE INDEX idx_templates_created_at ON templates(created_at); +``` + +### 6. AI分类记录表 (video_classifications) +```sql +CREATE TABLE video_classifications ( + id TEXT PRIMARY KEY NOT NULL, + material_id TEXT NOT NULL, + classification_result TEXT NOT NULL, -- JSON格式存储分类结果 + confidence_score REAL, + processing_time INTEGER, -- 处理时间(毫秒) + ai_model_version TEXT, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), + error_message TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + + FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE +); + +-- 索引 +CREATE INDEX idx_video_classifications_material_id ON video_classifications(material_id); +CREATE INDEX idx_video_classifications_status ON video_classifications(status); +CREATE INDEX idx_video_classifications_created_at ON video_classifications(created_at); +``` + +### 7. 穿搭图片表 (outfit_images) +```sql +CREATE TABLE outfit_images ( + id TEXT PRIMARY KEY NOT NULL, + model_id TEXT NOT NULL, + image_path TEXT NOT NULL, + prompt TEXT, + negative_prompt TEXT, + generation_params TEXT, -- JSON格式存储生成参数 + is_favorite INTEGER NOT NULL DEFAULT 0, + tags TEXT, -- JSON数组格式存储标签 + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + is_active INTEGER NOT NULL DEFAULT 1, + + FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE +); + +-- 索引 +CREATE INDEX idx_outfit_images_model_id ON outfit_images(model_id); +CREATE INDEX idx_outfit_images_favorite ON outfit_images(is_favorite); +CREATE INDEX idx_outfit_images_created_at ON outfit_images(created_at); +CREATE UNIQUE INDEX idx_outfit_images_path ON outfit_images(image_path); +``` + +### 8. 对话记录表 (conversations) +```sql +CREATE TABLE conversations ( + id TEXT PRIMARY KEY NOT NULL, + session_id TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')), + content TEXT NOT NULL, + metadata TEXT, -- JSON格式存储元数据 + created_at TEXT NOT NULL DEFAULT (datetime('now')), + is_active INTEGER NOT NULL DEFAULT 1 +); + +-- 索引 +CREATE INDEX idx_conversations_session_id ON conversations(session_id); +CREATE INDEX idx_conversations_role ON conversations(role); +CREATE INDEX idx_conversations_created_at ON conversations(created_at); +``` + +## 数据库迁移规范 + +### 迁移文件结构 +``` +migrations/ +├── 001_initial_schema.sql +├── 002_add_model_photos.sql +├── 003_add_video_classifications.sql +├── 004_add_outfit_images.sql +└── 005_add_conversations.sql +``` + +### 迁移脚本模板 +```sql +-- Migration: 001_initial_schema.sql +-- Description: 创建初始数据库结构 +-- Date: 2024-01-01 + +BEGIN TRANSACTION; + +-- 创建项目表 +CREATE TABLE projects ( + -- 表结构定义 +); + +-- 创建索引 +CREATE INDEX idx_projects_name ON projects(name); + +-- 插入初始数据(如果需要) +INSERT INTO projects (id, name, path) VALUES +('default', 'Default Project', '/default/path'); + +COMMIT; +``` + +### 迁移管理 +```rust +pub struct DatabaseMigration { + version: u32, + description: String, + sql: String, +} + +impl Database { + pub async fn migrate(&self) -> Result<()> { + // 创建迁移记录表 + self.create_migration_table().await?; + + // 获取当前版本 + let current_version = self.get_current_version().await?; + + // 执行待处理的迁移 + let migrations = self.get_pending_migrations(current_version)?; + + for migration in migrations { + self.execute_migration(&migration).await?; + } + + Ok(()) + } +} +``` + +## 查询优化规范 + +### 索引策略 +```sql +-- 单列索引 +CREATE INDEX idx_materials_project_id ON materials(project_id); + +-- 复合索引 +CREATE INDEX idx_materials_project_type ON materials(project_id, material_type); + +-- 唯一索引 +CREATE UNIQUE INDEX idx_projects_path ON projects(path); + +-- 部分索引 +CREATE INDEX idx_active_materials ON materials(project_id) WHERE is_active = 1; +``` + +### 查询优化 +```sql +-- ✅ 使用索引的查询 +SELECT * FROM materials +WHERE project_id = ? AND material_type = 'video' +ORDER BY created_at DESC +LIMIT 20; + +-- ✅ 使用EXISTS而不是IN +SELECT * FROM projects p +WHERE EXISTS ( + SELECT 1 FROM materials m + WHERE m.project_id = p.id AND m.is_active = 1 +); + +-- ✅ 使用适当的JOIN +SELECT p.name, COUNT(m.id) as material_count +FROM projects p +LEFT JOIN materials m ON p.id = m.project_id AND m.is_active = 1 +WHERE p.is_active = 1 +GROUP BY p.id, p.name; +``` + +### 分页查询 +```sql +-- 基于OFFSET的分页(适用于小数据集) +SELECT * FROM materials +WHERE project_id = ? +ORDER BY created_at DESC +LIMIT ? OFFSET ?; + +-- 基于游标的分页(适用于大数据集) +SELECT * FROM materials +WHERE project_id = ? AND created_at < ? +ORDER BY created_at DESC +LIMIT ?; +``` + +## 数据完整性规范 + +### 外键约束 +```sql +-- 级联删除 +FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE + +-- 限制删除 +FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE RESTRICT + +-- 设置NULL +FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL +``` + +### 检查约束 +```sql +-- 数值范围检查 +CHECK (age > 0 AND age < 150) + +-- 枚举值检查 +CHECK (status IN ('active', 'inactive', 'archived')) + +-- 字符串长度检查 +CHECK (length(name) > 0 AND length(name) <= 255) +``` + +### 触发器 +```sql +-- 自动更新时间戳 +CREATE TRIGGER update_materials_timestamp +AFTER UPDATE ON materials +FOR EACH ROW +BEGIN + UPDATE materials SET updated_at = datetime('now') WHERE id = NEW.id; +END; + +-- 软删除触发器 +CREATE TRIGGER soft_delete_materials +INSTEAD OF DELETE ON materials +FOR EACH ROW +BEGIN + UPDATE materials SET is_active = 0, updated_at = datetime('now') WHERE id = OLD.id; +END; +``` + +## 备份和恢复规范 + +### 备份策略 +```rust +pub struct DatabaseBackup { + source_path: PathBuf, + backup_dir: PathBuf, +} + +impl DatabaseBackup { + pub async fn create_backup(&self) -> Result { + let timestamp = Utc::now().format("%Y%m%d_%H%M%S"); + let backup_name = format!("mixvideo_backup_{}.db", timestamp); + let backup_path = self.backup_dir.join(backup_name); + + // 使用SQLite的备份API + let source = Connection::open(&self.source_path)?; + let backup = Connection::open(&backup_path)?; + + let backup_handle = rusqlite::backup::Backup::new(&source, &backup)?; + backup_handle.run_to_completion(5, Duration::from_millis(250), None)?; + + Ok(backup_path) + } + + pub async fn restore_backup(&self, backup_path: &Path) -> Result<()> { + // 验证备份文件 + self.validate_backup(backup_path)?; + + // 创建当前数据库的备份 + let current_backup = self.create_backup().await?; + + // 恢复数据库 + std::fs::copy(backup_path, &self.source_path)?; + + // 验证恢复的数据库 + self.validate_database().await?; + + Ok(()) + } +} +``` + +### 数据验证 +```sql +-- 检查数据完整性 +PRAGMA integrity_check; + +-- 检查外键约束 +PRAGMA foreign_key_check; + +-- 分析数据库统计信息 +ANALYZE; +``` + +## 性能监控规范 + +### 查询性能分析 +```sql +-- 启用查询计划分析 +EXPLAIN QUERY PLAN SELECT * FROM materials WHERE project_id = ?; + +-- 查看表统计信息 +SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index'; + +-- 检查表大小 +SELECT + name, + COUNT(*) as row_count, + SUM(length(sql)) as size_estimate +FROM sqlite_master +WHERE type = 'table' +GROUP BY name; +``` + +### 性能监控 +```rust +pub struct DatabaseMetrics { + query_times: HashMap>, + connection_pool_stats: ConnectionPoolStats, +} + +impl DatabaseMetrics { + pub fn record_query_time(&mut self, query: &str, duration: Duration) { + self.query_times + .entry(query.to_string()) + .or_insert_with(Vec::new) + .push(duration); + } + + pub fn get_slow_queries(&self, threshold: Duration) -> Vec<(String, Duration)> { + self.query_times + .iter() + .filter_map(|(query, times)| { + let avg_time = times.iter().sum::() / times.len() as u32; + if avg_time > threshold { + Some((query.clone(), avg_time)) + } else { + None + } + }) + .collect() + } +} +``` + +## 安全规范 + +### 数据加密 +```rust +// 敏感数据加密存储 +pub fn encrypt_sensitive_data(data: &str, key: &[u8]) -> Result { + // 使用AES加密 + let cipher = Aes256Gcm::new(key.into()); + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let ciphertext = cipher.encrypt(&nonce, data.as_bytes())?; + + // 组合nonce和密文 + let mut result = nonce.to_vec(); + result.extend_from_slice(&ciphertext); + + Ok(base64::encode(result)) +} +``` + +### 访问控制 +```sql +-- 创建只读视图 +CREATE VIEW materials_readonly AS +SELECT id, name, material_type, size, created_at +FROM materials +WHERE is_active = 1; + +-- 限制敏感字段访问 +CREATE VIEW models_public AS +SELECT id, name, stage_name, gender, description, tags, avatar_path, rating +FROM models +WHERE is_active = 1; +``` + +### 审计日志 +```sql +CREATE TABLE audit_logs ( + id TEXT PRIMARY KEY NOT NULL, + table_name TEXT NOT NULL, + operation TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE')), + record_id TEXT NOT NULL, + old_values TEXT, -- JSON格式 + new_values TEXT, -- JSON格式 + user_id TEXT, + timestamp TEXT NOT NULL DEFAULT (datetime('now')) +); +``` diff --git a/.promptx/development-guide.md b/.promptx/development-guide.md new file mode 100644 index 0000000..8f5d87a --- /dev/null +++ b/.promptx/development-guide.md @@ -0,0 +1,531 @@ +# MixVideo 开发指南 + +## 快速开始 + +### 环境准备 +```bash +# 检查环境要求 +node --version # >= 18.0.0 +rustc --version # >= 1.70.0 +pnpm --version # >= 8.0.0 + +# 克隆项目 +git clone +cd mixvideo + +# 安装依赖 +pnpm install + +# 启动开发环境 +pnpm tauri:dev +``` + +### 项目结构概览 +``` +mixvideo/ +├── .promptx/ # 开发规范文档 +├── apps/desktop/ # 主应用 +│ ├── src/ # React前端 +│ └── src-tauri/ # Rust后端 +├── docs/ # 项目文档 +├── package.json # 根配置 +├── pnpm-workspace.yaml # 工作空间配置 +└── Cargo.toml # Rust工作空间 +``` + +## 开发工作流 + +### 1. 功能开发流程 +```bash +# 1. 创建功能分支 +git checkout -b feature/新功能名称 + +# 2. 开发和测试 +pnpm tauri:dev # 启动开发服务器 +pnpm test # 运行测试 + +# 3. 代码检查 +pnpm lint # ESLint检查 +pnpm format # Prettier格式化 +cargo clippy # Rust代码检查 + +# 4. 提交代码 +git add . +git commit -m "feat: 添加新功能描述" + +# 5. 推送和合并 +git push origin feature/新功能名称 +# 创建Pull Request +``` + +### 2. 提交信息规范 +```bash +# 提交类型 +feat: 新功能 +fix: 修复bug +docs: 文档更新 +style: 代码格式调整 +refactor: 重构代码 +test: 测试相关 +chore: 构建工具或辅助工具的变动 + +# 示例 +git commit -m "feat: 添加AI视频分类功能" +git commit -m "fix: 修复素材导入时的路径问题" +git commit -m "docs: 更新API文档" +``` + +### 3. 代码审查清单 +- [ ] 功能是否按需求实现 +- [ ] 代码是否遵循项目规范 +- [ ] 是否有适当的错误处理 +- [ ] 是否有相应的测试覆盖 +- [ ] 性能是否有考虑 +- [ ] 安全性是否有保障 +- [ ] 文档是否更新 + +## 核心开发模式 + +### 前端开发模式 + +#### 1. 组件开发 +```typescript +// 1. 定义类型 +interface ComponentProps { + id: string; + title: string; + onAction?: (data: any) => void; +} + +// 2. 实现组件 +export const Component: FC = ({ id, title, onAction }) => { + // Hooks + const [state, setState] = useState(); + const { data, loading } = useCustomHook(); + + // 事件处理 + const handleAction = useCallback(() => { + onAction?.(data); + }, [onAction, data]); + + // 渲染 + return ( +
+ {/* 组件内容 */} +
+ ); +}; +``` + +#### 2. 状态管理 +```typescript +// 使用Zustand进行状态管理 +export const useFeatureStore = create((set, get) => ({ + // 状态 + data: [], + loading: false, + error: null, + + // 操作 + fetchData: async () => { + set({ loading: true }); + try { + const data = await api.getData(); + set({ data, loading: false }); + } catch (error) { + set({ error: error.message, loading: false }); + } + }, +})); +``` + +#### 3. 服务层调用 +```typescript +// 调用Tauri命令 +export class FeatureService { + static async getData(): Promise { + try { + return await invoke('get_data'); + } catch (error) { + throw new Error(`获取数据失败: ${error}`); + } + } +} +``` + +### 后端开发模式 + +#### 1. 数据模型 +```rust +// 定义数据模型 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Entity { + pub id: String, + pub name: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Entity { + pub fn new(name: String) -> Self { + let now = Utc::now(); + Self { + id: uuid::Uuid::new_v4().to_string(), + name, + created_at: now, + updated_at: now, + } + } +} +``` + +#### 2. 仓库层 +```rust +// 数据访问层 +pub struct EntityRepository { + database: Arc, +} + +impl EntityRepository { + pub async fn create(&self, entity: &Entity) -> Result<()> { + let conn = self.database.get_connection().await?; + // 数据库操作 + Ok(()) + } + + pub async fn get_by_id(&self, id: &str) -> Result> { + let conn = self.database.get_read_connection().await?; + // 查询操作 + Ok(None) + } +} +``` + +#### 3. 业务服务 +```rust +// 业务逻辑层 +pub struct EntityService; + +impl EntityService { + pub async fn create_entity( + repository: &EntityRepository, + request: CreateEntityRequest, + ) -> Result { + // 业务验证 + if request.name.trim().is_empty() { + return Err(anyhow!("名称不能为空")); + } + + // 创建实体 + let entity = Entity::new(request.name); + + // 保存到数据库 + repository.create(&entity).await?; + + Ok(entity) + } +} +``` + +#### 4. Tauri命令 +```rust +// 表现层命令 +#[command] +pub async fn create_entity( + state: State<'_, AppState>, + request: CreateEntityRequest, +) -> Result { + let repository = state.get_entity_repository() + .map_err(|e| e.to_string())?; + + EntityService::create_entity(&repository, request) + .await + .map_err(|e| e.to_string()) +} +``` + +## 测试策略 + +### 前端测试 +```typescript +// 组件测试 +describe('Component', () => { + it('应该正确渲染', () => { + render(); + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + + it('应该处理用户交互', async () => { + const onAction = vi.fn(); + render(); + + fireEvent.click(screen.getByRole('button')); + await waitFor(() => { + expect(onAction).toHaveBeenCalled(); + }); + }); +}); + +// Hook测试 +describe('useCustomHook', () => { + it('应该返回正确的数据', async () => { + const { result } = renderHook(() => useCustomHook()); + + await waitFor(() => { + expect(result.current.data).toBeDefined(); + }); + }); +}); +``` + +### 后端测试 +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_create_entity() { + // 准备测试数据 + let db = setup_test_database().await; + let repository = EntityRepository::new(db); + let request = CreateEntityRequest { + name: "Test Entity".to_string(), + }; + + // 执行测试 + let result = EntityService::create_entity(&repository, request).await; + + // 验证结果 + assert!(result.is_ok()); + let entity = result.unwrap(); + assert_eq!(entity.name, "Test Entity"); + } +} +``` + +## 调试技巧 + +### 前端调试 +```typescript +// 1. 使用浏览器开发者工具 +console.log('调试信息:', data); +console.error('错误信息:', error); + +// 2. React DevTools +// 安装React DevTools浏览器扩展 + +// 3. Zustand DevTools +const useStore = create( + devtools((set, get) => ({ + // store实现 + })) +); + +// 4. 网络请求调试 +// 在Network面板查看Tauri命令调用 +``` + +### 后端调试 +```rust +// 1. 使用tracing进行日志记录 +use tracing::{info, warn, error, debug}; + +#[instrument] +async fn debug_function() { + info!("函数开始执行"); + debug!("调试信息: {:?}", data); + warn!("警告信息"); + error!("错误信息: {}", error); +} + +// 2. 使用dbg!宏 +let result = dbg!(some_calculation()); + +// 3. 条件编译调试代码 +#[cfg(debug_assertions)] +println!("调试模式下的输出"); +``` + +### 数据库调试 +```sql +-- 查看查询计划 +EXPLAIN QUERY PLAN SELECT * FROM materials WHERE project_id = ?; + +-- 检查索引使用 +.schema materials +.indices materials + +-- 分析表统计 +ANALYZE; +``` + +## 性能优化 + +### 前端性能优化 +```typescript +// 1. 组件优化 +const OptimizedComponent = memo(({ data }) => { + const memoizedValue = useMemo(() => { + return expensiveCalculation(data); + }, [data]); + + const memoizedCallback = useCallback(() => { + handleAction(data); + }, [data]); + + return
{memoizedValue}
; +}); + +// 2. 懒加载 +const LazyComponent = lazy(() => import('./LazyComponent')); + +// 3. 虚拟化长列表 +import { FixedSizeList as List } from 'react-window'; + +const VirtualList = ({ items }) => ( + + {({ index, style }) => ( +
+ +
+ )} +
+); +``` + +### 后端性能优化 +```rust +// 1. 数据库连接池 +let pool = ConnectionPool::new(database_url, pool_config)?; + +// 2. 批量操作 +async fn batch_insert(items: &[Item]) -> Result<()> { + let conn = pool.get().await?; + let tx = conn.transaction()?; + + for item in items { + tx.execute("INSERT INTO ...", params![...])?; + } + + tx.commit()?; + Ok(()) +} + +// 3. 异步并发 +use futures::future::join_all; + +let futures: Vec<_> = items.iter() + .map(|item| process_item(item)) + .collect(); + +let results = join_all(futures).await; +``` + +## 常见问题解决 + +### 1. 构建问题 +```bash +# 清理缓存 +pnpm clean +rm -rf node_modules +rm -rf target +pnpm install + +# Rust编译问题 +cargo clean +cargo build +``` + +### 2. 开发环境问题 +```bash +# 端口冲突 +lsof -ti:1420 | xargs kill -9 + +# 权限问题 +sudo chown -R $(whoami) ~/.cargo +``` + +### 3. 数据库问题 +```rust +// 数据库锁定 +// 检查是否有其他进程占用数据库 +// 使用WAL模式减少锁定 + +// 迁移失败 +// 检查SQL语法 +// 验证外键约束 +``` + +### 4. 前端问题 +```typescript +// 状态更新问题 +// 检查依赖数组 +// 使用useCallback和useMemo + +// 渲染性能问题 +// 使用React DevTools Profiler +// 检查不必要的重渲染 +``` + +## 部署指南 + +### 开发环境部署 +```bash +# 启动开发服务器 +pnpm tauri:dev +``` + +### 生产环境构建 +```bash +# 构建应用 +pnpm tauri:build + +# 构建产物位置 +# Windows: target/release/bundle/msi/ +# macOS: target/release/bundle/dmg/ +# Linux: target/release/bundle/deb/ +``` + +### 发布流程 +```bash +# 1. 更新版本号 +# 修改 package.json 和 Cargo.toml 中的版本号 + +# 2. 创建发布标签 +git tag v0.2.1 +git push origin v0.2.1 + +# 3. GitHub Actions自动构建和发布 +# 查看 .github/workflows/ 中的配置 +``` + +## 最佳实践总结 + +### 代码质量 +1. **遵循规范**: 严格按照项目编码规范开发 +2. **测试驱动**: 为核心功能编写测试 +3. **代码审查**: 所有代码都要经过审查 +4. **文档更新**: 及时更新相关文档 + +### 性能考虑 +1. **数据库优化**: 合理使用索引和查询优化 +2. **前端优化**: 使用虚拟化、懒加载等技术 +3. **内存管理**: 避免内存泄漏和过度使用 +4. **网络优化**: 减少不必要的网络请求 + +### 安全意识 +1. **输入验证**: 严格验证所有用户输入 +2. **错误处理**: 不泄露敏感信息 +3. **权限控制**: 实施适当的访问控制 +4. **数据保护**: 保护用户数据安全 + +### 用户体验 +1. **响应式设计**: 支持多种设备和屏幕尺寸 +2. **加载状态**: 提供清晰的加载反馈 +3. **错误提示**: 友好的错误信息和恢复建议 +4. **无障碍性**: 支持键盘导航和屏幕阅读器 diff --git a/.promptx/frontend-coding-standards.md b/.promptx/frontend-coding-standards.md new file mode 100644 index 0000000..1a4ad26 --- /dev/null +++ b/.promptx/frontend-coding-standards.md @@ -0,0 +1,644 @@ +# MixVideo 前端开发规范 + +## 技术栈规范 + +### 核心技术 +- **React 18**: 函数组件 + Hooks +- **TypeScript 5.8**: 严格类型检查 +- **Vite 6.0**: 构建工具 +- **TailwindCSS 3.4**: 样式框架 +- **Zustand 4.4**: 状态管理 +- **React Router 6.20**: 路由管理 + +### 开发工具 +- **ESLint**: 代码检查 +- **Prettier**: 代码格式化 +- **Vitest**: 单元测试 +- **Testing Library**: 组件测试 + +## 项目结构规范 + +### 目录结构 +``` +src/ +├── components/ # 可复用组件 +│ ├── ui/ # 基础UI组件 +│ ├── business/ # 业务组件 +│ └── __tests__/ # 组件测试 +├── pages/ # 页面组件 +├── hooks/ # 自定义Hooks +├── services/ # 业务服务层 +├── store/ # Zustand状态管理 +├── types/ # TypeScript类型定义 +├── utils/ # 工具函数 +├── styles/ # 样式文件 +│ ├── design-system.css +│ └── animations.css +└── tests/ # 测试文件 + ├── setup.ts + ├── components/ + ├── integration/ + └── e2e/ +``` + +### 文件命名规范 +- **组件文件**: PascalCase (如 `MaterialCard.tsx`) +- **Hook文件**: camelCase (如 `useMaterialSearch.ts`) +- **服务文件**: camelCase (如 `materialService.ts`) +- **类型文件**: camelCase (如 `material.ts`) +- **工具文件**: camelCase (如 `imagePathUtils.ts`) + +## 组件开发规范 + +### 组件结构模板 +```typescript +import React from 'react'; +import { clsx } from 'clsx'; + +// 类型定义 +interface ComponentProps { + // 必需属性 + id: string; + title: string; + + // 可选属性 + description?: string; + className?: string; + + // 事件处理 + onClick?: () => void; + onSubmit?: (data: FormData) => void; +} + +/** + * 组件描述 + * 遵循 Tauri 开发规范的组件设计原则 + */ +export const Component: React.FC = ({ + id, + title, + description, + className, + onClick, + onSubmit, +}) => { + // Hooks (按顺序) + const [state, setState] = useState(initialState); + const { data, loading, error } = useCustomHook(); + + // 事件处理函数 + const handleClick = useCallback(() => { + onClick?.(); + }, [onClick]); + + // 副作用 + useEffect(() => { + // 副作用逻辑 + }, [dependencies]); + + // 条件渲染 + if (loading) { + return ; + } + + if (error) { + return ; + } + + // 主渲染 + return ( +
+

{title}

+ {description && ( +

{description}

+ )} +
+ ); +}; + +// 默认导出 +export default Component; +``` + +### 组件设计原则 +1. **单一职责**: 每个组件只负责一个功能 +2. **可复用性**: 通过props实现组件的灵活配置 +3. **可测试性**: 组件逻辑清晰,易于测试 +4. **性能优化**: 合理使用memo、useMemo、useCallback +5. **无障碍性**: 支持键盘导航和屏幕阅读器 + +### Props设计规范 +```typescript +// ✅ 好的Props设计 +interface GoodProps { + // 必需属性在前 + id: string; + title: string; + + // 可选属性在后 + description?: string; + variant?: 'primary' | 'secondary' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + disabled?: boolean; + + // 事件处理器 + onClick?: (event: MouseEvent) => void; + onSubmit?: (data: FormData) => Promise; + + // 样式相关 + className?: string; + style?: CSSProperties; +} + +// ❌ 避免的Props设计 +interface BadProps { + data: any; // 避免使用any + config: object; // 避免使用object + callback: Function; // 避免使用Function +} +``` + +## TypeScript 规范 + +### 类型定义规范 +```typescript +// 基础类型定义 +export interface Material { + id: string; + name: string; + path: string; + type: MaterialType; + size: number; + duration?: number; + createdAt: Date; + updatedAt: Date; +} + +// 枚举定义 +export enum MaterialType { + VIDEO = 'video', + AUDIO = 'audio', + IMAGE = 'image', +} + +// 联合类型 +export type Status = 'idle' | 'loading' | 'success' | 'error'; + +// 泛型接口 +export interface ApiResponse { + data: T; + message: string; + success: boolean; +} + +// 工具类型 +export type CreateMaterialRequest = Omit; +export type UpdateMaterialRequest = Partial>; +``` + +### 类型导入导出规范 +```typescript +// 类型导入 +import type { Material, MaterialType } from '../types/material'; +import type { ComponentProps } from 'react'; + +// 值导入 +import { MaterialService } from '../services/materialService'; +import { useMaterialStore } from '../store/materialStore'; + +// 混合导入 +import React, { type FC, type ReactNode } from 'react'; +``` + +## 状态管理规范 + +### Zustand Store 设计 +```typescript +import { create } from 'zustand'; +import type { Material } from '../types/material'; + +interface MaterialState { + // 状态数据 + materials: Material[]; + selectedMaterial: Material | null; + loading: boolean; + error: string | null; + + // 同步操作 + setMaterials: (materials: Material[]) => void; + setSelectedMaterial: (material: Material | null) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + + // 异步操作 + fetchMaterials: () => Promise; + createMaterial: (request: CreateMaterialRequest) => Promise; + updateMaterial: (id: string, request: UpdateMaterialRequest) => Promise; + deleteMaterial: (id: string) => Promise; +} + +export const useMaterialStore = create((set, get) => ({ + // 初始状态 + materials: [], + selectedMaterial: null, + loading: false, + error: null, + + // 同步操作 + setMaterials: (materials) => set({ materials }), + setSelectedMaterial: (selectedMaterial) => set({ selectedMaterial }), + setLoading: (loading) => set({ loading }), + setError: (error) => set({ error }), + + // 异步操作 + fetchMaterials: async () => { + try { + set({ loading: true, error: null }); + const materials = await MaterialService.getAllMaterials(); + set({ materials, loading: false }); + } catch (error) { + set({ error: error.message, loading: false }); + } + }, + + createMaterial: async (request) => { + try { + set({ loading: true, error: null }); + const material = await MaterialService.createMaterial(request); + set((state) => ({ + materials: [...state.materials, material], + loading: false, + })); + } catch (error) { + set({ error: error.message, loading: false }); + } + }, +})); +``` + +### 状态管理原则 +1. **单一数据源**: 每个状态只有一个来源 +2. **不可变更新**: 使用不可变的方式更新状态 +3. **异步处理**: 统一的异步操作模式 +4. **错误处理**: 完善的错误状态管理 + +## 服务层规范 + +### 服务类设计 +```typescript +import { invoke } from '@tauri-apps/api/core'; +import type { Material, CreateMaterialRequest, UpdateMaterialRequest } from '../types/material'; + +/** + * 素材管理服务 + * 遵循 Tauri 开发规范的前端服务层设计 + */ +export class MaterialService { + /** + * 获取所有素材 + */ + static async getAllMaterials(): Promise { + try { + const materials = await invoke('get_all_materials'); + return materials; + } catch (error) { + console.error('获取素材列表失败:', error); + throw new Error(`获取素材列表失败: ${error}`); + } + } + + /** + * 根据ID获取素材 + */ + static async getMaterialById(id: string): Promise { + try { + const material = await invoke('get_material_by_id', { id }); + return material; + } catch (error) { + console.error('获取素材详情失败:', error); + throw new Error(`获取素材详情失败: ${error}`); + } + } + + /** + * 创建素材 + */ + static async createMaterial(request: CreateMaterialRequest): Promise { + try { + const material = await invoke('create_material', { request }); + return material; + } catch (error) { + console.error('创建素材失败:', error); + throw new Error(`创建素材失败: ${error}`); + } + } +} +``` + +### 服务层原则 +1. **静态方法**: 使用静态方法避免实例化 +2. **错误处理**: 统一的错误处理和日志记录 +3. **类型安全**: 严格的类型定义和检查 +4. **文档注释**: 详细的JSDoc注释 + +## 样式规范 + +### TailwindCSS 使用规范 +```typescript +// ✅ 推荐的样式写法 +const buttonStyles = { + base: 'inline-flex items-center justify-center rounded-md font-medium transition-colors', + variants: { + primary: 'bg-primary-600 text-white hover:bg-primary-700', + secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300', + danger: 'bg-red-600 text-white hover:bg-red-700', + }, + sizes: { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-base', + lg: 'px-6 py-3 text-lg', + }, +}; + +// 使用clsx进行条件样式 +const className = clsx( + buttonStyles.base, + buttonStyles.variants[variant], + buttonStyles.sizes[size], + disabled && 'opacity-50 cursor-not-allowed', + className +); +``` + +### CSS变量使用 +```css +/* 使用设计系统中定义的CSS变量 */ +.custom-component { + background-color: var(--primary-500); + color: var(--gray-50); + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); +} +``` + +## 自定义Hooks规范 + +### Hook设计模板 +```typescript +import { useState, useEffect, useCallback } from 'react'; +import type { Material } from '../types/material'; +import { MaterialService } from '../services/materialService'; + +interface UseMaterialSearchOptions { + initialQuery?: string; + autoSearch?: boolean; +} + +interface UseMaterialSearchReturn { + materials: Material[]; + query: string; + loading: boolean; + error: string | null; + search: (query: string) => Promise; + clearResults: () => void; +} + +/** + * 素材搜索Hook + * 提供素材搜索功能的封装 + */ +export const useMaterialSearch = ( + options: UseMaterialSearchOptions = {} +): UseMaterialSearchReturn => { + const { initialQuery = '', autoSearch = false } = options; + + const [materials, setMaterials] = useState([]); + const [query, setQuery] = useState(initialQuery); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const search = useCallback(async (searchQuery: string) => { + try { + setLoading(true); + setError(null); + setQuery(searchQuery); + + const results = await MaterialService.searchMaterials(searchQuery); + setMaterials(results); + } catch (err) { + setError(err instanceof Error ? err.message : '搜索失败'); + setMaterials([]); + } finally { + setLoading(false); + } + }, []); + + const clearResults = useCallback(() => { + setMaterials([]); + setQuery(''); + setError(null); + }, []); + + useEffect(() => { + if (autoSearch && initialQuery) { + search(initialQuery); + } + }, [autoSearch, initialQuery, search]); + + return { + materials, + query, + loading, + error, + search, + clearResults, + }; +}; +``` + +## 测试规范 + +### 组件测试模板 +```typescript +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { MaterialCard } from '../MaterialCard'; +import type { Material } from '../../types/material'; + +const mockMaterial: Material = { + id: '1', + name: 'Test Material', + path: '/test/path', + type: 'video', + size: 1024, + createdAt: new Date(), + updatedAt: new Date(), +}; + +describe('MaterialCard', () => { + it('应该正确渲染素材信息', () => { + render(); + + expect(screen.getByText('Test Material')).toBeInTheDocument(); + expect(screen.getByText('video')).toBeInTheDocument(); + }); + + it('应该处理点击事件', async () => { + const onClickMock = vi.fn(); + render(); + + fireEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(onClickMock).toHaveBeenCalledWith(mockMaterial); + }); + }); + + it('应该显示加载状态', () => { + render(); + + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); + }); +}); +``` + +### 测试原则 +1. **测试行为**: 测试组件的行为而不是实现细节 +2. **用户视角**: 从用户的角度编写测试 +3. **覆盖率**: 关键功能100%覆盖 +4. **可维护性**: 测试代码也要易于维护 + +## 性能优化规范 + +### React性能优化 +```typescript +// 使用memo优化组件重渲染 +export const MaterialCard = memo(({ material, onClick }) => { + // 组件实现 +}); + +// 使用useMemo优化计算 +const expensiveValue = useMemo(() => { + return computeExpensiveValue(data); +}, [data]); + +// 使用useCallback优化函数引用 +const handleClick = useCallback((id: string) => { + onClick?.(id); +}, [onClick]); + +// 使用lazy加载组件 +const LazyComponent = lazy(() => import('./LazyComponent')); +``` + +### 列表优化 +```typescript +// 使用react-window进行虚拟化 +import { FixedSizeList as List } from 'react-window'; + +const VirtualizedList: FC<{ items: Material[] }> = ({ items }) => ( + + {({ index, style, data }) => ( +
+ +
+ )} +
+); +``` + +## 错误处理规范 + +### 错误边界 +```typescript +import React, { Component, type ReactNode } from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component< + { children: ReactNode }, + ErrorBoundaryState +> { + constructor(props: { children: ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( +
+

出现了错误

+

{this.state.error?.message}

+
+ ); + } + + return this.props.children; + } +} +``` + +### 异步错误处理 +```typescript +// 在组件中处理异步错误 +const [error, setError] = useState(null); + +const handleAsyncOperation = async () => { + try { + setError(null); + await someAsyncOperation(); + } catch (err) { + setError(err instanceof Error ? err.message : '操作失败'); + } +}; + +// 在服务层统一错误处理 +export const handleApiError = (error: unknown): never => { + if (error instanceof Error) { + throw new Error(`API错误: ${error.message}`); + } + throw new Error('未知错误'); +}; +``` + +## 代码质量规范 + +### ESLint配置 +```json +{ + "extends": [ + "@vitejs/eslint-config-react", + "@typescript-eslint/recommended" + ], + "rules": { + "react-hooks/exhaustive-deps": "error", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/explicit-function-return-type": "warn" + } +} +``` + +### 代码审查清单 +- [ ] 组件是否遵循单一职责原则 +- [ ] TypeScript类型是否完整准确 +- [ ] 是否有适当的错误处理 +- [ ] 是否有性能优化考虑 +- [ ] 是否有相应的测试覆盖 +- [ ] 代码是否符合团队规范 diff --git a/.promptx/project-overview.md b/.promptx/project-overview.md new file mode 100644 index 0000000..803fcb9 --- /dev/null +++ b/.promptx/project-overview.md @@ -0,0 +1,347 @@ +# MixVideo 项目概览与开发规范 + +## 项目简介 + +MixVideo 是一个基于 Tauri 2.0 构建的跨平台AI视频创作桌面应用,采用 Rust + React + TypeScript 技术栈,旨在构建全球领先的AI驱动视频创作生态平台。 + +### 核心愿景 +**"构建全球领先的AI驱动视频创作生态平台,让每个人都能轻松创作出专业级的视频内容"** + +## 技术架构 + +### 整体架构 +``` +mixvideo/ +├── apps/ +│ └── desktop/ # Tauri桌面应用 +│ ├── src/ # React前端代码 +│ │ ├── components/ # UI组件 +│ │ ├── pages/ # 页面组件 +│ │ ├── store/ # 状态管理(Zustand) +│ │ ├── services/ # 业务服务 +│ │ ├── types/ # TypeScript类型定义 +│ │ ├── hooks/ # React Hooks +│ │ ├── utils/ # 工具函数 +│ │ └── styles/ # 样式文件 +│ └── src-tauri/ # Rust后端代码 +│ ├── src/ +│ │ ├── infrastructure/ # 基础设施层 +│ │ ├── data/ # 数据访问层 +│ │ ├── business/ # 业务逻辑层 +│ │ ├── presentation/ # 表现层(Commands) +│ │ └── services/ # 服务层 +│ └── Cargo.toml +├── packages/ # 共享包(未来扩展) +├── docs/ # 项目文档 +└── .promptx/ # 开发规范文档 +``` + +### 前端技术栈 +- **框架**: React 18 + TypeScript 5.8 +- **构建工具**: Vite 6.0 +- **状态管理**: Zustand 4.4 +- **UI框架**: TailwindCSS 3.4 +- **图标**: Lucide React + Heroicons +- **路由**: React Router DOM 6.20 +- **测试**: Vitest + Testing Library +- **包管理**: PNPM 8.15 (Workspace) + +### 后端技术栈 +- **框架**: Tauri 2.0 + Rust 1.70+ +- **数据库**: SQLite (WAL模式,支持连接池) +- **AI集成**: Google Gemini API +- **多媒体**: FFmpeg +- **异步**: Tokio + async/await +- **序列化**: Serde + JSON +- **错误处理**: anyhow + thiserror + +## 核心功能模块 + +### 1. 项目管理 +- 多项目管理,本地路径绑定 +- 素材管理:视频、音频、图片统一管理 +- 版本控制:素材版本追踪,支持回滚 + +### 2. AI智能分析 +- 基于Gemini AI的视频内容自动分类 +- 场景检测:自动识别视频场景变化,智能分割片段 +- 质量评估:AI评估视频质量,提供优化建议 + +### 3. 模板系统 +- 可视化模板编辑器 +- AI驱动的素材与模板自动匹配 +- 批量处理:一键应用模板到多个素材 + +### 4. 模特管理 +- 完整的模特信息管理系统 +- 支持多类型照片分类存储 +- 素材与模特自动关联 + +### 5. 穿搭推荐 +- AI驱动的穿搭搭配推荐 +- 智能搜索和筛选 +- 收藏和比较功能 + +### 6. 视频生成 +- 基于模板的视频自动生成 +- 支持多种输出格式 +- 批量导出处理 + +## 四层架构设计 + +### 1. 基础设施层 (Infrastructure) +- **数据库管理**: SQLite连接池,读写分离 +- **外部服务**: Gemini API, FFmpeg集成 +- **文件系统**: 文件操作,路径管理 +- **日志系统**: 结构化日志,性能监控 +- **事件总线**: 组件间通信 + +### 2. 数据访问层 (Data) +- **模型定义**: 所有业务实体模型 +- **仓库模式**: 数据访问抽象 +- **数据库迁移**: 版本管理 +- **连接管理**: 连接池优化 + +### 3. 业务逻辑层 (Business) +- **服务层**: 核心业务逻辑 +- **领域模型**: 业务规则封装 +- **工作流**: 复杂业务流程 +- **验证**: 数据验证规则 + +### 4. 表现层 (Presentation) +- **Tauri Commands**: 前后端通信接口 +- **参数验证**: 输入参数校验 +- **错误处理**: 统一错误响应 +- **权限控制**: 操作权限管理 + +## 开发环境配置 + +### 环境要求 +- Node.js >= 18.0.0 +- Rust >= 1.70.0 +- PNPM >= 8.0.0 +- Tauri CLI >= 2.0.0 + +### 开发命令 +```bash +# 安装依赖 +pnpm install + +# 开发模式 +pnpm tauri:dev + +# 构建应用 +pnpm tauri:build + +# 运行测试 +pnpm test + +# 代码格式化 +pnpm format + +# 代码检查 +pnpm lint +``` + +## 核心设计原则 + +### 1. 安全第一 +- 数据库文件安全存储 +- 输入参数严格验证 +- 错误信息不泄露敏感数据 + +### 2. 性能优化 +- 数据库连接池 +- 读写分离 +- 虚拟化列表 +- 懒加载 + +### 3. 用户体验 +- 响应式设计 +- 优雅的加载状态 +- 友好的错误提示 +- 流畅的动画效果 + +### 4. 可维护性 +- 清晰的分层架构 +- 统一的编码规范 +- 完善的测试覆盖 +- 详细的文档说明 + +### 5. 可扩展性 +- 模块化设计 +- 插件化架构 +- 配置化管理 +- 微服务准备 + +## 数据库设计 + +### 核心表结构 +- **projects**: 项目管理 +- **materials**: 素材管理 +- **models**: 模特信息 +- **templates**: 模板定义 +- **video_classifications**: AI分类记录 +- **outfit_images**: 穿搭图片 +- **conversations**: 对话记录 + +### 数据库特性 +- WAL模式:提高并发性能 +- 外键约束:保证数据一致性 +- 索引优化:提升查询性能 +- 连接池:支持高并发访问 + +## 状态管理规范 + +### Zustand Store 设计 +- **uiStore**: UI状态管理 +- **projectStore**: 项目状态 +- **materialStore**: 素材状态 +- **outfitSearchStore**: 穿搭搜索状态 +- **videoClassificationStore**: 视频分类状态 + +### 状态管理原则 +- 单一数据源 +- 不可变更新 +- 异步操作处理 +- 错误状态管理 + +## UI/UX 设计规范 + +### 设计系统 +- **颜色系统**: 现代蓝色主题,语义化颜色 +- **字体系统**: Inter + JetBrains Mono +- **间距系统**: 8px基础网格 +- **圆角系统**: 统一的圆角规范 +- **阴影系统**: 层次化阴影效果 + +### 组件规范 +- 原子化设计 +- 可复用组件 +- 一致的交互模式 +- 无障碍访问支持 + +### 动画规范 +- 流畅的过渡效果 +- 有意义的动画反馈 +- 性能优化的动画 +- 可配置的动画偏好 + +## 测试策略 + +### 测试类型 +- **单元测试**: 核心业务逻辑 +- **集成测试**: API和数据库集成 +- **组件测试**: React组件测试 +- **E2E测试**: 用户界面端到端测试 + +### 测试工具 +- **前端**: Vitest + Testing Library +- **后端**: Rust内置测试框架 +- **E2E**: 计划使用Playwright + +### 测试覆盖率 +- 目标覆盖率: 80%+ +- 关键路径: 100%覆盖 +- 持续集成: 自动化测试 + +## 版本管理 + +### 版本号规范 +- 语义化版本: MAJOR.MINOR.PATCH +- 当前版本: v0.2.1 +- 发布周期: 迭代式发布 + +### Git工作流 +- 主分支: master +- 功能分支: feature/功能名 +- 修复分支: hotfix/问题描述 +- 发布分支: release/版本号 + +## 部署与发布 + +### 构建产物 +- Windows: .msi + .exe +- macOS: .dmg + .app +- Linux: .deb + .rpm + .AppImage + +### 自动化发布 +- GitHub Actions +- 自动构建 +- 自动发布 +- 版本标签管理 + +## 性能监控 + +### 监控指标 +- 应用启动时间 +- 数据库查询性能 +- AI分类处理时间 +- 内存使用情况 + +### 优化策略 +- 数据库查询优化 +- 图片懒加载 +- 虚拟化列表 +- 缓存策略 + +## 错误处理 + +### 错误分类 +- 用户输入错误 +- 系统运行错误 +- 网络连接错误 +- 外部服务错误 + +### 处理策略 +- 统一错误格式 +- 友好错误提示 +- 错误日志记录 +- 自动重试机制 + +## 国际化支持 + +### 多语言支持 +- 中文(简体): 主要语言 +- 英文: 计划支持 +- 其他语言: 未来扩展 + +### 本地化策略 +- 文本外部化 +- 日期时间格式 +- 数字格式 +- 文化适配 + +## 安全考虑 + +### 数据安全 +- 本地数据加密 +- 敏感信息保护 +- 安全的文件操作 +- API密钥管理 + +### 应用安全 +- 输入验证 +- SQL注入防护 +- 文件路径验证 +- 权限控制 + +## 未来规划 + +### 短期目标 (3个月) +- 完善AI分类功能 +- 优化用户界面 +- 提升性能表现 +- 增加测试覆盖 + +### 中期目标 (6个月) +- 添加更多AI功能 +- 支持更多文件格式 +- 云端同步功能 +- 插件系统 + +### 长期目标 (1年) +- 多平台支持 +- 微服务架构 +- 商业化功能 +- 生态系统建设 diff --git a/.promptx/ui-ux-design-standards.md b/.promptx/ui-ux-design-standards.md new file mode 100644 index 0000000..17ed625 --- /dev/null +++ b/.promptx/ui-ux-design-standards.md @@ -0,0 +1,499 @@ +# MixVideo UI/UX 设计规范 + +## 设计理念 + +### 核心原则 +1. **用户至上**: 以用户体验为中心的设计 +2. **简洁高效**: 简洁的界面,高效的操作流程 +3. **一致性**: 统一的视觉语言和交互模式 +4. **可访问性**: 支持无障碍访问和多设备适配 +5. **现代化**: 符合现代设计趋势的视觉风格 + +### 设计目标 +- **专业性**: 体现专业视频创作工具的品质 +- **易用性**: 降低学习成本,提升操作效率 +- **美观性**: 现代化的视觉设计,愉悦的用户体验 +- **功能性**: 功能导向的界面布局和交互设计 + +## 视觉设计系统 + +### 颜色系统 +```css +/* 主色调 - 现代蓝色系 */ +:root { + --primary-50: #eff6ff; + --primary-100: #dbeafe; + --primary-200: #bfdbfe; + --primary-300: #93c5fd; + --primary-400: #60a5fa; + --primary-500: #3b82f6; /* 主品牌色 */ + --primary-600: #2563eb; + --primary-700: #1d4ed8; + --primary-800: #1e40af; + --primary-900: #1e3a8a; + + /* 中性色系 */ + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + + /* 语义色彩 */ + --success-500: #10b981; + --warning-500: #f59e0b; + --error-500: #ef4444; + --info-500: #3b82f6; +} +``` + +### 颜色使用规范 +- **主色**: 用于主要操作按钮、链接、重要状态指示 +- **中性色**: 用于文本、边框、背景、次要元素 +- **语义色**: 用于状态反馈、警告、错误、成功提示 + +### 字体系统 +```css +/* 字体族 */ +font-family: { + sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'], + mono: ['JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', 'monospace'] +} + +/* 字体大小 */ +fontSize: { + xs: ['0.75rem', { lineHeight: '1rem' }], /* 12px */ + sm: ['0.875rem', { lineHeight: '1.25rem' }], /* 14px */ + base: ['1rem', { lineHeight: '1.5rem' }], /* 16px */ + lg: ['1.125rem', { lineHeight: '1.75rem' }], /* 18px */ + xl: ['1.25rem', { lineHeight: '1.75rem' }], /* 20px */ + '2xl': ['1.5rem', { lineHeight: '2rem' }], /* 24px */ + '3xl': ['1.875rem', { lineHeight: '2.25rem' }], /* 30px */ + '4xl': ['2.25rem', { lineHeight: '2.5rem' }], /* 36px */ +} +``` + +### 字体使用规范 +- **标题**: 使用较大字号,font-weight: 600-700 +- **正文**: 使用base字号,font-weight: 400 +- **说明文字**: 使用sm字号,颜色为gray-600 +- **代码**: 使用mono字体族 + +### 间距系统 +```css +/* 基于8px网格的间距系统 */ +spacing: { + 0: '0px', + 1: '0.25rem', /* 4px */ + 2: '0.5rem', /* 8px */ + 3: '0.75rem', /* 12px */ + 4: '1rem', /* 16px */ + 5: '1.25rem', /* 20px */ + 6: '1.5rem', /* 24px */ + 8: '2rem', /* 32px */ + 10: '2.5rem', /* 40px */ + 12: '3rem', /* 48px */ + 16: '4rem', /* 64px */ + 20: '5rem', /* 80px */ + 24: '6rem', /* 96px */ +} +``` + +### 圆角系统 +```css +borderRadius: { + none: '0', + sm: '0.125rem', /* 2px */ + DEFAULT: '0.25rem', /* 4px */ + md: '0.375rem', /* 6px */ + lg: '0.5rem', /* 8px */ + xl: '0.75rem', /* 12px */ + '2xl': '1rem', /* 16px */ + '3xl': '1.5rem', /* 24px */ + full: '9999px', +} +``` + +### 阴影系统 +```css +boxShadow: { + xs: '0 1px 2px 0 rgb(0 0 0 / 0.05)', + sm: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + DEFAULT: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + md: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', + lg: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', + xl: '0 25px 50px -12px rgb(0 0 0 / 0.25)', + glow: '0 0 20px rgb(59 130 246 / 0.15)', +} +``` + +## 组件设计规范 + +### 按钮设计 +```typescript +// 按钮变体 +const buttonVariants = { + primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500', + secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500', + outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-primary-500', + ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500', + danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', +}; + +// 按钮尺寸 +const buttonSizes = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-base', + lg: 'px-6 py-3 text-lg', +}; + +// 按钮状态 +const buttonStates = { + default: '', + loading: 'opacity-75 cursor-not-allowed', + disabled: 'opacity-50 cursor-not-allowed', +}; +``` + +### 输入框设计 +```typescript +const inputStyles = { + base: 'block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500', + error: 'border-red-300 text-red-900 placeholder-red-300 focus:border-red-500 focus:ring-red-500', + disabled: 'bg-gray-50 text-gray-500 cursor-not-allowed', +}; + +const inputSizes = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-3 py-2 text-base', + lg: 'px-4 py-3 text-lg', +}; +``` + +### 卡片设计 +```typescript +const cardStyles = { + base: 'bg-white rounded-lg shadow-sm border border-gray-200', + hover: 'hover:shadow-md transition-shadow duration-200', + interactive: 'cursor-pointer hover:shadow-lg transform hover:-translate-y-1 transition-all duration-200', + selected: 'ring-2 ring-primary-500 border-primary-500', +}; +``` + +### 模态框设计 +```typescript +const modalStyles = { + overlay: 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50', + container: 'bg-white rounded-lg shadow-xl max-w-md w-full max-h-screen overflow-y-auto', + header: 'px-6 py-4 border-b border-gray-200', + body: 'px-6 py-4', + footer: 'px-6 py-4 border-t border-gray-200 flex justify-end space-x-3', +}; +``` + +## 布局设计规范 + +### 网格系统 +```css +/* 12列网格系统 */ +.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); } +.col-span-1 { grid-column: span 1 / span 1; } +.col-span-2 { grid-column: span 2 / span 2; } +/* ... */ +.col-span-12 { grid-column: span 12 / span 12; } + +/* 响应式断点 */ +@media (min-width: 640px) { /* sm */ } +@media (min-width: 768px) { /* md */ } +@media (min-width: 1024px) { /* lg */ } +@media (min-width: 1280px) { /* xl */ } +@media (min-width: 1536px) { /* 2xl */ } +``` + +### 页面布局 +```typescript +// 主布局结构 +const layoutStructure = { + header: 'h-16 bg-white border-b border-gray-200', + sidebar: 'w-64 bg-gray-50 border-r border-gray-200', + main: 'flex-1 overflow-auto', + content: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6', +}; + +// 侧边栏导航 +const sidebarStyles = { + nav: 'space-y-1 px-3 py-4', + item: 'flex items-center px-3 py-2 text-sm font-medium rounded-md', + active: 'bg-primary-100 text-primary-700', + inactive: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900', +}; +``` + +### 内容区域 +```typescript +const contentStyles = { + section: 'mb-8', + sectionHeader: 'mb-4', + sectionTitle: 'text-2xl font-bold text-gray-900', + sectionDescription: 'text-gray-600 mt-1', + card: 'bg-white rounded-lg shadow-sm border border-gray-200 p-6', +}; +``` + +## 交互设计规范 + +### 动画系统 +```css +/* 过渡动画 */ +.transition-all { transition: all 0.2s ease-in-out; } +.transition-colors { transition: color, background-color, border-color 0.2s ease-in-out; } +.transition-transform { transition: transform 0.2s ease-in-out; } + +/* 进入动画 */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { transform: translateY(10px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +@keyframes scaleIn { + from { transform: scale(0.95); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +/* 加载动画 */ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} +``` + +### 交互状态 +```typescript +const interactionStates = { + hover: { + button: 'hover:bg-opacity-90 hover:shadow-md', + card: 'hover:shadow-lg hover:-translate-y-1', + link: 'hover:text-primary-600 hover:underline', + }, + focus: { + input: 'focus:ring-2 focus:ring-primary-500 focus:border-primary-500', + button: 'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2', + }, + active: { + button: 'active:transform active:scale-95', + tab: 'bg-primary-100 text-primary-700 border-primary-500', + }, + disabled: { + button: 'opacity-50 cursor-not-allowed', + input: 'bg-gray-50 text-gray-500 cursor-not-allowed', + }, +}; +``` + +### 反馈机制 +```typescript +// 加载状态 +const loadingStates = { + spinner: 'animate-spin h-5 w-5 text-primary-600', + skeleton: 'animate-pulse bg-gray-200 rounded', + progress: 'bg-primary-600 h-2 rounded-full transition-all duration-300', +}; + +// 状态指示 +const statusIndicators = { + success: 'bg-green-100 text-green-800 border border-green-200', + warning: 'bg-yellow-100 text-yellow-800 border border-yellow-200', + error: 'bg-red-100 text-red-800 border border-red-200', + info: 'bg-blue-100 text-blue-800 border border-blue-200', +}; + +// 通知样式 +const notificationStyles = { + container: 'fixed top-4 right-4 z-50 space-y-2', + item: 'bg-white rounded-lg shadow-lg border p-4 max-w-sm', + success: 'border-l-4 border-green-500', + error: 'border-l-4 border-red-500', + warning: 'border-l-4 border-yellow-500', + info: 'border-l-4 border-blue-500', +}; +``` + +## 响应式设计规范 + +### 断点系统 +```typescript +const breakpoints = { + sm: '640px', // 小屏幕手机 + md: '768px', // 平板 + lg: '1024px', // 小型桌面 + xl: '1280px', // 桌面 + '2xl': '1536px', // 大屏桌面 +}; + +// 响应式设计原则 +const responsiveRules = { + mobile: { + // 移动端优先 + padding: 'px-4', + fontSize: 'text-sm', + spacing: 'space-y-4', + }, + tablet: { + // 平板适配 + padding: 'md:px-6', + fontSize: 'md:text-base', + spacing: 'md:space-y-6', + }, + desktop: { + // 桌面适配 + padding: 'lg:px-8', + fontSize: 'lg:text-lg', + spacing: 'lg:space-y-8', + }, +}; +``` + +### 自适应布局 +```typescript +// 网格自适应 +const responsiveGrid = { + mobile: 'grid-cols-1', + tablet: 'md:grid-cols-2', + desktop: 'lg:grid-cols-3 xl:grid-cols-4', +}; + +// 侧边栏响应式 +const responsiveSidebar = { + mobile: 'hidden', // 移动端隐藏 + tablet: 'md:block md:w-64', // 平板显示 + desktop: 'lg:w-72', // 桌面加宽 +}; +``` + +## 无障碍设计规范 + +### 键盘导航 +```typescript +const keyboardNavigation = { + focusable: 'focus:outline-none focus:ring-2 focus:ring-primary-500', + skipLink: 'sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4', + tabIndex: { + interactive: '0', + nonInteractive: '-1', + }, +}; +``` + +### 屏幕阅读器支持 +```typescript +const screenReaderSupport = { + srOnly: 'sr-only', // 仅屏幕阅读器可见 + ariaLabel: 'aria-label', // 无障碍标签 + ariaDescribedBy: 'aria-describedby', // 描述关联 + role: 'role', // 元素角色 +}; +``` + +### 颜色对比度 +```typescript +// 确保足够的颜色对比度 +const contrastRatios = { + normalText: '4.5:1', // 普通文本最小对比度 + largeText: '3:1', // 大文本最小对比度 + uiComponents: '3:1', // UI组件最小对比度 +}; +``` + +## 性能优化规范 + +### 图片优化 +```typescript +const imageOptimization = { + formats: ['webp', 'avif', 'jpg', 'png'], + sizes: { + thumbnail: '150x150', + small: '300x300', + medium: '600x600', + large: '1200x1200', + }, + lazy: 'loading="lazy"', + responsive: 'sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"', +}; +``` + +### 动画性能 +```css +/* 使用transform和opacity进行动画 */ +.animate-optimized { + will-change: transform, opacity; + transform: translateZ(0); /* 启用硬件加速 */ +} + +/* 避免动画布局属性 */ +.avoid-layout-animation { + /* 避免动画这些属性:width, height, padding, margin, top, left */ +} +``` + +### 虚拟化列表 +```typescript +// 大列表虚拟化 +const virtualizationConfig = { + itemHeight: 80, + containerHeight: 600, + overscan: 5, // 预渲染项目数 + threshold: 100, // 启用虚拟化的最小项目数 +}; +``` + +## 设计审查清单 + +### 视觉设计 +- [ ] 是否符合品牌色彩规范 +- [ ] 字体大小和层次是否清晰 +- [ ] 间距是否遵循8px网格系统 +- [ ] 阴影和圆角是否一致 +- [ ] 图标风格是否统一 + +### 交互设计 +- [ ] 交互反馈是否及时明确 +- [ ] 加载状态是否友好 +- [ ] 错误提示是否清晰 +- [ ] 操作流程是否简洁 +- [ ] 快捷键是否合理 + +### 响应式设计 +- [ ] 移动端适配是否完整 +- [ ] 平板端布局是否合理 +- [ ] 桌面端是否充分利用空间 +- [ ] 断点切换是否平滑 +- [ ] 触摸目标是否足够大 + +### 无障碍设计 +- [ ] 颜色对比度是否符合标准 +- [ ] 键盘导航是否完整 +- [ ] 屏幕阅读器支持是否充分 +- [ ] 焦点指示是否清晰 +- [ ] 语义化标签是否正确 + +### 性能优化 +- [ ] 图片是否优化 +- [ ] 动画是否流畅 +- [ ] 大列表是否虚拟化 +- [ ] 资源是否懒加载 +- [ ] 关键渲染路径是否优化