新功能: - 添加AI分类权重字段,支持按权重顺序匹配 - 新增PriorityOrder匹配规则类型 - 实现按权重顺序的素材匹配算法 - 添加权重编辑器UI组件 数据模型扩展: - AiClassification模型添加weight字段 - SegmentMatchingRule枚举添加PriorityOrder类型 - 扩展相关的请求和响应类型定义 数据库迁移: - 创建019迁移脚本为ai_classifications表添加weight字段 - 为现有数据设置默认权重值 - 添加权重索引提高查询性能 后端服务实现: - MaterialMatchingService支持按顺序匹配逻辑 - AiClassificationService添加按权重获取分类方法 - 更新所有相关的构造函数和命令处理 前端UI优化: - SegmentMatchingRuleEditor支持按顺序匹配配置 - 新增WeightEditor组件用于权重设置 - AI分类设置页面集成权重编辑功能 - 更新TypeScript类型定义 测试验证: - 添加完整的单元测试套件 - 6个测试用例全部通过 - 验证权重排序和匹配规则逻辑 遵循promptx/tauri-desktop-app-expert开发规范 支持用户自定义分类权重,实现智能按顺序匹配
150 lines
4.5 KiB
TypeScript
150 lines
4.5 KiB
TypeScript
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>
|
||
);
|
||
};
|