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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user