mixvideo-v2/apps/desktop/src/components/template/SegmentMatchingRuleEditor.tsx

301 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
));
}
} else if (ruleType === 'random') {
setEditingRule(SegmentMatchingRuleHelper.createRandomMatch());
} else if (ruleType === 'priority_order') {
// 默认选择所有激活的AI分类
const categoryIds = aiClassifications.map(c => c.id);
setEditingRule(SegmentMatchingRuleHelper.createPriorityOrder(categoryIds));
}
};
const handleAiClassificationChange = (classificationId: string) => {
const classification = aiClassifications.find(c => c.id === classificationId);
if (classification) {
setEditingRule(SegmentMatchingRuleHelper.createAiClassification(
classification.id,
classification.name
));
}
};
const handlePriorityOrderChange = (categoryId: string, isSelected: boolean) => {
if (typeof editingRule === 'object' && 'PriorityOrder' in editingRule) {
const currentCategoryIds = editingRule.PriorityOrder.category_ids;
let newCategoryIds: string[];
if (isSelected) {
// 添加分类ID
newCategoryIds = [...currentCategoryIds, categoryId];
} else {
// 移除分类ID
newCategoryIds = currentCategoryIds.filter(id => id !== categoryId);
}
setEditingRule(SegmentMatchingRuleHelper.createPriorityOrder(newCategoryIds));
}
};
const getCurrentRuleType = (rule: SegmentMatchingRule): string => {
if (SegmentMatchingRuleHelper.isFixedMaterial(rule)) {
return 'fixed';
} else if (SegmentMatchingRuleHelper.isAiClassification(rule)) {
return 'ai_classification';
} else if (SegmentMatchingRuleHelper.isRandomMatch(rule)) {
return 'random';
} else if (SegmentMatchingRuleHelper.isPriorityOrder(rule)) {
return 'priority_order';
}
return 'fixed'; // 默认值
};
const ruleTypeOptions = [
{ value: 'fixed', label: '固定素材' },
{ value: 'ai_classification', label: 'AI分类素材' },
{ value: 'random', label: '随机匹配' },
{ value: 'priority_order', label: '按顺序匹配' },
];
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'
: SegmentMatchingRuleHelper.isAiClassification(currentRule)
? 'bg-blue-100 text-blue-800'
: SegmentMatchingRuleHelper.isRandomMatch(currentRule)
? 'bg-green-100 text-green-800'
: SegmentMatchingRuleHelper.isPriorityOrder(currentRule)
? 'bg-purple-100 text-purple-800'
: 'bg-gray-100 text-gray-800'
}`}>
{SegmentMatchingRuleHelper.getDisplayName(currentRule)}
</span>
</div>
<button
onClick={handleStartEdit}
className="inline-flex items-center px-2 py-1 bg-blue-50 text-blue-600 text-xs font-medium rounded-md hover:bg-blue-100 hover:text-blue-700 transition-all duration-200 border border-blue-200"
title="编辑匹配规则"
>
<PencilIcon className="w-3 h-3 mr-1" />
</button>
</div>
);
}
return (
<div className="space-y-3 bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-blue-800">:</span>
<div className="flex items-center space-x-2">
<button
onClick={handleSaveRule}
disabled={loading}
className="inline-flex items-center px-3 py-1.5 bg-green-600 text-white text-xs font-medium rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow-md"
title="保存匹配规则"
>
<CheckIcon className="w-3 h-3 mr-1" />
{loading ? '保存中...' : '保存'}
</button>
<button
onClick={handleCancelEdit}
disabled={loading}
className="inline-flex items-center px-3 py-1.5 bg-gray-100 text-gray-700 text-xs font-medium rounded-md hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
title="取消编辑"
>
<XMarkIcon className="w-3 h-3 mr-1" />
</button>
</div>
</div>
<div className="space-y-2">
<div>
<label className="block text-xs font-medium text-blue-800 mb-1">
</label>
<CustomSelect
value={getCurrentRuleType(editingRule)}
onChange={handleRuleTypeChange}
options={ruleTypeOptions}
placeholder="选择规则类型"
className="text-xs bg-white border-blue-200"
/>
</div>
{SegmentMatchingRuleHelper.isAiClassification(editingRule) && (
<div>
<label className="block text-xs font-medium text-blue-800 mb-1">
AI分类
</label>
<CustomSelect
value={SegmentMatchingRuleHelper.getAiClassificationInfo(editingRule)?.category_id || ''}
onChange={handleAiClassificationChange}
options={classificationOptions}
placeholder="选择AI分类"
className="text-xs bg-white border-blue-200"
/>
</div>
)}
{SegmentMatchingRuleHelper.isPriorityOrder(editingRule) && (
<div>
<label className="block text-xs font-medium text-blue-800 mb-1">
</label>
<div className="text-xs text-gray-600 mb-2">
AI分类的权重顺序依次尝试匹配素材
</div>
<div className="space-y-1 max-h-32 overflow-y-auto border border-blue-200 rounded-md p-2 bg-white">
{aiClassifications
.sort((a, b) => (b.weight || 0) - (a.weight || 0)) // 按权重降序排列
.map((classification) => {
const currentCategoryIds = typeof editingRule === 'object' && 'PriorityOrder' in editingRule
? editingRule.PriorityOrder.category_ids
: [];
const isSelected = currentCategoryIds.includes(classification.id);
return (
<label key={classification.id} className="flex items-center space-x-2 cursor-pointer hover:bg-blue-50 p-1 rounded">
<input
type="checkbox"
checked={isSelected}
onChange={(e) => handlePriorityOrderChange(classification.id, e.target.checked)}
className="w-3 h-3 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-xs text-gray-700 flex-1">
{classification.name}
</span>
<span className="text-xs text-gray-500 bg-gray-100 px-1 rounded">
: {classification.weight || 0}
</span>
</label>
);
})}
</div>
{typeof editingRule === 'object' && 'PriorityOrder' in editingRule && editingRule.PriorityOrder.category_ids.length === 0 && (
<div className="text-xs text-amber-700 bg-amber-100 border border-amber-200 p-2 rounded-md mt-2">
AI分类
</div>
)}
</div>
)}
</div>
{error && (
<div className="text-xs text-red-700 bg-red-100 border border-red-200 p-2 rounded-md">
{error}
</div>
)}
{loading && (
<div className="text-xs text-blue-700 bg-white border border-blue-300 p-2 rounded-md flex items-center">
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-blue-600 mr-2"></div>
...
</div>
)}
</div>
);
};