Files
mixvideo-v2/apps/desktop/src/components/ai-classification/WeightEditor.tsx
imeepos 0cfacd0662 feat: 实现模板匹配按顺序匹配功能
新功能:
- 添加AI分类权重字段,支持按权重顺序匹配
- 新增PriorityOrder匹配规则类型
- 实现按权重顺序的素材匹配算法
- 添加权重编辑器UI组件

数据模型扩展:
- AiClassification模型添加weight字段
- SegmentMatchingRule枚举添加PriorityOrder类型
- 扩展相关的请求和响应类型定义

数据库迁移:
- 创建019迁移脚本为ai_classifications表添加weight字段
- 为现有数据设置默认权重值
- 添加权重索引提高查询性能

后端服务实现:
- MaterialMatchingService支持按顺序匹配逻辑
- AiClassificationService添加按权重获取分类方法
- 更新所有相关的构造函数和命令处理

前端UI优化:
- SegmentMatchingRuleEditor支持按顺序匹配配置
- 新增WeightEditor组件用于权重设置
- AI分类设置页面集成权重编辑功能
- 更新TypeScript类型定义

测试验证:
- 添加完整的单元测试套件
- 6个测试用例全部通过
- 验证权重排序和匹配规则逻辑

遵循promptx/tauri-desktop-app-expert开发规范
支持用户自定义分类权重,实现智能按顺序匹配
2025-07-23 20:28:36 +08:00

150 lines
4.5 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.
import React, { useState } from 'react';
import { PencilIcon, CheckIcon, XMarkIcon } from '@heroicons/react/24/outline';
interface WeightEditorProps {
/** 当前权重值 */
weight: number;
/** 权重更新回调 */
onWeightUpdate: (newWeight: number) => Promise<void>;
/** 是否禁用编辑 */
disabled?: boolean;
/** 自定义样式类名 */
className?: string;
}
/**
* 权重编辑器组件
* 遵循前端开发规范的组件设计,提供内联编辑权重的功能
*/
export const WeightEditor: React.FC<WeightEditorProps> = ({
weight,
onWeightUpdate,
disabled = false,
className = '',
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editingWeight, setEditingWeight] = useState(weight);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleStartEdit = () => {
if (disabled) return;
setIsEditing(true);
setEditingWeight(weight);
setError(null);
};
const handleSave = async () => {
if (editingWeight < 0 || editingWeight > 100) {
setError('权重值必须在 0-100 之间');
return;
}
setLoading(true);
setError(null);
try {
await onWeightUpdate(editingWeight);
setIsEditing(false);
} catch (err) {
setError(err instanceof Error ? err.message : '更新权重失败');
} finally {
setLoading(false);
}
};
const handleCancel = () => {
setIsEditing(false);
setEditingWeight(weight);
setError(null);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSave();
} else if (e.key === 'Escape') {
handleCancel();
}
};
if (!isEditing) {
return (
<div className={`flex items-center space-x-2 ${className}`}>
<span className="text-sm font-medium text-gray-700">
: {weight}
</span>
{!disabled && (
<button
onClick={handleStartEdit}
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
title="编辑权重"
>
<PencilIcon className="w-4 h-4" />
</button>
)}
</div>
);
}
return (
<div className={`space-y-2 ${className}`}>
<div className="flex items-center space-x-2">
<div className="flex-1">
<label className="block text-xs font-medium text-gray-700 mb-1">
(0-100)
</label>
<input
type="number"
min="0"
max="100"
value={editingWeight}
onChange={(e) => setEditingWeight(parseInt(e.target.value) || 0)}
onKeyPress={handleKeyPress}
disabled={loading}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50"
placeholder="输入权重值"
autoFocus
/>
</div>
<div className="flex space-x-1 mt-5">
<button
onClick={handleSave}
disabled={loading}
className="inline-flex items-center px-2 py-1 bg-blue-600 text-white text-xs font-medium rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
title="保存权重"
>
<CheckIcon className="w-3 h-3 mr-1" />
{loading ? '保存中...' : '保存'}
</button>
<button
onClick={handleCancel}
disabled={loading}
className="inline-flex items-center px-2 py-1 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>
{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 className="text-xs text-gray-500">
💡
</div>
</div>
);
};