feat: 实现模板片段匹配规则功能并修复数据库迁移问题
新功能: - 为TrackSegment添加匹配规则字段,支持固定素材和AI分类两种规则 - 实现SegmentMatchingRuleEditor组件,支持在模板详情页面编辑片段匹配规则 - 添加update_segment_matching_rule和get_segment_matching_rule API接口 - 扩展前端类型定义和服务函数以支持匹配规则操作 修复: - 修复数据库迁移逻辑导致每次重启清空素材和轨道数据的问题 - 为模板表和轨道片段表迁移添加条件检查,只在必要时执行 - 修正matching_rule字段的默认值格式,匹配Rust枚举序列化格式 - 完善轨道片段表重建时的字段迁移逻辑 技术改进: - 数据库schema更新,添加matching_rule列到track_segments表 - 优化数据库迁移性能,避免不必要的表重建操作 - 增强错误处理和日志输出,便于问题排查 文件变更: - 后端: template_service.rs, template.rs, database.rs, template_commands.rs, lib.rs - 前端: SegmentMatchingRuleEditor.tsx, TemplateDetailModal.tsx, templateStore.ts, template.ts
This commit is contained in:
@@ -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<TrackSegment> {
|
||||
// 解析匹配规则,如果解析失败则使用默认值
|
||||
let matching_rule = row.get::<_, Option<String>>("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<SegmentMatchingRule> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
let matching_rule_json: Option<String> = 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,41 @@ pub struct Track {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 片段匹配规则类型
|
||||
#[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<String>, // JSON格式的片段属性(如变换、效果等)
|
||||
pub matching_rule: SegmentMatchingRule, // 片段匹配规则
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Database>>,
|
||||
) -> 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<Database>>,
|
||||
) -> Result<SegmentMatchingRule, String> {
|
||||
let service = TemplateService::new(database.inner().clone());
|
||||
|
||||
service.get_segment_matching_rule(&segment_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -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<SegmentMatchingRuleEditorProps> = ({
|
||||
segmentId,
|
||||
currentRule,
|
||||
onRuleUpdated,
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editingRule, setEditingRule] = useState<SegmentMatchingRule>(currentRule);
|
||||
const [aiClassifications, setAiClassifications] = useState<AiClassification[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs font-medium text-gray-700">匹配规则:</span>
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
SegmentMatchingRuleHelper.isFixedMaterial(currentRule)
|
||||
? 'bg-gray-100 text-gray-800'
|
||||
: 'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{SegmentMatchingRuleHelper.getDisplayName(currentRule)}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleStartEdit}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title="编辑匹配规则"
|
||||
>
|
||||
<PencilIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-700">编辑匹配规则:</span>
|
||||
<div className="flex items-center space-x-1">
|
||||
<button
|
||||
onClick={handleSaveRule}
|
||||
disabled={loading}
|
||||
className="p-1 text-green-600 hover:text-green-800 disabled:opacity-50 transition-colors"
|
||||
title="保存"
|
||||
>
|
||||
<CheckIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
disabled={loading}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50 transition-colors"
|
||||
title="取消"
|
||||
>
|
||||
<XMarkIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
规则类型
|
||||
</label>
|
||||
<CustomSelect
|
||||
value={getCurrentRuleType(editingRule)}
|
||||
onChange={handleRuleTypeChange}
|
||||
options={ruleTypeOptions}
|
||||
placeholder="选择规则类型"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{SegmentMatchingRuleHelper.isAiClassification(editingRule) && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
AI分类
|
||||
</label>
|
||||
<CustomSelect
|
||||
value={SegmentMatchingRuleHelper.getAiClassificationInfo(editingRule)?.category_id || ''}
|
||||
onChange={handleAiClassificationChange}
|
||||
options={classificationOptions}
|
||||
placeholder="选择AI分类"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-xs text-red-600 bg-red-50 p-2 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="text-xs text-blue-600 bg-blue-50 p-2 rounded">
|
||||
正在保存...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<TemplateDetailModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 匹配规则编辑器 */}
|
||||
<div className="bg-yellow-50 p-2 rounded mt-1">
|
||||
<SegmentMatchingRuleEditor
|
||||
segmentId={segment.id}
|
||||
currentRule={segment.matching_rule}
|
||||
onRuleUpdated={(newRule) => {
|
||||
// 可以在这里添加更新后的回调逻辑
|
||||
console.log('片段匹配规则已更新:', segment.id, newRule);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{segment.template_material_id && (
|
||||
<div>
|
||||
使用素材ID: <span className="font-mono text-blue-600">{segment.template_material_id}</span>
|
||||
|
||||
@@ -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<void>;
|
||||
updateTemplate: (template: Template) => Promise<void>;
|
||||
getImportProgress: (templateId: string) => Promise<ImportProgress | null>;
|
||||
updateSegmentMatchingRule: (segmentId: string, matchingRule: SegmentMatchingRule) => Promise<void>;
|
||||
getSegmentMatchingRule: (segmentId: string) => Promise<SegmentMatchingRule>;
|
||||
clearError: () => void;
|
||||
setCurrentTemplate: (template: Template | null) => void;
|
||||
}
|
||||
@@ -172,6 +175,42 @@ export const useTemplateStore = create<TemplateStore>((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 });
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user