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:
imeepos
2025-07-15 09:43:04 +08:00
parent be8c032158
commit 05d29832b0
9 changed files with 581 additions and 71 deletions

View File

@@ -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>
);
};

View File

@@ -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>