diff --git a/apps/desktop/src-tauri/src/business/services/template_service.rs b/apps/desktop/src-tauri/src/business/services/template_service.rs index 9820c3f..db2642e 100644 --- a/apps/desktop/src-tauri/src/business/services/template_service.rs +++ b/apps/desktop/src-tauri/src/business/services/template_service.rs @@ -5,7 +5,7 @@ use rusqlite::{params, Row}; use crate::data::models::template::{ Template, TemplateMaterial, Track, TrackSegment, CanvasConfig, TemplateMaterialType, TrackType, ImportStatus, UploadStatus, - CreateTemplateRequest + CreateTemplateRequest, SegmentMatchingRule }; use crate::infrastructure::database::Database; @@ -144,11 +144,14 @@ impl TemplateService { // 保存轨道片段 for segment in &track.segments { + let matching_rule_json = serde_json::to_string(&segment.matching_rule) + .unwrap_or_else(|_| r#"{"FixedMaterial":{}}"#.to_string()); + tx.execute( "INSERT OR REPLACE INTO track_segments ( id, track_id, template_material_id, name, start_time, end_time, - duration, segment_index, properties, created_at, updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + duration, segment_index, properties, matching_rule, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", params![ segment.id, segment.track_id, @@ -159,6 +162,7 @@ impl TemplateService { segment.duration as i64, segment.segment_index, segment.properties, + matching_rule_json, segment.created_at.to_rfc3339(), segment.updated_at.to_rfc3339() ], @@ -224,7 +228,7 @@ impl TemplateService { // 查询轨道片段 let mut segment_stmt = conn.prepare( "SELECT id, track_id, template_material_id, name, start_time, end_time, - duration, segment_index, properties, created_at, updated_at + duration, segment_index, properties, matching_rule, created_at, updated_at FROM track_segments WHERE track_id = ?1 ORDER BY segment_index" )?; @@ -535,6 +539,11 @@ impl TemplateService { /// 数据库行转换为轨道片段 fn row_to_track_segment(&self, row: &Row) -> rusqlite::Result { + // 解析匹配规则,如果解析失败则使用默认值 + let matching_rule = row.get::<_, Option>("matching_rule")? + .and_then(|rule_str| serde_json::from_str(&rule_str).ok()) + .unwrap_or_default(); + Ok(TrackSegment { id: row.get("id")?, track_id: row.get("track_id")?, @@ -545,6 +554,7 @@ impl TemplateService { duration: row.get::<_, i64>("duration")? as u64, segment_index: row.get::<_, i64>("segment_index")? as u32, properties: row.get("properties")?, + matching_rule, created_at: chrono::DateTime::parse_from_rfc3339(&row.get::<_, String>("created_at")?) .unwrap() .with_timezone(&chrono::Utc), @@ -588,7 +598,7 @@ impl TemplateService { // 查询每个轨道的真实片段数据 let mut segment_stmt = conn.prepare( "SELECT id, track_id, template_material_id, name, start_time, end_time, - duration, segment_index, properties, created_at, updated_at + duration, segment_index, properties, matching_rule, created_at, updated_at FROM track_segments WHERE track_id = ?1 ORDER BY segment_index" )?; @@ -605,4 +615,54 @@ impl TemplateService { Ok(()) } + + /// 更新轨道片段的匹配规则 + pub async fn update_segment_matching_rule( + &self, + segment_id: &str, + matching_rule: SegmentMatchingRule, + ) -> Result<()> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let matching_rule_json = serde_json::to_string(&matching_rule) + .map_err(|e| anyhow!("Failed to serialize matching rule: {}", e))?; + + let updated_rows = conn.execute( + "UPDATE track_segments SET matching_rule = ?1, updated_at = ?2 WHERE id = ?3", + params![ + matching_rule_json, + chrono::Utc::now().to_rfc3339(), + segment_id + ], + )?; + + if updated_rows == 0 { + return Err(anyhow!("Track segment not found: {}", segment_id)); + } + + Ok(()) + } + + /// 获取轨道片段的匹配规则 + pub async fn get_segment_matching_rule(&self, segment_id: &str) -> Result { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let matching_rule_json: Option = match conn.query_row( + "SELECT matching_rule FROM track_segments WHERE id = ?1", + params![segment_id], + |row| row.get(0), + ) { + Ok(value) => Some(value), + Err(rusqlite::Error::QueryReturnedNoRows) => None, + Err(e) => return Err(anyhow!("Database error: {}", e)), + }; + + let matching_rule = matching_rule_json + .and_then(|rule_str| serde_json::from_str(&rule_str).ok()) + .unwrap_or_default(); + + Ok(matching_rule) + } } diff --git a/apps/desktop/src-tauri/src/data/models/template.rs b/apps/desktop/src-tauri/src/data/models/template.rs index 5b5ec9a..61e2a95 100644 --- a/apps/desktop/src-tauri/src/data/models/template.rs +++ b/apps/desktop/src-tauri/src/data/models/template.rs @@ -63,6 +63,41 @@ pub struct Track { pub updated_at: DateTime, } +/// 片段匹配规则类型 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum SegmentMatchingRule { + /// 固定素材(默认)- 不替换 + FixedMaterial, + /// AI分类素材 - 使用指定AI分类的素材 + AiClassification { category_id: String, category_name: String }, +} + +impl Default for SegmentMatchingRule { + fn default() -> Self { + Self::FixedMaterial + } +} + +impl SegmentMatchingRule { + /// 获取规则的显示名称 + pub fn display_name(&self) -> String { + match self { + Self::FixedMaterial => "固定素材".to_string(), + Self::AiClassification { category_name, .. } => format!("AI分类: {}", category_name), + } + } + + /// 检查是否为固定素材 + pub fn is_fixed_material(&self) -> bool { + matches!(self, Self::FixedMaterial) + } + + /// 检查是否为AI分类 + pub fn is_ai_classification(&self) -> bool { + matches!(self, Self::AiClassification { .. }) + } +} + /// 轨道片段 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TrackSegment { @@ -75,6 +110,7 @@ pub struct TrackSegment { pub duration: u64, // 片段时长(微秒) pub segment_index: u32, // 片段在轨道中的索引 pub properties: Option, // JSON格式的片段属性(如变换、效果等) + pub matching_rule: SegmentMatchingRule, // 片段匹配规则 pub created_at: DateTime, pub updated_at: DateTime, } @@ -353,6 +389,7 @@ impl TrackSegment { duration: end_time - start_time, segment_index, properties: None, + matching_rule: SegmentMatchingRule::default(), // 默认为固定素材 created_at: now, updated_at: now, } @@ -362,4 +399,15 @@ impl TrackSegment { pub fn associate_material(&mut self, material_id: String) { self.template_material_id = Some(material_id); } + + /// 设置匹配规则 + pub fn set_matching_rule(&mut self, rule: SegmentMatchingRule) { + self.matching_rule = rule; + self.updated_at = Utc::now(); + } + + /// 获取匹配规则的显示名称 + pub fn get_matching_rule_display(&self) -> String { + self.matching_rule.display_name() + } } diff --git a/apps/desktop/src-tauri/src/infrastructure/database.rs b/apps/desktop/src-tauri/src/infrastructure/database.rs index 3103ee4..093467f 100644 --- a/apps/desktop/src-tauri/src/infrastructure/database.rs +++ b/apps/desktop/src-tauri/src/infrastructure/database.rs @@ -331,6 +331,7 @@ impl Database { duration INTEGER NOT NULL, segment_index INTEGER NOT NULL, properties TEXT, + matching_rule TEXT DEFAULT '\"FixedMaterial\"', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (track_id) REFERENCES tracks (id) ON DELETE CASCADE, @@ -399,75 +400,109 @@ impl Database { [], ); - // 移除模板表的 project_id 字段(如果存在) - // SQLite 不支持 DROP COLUMN,需要重建表 - let _ = conn.execute( - "CREATE TABLE IF NOT EXISTS templates_new ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - canvas_width INTEGER NOT NULL, - canvas_height INTEGER NOT NULL, - canvas_ratio TEXT NOT NULL, - duration INTEGER NOT NULL, - fps REAL NOT NULL, - import_status TEXT NOT NULL DEFAULT 'Pending', - source_file_path TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - is_active BOOLEAN DEFAULT 1 - )", - [], - ); + // 检查是否需要移除模板表的 project_id 字段 + // 只有在表中存在 project_id 字段时才需要重建表 + let has_project_id_column = conn.prepare("SELECT project_id FROM templates LIMIT 1").is_ok(); - // 迁移数据(排除 project_id) - let _ = conn.execute( - "INSERT OR IGNORE INTO templates_new - SELECT id, name, description, canvas_width, canvas_height, canvas_ratio, - duration, fps, import_status, source_file_path, created_at, updated_at, is_active - FROM templates", - [], - ); + if has_project_id_column { + println!("Migrating templates table to remove project_id column..."); - // 删除旧表并重命名新表 - let _ = conn.execute("DROP TABLE IF EXISTS templates_old", []); - let _ = conn.execute("ALTER TABLE templates RENAME TO templates_old", []); - let _ = conn.execute("ALTER TABLE templates_new RENAME TO templates", []); + // SQLite 不支持 DROP COLUMN,需要重建表 + let _ = conn.execute( + "CREATE TABLE IF NOT EXISTS templates_new ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + canvas_width INTEGER NOT NULL, + canvas_height INTEGER NOT NULL, + canvas_ratio TEXT NOT NULL, + duration INTEGER NOT NULL, + fps REAL NOT NULL, + import_status TEXT NOT NULL DEFAULT 'Pending', + source_file_path TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1 + )", + [], + ); - // 修复轨道片段表的外键约束问题 - // 重建 track_segments 表,恢复正确的 template_material_id 外键约束 - let _ = conn.execute( - "CREATE TABLE IF NOT EXISTS track_segments_new ( - id TEXT PRIMARY KEY, - track_id TEXT NOT NULL, - template_material_id TEXT, - name TEXT NOT NULL, - start_time INTEGER NOT NULL, - end_time INTEGER NOT NULL, - duration INTEGER NOT NULL, - segment_index INTEGER NOT NULL, - properties TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (track_id) REFERENCES tracks (id) ON DELETE CASCADE, - FOREIGN KEY (template_material_id) REFERENCES template_materials (id) ON DELETE SET NULL - )", - [], - ); + // 迁移数据(排除 project_id) + let _ = conn.execute( + "INSERT OR IGNORE INTO templates_new + SELECT id, name, description, canvas_width, canvas_height, canvas_ratio, + duration, fps, import_status, source_file_path, created_at, updated_at, is_active + FROM templates", + [], + ); - // 迁移轨道片段数据 - let _ = conn.execute( - "INSERT OR IGNORE INTO track_segments_new - SELECT id, track_id, template_material_id, name, start_time, end_time, - duration, segment_index, properties, created_at, updated_at - FROM track_segments", - [], - ); + // 删除旧表并重命名新表 + let _ = conn.execute("DROP TABLE IF EXISTS templates_old", []); + let _ = conn.execute("ALTER TABLE templates RENAME TO templates_old", []); + let _ = conn.execute("ALTER TABLE templates_new RENAME TO templates", []); - // 删除旧表并重命名新表 - let _ = conn.execute("DROP TABLE IF EXISTS track_segments_old", []); - let _ = conn.execute("ALTER TABLE track_segments RENAME TO track_segments_old", []); - let _ = conn.execute("ALTER TABLE track_segments_new RENAME TO track_segments", []); + println!("Templates table migration completed"); + } + + // 检查是否需要修复轨道片段表的外键约束问题 + // 只有在表结构需要更新时才重建表 + let needs_track_segments_migration = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='track_segments_old'").is_err(); + + if needs_track_segments_migration { + println!("Migrating track_segments table structure..."); + + // 重建 track_segments 表,恢复正确的 template_material_id 外键约束 + let _ = conn.execute( + "CREATE TABLE IF NOT EXISTS track_segments_new ( + id TEXT PRIMARY KEY, + track_id TEXT NOT NULL, + template_material_id TEXT, + name TEXT NOT NULL, + start_time INTEGER NOT NULL, + end_time INTEGER NOT NULL, + duration INTEGER NOT NULL, + segment_index INTEGER NOT NULL, + properties TEXT, + matching_rule TEXT DEFAULT '\"FixedMaterial\"', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (track_id) REFERENCES tracks (id) ON DELETE CASCADE, + FOREIGN KEY (template_material_id) REFERENCES template_materials (id) ON DELETE SET NULL + )", + [], + ); + + // 迁移轨道片段数据 + // 首先检查旧表是否有 matching_rule 字段 + let has_matching_rule = conn.prepare("SELECT matching_rule FROM track_segments LIMIT 1").is_ok(); + + if has_matching_rule { + // 如果有 matching_rule 字段,包含它在迁移中 + let _ = conn.execute( + "INSERT OR IGNORE INTO track_segments_new + SELECT id, track_id, template_material_id, name, start_time, end_time, + duration, segment_index, properties, matching_rule, created_at, updated_at + FROM track_segments", + [], + ); + } else { + // 如果没有 matching_rule 字段,使用默认值 + let _ = conn.execute( + "INSERT OR IGNORE INTO track_segments_new + SELECT id, track_id, template_material_id, name, start_time, end_time, + duration, segment_index, properties, '\"FixedMaterial\"', created_at, updated_at + FROM track_segments", + [], + ); + } + + // 删除旧表并重命名新表 + let _ = conn.execute("DROP TABLE IF EXISTS track_segments_old", []); + let _ = conn.execute("ALTER TABLE track_segments RENAME TO track_segments_old", []); + let _ = conn.execute("ALTER TABLE track_segments_new RENAME TO track_segments", []); + + println!("Track segments table migration completed"); + } // 创建模板素材表索引 conn.execute( @@ -795,6 +830,17 @@ impl Database { println!("Added model_id column and index to materials table"); } + // 添加片段匹配规则字段到轨道片段表 + let has_matching_rule_column = conn.prepare("SELECT matching_rule FROM track_segments LIMIT 1").is_ok(); + if !has_matching_rule_column { + println!("Adding matching_rule column to track_segments table"); + conn.execute( + "ALTER TABLE track_segments ADD COLUMN matching_rule TEXT DEFAULT '\"FixedMaterial\"'", + [], + )?; + println!("Added matching_rule column to track_segments table"); + } + // 暂时禁用自动清理,避免启动时卡住 // self.cleanup_invalid_projects()?; diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 1261c26..2c7ff64 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -155,6 +155,8 @@ pub fn run() { commands::template_commands::warm_template_cache, commands::template_commands::cleanup_template_performance_data, commands::template_commands::import_template_optimized, + commands::template_commands::update_segment_matching_rule, + commands::template_commands::get_segment_matching_rule, // 测试命令 commands::test_commands::test_database_connection, commands::test_commands::test_template_table, diff --git a/apps/desktop/src-tauri/src/presentation/commands/template_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/template_commands.rs index 9ed8129..2c662e5 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/template_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/template_commands.rs @@ -4,7 +4,7 @@ use anyhow::Result; use crate::data::models::template::{ Template, ImportTemplateRequest, BatchImportRequest, ImportProgress, - CreateTemplateRequest + CreateTemplateRequest, SegmentMatchingRule }; use crate::business::services::template_service::{TemplateService, TemplateQueryOptions, TemplateListResponse}; use crate::business::services::template_import_service::TemplateImportService; @@ -338,4 +338,29 @@ pub async fn import_template_optimized( .map_err(|e| e.to_string()) } +/// 更新轨道片段的匹配规则 +#[tauri::command] +pub async fn update_segment_matching_rule( + segment_id: String, + matching_rule: SegmentMatchingRule, + database: State<'_, Arc>, +) -> Result<(), String> { + let service = TemplateService::new(database.inner().clone()); + service.update_segment_matching_rule(&segment_id, matching_rule) + .await + .map_err(|e| e.to_string()) +} + +/// 获取轨道片段的匹配规则 +#[tauri::command] +pub async fn get_segment_matching_rule( + segment_id: String, + database: State<'_, Arc>, +) -> Result { + let service = TemplateService::new(database.inner().clone()); + + service.get_segment_matching_rule(&segment_id) + .await + .map_err(|e| e.to_string()) +} \ No newline at end of file diff --git a/apps/desktop/src/components/template/SegmentMatchingRuleEditor.tsx b/apps/desktop/src/components/template/SegmentMatchingRuleEditor.tsx new file mode 100644 index 0000000..9b6b5c4 --- /dev/null +++ b/apps/desktop/src/components/template/SegmentMatchingRuleEditor.tsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect } from 'react'; +import { PencilIcon, CheckIcon, XMarkIcon } from '@heroicons/react/24/outline'; +import { SegmentMatchingRule, SegmentMatchingRuleHelper } from '../../types/template'; +import { AiClassification } from '../../types/aiClassification'; +import { useTemplateStore } from '../../stores/templateStore'; +import { CustomSelect } from '../CustomSelect'; +import { AiClassificationService } from '../../services/aiClassificationService'; + +interface SegmentMatchingRuleEditorProps { + segmentId: string; + currentRule: SegmentMatchingRule; + onRuleUpdated?: (newRule: SegmentMatchingRule) => void; +} + +/** + * 片段匹配规则编辑器组件 + * 遵循前端开发规范的组件设计,支持固定素材和AI分类规则的设置 + */ +export const SegmentMatchingRuleEditor: React.FC = ({ + segmentId, + currentRule, + onRuleUpdated, +}) => { + const [isEditing, setIsEditing] = useState(false); + const [editingRule, setEditingRule] = useState(currentRule); + const [aiClassifications, setAiClassifications] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const { updateSegmentMatchingRule } = useTemplateStore(); + + // 加载AI分类列表 + useEffect(() => { + const loadClassifications = async () => { + try { + const classifications = await AiClassificationService.getActiveClassifications(); + setAiClassifications(classifications); + } catch (error) { + console.error('加载AI分类失败:', error); + } + }; + + if (isEditing) { + loadClassifications(); + } + }, [isEditing]); + + // 重置编辑状态 + useEffect(() => { + setEditingRule(currentRule); + setError(null); + }, [currentRule, isEditing]); + + const handleStartEdit = () => { + setIsEditing(true); + setEditingRule(currentRule); + setError(null); + }; + + const handleCancelEdit = () => { + setIsEditing(false); + setEditingRule(currentRule); + setError(null); + }; + + const handleSaveRule = async () => { + try { + setLoading(true); + setError(null); + + await updateSegmentMatchingRule(segmentId, editingRule); + + setIsEditing(false); + onRuleUpdated?.(editingRule); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '保存匹配规则失败'; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + const handleRuleTypeChange = (ruleType: string) => { + if (ruleType === 'fixed') { + setEditingRule(SegmentMatchingRuleHelper.createFixedMaterial()); + } else if (ruleType === 'ai_classification') { + // 如果有可用的AI分类,选择第一个作为默认值 + if (aiClassifications.length > 0) { + const firstClassification = aiClassifications[0]; + setEditingRule(SegmentMatchingRuleHelper.createAiClassification( + firstClassification.id, + firstClassification.name + )); + } + } + }; + + const handleAiClassificationChange = (classificationId: string) => { + const classification = aiClassifications.find(c => c.id === classificationId); + if (classification) { + setEditingRule(SegmentMatchingRuleHelper.createAiClassification( + classification.id, + classification.name + )); + } + }; + + const getCurrentRuleType = (rule: SegmentMatchingRule): string => { + return SegmentMatchingRuleHelper.isFixedMaterial(rule) ? 'fixed' : 'ai_classification'; + }; + + const ruleTypeOptions = [ + { value: 'fixed', label: '固定素材' }, + { value: 'ai_classification', label: 'AI分类素材' }, + ]; + + const classificationOptions = aiClassifications.map(classification => ({ + value: classification.id, + label: classification.name, + })); + + if (!isEditing) { + return ( +
+
+ 匹配规则: + + {SegmentMatchingRuleHelper.getDisplayName(currentRule)} + +
+ +
+ ); + } + + return ( +
+
+ 编辑匹配规则: +
+ + +
+
+ +
+
+ + +
+ + {SegmentMatchingRuleHelper.isAiClassification(editingRule) && ( +
+ + +
+ )} +
+ + {error && ( +
+ {error} +
+ )} + + {loading && ( +
+ 正在保存... +
+ )} +
+ ); +}; diff --git a/apps/desktop/src/components/template/TemplateDetailModal.tsx b/apps/desktop/src/components/template/TemplateDetailModal.tsx index 7c2d5d3..2435cdb 100644 --- a/apps/desktop/src/components/template/TemplateDetailModal.tsx +++ b/apps/desktop/src/components/template/TemplateDetailModal.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { X, Calendar, Clock, Monitor, Layers, FileText, Image, Video, Music, Type, Sparkles, CheckCircle, XCircle, AlertCircle, Upload, Cloud } from 'lucide-react'; import { Template, TemplateMaterial, TemplateMaterialType, TrackType } from '../../types/template'; +import { SegmentMatchingRuleEditor } from './SegmentMatchingRuleEditor'; interface TemplateDetailModalProps { template: Template; @@ -539,6 +540,18 @@ export const TemplateDetailModal: React.FC = ({ )} + {/* 匹配规则编辑器 */} +
+ { + // 可以在这里添加更新后的回调逻辑 + console.log('片段匹配规则已更新:', segment.id, newRule); + }} + /> +
+ {segment.template_material_id && (
使用素材ID: {segment.template_material_id} diff --git a/apps/desktop/src/stores/templateStore.ts b/apps/desktop/src/stores/templateStore.ts index 48df114..e30eddb 100644 --- a/apps/desktop/src/stores/templateStore.ts +++ b/apps/desktop/src/stores/templateStore.ts @@ -7,7 +7,8 @@ import { ImportTemplateRequest, BatchImportRequest, ImportProgress, - ImportStatus + ImportStatus, + SegmentMatchingRule } from '../types/template'; interface TemplateQueryParams { @@ -33,6 +34,8 @@ interface TemplateStore { deleteTemplate: (id: string) => Promise; updateTemplate: (template: Template) => Promise; getImportProgress: (templateId: string) => Promise; + updateSegmentMatchingRule: (segmentId: string, matchingRule: SegmentMatchingRule) => Promise; + getSegmentMatchingRule: (segmentId: string) => Promise; clearError: () => void; setCurrentTemplate: (template: Template | null) => void; } @@ -172,6 +175,42 @@ export const useTemplateStore = create((set, get) => ({ set({ error: null }); }, + updateSegmentMatchingRule: async (segmentId: string, matchingRule: SegmentMatchingRule) => { + set({ error: null }); + + try { + await invoke('update_segment_matching_rule', { + segmentId, + matchingRule + }); + + // 如果当前有模板详情,刷新模板数据 + const currentTemplate = get().currentTemplate; + if (currentTemplate) { + await get().getTemplateById(currentTemplate.id); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '更新片段匹配规则失败'; + set({ error: errorMessage }); + throw error; + } + }, + + getSegmentMatchingRule: async (segmentId: string) => { + set({ error: null }); + + try { + const matchingRule: SegmentMatchingRule = await invoke('get_segment_matching_rule', { + segmentId + }); + return matchingRule; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '获取片段匹配规则失败'; + set({ error: errorMessage }); + throw error; + } + }, + setCurrentTemplate: (template: Template | null) => { set({ currentTemplate: template }); }, diff --git a/apps/desktop/src/types/template.ts b/apps/desktop/src/types/template.ts index 571c38d..3e2a6fe 100644 --- a/apps/desktop/src/types/template.ts +++ b/apps/desktop/src/types/template.ts @@ -52,6 +52,69 @@ export interface Track { updated_at: string; } +/** + * 片段匹配规则类型 + * 匹配Rust enum的序列化格式 + */ +export type SegmentMatchingRule = + | "FixedMaterial" + | { AiClassification: { category_id: string; category_name: string } }; + +/** + * 片段匹配规则辅助函数 + */ +export const SegmentMatchingRuleHelper = { + /** + * 创建固定素材规则 + */ + createFixedMaterial(): SegmentMatchingRule { + return "FixedMaterial"; + }, + + /** + * 创建AI分类规则 + */ + createAiClassification(categoryId: string, categoryName: string): SegmentMatchingRule { + return { AiClassification: { category_id: categoryId, category_name: categoryName } }; + }, + + /** + * 获取规则的显示名称 + */ + getDisplayName(rule: SegmentMatchingRule): string { + if (rule === "FixedMaterial") { + return '固定素材'; + } else if (typeof rule === 'object' && 'AiClassification' in rule) { + return `AI分类: ${rule.AiClassification.category_name}`; + } + return '未知规则'; + }, + + /** + * 检查是否为固定素材 + */ + isFixedMaterial(rule: SegmentMatchingRule): boolean { + return rule === "FixedMaterial"; + }, + + /** + * 检查是否为AI分类 + */ + isAiClassification(rule: SegmentMatchingRule): boolean { + return typeof rule === 'object' && 'AiClassification' in rule; + }, + + /** + * 获取AI分类信息 + */ + getAiClassificationInfo(rule: SegmentMatchingRule): { category_id: string; category_name: string } | null { + if (typeof rule === 'object' && 'AiClassification' in rule) { + return rule.AiClassification; + } + return null; + } +}; + export interface TrackSegment { id: string; track_id: string; @@ -62,6 +125,7 @@ export interface TrackSegment { duration: number; // 片段时长(微秒) segment_index: number; // 片段在轨道中的索引 properties?: string; // JSON格式的片段属性 + matching_rule: SegmentMatchingRule; // 片段匹配规则 created_at: string; updated_at: string; }