Files
mixvideo-v2/apps/desktop/src/components/template/SegmentMatchingRuleEditor.tsx
imeepos 05d29832b0 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
2025-07-15 09:43:04 +08:00

214 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};