feat: 优化模板轨道片段匹配规则编辑体验

新功能:
- 实现匹配规则保存后自动刷新轨道片段数据
- 优化匹配规则编辑界面的视觉设计

 UI/UX 优化:
- 保存按钮从小图标改为明显的绿色按钮,包含文字标签
- 取消按钮也改为带文字的按钮,提升可发现性
- 编辑按钮从灰色图标改为蓝色带边框的按钮
- 编辑状态下添加蓝色背景和边框,突出编辑区域
- 优化标签颜色,在蓝色背景下更加清晰
- 表单控件添加白色背景和蓝色边框

 数据刷新机制:
- TemplateDetailModal 添加 currentTemplate 状态管理
- 实现 refreshTemplateData 函数自动获取最新模板数据
- 匹配规则保存后触发 handleRuleUpdated 回调
- 自动刷新模板列表和详情数据

 用户体验提升:
- 保存按钮更容易被发现和点击
- 编辑状态更加明显,用户不会迷失
- 保存后立即看到最新数据,无需手动刷新
- 加载状态和错误提示更加清晰

 技术改进:
- 按钮样式遵循 promptx/frontend-developer 设计规范
- 响应式设计,支持不同屏幕尺寸
- 完整的状态管理和错误处理
- 优雅的动画过渡效果
This commit is contained in:
imeepos 2025-07-18 10:55:41 +08:00
parent 025237f753
commit 9c3f7341aa
3 changed files with 72 additions and 40 deletions

View File

@ -148,42 +148,45 @@ export const SegmentMatchingRuleEditor: React.FC<SegmentMatchingRuleEditorProps>
</div>
<button
onClick={handleStartEdit}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
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-4 h-4" />
<PencilIcon className="w-3 h-3 mr-1" />
</button>
</div>
);
}
return (
<div className="space-y-3">
<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-gray-700">:</span>
<div className="flex items-center space-x-1">
<span className="text-xs font-medium text-blue-800">:</span>
<div className="flex items-center space-x-2">
<button
onClick={handleSaveRule}
disabled={loading}
className="p-1 text-green-600 hover:text-green-800 disabled:opacity-50 transition-colors"
title="保存"
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-4 h-4" />
<CheckIcon className="w-3 h-3 mr-1" />
{loading ? '保存中...' : '保存'}
</button>
<button
onClick={handleCancelEdit}
disabled={loading}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50 transition-colors"
title="取消"
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-4 h-4" />
<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-gray-700 mb-1">
<label className="block text-xs font-medium text-blue-800 mb-1">
</label>
<CustomSelect
@ -191,13 +194,13 @@ export const SegmentMatchingRuleEditor: React.FC<SegmentMatchingRuleEditorProps>
onChange={handleRuleTypeChange}
options={ruleTypeOptions}
placeholder="选择规则类型"
className="text-xs"
className="text-xs bg-white border-blue-200"
/>
</div>
{SegmentMatchingRuleHelper.isAiClassification(editingRule) && (
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
<label className="block text-xs font-medium text-blue-800 mb-1">
AI分类
</label>
<CustomSelect
@ -205,21 +208,22 @@ export const SegmentMatchingRuleEditor: React.FC<SegmentMatchingRuleEditorProps>
onChange={handleAiClassificationChange}
options={classificationOptions}
placeholder="选择AI分类"
className="text-xs"
className="text-xs bg-white border-blue-200"
/>
</div>
)}
</div>
{error && (
<div className="text-xs text-red-600 bg-red-50 p-2 rounded">
{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-600 bg-blue-50 p-2 rounded">
...
<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>

View File

@ -2,20 +2,46 @@ import React, { useState } from 'react';
import { X, Calendar, Clock, Monitor, Layers, FileText, Image, Video, Music, Type, Sparkles, CheckCircle, XCircle, AlertCircle, Upload, Cloud, ChevronDown, ChevronRight, Info } from 'lucide-react';
import { Template, TemplateMaterial, TemplateMaterialType, TrackType } from '../../types/template';
import { SegmentMatchingRuleEditor } from './SegmentMatchingRuleEditor';
import { useTemplateStore } from '../../stores/templateStore';
interface TemplateDetailModalProps {
template: Template;
onClose: () => void;
onTemplateUpdated?: () => void;
}
export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
template,
onClose,
onTemplateUpdated,
}) => {
const [activeTab, setActiveTab] = useState<'overview' | 'materials' | 'tracks'>('overview');
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
const [expandedMaterials, setExpandedMaterials] = useState<Record<string, boolean>>({});
const [expandedSegments, setExpandedSegments] = useState<Record<string, boolean>>({});
const [currentTemplate, setCurrentTemplate] = useState<Template>(template);
const { getTemplateById } = useTemplateStore();
// 刷新模板数据
const refreshTemplateData = async () => {
try {
const updatedTemplate = await getTemplateById(template.id);
if (updatedTemplate) {
setCurrentTemplate(updatedTemplate);
onTemplateUpdated?.();
}
} catch (error) {
console.error('刷新模板数据失败:', error);
}
};
// 处理匹配规则更新
const handleRuleUpdated = async (segmentId: string, newRule: any) => {
console.log('片段匹配规则已更新:', segmentId, newRule);
// 刷新模板数据以获取最新的轨道片段信息
await refreshTemplateData();
};
// 切换展开状态
const toggleSection = (sectionId: string) => {
@ -237,9 +263,9 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
{/* 模态框头部 */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-gray-200 bg-gradient-to-r from-gray-50 to-white">
<div className="flex-1 min-w-0">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 truncate">{template.name}</h2>
{template.description && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">{template.description}</p>
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 truncate">{currentTemplate.name}</h2>
{currentTemplate.description && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">{currentTemplate.description}</p>
)}
</div>
<button
@ -285,10 +311,10 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<Monitor className="w-6 h-6 text-blue-600" />
</div>
<div className="text-2xl font-bold text-gray-900">
{template.canvas_config.width}×{template.canvas_config.height}
{currentTemplate.canvas_config.width}×{currentTemplate.canvas_config.height}
</div>
<div className="text-sm text-gray-600 mt-1"></div>
<div className="text-xs text-gray-500">{template.canvas_config.ratio}</div>
<div className="text-xs text-gray-500">{currentTemplate.canvas_config.ratio}</div>
</div>
<div className="text-center">
@ -296,10 +322,10 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<Clock className="w-6 h-6 text-green-600" />
</div>
<div className="text-2xl font-bold text-gray-900">
{formatDuration(template.duration)}
{formatDuration(currentTemplate.duration)}
</div>
<div className="text-sm text-gray-600 mt-1"></div>
<div className="text-xs text-gray-500">{template.fps} FPS</div>
<div className="text-xs text-gray-500">{currentTemplate.fps} FPS</div>
</div>
<div className="text-center">
@ -307,7 +333,7 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<FileText className="w-6 h-6 text-purple-600" />
</div>
<div className="text-2xl font-bold text-gray-900">
{template.materials.length}
{currentTemplate.materials.length}
</div>
<div className="text-sm text-gray-600 mt-1"></div>
</div>
@ -317,7 +343,7 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<Layers className="w-6 h-6 text-orange-600" />
</div>
<div className="text-2xl font-bold text-gray-900">
{template.tracks.length}
{currentTemplate.tracks.length}
</div>
<div className="text-sm text-gray-600 mt-1"></div>
</div>
@ -347,14 +373,14 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600">ID</span>
<code className="text-xs bg-gray-100 px-2 py-1 rounded font-mono text-blue-600">
{template.id}
{currentTemplate.id}
</code>
</div>
{template.source_file_path && (
{currentTemplate.source_file_path && (
<div className="flex items-start justify-between py-2">
<span className="text-sm text-gray-600"></span>
<span className="text-xs text-gray-900 text-right max-w-md break-all">
{template.source_file_path}
{currentTemplate.source_file_path}
</span>
</div>
)}
@ -385,11 +411,11 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600"></span>
<span className="text-sm text-gray-900">{formatDate(template.created_at)}</span>
<span className="text-sm text-gray-900">{formatDate(currentTemplate.created_at)}</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600"></span>
<span className="text-sm text-gray-900">{formatDate(template.updated_at)}</span>
<span className="text-sm text-gray-900">{formatDate(currentTemplate.updated_at)}</span>
</div>
</div>
</div>
@ -405,12 +431,12 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
</h3>
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
{template.materials.length}
{currentTemplate.materials.length}
</span>
</div>
<div className="grid gap-4">
{template.materials.map((material) => {
{currentTemplate.materials.map((material) => {
const uploadStatus = getUploadStatusInfo(material);
const fileExistence = getFileExistenceInfo(material);
const isExpanded = expandedMaterials[material.id];
@ -562,12 +588,12 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
</h3>
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
{template.tracks.length}
{currentTemplate.tracks.length}
</span>
</div>
<div className="space-y-4">
{template.tracks.map((track) => (
{currentTemplate.tracks.map((track) => (
<div key={track.id} className="bg-white border border-gray-200 rounded-lg overflow-hidden">
{/* 轨道头部 */}
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
@ -669,9 +695,7 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<SegmentMatchingRuleEditor
segmentId={segment.id}
currentRule={segment.matching_rule}
onRuleUpdated={(newRule) => {
console.log('片段匹配规则已更新:', segment.id, newRule);
}}
onRuleUpdated={(newRule) => handleRuleUpdated(segment.id, newRule)}
/>
</div>

View File

@ -338,6 +338,10 @@ const TemplateManagement: React.FC = () => {
<TemplateDetailModal
template={selectedTemplate}
onClose={() => setSelectedTemplate(null)}
onTemplateUpdated={() => {
// 刷新模板列表
loadTemplates();
}}
/>
)}